File: CustomDebugInfoReader.cs
Web Access
Project: src\src\Dependencies\CodeAnalysis.Debugging\Microsoft.CodeAnalysis.Debugging.Package.csproj (Microsoft.CodeAnalysis.Debugging.Package)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis.PooledObjects;
 
#pragma warning disable CA1200 // Avoid using cref tags with a prefix
 
namespace Microsoft.CodeAnalysis.Debugging
{
    /// <summary>
    /// A collection of utility method for consuming custom debug info from a PDB.
    /// </summary>
    /// <remarks>
    /// This is not a public API, so we're just going to let bad offsets fail on their own.
    /// </remarks>
    internal static class CustomDebugInfoReader
    {
        /// <summary>
        /// This is the first header in the custom debug info blob.
        /// </summary>
        private static void ReadGlobalHeader(byte[] bytes, ref int offset, out byte version, out byte count)
        {
            version = bytes[offset + 0];
            count = bytes[offset + 1];
            offset += CustomDebugInfoConstants.GlobalHeaderSize;
        }
 
        /// <summary>
        /// After the global header (see <see cref="ReadGlobalHeader"/> comes list of custom debug info record.
        /// Each record begins with a standard header.
        /// </summary>
        private static void ReadRecordHeader(byte[] bytes, ref int offset, out byte version, out CustomDebugInfoKind kind, out int size, out int alignmentSize)
        {
            version = bytes[offset + 0];
            kind = (CustomDebugInfoKind)bytes[offset + 1];
            alignmentSize = bytes[offset + 3];
 
            // two bytes of padding after kind
            size = BitConverter.ToInt32(bytes, offset + 4);
 
            offset += CustomDebugInfoConstants.RecordHeaderSize;
        }
 
        /// <exception cref="InvalidOperationException"></exception>
        public static ImmutableArray<byte> TryGetCustomDebugInfoRecord(byte[] customDebugInfo, CustomDebugInfoKind recordKind)
        {
            foreach (var record in GetCustomDebugInfoRecords(customDebugInfo))
            {
                if (record.Kind == recordKind)
                {
                    return record.Data;
                }
            }
 
            return default;
        }
 
        /// <remarks>
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        /// <exception cref="InvalidOperationException"></exception>
        public static IEnumerable<CustomDebugInfoRecord> GetCustomDebugInfoRecords(byte[] customDebugInfo)
        {
            if (customDebugInfo.Length < CustomDebugInfoConstants.GlobalHeaderSize)
            {
                throw new InvalidOperationException("Invalid header.");
            }
 
            var offset = 0;
            ReadGlobalHeader(customDebugInfo, ref offset, out var globalVersion, out _);
 
            if (globalVersion != CustomDebugInfoConstants.Version)
            {
                yield break;
            }
 
            while (offset <= customDebugInfo.Length - CustomDebugInfoConstants.RecordHeaderSize)
            {
                ReadRecordHeader(customDebugInfo, ref offset, out var version, out var kind, out var size, out var alignmentSize);
                if (size < CustomDebugInfoConstants.RecordHeaderSize)
                {
                    throw new InvalidOperationException("Invalid header.");
                }
 
                switch (kind)
                {
                    case CustomDebugInfoKind.EditAndContinueLambdaMap:
                    case CustomDebugInfoKind.EditAndContinueLocalSlotMap:
                    case CustomDebugInfoKind.TupleElementNames:
                        break;
                    default:
                        // ignore alignment for CDIs that don't support it
                        alignmentSize = 0;
                        break;
                }
 
                var bodySize = size - CustomDebugInfoConstants.RecordHeaderSize;
                if (offset > customDebugInfo.Length - bodySize || alignmentSize > 3 || alignmentSize > bodySize)
                {
                    throw new InvalidOperationException("Invalid header.");
                }
 
                yield return new CustomDebugInfoRecord(kind, version, ImmutableArray.Create(customDebugInfo, offset, bodySize - alignmentSize));
                offset += bodySize;
            }
        }
 
        /// <summary>
        /// For each namespace declaration enclosing a method (innermost-to-outermost), there is a count
        /// of the number of imports in that declaration.
        /// </summary>
        /// <remarks>
        /// There's always at least one entry (for the global namespace).
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        public static ImmutableArray<short> DecodeUsingRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
            var numCounts = ReadInt16(bytes, ref offset);
 
            var builder = ArrayBuilder<short>.GetInstance(numCounts);
            for (var i = 0; i < numCounts; i++)
            {
                builder.Add(ReadInt16(bytes, ref offset));
            }
 
