File: System\TermInfo.DatabaseFactory.cs
Web Access
Project: src\src\libraries\System.Console\src\System.Console.csproj (System.Console)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Microsoft.Win32.SafeHandles;
 
namespace System;
 
internal static partial class TermInfo
{
    internal sealed class DatabaseFactory
    {
        /// <summary>
        /// The default locations in which to search for terminfo databases.
        /// This is the ordering of well-known locations used by ncurses.
        /// </summary>
        internal static readonly string[] SystemTermInfoLocations = {
            "/etc/terminfo",
            "/lib/terminfo",
            "/usr/share/terminfo",
            "/usr/share/misc/terminfo",
            "/usr/local/share/terminfo"
        };
 
        internal static string? HomeTermInfoLocation
        {
            get
            {
                string? home = PersistedFiles.GetHomeDirectory();
                return home is null ? null : home + "/.terminfo";
            }
        }
 
        internal static string? EnvVarTermInfoLocation
            => Environment.GetEnvironmentVariable("TERMINFO");
 
        /// <summary>Read the database for the current terminal as specified by the "TERM" environment variable.</summary>
        /// <returns>The database, or null if it could not be found.</returns>
        internal static Database? ReadActiveDatabase()
        {
            string? term = Environment.GetEnvironmentVariable("TERM");
            return !string.IsNullOrEmpty(term) ? ReadDatabase(term) : null;
        }
 
        /// <summary>Read the database for the specified terminal.</summary>
        /// <param name="term">The identifier for the terminal.</param>
        /// <returns>The database, or null if it could not be found.</returns>
        internal static Database? ReadDatabase(string term)
        {
            // This follows the same search order as prescribed by ncurses.
            Database? db;
 
            // First try a location specified in the TERMINFO environment variable.
            string? terminfo = EnvVarTermInfoLocation;
            if ((db = ReadDatabase(term, terminfo)) != null)
            {
                return db;
            }
 
            // Then try in the user's home directory.
            terminfo = HomeTermInfoLocation;
            if ((db = ReadDatabase(term, terminfo)) != null)
            {
                return db;
            }
 
            // Then try a set of well-known locations.
            foreach (string terminfoLocation in SystemTermInfoLocations)
            {
                if ((db = ReadDatabase(term, terminfoLocation)) != null)
                {
                    return db;
                }
            }
 
            // Couldn't find one
            return null;
        }
 
        /// <summary>Attempt to open as readonly the specified file path.</summary>
        /// <param name="filePath">The path to the file to open.</param>
        /// <param name="fd">If successful, the opened file descriptor; otherwise, -1.</param>
        /// <returns>true if the file was successfully opened; otherwise, false.</returns>
        private static bool TryOpen(string filePath, [NotNullWhen(true)] out SafeFileHandle? fd)
        {
            fd = Interop.Sys.Open(filePath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC, 0);
            if (fd.IsInvalid)
            {
                // Don't throw in this case, as we'll be polling multiple locations looking for the file.
                fd.Dispose();
                fd = null;
                return false;
            }
 
            return true;
        }
 
        /// <summary>Read the database for the specified terminal from the specified directory.</summary>
        /// <param name="term">The identifier for the terminal.</param>
        /// <param name="directoryPath">The path to the directory containing terminfo database files.</param>
        /// <returns>The database, or null if it could not be found.</returns>
        internal static Database? ReadDatabase(string? term, string? directoryPath)
        {
            if (string.IsNullOrEmpty(term) || string.IsNullOrEmpty(directoryPath))
            {
                return null;
            }
 
            Span<char> stackBuffer = stackalloc char[256];
            SafeFileHandle? fd;
            if (!TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{term[0]}/{term}"), out fd) &&       // /directory/termFirstLetter/term      (Linux)
                !TryOpen(string.Create(null, stackBuffer, $"{directoryPath}/{(int)term[0]:X}/{term}"), out fd))  // /directory/termFirstLetterAsHex/term (Mac)
            {
                return null;
            }
 
            using (fd)
            {
                // Read in all of the terminfo data
                long termInfoLength = RandomAccess.GetLength(fd);
                const int HeaderLength = 12;
                if (termInfoLength <= HeaderLength)
                {
                    throw new InvalidOperationException(SR.IO_TermInfoInvalid);
                }
 
                byte[] data = new byte[(int)termInfoLength];
                long fileOffset = 0;
                do
                {
                    int bytesRead = RandomAccess.Read(fd, new Span<byte>(data, (int)fileOffset, (int)(termInfoLength - fileOffset)), fileOffset);
                    if (bytesRead == 0)
                    {
                        throw new InvalidOperationException(SR.IO_TermInfoInvalid);
                    }
 
                    fileOffset += bytesRead;
                } while (fileOffset < termInfoLength);
 
                // Create the database from the data
                return new Database(term, data);
            }
        }
    }
}