File: System\TermInfo.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.Collections.Generic;
using System.Diagnostics;
using System.Text;
 
namespace System
{
    /// <summary>Provides access to and processing of a terminfo database.</summary>
    internal static partial class TermInfo
    {
        /// <summary>Provides support for evaluating parameterized terminfo database format strings.</summary>
        internal static class ParameterizedStrings
        {
            /// <summary>A cached stack to use to avoid allocating a new stack object for every evaluation.</summary>
            [ThreadStatic]
            private static Stack<FormatParam>? t_cachedStack;
 
            /// <summary>A cached array of arguments to use to avoid allocating a new array object for every evaluation.</summary>
            [ThreadStatic]
            private static FormatParam[]? t_cachedOneElementArgsArray;
 
            /// <summary>A cached array of arguments to use to avoid allocating a new array object for every evaluation.</summary>
            [ThreadStatic]
            private static FormatParam[]? t_cachedTwoElementArgsArray;
 
            /// <summary>Evaluates a terminfo formatting string, using the supplied argument.</summary>
            /// <param name="format">The format string.</param>
            /// <param name="arg">The argument to the format string.</param>
            /// <returns>The formatted string.</returns>
            public static string Evaluate(string format, FormatParam arg)
            {
                FormatParam[] args = t_cachedOneElementArgsArray ??= new FormatParam[1];
 
                args[0] = arg;
 
                return Evaluate(format, args);
            }
 
            /// <summary>Evaluates a terminfo formatting string, using the supplied arguments.</summary>
            /// <param name="format">The format string.</param>
            /// <param name="arg1">The first argument to the format string.</param>
            /// <param name="arg2">The second argument to the format string.</param>
            /// <returns>The formatted string.</returns>
            public static string Evaluate(string format, FormatParam arg1, FormatParam arg2)
            {
                FormatParam[] args = t_cachedTwoElementArgsArray ??= new FormatParam[2];
 
                args[0] = arg1;
                args[1] = arg2;
 
                return Evaluate(format, args);
            }
 
            /// <summary>Evaluates a terminfo formatting string, using the supplied arguments.</summary>
            /// <param name="format">The format string.</param>
            /// <param name="args">The arguments to the format string.</param>
            /// <returns>The formatted string.</returns>
            public static string Evaluate(string format, params FormatParam[] args)
            {
                ArgumentNullException.ThrowIfNull(format);
                ArgumentNullException.ThrowIfNull(args);
 
                // Initialize the stack to use for processing.
                Stack<FormatParam>? stack = t_cachedStack;
                if (stack == null)
                {
                    t_cachedStack = stack = new Stack<FormatParam>();
                }
                else
                {
                    stack.Clear();
                }
 
                // "dynamic" and "static" variables are much less often used (the "dynamic" and "static"
                // terminology appears to just refer to two different collections rather than to any semantic
                // meaning).  As such, we'll only initialize them if we really need them.
                FormatParam[]? dynamicVars = null, staticVars = null;
 
                int pos = 0;
                return EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars);
 
                // EvaluateInternal may throw IndexOutOfRangeException and InvalidOperationException
                // if the format string is malformed or if it's inconsistent with the parameters provided.
            }
 
