//
// Copyright (c) 2022-present, Trail of Bits, Inc.
// All rights reserved.
//
// This source code is licensed in accordance with the terms specified in
// the LICENSE file found in the root directory of this source tree.
//
using BrightIdeasSoftware;
using NtApiDotNet;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;

namespace RpcInvestigator.Util
{
    using static NativeTraceConsumer;
    using static TraceLogger;

    public class ParsedEtwEvent
    {
        //
        // This struct needs to be kept tidy. Some potentially useful fields have
        // been purposefully left out, as ETW data can be a firehose and memory
        // consumed quickly. Instances of these class are kept in memory as long
        // as the window containing the listview displays them.
        //
        // Further, this field list is reduced to only those observed as meaningful
        // for the Microsoft-Windows-RPC provider. Any other provider will probably
        // want other fields added, like EventId, Version, Provider, etc. To save
        // space, they are removed here instead of keeping them without the [OLVColumn]
        // attribute.
        //
        [OLVColumn]
        public uint ProcessId { get; set; }
        [OLVColumn]
        public long ProcessStartKey { get; set; }
        [OLVColumn]
        public uint ThreadId { get; set; }
        [OLVColumn]
        public Sid UserSid { get; set; }
        [OLVColumn]
        public Guid ActivityId { get; set; }
        [OLVColumn]
        public DateTime Timestamp { get; set; }
        [OLVColumn]
        public string Level { get; set; }
        [OLVColumn]
        public string Channel { get; set; }
        [OLVColumn]
        public List<string> Keywords { get; set; }
        [OLVColumn]
        public string Task { get; set; }
        [OLVColumn]
        public string Opcode { get; set; }
        public Dictionary<string, string> UserDataProperties { get; set; }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            //sb.AppendLine("Event ID: " + EventId);
            //sb.AppendLine("Version: " + Version);
            sb.AppendLine("Timestamp: " + Timestamp.ToString());
            sb.AppendLine("Process: " + ProcessId + " (Key=" + ProcessStartKey + ")");
            sb.AppendLine("TID: " + ThreadId);
            if (UserSid != null)
            {
                sb.AppendLine("User SID: " + UserSid.ToString());
            }
            if (ActivityId != null)
            {
                sb.AppendLine("Activity ID: " + ActivityId.ToString());
            }
            sb.AppendLine("Level: " + Level);
            sb.AppendLine("Channel: " + Channel);
            sb.AppendLine("Keywords: ");
            foreach (var k in Keywords)
            {
                sb.AppendLine("   " + k);
            }
            sb.AppendLine("Task: " + Task);
            sb.AppendLine("Opcode: " + Opcode);
            sb.AppendLine("UserData Properties:");
            if (UserDataProperties != null)
            {
                foreach (var kvp in UserDataProperties)
                {
                    sb.AppendLine("   " + kvp.Key + " : " + kvp.Value);
                }
            }
            return sb.ToString();
        }
    }

    public class EtwEventParserBuffers : IDisposable
    {
        public EVENT_RECORD m_Event;
        public TRACE_EVENT_INFO m_TraceEventInfo;
        public EVENT_MAP_INFO m_MapInfo;
        public IntPtr m_TraceEventInfoBuffer;
        public IntPtr m_TdhMapBuffer;
        public IntPtr m_TdhOutputBuffer;
        public IntPtr m_EventBuffer;
        public static int ETW_MAX_EVENT_SIZE = 65536; // 65K
        public static int MAP_SIZE = 1024 * 4000; // 4MB
        public static int TDH_STR_SIZE = 1024 * 4000; // 4MB
        public static int TRACE_EVENT_INFO_SIZE = 1024 * 4000; // 4MB
        private bool m_Disposed;

        public EtwEventParserBuffers()
        {
            //
            // Pre-allocate large buffers to re-use across all events that
            // reuse this parser. This helps performance tremendously.
            //
            // The map buffer holds manifest data that can be ulong.max size.
            // We pick a decently large size here (4MB)
            //
            // The TDH output buffer contains arbitrary unicode string data,
            // and can be as large as the ETW provider wants AFAIK.
            //
            // Event buffer can never exceed 65k.
            //
            // The TRACE_EVENT_INFO structure is variable-length and the total
            // size depends on the ETW provider's manifest.
            //
            m_TdhMapBuffer = Marshal.AllocHGlobal(MAP_SIZE);
            m_TdhOutputBuffer = Marshal.AllocHGlobal(TDH_STR_SIZE);
            m_EventBuffer = Marshal.AllocHGlobal(ETW_MAX_EVENT_SIZE);
            m_TraceEventInfoBuffer = Marshal.AllocHGlobal(TRACE_EVENT_INFO_SIZE);
        }

        ~EtwEventParserBuffers()
        {
            Dispose(false);
        }

        private void Dispose(bool disposing)
        {
            if (m_Disposed)
            {
                return;
            }

            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Disposing EtwEventParserBuffers");

            m_Disposed = true;

            if (m_TdhMapBuffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_TdhMapBuffer);
            }

            if (m_TdhOutputBuffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_TdhOutputBuffer);
            }

            if (m_EventBuffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_EventBuffer);
            }

            if (m_TraceEventInfoBuffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(m_TraceEventInfoBuffer);
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void SetEvent(EVENT_RECORD Event)
        {
            m_Event = Event;
            Marshal.StructureToPtr(Event, m_EventBuffer, false);
        }

        public void SetTraceInfo()
        {
            m_TraceEventInfo = (TRACE_EVENT_INFO)Marshal.PtrToStructure(
                m_TraceEventInfoBuffer, typeof(TRACE_EVENT_INFO));
        }

        public void SetMapInfoBuffer()
        {
            m_MapInfo = (EVENT_MAP_INFO)Marshal.PtrToStructure(
                m_TdhMapBuffer, typeof(EVENT_MAP_INFO));
        }
    }

    public class EtwEventParser
    {
        private EtwEventParserBuffers m_Buffers;
        private long m_UniqueId;
        private ParsedEtwEvent m_ParsedEvent;
        private long m_PerfFreq;

        public EtwEventParser(
            EVENT_RECORD Event,
            EtwEventParserBuffers Buffers,
            long PerfFreq)
        {
            m_Buffers = Buffers;
            m_Buffers.SetEvent(Event);
            m_UniqueId = Event.EventHeader.TimeStamp;
            m_PerfFreq = PerfFreq;
        }

        public
        void
        DumpDiagnosticInfo(string DestinationFolder)
        {
            var size = m_Buffers.m_Event.EventHeader.Size;
            var rawEvent = new byte[size];
            Marshal.Copy(m_Buffers.m_EventBuffer, rawEvent, 0, size);
            string target = Path.Combine(DestinationFolder, "Event-raw-" +
                m_UniqueId.ToString("X") + ".bin");
            File.WriteAllBytes(target, rawEvent);
            target = Path.Combine(DestinationFolder, "Event-text-" +
                m_UniqueId.ToString("X") + ".txt");
            File.WriteAllText(target, m_ParsedEvent.ToString());
        }

        public
        ParsedEtwEvent
        Parse()
        {
            Trace(TraceLoggerType.EtwEventParser,
                 TraceEventType.Verbose,
                 "Parsing event 0x" + m_UniqueId.ToString("X"));

            //
            // Ignore string-only events and those generated by WPP.
            //
            if (m_Buffers.m_Event.EventHeader.Flags.HasFlag(
                    EventHeaderFlags.StringOnly) ||
                m_Buffers.m_Event.EventHeader.Flags.HasFlag(
                    EventHeaderFlags.TraceMessage))
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Warning,
                    "Ignoring string-only or WPP-generated event");
                return null;
            }
            //
            // Ignore events that require legacy WMI MOF manifest to parse user data.
            //
            if (m_Buffers.m_Event.EventHeader.EventProperty.HasFlag(
                EventHeaderPropertyFlags.LegacyEventLog))
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Warning,
                    "Ignoring legacy event log style event");
                return null;
            }

            m_ParsedEvent = new ParsedEtwEvent();

            //
            // Get stuff hanging right off the EVENT_RECORD's header
            //
            m_ParsedEvent.ProcessId = m_Buffers.m_Event.EventHeader.ProcessId;
            m_ParsedEvent.ThreadId = m_Buffers.m_Event.EventHeader.ThreadId;
            m_ParsedEvent.ActivityId = m_Buffers.m_Event.EventHeader.ActivityId;
            //
            // Event timestamp is stored in QPC format, convert to a scaled
            // 100-ns standard system time that DateTime can cope with.
            //
            var scaledTimestamp = (long)
                (m_Buffers.m_Event.EventHeader.TimeStamp * 10000000.0 / m_PerfFreq);
            m_ParsedEvent.Timestamp = DateTime.FromFileTime(scaledTimestamp);

            //parsedEvent.ProviderGuid = m_Buffers.m_Event.EventHeader.ProviderId;

            //
            // The remaining parser steps require TRACE_EVENT_INFO. We cache the
            // resulting buffer internally for the remainder of processing this
            // event.
            //
            if (!GetTraceEventInfo())
            {
                return null;
            }

            //
            // Use TDH to parse meta information from the event descriptor
            // inside the event header (keywords, opcode, task, etc)
            //
            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Parsing trace event meta information");
            if (!ParseMetaInformation())
            {
                return null;
            }

            //
            // Parse "extended data" which is stuff added by the OS to every
            // ETW event - like SID, stack trace, process start key,
            // and so on (some of these must be turned on when you call
            // EnableTraceEx)
            //
            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Parsing trace extended data");
            if (!ParseExtendedData())
            {
                return null;
            }

            Trace(TraceLoggerType.EtwEventParser,
                 TraceEventType.Information,
                 m_ParsedEvent.ToString());

            //
            // Use TDH and the manifest to parse custom "user data" which
            // has information unique to this provider.
            //
            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Parsing trace user data");
            if (m_Buffers.m_Event.UserDataLength <= 0)
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Warning,
                    "Event contained no user data.");
                return m_ParsedEvent;
            }
            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Event has " + m_Buffers.m_Event.UserDataLength +
                " bytes of user data.");

            //
            // Instantiate a PropertyParser to reuse the pre-allocated buffers.
            //
            var propertyParser = new PropertyParser(m_Buffers);
            if (!propertyParser.Initialize())
            {
                return null;
            }

            //
            // Parsing begins at the top level properties. The PropertyParser.Parse
            // method will recurse as needed.
            //
            var properties = new Dictionary<string, string>();
            var success = propertyParser.Parse(
                0,
                m_Buffers.m_TraceEventInfo.TopLevelPropertyCount,
                ref properties,
                new StringBuilder());
            m_ParsedEvent.UserDataProperties = properties;
            if (!success)
            {
                return null;
            }
            return m_ParsedEvent;
        }

        private
        bool
        GetTraceEventInfo()
        {
            uint bufferSize = (uint)EtwEventParserBuffers.TRACE_EVENT_INFO_SIZE;
            var status = TdhGetEventInformation(
                m_Buffers.m_EventBuffer,
                0,
                IntPtr.Zero,
                m_Buffers.m_TraceEventInfoBuffer,
                ref bufferSize);
            if (status != NtApiDotNet.Win32.Win32Error.SUCCESS)
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Error,
                    "TdhGetEventInformation failed: " +
                    status.ToString("X"));
                return false;
            }
            m_Buffers.SetTraceInfo();
            return true;
        }

        private
        bool
        ParseMetaInformation()
        {
            var buffer = m_Buffers.m_TraceEventInfoBuffer;

            try
            {
                /*if (m_Buffers.m_TraceEventInfo.ProviderNameOffset > 0)
                {
                    ParsedEvent.Provider = Marshal.PtrToStringUni(
                        IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.ProviderNameOffset));
                }*/

                if (m_Buffers.m_TraceEventInfo.LevelNameOffset > 0)
                {
                    m_ParsedEvent.Level = Marshal.PtrToStringUni(
                        IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.LevelNameOffset));
                }
                else
                {
                    m_ParsedEvent.Level = m_Buffers.m_Event.EventHeader.Level.ToString();
                }

                if (m_Buffers.m_TraceEventInfo.ChannelNameOffset > 0)
                {
                    m_ParsedEvent.Channel = Marshal.PtrToStringUni(
                        IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.ChannelNameOffset));
                }
                else
                {
                    m_ParsedEvent.Channel =
                        m_Buffers.m_Event.EventHeader.Channel.ToString();
                }

                m_ParsedEvent.Keywords = new List<string>();
                if (m_Buffers.m_TraceEventInfo.KeywordsNameOffset > 0)
                {
                    for (int offset = m_Buffers.m_TraceEventInfo.KeywordsNameOffset; ;)
                    {
                        var str = Marshal.PtrToStringUni(IntPtr.Add(buffer, offset));
                        if (string.IsNullOrEmpty(str))
                        {
                            break;
                        }
                        m_ParsedEvent.Keywords.Add(str);
                        offset += Encoding.Unicode.GetByteCount(str) + 2;
                    }
                }

                if (m_Buffers.m_TraceEventInfo.TaskNameOffset > 0)
                {
                    m_ParsedEvent.Task = Marshal.PtrToStringUni(
                        IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.TaskNameOffset));
                }
                else
                {
                    m_ParsedEvent.Task = m_Buffers.m_Event.EventHeader.Task.ToString();
                }

                if (m_Buffers.m_TraceEventInfo.OpcodeNameOffset > 0)
                {
                    m_ParsedEvent.Opcode = Marshal.PtrToStringUni(
                        IntPtr.Add(buffer, m_Buffers.m_TraceEventInfo.OpcodeNameOffset));
                }
                else
                {
                    m_ParsedEvent.Opcode =
                        m_Buffers.m_Event.EventHeader.Opcode.ToString();
                }
            }
            catch (Exception ex)
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Error,
                    "An exception occurred when marshaling the " +
                    "TRACE_EVENT_INFO struct: " + ex.Message);
                return false;
            }
            return true;
        }

        private
        bool
        ParseExtendedData()
        {
            if (!m_Buffers.m_Event.EventHeader.Flags.HasFlag(
                    EventHeaderFlags.ExtendedInfo) ||
                m_Buffers.m_Event.ExtendedDataCount <= 0)
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Warning,
                    "Event contained no extended data.");
                return true;
            }

            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Information,
                "Event has " + m_Buffers.m_Event.ExtendedDataCount +
                " extended data items.");

            IntPtr buffer = m_Buffers.m_Event.ExtendedData;
            for (int i = 0; i < m_Buffers.m_Event.ExtendedDataCount; i++)
            {
                var item = (EVENT_HEADER_EXTENDED_DATA_ITEM)Marshal.PtrToStructure(
                    buffer, typeof(EVENT_HEADER_EXTENDED_DATA_ITEM));
                if (item.DataSize == 0)
                {
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Error,
                        "Corrupt extended data, zero-length " +
                        "extended data item.");
                    return false;
                }
                unsafe
                {
                    IntPtr data = new IntPtr((void*)item.DataPtr);
                    try
                    {
                        switch (item.ExtType)
                        {
                            case EventHeaderExtendedDataType.Sid:
                                {
                                    m_ParsedEvent.UserSid = new Sid(data);
                                    break;
                                }
                            case EventHeaderExtendedDataType.RelatedActivityId:
                                {
                                    //
                                    // This is always 0 for RPC events but could
                                    // be useful in the future.
                                    //
                                    //ParsedEvent.ParentActivityId =
                                    //    (Guid)Marshal.PtrToStructure(data, typeof(Guid));
                                    break;
                                }
                            case EventHeaderExtendedDataType.ProcessStartKey:
                                {
                                    m_ParsedEvent.ProcessStartKey = Marshal.ReadInt64(data);
                                    break;
                                }
                            default:
                                {
                                    break;
                                }
                        }
                    }
                    catch (Exception ex)
                    {
                        Trace(TraceLoggerType.EtwEventParser,
                            TraceEventType.Error,
                            "Failed to cast extended data item #" + i +
                            ", type " + item.ExtType + ", length " + item.DataSize +
                            ", pointer 0x" + item.DataPtr + ": " + ex.Message);
                        return false;
                    }
                    var size = Marshal.SizeOf(typeof(EVENT_HEADER_EXTENDED_DATA_ITEM));
                    buffer = IntPtr.Add(buffer, size);
                }
            }
            return true;
        }
    }

    public class PropertyParser
    {
        private EtwEventParserBuffers m_Buffers;
        private Dictionary<int, ushort> m_PropertyIndexLookup;
        private List<EVENT_PROPERTY_INFO> m_PropertyInfo;
        private IntPtr m_UserDataCurrentPosition;
        private IntPtr m_UserDataEndPosition;
        private ushort m_UserDataRemaining;

        public PropertyParser(EtwEventParserBuffers Buffers)
        {
            m_Buffers = Buffers;
            m_PropertyIndexLookup = new Dictionary<int, ushort>();
            m_UserDataCurrentPosition = m_Buffers.m_Event.UserData;
            m_UserDataEndPosition =
                IntPtr.Add(m_UserDataCurrentPosition, m_Buffers.m_Event.UserDataLength);
            m_UserDataRemaining = m_Buffers.m_Event.UserDataLength;
        }

        public
        bool
        Initialize()
        {
            //
            // Parse the property array from the TRACE_EVENT_INFO for this event.
            //
            try
            {
                int offset = Marshal.OffsetOf(typeof(TRACE_EVENT_INFO),
                    "EventPropertyInfoArray").ToInt32();
                IntPtr arrayStart = IntPtr.Add(m_Buffers.m_TraceEventInfoBuffer, offset);
                m_PropertyInfo = MarshalHelper.MarshalArray<EVENT_PROPERTY_INFO>(arrayStart,
                            (uint)m_Buffers.m_TraceEventInfo.PropertyCount);
            }
            catch (Exception ex)
            {
                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Error,
                    "Failed to retrieve property array: " + ex.Message);
                return false;
            }
            return true;
        }

        public
        bool
        Parse(
            int PropertyIndexStart,
            int PropertyIndexEnd,
            ref Dictionary<string, string> Properties,
            StringBuilder ParentStruct)
        {
            for (int i = PropertyIndexStart; i < PropertyIndexEnd; i++)
            {
                var propertyInfo = m_PropertyInfo[i];
                string propertyName = "(Unnamed)";
                if (propertyInfo.NameOffset != 0)
                {
                    propertyName = Marshal.PtrToStringUni(
                        IntPtr.Add(m_Buffers.m_TraceEventInfoBuffer,
                            propertyInfo.NameOffset));
                }

                bool isArray = false;
                var arrayCount = GetArrayLength(i);

                if (arrayCount > 1 || propertyInfo.Flags.HasFlag(
                    PROPERTY_FLAGS.ParamCount | PROPERTY_FLAGS.ParamFixedCount))
                {
                    isArray = true;
                }

                //
                // If this property is a scalar integer, remember the value
                // in case it is needed for a subsequent property's length
                // or count.
                //
                if (!propertyInfo.Flags.HasFlag(
                        PROPERTY_FLAGS.Struct | PROPERTY_FLAGS.ParamCount) &&
                    propertyInfo.CountOrCountIndex == 1)
                {
                    StorePropertyLookup(i);
                }

                Trace(TraceLoggerType.EtwEventParser,
                    TraceEventType.Verbose,
                    "Parsing property #" + i + " [" + propertyName + "]" +
                    (isArray ? " - Array with " + arrayCount + " elements" : ""));

                //
                // For simplicity, non-array properties are treated like 1-length
                // arrays.
                //
                for (int j = 0; j < arrayCount; j++)
                {
                    var usePropertyName = propertyName;

                    if (isArray)
                    {
                        usePropertyName += "-" + j;
                        Trace(TraceLoggerType.EtwEventParser,
                            TraceEventType.Verbose,
                            usePropertyName);
                    }

                    StringBuilder structValue = new StringBuilder();

                    if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.Struct))
                    {
                        Trace(TraceLoggerType.EtwEventParser,
                            TraceEventType.Verbose,
                            "  Property is a struct (start index = " +
                            propertyInfo.StructStartIndex + ", count = " +
                            propertyInfo.NumOfStructMembers + "), recursing...");

                        //
                        // Recurse structs.
                        //
                        structValue.AppendLine(usePropertyName);
                        int startIndex = propertyInfo.StructStartIndex;
                        int endIndex = startIndex + propertyInfo.NumOfStructMembers;
                        if (!Parse(startIndex, endIndex, ref Properties, structValue))
                        {
                            return false;
                        }
                    }

                    if (structValue.Length > 0)
                    {
                        //
                        // Returning from struct recursion? Use that accumulated
                        // value for this property.
                        //
                        AddProperty(usePropertyName,
                            structValue.ToString(),
                            new StringBuilder(),
                            ref Properties);
                    }
                    else if (!GetPropertyValue(i, j, ref Properties, ParentStruct))
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private
        bool
        GetPropertyValue(
            int PropertyIndex,
            int ArrayIndex,
            ref Dictionary<string, string> Properties,
            StringBuilder ParentStruct
            )
        {
            var propertyInfo = m_PropertyInfo[PropertyIndex];
            var useMap = false;
            var propertyName = "(Unnamed)";
            var propertyLength = GetPropertyLength(PropertyIndex);
            var traceEventInfoBuffer = m_Buffers.m_TraceEventInfoBuffer;
            var eventBuffer = m_Buffers.m_EventBuffer;

            if (propertyInfo.NameOffset != 0)
            {
                propertyName = Marshal.PtrToStringUni(
                    IntPtr.Add(traceEventInfoBuffer, propertyInfo.NameOffset));
            }

            Trace(TraceLoggerType.EtwEventParser,
                TraceEventType.Verbose,
                "     Property named " + propertyName + " (length=" +
                propertyLength + ")");

            //
            // If the property has an associated map (i.e. an enumerated type),
            // try to look up the map data. (If this is an array, we only need
            // to do the lookup on the first iteration.)
            //
            if (propertyInfo.MapNameOffset != 0 && ArrayIndex == 0)
            {
                switch (propertyInfo.InType)
                {
                    case TdhInputType.UInt8:
                    case TdhInputType.UInt16:
                    case TdhInputType.UInt32:
                    case TdhInputType.HexInt32:
                        {
                            var mapName = Marshal.PtrToStringUni(
                                    IntPtr.Add(traceEventInfoBuffer, propertyInfo.MapNameOffset));
                            uint sizeNeeded = (uint)EtwEventParserBuffers.MAP_SIZE;
                            var status = TdhGetEventMapInformation(
                                eventBuffer,
                                mapName,
                                m_Buffers.m_TdhMapBuffer,
                                ref sizeNeeded).MapDosErrorToStatus();
                            if (status != NtStatus.STATUS_SUCCESS)
                            {
                                //
                                // We could retry, but I want to avoid frequent memory
                                // allocations. If this happens, investigate increasing
                                // the static buffer size.
                                //
                                Trace(TraceLoggerType.EtwEventParser,
                                    TraceEventType.Error,
                                    "TdhGetEventMapInformation() failed: 0x" +
                                    status.ToString("X"));
                                break;
                            }

                            useMap = true;
                            m_Buffers.SetMapInfoBuffer();
                            break;
                        }
                    default:
                        {
                            break;
                        }
                }
            }

            //
            // Loop because we may need to retry the call to TdhFormatProperty.
            //
            for ( ; ; )
            {
                if (IsEmptyProperty(propertyLength, PropertyIndex))
                {
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Verbose,
                        "     Property has an empty value.");
                    AddProperty(propertyName,
                        "<empty>",
                        ParentStruct,
                        ref Properties);
                    return true;
                }

                uint outputBufferSize = 65535; // max is 64kb.
                ushort dataConsumed = 0;
                var result = TdhFormatProperty(
                    traceEventInfoBuffer,
                    useMap ? m_Buffers.m_TdhMapBuffer : IntPtr.Zero,
                    (uint)(m_Buffers.m_Event.EventHeader.Flags.HasFlag(
                        EventHeaderFlags.Is32BitHeader) ? 4 : 8),
                    propertyInfo.InType,
                    propertyInfo.OutType == TdhOutputType.NoPrin ?
                        TdhOutputType.Null : propertyInfo.OutType,
                    propertyLength,
                    m_UserDataRemaining,
                    m_UserDataCurrentPosition,
                    ref outputBufferSize,
                    m_Buffers.m_TdhOutputBuffer,
                    ref dataConsumed);
                var status = result.MapDosErrorToStatus();
                if (status == NtStatus.STATUS_BUFFER_TOO_SMALL)
                {
                    //
                    // We could retry, but I want to avoid frequent memory
                    // allocations. However, this should never happen because
                    // we allocate the max ushort size the prototype allows.
                    //
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Error,
                        "TdhFormatProperty() failed, buffer too small: 0x" +
                        status.ToString("X"));
                    return false;
                }
                else if (result == NtApiDotNet.Win32.Win32Error.ERROR_EVT_INVALID_EVENT_DATA && useMap)
                {
                    //
                    // If the value isn't in the map, TdhFormatProperty treats it
                    // as an error instead of just putting the number in. We'll
                    // try again with no map.
                    //
                    useMap = false;
                    continue;
                }
                else if (!status.IsSuccess())
                {
                    var error = "TdhFormatProperty() failed: 0x" + status.ToString("X");
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Error,
                        error);
                    return false;
                }
                else
                {
                    var propertyValue = Marshal.PtrToStringUni(
                        m_Buffers.m_TdhOutputBuffer);
                    AddProperty(propertyName,
                        propertyValue,
                        ParentStruct,
                        ref Properties);
                    AdvanceBufferPosition(dataConsumed);
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Verbose,
                        "     Property value is "+propertyValue);
                    return true;
                }
            }
        }

        private
        bool
        IsEmptyProperty(int PropertyLength, int PropertyIndex)
        {
            var propertyInfo = m_PropertyInfo[PropertyIndex];

            //
            // Null data or null-terminated strings are not supported by TdhFormatProperty
            //
            return PropertyLength == 0 &&
                (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamLength |
                    PROPERTY_FLAGS.ParamFixedLength)) &&
                (propertyInfo.InType == TdhInputType.UnicodeString ||
                 propertyInfo.InType == TdhInputType.AnsiString);
        }

        private
        void
        StorePropertyLookup(int PropertyIndex)
        {
            var propertyInfo = m_PropertyInfo[PropertyIndex];

            if (m_PropertyIndexLookup.ContainsKey(PropertyIndex))
            {
                //
                // Properties that are repeated in arrays will be revisited for each
                // array element. The index lookup is updated each time.
                //
                m_PropertyIndexLookup.Remove(PropertyIndex);
            }
            //
            // Note: The integer values read here from the Marshaler are
            // read from the current position in the buffer and the buffer
            // should NOT be advanced.
            //
            switch (propertyInfo.InType)
            {
                case TdhInputType.Int8:
                case TdhInputType.UInt8:
                    {
                        if (CanSeek(1))
                        {
                            var value = Marshal.ReadByte(m_UserDataCurrentPosition);
                            m_PropertyIndexLookup.Add(
                                PropertyIndex,
                                value);
                        }
                        else
                        {
                            Trace(TraceLoggerType.EtwEventParser,
                                 TraceEventType.Error,
                                 "  Unable to read 1 byte for PropertyIndex " +
                                 PropertyIndex);
                        }
                        break;
                    }
                case TdhInputType.Int16:
                case TdhInputType.UInt16:
                    {
                        if (CanSeek(2))
                        {
                            var value = (ushort)Marshal.ReadInt16(
                                m_UserDataCurrentPosition);
                            m_PropertyIndexLookup.Add(PropertyIndex, value);
                        }
                        else
                        {
                            Trace(TraceLoggerType.EtwEventParser,
                                 TraceEventType.Error,
                                 "  Unable to read 2 bytes for PropertyIndex " +
                                 PropertyIndex);
                        }
                        break;
                    }
                case TdhInputType.Int32:
                case TdhInputType.UInt32:
                case TdhInputType.HexInt32:
                    {
                        if (CanSeek(4))
                        {
                            var value = Marshal.ReadInt32(m_UserDataCurrentPosition);
                            var asUshort = (ushort)(value > 0xffff ? 0xff : value);
                            m_PropertyIndexLookup.Add(PropertyIndex, asUshort);
                        }
                        else
                        {
                            Trace(TraceLoggerType.EtwEventParser,
                                 TraceEventType.Error,
                                 "  Unable to read 4 bytes for PropertyIndex " +
                                 PropertyIndex);
                        }
                        break;
                    }
                default:
                    {
                        break;
                    }
            }
        }

        private
        ushort
        GetPropertyLength(int PropertyIndex)
        {
            var propertyInfo = m_PropertyInfo[PropertyIndex];

            if (propertyInfo.OutType == TdhOutputType.Ipv6 &&
                propertyInfo.InType == TdhInputType.Binary &&
                propertyInfo.LengthOrLengthIndex == 0 &&
                !propertyInfo.Flags.HasFlag(
                    PROPERTY_FLAGS.ParamLength | PROPERTY_FLAGS.ParamFixedLength))
            {
                //
                // special case for incorrectly-defined IPV6 addresses
                //
                return 16;
            }

            if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamLength))
            {
                //
                // The length of the property was previously parsed, so
                // look up that value now.
                //
                return m_PropertyIndexLookup[propertyInfo.LengthOrLengthIndex];
            }

            //
            // The length of the property is directly in the field.
            //
            return propertyInfo.LengthOrLengthIndex;
        }

        private
        ushort
        GetArrayLength(int PropertyIndex)
        {
            var propertyInfo = m_PropertyInfo[PropertyIndex];

            if (propertyInfo.Flags.HasFlag(PROPERTY_FLAGS.ParamCount))
            {
                //
                // The length of the array was previously parsed, so
                // look up that value now.
                //
                return m_PropertyIndexLookup[propertyInfo.CountOrCountIndex];
            }

            //
            // The length of the array is directly in the field.
            //
            return propertyInfo.CountOrCountIndex;
        }

        private
        bool
        CanSeek(int NumBytes)
        {
            return m_UserDataEndPosition.ToInt64() -
                m_UserDataCurrentPosition.ToInt64() >= NumBytes;
        }

        private
        void
        AdvanceBufferPosition(ushort NumBytes)
        {
            m_UserDataCurrentPosition = IntPtr.Add(
                m_UserDataCurrentPosition, NumBytes);
            m_UserDataRemaining -= NumBytes;
        }

        private
        void
        AddProperty(
            string PropertyName,
            string PropertyValue,
            StringBuilder ParentStruct,
            ref Dictionary<string, string> Properties)
        {
            if (ParentStruct.Length > 0)
            {
                //
                // We're not directly inserting this property value into the UserData
                // properties because it's actually a field of a struct property we're
                // recursing.  Append it to that value.
                //
                ParentStruct.AppendLine("." + PropertyName + " = " + PropertyValue);
            }
            else
            {
                if (Properties.ContainsKey(PropertyName))
                {
                    //
                    // This is unexpected. ETW event manifests must specify unique
                    // property names IIUC. If this turns out to be untrue, we can
                    // fudge a unique name here. For now, we'll simply drop this
                    // property entirely.
                    //
                    Trace(TraceLoggerType.EtwEventParser,
                        TraceEventType.Error,
                        "The property " + PropertyName + " already exists in the list "+
                        "of UserData properties parsed from this event. Ignoring this "+
                        "discovered property value.");
                    return;
                }
                Properties.Add(PropertyName, PropertyValue);
            }
        }
    }
}