            return builder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// This indicates that further information can be obtained by looking at the custom debug
        /// info of another method (specified by token).
        /// </summary>
        /// <remarks>
        /// Appears when multiple method would otherwise have identical using records (see <see cref="DecodeUsingRecord"/>).
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        public static int DecodeForwardRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
            return ReadInt32(bytes, ref offset);
        }
 
        /// <summary>
        /// This indicates that further information can be obtained by looking at the custom debug
        /// info of another method (specified by token).
        /// </summary>
        /// <remarks>
        /// Appears when there are extern aliases and edit-and-continue is disabled.
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        public static int DecodeForwardToModuleRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
            return ReadInt32(bytes, ref offset);
        }
 
        /// <summary>
        /// Scopes of state machine hoisted local variables.
        /// </summary>
        /// <remarks>
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        public static ImmutableArray<StateMachineHoistedLocalScope> DecodeStateMachineHoistedLocalScopesRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
 
            var bucketCount = ReadInt32(bytes, ref offset);
 
            var builder = ArrayBuilder<StateMachineHoistedLocalScope>.GetInstance(bucketCount);
            for (var i = 0; i < bucketCount; i++)
            {
                var startOffset = ReadInt32(bytes, ref offset);
                var endOffset = ReadInt32(bytes, ref offset);
 
                // The range is stored as end-inclusive.
                // The case [0,0] is ambiguous in Windows PDBs.
                // It means either a user defined local with range [0, 1) or a synthesized local.
                // It is unlikely that a user local scope spans just 1B from the start of the method. 
                // Assume therefore that [0,0] means a synthesized local.
                if (startOffset != 0 || endOffset != 0)
                {
                    endOffset++;
                }
 
                builder.Add(new StateMachineHoistedLocalScope(startOffset, endOffset));
            }
 
            return builder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// Indicates that this method is the iterator state machine for the method named in the record.
        /// </summary>
        /// <remarks>
        /// Appears on kick-off methods of a state machine.
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// 
        /// Encodes NULL-terminated UTF16 name of the state machine type.
        /// The ending NULL character might not be present if the PDB was generated by an older compiler.
        /// </remarks>
        /// <exception cref="InvalidOperationException">Bad data.</exception>
        public static string DecodeForwardIteratorRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
 
            var pooled = PooledStringBuilder.GetInstance();
            var builder = pooled.Builder;
            while (offset < bytes.Length)
            {
                var ch = (char)ReadInt16(bytes, ref offset);
                if (ch == 0)
                {
                    break;
                }
 
                builder.Append(ch);
            }
 
            return pooled.ToStringAndFree();
        }
 
        /// <summary>
        /// Does for locals what System.Runtime.CompilerServices.DynamicAttribute does for parameters, return types, and fields.
        /// In particular, indicates which occurrences of <see cref="object"/> in the signature are really dynamic.
        /// </summary>
        /// <remarks>
        /// Appears when there are dynamic locals.
        /// Exposed for <see cref="T:Roslyn.Test.PdbUtilities.PdbToXmlConverter"/>.
        /// </remarks>
        /// <exception cref="InvalidOperationException">Bad data.</exception>
        public static ImmutableArray<DynamicLocalInfo> DecodeDynamicLocalsRecord(ImmutableArray<byte> bytes)
        {
            const int FlagBytesCount = 64;
 
            var flagsBuilder = ArrayBuilder<bool>.GetInstance(FlagBytesCount);
            var pooledNameBuilder = PooledStringBuilder.GetInstance();
            var nameBuilder = pooledNameBuilder.Builder;
 
            var offset = 0;
            var bucketCount = ReadInt32(bytes, ref offset);
            var builder = ArrayBuilder<DynamicLocalInfo>.GetInstance(bucketCount);
 
            for (var i = 0; i < bucketCount; i++)
            {
                Debug.Assert(flagsBuilder.Count == 0);
                Debug.Assert(nameBuilder.Length == 0);
 
                for (var j = 0; j < FlagBytesCount; j++)
                {
                    flagsBuilder.Add(ReadByte(bytes, ref offset) != 0);
                }
 
                var flagCount = ReadInt32(bytes, ref offset);
                if (flagCount < flagsBuilder.Count)
                {
                    flagsBuilder.Count = flagCount;
                }
 
                var slotId = ReadInt32(bytes, ref offset);
 
                const int NameBytesCount = 128;
                var nameEnd = offset + NameBytesCount;
                while (offset < nameEnd)
                {
                    var ch = (char)ReadInt16(bytes, ref offset);
                    if (ch == 0)
                    {
                        // The Identifier name takes 64 WCHAR no matter how big its actual length is.
                        offset = nameEnd;
                        break;
                    }
 
                    nameBuilder.Append(ch);
                }
 
                builder.Add(new DynamicLocalInfo(flagsBuilder.ToImmutable(), slotId, nameBuilder.ToString()));
 
                flagsBuilder.Clear();
                nameBuilder.Clear();
            }
 
            flagsBuilder.Free();
            pooledNameBuilder.Free();
            return builder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// Tuple element names for locals.
        /// </summary>
        public static ImmutableArray<TupleElementNamesInfo> DecodeTupleElementNamesRecord(ImmutableArray<byte> bytes)
        {
            var offset = 0;
            var n = ReadInt32(bytes, ref offset);
            var builder = ArrayBuilder<TupleElementNamesInfo>.GetInstance(n);
            for (var i = 0; i < n; i++)
            {
                builder.Add(DecodeTupleElementNamesInfo(bytes, ref offset));
            }
 
            return builder.ToImmutableAndFree();
        }
 
        private static TupleElementNamesInfo DecodeTupleElementNamesInfo(ImmutableArray<byte> bytes, ref int offset)
        {
            var n = ReadInt32(bytes, ref offset);
            var builder = ArrayBuilder<string>.GetInstance(n);
            for (var i = 0; i < n; i++)
            {
                var value = ReadUtf8String(bytes, ref offset);
                builder.Add(string.IsNullOrEmpty(value) ? null : value);
            }
 
            var slotIndex = ReadInt32(bytes, ref offset);
            var scopeStart = ReadInt32(bytes, ref offset);
            var scopeEnd = ReadInt32(bytes, ref offset);
            var localName = ReadUtf8String(bytes, ref offset);
            return new TupleElementNamesInfo(builder.ToImmutableAndFree(), slotIndex, localName, scopeStart, scopeEnd);
        }
 
        /// <summary>
        /// Get the import strings for a given method, following forward pointers as necessary.
        /// </summary>
        /// <returns>
        /// For each namespace enclosing the method, a list of import strings, innermost to outermost.
        /// There should always be at least one entry, for the global namespace.
        /// </returns>
        public static ImmutableArray<ImmutableArray<string>> GetCSharpGroupedImportStrings<TArg>(
            int methodToken,
            TArg arg,
            Func<int, TArg, byte[]> getMethodCustomDebugInfo,
            Func<int, TArg, ImmutableArray<string>> getMethodImportStrings,
            out ImmutableArray<string> externAliasStrings)
        {
            externAliasStrings = default;
 
            ImmutableArray<short> groupSizes = default;
            var seenForward = false;
 
RETRY:
            var bytes = getMethodCustomDebugInfo(methodToken, arg);
            if (bytes == null)
            {
                return default;
            }
 
            foreach (var record in GetCustomDebugInfoRecords(bytes))
            {
                switch (record.Kind)
                {
                    case CustomDebugInfoKind.UsingGroups:
                        if (!groupSizes.IsDefault)
                        {
                            throw new InvalidOperationException(string.Format("Expected at most one Using record for method {0}", FormatMethodToken(methodToken)));
                        }
 
                        groupSizes = DecodeUsingRecord(record.Data);
                        break;
 
                    case CustomDebugInfoKind.ForwardMethodInfo:
                        if (!externAliasStrings.IsDefault)
                        {
                            throw new InvalidOperationException(string.Format("Did not expect both Forward and ForwardToModule records for method {0}", FormatMethodToken(methodToken)));
                        }
 
                        methodToken = DecodeForwardRecord(record.Data);
 
                        // Follow at most one forward link (as in FUNCBRECEE::ensureNamespaces).
                        // NOTE: Dev11 may produce chains of forward links (e.g. for System.Collections.Immutable).
                        if (!seenForward)
                        {
                            seenForward = true;
                            goto RETRY;
                        }
 
                        break;
 
                    case CustomDebugInfoKind.ForwardModuleInfo:
                        if (!externAliasStrings.IsDefault)
                        {
                            throw new InvalidOperationException(string.Format("Expected at most one ForwardToModule record for method {0}", FormatMethodToken(methodToken)));
                        }
 
                        var moduleInfoMethodToken = DecodeForwardToModuleRecord(record.Data);
 
                        var allModuleInfoImportStrings = getMethodImportStrings(moduleInfoMethodToken, arg);
                        Debug.Assert(!allModuleInfoImportStrings.IsDefault);
 
                        var externAliasBuilder = ArrayBuilder<string>.GetInstance();
 
                        foreach (var importString in allModuleInfoImportStrings)
                        {
                            if (IsCSharpExternAliasInfo(importString))
                            {
                                externAliasBuilder.Add(importString);
                            }
                        }
 
                        externAliasStrings = externAliasBuilder.ToImmutableAndFree();
                        break;
                }
            }
 
            if (groupSizes.IsDefault)
            {
                // This can happen in malformed PDBs (e.g. chains of forwards).
                return default;
            }
 
            var importStrings = getMethodImportStrings(methodToken, arg);
            Debug.Assert(!importStrings.IsDefault);
 
            var resultBuilder = ArrayBuilder<ImmutableArray<string>>.GetInstance(groupSizes.Length);
            var groupBuilder = ArrayBuilder<string>.GetInstance();
            var pos = 0;
 
            foreach (var groupSize in groupSizes)
            {
                for (var i = 0; i < groupSize; i++, pos++)
                {
                    if (pos >= importStrings.Length)
                    {
                        throw new InvalidOperationException(string.Format("Group size indicates more imports than there are import strings (method {0}).", FormatMethodToken(methodToken)));
                    }
 
                    var importString = importStrings[pos];
                    if (IsCSharpExternAliasInfo(importString))
                    {
                        throw new InvalidOperationException(string.Format("Encountered extern alias info before all import strings were consumed (method {0}).", FormatMethodToken(methodToken)));
                    }
 
                    groupBuilder.Add(importString);
                }
 
                resultBuilder.Add(groupBuilder.ToImmutable());
                groupBuilder.Clear();
            }
 
            if (externAliasStrings.IsDefault)
            {
                Debug.Assert(groupBuilder.Count == 0);
 
                // Extern alias detail strings (prefix "Z") are not included in the group counts.
                for (; pos < importStrings.Length; pos++)
                {
                    var importString = importStrings[pos];
                    if (!IsCSharpExternAliasInfo(importString))
                    {
                        throw new InvalidOperationException(string.Format("Expected only extern alias info strings after consuming the indicated number of imports (method {0}).", FormatMethodToken(methodToken)));
                    }
 
                    groupBuilder.Add(importString);
                }
 
                externAliasStrings = groupBuilder.ToImmutableAndFree();
            }
            else
            {
                groupBuilder.Free();
 
                if (pos < importStrings.Length)
                {
                    throw new InvalidOperationException(string.Format("Group size indicates fewer imports than there are import strings (method {0}).", FormatMethodToken(methodToken)));
                }
            }
 
            return resultBuilder.ToImmutableAndFree();
        }
 
        /// <summary>
        /// Get the import strings for a given method, following forward pointers as necessary.
        /// </summary>
        /// <returns>
        /// A list of import strings.  There should always be at least one entry, for the global namespace.
        /// </returns>
        public static ImmutableArray<string> GetVisualBasicImportStrings<TArg>(
            int methodToken,
            TArg arg,
            Func<int, TArg, ImmutableArray<string>> getMethodImportStrings)
        {
            var importStrings = getMethodImportStrings(methodToken, arg);
            Debug.Assert(!importStrings.IsDefault);
 
            if (importStrings.IsEmpty)
            {
                return ImmutableArray<string>.Empty;
            }
 
            // Follow at most one forward link.
            // As in PdbUtil::GetRawNamespaceListCore, we consider only the first string when
            // checking for forwarding.
            var importString = importStrings[0];
            if (importString.Length >= 2 && importString[0] == '@')
            {
                var ch1 = importString[1];
                if (ch1 is >= '0' and <= '9')
                {
                    if (int.TryParse(importString.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture, out var tempMethodToken))
                    {
                        importStrings = getMethodImportStrings(tempMethodToken, arg);
                        Debug.Assert(!importStrings.IsDefault);
                    }
                }
            }
 
            return importStrings;
        }
 
        private static int ReadInt32(ImmutableArray<byte> bytes, ref int offset)
        {
            var i = offset;
            if (i + sizeof(int) > bytes.Length)
            {
                throw new InvalidOperationException("Read out of buffer.");
            }
 
            offset += sizeof(int);
            return bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24);
        }
 
        private static short ReadInt16(ImmutableArray<byte> bytes, ref int offset)
        {
            var i = offset;
            if (i + sizeof(short) > bytes.Length)
            {
                throw new InvalidOperationException("Read out of buffer.");
            }
 
            offset += sizeof(short);
            return (short)(bytes[i] | (bytes[i + 1] << 8));
        }
 
        private static byte ReadByte(ImmutableArray<byte> bytes, ref int offset)
        {
            var i = offset;
            if (i + sizeof(byte) > bytes.Length)
            {
                throw new InvalidOperationException("Read out of buffer.");
            }
 
            offset += sizeof(byte);
            return bytes[i];
        }
 
        private static bool IsCSharpExternAliasInfo(string import)
        {
            return import.Length > 0 && import[0] == 'Z';
        }
 
        /// <summary>
        /// Parse a string representing a C# using (or extern alias) directive.
        /// </summary>
        /// <remarks>
        /// <![CDATA[
        /// For C#:
        ///  "USystem" -> <namespace name="System" />
        ///  "AS USystem" -> <alias name="S" target="System" kind="namespace" />
        ///  "AC TSystem.Console" -> <alias name="C" target="System.Console" kind="type" />
        ///  "AS ESystem alias" -> <alias name="S" qualifier="alias" target="System" kind="type" />
        ///  "XOldLib" -> <extern alias="OldLib" />
        ///  "ZOldLib assembly" -> <externinfo name="OldLib" assembly="assembly" />
        ///  "ESystem alias" -> <namespace qualifier="alias" name="System" />
        ///  "TSystem.Math" -> <type name="System.Math" />
        /// ]]>
        /// </remarks>
        public static bool TryParseCSharpImportString(string import, out string alias, out string externAlias, out string target, out ImportTargetKind kind)
        {
            alias = null;
            externAlias = null;
            target = null;
            kind = default;
 
            if (string.IsNullOrEmpty(import))
            {
                return false;
            }
 
            switch (import[0])
            {
                case 'U': // C# (namespace) using
                    alias = null;
                    externAlias = null;
                    target = import.Substring(1);
                    kind = ImportTargetKind.Namespace;
                    return true;
 
                case 'E': // C# (namespace) using
                    // NOTE: Dev12 has related cases "I" and "O" in EMITTER::ComputeDebugNamespace,
                    // but they were probably implementation details that do not affect Roslyn.
                    if (!TrySplit(import, 1, ' ', out target, out externAlias))
                    {
                        return false;
                    }
 
                    alias = null;
                    kind = ImportTargetKind.Namespace;
                    return true;
 
                case 'T': // C# (type) using
                    alias = null;
                    externAlias = null;
                    target = import.Substring(1);
                    kind = ImportTargetKind.Type;
                    return true;
 
                case 'A': // C# type or namespace alias
                    if (!TrySplit(import, 1, ' ', out alias, out target))
                    {
                        return false;
                    }
 
                    switch (target[0])
                    {
                        case 'U':
                            kind = ImportTargetKind.Namespace;
                            target = target.Substring(1);
                            externAlias = null;
                            return true;
 
                        case 'T':
                            kind = ImportTargetKind.Type;
                            target = target.Substring(1);
                            externAlias = null;
                            return true;
 
                        case 'E':
                            kind = ImportTargetKind.Namespace; // Never happens for types.
                            if (!TrySplit(target, 1, ' ', out target, out externAlias))
                            {
                                return false;
                            }
 
                            return true;
 
                        default:
                            return false;
                    }
 
                case 'X': // C# extern alias (in file)
                    externAlias = null;
                    alias = import.Substring(1); // For consistency with the portable format, store it in alias, rather than externAlias.
                    target = null;
                    kind = ImportTargetKind.Assembly;
                    return true;
 
                case 'Z': // C# extern alias (module-level)
                    // For consistency with the portable format, store it in alias, rather than externAlias.
                    if (!TrySplit(import, 1, ' ', out alias, out target))
                    {
                        return false;
                    }
 
                    externAlias = null;
                    kind = ImportTargetKind.Assembly;
                    return true;
 
                default:
                    return false;
            }
        }
 
        /// <summary>
        /// Parse a string representing a VB import statement.
        /// </summary>
        /// <exception cref="ArgumentNullException"><paramref name="import"/> is null.</exception>
        /// <exception cref="ArgumentException">Format of <paramref name="import"/> is not valid.</exception>
        public static bool TryParseVisualBasicImportString(string import, out string alias, out string target, out ImportTargetKind kind, out VBImportScopeKind scope)
        {
            alias = null;
            target = null;
            kind = default;
            scope = default;
 
            if (import == null)
            {
                return false;
            }
 
            // VB current namespace
            if (import.Length == 0)
            {
                alias = null;
                target = import;
                kind = ImportTargetKind.CurrentNamespace;
                scope = VBImportScopeKind.Unspecified;
                return true;
            }
 
            var pos = 0;
            switch (import[pos])
            {
                case '&':
                // Indicates the presence of embedded PIA types from a given assembly.  No longer required (as of Roslyn).
                case '$':
                case '#':
                    // From ProcedureContext::LoadImportsAndDefaultNamespaceNormal:
                    //   "Module Imports and extension types are no longer needed since we are not doing custom name lookup"
                    alias = null;
                    target = import;
                    kind = ImportTargetKind.Defunct;
                    scope = VBImportScopeKind.Unspecified;
                    return true;
                case '*': // VB default namespace
                    // see PEBuilder.cpp in vb\language\CodeGen
                    pos++;
                    alias = null;
                    target = import.Substring(pos);
                    kind = ImportTargetKind.DefaultNamespace;
                    scope = VBImportScopeKind.Unspecified;
                    return true;
                case '@': // VB cases other than default and current namespace
                    // see PEBuilder.cpp in vb\language\CodeGen
                    pos++;
                    if (pos >= import.Length)
                    {
                        return false;
                    }
 
                    scope = VBImportScopeKind.Unspecified;
                    switch (import[pos])
                    {
                        case 'F':
                            scope = VBImportScopeKind.File;
                            pos++;
                            break;
                        case 'P':
                            scope = VBImportScopeKind.Project;
                            pos++;
                            break;
                    }
 
                    if (pos >= import.Length)
                    {
                        return false;
                    }
 
                    switch (import[pos])
                    {
                        case 'A':
                            pos++;
 
                            if (import[pos] != ':')
                            {
                                return false;
                            }
 
                            pos++;
 
                            if (!TrySplit(import, pos, '=', out alias, out target))
                            {
                                return false;
                            }
 
                            kind = ImportTargetKind.NamespaceOrType;
                            return true;
 
                        case 'X':
                            pos++;
 
                            if (import[pos] != ':')
                            {
                                return false;
                            }
 
                            pos++;
 
                            if (!TrySplit(import, pos, '=', out alias, out target))
                            {
                                return false;
                            }
 
                            kind = ImportTargetKind.XmlNamespace;
                            return true;
 
                        case 'T':
                            pos++;
 
                            if (import[pos] != ':')
                            {
                                return false;
                            }
 
                            pos++;
 
                            alias = null;
                            target = import.Substring(pos);
                            kind = ImportTargetKind.Type;
                            return true;
 
                        case ':':
                            pos++;
                            alias = null;
                            target = import.Substring(pos);
                            kind = ImportTargetKind.Namespace;
                            return true;
 
                        default:
                            alias = null;
                            target = import.Substring(pos);
                            kind = ImportTargetKind.MethodToken;
                            return true;
                    }
 
                default:
                    // VB current namespace
                    alias = null;
                    target = import;
                    kind = ImportTargetKind.CurrentNamespace;
                    scope = VBImportScopeKind.Unspecified;
                    return true;
            }
        }
 
        private static bool TrySplit(string input, int offset, char separator, out string before, out string after)
        {
            var separatorPos = input.IndexOf(separator, offset);
 
            // Allow zero-length before for the global namespace (empty string).
            // Allow zero-length after for an XML alias in VB ("@PX:=").  Not sure what it means.
            if (offset <= separatorPos && separatorPos < input.Length)
            {
                before = input.Substring(offset, separatorPos - offset);
                after = separatorPos + 1 == input.Length
                    ? ""
                    : input.Substring(separatorPos + 1);
                return true;
            }
 
            before = null;
            after = null;
            return false;
        }
 
        private static string FormatMethodToken(int methodToken)
        {
            return string.Format("0x{0:x8}", methodToken);
        }
 
        /// <summary>
        /// Read UTF-8 string with null terminator.
        /// </summary>
        private static string ReadUtf8String(ImmutableArray<byte> bytes, ref int offset)
        {
            var builder = ArrayBuilder<byte>.GetInstance();
            while (true)
            {
                var b = ReadByte(bytes, ref offset);
                if (b == 0)
                {
                    break;
                }
 
                builder.Add(b);
            }
 
            var block = builder.ToArrayAndFree();
            return Encoding.UTF8.GetString(block, 0, block.Length);
        }
    }
}