File: src\runtime\src\libraries\System.Private.CoreLib\src\System\Diagnostics\Tracing\ManifestBuilder.cs
Web Access
Project: src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib)
// 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.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using System.Resources;
using System.Text;

namespace System.Diagnostics.Tracing;

/// <summary>
/// ManifestBuilder is designed to isolate the details of the message of the event from the
/// rest of EventSource.  This one happens to create XML.
/// </summary>
internal sealed class ManifestBuilder
{
    /// <summary>
    /// Build a manifest for 'providerName' with the given GUID, which will be packaged into 'dllName'.
    /// 'resources, is a resource manager.  If specified all messages are localized using that manager.
    /// </summary>
    public ManifestBuilder(string providerName, Guid providerGuid, string? dllName, ResourceManager? resources,
                            EventManifestOptions flags) : this(resources, flags)
    {
        this.providerName = providerName;

        sb = new StringBuilder();
        events = new StringBuilder();
        templates = new StringBuilder();
        sb.AppendLine("<instrumentationManifest xmlns=\"http://schemas.microsoft.com/win/2004/08/events\">");
        sb.AppendLine(" <instrumentation xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:win=\"http://manifests.microsoft.com/win/2004/08/windows/events\">");
        sb.AppendLine("  <events xmlns=\"http://schemas.microsoft.com/win/2004/08/events\">");
        sb.Append($"<provider name=\"{providerName}\" guid=\"{{{providerGuid}}}\"");
        if (dllName != null)
            sb.Append($" resourceFileName=\"{dllName}\" messageFileName=\"{dllName}\"");

        sb.Append(" symbol=\"");
        int pos = sb.Length;
        sb.Append(providerName); // Period and dash are illegal; replace them.
        sb.Replace('.', '_', pos, sb.Length - pos).Replace("-", "", pos, sb.Length - pos);
        sb.AppendLine("\">");
    }

    /// <summary>
    /// <term>Will NOT build a manifest!</term> If the intention is to build a manifest don't use this constructor.
    ///'resources, is a resource manager.  If specified all messages are localized using that manager.
    /// </summary>
    internal ManifestBuilder(ResourceManager? resources, EventManifestOptions flags)
    {
        providerName = "";

        this.flags = flags;

        this.resources = resources;
        sb = null;
        events = null;
        templates = null;
        opcodeTab = new Dictionary<int, string>();
        stringTab = new Dictionary<string, string>();
        errors = new List<string>();
        perEventByteArrayArgIndices = new Dictionary<string, List<int>>();
    }