            /// <summary>Evaluates a terminfo formatting string, using the supplied arguments and processing data structures.</summary>
            /// <param name="format">The format string.</param>
            /// <param name="pos">The position in <paramref name="format"/> to start processing.</param>
            /// <param name="args">The arguments to the format string.</param>
            /// <param name="stack">The stack to use as the format string is evaluated.</param>
            /// <param name="dynamicVars">A lazily-initialized collection of variables.</param>
            /// <param name="staticVars">A lazily-initialized collection of variables.</param>
            /// <returns>
            /// The formatted string; this may be empty if the evaluation didn't yield any output.
            /// The evaluation stack will have a 1 at the top if all processing was completed at invoked level
            /// of recursion, and a 0 at the top if we're still inside of a conditional that requires more processing.
            /// </returns>
            private static string EvaluateInternal(
                string format, ref int pos, FormatParam[] args, Stack<FormatParam> stack,
                ref FormatParam[]? dynamicVars, ref FormatParam[]? staticVars)
            {
                // Create a StringBuilder to store the output of this processing.  We use the format's length as an
                // approximation of an upper-bound for how large the output will be, though with parameter processing,
                // this is just an estimate, sometimes way over, sometimes under.
                var output = new ValueStringBuilder(stackalloc char[256]);
 
                // Format strings support conditionals, including the equivalent of "if ... then ..." and
                // "if ... then ... else ...", as well as "if ... then ... else ... then ..."
                // and so on, where an else clause can not only be evaluated for string output but also
                // as a conditional used to determine whether to evaluate a subsequent then clause.
                // We use recursion to process these subsequent parts, and we track whether we're processing
                // at the same level of the initial if clause (or whether we're nested).
                bool sawIfConditional = false;
 
                // Process each character in the format string, starting from the position passed in.
                for (; pos < format.Length; pos++)
                {
                    // '%' is the escape character for a special sequence to be evaluated.
                    // Anything else just gets pushed to output.
                    if (format[pos] != '%')
                    {
                        output.Append(format[pos]);
                        continue;
                    }
 
                    // We have a special parameter sequence to process.  Now we need
                    // to look at what comes after the '%'.
                    ++pos;
                    switch (format[pos])
                    {
                        // Output appending operations
                        case '%': // Output the escaped '%'
                            output.Append('%');
                            break;
                        case 'c': // Pop the stack and output it as a char
                            output.AppendSpanFormattable((char)stack.Pop().Int32);
                            break;
                        case 's': // Pop the stack and output it as a string
                            output.Append(stack.Pop().String);
                            break;
                        case 'd': // Pop the stack and output it as an integer
                            output.AppendSpanFormattable(stack.Pop().Int32);
                            break;
                        case 'o':
                        case 'X':
                        case 'x':
                        case ':':
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            // printf strings of the format "%[[:]flags][width[.precision]][doxXs]" are allowed
                            // (with a ':' used in front of flags to help differentiate from binary operations, as flags can
                            // include '-' and '+').  While above we've special-cased common usage (e.g. %d, %s),
                            // for more complicated expressions we delegate to printf.
                            int printfEnd = pos;
                            for (; printfEnd < format.Length; printfEnd++) // find the end of the printf format string
                            {
                                char ec = format[printfEnd];
                                if (ec == 'd' || ec == 'o' || ec == 'x' || ec == 'X' || ec == 's')
                                {
                                    break;
                                }
                            }
                            if (printfEnd >= format.Length)
                            {
                                throw new InvalidOperationException(SR.IO_TermInfoInvalid);
                            }
                            string printfFormat = format.Substring(pos - 1, printfEnd - pos + 2); // extract the format string
                            if (printfFormat.Length > 1 && printfFormat[1] == ':')
                            {
                                printfFormat = printfFormat.Remove(1, 1);
                            }
                            output.Append(FormatPrintF(printfFormat, stack.Pop().Object)); // do the printf formatting and append its output
                            break;
 
                        // Stack pushing operations
                        case 'p': // Push the specified parameter (1-based) onto the stack
                            pos++;
                            Debug.Assert(char.IsAsciiDigit(format[pos]));
                            stack.Push(args[format[pos] - '1']);
                            break;
                        case 'l': // Pop a string and push its length
                            stack.Push(stack.Pop().String.Length);
                            break;
                        case '{': // Push integer literal, enclosed between braces
                            pos++;
                            int intLit = 0;
                            while (format[pos] != '}')
                            {
                                Debug.Assert(char.IsAsciiDigit(format[pos]));
                                intLit = (intLit * 10) + (format[pos] - '0');
                                pos++;
                            }
                            stack.Push(intLit);
                            break;
                        case '\'': // Push literal character, enclosed between single quotes
                            stack.Push((int)format[pos + 1]);
                            Debug.Assert(format[pos + 2] == '\'');
                            pos += 2;
                            break;
 
                        // Storing and retrieving "static" and "dynamic" variables
                        case 'P': // Pop a value and store it into either static or dynamic variables based on whether the a-z variable is capitalized
                            pos++;
                            int setIndex;
                            FormatParam[] targetVars = GetDynamicOrStaticVariables(format[pos], ref dynamicVars, ref staticVars, out setIndex);
                            targetVars[setIndex] = stack.Pop();
                            break;
                        case 'g': // Push a static or dynamic variable; which is based on whether the a-z variable is capitalized
                            pos++;
                            int getIndex;
                            FormatParam[] sourceVars = GetDynamicOrStaticVariables(format[pos], ref dynamicVars, ref staticVars, out getIndex);
                            stack.Push(sourceVars[getIndex]);
                            break;
 
                        // Binary operations
                        case '+':
                        case '-':
                        case '*':
                        case '/':
                        case 'm':
                        case '^': // arithmetic
                        case '&':
                        case '|':                                         // bitwise
                        case '=':
                        case '>':
                        case '<':                               // comparison
                        case 'A':
                        case 'O':                                         // logical
                            int second = stack.Pop().Int32; // it's a stack... the second value was pushed last
                            int first = stack.Pop().Int32;
                            char c = format[pos];
                            stack.Push(
                                c == '+' ? (first + second) :
                                c == '-' ? (first - second) :
                                c == '*' ? (first * second) :
                                c == '/' ? (first / second) :
                                c == 'm' ? (first % second) :
                                c == '^' ? (first ^ second) :
                                c == '&' ? (first & second) :
                                c == '|' ? (first | second) :
                                c == '=' ? AsInt(first == second) :
                                c == '>' ? AsInt(first > second) :
                                c == '<' ? AsInt(first < second) :
                                c == 'A' ? AsInt(AsBool(first) && AsBool(second)) :
                                c == 'O' ? AsInt(AsBool(first) || AsBool(second)) :
                                0); // not possible; we just validated above
                            break;
 
                        // Unary operations
                        case '!':
                        case '~':
                            int value = stack.Pop().Int32;
                            stack.Push(
                                format[pos] == '!' ? AsInt(!AsBool(value)) :
                                ~value);
                            break;
 
                        // Some terminfo files appear to have a fairly liberal interpretation of %i. The spec states that %i increments the first two arguments,
                        // but some uses occur when there's only a single argument. To make sure we accommodate these files, we increment the values
                        // of up to (but not requiring) two arguments.
                        case 'i':
                            if (args.Length > 0)
                            {
                                args[0] = 1 + args[0].Int32;
                                if (args.Length > 1)
                                    args[1] = 1 + args[1].Int32;
                            }
                            break;
 
                        // Conditional of the form %? if-part %t then-part %e else-part %;
                        // The "%e else-part" is optional.
                        case '?':
                            sawIfConditional = true;
                            break;
                        case 't':
                            // We hit the end of the if-part and are about to start the then-part.
                            // The if-part left its result on the stack; pop and evaluate.
                            bool conditionalResult = AsBool(stack.Pop().Int32);
 
                            // Regardless of whether it's true, run the then-part to get past it.
                            // If the conditional was true, output the then results.
                            pos++;
                            string thenResult = EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars);
                            if (conditionalResult)
                            {
                                output.Append(thenResult);
                            }
                            Debug.Assert(format[pos] == 'e' || format[pos] == ';');
 
                            // We're past the then; the top of the stack should now be a Boolean
                            // indicating whether this conditional has more to be processed (an else clause).
                            if (!AsBool(stack.Pop().Int32))
                            {
                                // Process the else clause, and if the conditional was false, output the else results.
                                pos++;
                                string elseResult = EvaluateInternal(format, ref pos, args, stack, ref dynamicVars, ref staticVars);
                                if (!conditionalResult)
                                {
                                    output.Append(elseResult);
                                }
 
                                // Now we should be done (any subsequent elseif logic will have been handled in the recursive call).
                                if (!AsBool(stack.Pop().Int32))
                                {
                                    throw new InvalidOperationException(SR.IO_TermInfoInvalid);
                                }
                            }
 
                            // If we're in a nested processing, return to our parent.
                            if (!sawIfConditional)
                            {
                                stack.Push(1);
                                return output.ToString();
                            }
 
                            // Otherwise, we're done processing the conditional in its entirety.
                            sawIfConditional = false;
                            break;
                        case 'e':
                        case ';':
                            // Let our caller know why we're exiting, whether due to the end of the conditional or an else branch.
                            stack.Push(AsInt(format[pos] == ';'));
                            return output.ToString();
 
                        // Anything else is an error
                        default:
                            throw new InvalidOperationException(SR.IO_TermInfoInvalid);
                    }
                }
 
