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)); + } + } +}