    public void AddOpcode(string name, int value)
    {
        if ((flags & EventManifestOptions.Strict) != 0)
        {
            if (value <= 10 || value >= 239)
            {
                ManifestError(SR.Format(SR.EventSource_IllegalOpcodeValue, name, value));
            }

            if (opcodeTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
            {
                ManifestError(SR.Format(SR.EventSource_OpcodeCollision, name, prevName, value));
            }
        }

        opcodeTab[value] = name;
    }

    public void AddTask(string name, int value)
    {
        if ((flags & EventManifestOptions.Strict) != 0)
        {
            if (value <= 0 || value >= 65535)
            {
                ManifestError(SR.Format(SR.EventSource_IllegalTaskValue, name, value));
            }

            if (taskTab != null && taskTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
            {
                ManifestError(SR.Format(SR.EventSource_TaskCollision, name, prevName, value));
            }
        }

        taskTab ??= new Dictionary<int, string>();
        taskTab[value] = name;
    }

    public void AddKeyword(string name, ulong value)
    {
        if ((value & (value - 1)) != 0) // Must be zero or a power of 2
        {
            ManifestError(SR.Format(SR.EventSource_KeywordNeedPowerOfTwo, $"0x{value:x}", name), true);
        }
        if ((flags & EventManifestOptions.Strict) != 0)
        {
            if (value >= 0x0000100000000000UL && !name.StartsWith("Session", StringComparison.Ordinal))
            {
                ManifestError(SR.Format(SR.EventSource_IllegalKeywordsValue, name, $"0x{value:x}"));
            }

            if (keywordTab != null && keywordTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
            {
                ManifestError(SR.Format(SR.EventSource_KeywordCollision, name, prevName, $"0x{value:x}"));
            }
        }

        keywordTab ??= new Dictionary<ulong, string>();
        keywordTab[value] = name;
    }

    /// <summary>
    /// Add a channel.  channelAttribute can be null
    /// </summary>
    public void AddChannel(string? name, int value, EventChannelAttribute? channelAttribute)
    {
        EventChannel chValue = (EventChannel)value;
        if (value < (int)EventChannel.Admin || value > 255)
            ManifestError(SR.Format(SR.EventSource_EventChannelOutOfRange, name, value));
        else if (chValue >= EventChannel.Admin && chValue <= EventChannel.Debug &&
                    channelAttribute != null && EventChannelToChannelType(chValue) != channelAttribute.EventChannelType)
        {
            // we want to ensure developers do not define EventChannels that conflict with the builtin ones,
            // but we want to allow them to override the default ones...
            ManifestError(SR.Format(SR.EventSource_ChannelTypeDoesNotMatchEventChannelValue,
                                                                        name, ((EventChannel)value).ToString()));
        }

        // TODO: validate there are no conflicting manifest exposed names (generally following the format "provider/type")

        ulong kwd = GetChannelKeyword(chValue);

        channelTab ??= new Dictionary<int, ChannelInfo>(4);
        channelTab[value] = new ChannelInfo { Name = name, Keywords = kwd, Attribs = channelAttribute };
    }

    private static EventChannelType EventChannelToChannelType(EventChannel channel)
    {
        Debug.Assert(channel >= EventChannel.Admin && channel <= EventChannel.Debug);
        return (EventChannelType)((int)channel - (int)EventChannel.Admin + (int)EventChannelType.Admin);
    }

    private static EventChannelAttribute GetDefaultChannelAttribute(EventChannel channel)
    {
        EventChannelAttribute attrib = new EventChannelAttribute();
        attrib.EventChannelType = EventChannelToChannelType(channel);
        if (attrib.EventChannelType <= EventChannelType.Operational)
            attrib.Enabled = true;
        return attrib;
    }

    public ulong[] GetChannelData()
    {
        if (this.channelTab == null)
        {
            return [];
        }

        // We create an array indexed by the channel id for fast look up.
        // E.g. channelMask[Admin] will give you the bit mask for Admin channel.
        int maxkey = -1;
        foreach (int item in this.channelTab.Keys)
        {
            if (item > maxkey)
            {
                maxkey = item;
            }
        }

        ulong[] channelMask = new ulong[maxkey + 1];
        foreach (KeyValuePair<int, ChannelInfo> item in this.channelTab)
        {
            channelMask[item.Key] = item.Value.Keywords;
        }

        return channelMask;
    }

    public void StartEvent(string eventName, EventAttribute eventAttribute)
    {
        Debug.Assert(numParams == 0);
        Debug.Assert(this.eventName == null);
        this.eventName = eventName;
        numParams = 0;
        byteArrArgIndices = null;

        events?.Append("  <event value=\"").Append(eventAttribute.EventId).
            Append("\" version=\"").Append(eventAttribute.Version).
            Append("\" level=\"");
        AppendLevelName(events, eventAttribute.Level);
        events?.Append("\" symbol=\"").Append(eventName).Append('"');

        // at this point we add to the manifest's stringTab a message that is as-of-yet
        // "untranslated to manifest convention", b/c we don't have the number or position
        // of any byte[] args (which require string format index updates)
        WriteMessageAttrib(events, "event", eventName, eventAttribute.Message);

        if (eventAttribute.Keywords != 0)
        {
            events?.Append(" keywords=\"");
            AppendKeywords(events, (ulong)eventAttribute.Keywords, eventName);
            events?.Append('"');
        }

        if (eventAttribute.Opcode != 0)
        {
            string? str = GetOpcodeName(eventAttribute.Opcode, eventName);
            events?.Append(" opcode=\"").Append(str).Append('"');
        }

        if (eventAttribute.Task != 0)
        {
            string? str = GetTaskName(eventAttribute.Task, eventName);
            events?.Append(" task=\"").Append(str).Append('"');
        }

        if (eventAttribute.Channel != 0)
        {
            string? str = GetChannelName(eventAttribute.Channel, eventName, eventAttribute.Message);
            events?.Append(" channel=\"").Append(str).Append('"');
        }
    }

    public void AddEventParameter(Type type, string name)
    {
        if (numParams == 0)
            templates?.Append("  <template tid=\"").Append(eventName).AppendLine("Args\">");
        if (type == typeof(byte[]))
        {
            // mark this index as "extraneous" (it has no parallel in the managed signature)
            // we use these values in TranslateToManifestConvention()
            byteArrArgIndices ??= new List<int>(4);
            byteArrArgIndices.Add(numParams);

            // add an extra field to the template representing the length of the binary blob
            numParams++;
            templates?.Append("   <data name=\"").Append(name).AppendLine("Size\" inType=\"win:UInt32\"/>");
        }
        numParams++;
        templates?.Append("   <data name=\"").Append(name).Append("\" inType=\"").Append(GetTypeName(type)).Append('"');
        // TODO: for 'byte*' types it assumes the user provided length is named using the same naming convention
        //       as for 'byte[]' args (blob_arg_name + "Size")
        if ((type.IsArray || type.IsPointer) && type.GetElementType() == typeof(byte))
        {
            // add "length" attribute to the "blob" field in the template (referencing the field added above)
            templates?.Append(" length=\"").Append(name).Append("Size\"");
        }
        // ETW does not support 64-bit value maps, so we don't specify these as ETW maps
        if (type.IsEnum && Enum.GetUnderlyingType(type) != typeof(ulong) && Enum.GetUnderlyingType(type) != typeof(long))
        {
            templates?.Append(" map=\"").Append(type.Name).Append('"');
            mapsTab ??= new Dictionary<string, Type>();
            mapsTab.TryAdd(type.Name, type);        // Remember that we need to dump the type enumeration
        }

        templates?.AppendLine("/>");
    }
    public void EndEvent()
    {
        Debug.Assert(eventName != null);

        if (numParams > 0)
        {
            templates?.AppendLine("  </template>");
            events?.Append(" template=\"").Append(eventName).Append("Args\"");
        }
        events?.AppendLine("/>");

        if (byteArrArgIndices != null)
            perEventByteArrayArgIndices[eventName] = byteArrArgIndices;

        // at this point we have all the information we need to translate the C# Message
        // to the manifest string we'll put in the stringTab
        string prefixedEventName = "event_" + eventName;
        if (stringTab.TryGetValue(prefixedEventName, out string? msg))
        {
            msg = TranslateToManifestConvention(msg, eventName);
            stringTab[prefixedEventName] = msg;
        }

        eventName = null;
        numParams = 0;
        byteArrArgIndices = null;
    }

    // Channel keywords are generated one per channel to allow channel based filtering in event viewer. These keywords are autogenerated
    // by mc.exe for compiling a manifest and are based on the order of the channels (fields) in the Channels inner class (when advanced
    // channel support is enabled), or based on the order the predefined channels appear in the EventAttribute properties (for simple
    // support). The manifest generated *MUST* have the channels specified in the same order (that's how our computed keywords are mapped
    // to channels by the OS infrastructure).
    // If channelKeyworkds is present, and has keywords bits in the ValidPredefinedChannelKeywords then it is
    // assumed that the keyword for that channel should be that bit.
    // otherwise we allocate a channel bit for the channel.
    // explicit channel bits are only used by WCF to mimic an existing manifest,
    // so we don't dont do error checking.
    public ulong GetChannelKeyword(EventChannel channel, ulong channelKeyword = 0)
    {
        // strip off any non-channel keywords, since we are only interested in channels here.
        channelKeyword &= ValidPredefinedChannelKeywords;
        channelTab ??= new Dictionary<int, ChannelInfo>(4);

        if (channelTab.Count == MaxCountChannels)
            ManifestError(SR.EventSource_MaxChannelExceeded);

        if (!channelTab.TryGetValue((int)channel, out ChannelInfo? info))
        {
            // If we were not given an explicit channel, allocate one.
            if (channelKeyword == 0)
            {
                channelKeyword = nextChannelKeywordBit;
                nextChannelKeywordBit >>= 1;
            }
        }
        else
        {
            channelKeyword = info.Keywords;
        }

        return channelKeyword;
    }

    public byte[] CreateManifest()
    {
        string str = CreateManifestString();
        return (str != "") ? Encoding.UTF8.GetBytes(str) : [];
    }

    public IList<string> Errors => errors;

    public bool HasResources => resources != null;

    /// <summary>
    /// When validating an event source it adds the error to the error collection.
    /// When not validating it throws an exception if runtimeCritical is "true".
    /// Otherwise the error is ignored.
    /// </summary>
    /// <param name="msg"></param>
    /// <param name="runtimeCritical"></param>
    public void ManifestError(string msg, bool runtimeCritical = false)
    {
        if ((flags & EventManifestOptions.Strict) != 0)
            errors.Add(msg);
        else if (runtimeCritical)
            throw new ArgumentException(msg);
    }

    private unsafe string CreateManifestString()
    {
        Span<char> ulongHexScratch = stackalloc char[16]; // long enough for ulong.MaxValue formatted as hex

        // Write out the channels
        if (channelTab != null)
        {
            sb?.AppendLine(" <channels>");
            var sortedChannels = new List<KeyValuePair<int, ChannelInfo>>();
            foreach (KeyValuePair<int, ChannelInfo> p in channelTab) { sortedChannels.Add(p); }
            sortedChannels.Sort((p1, p2) => -Comparer<ulong>.Default.Compare(p1.Value.Keywords, p2.Value.Keywords));

            foreach (KeyValuePair<int, ChannelInfo> kvpair in sortedChannels)
            {
                int channel = kvpair.Key;
                ChannelInfo channelInfo = kvpair.Value;

                string? channelType = null;
                bool enabled = false;
                string? fullName = null;

                if (channelInfo.Attribs != null)
                {
                    EventChannelAttribute attribs = channelInfo.Attribs;
                    if (Enum.IsDefined(attribs.EventChannelType))
                        channelType = attribs.EventChannelType.ToString();
                    enabled = attribs.Enabled;
                }

                fullName ??= providerName + "/" + channelInfo.Name;

                sb?.Append("  <channel chid=\"").Append(channelInfo.Name).Append("\" name=\"").Append(fullName).Append('"');
                Debug.Assert(channelInfo.Name != null);
                WriteMessageAttrib(sb, "channel", channelInfo.Name, null);
                sb?.Append(" value=\"").Append(channel).Append('"');
                if (channelType != null)
                    sb?.Append(" type=\"").Append(channelType).Append('"');
                sb?.Append(" enabled=\"").Append(enabled ? "true" : "false").AppendLine("\"/>");
            }
            sb?.AppendLine(" </channels>");
        }

        // Write out the tasks
        if (taskTab != null)
        {
            sb?.AppendLine(" <tasks>");
            var sortedTasks = new List<int>(taskTab.Keys);
            sortedTasks.Sort();

            foreach (int task in sortedTasks)
            {
                sb?.Append("  <task");
                WriteNameAndMessageAttribs(sb, "task", taskTab[task]);
                sb?.Append(" value=\"").Append(task).AppendLine("\"/>");
            }
            sb?.AppendLine(" </tasks>");
        }

        // Write out the maps

        // Scoping the call to enum GetFields to a local function to limit the trimming suppressions
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
        Justification = "Trimmer does not trim enums")]
        static FieldInfo[] GetEnumFields(Type localEnumType)
        {
            Debug.Assert(localEnumType.IsEnum);
            return localEnumType.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
        }

        if (mapsTab != null)
        {
            sb?.AppendLine(" <maps>");
            foreach (Type enumType in mapsTab.Values)
            {
                bool isbitmap = EventSource.IsCustomAttributeDefinedHelper(enumType, typeof(FlagsAttribute), flags);
                string mapKind = isbitmap ? "bitMap" : "valueMap";
                sb?.Append("  <").Append(mapKind).Append(" name=\"").Append(enumType.Name).AppendLine("\">");

                // write out each enum value
                FieldInfo[] staticFields = GetEnumFields(enumType);
                bool anyValuesWritten = false;
                foreach (FieldInfo staticField in staticFields)
                {
                    object? constantValObj = staticField.GetRawConstantValue();

                    if (constantValObj != null)
                    {
                        ulong hexValue;
                        if (constantValObj is ulong)
                            hexValue = (ulong)constantValObj;    // This is the only integer type that can't be represented by a long.
                        else
                            hexValue = (ulong)Convert.ToInt64(constantValObj); // Handles all integer types except ulong.

                        // ETW requires all bitmap values to be powers of 2.  Skip the ones that are not.
                        // TODO: Warn people about the dropping of values.
                        if (isbitmap && !BitOperations.IsPow2(hexValue))
                            continue;

                        hexValue.TryFormat(ulongHexScratch, out int charsWritten, "x");
                        ReadOnlySpan<char> hexValueFormatted = ulongHexScratch.Slice(0, charsWritten);

                        sb?.Append("   <map value=\"0x").Append(hexValueFormatted).Append('"');
                        WriteMessageAttrib(sb, "map", enumType.Name + "." + staticField.Name, staticField.Name);
                        sb?.AppendLine("/>");
                        anyValuesWritten = true;
                    }
                }

                // the OS requires that bitmaps and valuemaps have at least one value or it reject the whole manifest.
                // To avoid that put a 'None' entry if there are no other values.
                if (!anyValuesWritten)
                {
                    sb?.Append("   <map value=\"0x0\"");
                    WriteMessageAttrib(sb, "map", enumType.Name + ".None", "None");
                    sb?.AppendLine("/>");
                }
                sb?.Append("  </").Append(mapKind).AppendLine(">");
            }
            sb?.AppendLine(" </maps>");
        }

        // Write out the opcodes
        sb?.AppendLine(" <opcodes>");
        var sortedOpcodes = new List<int>(opcodeTab.Keys);
        sortedOpcodes.Sort();

        foreach (int opcode in sortedOpcodes)
        {
            sb?.Append("  <opcode");
            WriteNameAndMessageAttribs(sb, "opcode", opcodeTab[opcode]);
            sb?.Append(" value=\"").Append(opcode).AppendLine("\"/>");
        }
        sb?.AppendLine(" </opcodes>");

        // Write out the keywords
        if (keywordTab != null)
        {
            sb?.AppendLine(" <keywords>");
            var sortedKeywords = new List<ulong>(keywordTab.Keys);
            sortedKeywords.Sort();

            foreach (ulong keyword in sortedKeywords)
            {
                sb?.Append("  <keyword");
                WriteNameAndMessageAttribs(sb, "keyword", keywordTab[keyword]);
                keyword.TryFormat(ulongHexScratch, out int charsWritten, "x");
                ReadOnlySpan<char> keywordFormatted = ulongHexScratch.Slice(0, charsWritten);
                sb?.Append(" mask=\"0x").Append(keywordFormatted).AppendLine("\"/>");
            }
            sb?.AppendLine(" </keywords>");
        }

        sb?.AppendLine(" <events>");
        sb?.Append(events);
        sb?.AppendLine(" </events>");

        sb?.AppendLine(" <templates>");
        if (templates?.Length > 0)
        {
            sb?.Append(templates);
        }
        else
        {
            // Work around a corner-case ETW issue where a manifest with no templates causes
            // ETW events to not get sent to their associated channel.
            sb?.AppendLine("    <template tid=\"_empty\"></template>");
        }
        sb?.AppendLine(" </templates>");

        sb?.AppendLine("</provider>");
        sb?.AppendLine("</events>");
        sb?.AppendLine("</instrumentation>");

        // Output the localization information.
        sb?.AppendLine("<localization>");

        var sortedStrings = new string[stringTab.Keys.Count];
        stringTab.Keys.CopyTo(sortedStrings, 0);
        Array.Sort(sortedStrings, StringComparer.Ordinal);

        CultureInfo ci = CultureInfo.CurrentUICulture;
        sb?.Append(" <resources culture=\"").Append(ci.Name).AppendLine("\">");
        sb?.AppendLine("  <stringTable>");
        foreach (string stringKey in sortedStrings)
        {
            string? val = GetLocalizedMessage(stringKey, ci, etwFormat: true);
            sb?.Append("   <string id=\"").Append(stringKey).Append("\" value=\"").Append(val).AppendLine("\"/>");
        }
        sb?.AppendLine("  </stringTable>");
        sb?.AppendLine(" </resources>");

        sb?.AppendLine("</localization>");
        sb?.AppendLine("</instrumentationManifest>");
        return sb?.ToString() ?? "";
    }

#region private
    private void WriteNameAndMessageAttribs(StringBuilder? stringBuilder, string elementName, string name)
    {
        stringBuilder?.Append(" name=\"").Append(name).Append('"');
        WriteMessageAttrib(sb, elementName, name, name);
    }
    private void WriteMessageAttrib(StringBuilder? stringBuilder, string elementName, string name, string? value)
    {
        string? key = null;

        // See if the user wants things localized.
        if (resources != null)
        {
            // resource fallback: strings in the neutral culture will take precedence over inline strings
            key = elementName + "_" + name;
            if (resources.GetString(key, CultureInfo.InvariantCulture) is string localizedString)
                value = localizedString;
        }

        if (value == null)
            return;

        key ??= elementName + "_" + name;
        stringBuilder?.Append(" message=\"$(string.").Append(key).Append(")\"");

        if (stringTab.TryGetValue(key, out string? prevValue) && !prevValue.Equals(value))
        {
            ManifestError(SR.Format(SR.EventSource_DuplicateStringKey, key), true);
            return;
        }

        stringTab[key] = value;
    }
    internal string? GetLocalizedMessage(string key, CultureInfo ci, bool etwFormat)
    {
        string? value = null;
        if (resources != null)
        {
            string? localizedString = resources.GetString(key, ci);
            if (localizedString != null)
            {
                value = localizedString;
                if (etwFormat && key.StartsWith("event_", StringComparison.Ordinal))
                {
                    string evtName = key.Substring("event_".Length);
                    value = TranslateToManifestConvention(value, evtName);
                }
            }
        }
        if (etwFormat && value == null)
            stringTab.TryGetValue(key, out value);

        return value;
    }

    private static void AppendLevelName(StringBuilder? sb, EventLevel level)
    {
        if ((int)level < 16)
        {
            sb?.Append("win:");
        }

        sb?.Append(level switch // avoid boxing that comes from level.ToString()
        {
            EventLevel.LogAlways => nameof(EventLevel.LogAlways),
            EventLevel.Critical => nameof(EventLevel.Critical),
            EventLevel.Error => nameof(EventLevel.Error),
            EventLevel.Warning => nameof(EventLevel.Warning),
            EventLevel.Informational => nameof(EventLevel.Informational),
            EventLevel.Verbose => nameof(EventLevel.Verbose),
            _ => ((int)level).ToString()
        });
    }

    private string? GetChannelName(EventChannel channel, string eventName, string? eventMessage)
    {
        if (channelTab == null || !channelTab.TryGetValue((int)channel, out ChannelInfo? info))
        {
            if (channel < EventChannel.Admin) // || channel > EventChannel.Debug)
                ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));

            // allow channels to be auto-defined.  The well known ones get their well known names, and the
            // rest get names Channel<N>.  This allows users to modify the Manifest if they want more advanced features.
            channelTab ??= new Dictionary<int, ChannelInfo>(4);

            string channelName = channel.ToString();        // For well know channels this is a nice name, otherwise a number
            if (EventChannel.Debug < channel)
                channelName = "Channel" + channelName;      // Add a 'Channel' prefix for numbers.

            AddChannel(channelName, (int)channel, GetDefaultChannelAttribute(channel));
            if (!channelTab.TryGetValue((int)channel, out info))
                ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));
        }
        // events that specify admin channels *must* have non-null "Message" attributes
        if (resources != null)
            eventMessage ??= resources.GetString("event_" + eventName, CultureInfo.InvariantCulture);

        Debug.Assert(info!.Attribs != null);
        if (info.Attribs.EventChannelType == EventChannelType.Admin && eventMessage == null)
            ManifestError(SR.Format(SR.EventSource_EventWithAdminChannelMustHaveMessage, eventName, info.Name));
        return info.Name;
    }
    private string GetTaskName(EventTask task, string eventName)
    {
        if (task == EventTask.None)
            return "";

        taskTab ??= new Dictionary<int, string>();
        if (!taskTab.TryGetValue((int)task, out string? ret))
            ret = taskTab[(int)task] = eventName;
        return ret;
    }

    private string? GetOpcodeName(EventOpcode opcode, string eventName)
    {
        switch (opcode)
        {
            case EventOpcode.Info:
                return "win:Info";
            case EventOpcode.Start:
                return "win:Start";
            case EventOpcode.Stop:
                return "win:Stop";
            case EventOpcode.DataCollectionStart:
                return "win:DC_Start";
            case EventOpcode.DataCollectionStop:
                return "win:DC_Stop";
            case EventOpcode.Extension:
                return "win:Extension";
            case EventOpcode.Reply:
                return "win:Reply";
            case EventOpcode.Resume:
                return "win:Resume";
            case EventOpcode.Suspend:
                return "win:Suspend";
            case EventOpcode.Send:
                return "win:Send";
            case EventOpcode.Receive:
                return "win:Receive";
        }

        if (opcodeTab == null || !opcodeTab.TryGetValue((int)opcode, out string? ret))
        {
            ManifestError(SR.Format(SR.EventSource_UndefinedOpcode, opcode, eventName), true);
            ret = null;
        }

        return ret;
    }

    private void AppendKeywords(StringBuilder? sb, ulong keywords, string eventName)
    {
        // ignore keywords associate with channels
        // See ValidPredefinedChannelKeywords def for more.
        keywords &= ~ValidPredefinedChannelKeywords;

        bool appended = false;
        for (ulong bit = 1; bit != 0; bit <<= 1)
        {
            if ((keywords & bit) != 0)
            {
                string? keyword = null;
                if ((keywordTab == null || !keywordTab.TryGetValue(bit, out keyword)) &&
                    (bit >= (ulong)0x1000000000000))
                {
                    // do not report Windows reserved keywords in the manifest (this allows the code
                    // to be resilient to potential renaming of these keywords)
                    keyword = string.Empty;
                }
                if (keyword == null)
                {
                    ManifestError(SR.Format(SR.EventSource_UndefinedKeyword, "0x" + bit.ToString("x", CultureInfo.CurrentCulture), eventName), true);
                    keyword = string.Empty;
                }

                if (keyword.Length != 0)
                {
                    if (appended)
                    {
                        sb?.Append(' ');
                    }

                    sb?.Append(keyword);
                    appended = true;
                }
            }
        }
    }

    private string GetTypeName(Type type)
    {
        if (type.IsEnum)
        {
            string typeName = GetTypeName(type.GetEnumUnderlyingType());
            return typeName switch // ETW requires enums to be unsigned.
            {
                "win:Int8" => "win:UInt8",
                "win:Int16" => "win:UInt16",
                "win:Int32" => "win:UInt32",
                "win:Int64" => "win:UInt64",
                _ => typeName,
            };
        }

        switch (Type.GetTypeCode(type))
        {
            case TypeCode.Boolean:
                return "win:Boolean";
            case TypeCode.Byte:
                return "win:UInt8";
            case TypeCode.Char:
            case TypeCode.UInt16:
                return "win:UInt16";
            case TypeCode.UInt32:
                return "win:UInt32";
            case TypeCode.UInt64:
                return "win:UInt64";
            case TypeCode.SByte:
                return "win:Int8";
            case TypeCode.Int16:
                return "win:Int16";
            case TypeCode.Int32:
                return "win:Int32";
            case TypeCode.Int64:
                return "win:Int64";
            case TypeCode.String:
                return "win:UnicodeString";
            case TypeCode.Single:
                return "win:Float";
            case TypeCode.Double:
                return "win:Double";
            case TypeCode.DateTime:
                return "win:FILETIME";
            default:
                if (type == typeof(Guid))
                    return "win:GUID";
                else if (type == typeof(IntPtr))
                    return "win:Pointer";
                else if ((type.IsArray || type.IsPointer) && type.GetElementType() == typeof(byte))
                    return "win:Binary";

                ManifestError(SR.Format(SR.EventSource_UnsupportedEventTypeInManifest, type.Name), true);
                return string.Empty;
        }
    }

    private static void UpdateStringBuilder([NotNull] ref StringBuilder? stringBuilder, string eventMessage, int startIndex, int count)
    {
        stringBuilder ??= new StringBuilder();
        stringBuilder.Append(eventMessage, startIndex, count);
    }

    private static readonly string[] s_escapes = ["&amp;", "&lt;", "&gt;", "&apos;", "&quot;", "%r", "%n", "%t"];
    // Manifest messages use %N conventions for their message substitutions.   Translate from
    // .NET conventions.   We can't use RegEx for this (we are in mscorlib), so we do it 'by hand'
    private string TranslateToManifestConvention(string eventMessage, string evtName)
    {
        StringBuilder? stringBuilder = null;        // We lazily create this
        int writtenSoFar = 0;
        for (int i = 0; ;)
        {
            if (i >= eventMessage.Length)
            {
                if (stringBuilder == null)
                    return eventMessage;
                UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
                return stringBuilder.ToString();
            }

            int chIdx;
            if (eventMessage[i] == '%')
            {
                // handle format message escaping character '%' by escaping it
                UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
                stringBuilder.Append("%%");
                i++;
                writtenSoFar = i;
            }
            else if (i < eventMessage.Length - 1 &&
                (eventMessage[i] == '{' && eventMessage[i + 1] == '{' || eventMessage[i] == '}' && eventMessage[i + 1] == '}'))
            {
                // handle C# escaped '{" and '}'
                UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
                stringBuilder.Append(eventMessage[i]);
                i++; i++;
                writtenSoFar = i;
            }
            else if (eventMessage[i] == '{')
            {
                int leftBracket = i;
                i++;
                int argNum = 0;
                while (i < eventMessage.Length && char.IsDigit(eventMessage[i]))
                {
                    argNum = argNum * 10 + eventMessage[i] - '0';
                    i++;
                }
                if (i < eventMessage.Length && eventMessage[i] == '}')
                {
                    i++;
                    UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, leftBracket - writtenSoFar);
                    int manIndex = TranslateIndexToManifestConvention(argNum, evtName);
                    stringBuilder.Append('%').Append(manIndex);
                    // An '!' after the insert specifier {n} will be interpreted as a literal.
                    // We'll escape it so that mc.exe does not attempt to consider it the
                    // beginning of a format string.
                    if (i < eventMessage.Length && eventMessage[i] == '!')
                    {
                        i++;
                        stringBuilder.Append("%!");
                    }
                    writtenSoFar = i;
                }
                else
                {
                    ManifestError(SR.Format(SR.EventSource_UnsupportedMessageProperty, evtName, eventMessage));
                }
            }
            else if ((chIdx = "&<>'\"\r\n\t".IndexOf(eventMessage[i])) >= 0)
            {
                UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
                i++;
                stringBuilder.Append(s_escapes[chIdx]);
                writtenSoFar = i;
            }
            else
                i++;
        }
    }

    private int TranslateIndexToManifestConvention(int idx, string evtName)
    {
        if (perEventByteArrayArgIndices.TryGetValue(evtName, out List<int>? byteArrArgIndices))
        {
            foreach (int byArrIdx in byteArrArgIndices)
            {
                if (idx >= byArrIdx)
                    ++idx;
                else
                    break;
            }
        }
        return idx + 1;
    }

    private sealed class ChannelInfo
    {
        public string? Name;
        public ulong Keywords;
        public EventChannelAttribute? Attribs;
    }

    private readonly Dictionary<int, string> opcodeTab;
    private Dictionary<int, string>? taskTab;
    private Dictionary<int, ChannelInfo>? channelTab;
    private Dictionary<ulong, string>? keywordTab;
    private Dictionary<string, Type>? mapsTab;
    private readonly Dictionary<string, string> stringTab;       // Maps unlocalized strings to localized ones

    // WCF used EventSource to mimic a existing ETW manifest.   To support this
    // in just their case, we allowed them to specify the keywords associated
    // with their channels explicitly.   ValidPredefinedChannelKeywords is
    // this set of channel keywords that we allow to be explicitly set.  You
    // can ignore these bits otherwise.
    internal const ulong ValidPredefinedChannelKeywords = 0xF000000000000000;
    private ulong nextChannelKeywordBit = 0x8000000000000000;   // available Keyword bit to be used for next channel definition, grows down
    private const int MaxCountChannels = 8; // a manifest can defined at most 8 ETW channels

    private readonly StringBuilder? sb;               // Holds the provider information.
    private readonly StringBuilder? events;           // Holds the events.
    private readonly StringBuilder? templates;
    private readonly string providerName;
    private readonly ResourceManager? resources;      // Look up localized strings here.
    private readonly EventManifestOptions flags;
    private readonly List<string> errors;           // list of currently encountered errors
    private readonly Dictionary<string, List<int>> perEventByteArrayArgIndices;  // "event_name" -> List_of_Indices_of_Byte[]_Arg

    // State we track between StartEvent and EndEvent.
    private string? eventName;               // Name of the event currently being processed.
    private int numParams;                  // keeps track of the number of args the event has.
    private List<int>? byteArrArgIndices;   // keeps track of the index of each byte[] argument
#endregion
}