diff --git a/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs b/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs index 55d2f9299..293562f36 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs @@ -65,22 +65,7 @@ public SerializedStringsStream(string name, in BinaryStreamReader reader) if (!_cachedStrings.TryGetValue(index, out var value) && index < _reader.Length) { var stringsReader = _reader.ForkRelative(index); - byte[] rawData = stringsReader.ReadBytesUntil(0); - - if (rawData.Length == 0) - { - value = Utf8String.Empty; - } - else - { - // Trim off null terminator byte if its present. - int actualLength = rawData.Length; - if (rawData[actualLength - 1] == 0) - actualLength--; - - value = new Utf8String(rawData, 0, actualLength); - } - + value = stringsReader.ReadUtf8String(); _cachedStrings[index] = value; } diff --git a/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj index b0b3df765..168f407cc 100644 --- a/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj +++ b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj @@ -21,6 +21,7 @@ + diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs new file mode 100644 index 000000000..3fb37697a --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs @@ -0,0 +1,30 @@ +using System; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all attributes that can be assigned to a single DBI stream. +/// +[Flags] +public enum DbiAttributes : ushort +{ + /// + /// Indicates no attributes were assigned. + /// + None = 0, + + /// + /// Indicates the program was linked in an incremental manner. + /// + IncrementallyLinked = 1, + + /// + /// Indicates private symbols were stripped from the PDB file. + /// + PrivateSymbolsStripped = 2, + + /// + /// Indicates the program was linked using link.exe with the undocumented /DEBUG:CTYPES flag. + /// + HasConflictingTypes = 4, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs new file mode 100644 index 000000000..247bd21ac --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs @@ -0,0 +1,595 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents the DBI Stream (also known as the Debug Information stream). +/// +public class DbiStream : SegmentBase +{ + /// + /// Gets the default fixed MSF stream index for the DBI stream. + /// + public const int StreamIndex = 3; + + private IList? _modules; + private IList? _sectionContributions; + private IList? _sectionMaps; + private readonly LazyVariable _typeServerMapStream; + private readonly LazyVariable _ecStream; + private IList? _sourceFiles; + private IList? _extraStreamIndices; + + /// + /// Creates a new empty DBI stream. + /// + public DbiStream() + { + _typeServerMapStream = new LazyVariable(GetTypeServerMapStream); + _ecStream = new LazyVariable(GetECStream); + IsNewVersionFormat = true; + } + + /// + /// Gets or sets the version signature assigned to the DBI stream. + /// + /// + /// This value should always be -1 for valid PDB files. + /// + public int VersionSignature + { + get; + set; + } = -1; + + /// + /// Gets or sets the version number of the DBI header. + /// + /// + /// Modern tooling only recognize the VC7.0 file format. + /// + public DbiStreamVersion VersionHeader + { + get; + set; + } = DbiStreamVersion.V70; + + /// + /// Gets or sets the number of times the DBI stream has been written. + /// + public uint Age + { + get; + set; + } = 1; + + /// + /// Gets or sets the MSF stream index of the Global Symbol Stream. + /// + public ushort GlobalStreamIndex + { + get; + set; + } + + /// + /// Gets or sets a bitfield containing the major and minor version of the toolchain that was used to build the program. + /// + public ushort BuildNumber + { + get; + set; + } + + /// + /// Gets or sets a value indicating that the DBI stream is using the new file format (NewDBI). + /// + public bool IsNewVersionFormat + { + get => (BuildNumber & 0x8000) != 0; + set => BuildNumber = (ushort) ((BuildNumber & ~0x8000) | (value ? 0x8000 : 0)); + } + + /// + /// Gets or sets the major version of the toolchain that was used to build the program. + /// + public byte BuildMajorVersion + { + get => (byte) ((BuildNumber >> 8) & 0x7F); + set => BuildNumber = (ushort) ((BuildNumber & ~0x7F00) | (value << 8)); + } + + /// + /// Gets or sets the minor version of the toolchain that was used to build the program. + /// + public byte BuildMinorVersion + { + get => (byte) (BuildNumber & 0xFF); + set => BuildNumber = (ushort) ((BuildNumber & ~0x00FF) | value); + } + + /// + /// Gets or sets the MSF stream index of the Public Symbol Stream. + /// + public ushort PublicStreamIndex + { + get; + set; + } + + /// + /// Gets or sets the version number of mspdbXXXX.dll that was used to produce this PDB file. + /// + public ushort PdbDllVersion + { + get; + set; + } + + /// + /// Gets or sets the MSF stream index of the Symbol Record Stream. + /// + public ushort SymbolRecordStreamIndex + { + get; + set; + } + + /// + /// Unknown. + /// + public ushort PdbDllRbld + { + get; + set; + } + + /// + /// Gets or sets the MSF stream index of the MFC type server. + /// + public uint MfcTypeServerIndex + { + get; + set; + } + + /// + /// Gets or sets attributes associated to the DBI stream. + /// + public DbiAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the machine type the program was compiled for. + /// + public MachineType Machine + { + get; + set; + } + + /// + /// Gets a collection of modules (object files) that were linked together into the program. + /// + public IList Modules + { + get + { + if (_modules is null) + Interlocked.CompareExchange(ref _modules, GetModules(), null); + return _modules; + } + } + + /// + /// Gets a collection of section contributions describing the layout of the sections of the final executable file. + /// + public IList SectionContributions + { + get + { + if (_sectionContributions is null) + Interlocked.CompareExchange(ref _sectionContributions, GetSectionContributions(), null); + return _sectionContributions; + } + } + + /// + /// Gets a collection of section mappings stored in the section mapping sub stream. + /// + /// + /// The exact purpose of this is unknown, but it seems to be always containing a copy of the sections in the final + /// executable file. + /// + public IList SectionMaps + { + get + { + if (_sectionMaps is null) + Interlocked.CompareExchange(ref _sectionMaps, GetSectionMaps(), null); + return _sectionMaps; + } + } + + /// + /// Gets or sets the contents of the type server map sub stream. + /// + /// + /// The exact purpose and layout of this sub stream is unknown, hence this property exposes the stream as + /// a raw segment. + /// + public ISegment? TypeServerMapStream + { + get => _typeServerMapStream.Value; + set => _typeServerMapStream.Value = value; + } + + /// + /// Gets or sets the contents of the Edit-and-Continue sub stream. + /// + /// + /// The exact purpose and layout of this sub stream is unknown, hence this property exposes the stream as + /// a raw segment. + /// + public ISegment? ECStream + { + get => _ecStream.Value; + set => _ecStream.Value = value; + } + + /// + /// Gets a collection of source files assigned to each module in . + /// + /// + /// Every collection of source files within this list corresponds to exactly the module within + /// at the same index. For example, the first source file list in this collection is the source file list of the + /// first module. + /// + public IList SourceFiles + { + get + { + if (_sourceFiles is null) + Interlocked.CompareExchange(ref _sourceFiles, GetSourceFiles(), null); + return _sourceFiles; + } + } + + /// + /// Gets a collection of indices referring to additional debug streams in the MSF file. + /// + public IList ExtraStreamIndices + { + get + { + if (_extraStreamIndices is null) + Interlocked.CompareExchange(ref _extraStreamIndices, GetExtraStreamIndices(), null); + return _extraStreamIndices; + } + } + + /// + /// Reads a single DBI stream from the provided input stream. + /// + /// The input stream. + /// The parsed DBI stream. + public static DbiStream FromReader(BinaryStreamReader reader) => new SerializedDbiStream(reader); + + /// + /// Obtains the list of module descriptors. + /// + /// The module descriptors + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetModules() => new List(); + + /// + /// Obtains the list of section contributions. + /// + /// The section contributions. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSectionContributions() => new List(); + + /// + /// Obtains the list of section maps. + /// + /// The section maps. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSectionMaps() => new List(); + + /// + /// Obtains the contents of the type server map sub stream. + /// + /// The contents of the sub stream. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetTypeServerMapStream() => null; + + /// + /// Obtains the contents of the EC sub stream. + /// + /// The contents of the sub stream. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetECStream() => null; + + /// + /// Obtains a table that assigns a list of source files to every module referenced in the PDB file. + /// + /// The table. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSourceFiles() => new List(); + + /// + /// Obtains the list of indices referring to additional debug streams in the MSF file. + /// + /// The list of indices. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetExtraStreamIndices() => new List(); + + /// + public override uint GetPhysicalSize() + { + return GetHeaderSize() + + GetModuleStreamSize() + + GetSectionContributionStreamSize() + + GetSectionMapStreamSize() + + GetSourceInfoStreamSize() + + GetTypeServerMapStreamSize() + + GetECStreamSize() + + GetOptionalDebugStreamSize() + ; + } + + private static uint GetHeaderSize() + { + return sizeof(int) // VersionSignature + + sizeof(DbiStreamVersion) // VersionHeader + + sizeof(uint) // Age + + sizeof(ushort) // GlobalStreamIndex + + sizeof(ushort) // BuildNumber + + sizeof(ushort) // PublicStreamIndex + + sizeof(ushort) // PdbDllVersion + + sizeof(ushort) // SymbolRecordStreamIndex + + sizeof(ushort) // PdbDllRbld + + sizeof(uint) // ModuleInfoSize + + sizeof(uint) // SectionContributionSize + + sizeof(uint) // SectionMapSize + + sizeof(uint) // SourceInfoSize + + sizeof(uint) // TypeServerMapSize + + sizeof(uint) // MfcTypeServerIndex + + sizeof(uint) // OptionalDebugStreamSize + + sizeof(uint) // ECStreamSize + + sizeof(DbiAttributes) // Attributes + + sizeof(MachineType) // MachineType + + sizeof(uint) // Padding + ; + } + + private uint GetModuleStreamSize() + { + return ((uint) Modules.Sum(m => m.GetPhysicalSize())).Align(sizeof(uint)); + } + + private uint GetSectionContributionStreamSize() + { + return sizeof(uint) // version + + SectionContribution.EntrySize * (uint) SectionContributions.Count; + } + + private uint GetSectionMapStreamSize() + { + return sizeof(ushort) // Count + + sizeof(ushort) // LogCount + + SectionMap.EntrySize * (uint) SectionMaps.Count; + } + + private uint GetSourceInfoStreamSize() + { + uint totalFileCount = 0; + uint nameBufferSize = 0; + var stringOffsets = new Dictionary(); + + // Simulate the construction of name buffer + for (int i = 0; i < SourceFiles.Count; i++) + { + var collection = SourceFiles[i]; + totalFileCount += (uint) collection.Count; + + for (int j = 0; j < collection.Count; j++) + { + // If name is not added yet, "append" to the name buffer. + var name = collection[j]; + if (!stringOffsets.ContainsKey(name)) + { + stringOffsets[name] = nameBufferSize; + nameBufferSize += (uint) name.ByteCount + 1u; + } + } + } + + return (sizeof(ushort) // ModuleCount + + sizeof(ushort) // SourceFileCount + + sizeof(ushort) * (uint) SourceFiles.Count // ModuleIndices + + sizeof(ushort) * (uint) SourceFiles.Count // SourceFileCounts + + sizeof(uint) * totalFileCount // NameOffsets + + nameBufferSize // NameBuffer + ).Align(4); + } + + private uint GetTypeServerMapStreamSize() + { + return TypeServerMapStream?.GetPhysicalSize().Align(sizeof(uint)) ?? 0u; + } + + private uint GetOptionalDebugStreamSize() + { + return (uint) (ExtraStreamIndices.Count * sizeof(ushort)); + } + + private uint GetECStreamSize() + { + return ECStream?.GetPhysicalSize() ?? 0u; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + WriteHeader(writer); + WriteModuleStream(writer); + WriteSectionContributionStream(writer); + WriteSectionMapStream(writer); + WriteSourceInfoStream(writer); + WriteTypeServerMapStream(writer); + WriteECStream(writer); + WriteOptionalDebugStream(writer); + } + + private void WriteHeader(IBinaryStreamWriter writer) + { + writer.WriteInt32(VersionSignature); + writer.WriteUInt32((uint) VersionHeader); + writer.WriteUInt32(Age); + writer.WriteUInt16(GlobalStreamIndex); + writer.WriteUInt16(BuildNumber); + writer.WriteUInt16(PublicStreamIndex); + writer.WriteUInt16(PdbDllVersion); + writer.WriteUInt16(SymbolRecordStreamIndex); + writer.WriteUInt16(PdbDllRbld); + + writer.WriteUInt32(GetModuleStreamSize()); + writer.WriteUInt32(GetSectionContributionStreamSize()); + writer.WriteUInt32(GetSectionMapStreamSize()); + writer.WriteUInt32(GetSourceInfoStreamSize()); + writer.WriteUInt32(GetTypeServerMapStreamSize()); + + writer.WriteUInt32(MfcTypeServerIndex); + + writer.WriteUInt32(GetOptionalDebugStreamSize()); + writer.WriteUInt32(GetECStreamSize()); + + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16((ushort) Machine); + + writer.WriteUInt32(0); + } + + private void WriteModuleStream(IBinaryStreamWriter writer) + { + var modules = Modules; + for (int i = 0; i < modules.Count; i++) + modules[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSectionContributionStream(IBinaryStreamWriter writer) + { + // TODO: make customizable + writer.WriteUInt32((uint) SectionContributionStreamVersion.Ver60); + + var contributions = SectionContributions; + for (int i = 0; i < contributions.Count; i++) + contributions[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSectionMapStream(IBinaryStreamWriter writer) + { + var maps = SectionMaps; + + // Count and LogCount. + writer.WriteUInt16((ushort) maps.Count); + writer.WriteUInt16((ushort) maps.Count); + + // Entries. + for (int i = 0; i < maps.Count; i++) + maps[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSourceInfoStream(IBinaryStreamWriter writer) + { + var sourceFiles = SourceFiles; + int totalFileCount = sourceFiles.Sum(x => x.Count); + + // Module and total file count (truncated to 16 bits) + writer.WriteUInt16((ushort) (sourceFiles.Count & 0xFFFF)); + writer.WriteUInt16((ushort) (totalFileCount & 0xFFFF)); + + // Module indices. Unsure if this is correct, but this array does not seem to be really used by the ref impl. + for (ushort i = 0; i < sourceFiles.Count; i++) + writer.WriteUInt16(i); + + // Source file counts. + for (int i = 0; i < sourceFiles.Count; i++) + writer.WriteUInt16((ushort) sourceFiles[i].Count); + + // Build up string buffer and name offset table. + using var stringBuffer = new MemoryStream(); + var stringWriter = new BinaryStreamWriter(stringBuffer); + var stringOffsets = new Dictionary(); + + for (int i = 0; i < sourceFiles.Count; i++) + { + var collection = sourceFiles[i]; + for (int j = 0; j < collection.Count; j++) + { + // If not present already, append to string buffer. + var name = collection[j]; + if (!stringOffsets.TryGetValue(name, out uint offset)) + { + offset = (uint) stringWriter.Offset; + stringOffsets[name] = offset; + stringWriter.WriteBytes(name.GetBytesUnsafe()); + stringWriter.WriteByte(0); + } + + // Write name offset + writer.WriteUInt32(offset); + } + } + + // Write string buffer. + writer.WriteBytes(stringBuffer.ToArray()); + + writer.Align(sizeof(uint)); + } + + private void WriteTypeServerMapStream(IBinaryStreamWriter writer) + { + TypeServerMapStream?.Write(writer); + writer.Align(sizeof(uint)); + } + + private void WriteOptionalDebugStream(IBinaryStreamWriter writer) + { + var extraIndices = ExtraStreamIndices; + + for (int i = 0; i < extraIndices.Count; i++) + writer.WriteUInt16(extraIndices[i]); + } + + private void WriteECStream(IBinaryStreamWriter writer) => ECStream?.Write(writer); +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs new file mode 100644 index 000000000..fe76de860 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs @@ -0,0 +1,15 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all possible DBI stream format version numbers. +/// +public enum DbiStreamVersion +{ +#pragma warning disable CS1591 + VC41 = 930803, + V50 = 19960307, + V60 = 19970606, + V70 = 19990903, + V110 = 20091201 +#pragma warning restore CS1591 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs new file mode 100644 index 000000000..174ec2dab --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs @@ -0,0 +1,207 @@ +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a reference to a single module (object file) that was linked into a program. +/// +public class ModuleDescriptor : IWritable +{ + /// + /// Gets or sets a description of the section within the final binary which contains code + /// and/or data from this module. + /// + public SectionContribution SectionContribution + { + get; + set; + } = new(); + + /// + /// Gets or sets the attributes assigned to this module descriptor. + /// + public ModuleDescriptorAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the index of the type server for this module. + /// + public ushort TypeServerIndex + { + get => (ushort) ((ushort) Attributes >> 8); + set => Attributes = (Attributes & ~ModuleDescriptorAttributes.TsmMask) | (ModuleDescriptorAttributes) (value << 8); + } + + /// + /// Gets or sets the MSF stream index of the stream that the symbols of this module. + /// + public ushort SymbolStreamIndex + { + get; + set; + } + + /// + /// Gets or sets the size of the CodeView data within the module's symbol stream. + /// + public uint SymbolDataSize + { + get; + set; + } + + /// + /// Gets or sets the size of the C11-style CodeView data within the module's symbol stream. + /// + public uint SymbolC11DataSize + { + get; + set; + } + + /// + /// Gets or sets the size of the C13-style CodeView data within the module's symbol stream. + /// + public uint SymbolC13DataSize + { + get; + set; + } + + /// + /// Gets or sets the number of source files that contributed to this module during the compilation. + /// + public ushort SourceFileCount + { + get; + set; + } + + /// + /// Gets or sets the offset in the names buffer of the primary translation unit. + /// + /// + /// For most compilers this value is set to zero. + /// + public uint SourceFileNameIndex + { + get; + set; + } + + /// + /// Gets or sets the offset in the names buffer of the PDB file. + /// + /// + /// For most modules (except the special * LINKER * module) this value is set to zero. + /// + public uint PdbFilePathNameIndex + { + get; + set; + } + + /// + /// Gets or sets the name of the module. + /// + /// + /// This is often a full path to the object file that was passed into link.exe directly, or a string in the + /// form of Import:dll_name + /// + public Utf8String? ModuleName + { + get; + set; + } + + /// + /// Gets or sets the name of the object file name. + /// + /// + /// In the case this module is linked directly passed to link.exe, this is the same as . + /// If the module comes from an archive, this is the full path to that archive. + /// + public Utf8String? ObjectFileName + { + get; + set; + } + + /// + /// Parses a single module descriptor from the provided input stream. + /// + /// The input stream. + /// THe parsed module descriptor. + public static ModuleDescriptor FromReader(ref BinaryStreamReader reader) + { + var result = new ModuleDescriptor(); + + reader.ReadUInt32(); + result.SectionContribution = SectionContribution.FromReader(ref reader); + result.Attributes = (ModuleDescriptorAttributes) reader.ReadUInt16(); + result.SymbolStreamIndex = reader.ReadUInt16(); + result.SymbolDataSize = reader.ReadUInt32(); + result.SymbolC11DataSize = reader.ReadUInt32(); + result.SymbolC13DataSize = reader.ReadUInt32(); + result.SourceFileCount = reader.ReadUInt16(); + reader.ReadUInt16(); + reader.ReadUInt32(); + result.SourceFileNameIndex = reader.ReadUInt32(); + result.PdbFilePathNameIndex = reader.ReadUInt32(); + result.ModuleName = reader.ReadUtf8String(); + result.ObjectFileName = reader.ReadUtf8String(); + reader.Align(4); + + return result; + } + + /// + public uint GetPhysicalSize() + { + return (sizeof(uint) // Unused1 + + SectionContribution.GetPhysicalSize() // SectionContribution + + sizeof(ModuleDescriptorAttributes) // Attributes + + sizeof(ushort) // SymbolStreamIndex + + sizeof(uint) // SymbolDataSize + + sizeof(uint) // SymbolC11DataSize + + sizeof(uint) // SymbolC13DataSize + + sizeof(ushort) // SourceFileCount + + sizeof(ushort) // Padding + + sizeof(uint) // Unused2 + + sizeof(uint) // SourceFileNameIndex + + sizeof(uint) // PdbFilePathNameIndex + + (uint) (ModuleName?.ByteCount ?? 0) + 1 // ModuleName + + (uint) (ObjectFileName?.ByteCount ?? 0) + 1 // ObjectFileName + ).Align(4); + } + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt32(0); + SectionContribution.Write(writer); + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16(SymbolStreamIndex); + writer.WriteUInt32(SymbolDataSize); + writer.WriteUInt32(SymbolC11DataSize); + writer.WriteUInt32(SymbolC13DataSize); + writer.WriteUInt16(SourceFileCount); + writer.WriteUInt16(0); + writer.WriteUInt32(0); + writer.WriteUInt32(SourceFileNameIndex); + writer.WriteUInt32(PdbFilePathNameIndex); + if (ModuleName is not null) + writer.WriteBytes(ModuleName.GetBytesUnsafe()); + writer.WriteByte(0); + if (ObjectFileName is not null) + writer.WriteBytes(ObjectFileName.GetBytesUnsafe()); + writer.WriteByte(0); + writer.Align(4); + } + + /// + public override string ToString() => ModuleName ?? ObjectFileName ?? "?"; +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs new file mode 100644 index 000000000..5b6a018c0 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs @@ -0,0 +1,25 @@ +using System; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Defines all possible flags that can be assigned to a module descriptor. +/// +[Flags] +public enum ModuleDescriptorAttributes : ushort +{ + /// + /// Indicates the module has been written to since reading the PDB. + /// + Dirty = 1, + + /// + /// Indicates the module contains Edit & Continue information. + /// + EC = 2, + + /// + /// Provides a mask for the type server index that is stored within the flags. + /// + TsmMask = 0xFF00, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs new file mode 100644 index 000000000..83daaae3e --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs @@ -0,0 +1,128 @@ +using System.Text; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Describes the section in the final executable file that a particular object or module is stored at. +/// +public class SectionContribution : IWritable +{ + /// + /// The total size in bytes of a single on the disk. + /// + public const int EntrySize = + sizeof(ushort) // Section + + sizeof(ushort) // Padding1 + + sizeof(uint) // Offset + + sizeof(uint) // Size + + sizeof(uint) // Characteristics + + sizeof(ushort) // ModuleIndex + + sizeof(ushort) // Padding2 + + sizeof(uint) // DataCrc + + sizeof(uint) // RelocCrc + ; + + /// + /// Gets or sets the index of the section. + /// + public ushort Section + { + get; + set; + } + + /// + /// Gets or sets the offset within the section that this contribution starts at. + /// + public uint Offset + { + get; + set; + } + + /// + /// Gets or sets the size of the section contribution. + /// + public uint Size + { + get; + set; + } + + /// + /// Gets or sets the section flags and permissions associated to this section contribution. + /// + public SectionFlags Characteristics + { + get; + set; + } + + /// + /// Gets or sets the index of the module. + /// + public ushort ModuleIndex + { + get; + set; + } + + /// + /// Gets or sets a cyclic redundancy code that can be used to verify the data section of this contribution. + /// + public uint DataCrc + { + get; + set; + } + + /// + /// Gets or sets a cyclic redundancy code that can be used to verify the relocation section of this contribution. + /// + public uint RelocCrc + { + get; + set; + } + + /// + /// Parses a single section contribution from the provided input stream. + /// + /// The input stream. + /// The parsed section contribution. + public static SectionContribution FromReader(ref BinaryStreamReader reader) + { + var result = new SectionContribution(); + + result.Section = reader.ReadUInt16(); + reader.ReadUInt16(); + result.Offset = reader.ReadUInt32(); + result.Size = reader.ReadUInt32(); + result.Characteristics = (SectionFlags) reader.ReadUInt32(); + result.ModuleIndex = reader.ReadUInt16(); + reader.ReadUInt16(); + result.DataCrc = reader.ReadUInt32(); + result.RelocCrc = reader.ReadUInt32(); + + return result; + } + + /// + public uint GetPhysicalSize() => EntrySize; + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt16(Section); + writer.WriteUInt16(0); + writer.WriteUInt32(Offset); + writer.WriteUInt32(Size); + writer.WriteUInt32((uint) Characteristics); + writer.WriteUInt16(ModuleIndex); + writer.WriteUInt16(0); + writer.WriteUInt32(DataCrc); + writer.WriteUInt32(RelocCrc); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs new file mode 100644 index 000000000..48daaf3a4 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs @@ -0,0 +1,17 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all valid versions of the Section Contribution sub stream's file format. +/// +public enum SectionContributionStreamVersion : uint +{ + /// + /// Indicates version 6.0 is used. + /// + Ver60 = 0xeffe0000 + 19970605, + + /// + /// Indicates version 2.0 is used. + /// + V2 = 0xeffe0000 + 20140516 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs new file mode 100644 index 000000000..bf43a7e56 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs @@ -0,0 +1,131 @@ +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a single entry in the Section Map sub stream of the DBI stream. +/// +public class SectionMap : IWritable +{ + /// + /// The total size in bytes of a single on the disk. + /// + public const int EntrySize = + sizeof(ushort) // Attributes + + sizeof(ushort) // Ovl + + sizeof(ushort) // Group + + sizeof(ushort) // Frame + + sizeof(ushort) // SectionName + + sizeof(ushort) // ClassName + + sizeof(uint) // Offset + + sizeof(uint) // SectionLength + ; + + /// + /// Gets or sets the attributes assigned to this section map. + /// + public SectionMapAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the logical overlay number of this section map. + /// + public ushort LogicalOverlayNumber + { + get; + set; + } + + /// + /// Gets or sets the group index into the descriptor array. + /// + public ushort Group + { + get; + set; + } + + /// + /// Gets or sets the frame index. + /// + public ushort Frame + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the segment or group name in string table, or 0xFFFF if no name was assigned. + /// + public ushort SectionName + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the class in the string table, or 0xFFFF if no name was assigned.. + /// + public ushort ClassName + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the logical segment within physical segment. If group is set in flags, this is the offset of the group. + /// + public uint Offset + { + get; + set; + } + + /// + /// Gets or sets the number of bytes that the segment or group consists of. + /// + public uint SectionLength + { + get; + set; + } + + /// + /// Parses a single section map from the provided input stream. + /// + /// The input stream. + /// The parsed section map. + public static SectionMap FromReader(ref BinaryStreamReader reader) + { + return new SectionMap + { + Attributes = (SectionMapAttributes) reader.ReadUInt16(), + LogicalOverlayNumber = reader.ReadUInt16(), + Group = reader.ReadUInt16(), + Frame = reader.ReadUInt16(), + SectionName = reader.ReadUInt16(), + ClassName = reader.ReadUInt16(), + Offset = reader.ReadUInt32(), + SectionLength = reader.ReadUInt32() + }; + } + + /// + public uint GetPhysicalSize() => EntrySize; + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16(LogicalOverlayNumber); + writer.WriteUInt16(Group); + writer.WriteUInt16(Frame); + writer.WriteUInt16(SectionName); + writer.WriteUInt16(ClassName); + writer.WriteUInt32(Offset); + writer.WriteUInt32(SectionLength); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs new file mode 100644 index 000000000..7f6449772 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs @@ -0,0 +1,42 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members describing all possible attributes that can be assigned to a single section map entry. +/// +public enum SectionMapAttributes : ushort +{ + /// + /// Indicates the segment is readable. + /// + Read = 1 << 0, + + /// + /// Indicates the segment is writable. + /// + Write = 1 << 1, + + /// + /// Indicates the segment is executable. + /// + Execute = 1 << 2, + + /// + /// Indicates the descriptor describes a 32-bit linear address. + /// + AddressIs32Bit = 1 << 3, + + /// + /// Indicates the frame represents a selector. + /// + IsSelector = 1 << 8, + + /// + /// Indicates the frame represents an absolute address. + /// + IsAbsoluteAddress = 1 << 9, + + /// + /// Indicates the descriptor represents a group. + /// + IsGroup = 1 << 10 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs new file mode 100644 index 000000000..962f02fc7 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Implements a DBI stream that pulls its data from an input stream. +/// +public class SerializedDbiStream : DbiStream +{ + private readonly BinaryStreamReader _moduleInfoReader; + private readonly BinaryStreamReader _sectionContributionReader; + private readonly BinaryStreamReader _sectionMapReader; + private readonly BinaryStreamReader _sourceInfoReader; + private readonly BinaryStreamReader _typeServerMapReader; + private readonly BinaryStreamReader _optionalDebugHeaderReader; + private readonly BinaryStreamReader _ecReader; + + /// + /// Parses a DBI stream from an input stream reader. + /// + /// The input stream. + public SerializedDbiStream(BinaryStreamReader reader) + { + VersionSignature = reader.ReadInt32(); + VersionHeader = (DbiStreamVersion) reader.ReadUInt32(); + Age = reader.ReadUInt32(); + GlobalStreamIndex = reader.ReadUInt16(); + BuildNumber = reader.ReadUInt16(); + if (!IsNewVersionFormat) + throw new NotSupportedException("The DBI stream uses the legacy file format, which is not supported."); + + PublicStreamIndex = reader.ReadUInt16(); + PdbDllVersion = reader.ReadUInt16(); + SymbolRecordStreamIndex = reader.ReadUInt16(); + PdbDllRbld = reader.ReadUInt16(); + + uint moduleInfoSize = reader.ReadUInt32(); + uint sectionContributionSize = reader.ReadUInt32(); + uint sectionMapSize = reader.ReadUInt32(); + uint sourceInfoSize = reader.ReadUInt32(); + uint typeServerMapSize = reader.ReadUInt32(); + + MfcTypeServerIndex = reader.ReadUInt32(); + + uint optionalDebugHeaderSize = reader.ReadUInt32(); + uint ecSize = reader.ReadUInt32(); + + Attributes = (DbiAttributes) reader.ReadUInt16(); + Machine = (MachineType) reader.ReadUInt16(); + + _ = reader.ReadUInt32(); + + _moduleInfoReader = reader.ForkRelative(reader.RelativeOffset, moduleInfoSize); + reader.Offset += moduleInfoSize; + _sectionContributionReader = reader.ForkRelative(reader.RelativeOffset, sectionContributionSize); + reader.Offset += sectionContributionSize; + _sectionMapReader = reader.ForkRelative(reader.RelativeOffset, sectionMapSize); + reader.Offset += sectionMapSize; + _sourceInfoReader = reader.ForkRelative(reader.RelativeOffset, sourceInfoSize); + reader.Offset += sourceInfoSize; + _typeServerMapReader = reader.ForkRelative(reader.RelativeOffset, typeServerMapSize); + reader.Offset += typeServerMapSize; + _ecReader = reader.ForkRelative(reader.RelativeOffset, ecSize); + reader.Offset += ecSize; + _optionalDebugHeaderReader = reader.ForkRelative(reader.RelativeOffset, optionalDebugHeaderSize); + reader.Offset += optionalDebugHeaderSize; + } + + /// + protected override IList GetModules() + { + var result = new List(); + + var reader = _moduleInfoReader.Fork(); + while (reader.CanRead(1)) + result.Add(ModuleDescriptor.FromReader(ref reader)); + + return result; + } + + /// + protected override IList GetSectionContributions() + { + var result = new List(); + + var reader = _sectionContributionReader.Fork(); + var version = (SectionContributionStreamVersion) reader.ReadUInt32(); + + while (reader.CanRead(1)) + { + result.Add(SectionContribution.FromReader(ref reader)); + if (version == SectionContributionStreamVersion.V2) + reader.ReadUInt32(); + } + + return result; + } + + /// + protected override IList GetSectionMaps() + { + var reader = _sectionMapReader.Fork(); + + ushort count = reader.ReadUInt16(); + ushort logCount = reader.ReadUInt16(); + + var result = new List(count); + for (int i = 0; i < count; i++) + result.Add(SectionMap.FromReader(ref reader)); + + return result; + } + + /// + protected override ISegment? GetTypeServerMapStream() + { + var reader = _typeServerMapReader.Fork(); + return reader.Length != 0 + ? reader.ReadSegment(reader.Length) + : null; + } + + /// + protected override ISegment? GetECStream() + { + var reader = _ecReader.Fork(); + return reader.Length != 0 + ? reader.ReadSegment(reader.Length) + : null; + } + + /// + protected override IList GetSourceFiles() + { + var reader = _sourceInfoReader.Fork(); + + ushort moduleCount = reader.ReadUInt16(); + ushort sourceFileCount = reader.ReadUInt16(); + + // Read module indices. + ushort[] moduleIndices = new ushort[moduleCount]; + for (int i = 0; i < moduleCount; i++) + moduleIndices[i] = reader.ReadUInt16(); + + // Read module source file counts. + int actualFileCount = 0; + ushort[] moduleFileCounts = new ushort[moduleCount]; + for (int i = 0; i < moduleCount; i++) + { + ushort count = reader.ReadUInt16(); + moduleFileCounts[i] = count; + actualFileCount += count; + } + + // Scope on the name buffer. + var stringReaderBuffer = reader.ForkRelative((uint) (reader.RelativeOffset + actualFileCount * sizeof(uint))); + + // Construct source file lists. + var result = new List(moduleCount); + for (int i = 0; i < moduleCount; i++) + { + var files = new SourceFileCollection(moduleIndices[i]); + ushort fileCount = moduleFileCounts[i]; + + // Read all file paths for this module. + for (int j = 0; j < fileCount; j++) + { + uint nameOffset = reader.ReadUInt32(); + var nameReader = stringReaderBuffer.ForkRelative(nameOffset); + files.Add(nameReader.ReadUtf8String()); + } + + result.Add(files); + } + + return result; + } + + /// + protected override IList GetExtraStreamIndices() + { + var reader = _optionalDebugHeaderReader.Fork(); + + var result = new List((int) (reader.Length / sizeof(ushort))); + + while (reader.CanRead(sizeof(ushort))) + result.Add(reader.ReadUInt16()); + + return result; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs new file mode 100644 index 000000000..b7ef673e6 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a collection of file paths used as input source code for a single module. +/// +public class SourceFileCollection : List +{ + /// + /// Creates a new empty source file collection. + /// + public SourceFileCollection() + { + } + + /// + /// Creates a new empty source file collection. + /// + /// The original module index for which this collection was compiled. + public SourceFileCollection(uint originalModuleIndex) + { + OriginalModuleIndex = originalModuleIndex; + } + + /// + /// Gets the original module index for which this collection was compiled (if available). + /// + /// + /// The exact purpose of this number is unclear, as this number cannot be reliably used as an index within the + /// DBI stream's module list. Use the index of this list within instead. + /// + public uint OriginalModuleIndex + { + get; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs new file mode 100644 index 000000000..eae02174c --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Represents the PDB Info Stream (also known as the PDB stream) +/// +public class InfoStream : SegmentBase +{ + /// + /// Gets the default fixed MSF stream index for the PDB Info stream. + /// + public const int StreamIndex = 1; + + private const int HeaderSize = + sizeof(InfoStreamVersion) // Version + + sizeof(uint) // Signature + + sizeof(uint) // Aage + + 16 //UniqueId + + sizeof(uint) // NameBufferSize + ; + + private IDictionary? _streamIndices; + private IList? _features; + + /// + /// Gets or sets the version of the file format of the PDB info stream. + /// + /// + /// Modern tooling only recognize the VC7.0 file format. + /// + public InfoStreamVersion Version + { + get; + set; + } = InfoStreamVersion.VC70; + + /// + /// Gets or sets the 32-bit UNIX time-stamp of the PDB file. + /// + public uint Signature + { + get; + set; + } + + /// + /// Gets or sets the number of times the PDB file has been written. + /// + public uint Age + { + get; + set; + } = 1; + + /// + /// Gets or sets the unique identifier assigned to the PDB file. + /// + public Guid UniqueId + { + get; + set; + } + + /// + /// Gets a mapping from stream names to their respective stream index within the underlying MSF file. + /// + public IDictionary StreamIndices + { + get + { + if (_streamIndices is null) + Interlocked.CompareExchange(ref _streamIndices, GetStreamIndices(), null); + return _streamIndices; + } + } + + /// + /// Gets a list of characteristics that this PDB has. + /// + public IList Features + { + get + { + if (_features is null) + Interlocked.CompareExchange(ref _features, GetFeatures(), null); + return _features; + } + } + + /// + /// Reads a single PDB info stream from the provided input stream. + /// + /// The input stream. + /// The parsed info stream. + public static InfoStream FromReader(BinaryStreamReader reader) => new SerializedInfoStream(reader); + + /// + /// Obtains the stream name to index mapping of the PDB file. + /// + /// The mapping. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IDictionary GetStreamIndices() => new Dictionary(); + + /// + /// Obtains the features of the PDB file. + /// + /// The features. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetFeatures() => new List + { + PdbFeature.VC140 + }; + + /// + public override uint GetPhysicalSize() + { + uint totalSize = HeaderSize; + + // Name buffer + foreach (var entry in StreamIndices) + totalSize += (uint) entry.Key.ByteCount + 1u; + + // Stream indices hash table. + totalSize += StreamIndices.GetPdbHashTableSize(ComputeStringHash); + + // Last NI + totalSize += sizeof(uint); + + // Feature codes. + totalSize += (uint) Features.Count * sizeof(PdbFeature); + + return totalSize; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + // Write basic info stream header. + writer.WriteUInt32((uint) Version); + writer.WriteUInt32(Signature); + writer.WriteUInt32(Age); + writer.WriteBytes(UniqueId.ToByteArray()); + + // Construct name buffer, keeping track of the offsets of every name. + using var nameBuffer = new MemoryStream(); + var nameWriter = new BinaryStreamWriter(nameBuffer); + + var stringOffsets = new Dictionary(); + foreach (var entry in StreamIndices) + { + uint offset = (uint) nameWriter.Offset; + nameWriter.WriteBytes(entry.Key.GetBytesUnsafe()); + nameWriter.WriteByte(0); + stringOffsets.Add(entry.Key, offset); + } + + writer.WriteUInt32((uint) nameBuffer.Length); + writer.WriteBytes(nameBuffer.ToArray()); + + // Write the hash table. + StreamIndices.WriteAsPdbHashTable(writer, + ComputeStringHash, + (key, value) => (stringOffsets[key], (uint) value)); + + // last NI, safe to put always zero. + writer.WriteUInt32(0); + + // Write feature codes. + var features = Features; + for (int i = 0; i < features.Count; i++) + writer.WriteUInt32((uint) features[i]); + } + + private static uint ComputeStringHash(Utf8String str) + { + // Note: The hash of a single entry is **deliberately** truncated to a 16 bit number. This is because + // the reference implementation of the name table returns a number of type HASH, which is a typedef + // for "unsigned short". If we don't do this, this will result in wrong buckets being filled in the + // hash table, and thus the serialization would fail. See NMTNI::hash() in Microsoft/microsoft-pdb. + + return (ushort) PdbHash.ComputeV1(str); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs new file mode 100644 index 000000000..d377f5ba1 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs @@ -0,0 +1,20 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Provides members defining all possible stream file format versions that PDB defines. +/// +public enum InfoStreamVersion +{ +#pragma warning disable CS1591 + VC2 = 19941610, + VC4 = 19950623, + VC41 = 19950814, + VC50 = 19960307, + VC98 = 19970604, + VC70Dep = 19990604, + VC70 = 20000404, + VC80 = 20030901, + VC110 = 20091201, + VC140 = 20140508, +#pragma warning restore CS1591 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs new file mode 100644 index 000000000..16e5d3a66 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs @@ -0,0 +1,28 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Provides members defining all possible features that a PDB can have. +/// +public enum PdbFeature : uint +{ + /// + /// Indicates no other feature flags are present, and that an IPI stream is present. + /// + VC110 = 20091201, + + /// + /// Indicates that other feature flags may be present, and that an IPI stream is present. + /// + VC140 = 20140508, + + /// + /// Indicates types can be duplicated in the TPI stream. + /// + NoTypeMerge = 0x4D544F4E, + + /// + /// Indicates the program was linked with /DEBUG:FASTLINK, and all type information is contained in the original + /// object files instead of TPI and IPI streams. + /// + MinimalDebugInfo = 0x494E494D, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs new file mode 100644 index 000000000..f448ce67f --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Implements an PDB info stream that pulls its data from an input stream. +/// +public class SerializedInfoStream : InfoStream +{ + private readonly BinaryStreamReader _reader; + private ulong _featureOffset; + + /// + /// Parses a PDB info stream from an input stream reader. + /// + /// The input stream. + public SerializedInfoStream(BinaryStreamReader reader) + { + Version = (InfoStreamVersion) reader.ReadUInt32(); + Signature = reader.ReadUInt32(); + Age = reader.ReadUInt32(); + + byte[] guidBytes = new byte[16]; + reader.ReadBytes(guidBytes, 0, guidBytes.Length); + + UniqueId = new Guid(guidBytes); + + _reader = reader; + } + + /// + protected override IDictionary GetStreamIndices() + { + var reader = _reader.Fork(); + uint length = reader.ReadUInt32(); + + var stringsReader = reader.ForkRelative(reader.RelativeOffset, length); + var hashTableReader = reader.ForkRelative(reader.RelativeOffset + length); + + var result = PdbHashTable.FromReader(ref hashTableReader, (key, value) => + { + var stringReader = stringsReader.ForkRelative(key); + var keyString = stringReader.ReadUtf8String(); + return (keyString, (int) value); + }); + + hashTableReader.ReadUInt32(); // lastNi (unused). + + _featureOffset = hashTableReader.Offset; + return result; + } + + /// + protected override IList GetFeatures() + { + // We need to read the stream name->index mapping to be able to read the features list of the PDB. + _ = StreamIndices; + + var result = new List(); + + var reader = _reader.ForkAbsolute(_featureOffset); + while (reader.CanRead(sizeof(uint))) + result.Add((PdbFeature) reader.ReadUInt32()); + + return result; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs new file mode 100644 index 000000000..ed5a6a03b --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs @@ -0,0 +1,49 @@ +namespace AsmResolver.Symbols.Pdb.Metadata; + +/// +/// Provides methods for computing hash codes for a PDB hash table. +/// +public static class PdbHash +{ + /// + /// Computes the V1 hash code for a UTF-8 string. + /// + /// The string to compute the hash for. + /// The hash code. + /// + /// See PDB/include/misc.h for reference implementation. + /// + public static unsafe uint ComputeV1(Utf8String value) + { + uint result = 0; + + uint count = (uint) value.ByteCount; + + fixed (byte* ptr = value.GetBytesUnsafe()) + { + byte* p = ptr; + + while (count >= 4) + { + result ^= *(uint*) p; + count -= 4; + p += 4; + } + + if (count >= 2) + { + result ^= *(ushort*) p; + count -= 2; + p += 2; + } + + if (count == 1) + result ^= *p; + } + + result |= 0x20202020; + result ^= result >> 11; + + return result ^ (result >> 16); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs new file mode 100644 index 000000000..fb58f25be --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata; + +/// +/// Provides methods for serializing and deserializing dictionaries as PDB hash tables. +/// +public static class PdbHashTable +{ + // Reference implementation from PDB/include/map.h + // Specifically, Map::load, Map::find and Map::save. + + /// + /// Reads a single PDB hash table from the input stream and converts it into a dictionary. + /// + /// The input stream to read from. + /// A function that maps the raw key-value pairs into high level constructs. + /// The type of keys in the final dictionary. + /// The type of values in the final dictionary. + /// The reconstructed dictionary. + public static Dictionary FromReader( + ref BinaryStreamReader reader, + Func mapper) + where TKey : notnull + { + uint count = reader.ReadUInt32(); + reader.ReadUInt32(); // Capacity + + uint presentWordCount = reader.ReadUInt32(); + reader.RelativeOffset += presentWordCount * sizeof(uint); + + uint deletedWordCount = reader.ReadUInt32(); + reader.RelativeOffset += deletedWordCount * sizeof(uint); + + var result = new Dictionary((int) count); + for (int i = 0; i < count; i++) + { + (uint rawKey, uint rawValue) = (reader.ReadUInt32(), reader.ReadUInt32()); + var (key, value) = mapper(rawKey, rawValue); + result.Add(key, value); + } + + return result; + } + + /// + /// Computes the number of bytes required to store the provided dictionary as a PDB hash table. + /// + /// The dictionary to serialize. + /// A function that computes the hash code for a single key within the dictionary. + /// The type of keys in the input dictionary. + /// The type of values in the input dictionary. + /// The number of bytes required. + public static uint GetPdbHashTableSize( + this IDictionary dictionary, + Func hasher) + where TKey : notnull + { + var info = dictionary.ToPdbHashTable(hasher, null); + + return sizeof(uint) // Count + + sizeof(uint) // Capacity + + sizeof(uint) // Present bitvector word count + + info.PresentWordCount * sizeof(uint) // Present bitvector words + + sizeof(uint) // Deleted bitvector word count (== 0) + + (sizeof(uint) + sizeof(uint)) * (uint) dictionary.Count + ; + } + + /// + /// Serializes a dictionary to a PDB hash table to an output stream. + /// + /// The dictionary to serialize. + /// The output stream to write to. + /// A function that computes the hash code for a single key within the dictionary. + /// A function that maps every key-value pair to raw key-value uint32 pairs. + /// The type of keys in the input dictionary. + /// The type of values in the input dictionary. + public static void WriteAsPdbHashTable( + this IDictionary dictionary, + IBinaryStreamWriter writer, + Func hasher, + Func mapper) + where TKey : notnull + { + var hashTable = dictionary.ToPdbHashTable(hasher, mapper); + + // Write count and capacity. + writer.WriteInt32(dictionary.Count); + writer.WriteUInt32(hashTable.Capacity); + + // Determine which words in the present bitvector to write. + uint wordCount = (hashTable.Capacity + sizeof(uint) - 1) / sizeof(uint); + uint[] words = new uint[wordCount]; + hashTable.Present.CopyTo(words, 0); + while (wordCount > 0 && words[wordCount - 1] == 0) + wordCount--; + + // Write the present bitvector. + writer.WriteUInt32(wordCount); + for (int i = 0; i < wordCount; i++) + writer.WriteUInt32(words[i]); + + // Write deleted bitvector. We just always do 0 (i.e. no deleted buckets). + writer.WriteUInt32(0); + + // Write all buckets. + for (int i = 0; i < hashTable.Keys!.Length; i++) + { + if (hashTable.Present.Get(i)) + { + writer.WriteUInt32(hashTable.Keys![i]); + writer.WriteUInt32(hashTable.Values![i]); + } + } + } + + private static HashTableInfo ToPdbHashTable( + this IDictionary dictionary, + Func hasher, + Func? mapper) + where TKey : notnull + { + uint capacity = ComputeRequiredCapacity(dictionary.Count); + + // Avoid allocating buckets if we actually don't need to (e.g. if we're simply measuring the total size). + uint[]? keys; + uint[]? values; + + if (mapper is null) + { + keys = null; + values = null; + } + else + { + keys = new uint[capacity]; + values = new uint[capacity]; + } + + var present = new BitArray((int) capacity, false); + + // Fill in buckets. + foreach (var item in dictionary) + { + // Find empty bucket to place key-value pair in. + uint hash = hasher(item.Key); + uint index = hash % capacity; + while (present.Get((int) index)) + index = (index + 1) % capacity; + + // Mark bucket as used. + present.Set((int) index, true); + + // Store key-value pair. + if (mapper is not null) + { + (uint key, uint value) = mapper(item.Key, item.Value); + keys![index] = key; + values![index] = value; + } + } + + // Determine final word count in present bit vector. + uint wordCount = (capacity + sizeof(uint) - 1) / sizeof(uint); + uint[] words = new uint[wordCount]; + present.CopyTo(words, 0); + while (wordCount > 0 && words[wordCount - 1] == 0) + wordCount--; + + return new HashTableInfo(capacity, keys, values, present, wordCount); + } + + private static uint ComputeRequiredCapacity(int totalItemCount) + { + // "Simulate" adding all items to the hash table, effectively calculating the capacity of the map. + // TODO: This can probably be calculated with a single formula instead. + + uint capacity = 1; + for (int i = 0; i <= totalItemCount; i++) + { + // Reference implementation allows only 67% of the capacity to be used. + uint maxLoad = capacity * 2 / 3 + 1; + if (i >= maxLoad) + capacity = 2 * maxLoad; + } + + return capacity; + } + + private readonly struct HashTableInfo + { + public readonly uint Capacity; + public readonly uint[]? Keys; + public readonly uint[]? Values; + public readonly BitArray Present; + public readonly uint PresentWordCount; + + public HashTableInfo(uint capacity, uint[]? keys, uint[]? values, BitArray present, uint presentWordCount) + { + Capacity = capacity; + Keys = keys; + Values = values; + Present = present; + PresentWordCount = presentWordCount; + } + } + +} diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index 74088ce4b..d339ceab3 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -328,18 +328,45 @@ public byte[] ReadToEnd() /// /// The delimeter byte to stop at. /// The read bytes, including the delimeter if it was found. - public byte[] ReadBytesUntil(byte delimeter) - { + public byte[] ReadBytesUntil(byte delimeter) => ReadBytesUntil(delimeter, true); + + /// + /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// + /// The delimeter byte to stop at. + /// + /// true if the final delimeter should be included in the return value, false otherwise. + /// + /// The read bytes. + /// + /// This function always consumes the delimeter from the input stream if it is present, regardless of the value + /// of . + /// + public byte[] ReadBytesUntil(byte delimeter, bool includeDelimeterInReturn) + { + bool shouldReadExtra = false; + var lookahead = Fork(); while (lookahead.RelativeOffset < lookahead.Length) { byte b = lookahead.ReadByte(); if (b == delimeter) + { + if (!includeDelimeterInReturn) + { + lookahead.RelativeOffset--; + shouldReadExtra = true; + } break; + } } byte[] buffer = new byte[lookahead.RelativeOffset - RelativeOffset]; ReadBytes(buffer, 0, buffer.Length); + + if (shouldReadExtra) + ReadByte(); + return buffer; } @@ -347,17 +374,7 @@ public byte[] ReadBytesUntil(byte delimeter) /// Reads a null-terminated ASCII string from the input stream. /// /// The read ASCII string, excluding the null terminator. - public string ReadAsciiString() - { - byte[] data = ReadBytesUntil(0); - int length = data.Length; - - // Exclude trailing 0 byte. - if (data[data.Length - 1] == 0) - length--; - - return Encoding.ASCII.GetString(data, 0, length); - } + public string ReadAsciiString() => Encoding.ASCII.GetString(ReadBytesUntil(0, false)); /// /// Reads a zero-terminated Unicode string from the stream. @@ -378,6 +395,18 @@ public string ReadUnicodeString() return builder.ToString(); } + /// + /// Reads a null-terminated UTF-8 string from the input stream. + /// + /// The read UTF-8 string, excluding the null terminator. + public Utf8String ReadUtf8String() + { + byte[] data = ReadBytesUntil(0, false); + return data.Length != 0 + ? new Utf8String(data) + : Utf8String.Empty; + } + /// /// Reads either a 32-bit or a 64-bit number from the input stream. /// diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs new file mode 100644 index 000000000..889913627 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs @@ -0,0 +1,213 @@ +using System; +using System.IO; +using System.Linq; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; +using AsmResolver.Symbols.Pdb.Metadata.Dbi; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata.Dbi; + +public class DbiStreamTest +{ + private DbiStream GetDbiStream(bool rebuild) + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var dbiStream = DbiStream.FromReader(file.Streams[DbiStream.StreamIndex].CreateReader()); + + if (rebuild) + { + using var stream = new MemoryStream(); + dbiStream.Write(new BinaryStreamWriter(stream)); + dbiStream = DbiStream.FromReader(ByteArrayDataSource.CreateReader(stream.ToArray())); + } + + return dbiStream; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Header(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(1u, dbiStream.Age); + Assert.Equal(DbiAttributes.None, dbiStream.Attributes); + Assert.Equal(MachineType.I386, dbiStream.Machine); + Assert.Equal(14, dbiStream.BuildMajorVersion); + Assert.Equal(29, dbiStream.BuildMinorVersion); + Assert.True(dbiStream.IsNewVersionFormat); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleNames(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new[] + { + "* CIL *", + "C:\\Users\\Admin\\source\\repos\\AsmResolver\\test\\TestBinaries\\Native\\SimpleDll\\Release\\dllmain.obj", + "C:\\Users\\Admin\\source\\repos\\AsmResolver\\test\\TestBinaries\\Native\\SimpleDll\\Release\\pch.obj", + "* Linker Generated Manifest RES *", + "Import:KERNEL32.dll", + "KERNEL32.dll", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\sehprolg4.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_cookie.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_report.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_support.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\guard_support.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\loadcfg.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\dyn_tls_init.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\ucrt_detection.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\cpu_disp.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\chandler4gs.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\secchk.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\argv_mode.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\default_local_stdio_options.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\tncleanup.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\dll_dllmain.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\initializers.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\utility.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\ucrt_stubs.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\utility_desktop.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\initsect.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\x86_exception_filter.obj", + "VCRUNTIME140.dll", + "Import:VCRUNTIME140.dll", + "Import:api-ms-win-crt-runtime-l1-1-0.dll", + "api-ms-win-crt-runtime-l1-1-0.dll", + "* Linker *", + }, dbiStream.Modules.Select(m => m.ModuleName?.Value)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SectionContributions(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new (ushort, uint)[] + { + (1, 1669053862), (16, 2162654757), (20, 1635644926), (20, 3159649454), (20, 1649652954), (20, 3877379438), + (20, 4262788820), (20, 199934614), (8, 4235719287), (8, 1374843914), (9, 4241735292), (9, 2170796787), + (19, 1300950661), (19, 3968158929), (18, 3928463356), (18, 3928463356), (18, 2109213706), (22, 1457516325), + (22, 3939645857), (22, 1393694582), (22, 546064581), (22, 1976627334), (22, 513172946), (22, 25744891), + (22, 1989765812), (22, 2066266302), (22, 3810887196), (22, 206965504), (22, 647717352), (22, 3911072265), + (22, 3290064241), (12, 3928463356), (24, 2717331243), (24, 3687876222), (25, 2318145338), (25, 2318145338), + (6, 542071654), (15, 1810708069), (10, 3974941622), (14, 1150179208), (17, 2709606169), (13, 2361171624), + (28, 0), (28, 0), (28, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), + (23, 3467414241), (23, 4079273803), (26, 1282639619), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (28, 0), (28, 0), (28, 0), (27, 0), (29, 0), (29, 0), + (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (10, 2556510175), (21, 2556510175), + (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), + (21, 2556510175), (20, 2556510175), (8, 4117779887), (31, 0), (11, 525614319), (31, 0), (31, 0), (31, 0), + (31, 0), (31, 0), (25, 2556510175), (25, 2556510175), (25, 2556510175), (25, 2556510175), (20, 3906165615), + (20, 1185345766), (20, 407658226), (22, 2869884627), (27, 0), (30, 0), (5, 0), (27, 0), (4, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (28, 0), (28, 0), (28, 0), + (27, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (28, 0), (28, 0), + (28, 0), (27, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (7, 4096381681), + (22, 454268333), (14, 1927129959), (23, 1927129959), (20, 0), (8, 0), (19, 0), (18, 0), (18, 0), (22, 0), + (24, 0), (10, 0), (14, 0), (2, 0), (31, 0), (3, 0), (3, 0) + }, dbiStream.SectionContributions.Select(x => (x.ModuleIndex, x.DataCrc))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SectionMaps(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new (ushort, ushort, ushort, ushort, ushort, ushort, uint, uint)[] + { + (0x010d, 0x0000, 0x0000, 0x0001, 0xffff, 0xffff, 0x00000000, 0x00000ce8), + (0x0109, 0x0000, 0x0000, 0x0002, 0xffff, 0xffff, 0x00000000, 0x00000834), + (0x010b, 0x0000, 0x0000, 0x0003, 0xffff, 0xffff, 0x00000000, 0x00000394), + (0x0109, 0x0000, 0x0000, 0x0004, 0xffff, 0xffff, 0x00000000, 0x000000f8), + (0x0109, 0x0000, 0x0000, 0x0005, 0xffff, 0xffff, 0x00000000, 0x0000013c), + (0x0208, 0x0000, 0x0000, 0x0000, 0xffff, 0xffff, 0x00000000, 0xffffffff), + }, + dbiStream.SectionMaps.Select(m => ((ushort) + m.Attributes, m.LogicalOverlayNumber, m.Group, m.Frame, + m.SectionName, m.ClassName, m.Offset, m.SectionLength))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SourceFiles(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + string[][] firstThreeActualFileLists = dbiStream.SourceFiles + .Take(3) + .Select(x => x + .Select(y => y.ToString()) + .ToArray() + ).ToArray(); + + Assert.Equal(new[] + { + Array.Empty(), + new[] + { + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\pch.h", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\dllmain.cpp", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\Release\SimpleDll.pch", + }, + new[] + { + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winuser.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\basetsd.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winbase.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\stralign.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\guiddef.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\winerror.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_wstring.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\processthreadsapi.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winnt.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\ctype.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\string.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_memory.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\memoryapi.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_memcpy_s.h", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\Release\SimpleDll.pch", + } + }, + firstThreeActualFileLists); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ExtraDebugIndices(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new ushort[] + { + 0x7, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xB, 0xFFFF, 0xFFFF, 0xFFFF, 0xD, 0xFFFF + }, dbiStream.ExtraStreamIndices); + } + + [Fact] + public void SizeCalculation() + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = DbiStream.FromReader(file.Streams[DbiStream.StreamIndex].CreateReader()); + + uint calculatedSize = infoStream.GetPhysicalSize(); + + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + + Assert.Equal(stream.Length, calculatedSize); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs new file mode 100644 index 000000000..03a19f01b --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AsmResolver.IO; +using AsmResolver.Symbols.Pdb.Metadata.Info; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata.Info; + +public class InfoStreamTest +{ + private static InfoStream GetInfoStream(bool rebuild) + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = InfoStream.FromReader(file.Streams[InfoStream.StreamIndex].CreateReader()); + + if (rebuild) + { + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + infoStream = InfoStream.FromReader(ByteArrayDataSource.CreateReader(stream.ToArray())); + } + + return infoStream; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Header(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(InfoStreamVersion.VC70, infoStream.Version); + Assert.Equal(1u, infoStream.Age); + Assert.Equal(Guid.Parse("205dc366-d8f8-4175-8e06-26dd76722df5"), infoStream.UniqueId); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NameTable(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(new Dictionary + { + ["/UDTSRCLINEUNDONE"] = 48, + ["/src/headerblock"] = 46, + ["/LinkInfo"] = 5, + ["/TMCache"] = 6, + ["/names"] = 12 + }, infoStream.StreamIndices); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void FeatureCodes(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(new[] {PdbFeature.VC140}, infoStream.Features); + } + + [Fact] + public void SizeCalculation() + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = InfoStream.FromReader(file.Streams[InfoStream.StreamIndex].CreateReader()); + + uint calculatedSize = infoStream.GetPhysicalSize(); + + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + + Assert.Equal(stream.Length, calculatedSize); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs new file mode 100644 index 000000000..5ca3e791f --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs @@ -0,0 +1,18 @@ +using AsmResolver.Symbols.Pdb.Metadata; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata; + +public class PdbHashTest +{ + [Theory] + [InlineData("/UDTSRCLINEUNDONE", 0x23296bb2)] + [InlineData("/src/headerblock", 0x2b237ecd)] + [InlineData("/LinkInfo", 0x282209ed)] + [InlineData("/TMCache", 0x2621d5e9)] + [InlineData("/names", 0x6d6cfc21)] + public void HashV1(string value, uint expected) + { + Assert.Equal(expected, PdbHash.ComputeV1(value)); + } +}