diff --git a/AsmResolver.sln b/AsmResolver.sln
index 5b61c298f..cee77d8fe 100644
--- a/AsmResolver.sln
+++ b/AsmResolver.sln
@@ -85,6 +85,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsmResolver.Symbols.WindowsPdb", "src\AsmResolver.Symbols.WindowsPdb\AsmResolver.Symbols.WindowsPdb.csproj", "{9E311832-D0F2-42CA-84DD-9A91B88F0287}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsmResolver.Symbols.WindowsPdb.Tests", "test\AsmResolver.Symbols.WindowsPdb.Tests\AsmResolver.Symbols.WindowsPdb.Tests.csproj", "{AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -423,6 +427,30 @@ Global
{2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x64.Build.0 = Release|Any CPU
{2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x86.ActiveCfg = Release|Any CPU
{2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x86.Build.0 = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x64.Build.0 = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x86.Build.0 = Debug|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x64.ActiveCfg = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x64.Build.0 = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x86.ActiveCfg = Release|Any CPU
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x86.Build.0 = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x64.Build.0 = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x86.Build.0 = Debug|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x64.ActiveCfg = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x64.Build.0 = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x86.ActiveCfg = Release|Any CPU
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -460,6 +488,8 @@ Global
{40483E28-C703-4933-BA5B-9512EF6E6A21} = {EA971BB0-94BA-44DB-B16C-212D2DB27E17}
{CF6A7E02-37DC-4963-AC14-76D74ADCD87A} = {B3AF102B-ABE1-41B2-AE48-C40702F45AB0}
{2D1DF5DA-7367-4490-B3F0-B996348E150B} = {B3AF102B-ABE1-41B2-AE48-C40702F45AB0}
+ {9E311832-D0F2-42CA-84DD-9A91B88F0287} = {34A95168-A162-4F6A-803B-B6F221FE9EA6}
+ {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE} = {786C1732-8C96-45DD-97BB-639C9AA7F45B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3302AC79-6D23-4E7D-8C5F-C0C7261044D0}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/AsmResolver.Symbols.WindowsPdb.csproj b/src/AsmResolver.Symbols.WindowsPdb/AsmResolver.Symbols.WindowsPdb.csproj
new file mode 100644
index 000000000..b0b3df765
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/AsmResolver.Symbols.WindowsPdb.csproj
@@ -0,0 +1,27 @@
+
+
+
+ AsmResolver
+ Windows PDB models for the AsmResolver executable file inspection toolsuite.
+ windows pdb symbols
+ enable
+ net6.0;netcoreapp3.1;netstandard2.0
+ true
+ true
+
+
+
+ true
+ bin\Debug\netstandard2.0\AsmResolver.Symbols.WindowsPdb.xml
+
+
+
+ true
+ bin\Release\netstandard2.0\AsmResolver.xml
+
+
+
+
+
+
+
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/FreeBlockMap.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/FreeBlockMap.cs
new file mode 100644
index 000000000..4339d4daa
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/FreeBlockMap.cs
@@ -0,0 +1,38 @@
+using System.Collections;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf.Builder;
+
+///
+/// Represents a block within a MSF file that contains information on which blocks in the MSF file are free to use.
+///
+public class FreeBlockMap : SegmentBase
+{
+ ///
+ /// Creates a new empty free block map.
+ ///
+ /// The size of a single block in the MSF file.
+ public FreeBlockMap(uint blockSize)
+ {
+ BitField = new BitArray((int) blockSize * 8, true);
+ }
+
+ ///
+ /// Gets the bit field indicating which blocks in the MSF file are free to use.
+ ///
+ public BitArray BitField
+ {
+ get;
+ }
+
+ ///
+ public override uint GetPhysicalSize() => (uint) (BitField.Count / 8);
+
+ ///
+ public override void Write(IBinaryStreamWriter writer)
+ {
+ byte[] data = new byte[BitField.Count / 8];
+ BitField.CopyTo(data, 0);
+ writer.WriteBytes(data);
+ }
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/IMsfFileBuilder.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/IMsfFileBuilder.cs
new file mode 100644
index 000000000..f340944a1
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/IMsfFileBuilder.cs
@@ -0,0 +1,14 @@
+namespace AsmResolver.Symbols.WindowsPdb.Msf.Builder;
+
+///
+/// Provides members for constructing new MSF files.
+///
+public interface IMsfFileBuilder
+{
+ ///
+ /// Reconstructs a new writable MSF file buffer from an instance of .
+ ///
+ /// The file to reconstruct.
+ /// The reconstructed buffer.
+ MsfFileBuffer CreateFile(MsfFile file);
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/MsfFileBuffer.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/MsfFileBuffer.cs
new file mode 100644
index 000000000..79ff2afc1
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/MsfFileBuffer.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf.Builder;
+
+///
+/// Represents a mutable buffer for building up a new MSF file.
+///
+public class MsfFileBuffer : SegmentBase
+{
+ private readonly Dictionary _blockIndices = new();
+ private readonly List _freeBlockMaps = new(2);
+ private readonly List _blocks;
+
+ ///
+ /// Creates a new empty MSF file buffer.
+ ///
+ /// The block size to use.
+ public MsfFileBuffer(uint blockSize)
+ {
+ SuperBlock = new MsfSuperBlock
+ {
+ Signature = MsfSuperBlock.BigMsfSignature,
+ BlockSize = blockSize,
+ FreeBlockMapIndex = 1,
+ BlockCount = 3,
+ };
+
+ _blocks = new List((int) blockSize);
+
+ InsertBlock(0, SuperBlock);
+ var fpm = GetOrCreateFreeBlockMap(1, out _);
+ InsertBlock(2, null);
+
+ fpm.BitField[0] = false;
+ fpm.BitField[1] = false;
+ fpm.BitField[2] = false;
+ }
+
+ ///
+ /// Gets the super block of the MSF file that is being constructed.
+ ///
+ public MsfSuperBlock SuperBlock
+ {
+ get;
+ }
+
+ ///
+ /// Determines whether a block in the MSF file buffer is available or not.
+ ///
+ /// The index of the block.
+ /// true if the block is available, false otherwise.
+ public bool BlockIsAvailable(int blockIndex)
+ {
+ var freeBlockMap = GetOrCreateFreeBlockMap(blockIndex, out int offset);
+ if (offset < 3 && (blockIndex == 0 || offset > 0))
+ return false;
+ return freeBlockMap.BitField[offset];
+ }
+
+ ///
+ /// Inserts a block of the provided MSF stream into the buffer.
+ ///
+ /// The MSF file index to insert the block into.
+ /// The stream to pull a chunk from.
+ /// The index of the chunk to store at the provided block index.
+ ///
+ /// Occurs when the index provided by is already in use.
+ ///
+ public void InsertBlock(int blockIndex, MsfStream stream, int chunkIndex)
+ {
+ var fpm = GetOrCreateFreeBlockMap(blockIndex, out int offset);
+ if (!fpm.BitField[offset])
+ throw new ArgumentException($"Block {blockIndex} is already in use.");
+
+ uint blockSize = SuperBlock.BlockSize;
+ var segment = new DataSourceSegment(
+ stream.Contents,
+ stream.Contents.BaseAddress + (ulong) (chunkIndex * blockSize),
+ (uint) (chunkIndex * blockSize),
+ (uint) Math.Min(stream.Contents.Length - (ulong) (chunkIndex * blockSize), blockSize));
+
+ InsertBlock(blockIndex, segment);
+
+ int[] indices = GetMutableBlockIndicesForStream(stream);
+ indices[chunkIndex] = blockIndex;
+
+ fpm.BitField[offset] = false;
+ }
+
+ private void InsertBlock(int blockIndex, ISegment? segment)
+ {
+ // Ensure enough blocks are present in the backing-buffer.
+ while (_blocks.Count <= blockIndex)
+ _blocks.Add(null);
+
+ // Insert block and update super block.
+ _blocks[blockIndex] = segment;
+ SuperBlock.BlockCount = (uint) _blocks.Count;
+ }
+
+ private FreeBlockMap GetOrCreateFreeBlockMap(int blockIndex, out int offset)
+ {
+ int index = Math.DivRem(blockIndex, (int) SuperBlock.BlockSize, out offset);
+ while (_freeBlockMaps.Count <= index)
+ {
+ var freeBlockMap = new FreeBlockMap(SuperBlock.BlockSize);
+ _freeBlockMaps.Add(freeBlockMap);
+ InsertBlock(index + (int) SuperBlock.FreeBlockMapIndex, freeBlockMap);
+ }
+
+ return _freeBlockMaps[index];
+ }
+
+ private int[] GetMutableBlockIndicesForStream(MsfStream stream)
+ {
+ if (!_blockIndices.TryGetValue(stream, out int[]? indices))
+ {
+ indices = new int[stream.GetRequiredBlockCount(SuperBlock.BlockSize)];
+ _blockIndices.Add(stream, indices);
+ }
+
+ return indices;
+ }
+
+ ///
+ /// Gets the allocated indices for the provided MSF stream.
+ ///
+ /// The stream.
+ /// The block indices.
+ public int[] GetBlockIndicesForStream(MsfStream stream) => (int[]) GetMutableBlockIndicesForStream(stream).Clone();
+
+ ///
+ /// Constructs a new MSF stream containing the stream directory.
+ ///
+ /// The files that the directory should list.
+ /// The constructed stream.
+ ///
+ /// This method does not add the stream to the buffer, nor does it update the super block.
+ ///
+ public MsfStream CreateStreamDirectory(IList streams)
+ {
+ using var contents = new MemoryStream();
+ var writer = new BinaryStreamWriter(contents);
+
+ // Stream count.
+ writer.WriteInt32(streams.Count);
+
+ // Stream sizes.
+ for (int i = 0; i < streams.Count; i++)
+ writer.WriteUInt32((uint) streams[i].Contents.Length);
+
+ // Stream indices.
+ for (int i = 0; i < streams.Count; i++)
+ {
+ int[] indices = GetMutableBlockIndicesForStream(streams[i]);
+ foreach (int index in indices)
+ writer.WriteInt32(index);
+ }
+
+ return new MsfStream(contents.ToArray());
+ }
+
+ ///
+ /// Creates a new MSF stream containing the block indices of the stream directory.
+ ///
+ /// The stream directory to store the indices for.
+ /// The constructed stream.
+ ///
+ /// This method does not add the stream to the buffer, nor does it update the super block.
+ ///
+ public MsfStream CreateStreamDirectoryMap(MsfStream streamDirectory)
+ {
+ using var contents = new MemoryStream();
+ var writer = new BinaryStreamWriter(contents);
+
+ int[] indices = GetMutableBlockIndicesForStream(streamDirectory);
+ foreach (int index in indices)
+ writer.WriteInt32(index);
+
+ return new MsfStream(contents.ToArray());
+ }
+
+ ///
+ public override uint GetPhysicalSize() => SuperBlock.BlockCount * SuperBlock.BlockSize;
+
+ ///
+ public override void Write(IBinaryStreamWriter writer)
+ {
+ foreach (var block in _blocks)
+ {
+ if (block is null)
+ {
+ writer.WriteZeroes((int) SuperBlock.BlockSize);
+ }
+ else
+ {
+ block.Write(writer);
+ writer.Align(SuperBlock.BlockSize);
+ }
+ }
+ }
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/SequentialMsfFileBuilder.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/SequentialMsfFileBuilder.cs
new file mode 100644
index 000000000..35195f776
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/Builder/SequentialMsfFileBuilder.cs
@@ -0,0 +1,62 @@
+namespace AsmResolver.Symbols.WindowsPdb.Msf.Builder;
+
+///
+/// Provides an implementation of the that places all blocks of every stream in sequence,
+/// and effectively defragments the file system.
+///
+public class SequentialMsfFileBuilder : IMsfFileBuilder
+{
+ ///
+ /// Gets the default instance of the class.
+ ///
+ public static SequentialMsfFileBuilder Instance
+ {
+ get;
+ } = new();
+
+ ///
+ public MsfFileBuffer CreateFile(MsfFile file)
+ {
+ var result = new MsfFileBuffer(file.BlockSize);
+
+ // Block 0, 1, and 2 are reserved for the super block, FPM1 and FPM2.
+ int currentIndex = 3;
+
+ // Add streams in sequence.
+ for (int i = 0; i < file.Streams.Count; i++)
+ AddStream(result, file.Streams[i], ref currentIndex);
+
+ // Construct and add stream directory.
+ var directory = result.CreateStreamDirectory(file.Streams);
+ result.SuperBlock.DirectoryByteCount = (uint) directory.Contents.Length;
+ AddStream(result, directory, ref currentIndex);
+
+ // Construct and add stream directory map.
+ var directoryMap = result.CreateStreamDirectoryMap(directory);
+ result.SuperBlock.DirectoryMapIndex = (uint) currentIndex;
+ AddStream(result, directoryMap, ref currentIndex);
+
+ return result;
+ }
+
+ private static void AddStream(MsfFileBuffer buffer, MsfStream stream, ref int currentIndex)
+ {
+ int blockCount = stream.GetRequiredBlockCount(buffer.SuperBlock.BlockSize);
+
+ for (int j = 0; j < blockCount; j++, currentIndex++)
+ {
+ // Skip over any of the FPM indices.
+ switch (currentIndex % 4096)
+ {
+ case 1:
+ currentIndex += 2;
+ break;
+ case 2:
+ currentIndex++;
+ break;
+ }
+
+ buffer.InsertBlock(currentIndex, stream, j);
+ }
+ }
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfFile.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfFile.cs
new file mode 100644
index 000000000..5e73e5d84
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfFile.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using AsmResolver.Collections;
+using AsmResolver.IO;
+using AsmResolver.Symbols.WindowsPdb.Msf.Builder;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf;
+
+///
+/// Models a file that is in the Microsoft Multi-Stream Format (MSF).
+///
+public class MsfFile
+{
+ private uint _blockSize;
+ private IList? _streams;
+
+ ///
+ /// Gets or sets the size of each block in the MSF file.
+ ///
+ ///
+ /// Occurs when the provided value is neither 512, 1024, 2048 or 4096.
+ ///
+ public uint BlockSize
+ {
+ get => _blockSize;
+ set
+ {
+ if (value is not (512 or 1024 or 2048 or 4096))
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(value),
+ "Block size must be either 512, 1024, 2048 or 4096 bytes.");
+ }
+
+ _blockSize = value;
+ }
+ }
+
+ ///
+ /// Gets a collection of streams that are present in the MSF file.
+ ///
+ public IList Streams
+ {
+ get
+ {
+ if (_streams is null)
+ Interlocked.CompareExchange(ref _streams, GetStreams(), null);
+ return _streams;
+ }
+ }
+
+ ///
+ /// Creates a new empty MSF file with a default block size of 4096.
+ ///
+ public MsfFile()
+ : this(4096)
+ {
+ }
+
+ ///
+ /// Creates a new empty MSF file with the provided block size.
+ ///
+ /// The block size to use. This must be a value of 512, 1024, 2048 or 4096.
+ /// Occurs when an invalid block size was provided.
+ public MsfFile(uint blockSize)
+ {
+ BlockSize = blockSize;
+ }
+
+ ///
+ /// Reads an MSF file from a file on the disk.
+ ///
+ /// The path to the file to read.
+ /// The read MSF file.
+ public static MsfFile FromFile(string path) => FromFile(UncachedFileService.Instance.OpenFile(path));
+
+ ///
+ /// Reads an MSF file from an input file.
+ ///
+ /// The file to read.
+ /// The read MSF file.
+ public static MsfFile FromFile(IInputFile file) => FromReader(file.CreateReader());
+
+ ///
+ /// Interprets a byte array as an MSF file.
+ ///
+ /// The data to interpret.
+ /// The read MSF file.
+ public static MsfFile FromBytes(byte[] data) => FromReader(ByteArrayDataSource.CreateReader(data));
+
+ ///
+ /// Reads an MSF file from the provided input stream reader.
+ ///
+ /// The reader.
+ /// The read MSF file.
+ public static MsfFile FromReader(BinaryStreamReader reader) => new SerializedMsfFile(reader);
+
+ ///
+ /// Obtains the list of streams stored in the MSF file.
+ ///
+ /// The streams.
+ ///
+ /// This method is called upon initialization of the property.
+ ///
+ protected virtual IList GetStreams() => new OwnedCollection(this);
+
+ ///
+ /// Reconstructs and writes the MSF file to the disk.
+ ///
+ /// The path of the file to write to.
+ public void Write(string path)
+ {
+ using var fs = File.Create(path);
+ Write(fs);
+ }
+
+ ///
+ /// Reconstructs and writes the MSF file to an output stream.
+ ///
+ /// The output stream.
+ public void Write(Stream stream) => Write(new BinaryStreamWriter(stream));
+
+ ///
+ /// Reconstructs and writes the MSF file to an output stream.
+ ///
+ /// The output stream.
+ public void Write(IBinaryStreamWriter writer) => Write(writer, SequentialMsfFileBuilder.Instance);
+
+ ///
+ /// Reconstructs and writes the MSF file to an output stream.
+ ///
+ /// The output stream.
+ /// The builder to use for reconstructing the MSF file.
+ public void Write(IBinaryStreamWriter writer, IMsfFileBuilder builder) => builder.CreateFile(this).Write(writer);
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStream.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStream.cs
new file mode 100644
index 000000000..942bf0fab
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStream.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AsmResolver.Collections;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf;
+
+///
+/// Represents a single stream in an Multi-Stream Format (MSF) file.
+///
+public class MsfStream : IOwnedCollectionElement
+{
+ ///
+ /// Creates a new MSF stream with the provided contents.
+ ///
+ /// The raw data of the stream.
+ public MsfStream(byte[] data)
+ : this(new ByteArrayDataSource(data))
+ {
+ }
+
+ ///
+ /// Creates a new MSF stream with the provided data source as contents.
+ ///
+ /// The data source containing the raw data of the stream.
+ public MsfStream(IDataSource contents)
+ {
+ Contents = contents;
+ OriginalBlockIndices = Array.Empty();
+ }
+
+ ///
+ /// Initializes an MSF stream with a data source and a list of original block indices that the stream was based on.
+ ///
+ /// The data source containing the raw data of the stream.
+ /// The original block indices that this MSF stream was based on.
+ public MsfStream(IDataSource contents, IEnumerable originalBlockIndices)
+ {
+ Contents = contents;
+ OriginalBlockIndices = originalBlockIndices.ToArray();
+ }
+
+ ///
+ /// Gets the parent MSF file that this stream is embedded in.
+ ///
+ public MsfFile? Parent
+ {
+ get;
+ private set;
+ }
+
+ MsfFile? IOwnedCollectionElement.Owner
+ {
+ get => Parent;
+ set => Parent = value;
+ }
+
+ ///
+ /// Gets or sets the contents of the stream.
+ ///
+ public IDataSource Contents
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets a collection of block indices that this stream was based of (if available).
+ ///
+ public IReadOnlyList OriginalBlockIndices
+ {
+ get;
+ }
+
+ ///
+ /// Gets the amount of blocks that is required to store this MSF stream.
+ ///
+ /// The number of blocks.
+ /// Occurs when the stream is not added to a file.
+ public int GetRequiredBlockCount()
+ {
+ if (Parent is null)
+ {
+ throw new InvalidOperationException(
+ "Determining the required block count of a stream requires the stream to be added to an MSF file.");
+ }
+
+ return GetRequiredBlockCount(Parent.BlockSize);
+ }
+
+ ///
+ /// Gets the amount of blocks that is required to store this MSF stream, given the provided block size.
+ ///
+ /// The block size.
+ /// The number of blocks.
+ public int GetRequiredBlockCount(uint blockSize)
+ {
+ return (int) ((Contents.Length + blockSize - 1) / blockSize);
+ }
+
+ ///
+ /// Creates a new binary reader that reads the raw contents of the stream.
+ ///
+ /// The constructed binary reader.
+ public BinaryStreamReader CreateReader() => new(Contents, Contents.BaseAddress, 0, (uint) Contents.Length);
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStreamDataSource.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStreamDataSource.cs
new file mode 100644
index 000000000..08e43ef2d
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfStreamDataSource.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf;
+
+///
+/// Implements a data source for a single MSF stream that pulls data from multiple (fragmented) blocks.
+///
+public class MsfStreamDataSource : IDataSource
+{
+ private readonly IDataSource[] _blocks;
+ private readonly long _blockSize;
+
+ ///
+ /// Creates a new MSF stream data source.
+ ///
+ /// The length of the stream.
+ /// The size of an individual block.
+ /// The blocks
+ ///
+ /// Occurs when the total size of the provided blocks is smaller than
+ /// * .
+ ///
+ public MsfStreamDataSource(ulong length, uint blockSize, IEnumerable blocks)
+ : this(length, blockSize, blocks.Select(x => new ByteArrayDataSource(x)))
+ {
+ }
+
+ ///
+ /// Creates a new MSF stream data source.
+ ///
+ /// The length of the stream.
+ /// The size of an individual block.
+ /// The blocks
+ ///
+ /// Occurs when the total size of the provided blocks is smaller than
+ /// * .
+ ///
+ public MsfStreamDataSource(ulong length, uint blockSize, IEnumerable blocks)
+ {
+ Length = length;
+ _blocks = blocks.ToArray();
+ _blockSize = blockSize;
+
+ if (length > (ulong) (_blocks.Length * blockSize))
+ throw new ArgumentException("Provided length is larger than the provided blocks combined.");
+ }
+
+ ///
+ public ulong BaseAddress => 0;
+
+ ///
+ public ulong Length
+ {
+ get;
+ }
+
+ ///
+ public byte this[ulong address]
+ {
+ get
+ {
+ if (!IsValidAddress(address))
+ throw new IndexOutOfRangeException();
+
+ var block = GetBlockAndOffset(address, out ulong offset);
+ return block[block.BaseAddress + offset];
+ }
+ }
+
+ ///
+ public bool IsValidAddress(ulong address) => address < Length;
+
+ ///
+ public int ReadBytes(ulong address, byte[] buffer, int index, int count)
+ {
+ int totalReadCount = 0;
+ int remainingBytes = Math.Min(count, (int) (Length - (address - BaseAddress)));
+
+ while (remainingBytes > 0)
+ {
+ // Obtain current block and offset within block.
+ var block = GetBlockAndOffset(address, out ulong offset);
+
+ // Read available bytes.
+ int readCount = Math.Min(remainingBytes, (int) _blockSize);
+ int actualReadCount = block.ReadBytes(block.BaseAddress + offset, buffer, index, readCount);
+
+ // Move to the next block.
+ totalReadCount += actualReadCount;
+ address += (ulong) actualReadCount;
+ index += actualReadCount;
+ remainingBytes -= actualReadCount;
+ }
+
+ return totalReadCount;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private IDataSource GetBlockAndOffset(ulong address, out ulong offset)
+ {
+ var block = _blocks[Math.DivRem((long) address, _blockSize, out long x)];
+ offset = (ulong) x;
+ return block;
+ }
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfSuperBlock.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfSuperBlock.cs
new file mode 100644
index 000000000..91dfe778d
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/MsfSuperBlock.cs
@@ -0,0 +1,132 @@
+using System;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf;
+
+///
+/// Represents the first block in a Multi-Stream Format (MSF) file.
+///
+public sealed class MsfSuperBlock : SegmentBase
+{
+ // Used in MSF v2.0
+ internal static readonly byte[] SmallMsfSignature =
+ {
+ 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20, 0x70, 0x72,
+ 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x20, 0x32, 0x2e, 0x30,
+ 0x30, 0x0d, 0x0a, 0x1a, 0x4a, 0x47
+ };
+
+ // Used in MSF v7.0
+ internal static readonly byte[] BigMsfSignature =
+ {
+ 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20,
+ 0x4d, 0x53, 0x46, 0x20, 0x37, 0x2e, 0x30, 0x30, 0x0d, 0x0a, 0x1a, 0x44, 0x53, 0x00, 0x00, 0x00
+ };
+
+ ///
+ /// Gets or sets the magic file signature in the super block, identifying the format version of the MSF file.
+ ///
+ public byte[] Signature
+ {
+ get;
+ set;
+ } = (byte[]) BigMsfSignature.Clone();
+
+ ///
+ /// Gets or sets the size of an individual block in bytes.
+ ///
+ public uint BlockSize
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the index of the block containing a bitfield indicating which blocks in the entire MSF file are
+ /// in use or not.
+ ///
+ public uint FreeBlockMapIndex
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the total number of blocks in the MSF file.
+ ///
+ public uint BlockCount
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the number of bytes of the stream directory in the MSF file.
+ ///
+ public uint DirectoryByteCount
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Gets or sets the index of the block containing all block indices that make up the stream directory of the MSF
+ /// file.
+ ///
+ public uint DirectoryMapIndex
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Reads a single MSF super block from the provided input stream.
+ ///
+ /// The input stream.
+ /// The parsed MSF super block.
+ /// Occurs when the super block is malformed.
+ public static MsfSuperBlock FromReader(ref BinaryStreamReader reader)
+ {
+ var result = new MsfSuperBlock();
+
+ // Check MSF header.
+ result.Signature = new byte[BigMsfSignature.Length];
+ int count = reader.ReadBytes(result.Signature, 0, result.Signature.Length);
+ if (count != BigMsfSignature.Length || !ByteArrayEqualityComparer.Instance.Equals(result.Signature, BigMsfSignature))
+ throw new BadImageFormatException("File does not start with a valid or supported MSF file signature.");
+
+ result.BlockSize = reader.ReadUInt32();
+ if (result.BlockSize is not (512 or 1024 or 2048 or 4096))
+ throw new BadImageFormatException("Block size must be either 512, 1024, 2048 or 4096 bytes.");
+
+ // We don't really use the free block map as we are not fully implementing the NTFS-esque file system, but we
+ // validate its contents regardless as a sanity check.
+ result.FreeBlockMapIndex = reader.ReadUInt32();
+ if (result.FreeBlockMapIndex is not (1 or 2))
+ throw new BadImageFormatException($"Free block map index must be 1 or 2, but was {result.FreeBlockMapIndex}.");
+
+ result.BlockCount = reader.ReadUInt32();
+
+ result.DirectoryByteCount = reader.ReadUInt32();
+ reader.Offset += sizeof(uint);
+ result.DirectoryMapIndex = reader.ReadUInt32();
+
+ return result;
+ }
+
+ ///
+ public override uint GetPhysicalSize() => (uint) BigMsfSignature.Length + sizeof(uint) * 6;
+
+ ///
+ public override void Write(IBinaryStreamWriter writer)
+ {
+ writer.WriteBytes(Signature);
+ writer.WriteUInt32(BlockSize);
+ writer.WriteUInt32(FreeBlockMapIndex);
+ writer.WriteUInt32(BlockCount);
+ writer.WriteUInt32(DirectoryByteCount);
+ writer.WriteUInt32(0);
+ writer.WriteUInt32(DirectoryMapIndex);
+ }
+
+}
diff --git a/src/AsmResolver.Symbols.WindowsPdb/Msf/SerializedMsfFile.cs b/src/AsmResolver.Symbols.WindowsPdb/Msf/SerializedMsfFile.cs
new file mode 100644
index 000000000..025780e57
--- /dev/null
+++ b/src/AsmResolver.Symbols.WindowsPdb/Msf/SerializedMsfFile.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using AsmResolver.Collections;
+using AsmResolver.IO;
+
+namespace AsmResolver.Symbols.WindowsPdb.Msf;
+
+///
+/// Provides an implementation for an MSF file that is read from an input file.
+///
+///
+/// Currently, this model only supports version 7.0 of the file format.
+///
+public class SerializedMsfFile : MsfFile
+{
+ private readonly BinaryStreamReader _reader;
+ private readonly MsfSuperBlock _originalSuperBlock;
+ private readonly IDataSource?[] _blocks;
+
+ ///
+ /// Interprets an input stream as an MSF file version 7.0.
+ ///
+ /// The input stream.
+ /// Occurs when the MSF file is malformed.
+ public SerializedMsfFile(BinaryStreamReader reader)
+ {
+ _originalSuperBlock = MsfSuperBlock.FromReader(ref reader);
+
+ BlockSize = _originalSuperBlock.BlockSize;
+ _blocks = new IDataSource?[_originalSuperBlock.BlockCount];
+ _reader = reader;
+ }
+
+ private IDataSource GetBlock(int index)
+ {
+ if (_blocks[index] is null)
+ {
+ // We lazily initialize all blocks by slicing the original data source of the reader.
+ var block = new DataSourceSlice(
+ _reader.DataSource,
+ _reader.DataSource.BaseAddress + (ulong) (index * _originalSuperBlock.BlockSize),
+ _originalSuperBlock.BlockSize);
+
+ Interlocked.CompareExchange(ref _blocks[index], block, null);
+ }
+
+ return _blocks[index]!;
+ }
+
+ ///
+ protected override IList GetStreams()
+ {
+ // Get the block indices of the Stream Directory stream.
+ var indicesBlock = GetBlock((int) _originalSuperBlock.DirectoryMapIndex);
+ var indicesReader = new BinaryStreamReader(indicesBlock, indicesBlock.BaseAddress, 0,
+ GetBlockCount(_originalSuperBlock.DirectoryByteCount) * sizeof(uint));
+
+ // Access the Stream Directory stream.
+ var directoryStream = CreateStreamFromIndicesReader(ref indicesReader, _originalSuperBlock.DirectoryByteCount);
+ var directoryReader = directoryStream.CreateReader();
+
+ // Stream Directory format is as follows:
+ // - stream count: uint32
+ // - stream sizes: uint32[stream count]
+ // - stream indices: uint32[stream count][]
+
+ int streamCount = directoryReader.ReadInt32();
+
+ // Read sizes.
+ uint[] streamSizes = new uint[streamCount];
+ for (int i = 0; i < streamCount; i++)
+ streamSizes[i] = directoryReader.ReadUInt32();
+
+ // Construct streams.
+ var result = new OwnedCollection(this, streamCount);
+ for (int i = 0; i < streamCount; i++)
+ {
+ // A size of 0xFFFFFFFF indicates the stream does not exist.
+ if (streamSizes[i] == uint.MaxValue)
+ continue;
+
+ result.Add(CreateStreamFromIndicesReader(ref directoryReader, streamSizes[i]));
+ }
+
+ return result;
+ }
+
+ private MsfStream CreateStreamFromIndicesReader(ref BinaryStreamReader indicesReader, uint streamSize)
+ {
+ // Read all indices.
+ int[] indices = new int[GetBlockCount(streamSize)];
+ for (int i = 0; i < indices.Length; i++)
+ indices[i] = indicesReader.ReadInt32();
+
+ // Transform indices to blocks.
+ var blocks = new IDataSource[indices.Length];
+ for (int i = 0; i < blocks.Length; i++)
+ blocks[i] = GetBlock(indices[i]);
+
+ // Construct stream.
+ var dataSource = new MsfStreamDataSource(streamSize, _originalSuperBlock.BlockSize, blocks);
+ return new MsfStream(dataSource, indices);
+ }
+
+ private uint GetBlockCount(uint streamSize)
+ {
+ return (streamSize + _originalSuperBlock.BlockSize - 1) / _originalSuperBlock.BlockSize;
+ }
+}
diff --git a/src/AsmResolver/IO/DataSourceSlice.cs b/src/AsmResolver/IO/DataSourceSlice.cs
new file mode 100644
index 000000000..f107316a0
--- /dev/null
+++ b/src/AsmResolver/IO/DataSourceSlice.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace AsmResolver.IO
+{
+ ///
+ /// Represents a data source that only exposes a part (slice) of another data source.
+ ///
+ public class DataSourceSlice : IDataSource
+ {
+ private readonly IDataSource _source;
+
+ ///
+ /// Creates a new data source slice.
+ ///
+ /// The original data source to slice.
+ /// The starting address.
+ /// The number of bytes.
+ ///
+ /// Occurs when and/or result in addresses that are invalid
+ /// in the original data source.
+ ///
+ public DataSourceSlice(IDataSource source, ulong start, ulong length)
+ {
+ _source = source;
+
+ if (!source.IsValidAddress(start))
+ throw new ArgumentOutOfRangeException(nameof(start));
+ if (length > 0 && !source.IsValidAddress(start + length - 1))
+ throw new ArgumentOutOfRangeException(nameof(length));
+
+ BaseAddress = start;
+ Length = length;
+ }
+
+ ///
+ public ulong BaseAddress
+ {
+ get;
+ }
+
+ ///
+ public ulong Length
+ {
+ get;
+ }
+
+ ///
+ public byte this[ulong address]
+ {
+ get
+ {
+ if (!IsValidAddress(address))
+ throw new IndexOutOfRangeException();
+ return _source[address];
+ }
+ }
+
+ ///
+ public bool IsValidAddress(ulong address) => address >= BaseAddress && address - BaseAddress < Length;
+
+ ///
+ public int ReadBytes(ulong address, byte[] buffer, int index, int count)
+ {
+ int maxCount = Math.Max(0, (int) (Length - (address - BaseAddress)));
+ return _source.ReadBytes(address, buffer, index, Math.Min(maxCount, count));
+ }
+ }
+}
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/AsmResolver.Symbols.WindowsPdb.Tests.csproj b/test/AsmResolver.Symbols.WindowsPdb.Tests/AsmResolver.Symbols.WindowsPdb.Tests.csproj
new file mode 100644
index 000000000..ff11f109f
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/AsmResolver.Symbols.WindowsPdb.Tests.csproj
@@ -0,0 +1,42 @@
+
+
+
+ net6.0
+ enable
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+
+
+
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfFileTest.cs b/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfFileTest.cs
new file mode 100644
index 000000000..80298ad77
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfFileTest.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using System.Linq;
+using AsmResolver.Symbols.WindowsPdb.Msf;
+using Xunit;
+
+namespace AsmResolver.Symbols.WindowsPdb.Tests.Msf;
+
+public class MsfFileTest
+{
+ [Fact]
+ public void RoundTrip()
+ {
+ var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb);
+
+ using var stream = new MemoryStream();
+ file.Write(stream);
+
+ var newFile = MsfFile.FromBytes(stream.ToArray());
+
+ Assert.Equal(file.BlockSize, newFile.BlockSize);
+ Assert.Equal(file.Streams.Count, newFile.Streams.Count);
+ Assert.All(Enumerable.Range(0, file.Streams.Count), i =>
+ {
+ Assert.Equal(file.Streams[i].CreateReader().ReadToEnd(), newFile.Streams[i].CreateReader().ReadToEnd());;
+ });
+ }
+}
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfStreamDataSourceTest.cs b/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfStreamDataSourceTest.cs
new file mode 100644
index 000000000..d8ce31566
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/Msf/MsfStreamDataSourceTest.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Linq;
+using AsmResolver.IO;
+using AsmResolver.Symbols.WindowsPdb.Msf;
+using Xunit;
+
+namespace AsmResolver.Symbols.WindowsPdb.Tests.Msf;
+
+public class MsfStreamDataSourceTest
+{
+ [Fact]
+ public void EmptyStream()
+ {
+ var source = new MsfStreamDataSource(0, 0x200, Array.Empty());
+
+ byte[] buffer = new byte[0x1000];
+ int readCount = source.ReadBytes(0, buffer, 0, buffer.Length);
+ Assert.Equal(0, readCount);
+ Assert.All(buffer, b => Assert.Equal(0, b));
+ }
+
+ [Theory]
+ [InlineData(0x200, 0x200)]
+ [InlineData(0x200, 0x100)]
+ public void StreamWithOneBlock(int blockSize, int actualSize)
+ {
+ byte[] block = new byte[blockSize];
+ for (int i = 0; i < blockSize; i++)
+ block[i] = (byte) (i & 0xFF);
+
+ var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block});
+
+ byte[] buffer = new byte[0x1000];
+ int readCount = source.ReadBytes(0, buffer, 0, buffer.Length);
+ Assert.Equal(actualSize, readCount);
+ Assert.Equal(block.Take(actualSize), buffer.Take(actualSize));
+ }
+
+ [Theory]
+ [InlineData(0x200, 0x400)]
+ [InlineData(0x200, 0x300)]
+ public void StreamWithTwoBlocks(int blockSize, int actualSize)
+ {
+ byte[] block1 = new byte[blockSize];
+ for (int i = 0; i < blockSize; i++)
+ block1[i] = (byte) 'A';
+
+ byte[] block2 = new byte[blockSize];
+ for (int i = 0; i < blockSize; i++)
+ block2[i] = (byte) 'B';
+
+ var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block1, block2});
+
+ byte[] buffer = new byte[0x1000];
+ int readCount = source.ReadBytes(0, buffer, 0, buffer.Length);
+ Assert.Equal(actualSize, readCount);
+ Assert.Equal(block1.Concat(block2).Take(actualSize), buffer.Take(actualSize));
+ }
+
+ [Theory]
+ [InlineData(0x200, 0x400)]
+ public void ReadInMiddleOfBlock(int blockSize, int actualSize)
+ {
+ byte[] block1 = new byte[blockSize];
+ for (int i = 0; i < blockSize; i++)
+ block1[i] = (byte) ((i*2) & 0xFF);
+
+ byte[] block2 = new byte[blockSize];
+ for (int i = 0; i < blockSize; i++)
+ block2[i] = (byte) ((i * 2 + 1) & 0xFF);
+
+ var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block1, block2});
+
+ byte[] buffer = new byte[blockSize];
+ int readCount = source.ReadBytes((ulong) blockSize / 4, buffer, 0, blockSize);
+ Assert.Equal(blockSize, readCount);
+ Assert.Equal(block1.Skip(blockSize / 4).Concat(block2).Take(blockSize), buffer);
+ }
+}
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..ccd260a2d
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.Designer.cs
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace AsmResolver.Symbols.WindowsPdb.Tests.Properties {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("AsmResolver.Symbols.WindowsPdb.Tests.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ internal static byte[] SimpleDllPdb {
+ get {
+ object obj = ResourceManager.GetObject("SimpleDllPdb", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+ }
+}
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.resx b/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.resx
new file mode 100644
index 000000000..64f81d46b
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/Properties/Resources.resx
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\SimpleDll.pdb;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/.gitignore b/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/.gitignore
new file mode 100644
index 000000000..bd46a47b5
--- /dev/null
+++ b/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/.gitignore
@@ -0,0 +1 @@
+!*.pdb
diff --git a/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/SimpleDll.pdb b/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/SimpleDll.pdb
new file mode 100644
index 000000000..2a5f3d4ad
Binary files /dev/null and b/test/AsmResolver.Symbols.WindowsPdb.Tests/Resources/SimpleDll.pdb differ
diff --git a/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs b/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs
new file mode 100644
index 000000000..937e79993
--- /dev/null
+++ b/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+using AsmResolver.IO;
+using Xunit;
+
+namespace AsmResolver.Tests.IO
+{
+ public class DataSourceSliceTest
+ {
+ private readonly IDataSource _source = new ByteArrayDataSource(new byte[]
+ {
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
+ });
+
+ [Fact]
+ public void EmptySlice()
+ {
+ var slice = new DataSourceSlice(_source, 0, 0);
+ Assert.Equal(0ul, slice.Length);
+ }
+
+ [Fact]
+ public void SliceStart()
+ {
+ var slice = new DataSourceSlice(_source, 0, 5);
+ Assert.Equal(5ul, slice.Length);
+ Assert.All(Enumerable.Range(0, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i]));
+ Assert.Throws(() => slice[5]);
+ }
+
+ [Fact]
+ public void SliceMiddle()
+ {
+ var slice = new DataSourceSlice(_source, 3, 5);
+ Assert.Equal(5ul, slice.Length);
+ Assert.All(Enumerable.Range(3, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i]));
+ Assert.Throws(() => slice[3 - 1]);
+ Assert.Throws(() => slice[3 + 5]);
+ }
+
+ [Fact]
+ public void SliceEnd()
+ {
+ var slice = new DataSourceSlice(_source, 5, 5);
+ Assert.Equal(5ul, slice.Length);
+ Assert.All(Enumerable.Range(5, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i]));
+ Assert.Throws(() => slice[5 - 1]);
+ }
+
+ [Fact]
+ public void ReadSlicedShouldReadUpToSliceAmountOfBytes()
+ {
+ var slice = new DataSourceSlice(_source, 3, 5);
+
+ byte[] data1 = new byte[7];
+ int originalCount = _source.ReadBytes(3, data1, 0, data1.Length);
+ Assert.Equal(7, originalCount);
+
+ byte[] data2 = new byte[3];
+ int newCount = slice.ReadBytes(3, data2, 0, data2.Length);
+ Assert.Equal(3, newCount);
+ Assert.Equal(data1.Take(3), data2.Take(3));
+
+ byte[] data3 = new byte[7];
+ int newCount2 = slice.ReadBytes(3, data3, 0, data3.Length);
+ Assert.Equal(5, newCount2);
+ Assert.Equal(data1.Take(5), data3.Take(5));
+ }
+ }
+}