                stack.Push(1);
                return output.ToString();
            }
 
            /// <summary>Converts an Int32 to a Boolean, with 0 meaning false and all non-zero values meaning true.</summary>
            /// <param name="i">The integer value to convert.</param>
            /// <returns>true if the integer was non-zero; otherwise, false.</returns>
            private static bool AsBool(int i) { return i != 0; }
 
            /// <summary>Converts a Boolean to an Int32, with true meaning 1 and false meaning 0.</summary>
            /// <param name="b">The Boolean value to convert.</param>
            /// <returns>1 if the Boolean is true; otherwise, 0.</returns>
            private static int AsInt(bool b) { return b ? 1 : 0; }
 
            /// <summary>Formats an argument into a printf-style format string.</summary>
            /// <param name="format">The printf-style format string.</param>
            /// <param name="arg">The argument to format.  This must be an Int32 or a String.</param>
            /// <returns>The formatted string.</returns>
            private static unsafe string FormatPrintF(string format, object arg)
            {
                Debug.Assert(arg is string || arg is int);
 
                // Determine how much space is needed to store the formatted string.
                string? stringArg = arg as string;
                int neededLength = stringArg != null ?
                    Interop.Sys.SNPrintF(null, 0, format, stringArg) :
                    Interop.Sys.SNPrintF(null, 0, format, (int)arg);
                if (neededLength == 0)
                {
                    return string.Empty;
                }
                if (neededLength < 0)
                {
                    throw new InvalidOperationException(SR.InvalidOperation_PrintF);
                }
 
                // Allocate the needed space, format into it, and return the data as a string.
                byte[] bytes = new byte[neededLength + 1]; // extra byte for the null terminator
                fixed (byte* ptr = &bytes[0])
                {
                    int length = stringArg != null ?
                        Interop.Sys.SNPrintF(ptr, bytes.Length, format, stringArg) :
                        Interop.Sys.SNPrintF(ptr, bytes.Length, format, (int)arg);
                    if (length != neededLength)
                    {
                        throw new InvalidOperationException(SR.InvalidOperation_PrintF);
                    }
                }
                return Encoding.ASCII.GetString(bytes, 0, neededLength);
            }
 
