diff --git a/Library/DiscUtils.Core/Compression/LookupHuffmanDecoder.cs b/Library/DiscUtils.Core/Compression/LookupHuffmanDecoder.cs new file mode 100644 index 000000000..19260a7b1 --- /dev/null +++ b/Library/DiscUtils.Core/Compression/LookupHuffmanDecoder.cs @@ -0,0 +1,119 @@ +using System; +using System.Runtime.CompilerServices; + +namespace DiscUtils.Compression; + +internal ref struct LookupHuffmanDecoder +{ + private readonly int _symbolCount; + private Span _lengths; + private Span _table; + private int _numBits; + + public LookupHuffmanDecoder( + int symbolCount, + Span lengths, + Span table) + { + _symbolCount = symbolCount; + _lengths = lengths.Slice(0, symbolCount); + _table = table; + _numBits = 0; + } + + public bool Build(ReadOnlySpan codeLengths) + { + if (codeLengths.Length < _symbolCount) + { + return false; + } + + codeLengths.Slice(0, _symbolCount).CopyTo(_lengths); + + int maxLength = 0; + for (int i = 0; i < _symbolCount; i++) + { + int len = _lengths[i]; + if (len > maxLength) + { + maxLength = len; + } + } + + _numBits = maxLength; + + if (_numBits > 16) + { + return false; + } + + if (_numBits == 0) + { + return true; + } + + int tableSize = 1 << _numBits; + if (_table.Length < tableSize) + { + return false; + } + + var table = _table.Slice(0, tableSize); + table.Fill(ushort.MaxValue); + + int position = 0; + + for (int bitLength = 1; bitLength <= _numBits; bitLength++) + { + for (ushort symbol = 0; symbol < _symbolCount; symbol++) + { + if (_lengths[symbol] == bitLength) + { + int numToFill = 1 << (_numBits - bitLength); + if (position > tableSize - numToFill) + { + return false; + } + + table.Slice(position, numToFill).Fill(symbol); + position += numToFill; + } + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Decode(scoped ref LzxBitReader reader) + { + if (_numBits == 0) + { + return -1; + } + + if (!reader.TryPeekBits(_numBits, out uint peek)) + { + return -1; + } + + ushort symbol = _table[(int)peek]; + if (symbol == ushort.MaxValue) + { + return -1; + } + + int len = _lengths[symbol]; + if (len <= 0) + { + return -1; + } + + if (!reader.TryConsumeBits(len)) + { + return -1; + } + + return symbol; + } +} diff --git a/Library/DiscUtils.Core/Compression/Lzx.cs b/Library/DiscUtils.Core/Compression/Lzx.cs new file mode 100644 index 000000000..a7dd434cd --- /dev/null +++ b/Library/DiscUtils.Core/Compression/Lzx.cs @@ -0,0 +1,178 @@ +using System; +using System.Buffers; + +namespace DiscUtils.Compression; + +public sealed class Lzx : IBlockDecompressor, IDisposable +{ + private readonly int _windowBits; + private readonly int _windowSize; + private readonly int _mainTreeSymbols; + private bool _disposed; + + private byte[] _window; + private byte[] _mainLengths; + private byte[] _lengthLengths; + private byte[] _alignedLengths; + private byte[] _preTreeLengths; + + private ushort[] _mainTable; + private ushort[] _lengthTable; + private ushort[] _alignedTable; + private ushort[] _preTreeTable; + + public Lzx(int windowBits) + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(windowBits); + ArgumentOutOfRangeException.ThrowIfGreaterThan(windowBits, 25); +#else + if (windowBits <= 0 || windowBits > 25) + { + throw new ArgumentOutOfRangeException(nameof(windowBits)); + } +#endif + + _windowBits = windowBits; + _windowSize = 1 << windowBits; + _mainTreeSymbols = 256 + 16 * windowBits; + + _window = ArrayPool.Shared.Rent(_windowSize); + _mainLengths = ArrayPool.Shared.Rent(_mainTreeSymbols); + _lengthLengths = ArrayPool.Shared.Rent(249); + _alignedLengths = ArrayPool.Shared.Rent(8); + _preTreeLengths = ArrayPool.Shared.Rent(20); + + // Oversized but simple and legacy-like: one 16-bit lookup table per live tree. + _mainTable = ArrayPool.Shared.Rent(1 << 16); + _lengthTable = ArrayPool.Shared.Rent(1 << 16); + _alignedTable = ArrayPool.Shared.Rent(1 << 16); + _preTreeTable = ArrayPool.Shared.Rent(1 << 16); + } + + public int WindowBits => _windowBits; + + public int E8FixupMaxSize { get; set; } = 12000000; + + int IBlockDecompressor.BlockSize { get; set; } + + public static bool TryDecompress( + ReadOnlySpan source, + Span destination, + int windowBits, + out int bytesConsumed, + out int bytesWritten) + { + using var lzx = new Lzx(windowBits); + + return lzx.TryDecompress( + source, + destination, + out bytesConsumed, + out bytesWritten); + } + + public bool TryDecompress( + ReadOnlySpan source, + Span destination, + out int bytesConsumed, + out int bytesWritten) + { + var workspace = GetWorkspace(); + var decoder = new LzxDecoder(WindowBits, E8FixupMaxSize, workspace); + return decoder.TryDecompress(source, destination, out bytesConsumed, out bytesWritten); + } + + bool IBlockDecompressor.TryDecompress( + ReadOnlySpan source, + Span destination, + out int bytesWritten) + => TryDecompress(source, destination, out _, out bytesWritten); + + private LzxWorkspace GetWorkspace() + { +#if NET7_0_OR_GREATER + ObjectDisposedException.ThrowIf(_disposed, this); +#else + if (_disposed) + { + throw new ObjectDisposedException(nameof(Lzx)); + } +#endif + + return new LzxWorkspace( + _window.AsSpan(0, _windowSize), + _mainLengths.AsSpan(0, _mainTreeSymbols), + _lengthLengths.AsSpan(0, 249), + _alignedLengths.AsSpan(0, 8), + _preTreeLengths.AsSpan(0, 20), + _mainTable.AsSpan(), + _lengthTable.AsSpan(), + _alignedTable.AsSpan(), + _preTreeTable.AsSpan()); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_window is not null) + { + ArrayPool.Shared.Return(_window); + _window = null!; + } + + if (_mainLengths is not null) + { + ArrayPool.Shared.Return(_mainLengths); + _mainLengths = null!; + } + + if (_lengthLengths is not null) + { + ArrayPool.Shared.Return(_lengthLengths); + _lengthLengths = null!; + } + + if (_alignedLengths is not null) + { + ArrayPool.Shared.Return(_alignedLengths); + _alignedLengths = null!; + } + + if (_preTreeLengths is not null) + { + ArrayPool.Shared.Return(_preTreeLengths); + _preTreeLengths = null!; + } + + if (_mainTable is not null) + { + ArrayPool.Shared.Return(_mainTable); + _mainTable = null!; + } + + if (_lengthTable is not null) + { + ArrayPool.Shared.Return(_lengthTable); + _lengthTable = null!; + } + + if (_alignedTable is not null) + { + ArrayPool.Shared.Return(_alignedTable); + _alignedTable = null!; + } + + if (_preTreeTable is not null) + { + ArrayPool.Shared.Return(_preTreeTable); + _preTreeTable = null!; + } + } +} diff --git a/Library/DiscUtils.Core/Compression/LzxBitReader.cs b/Library/DiscUtils.Core/Compression/LzxBitReader.cs new file mode 100644 index 000000000..f566aaa5c --- /dev/null +++ b/Library/DiscUtils.Core/Compression/LzxBitReader.cs @@ -0,0 +1,188 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace DiscUtils.Compression; + +internal ref struct LzxBitReader +{ + private readonly ReadOnlySpan _source; + private int _rawPos; + private uint _bitBuffer; + private int _bitsAvailable; + private long _positionBits; + + public LzxBitReader(ReadOnlySpan source) + { + _source = source; + _rawPos = 0; + _bitBuffer = 0; + _bitsAvailable = 0; + _positionBits = 0; + } + + public int BytesConsumed => _rawPos; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Need(int count) + { + while (_bitsAvailable < count) + { + byte lo = 0; + byte hi = 0; + + if ((uint)_rawPos < (uint)_source.Length) + { + lo = _source[_rawPos++]; + } + + if ((uint)_rawPos < (uint)_source.Length) + { + hi = _source[_rawPos++]; + } + + _bitBuffer = (_bitBuffer << 16) | (uint)(lo | (hi << 8)); + _bitsAvailable += 16; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryPeekBits(int count, out uint value) + { + value = 0; + + if ((uint)count > 16u) + { + return false; + } + + if (_bitsAvailable < count) + { + Need(count); + } + + if (count == 0) + { + return true; + } + + uint mask = (1u << count) - 1u; + value = (_bitBuffer >> (_bitsAvailable - count)) & mask; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadBits(int count, out uint value) + { + value = 0; + + if ((uint)count > 16u) + { + return false; + } + + if (_bitsAvailable < count) + { + Need(count); + } + + _bitsAvailable -= count; + _positionBits += count; + + if (count == 0) + { + return true; + } + + uint mask = (1u << count) - 1u; + value = (_bitBuffer >> _bitsAvailable) & mask; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryConsumeBits(int count) + { + if ((uint)count > 16u) + { + return false; + } + + if (_bitsAvailable < count) + { + Need(count); + } + + _bitsAvailable -= count; + _positionBits += count; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool AlignTo16Bits() + { + // Match legacy Align(16): consumes 1..16 bits, never 0. + int offset = (int)(_positionBits % 16); + int consume = 16 - offset; + return TryConsumeBits(consume); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadRawByte(out byte value) + { + if ((_positionBits & 7) != 0) + { + value = 0; + return false; + } + + if ((uint)_rawPos >= (uint)_source.Length) + { + value = 0; + return false; + } + + value = _source[_rawPos++]; + _positionBits += 8; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadRawUInt32(out uint value) + { + value = 0; + + if ((_positionBits & 7) != 0) + { + return false; + } + + if ((uint)(_rawPos + 3) >= (uint)_source.Length) + { + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(_source.Slice(_rawPos, 4)); + _rawPos += 4; + _positionBits += 32; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadRawBytes(scoped Span destination) + { + if ((_positionBits & 7) != 0) + { + return false; + } + + if ((uint)_rawPos > (uint)_source.Length || destination.Length > _source.Length - _rawPos) + { + return false; + } + + _source.Slice(_rawPos, destination.Length).CopyTo(destination); + _rawPos += destination.Length; + _positionBits += destination.Length * 8L; + return true; + } +} diff --git a/Library/DiscUtils.Core/Compression/LzxDecoder.cs b/Library/DiscUtils.Core/Compression/LzxDecoder.cs new file mode 100644 index 000000000..3405d6198 --- /dev/null +++ b/Library/DiscUtils.Core/Compression/LzxDecoder.cs @@ -0,0 +1,754 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace DiscUtils.Compression; + +internal ref struct LzxDecoder +{ + private static readonly uint[] s_positionSlots; + private static readonly uint[] s_extraBits; + + private readonly int _windowBits; + private readonly int _windowSize; + private readonly int _E8FixupMaxSize; + private readonly int _numPositionSlots; + + private readonly LzxWorkspace _workspace; + + private Span _window; + private int _windowPos; + + private uint _r0; + private uint _r1; + private uint _r2; + + private Span _mainLengths; + private Span _lengthLengths; + private Span _alignedLengths; + private Span _preTreeLengths; + + static LzxDecoder() + { + var positionSlots = new uint[50]; + var extraBits = new uint[50]; + + uint numBits = 0; + positionSlots[1] = 1; + + for (int i = 2; i < 50; i += 2) + { + extraBits[i] = numBits; + extraBits[i + 1] = numBits; + positionSlots[i] = positionSlots[i - 1] + (uint)(1 << (int)extraBits[i - 1]); + positionSlots[i + 1] = positionSlots[i] + (uint)(1 << (int)numBits); + + if (numBits < 17) + { + numBits++; + } + } + + s_positionSlots = positionSlots; + s_extraBits = extraBits; + } + + public LzxDecoder(int windowBits, int e8FixupMaxSize, LzxWorkspace workspace) + { + _windowBits = windowBits; + _windowSize = 1 << windowBits; + _E8FixupMaxSize = e8FixupMaxSize; + _numPositionSlots = _windowBits * 2; + + _workspace = workspace; + + _window = workspace.Window.Slice(0, _windowSize); + _mainLengths = workspace.MainLengths.Slice(0, 256 + 8 * _numPositionSlots); + _lengthLengths = workspace.LengthLengths.Slice(0, 249); + _alignedLengths = workspace.AlignedLengths.Slice(0, 8); + _preTreeLengths = workspace.PreTreeLengths.Slice(0, 20); + + _window.Clear(); + _mainLengths.Clear(); + _lengthLengths.Clear(); + _alignedLengths.Clear(); + _preTreeLengths.Clear(); + + _windowPos = 0; + _r0 = 1; + _r1 = 1; + _r2 = 1; + } + + public bool TryDecompress( + ReadOnlySpan source, + Span destination, + out int bytesConsumed, + out int bytesWritten) + { + bytesConsumed = 0; + bytesWritten = 0; + + var reader = new LzxBitReader(source); + int dstPos = 0; + + if (!reader.TryReadBits(3, out uint blockTypeValue)) + { + return false; + } + + var blockType = (BlockType)blockTypeValue; + + while (blockType != BlockType.None && dstPos < destination.Length) + { + if (!TryReadBlockSize(ref reader, out int blockSize)) + { + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return false; + } + + switch (blockType) + { + case BlockType.Uncompressed: + if (!DecodeUncompressedBlock(ref reader, destination, ref dstPos, blockSize)) + { + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return false; + } + break; + + case BlockType.Verbatim: + case BlockType.AlignedOffset: + if (!DecodeCompressedBlock(ref reader, blockType, destination, ref dstPos, blockSize)) + { + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return false; + } + break; + + default: + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return false; + } + + if (!reader.TryReadBits(3, out blockTypeValue)) + { + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return false; + } + + blockType = (BlockType)blockTypeValue; + } + + if (_E8FixupMaxSize > 0) + { + ApplyE8Fixup(destination.Slice(0, dstPos), _E8FixupMaxSize); + } + + bytesConsumed = reader.BytesConsumed; + bytesWritten = dstPos; + return true; + } + + private static bool TryReadBlockSize(scoped ref LzxBitReader reader, out int blockSize) + { + blockSize = 0; + + if (!reader.TryReadBits(1, out uint hi)) + { + return false; + } + + if (hi == 1) + { + blockSize = 1 << 15; + return true; + } + + if (!reader.TryReadBits(16, out uint lo)) + { + return false; + } + + blockSize = (int)lo; + return true; + } + + private bool DecodeUncompressedBlock( + scoped ref LzxBitReader reader, + Span output, + ref int dstPos, + int blockSize) + { + if (!reader.AlignTo16Bits()) + { + return false; + } + + if (!reader.TryReadRawUInt32(out _r0) || + !reader.TryReadRawUInt32(out _r1) || + !reader.TryReadRawUInt32(out _r2)) + { + return false; + } + + int remainingOutput = output.Length - dstPos; + int bytesToStore = Math.Min(blockSize, remainingOutput); + + if (!reader.TryReadRawBytes(output.Slice(dstPos, bytesToStore))) + { + return false; + } + + for (int i = 0; i < bytesToStore; i++) + { + WriteWindowByte(output[dstPos + i]); + } + + dstPos += bytesToStore; + + int skipped = blockSize - bytesToStore; + if (skipped > 0) + { + Span scratch = stackalloc byte[256]; + + while (skipped > 0) + { + int chunk = Math.Min(skipped, scratch.Length); + if (!reader.TryReadRawBytes(scratch.Slice(0, chunk))) + { + return false; + } + + for (int i = 0; i < chunk; i++) + { + WriteWindowByte(scratch[i]); + } + + skipped -= chunk; + } + } + + if ((blockSize & 1) != 0) + { + if (!reader.TryReadRawByte(out _)) + { + return false; + } + } + + return true; + } + + private bool DecodeCompressedBlock( + scoped ref LzxBitReader reader, + BlockType blockType, + Span output, + ref int dstPos, + int blockSize) + { + LookupHuffmanDecoder alignedTree = default; + bool haveAlignedTree = false; + + if (blockType == BlockType.AlignedOffset) + { + if (!ReadAlignedTree(ref reader, out alignedTree)) + { + return false; + } + + haveAlignedTree = true; + } + + if (!ReadMainTree(ref reader, out var mainTree)) + { + return false; + } + + if (!ReadLengthTree(ref reader, out var lengthTree)) + { + return false; + } + + int blockRemaining = blockSize; + + while (blockRemaining > 0) + { + int symbol = mainTree.Decode(ref reader); + if (symbol < 0) + { + return false; + } + + if (symbol < 256) + { + WriteLiteral(output, ref dstPos, (byte)symbol); + blockRemaining--; + continue; + } + + int footer = symbol - 256; + int lengthHeader = footer & 7; + int positionSlot = footer >> 3; + + int matchLength = lengthHeader + 2; + if (lengthHeader == 7) + { + int extraLen = lengthTree.Decode(ref reader); + if (extraLen < 0) + { + return false; + } + + matchLength += extraLen; + } + + if (!TryDecodeMatchOffset( + ref reader, + blockType, + positionSlot, + haveAlignedTree, + ref alignedTree, + out uint matchOffset)) + { + return false; + } + + if (!CopyMatchFromWindow(output, ref dstPos, matchOffset, matchLength, ref blockRemaining)) + { + return false; + } + } + + return true; + } + + private bool ReadMainTree(scoped ref LzxBitReader reader, out LookupHuffmanDecoder mainTree) + { + var preTree = CreatePreTreeDecoder(); + if (!ReadFixedTree(ref reader, _preTreeLengths, 20, 4, ref preTree)) + { + mainTree = default; + return false; + } + + if (!ReadLengths(ref reader, ref preTree, _mainLengths, 0, 256)) + { + mainTree = default; + return false; + } + + preTree = CreatePreTreeDecoder(); + if (!ReadFixedTree(ref reader, _preTreeLengths, 20, 4, ref preTree)) + { + mainTree = default; + return false; + } + + if (!ReadLengths(ref reader, ref preTree, _mainLengths, 256, 8 * _numPositionSlots)) + { + mainTree = default; + return false; + } + + mainTree = CreateMainDecoder(); + return mainTree.Build(_mainLengths); + } + + private bool ReadLengthTree(scoped ref LzxBitReader reader, out LookupHuffmanDecoder lengthTree) + { + var preTree = CreatePreTreeDecoder(); + if (!ReadFixedTree(ref reader, _preTreeLengths, 20, 4, ref preTree)) + { + lengthTree = default; + return false; + } + + if (!ReadLengths(ref reader, ref preTree, _lengthLengths, 0, 249)) + { + lengthTree = default; + return false; + } + + lengthTree = CreateLengthDecoder(); + return lengthTree.Build(_lengthLengths); + } + + private bool ReadAlignedTree(scoped ref LzxBitReader reader, out LookupHuffmanDecoder alignedTree) + { + var decoder = CreateAlignedDecoder(); + + if (!ReadFixedTree(ref reader, _alignedLengths, 8, 3, ref decoder)) + { + alignedTree = default; + return false; + } + + alignedTree = decoder; + + return true; + } + + private static bool ReadFixedTree( + scoped ref LzxBitReader reader, + Span codeLengths, + int symbolCount, + int bitsPerLength, + scoped ref LookupHuffmanDecoder decoder) + { + for (int i = 0; i < symbolCount; i++) + { + if (!reader.TryReadBits(bitsPerLength, out uint value)) + { + return false; + } + + codeLengths[i] = (byte)value; + } + + return decoder.Build(codeLengths.Slice(0, symbolCount)); + } + + private static bool ReadLengths( + scoped ref LzxBitReader reader, + scoped ref LookupHuffmanDecoder preTree, + Span lengths, + int offset, + int count) + { + int i = 0; + + while (i < count) + { + int value = preTree.Decode(ref reader); + if (value < 0) + { + return false; + } + + if (value == 17) + { + if (!reader.TryReadBits(4, out uint n)) + { + return false; + } + + int numZeros = 4 + (int)n; + for (int j = 0; j < numZeros && i < count; j++) + { + lengths[offset + i++] = 0; + } + } + else if (value == 18) + { + if (!reader.TryReadBits(5, out uint n)) + { + return false; + } + + int numZeros = 20 + (int)n; + for (int j = 0; j < numZeros && i < count; j++) + { + lengths[offset + i++] = 0; + } + } + else if (value == 19) + { + if (!reader.TryReadBits(1, out uint same)) + { + return false; + } + + int extra = preTree.Decode(ref reader); + if ((uint)extra > 16u) + { + return false; + } + + byte symbol = (byte)((17 + lengths[offset + i] - extra) % 17); + int reps = 4 + (int)same; + + for (int j = 0; j < reps && i < count; j++) + { + lengths[offset + i++] = symbol; + } + } + else + { + lengths[offset + i] = (byte)((17 + lengths[offset + i] - value) % 17); + i++; + } + } + + return true; + } + + private bool TryDecodeMatchOffset( + scoped ref LzxBitReader reader, + BlockType blockType, + int positionSlot, + bool haveAlignedTree, + scoped ref LookupHuffmanDecoder alignedTree, + out uint matchOffset) + { + matchOffset = 0; + + if (positionSlot == 0) + { + matchOffset = _r0; + return true; + } + + if (positionSlot == 1) + { + matchOffset = _r1; + _r1 = _r0; + _r0 = matchOffset; + return true; + } + + if (positionSlot == 2) + { + matchOffset = _r2; + _r2 = _r0; + _r0 = matchOffset; + return true; + } + + int extra = (int)s_extraBits[positionSlot]; + uint formattedOffset; + + if (blockType == BlockType.AlignedOffset) + { + if (!haveAlignedTree) + { + return false; + } + + uint verbatimBits = 0; + uint alignedBits = 0; + + if (extra >= 3) + { + if (!reader.TryReadBits(extra - 3, out uint highBits)) + { + return false; + } + + verbatimBits = highBits << 3; + + int alignedSymbol = alignedTree.Decode(ref reader); + if (alignedSymbol < 0) + { + return false; + } + + alignedBits = (uint)alignedSymbol; + } + else if (extra > 0) + { + if (!reader.TryReadBits(extra, out verbatimBits)) + { + return false; + } + } + + formattedOffset = s_positionSlots[positionSlot] + verbatimBits + alignedBits; + } + else + { + uint verbatimBits = 0; + if (extra > 0) + { + if (!reader.TryReadBits(extra, out verbatimBits)) + { + return false; + } + } + + formattedOffset = s_positionSlots[positionSlot] + verbatimBits; + } + + matchOffset = formattedOffset - 2; + + _r2 = _r1; + _r1 = _r0; + _r0 = matchOffset; + return true; + } + + private bool CopyMatchFromWindow( + Span output, + ref int dstPos, + uint matchOffset, + int matchLength, + ref int blockRemaining) + { + if (matchOffset == 0 || matchOffset > (uint)_windowSize) + { + return false; + } + + if (matchLength < 0 || matchLength > blockRemaining) + { + return false; + } + + int src = _windowPos - (int)matchOffset; + if (src < 0) + { + src += _windowSize; + } + + while (matchLength > 0) + { + // How much can we read contiguously from current source position + // before wrapping the circular window? + int srcRun = _windowSize - src; + + // How much can we write contiguously to current window position + // before wrapping the circular window? + int dstRun = _windowSize - _windowPos; + + int chunk = Math.Min(matchLength, Math.Min(srcRun, dstRun)); + + // If source and destination are safely separated inside the current + // contiguous chunk, we can copy the whole chunk with Span.CopyTo. + // Otherwise fall back to byte-serial copying to preserve overlap behavior. + if (chunk > 0 && (src + chunk <= _windowPos || _windowPos + chunk <= src)) + { + var srcSlice = _window.Slice(src, chunk); + var dstSlice = _window.Slice(_windowPos, chunk); + + srcSlice.CopyTo(dstSlice); + + int outChunk = Math.Min(chunk, output.Length - dstPos); + if (outChunk > 0) + { + dstSlice.Slice(0, outChunk).CopyTo(output.Slice(dstPos, outChunk)); + } + + dstPos += chunk; + blockRemaining -= chunk; + matchLength -= chunk; + + _windowPos += chunk; + if (_windowPos == _windowSize) + { + _windowPos = 0; + } + + src += chunk; + if (src == _windowSize) + { + src = 0; + } + } + else + { + byte value = _window[src]; + + if ((uint)dstPos < (uint)output.Length) + { + output[dstPos] = value; + } + + _window[_windowPos] = value; + + dstPos++; + blockRemaining--; + matchLength--; + + src++; + if (src == _windowSize) + { + src = 0; + } + + _windowPos++; + if (_windowPos == _windowSize) + { + _windowPos = 0; + } + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteLiteral(Span output, ref int dstPos, byte value) + { + if ((uint)dstPos < (uint)output.Length) + { + output[dstPos] = value; + } + + dstPos++; + WriteWindowByte(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LookupHuffmanDecoder CreateMainDecoder() + => new(_mainLengths.Length, _mainLengths, _workspace.MainTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LookupHuffmanDecoder CreateLengthDecoder() + => new(249, _lengthLengths, _workspace.LengthTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LookupHuffmanDecoder CreateAlignedDecoder() + => new(8, _alignedLengths, _workspace.AlignedTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LookupHuffmanDecoder CreatePreTreeDecoder() + => new(20, _preTreeLengths, _workspace.PreTreeTable); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteWindowByte(byte value) + { + _window[_windowPos] = value; + _windowPos++; + if (_windowPos == _windowSize) + { + _windowPos = 0; + } + } + + private static void ApplyE8Fixup(Span buffer, int fileSize) + { + int i = 0; + while (i < buffer.Length - 10) + { + if (buffer[i] == 0xE8) + { + int absoluteValue = BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(i + 1, 4)); + if (absoluteValue >= -i && absoluteValue < fileSize) + { + int offsetValue = absoluteValue >= 0 + ? absoluteValue - i + : absoluteValue + fileSize; + + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(i + 1, 4), offsetValue); + } + + i += 4; + } + + i++; + } + } + + private enum BlockType + { + None = 0, + Verbatim = 1, + AlignedOffset = 2, + Uncompressed = 3 + } +} \ No newline at end of file diff --git a/Library/DiscUtils.Core/Compression/LzxStream.cs b/Library/DiscUtils.Core/Compression/LzxStream.cs index 3327bb87c..ececb77a8 100644 --- a/Library/DiscUtils.Core/Compression/LzxStream.cs +++ b/Library/DiscUtils.Core/Compression/LzxStream.cs @@ -202,8 +202,7 @@ private void ReadBlocks() /// conversion. private void FixupBlockBuffer() { - var i = 0; - while (i < _bufferCount - 10) + for (var i = 0; i < _bufferCount - 10; i++) { if (_buffer[i] == 0xE8) { @@ -212,6 +211,7 @@ private void FixupBlockBuffer() if (absoluteValue >= -i && absoluteValue < _fileSize) { int offsetValue; + if (absoluteValue >= 0) { offsetValue = absoluteValue - i; @@ -226,8 +226,6 @@ private void FixupBlockBuffer() i += 4; } - - ++i; } } diff --git a/Library/DiscUtils.Core/Compression/LzxWorkspace.cs b/Library/DiscUtils.Core/Compression/LzxWorkspace.cs new file mode 100644 index 000000000..b8e0fdaa0 --- /dev/null +++ b/Library/DiscUtils.Core/Compression/LzxWorkspace.cs @@ -0,0 +1,40 @@ +using System; + +namespace DiscUtils.Compression; + +internal ref struct LzxWorkspace +{ + public Span Window; + + public Span MainLengths; + public Span LengthLengths; + public Span AlignedLengths; + public Span PreTreeLengths; + + public Span MainTable; + public Span LengthTable; + public Span AlignedTable; + public Span PreTreeTable; + + public LzxWorkspace( + Span window, + Span mainLengths, + Span lengthLengths, + Span alignedLengths, + Span preTreeLengths, + Span mainTable, + Span lengthTable, + Span alignedTable, + Span preTreeTable) + { + Window = window; + MainLengths = mainLengths; + LengthLengths = lengthLengths; + AlignedLengths = alignedLengths; + PreTreeLengths = preTreeLengths; + MainTable = mainTable; + LengthTable = lengthTable; + AlignedTable = alignedTable; + PreTreeTable = preTreeTable; + } +} diff --git a/Library/DiscUtils.MountDokan/DokanDiscUtils.cs b/Library/DiscUtils.MountDokan/DokanDiscUtils.cs index 2bc72fff6..9d6f7ef2d 100644 --- a/Library/DiscUtils.MountDokan/DokanDiscUtils.cs +++ b/Library/DiscUtils.MountDokan/DokanDiscUtils.cs @@ -1295,7 +1295,8 @@ private FileAttributes FilterAttributes(FileAttributes attributes) public IEnumerable FindFilesHelper(ReadOnlyNativeMemory pathPtr, string searchPattern) { - var OSPath = pathPtr.Span.Trim('\\').ToString(); + var pathTrimmed = pathPtr.Span.Trim('\\'); + var OSPath = pathTrimmed.IsEmpty ? string.Empty : pathTrimmed.ToString(); var path = TranslatePath(OSPath); searchPattern ??= "*"; diff --git a/Library/DiscUtils.Ntfs/FileNameRecord.cs b/Library/DiscUtils.Ntfs/FileNameRecord.cs index ca00ae41c..05fbfc68a 100644 --- a/Library/DiscUtils.Ntfs/FileNameRecord.cs +++ b/Library/DiscUtils.Ntfs/FileNameRecord.cs @@ -25,6 +25,7 @@ using System.Text; using DiscUtils.Streams; using DiscUtils.Streams.Compatibility; +using LTRData.Extensions.Buffers; namespace DiscUtils.Ntfs; @@ -63,6 +64,20 @@ public FileNameRecord(FileNameRecord toCopy) public int Size => 0x42 + FileName.Length * 2; + public string SearchName + { + get + { + var fileName = FileName; + if (!fileName.Contains('.')) + { + return $"{fileName}."; + } + + return fileName; + } + } + public int ReadFrom(ReadOnlySpan buffer) { ParentDirectory = new FileRecordReference(EndianUtilities.ToUInt64LittleEndian(buffer)); diff --git a/Library/DiscUtils.Ntfs/Internals/WofStream.cs b/Library/DiscUtils.Ntfs/Internals/WofStream.cs index ca966b6c0..28bd41434 100644 --- a/Library/DiscUtils.Ntfs/Internals/WofStream.cs +++ b/Library/DiscUtils.Ntfs/Internals/WofStream.cs @@ -42,6 +42,29 @@ internal class WofStream(long uncompressedSize, Wof.CompressionFormat compressionFormat, SparseStream compressedData) : SparseStream { + private readonly IBlockDecompressor _blockDecompressor = GetDecompressor(compressionFormat); + + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + if (disposing) + { + if (_blockDecompressor is IDisposable disposable) + { + disposable.Dispose(); + } + + compressedData.Dispose(); + } + + compressedData = null; + + base.Dispose(disposing); + } + private (long offset, int size, int chunkSize) PrepareReadChunk(int chunkIndex) { long offset = chunkTableSize; @@ -70,27 +93,12 @@ internal class WofStream(long uncompressedSize, return (offset, size, chunkSize); } - private Stream GetDecompressStream(int chunkSize, Stream compressedData, long offset, int size) - { - var compressed = new SubStream(compressedData, Ownership.None, offset, size); - - if (compressionFormat == Wof.CompressionFormat.LZX) - { - return new LzxStream(compressed, windowBits: 15, chunkSize); - } - - return new XpressStream(compressed, chunkSize); - } - - private IBlockDecompressor GetDecompressor() + private static IBlockDecompressor GetDecompressor(Wof.CompressionFormat compressionFormat) => compressionFormat switch { - if (compressionFormat == Wof.CompressionFormat.LZX) - { - return null; - } - - return XpressHuffman.Default; - } + Wof.CompressionFormat.LZX => new Lzx(windowBits: 15), + Wof.CompressionFormat.XPress4K or Wof.CompressionFormat.XPress8K or Wof.CompressionFormat.XPress16K => XpressHuffman.Default, + _ => null, + }; private int ReadChunk(Span uncompressedData, int chunkIndex) { @@ -98,33 +106,24 @@ private int ReadChunk(Span uncompressedData, int chunkIndex) compressedData.Position = offset; - if (size == chunkSize) + if (size == chunkSize || _blockDecompressor is null) { return compressedData.Read(uncompressedData.Slice(0, chunkSize)); } - if (GetDecompressor() is { } decompressor) - { - var compressed = ArrayPool.Shared.Rent(size); + var compressed = ArrayPool.Shared.Rent(size); - try - { - compressedData.ReadExactly(compressed, 0, size); + try + { + compressedData.ReadExactly(compressed, 0, size); - decompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.Slice(0, chunkSize), out var decompressedSize); + _blockDecompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.Slice(0, chunkSize), out var decompressedSize); - return decompressedSize; - } - finally - { - ArrayPool.Shared.Return(compressed); - } + return decompressedSize; } - else + finally { - using var decompressStream = GetDecompressStream(chunkSize, compressedData, offset, size); - - return decompressStream.Read(uncompressedData.Slice(0, chunkSize)); + ArrayPool.Shared.Return(compressed); } } @@ -134,33 +133,24 @@ private async ValueTask ReadChunkAsync(Memory uncompressedData, int c compressedData.Position = offset; - if (size == chunkSize) + if (size == chunkSize || _blockDecompressor is null) { return await compressedData.ReadAsync(uncompressedData.Slice(0, chunkSize), cancellationToken).ConfigureAwait(false); } - if (GetDecompressor() is { } decompressor) - { - var compressed = ArrayPool.Shared.Rent(size); + var compressed = ArrayPool.Shared.Rent(size); - try - { - await compressedData.ReadExactlyAsync(compressed.AsMemory(0, size), cancellationToken).ConfigureAwait(false); + try + { + await compressedData.ReadExactlyAsync(compressed.AsMemory(0, size), cancellationToken).ConfigureAwait(false); - decompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.Span.Slice(0, chunkSize), out var decompressedSize); + _blockDecompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.Span.Slice(0, chunkSize), out var decompressedSize); - return decompressedSize; - } - finally - { - ArrayPool.Shared.Return(compressed); - } + return decompressedSize; } - else + finally { - using var decompressStream = GetDecompressStream(chunkSize, compressedData, offset, size); - - return await decompressStream.ReadAsync(uncompressedData.Slice(0, chunkSize), cancellationToken).ConfigureAwait(false); + ArrayPool.Shared.Return(compressed); } } @@ -170,33 +160,24 @@ private int ReadChunk(byte[] uncompressedData, int byteOffset, int chunkIndex) compressedData.Position = offset; - if (size == chunkSize) + if (size == chunkSize || _blockDecompressor is null) { return compressedData.Read(uncompressedData, byteOffset, chunkSize); } - if (GetDecompressor() is { } decompressor) - { - var compressed = ArrayPool.Shared.Rent(size); + var compressed = ArrayPool.Shared.Rent(size); - try - { - compressedData.ReadExactly(compressed, 0, size); + try + { + compressedData.ReadExactly(compressed, 0, size); - decompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.AsSpan(byteOffset, chunkSize), out var decompressedSize); + _blockDecompressor.TryDecompress(compressed.AsSpan(0, size), uncompressedData.AsSpan(byteOffset, chunkSize), out var decompressedSize); - return decompressedSize; - } - finally - { - ArrayPool.Shared.Return(compressed); - } + return decompressedSize; } - else + finally { - using var decompressStream = GetDecompressStream(chunkSize, compressedData, offset, size); - - return decompressStream.Read(uncompressedData, byteOffset, chunkSize); + ArrayPool.Shared.Return(compressed); } } diff --git a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs index a4a0e0b7f..e313ce9f4 100644 --- a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs +++ b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs @@ -600,7 +600,7 @@ public override IEnumerable GetFileSystemEntries(string path, string sea var parentDir = GetDirectory(parentDirEntry.Reference); var results = parentDir.GetAllEntries(FilterEntry) - .Where(dirEntry => filter is null || filter(dirEntry.Details.FileName)) + .Where(dirEntry => filter is null || filter(dirEntry.Details.SearchName)) .Select(dirEntry => Utilities.CombinePaths(path, dirEntry.Details.FileName)); foreach (var result in results) diff --git a/Library/DiscUtils.Wim/FileResourceStream.cs b/Library/DiscUtils.Wim/FileResourceStream.cs index 5895f1050..46dabec41 100644 --- a/Library/DiscUtils.Wim/FileResourceStream.cs +++ b/Library/DiscUtils.Wim/FileResourceStream.cs @@ -20,15 +20,15 @@ // DEALINGS IN THE SOFTWARE. // +using DiscUtils.Compression; +using DiscUtils.Streams; +using LTRData.Extensions.Buffers; using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; -using DiscUtils.Compression; -using DiscUtils.Streams; -using DiscUtils.Streams.Compatibility; -using LTRData.Extensions.Buffers; namespace DiscUtils.Wim; @@ -38,8 +38,6 @@ namespace DiscUtils.Wim; /// Stream access must be strictly sequential. internal class FileResourceStream : SparseStream.ReadOnlySparseStream { - private const int E8DecodeFileSize = 12000000; - private readonly Stream _baseStream; private readonly long[] _chunkLength; @@ -47,18 +45,19 @@ internal class FileResourceStream : SparseStream.ReadOnlySparseStream private readonly int _chunkSize; private int _currentChunk; - private Stream? _currentChunkStream; + private byte[]? _chunkBuffer; + private ReadOnlyMemory _currentChunkData; private readonly ShortResourceHeader _header; - private readonly bool _lzxCompression; + private readonly IBlockDecompressor? _blockDecompressor; private readonly long _offsetDelta; private long _position; - public FileResourceStream(Stream baseStream, ShortResourceHeader header, bool lzxCompression, int chunkSize) + public FileResourceStream(Stream baseStream, ShortResourceHeader header, FileFlags fileFlags, int chunkSize) { _baseStream = baseStream; _header = header; - _lzxCompression = lzxCompression; + _blockDecompressor = GetDecompressor(fileFlags); _chunkSize = chunkSize; if (baseStream.Length > uint.MaxValue) @@ -84,6 +83,41 @@ public FileResourceStream(Stream baseStream, ShortResourceHeader header, bool lz _currentChunk = -1; } + private static IBlockDecompressor? GetDecompressor(FileFlags flags) + { + if ((flags & FileFlags.LzxCompression) != 0) + { + return new Lzx(windowBits: 15); + } + else if ((flags & FileFlags.XpressCompression) != 0) + { + return XpressHuffman.Default; + } + + return null; + } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + if (disposing) + { + if (_blockDecompressor is IDisposable disposable) + { + disposable.Dispose(); + } + + if (_chunkBuffer is not null) + { + ArrayPool.Shared.Return(_chunkBuffer); + _chunkBuffer = null; + } + } + + base.Dispose(disposing); + } + public override bool CanRead => true; public override bool CanSeek => false; @@ -99,9 +133,20 @@ public override long Position set => _position = value; } + + public bool IsDisposed { get; private set; } public override int Read(byte[] buffer, int offset, int count) { +#if NET7_0_OR_GREATER + ObjectDisposedException.ThrowIf(IsDisposed, this); +#else + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(FileResourceStream)); + } +#endif + if (_position >= Length) { return 0; @@ -110,27 +155,29 @@ public override int Read(byte[] buffer, int offset, int count) var maxToRead = (int)Math.Min(Length - _position, count); var totalRead = 0; + while (totalRead < maxToRead) { var chunk = (int)(_position / _chunkSize); var chunkOffset = (int)(_position % _chunkSize); var numToRead = Math.Min(maxToRead - totalRead, _chunkSize - chunkOffset); - if (_currentChunk != chunk || _currentChunkStream is null) + if (numToRead == 0) { - _currentChunkStream = OpenChunkStream(chunk); - _currentChunk = chunk; + return totalRead; } - _currentChunkStream.Position = chunkOffset; - var numRead = _currentChunkStream.Read(buffer, offset + totalRead, numToRead); - if (numRead == 0) + if (_currentChunk != chunk || _currentChunkData.IsEmpty) { - return totalRead; + _chunkBuffer ??= ArrayPool.Shared.Rent(_chunkSize); + _currentChunkData = new(_chunkBuffer, 0, DecompressChunk(chunk, _chunkBuffer)); + _currentChunk = chunk; } - _position += numRead; - totalRead += numRead; + _currentChunkData.Span.Slice(chunkOffset, numToRead).CopyTo(buffer.AsSpan(offset + totalRead, numToRead)); + + _position += numToRead; + totalRead += numToRead; } return totalRead; @@ -138,6 +185,15 @@ public override int Read(byte[] buffer, int offset, int count) public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { +#if NET7_0_OR_GREATER + ObjectDisposedException.ThrowIf(IsDisposed, this); +#else + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(FileResourceStream)); + } +#endif + if (_position >= Length) { return 0; @@ -146,27 +202,29 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation var maxToRead = (int)Math.Min(Length - _position, buffer.Length); var totalRead = 0; + while (totalRead < maxToRead) { var chunk = (int)(_position / _chunkSize); var chunkOffset = (int)(_position % _chunkSize); var numToRead = Math.Min(maxToRead - totalRead, _chunkSize - chunkOffset); - if (_currentChunk != chunk || _currentChunkStream is null) + if (numToRead == 0) { - _currentChunkStream = OpenChunkStream(chunk); - _currentChunk = chunk; + return totalRead; } - _currentChunkStream.Position = chunkOffset; - var numRead = await _currentChunkStream.ReadAsync(buffer.Slice(totalRead, numToRead), cancellationToken).ConfigureAwait(false); - if (numRead == 0) + if (_currentChunk != chunk || _currentChunkData.IsEmpty) { - return totalRead; + _chunkBuffer ??= ArrayPool.Shared.Rent(_chunkSize); + _currentChunkData = new(_chunkBuffer, 0, await DecompressChunkAsync(chunk, _chunkBuffer, cancellationToken).ConfigureAwait(false)); + _currentChunk = chunk; } - _position += numRead; - totalRead += numRead; + _currentChunkData.Slice(chunkOffset, numToRead).CopyTo(buffer.Slice(totalRead, numToRead)); + + _position += numToRead; + totalRead += numToRead; } return totalRead; @@ -174,6 +232,15 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation public override int Read(Span buffer) { +#if NET7_0_OR_GREATER + ObjectDisposedException.ThrowIf(IsDisposed, this); +#else + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(FileResourceStream)); + } +#endif + if (_position >= Length) { return 0; @@ -182,27 +249,29 @@ public override int Read(Span buffer) var maxToRead = (int)Math.Min(Length - _position, buffer.Length); var totalRead = 0; + while (totalRead < maxToRead) { var chunk = (int)(_position / _chunkSize); var chunkOffset = (int)(_position % _chunkSize); var numToRead = Math.Min(maxToRead - totalRead, _chunkSize - chunkOffset); - if (_currentChunk != chunk || _currentChunkStream is null) + if (numToRead == 0) { - _currentChunkStream = OpenChunkStream(chunk); - _currentChunk = chunk; + return totalRead; } - _currentChunkStream.Position = chunkOffset; - var numRead = _currentChunkStream.Read(buffer.Slice(totalRead, numToRead)); - if (numRead == 0) + if (_currentChunk != chunk || _currentChunkData.IsEmpty) { - return totalRead; + _chunkBuffer ??= ArrayPool.Shared.Rent(_chunkSize); + _currentChunkData = new(_chunkBuffer, 0, DecompressChunk(chunk, _chunkBuffer)); + _currentChunk = chunk; } - _position += numRead; - totalRead += numRead; + _currentChunkData.Span.Slice(chunkOffset, numToRead).CopyTo(buffer.Slice(totalRead, numToRead)); + + _position += numToRead; + totalRead += numToRead; } return totalRead; @@ -213,7 +282,7 @@ public override long Seek(long offset, SeekOrigin origin) throw new NotSupportedException(); } - private Stream OpenChunkStream(int chunk) + private int DecompressChunk(int chunk, Memory buffer) { var targetUncompressed = _chunkSize; if (chunk == _chunkLength.Length - 1) @@ -221,17 +290,71 @@ private Stream OpenChunkStream(int chunk) targetUncompressed = (int)(Length - _position); } - Stream rawChunkStream = new SubStream(_baseStream, _offsetDelta + _chunkOffsets[chunk], _chunkLength[chunk]); - if ((_header.Flags & ResourceFlags.Compressed) != 0 && _chunkLength[chunk] != targetUncompressed) + var compressedSize = (int)_chunkLength[chunk]; + + _baseStream.Position = _offsetDelta + _chunkOffsets[chunk]; + + if (_blockDecompressor is null + || (_header.Flags & ResourceFlags.Compressed) == 0 || _chunkLength[chunk] == targetUncompressed) + { + return _baseStream.ReadMaximum(buffer.Span.Slice(0, compressedSize)); + } + + var rawChunk = ArrayPool.Shared.Rent(compressedSize); + try { - if (_lzxCompression) + _baseStream.ReadExactly(rawChunk.AsSpan(0, compressedSize)); + + var bufferSpan = buffer.Span; + + bufferSpan.Clear(); + + if (!_blockDecompressor.TryDecompress(rawChunk.AsSpan(0, compressedSize), bufferSpan.Slice(0, targetUncompressed), out var bytesWritten)) { - return new LzxStream(rawChunkStream, windowBits: 15, E8DecodeFileSize); + throw new IOException($"Failed to decompress chunk {chunk} in resource at location {_header.FileOffset}"); } + } + finally + { + ArrayPool.Shared.Return(rawChunk); + } + + return targetUncompressed; + } + + private async ValueTask DecompressChunkAsync(int chunk, Memory buffer, CancellationToken cancellationToken) + { + var targetUncompressed = _chunkSize; + if (chunk == _chunkLength.Length - 1) + { + targetUncompressed = (int)(Length - _position); + } + + var compressedSize = (int)_chunkLength[chunk]; - return new XpressStream(rawChunkStream, targetUncompressed); + _baseStream.Position = _offsetDelta + _chunkOffsets[chunk]; + + if (_blockDecompressor is null + || (_header.Flags & ResourceFlags.Compressed) == 0 || _chunkLength[chunk] == targetUncompressed) + { + return await _baseStream.ReadMaximumAsync(buffer.Slice(0, compressedSize), cancellationToken).ConfigureAwait(false); + } + + var rawChunk = ArrayPool.Shared.Rent(compressedSize); + try + { + await _baseStream.ReadExactlyAsync(rawChunk.AsMemory(0, compressedSize), cancellationToken).ConfigureAwait(false); + + if (!_blockDecompressor.TryDecompress(rawChunk.AsSpan(0, compressedSize), buffer.Span.Slice(0, targetUncompressed), out var bytesWritten)) + { + throw new IOException($"Failed to decompress chunk {chunk} in resource at location {_header.FileOffset}"); + } + } + finally + { + ArrayPool.Shared.Return(rawChunk); } - return rawChunkStream; + return targetUncompressed; } } \ No newline at end of file diff --git a/Library/DiscUtils.Wim/WimFile.cs b/Library/DiscUtils.Wim/WimFile.cs index 571edff00..f4b562e93 100644 --- a/Library/DiscUtils.Wim/WimFile.cs +++ b/Library/DiscUtils.Wim/WimFile.cs @@ -20,6 +20,7 @@ // DEALINGS IN THE SOFTWARE. // +using DiscUtils.Compression; using DiscUtils.Streams; using System; using System.Collections.Generic; @@ -194,12 +195,13 @@ internal SparseStream OpenResourceStream(ShortResourceHeader hdr) { SparseStream fileSectionStream = new SubStream(FileStream, Ownership.None, hdr.FileOffset, hdr.CompressedSize); + if ((hdr.Flags & ResourceFlags.Compressed) == 0) { return fileSectionStream; } - return new FileResourceStream(fileSectionStream, hdr, (_fileHeader.Flags & FileFlags.LzxCompression) != 0, + return new FileResourceStream(fileSectionStream, hdr, _fileHeader.Flags, _fileHeader.CompressionSize); } diff --git a/Library/DiscUtils.Wim/WimFileSystem.cs b/Library/DiscUtils.Wim/WimFileSystem.cs index e891a56ac..fa8afdfae 100644 --- a/Library/DiscUtils.Wim/WimFileSystem.cs +++ b/Library/DiscUtils.Wim/WimFileSystem.cs @@ -238,6 +238,11 @@ public long GetFileId(string path) var dirEntry = GetEntry(path) ?? throw new FileNotFoundException("File or directory not found", path); + if (dirEntry.Hash.IsDefaultOrEmpty) + { + return -1; + } + return MemoryMarshal.Read(dirEntry.Hash.AsSpan()) ^ MemoryMarshal.Read(dirEntry.Hash.AsSpan().Slice(8)) ^ MemoryMarshal.Read(dirEntry.Hash.AsSpan().Slice(16)); @@ -347,7 +352,7 @@ public override IEnumerable GetFileSystemEntries(string path, string sea var parentDir = GetDirectory(parentDirEntry.SubdirOffset); var result = parentDir - .Where(dirEntry => filter is null || filter(dirEntry.FileName)) + .Where(dirEntry => filter is null || filter(dirEntry.SearchName)) .Select(dirEntry => Utilities.CombinePaths(path, dirEntry.FileName)); return result;