File: PrintfStressMessageFormatter.cs
Web Access
Project: src\src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.Contracts\Microsoft.Diagnostics.DataContractReader.Contracts.csproj (Microsoft.Diagnostics.DataContractReader.Contracts)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Text;
using Microsoft.Diagnostics.DataContractReader;
using Microsoft.Diagnostics.DataContractReader.Contracts;

namespace Microsoft.Diagnostics.DataContractReader;

public sealed class PrintfStressMessageFormatter
{
    public interface ISpecialPointerFormatter
    {
        string FormatMethodTable(TargetPointer pointer);
        string FormatMethodDesc(TargetPointer pointer);
        string FormatVTable(TargetPointer pointer);
        string FormatStackTrace(TargetPointer pointer);
    }

    private record struct PaddingFormat(int Width, char FormatChar, int Precision = 6);

    private readonly Target _target;
    private readonly ISpecialPointerFormatter _pointerFormatter;
    private readonly Dictionary<string, Action<TargetPointer, PaddingFormat, StringBuilder>> _formatActions;
    private readonly Dictionary<string, Action<TargetPointer, PaddingFormat, StringBuilder>> _alternateActions;

    public PrintfStressMessageFormatter(Target target, ISpecialPointerFormatter pointerFormatter)
    {
        _target = target;
        _pointerFormatter = pointerFormatter;
        _formatActions = new()
        {
            { "pM", FormatMethodDesc },
            { "pT", FormatMethodTable },
            { "pV", FormatVTable },
            { "pK", FormatStackTrace },
            { "s", FormatAsciiString },
            { "hs", FormatAsciiString },
            { "S", FormatUtf16String },
            { "ls", FormatUtf16String },
            { "p", FormatPointer },
            { "f", FormatFloatingPoint },
            { "d", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<int>(ptr, 'd', paddingFormat)) },
            { "i", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<int>(ptr, 'd', paddingFormat)) },
            { "u", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'd', paddingFormat)) },
            { "x", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'x', paddingFormat)) },
            { "X", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'X', paddingFormat)) },
            { "lu", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'd', paddingFormat)) },
            { "lld", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<long>(ptr, 'd', paddingFormat)) },
            { "lli", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<long>(ptr, 'd', paddingFormat)) },
            { "llu", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'd', paddingFormat)) },
            { "llx", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'x', paddingFormat)) },
            { "llX", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'X', paddingFormat)) },
            { "zd", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<long>(ptr, 'd', paddingFormat)) },
            { "zi", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<long>(ptr, 'd', paddingFormat)) },
            { "zu", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'd', paddingFormat)) },
            { "zx", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'x', paddingFormat)) },
            { "zX", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'X', paddingFormat)) },
            { "I64u", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'd', paddingFormat)) },
            { "Id", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<long>(ptr, 'd', paddingFormat)) },
            { "Ix", (ptr, paddingFormat, builder) => builder.Append(FormatInteger<ulong>(ptr, 'x', paddingFormat)) },
            { "I64p", FormatPointer },
        };

        _alternateActions = new()
        {
            { "X", FormatHexWithPrefix },
            { "x", FormatHexWithPrefix },
        };
    }

    private void FormatPointer(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        // Default formatting for pointers is to format as "padded to full byte width with 0s in hex".
        // Allow custom printf formatting to override this if desired.
        if (paddingFormat == new PaddingFormat(0, ' '))
        {
            builder.Append(ptr.Value.ToString($"X{_target.PointerSize * 2}"));
            return;
        }
        builder.Append(FormatInteger<ulong>(ptr, 'X', paddingFormat));
    }

    private void FormatMethodDesc(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        builder.Append(_pointerFormatter.FormatMethodDesc(ptr));
    }

    private void FormatMethodTable(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        builder.Append(_pointerFormatter.FormatMethodTable(ptr));
    }

    private void FormatVTable(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        builder.Append(_pointerFormatter.FormatVTable(ptr));
    }

    private void FormatStackTrace(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        builder.Append(_pointerFormatter.FormatStackTrace(ptr));
    }

    private void FormatAsciiString(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        try
        {
            builder.Append(_target.ReadUtf8String(ptr).PadLeft(paddingFormat.Width, paddingFormat.FormatChar));
        }
        catch (VirtualReadException)
        {
            builder.Append($"(#Could not read address of string at 0x{ptr.Value:x}#)");
        }
    }

    private void FormatUtf16String(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        try
        {
            builder.Append(_target.ReadUtf16String(ptr).PadLeft(paddingFormat.Width, paddingFormat.FormatChar));
        }
        catch (VirtualReadException)
        {
            builder.Append($"(#Could not read address of string at 0x{ptr.Value:x}#)");
        }
    }

    private static void FormatHexWithPrefix(TargetPointer ptr, PaddingFormat paddingFormat, StringBuilder builder)
    {
        if (paddingFormat.FormatChar == '0')
        {
            // We need to subtract 2 from the width to account for the "0x" prefix.
            string format = $"x{Math.Max(paddingFormat.Width - 2, 0)}";
            ReadOnlySpan<char> value = ptr.Value.ToString(format);
            if (value.Length > paddingFormat.Width)
            {
                value = value[^paddingFormat.Width..];
            }
            builder.Append($"0x{value}");
        }
        else
        {
            builder.Append($"0x{ptr.Value:x}".PadLeft(paddingFormat.Width, paddingFormat.FormatChar));
        }
    }

    private static string FormatInteger<T>(TargetPointer value, char format, PaddingFormat paddingFormat)
        where T : INumberBase<T>
    {
        if (paddingFormat.FormatChar == '0')
        {
            return T.CreateTruncating(value.Value).ToString($"{format}{paddingFormat.Width}", formatProvider: CultureInfo.InvariantCulture);
        }
        else
        {
            return T.CreateTruncating(value.Value).ToString($"{format}", formatProvider: CultureInfo.InvariantCulture).PadLeft(paddingFormat.Width, paddingFormat.FormatChar);
        }
    }

    private static void FormatFloatingPoint(TargetPointer valueAsBits, PaddingFormat paddingFormat, StringBuilder builder)
    {
        double value = BitConverter.UInt64BitsToDouble(valueAsBits.Value);
        if (paddingFormat.Precision == 0)
        {
            if (paddingFormat.FormatChar == '0')
            {
                builder.Append(value.ToString($"F{paddingFormat.Width}", provider: CultureInfo.InvariantCulture));
            }
            else
            {
                builder.Append(value.ToString("F", provider: CultureInfo.InvariantCulture).PadLeft(paddingFormat.Width, paddingFormat.FormatChar));
            }
        }
        else
        {
            if (paddingFormat.FormatChar == '0')
            {
                // Create a format string of 00000.### where there are Precision #s and Width - Precision - 1 0s.
                string formatString = string.Create(paddingFormat.Width, paddingFormat, (buffer, format) =>
                {
                    buffer.Fill('0');
                    buffer[^paddingFormat.Precision..].Fill('#');
                    buffer[buffer.Length - paddingFormat.Precision - 1] = '.';
                });
                builder.Append(value.ToString(formatString, provider: CultureInfo.InvariantCulture));
            }
            else
            {
                // Create a format string of #####.### where there are Precision #s after the dot, '0' before the dot, and #s until the string is Width long at the start.
                string formatString = string.Create(paddingFormat.Width, paddingFormat, (buffer, format) =>
                {
                    buffer.Fill('#');
                    buffer[buffer.Length - paddingFormat.Precision - 1] = '.';
                    buffer[buffer.Length - paddingFormat.Precision - 2] = '0';
                });
                builder.Append(value.ToString(formatString, provider: CultureInfo.InvariantCulture).PadLeft(paddingFormat.Width, paddingFormat.FormatChar));
            }
        }
    }

    public string GetFormattedMessage(StressMsgData stressMsg)
    {
        Debug.Assert(stressMsg.FormatString != TargetPointer.Null);
        string formatString = _target.ReadUtf8String(stressMsg.FormatString);
        int currentArg = 0;
        int startIndex = 0;
        StringBuilder sb = new();
        while (startIndex < formatString.Length)
        {
            int nextFormatter = formatString.IndexOf('%', startIndex);
            if (nextFormatter == -1)
            {
                sb.Append(formatString.AsSpan()[startIndex..]);
                break;
            }

            sb.Append(formatString.AsSpan()[startIndex..nextFormatter]);

            if (nextFormatter == formatString.Length - 1)
            {
                sb.Append('%');
            }
            else
            {
                startIndex = nextFormatter + 1;
                char operand = formatString[startIndex++];
                if (operand == '%')
                {
                    sb.Append('%');
                    continue;
                }

                var formatActions = _formatActions;

                if (operand == '#')
                {
                    formatActions = _alternateActions;
                    operand = formatString[startIndex++];
                }

                PaddingFormat paddingFormat = new PaddingFormat(0, ' ');

                if (operand == '0')
                {
                    paddingFormat = paddingFormat with { FormatChar = '0' };
                    operand = formatString[startIndex++];
                }

                while (operand is >= '0' and <= '9')
                {
                    paddingFormat = paddingFormat with { Width = paddingFormat.Width * 10 + (operand - '0') };
                    operand = formatString[startIndex++];
                }

                if (operand == '.')
                {
                    paddingFormat = paddingFormat with { Precision = 0 };
                    operand = formatString[startIndex++];
                    while (operand is >= '0' and <= '9')
                    {
                        paddingFormat = paddingFormat with { Precision = paddingFormat.Precision * 10 + (operand - '0') };
                        operand = formatString[startIndex++];
                    }
                }

                string specifier;

                // Check for width specifiers to form the format specifier we'll look up in the table.
                if (operand == 'l')
                {
                    char nextChar = formatString[startIndex++];
                    if (nextChar != 'l')
                    {
                        specifier = "l" + nextChar;
                    }
                    else
                    {
                        specifier = "ll" + formatString[startIndex++];
                    }
                }
                else if (operand == 'z')
                {
                    specifier = "z" + formatString[startIndex++];
                }
                else if (operand == 'p')
                {
                    if (startIndex < formatString.Length
                        && formatString[startIndex] is 'M' or 'T' or 'V' or 'K')
                    {
                        specifier = "p" + formatString[startIndex++];
                    }
                    else
                    {
                        specifier = "p";
                    }
                }
                else if (operand == 'I')
                {
                    if (formatString.Length - startIndex >= 3
                        && formatString.AsSpan()[startIndex..(startIndex + 2)].SequenceEqual("64"))
                    {
                        specifier = "I64" + formatString[startIndex + 2];
                        startIndex += 3;
                    }
                    else
                    {
                        specifier = "I" + formatString[startIndex++];
                    }
                }
                else
                {
                    specifier = operand.ToString();
                }

                if (!formatActions.TryGetValue(specifier, out var action))
                {
                    throw new InvalidOperationException($"Unknown format specifier '{specifier}' in string '{formatString}'");
                }

                action(stressMsg.Args[currentArg++], paddingFormat, sb);
            }
        }

        return sb.ToString();
    }
}