            /// <summary>Gets the lazily-initialized dynamic or static variables collection, based on the supplied variable name.</summary>
            /// <param name="c">The name of the variable.</param>
            /// <param name="dynamicVars">The lazily-initialized dynamic variables collection.</param>
            /// <param name="staticVars">The lazily-initialized static variables collection.</param>
            /// <param name="index">The index to use to index into the variables.</param>
            /// <returns>The variables collection.</returns>
            private static FormatParam[] GetDynamicOrStaticVariables(
                char c, ref FormatParam[]? dynamicVars, ref FormatParam[]? staticVars, out int index)
            {
                if (char.IsAsciiLetterUpper(c))
                {
                    index = c - 'A';
                    return staticVars ??= new FormatParam[26]; // one slot for each letter of alphabet
                }
                else if (char.IsAsciiLetterLower(c))
                {
                    index = c - 'a';
                    return dynamicVars ??= new FormatParam[26]; // one slot for each letter of alphabet
                }
                else throw new InvalidOperationException(SR.IO_TermInfoInvalid);
            }
 
            /// <summary>
            /// Represents a parameter to a terminfo formatting string.
            /// It is a discriminated union of either an integer or a string,
            /// with characters represented as integers.
            /// </summary>
            public readonly struct FormatParam
            {
                /// <summary>The integer stored in the parameter.</summary>
                private readonly int _int32;
                /// <summary>The string stored in the parameter.</summary>
                private readonly string? _string; // null means an Int32 is stored
 
                /// <summary>Initializes the parameter with an integer value.</summary>
                /// <param name="value">The value to be stored in the parameter.</param>
                public FormatParam(int value) : this(value, null) { }
 
                /// <summary>Initializes the parameter with a string value.</summary>
                /// <param name="value">The value to be stored in the parameter.</param>
                public FormatParam(string? value) : this(0, value ?? string.Empty) { }
 
                /// <summary>Initializes the parameter.</summary>
                /// <param name="intValue">The integer value.</param>
                /// <param name="stringValue">The string value.</param>
                private FormatParam(int intValue, string? stringValue)
                {
                    _int32 = intValue;
                    _string = stringValue;
                }
 
                /// <summary>Implicit converts an integer into a parameter.</summary>
                public static implicit operator FormatParam(int value)
                {
                    return new FormatParam(value);
                }
 
                /// <summary>Implicit converts a string into a parameter.</summary>
                public static implicit operator FormatParam(string? value)
                {
                    return new FormatParam(value);
                }
 
                /// <summary>Gets the integer value of the parameter. If a string was stored, 0 is returned.</summary>
                public int Int32 { get { return _int32; } }
 
                /// <summary>Gets the string value of the parameter.  If an Int32 or a null String were stored, an empty string is returned.</summary>
                public string String { get { return _string ?? string.Empty; } }
 
                /// <summary>Gets the string or the integer value as an object.</summary>
                public object Object { get { return _string ?? (object)_int32; } }
            }
        }
    }
}