From 2d4d9c285a132eae76ec2c8d07a91a031039b013 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:45:49 +0000 Subject: [PATCH 1/9] Fix sync methods called in async RAR unpacker paths (Unpack29Async, Unpack5Async) Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/7f2fcb59-4a41-4b27-ab32-17afec510b5e Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Compressors/Rar/UnpackV1/Unpack.Async.cs | 201 +++++++++++++++++- .../Rar/UnpackV1/Unpack50.Async.cs | 4 +- src/SharpCompress/packages.lock.json | 12 +- 3 files changed, 205 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs index 37655cf1..0581ba26 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -112,7 +113,10 @@ internal sealed partial class Unpack { return; } - if ((!solid || !tablesRead) && !ReadTables()) + if ( + (!solid || !tablesRead) + && !await ReadTablesAsync(cancellationToken).ConfigureAwait(false) + ) { return; } @@ -161,7 +165,7 @@ internal sealed partial class Unpack var NextCh = ppm.DecodeChar(); if (NextCh == 0) { - if (!ReadTables()) + if (!await ReadTablesAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -294,7 +298,7 @@ internal sealed partial class Unpack } if (Number == 256) { - if (!ReadEndOfBlock()) + if (!await ReadEndOfBlockAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -302,7 +306,7 @@ internal sealed partial class Unpack } if (Number == 257) { - if (!ReadVMCode()) + if (!await ReadVMCodeAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -600,4 +604,193 @@ internal sealed partial class Unpack writtenFileSize += size; destUnpSize -= size; } + + private async Task ReadTablesAsync(CancellationToken cancellationToken = default) + { + var bitLength = new byte[PackDef.BC]; + var table = new byte[PackDef.HUFF_TABLE_SIZE]; + + if (inAddr > readTop - 25) + { + if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false)) + { + return false; + } + } + AddBits((8 - inBit) & 7); + long bitField = GetBits() & unchecked((int)0xffFFffFF); + if ((bitField & 0x8000) != 0) + { + unpBlockType = BlockTypes.BLOCK_PPM; + return ppm.DecodeInit(this, PpmEscChar); + } + unpBlockType = BlockTypes.BLOCK_LZ; + + prevLowDist = 0; + lowDistRepCount = 0; + + if ((bitField & 0x4000) == 0) + { + new Span(unpOldTable).Clear(); + } + AddBits(2); + + for (var i = 0; i < PackDef.BC; i++) + { + var length = (Utility.URShift(GetBits(), 12)) & 0xFF; + AddBits(4); + if (length == 15) + { + var zeroCount = (Utility.URShift(GetBits(), 12)) & 0xFF; + AddBits(4); + if (zeroCount == 0) + { + bitLength[i] = 15; + } + else + { + zeroCount += 2; + while (zeroCount-- > 0 && i < bitLength.Length) + { + bitLength[i++] = 0; + } + i--; + } + } + else + { + bitLength[i] = (byte)length; + } + } + + UnpackUtility.makeDecodeTables(bitLength, 0, BD, PackDef.BC); + + var TableSize = PackDef.HUFF_TABLE_SIZE; + + for (var i = 0; i < TableSize; ) + { + if (inAddr > readTop - 5) + { + if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false)) + { + return false; + } + } + var Number = this.decodeNumber(BD); + if (Number < 16) + { + table[i] = (byte)((Number + unpOldTable[i]) & 0xf); + i++; + } + else if (Number < 18) + { + int N; + if (Number == 16) + { + N = (Utility.URShift(GetBits(), 13)) + 3; + AddBits(3); + } + else + { + N = (Utility.URShift(GetBits(), 9)) + 11; + AddBits(7); + } + while (N-- > 0 && i < TableSize) + { + table[i] = table[i - 1]; + i++; + } + } + else + { + int N; + if (Number == 18) + { + N = (Utility.URShift(GetBits(), 13)) + 3; + AddBits(3); + } + else + { + N = (Utility.URShift(GetBits(), 9)) + 11; + AddBits(7); + } + while (N-- > 0 && i < TableSize) + { + table[i++] = 0; + } + } + } + tablesRead = true; + if (inAddr > readTop) + { + return false; + } + UnpackUtility.makeDecodeTables(table, 0, LD, PackDef.NC); + UnpackUtility.makeDecodeTables(table, PackDef.NC, DD, PackDef.DC); + UnpackUtility.makeDecodeTables(table, PackDef.NC + PackDef.DC, LDD, PackDef.LDC); + UnpackUtility.makeDecodeTables( + table, + PackDef.NC + PackDef.DC + PackDef.LDC, + RD, + PackDef.RC + ); + + new Span(table).CopyTo(unpOldTable); + return true; + } + + private async Task ReadEndOfBlockAsync(CancellationToken cancellationToken = default) + { + var BitField = GetBits(); + bool NewTable, + NewFile = false; + if ((BitField & 0x8000) != 0) + { + NewTable = true; + AddBits(1); + } + else + { + NewFile = true; + NewTable = (BitField & 0x4000) != 0; + AddBits(2); + } + tablesRead = !NewTable; + return !( + NewFile || NewTable && !await ReadTablesAsync(cancellationToken).ConfigureAwait(false) + ); + } + + private async Task ReadVMCodeAsync(CancellationToken cancellationToken = default) + { + var FirstByte = GetBits() >> 8; + AddBits(8); + var Length = (FirstByte & 7) + 1; + if (Length == 7) + { + Length = (GetBits() >> 8) + 7; + AddBits(8); + } + else if (Length == 8) + { + Length = GetBits(); + AddBits(16); + } + + var vmCode = new List(); + for (var I = 0; I < Length; I++) + { + if ( + inAddr >= readTop - 1 + && !await unpReadBufAsync(cancellationToken).ConfigureAwait(false) + && I < Length - 1 + ) + { + return false; + } + vmCode.Add((byte)(GetBits() >> 8)); + AddBits(8); + } + return AddVMCode(FirstByte, vmCode); + } } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs index 48df787e..432922c4 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs @@ -70,7 +70,7 @@ internal partial class Unpack // So we can safefly use these tables below. if ( !await ReadBlockHeaderAsync(cancellationToken).ConfigureAwait(false) - || !ReadTables() + || !await ReadTablesAsync(cancellationToken).ConfigureAwait(false) || !TablesRead5 ) { @@ -101,7 +101,7 @@ internal partial class Unpack } if ( !await ReadBlockHeaderAsync(cancellationToken).ConfigureAwait(false) - || !ReadTables() + || !await ReadTablesAsync(cancellationToken).ConfigureAwait(false) ) { return; diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 7d5f0448..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 896dfd6537a9d2e1115e3ed018671dc53cf1da24 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 08:32:44 +0100 Subject: [PATCH 2/9] some AI changes --- .../Compressors/PPMd/H/ModelPPM.cs | 59 ++++++++++++++ .../Compressors/PPMd/H/RangeCoder.cs | 22 +++++- .../Compressors/Rar/UnpackV1/Unpack.Async.cs | 73 ++++++++++++++++-- .../Rar/UnpackV1/Unpack15.Async.cs | 4 +- .../Rar/UnpackV1/Unpack20.Async.cs | 27 ++++++- .../Rar/UnpackV1/Unpack50.Async.cs | 26 ++++++- .../Rar/RarReaderAsyncTests.cs | 76 +++++++++++++++++++ 7 files changed, 269 insertions(+), 18 deletions(-) diff --git a/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs b/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs index 793dccae..1718cbb4 100644 --- a/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs +++ b/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs @@ -333,6 +333,65 @@ internal class ModelPpm return (_minContext.Address != 0); } + internal async ValueTask DecodeInitAsync( + IRarUnpack unpackRead, + int escChar, + CancellationToken cancellationToken = default + ) + { + var maxOrder = + await unpackRead.ReadCharAsync(cancellationToken).ConfigureAwait(false) & 0xff; + var reset = ((maxOrder & 0x20) != 0); + + var maxMb = 0; + if (reset) + { + maxMb = await unpackRead.ReadCharAsync(cancellationToken).ConfigureAwait(false); + } + else + { + if (SubAlloc.GetAllocatedMemory() == 0) + { + return false; + } + } + if ((maxOrder & 0x40) != 0) + { + escChar = await unpackRead.ReadCharAsync(cancellationToken).ConfigureAwait(false); + unpackRead.PpmEscChar = escChar; + } + Coder = new RangeCoder(); + await Coder.InitAsync(unpackRead, cancellationToken).ConfigureAwait(false); + if (reset) + { + maxOrder = (maxOrder & 0x1f) + 1; + if (maxOrder > 16) + { + maxOrder = 16 + ((maxOrder - 16) * 3); + } + if (maxOrder == 1) + { + SubAlloc.StopSubAllocator(); + return false; + } + SubAlloc.StartSubAllocator((maxMb + 1) << 20); + _minContext = new PpmContext(Heap); + + _maxContext = new PpmContext(Heap); + FoundState = new State(Heap); + _dummySee2Cont = new See2Context(); + for (var i = 0; i < 25; i++) + { + for (var j = 0; j < 16; j++) + { + _see2Cont[i][j] = new See2Context(); + } + } + StartModelRare(maxOrder); + } + return _minContext.Address != 0; + } + public virtual int DecodeChar() { // Debug diff --git a/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs b/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs index 0a3b2321..59bdd926 100644 --- a/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs +++ b/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs @@ -19,7 +19,7 @@ internal class RangeCoder private long _low, _code, _range; - private readonly IRarUnpack _unpackRead; + private IRarUnpack _unpackRead; private readonly Stream _stream; internal RangeCoder(IRarUnpack unpackRead) @@ -36,6 +36,24 @@ internal class RangeCoder internal RangeCoder() { } + internal async ValueTask InitAsync( + IRarUnpack unpackRead, + CancellationToken cancellationToken = default + ) + { + _unpackRead = unpackRead; + SubRange = new SubRange(); + + _low = _code = 0L; + _range = 0xFFFFffffL; + for (var i = 0; i < 4; i++) + { + _code = + ((_code << 8) | await ReadCharAsync(cancellationToken).ConfigureAwait(false)) + & UINT_MASK; + } + } + private void Init() { SubRange = new SubRange(); @@ -131,7 +149,7 @@ internal class RangeCoder { if (_unpackRead != null) { - return _unpackRead.Char; + return await _unpackRead.ReadCharAsync(cancellationToken).ConfigureAwait(false); } if (_stream != null) { diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs index 0581ba26..2bea6ce8 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs @@ -141,7 +141,7 @@ internal sealed partial class Unpack if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 260 && wrPtr != unpPtr) { - UnpWriteBuf(); + await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false); if (destUnpSize < 0) { return; @@ -154,7 +154,7 @@ internal sealed partial class Unpack } if (unpBlockType == BlockTypes.BLOCK_PPM) { - var Ch = ppm.DecodeChar(); + var Ch = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); if (Ch == -1) { ppmError = true; @@ -162,7 +162,7 @@ internal sealed partial class Unpack } if (Ch == PpmEscChar) { - var NextCh = ppm.DecodeChar(); + var NextCh = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); if (NextCh == 0) { if (!await ReadTablesAsync(cancellationToken).ConfigureAwait(false)) @@ -177,7 +177,7 @@ internal sealed partial class Unpack } if (NextCh == 3) { - if (!ReadVMCodePPM()) + if (!await ReadVMCodePPMAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -190,7 +190,8 @@ internal sealed partial class Unpack var failed = false; for (var I = 0; I < 4 && !failed; I++) { - var ch = ppm.DecodeChar(); + var ch = await ppm.DecodeCharAsync(cancellationToken) + .ConfigureAwait(false); if (ch == -1) { failed = true; @@ -216,7 +217,8 @@ internal sealed partial class Unpack } if (NextCh == 5) { - var Length = ppm.DecodeChar(); + var Length = await ppm.DecodeCharAsync(cancellationToken) + .ConfigureAwait(false); if (Length == -1) { break; @@ -354,7 +356,7 @@ internal sealed partial class Unpack CopyString(2, Distance); } } - UnpWriteBuf(); + await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false); } private async Task UnpWriteBufAsync(CancellationToken cancellationToken = default) @@ -622,7 +624,8 @@ internal sealed partial class Unpack if ((bitField & 0x8000) != 0) { unpBlockType = BlockTypes.BLOCK_PPM; - return ppm.DecodeInit(this, PpmEscChar); + return await ppm.DecodeInitAsync(this, PpmEscChar, cancellationToken) + .ConfigureAwait(false); } unpBlockType = BlockTypes.BLOCK_LZ; @@ -793,4 +796,58 @@ internal sealed partial class Unpack } return AddVMCode(FirstByte, vmCode); } + + public async ValueTask ReadCharAsync(CancellationToken cancellationToken = default) + { + if (inAddr > MAX_SIZE - 30) + { + await unpReadBufAsync(cancellationToken).ConfigureAwait(false); + } + return InBuf[inAddr++] & 0xff; + } + + private async Task ReadVMCodePPMAsync(CancellationToken cancellationToken = default) + { + var FirstByte = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); + if (FirstByte == -1) + { + return false; + } + var Length = (FirstByte & 7) + 1; + if (Length == 7) + { + var B1 = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); + if (B1 == -1) + { + return false; + } + Length = B1 + 7; + } + else if (Length == 8) + { + var B1 = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); + if (B1 == -1) + { + return false; + } + var B2 = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); + if (B2 == -1) + { + return false; + } + Length = (B1 * 256) + B2; + } + + var vmCode = new List(); + for (var I = 0; I < Length; I++) + { + var Ch = await ppm.DecodeCharAsync(cancellationToken).ConfigureAwait(false); + if (Ch == -1) + { + return false; + } + vmCode.Add((byte)Ch); + } + return AddVMCode(FirstByte, vmCode); + } } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack15.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack15.Async.cs index 2975fbbf..f2e8c4ca 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack15.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack15.Async.cs @@ -48,7 +48,7 @@ internal partial class Unpack } if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr) { - oldUnpWriteBuf(); + await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false); if (suspended) { return; @@ -105,7 +105,7 @@ internal partial class Unpack } } } - oldUnpWriteBuf(); + await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false); } private async Task unpReadBufAsync(CancellationToken cancellationToken = default) diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack20.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack20.Async.cs index 69bc6de0..9ca54f31 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack20.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack20.Async.cs @@ -45,7 +45,7 @@ internal partial class Unpack } if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr) { - oldUnpWriteBuf(); + await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false); if (suspended) { return; @@ -157,8 +157,8 @@ internal partial class Unpack CopyString20(2, Distance); } } - ReadLastTables(); - oldUnpWriteBuf(); + await ReadLastTablesAsync(cancellationToken).ConfigureAwait(false); + await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false); } private async Task ReadTables20Async(CancellationToken cancellationToken = default) @@ -272,4 +272,25 @@ internal partial class Unpack } return true; } + + private async Task ReadLastTablesAsync(CancellationToken cancellationToken = default) + { + if (readTop >= inAddr + 5) + { + if (UnpAudioBlock != 0) + { + if (this.decodeNumber(MD[UnpCurChannel]) == 256) + { + await ReadTables20Async(cancellationToken).ConfigureAwait(false); + } + } + else + { + if (this.decodeNumber(LD) == 269) + { + await ReadTables20Async(cancellationToken).ConfigureAwait(false); + } + } + } + } } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs index 432922c4..42f7d439 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs @@ -118,7 +118,7 @@ internal partial class Unpack && WriteBorder != UnpPtr ) { - UnpWriteBuf(); + await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false); if (WrittenFileSize > DestUnpSize) { return; @@ -197,7 +197,7 @@ internal partial class Unpack var Filter = new UnpackFilter(); if ( !await ReadFilterAsync(Filter, cancellationToken).ConfigureAwait(false) - || !AddFilter(Filter) + || !await AddFilterAsync(Filter, cancellationToken).ConfigureAwait(false) ) { break; @@ -232,7 +232,7 @@ internal partial class Unpack continue; } } - UnpWriteBuf(); + await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false); } private async Task ReadBlockHeaderAsync(CancellationToken cancellationToken = default) @@ -318,4 +318,24 @@ internal partial class Unpack return true; } + + private async Task AddFilterAsync( + UnpackFilter Filter, + CancellationToken cancellationToken = default + ) + { + if (Filters.Count >= MAX_UNPACK_FILTERS) + { + await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false); + if (Filters.Count >= MAX_UNPACK_FILTERS) + { + InitFilters(); + } + } + + Filter.NextWindow = WrPtr != UnpPtr && ((WrPtr - UnpPtr) & MaxWinMask) <= Filter.BlockStart; + Filter.uBlockStart = (uint)((Filter.BlockStart + UnpPtr) & MaxWinMask); + Filters.Add(Filter); + return true; + } } diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index d6d82e26..b922ee7e 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -14,6 +14,30 @@ namespace SharpCompress.Test.Rar; public class RarReaderAsyncTests : ReaderTests { + [Theory] + [InlineData("Rar15.rar")] + [InlineData("Rar.rar")] + [InlineData("Rar.Audio_program.rar")] + [InlineData("Rar5.rar")] + [InlineData("Rar5.solid.rar")] + public async ValueTask Rar_Reader_Async_Uses_Only_Async_Stream_Operations(string filename) + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); + await using var reader = await ReaderFactory.OpenAsyncReader( + new AsyncOnlyStream(stream), + new ReaderOptions { LookForHeader = true } + ); + + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + using var output = new SyncWriteNotSupportedStream(new MemoryStream()); + await reader.WriteEntryToAsync(output); + } + } + } + [Fact] public async ValueTask Rar_Multi_Reader_Async() => await DoRar_Multi_Reader_Async([ @@ -371,4 +395,56 @@ public class RarReaderAsyncTests : ReaderTests } VerifyFiles(); } + + private sealed class SyncWriteNotSupportedStream(Stream stream) : Stream + { + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position + { + get => stream.Position; + set => stream.Position = value; + } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + + public override void SetLength(long value) => stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Synchronous Write is not supported"); + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + System.Threading.CancellationToken cancellationToken + ) => stream.WriteAsync(buffer, offset, count, cancellationToken); + +#if NET8_0_OR_GREATER + public override ValueTask WriteAsync( + ReadOnlyMemory buffer, + System.Threading.CancellationToken cancellationToken = default + ) => stream.WriteAsync(buffer, cancellationToken); +#endif + + protected override void Dispose(bool disposing) + { + if (disposing) + { + stream.Dispose(); + } + base.Dispose(disposing); + } + } } From 18fade571e60587a07564fe4ca048a86d64979ef Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 08:39:54 +0100 Subject: [PATCH 3/9] made Char be a method to match async method --- .../Compressors/PPMd/H/ModelPPM.cs | 6 ++--- .../Compressors/PPMd/H/RangeCoder.cs | 23 ++++++++---------- .../Compressors/Rar/IRarUnpack.cs | 3 ++- .../Compressors/Rar/UnpackV1/Unpack.cs | 11 ++++----- .../Compressors/Rar/UnpackV2017/Unpack.cs | 24 ++++++++++++------- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs b/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs index 1718cbb4..6405423a 100644 --- a/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs +++ b/src/SharpCompress/Compressors/PPMd/H/ModelPPM.cs @@ -281,13 +281,13 @@ internal class ModelPpm internal bool DecodeInit(IRarUnpack unpackRead, int escChar) { - var maxOrder = unpackRead.Char & 0xff; + var maxOrder = unpackRead.ReadChar() & 0xff; var reset = ((maxOrder & 0x20) != 0); var maxMb = 0; if (reset) { - maxMb = unpackRead.Char; + maxMb = unpackRead.ReadChar(); } else { @@ -298,7 +298,7 @@ internal class ModelPpm } if ((maxOrder & 0x40) != 0) { - escChar = unpackRead.Char; + escChar = unpackRead.ReadChar(); unpackRead.PpmEscChar = escChar; } Coder = new RangeCoder(unpackRead); diff --git a/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs b/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs index 59bdd926..7cda0fec 100644 --- a/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs +++ b/src/SharpCompress/Compressors/PPMd/H/RangeCoder.cs @@ -62,7 +62,7 @@ internal class RangeCoder _range = 0xFFFFffffL; for (var i = 0; i < 4; i++) { - _code = ((_code << 8) | Char) & UINT_MASK; + _code = ((_code << 8) | ReadChar()) & UINT_MASK; } } @@ -91,20 +91,17 @@ internal class RangeCoder } } - private long Char + private long ReadChar() { - get + if (_unpackRead != null) { - if (_unpackRead != null) - { - return (_unpackRead.Char); - } - if (_stream != null) - { - return _stream.ReadByte(); - } - return -1; + return (_unpackRead.ReadChar()); } + if (_stream != null) + { + return _stream.ReadByte(); + } + return -1; } internal SubRange SubRange { get; private set; } @@ -139,7 +136,7 @@ internal class RangeCoder _range = (-_low & (BOT - 1)) & UINT_MASK; c2 = false; } - _code = ((_code << 8) | Char) & UINT_MASK; + _code = ((_code << 8) | ReadChar()) & UINT_MASK; _range = (_range << 8) & UINT_MASK; _low = (_low << 8) & UINT_MASK; } diff --git a/src/SharpCompress/Compressors/Rar/IRarUnpack.cs b/src/SharpCompress/Compressors/Rar/IRarUnpack.cs index 7626f4ac..45729bf5 100644 --- a/src/SharpCompress/Compressors/Rar/IRarUnpack.cs +++ b/src/SharpCompress/Compressors/Rar/IRarUnpack.cs @@ -22,6 +22,7 @@ internal interface IRarUnpack bool Suspended { get; set; } long DestSize { get; } - int Char { get; } + int ReadChar(); + ValueTask ReadCharAsync(CancellationToken cancellationToken); int PpmEscChar { get; set; } } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs index 929c4eb7..65697a6a 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs @@ -55,16 +55,13 @@ internal sealed partial class Unpack : BitInput, IRarUnpack set => suspended = value; } - public int Char + public int ReadChar() { - get + if (inAddr > MAX_SIZE - 30) { - if (inAddr > MAX_SIZE - 30) - { - unpReadBuf(); - } - return (InBuf[inAddr++] & 0xff); + unpReadBuf(); } + return (InBuf[inAddr++] & 0xff); } public int PpmEscChar { get; set; } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.cs index 7784d98a..27cf0a67 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.cs @@ -154,17 +154,25 @@ internal partial class Unpack : IRarUnpack public long DestSize => DestUnpSize; - public int Char + public int ReadChar() { - get + // TODO: coderb: not sure where the "MAXSIZE-30" comes from, ported from V1 code + if (InAddr > MAX_SIZE - 30) { - // TODO: coderb: not sure where the "MAXSIZE-30" comes from, ported from V1 code - if (InAddr > MAX_SIZE - 30) - { - UnpReadBuf(); - } - return InBuf[InAddr++]; + UnpReadBuf(); } + return InBuf[InAddr++]; + } + + public async ValueTask ReadCharAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + // TODO: coderb: not sure where the "MAXSIZE-30" comes from, ported from V1 code + if (InAddr > MAX_SIZE - 30) + { + await UnpReadBufAsync(cancellationToken).ConfigureAwait(false); + } + return InBuf[InAddr++]; } public int PpmEscChar From fc096e1996af99f5733a91be189c061d5391b6ba Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 08:48:30 +0100 Subject: [PATCH 4/9] new tests --- .../Mocks/SyncWriteNotSupportedStream.cs | 58 +++++++++++++++++++ .../Rar/RarArchiveAsyncTests.cs | 25 ++++++++ .../SharpCompress.Test/Rar/RarArchiveTests.cs | 26 +++++++++ .../Rar/RarReaderAsyncTests.cs | 52 ----------------- 4 files changed, 109 insertions(+), 52 deletions(-) create mode 100644 tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs diff --git a/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs b/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs new file mode 100644 index 00000000..6ee98480 --- /dev/null +++ b/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Test.Mocks; + +public sealed class SyncWriteNotSupportedStream(Stream stream) : Stream +{ + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position + { + get => stream.Position; + set => stream.Position = value; + } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + + public override void SetLength(long value) => stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException("Synchronous Write is not supported"); + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => stream.WriteAsync(buffer, offset, count, cancellationToken); + +#if NET8_0_OR_GREATER + public override ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => stream.WriteAsync(buffer, cancellationToken); +#endif + + protected override void Dispose(bool disposing) + { + if (disposing) + { + stream.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index bba174d6..09c274c5 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -13,6 +13,31 @@ namespace SharpCompress.Test.Rar; public class RarArchiveAsyncTests : ArchiveTests { + [Theory] + [InlineData("Rar15.rar")] + [InlineData("Rar2.rar")] + [InlineData("Rar.rar")] + [InlineData("Rar.Audio_program.rar")] + [InlineData("Rar5.rar")] + [InlineData("Rar5.solid.rar")] + public async ValueTask Rar_Archive_Recently_Changed_Unpackers_Async(string filename) + { + var extractedEntries = 0; + await using var archive = await RarArchive.OpenAsyncArchive( + Path.Combine(TEST_ARCHIVES_PATH, filename), + new ReaderOptions { LookForHeader = true } + ); + + await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) + { + using var output = new SyncWriteNotSupportedStream(new MemoryStream()); + await entry.WriteToAsync(output); + extractedEntries++; + } + + Assert.True(extractedEntries > 0); + } + [Fact] public async ValueTask Rar_EncryptedFileAndHeader_Archive_Async() => await ReadRarPasswordAsync("Rar.encrypted_filesAndHeader.rar", "test"); diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index e4fef73f..130c20b6 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -13,6 +13,32 @@ namespace SharpCompress.Test.Rar; public class RarArchiveTests : ArchiveTests { + [Theory] + [InlineData("Rar15.rar")] + [InlineData("Rar2.rar")] + [InlineData("Rar.rar")] + [InlineData("Rar.Audio_program.rar")] + [InlineData("Rar5.rar")] + [InlineData("Rar5.solid.rar")] + public void Rar_Archive_Recently_Changed_Unpackers_Sync(string filename) + { + var extractedEntries = 0; + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); + using var archive = RarArchive.OpenArchive( + stream, + new ReaderOptions { LookForHeader = true } + ); + + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + using var output = new MemoryStream(); + entry.WriteTo(output); + extractedEntries++; + } + + Assert.True(extractedEntries > 0); + } + [Fact] public void Rar_EncryptedFileAndHeader_Archive() => ReadRarPassword("Rar.encrypted_filesAndHeader.rar", "test"); diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index b922ee7e..1ed4b8f5 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -395,56 +395,4 @@ public class RarReaderAsyncTests : ReaderTests } VerifyFiles(); } - - private sealed class SyncWriteNotSupportedStream(Stream stream) : Stream - { - public override bool CanRead => stream.CanRead; - - public override bool CanSeek => stream.CanSeek; - - public override bool CanWrite => stream.CanWrite; - - public override long Length => stream.Length; - - public override long Position - { - get => stream.Position; - set => stream.Position = value; - } - - public override void Flush() => stream.Flush(); - - public override int Read(byte[] buffer, int offset, int count) => - stream.Read(buffer, offset, count); - - public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); - - public override void SetLength(long value) => stream.SetLength(value); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException("Synchronous Write is not supported"); - - public override Task WriteAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) => stream.WriteAsync(buffer, offset, count, cancellationToken); - -#if NET8_0_OR_GREATER - public override ValueTask WriteAsync( - ReadOnlyMemory buffer, - System.Threading.CancellationToken cancellationToken = default - ) => stream.WriteAsync(buffer, cancellationToken); -#endif - - protected override void Dispose(bool disposing) - { - if (disposing) - { - stream.Dispose(); - } - base.Dispose(disposing); - } - } } From 0352740ea10fc8f97478decb9173d3868f7750d6 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 09:14:31 +0100 Subject: [PATCH 5/9] more async tests --- .../Rar/UnpackV2017/Unpack.unpack15_async.cs | 4 +- .../Rar/UnpackV2017/Unpack.unpack20_async.cs | 24 +++++++- .../Mocks/AsyncOnlyStream.cs | 9 +-- .../Mocks/SyncWriteNotSupportedStream.cs | 58 ------------------- .../Rar/RarArchiveAsyncTests.cs | 2 +- .../Rar/RarReaderAsyncTests.cs | 2 +- 6 files changed, 27 insertions(+), 72 deletions(-) delete mode 100644 tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack15_async.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack15_async.cs index e615527e..de8036b2 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack15_async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack15_async.cs @@ -41,7 +41,7 @@ internal partial class Unpack if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr) { - UnpWriteBuf20(); + await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false); } if (StMode != 0) @@ -95,6 +95,6 @@ internal partial class Unpack } } } - UnpWriteBuf20(); + await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack20_async.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack20_async.cs index 0619189a..89083eb4 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack20_async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack20_async.cs @@ -49,7 +49,7 @@ internal partial class Unpack if (((WrPtr - UnpPtr) & MaxWinMask) < 270 && WrPtr != UnpPtr) { - UnpWriteBuf20(); + await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false); if (Suspended) { return; @@ -165,8 +165,8 @@ internal partial class Unpack continue; } } - ReadLastTables(); - UnpWriteBuf20(); + await ReadLastTables20Async(cancellationToken).ConfigureAwait(false); + await UnpWriteBuf20Async(cancellationToken).ConfigureAwait(false); } private async Task UnpWriteBuf20Async(CancellationToken cancellationToken = default) @@ -316,4 +316,22 @@ internal partial class Unpack Array.Copy(Table, 0, this.UnpOldTable20, 0, UnpOldTable20.Length); return true; } + + private async Task ReadLastTables20Async(CancellationToken cancellationToken = default) + { + if (ReadTop >= Inp.InAddr + 5) + { + if (UnpAudioBlock) + { + if (DecodeNumber(Inp, MD[UnpCurChannel]) == 256) + { + await ReadTables20Async(cancellationToken).ConfigureAwait(false); + } + } + else if (DecodeNumber(Inp, BlockTables.LD) == 269) + { + await ReadTables20Async(cancellationToken).ConfigureAwait(false); + } + } + } } diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs index 07a679ef..cf6416fe 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -5,14 +5,9 @@ using System.Threading.Tasks; namespace SharpCompress.Test.Mocks; -public class AsyncOnlyStream : Stream +public class AsyncOnlyStream(Stream stream) : Stream { - private readonly Stream _stream; - - public AsyncOnlyStream(Stream stream) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - } + private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); public override bool CanRead => _stream.CanRead; public override bool CanSeek => _stream.CanSeek; diff --git a/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs b/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs deleted file mode 100644 index 6ee98480..00000000 --- a/tests/SharpCompress.Test/Mocks/SyncWriteNotSupportedStream.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SharpCompress.Test.Mocks; - -public sealed class SyncWriteNotSupportedStream(Stream stream) : Stream -{ - public override bool CanRead => stream.CanRead; - - public override bool CanSeek => stream.CanSeek; - - public override bool CanWrite => stream.CanWrite; - - public override long Length => stream.Length; - - public override long Position - { - get => stream.Position; - set => stream.Position = value; - } - - public override void Flush() => stream.Flush(); - - public override int Read(byte[] buffer, int offset, int count) => - stream.Read(buffer, offset, count); - - public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); - - public override void SetLength(long value) => stream.SetLength(value); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException("Synchronous Write is not supported"); - - public override Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => stream.WriteAsync(buffer, offset, count, cancellationToken); - -#if NET8_0_OR_GREATER - public override ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) => stream.WriteAsync(buffer, cancellationToken); -#endif - - protected override void Dispose(bool disposing) - { - if (disposing) - { - stream.Dispose(); - } - base.Dispose(disposing); - } -} diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 09c274c5..3293fe75 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -30,7 +30,7 @@ public class RarArchiveAsyncTests : ArchiveTests await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { - using var output = new SyncWriteNotSupportedStream(new MemoryStream()); + using var output = new AsyncOnlyStream(new MemoryStream()); await entry.WriteToAsync(output); extractedEntries++; } diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index 1ed4b8f5..11c40f77 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -32,7 +32,7 @@ public class RarReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { - using var output = new SyncWriteNotSupportedStream(new MemoryStream()); + using var output = new AsyncOnlyStream(new MemoryStream()); await reader.WriteEntryToAsync(output); } } From 2c0a15e0f0750d1e43cb95c25682181293af035a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 09:39:23 +0100 Subject: [PATCH 6/9] make async and sync the same --- .../Common/Rar/AsyncMarkingBinaryReader.cs | 16 +++++++++++++++- src/SharpCompress/IO/AsyncBinaryReader.cs | 9 ++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs b/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs index c0b25ebb..5135e62c 100644 --- a/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs +++ b/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs @@ -45,7 +45,21 @@ internal class AsyncMarkingBinaryReader : IDisposable { CurrentReadByteCount += count; var bytes = new byte[count]; - await _reader.ReadBytesAsync(bytes, 0, count, cancellationToken).ConfigureAwait(false); + try + { + await _reader.ReadBytesAsync(bytes, 0, count, cancellationToken).ConfigureAwait(false); + } + catch (IncompleteArchiveException ex) + { + throw new InvalidFormatException( + string.Format( + Constants.DefaultCultureInfo, + "Could not read the requested amount of bytes. End of stream reached. Requested: {0}", + count + ), + ex + ); + } return bytes; } diff --git a/src/SharpCompress/IO/AsyncBinaryReader.cs b/src/SharpCompress/IO/AsyncBinaryReader.cs index 9fef056a..f3e8c91b 100644 --- a/src/SharpCompress/IO/AsyncBinaryReader.cs +++ b/src/SharpCompress/IO/AsyncBinaryReader.cs @@ -60,15 +60,10 @@ public sealed class AsyncBinaryReader : IDisposable int offset, int count, CancellationToken ct = default - ) - { - await _stream.ReadExactAsync(bytes, offset, count, ct).ConfigureAwait(false); - } + ) => await _stream.ReadExactAsync(bytes, offset, count, ct).ConfigureAwait(false); - public async ValueTask SkipAsync(int count, CancellationToken ct = default) - { + public async ValueTask SkipAsync(int count, CancellationToken ct = default) => await _stream.SkipAsync(count, ct).ConfigureAwait(false); - } public void Dispose() { From 9156a75c56ce056cb5bc72d87453726dfab8d8ef Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 10:26:18 +0100 Subject: [PATCH 7/9] pool arrays --- .../Compressors/Rar/UnpackV1/Unpack.Async.cs | 229 ++++++++++-------- 1 file changed, 124 insertions(+), 105 deletions(-) diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs index 2bea6ce8..c5096f47 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs @@ -609,137 +609,156 @@ internal sealed partial class Unpack private async Task ReadTablesAsync(CancellationToken cancellationToken = default) { - var bitLength = new byte[PackDef.BC]; - var table = new byte[PackDef.HUFF_TABLE_SIZE]; + var bitLengthArray = ArrayPool.Shared.Rent(PackDef.BC); + var bitLength = new Memory(bitLengthArray, 0, PackDef.BC); + var tableArray = ArrayPool.Shared.Rent(PackDef.HUFF_TABLE_SIZE); + var table = new Memory(tableArray, 0, PackDef.HUFF_TABLE_SIZE); - if (inAddr > readTop - 25) + try { - if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false)) - { - return false; - } - } - AddBits((8 - inBit) & 7); - long bitField = GetBits() & unchecked((int)0xffFFffFF); - if ((bitField & 0x8000) != 0) - { - unpBlockType = BlockTypes.BLOCK_PPM; - return await ppm.DecodeInitAsync(this, PpmEscChar, cancellationToken) - .ConfigureAwait(false); - } - unpBlockType = BlockTypes.BLOCK_LZ; - - prevLowDist = 0; - lowDistRepCount = 0; - - if ((bitField & 0x4000) == 0) - { - new Span(unpOldTable).Clear(); - } - AddBits(2); - - for (var i = 0; i < PackDef.BC; i++) - { - var length = (Utility.URShift(GetBits(), 12)) & 0xFF; - AddBits(4); - if (length == 15) - { - var zeroCount = (Utility.URShift(GetBits(), 12)) & 0xFF; - AddBits(4); - if (zeroCount == 0) - { - bitLength[i] = 15; - } - else - { - zeroCount += 2; - while (zeroCount-- > 0 && i < bitLength.Length) - { - bitLength[i++] = 0; - } - i--; - } - } - else - { - bitLength[i] = (byte)length; - } - } - - UnpackUtility.makeDecodeTables(bitLength, 0, BD, PackDef.BC); - - var TableSize = PackDef.HUFF_TABLE_SIZE; - - for (var i = 0; i < TableSize; ) - { - if (inAddr > readTop - 5) + if (inAddr > readTop - 25) { if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false)) { return false; } } - var Number = this.decodeNumber(BD); - if (Number < 16) + + AddBits((8 - inBit) & 7); + long bitField = GetBits() & unchecked((int)0xffFFffFF); + if ((bitField & 0x8000) != 0) { - table[i] = (byte)((Number + unpOldTable[i]) & 0xf); - i++; + unpBlockType = BlockTypes.BLOCK_PPM; + return await ppm.DecodeInitAsync(this, PpmEscChar, cancellationToken) + .ConfigureAwait(false); } - else if (Number < 18) + + unpBlockType = BlockTypes.BLOCK_LZ; + + prevLowDist = 0; + lowDistRepCount = 0; + + if ((bitField & 0x4000) == 0) { - int N; - if (Number == 16) + new Span(unpOldTable).Clear(); + } + + AddBits(2); + + for (var i = 0; i < PackDef.BC; i++) + { + var length = (Utility.URShift(GetBits(), 12)) & 0xFF; + AddBits(4); + if (length == 15) { - N = (Utility.URShift(GetBits(), 13)) + 3; - AddBits(3); + var zeroCount = (Utility.URShift(GetBits(), 12)) & 0xFF; + AddBits(4); + if (zeroCount == 0) + { + bitLength.Span[i] = 15; + } + else + { + zeroCount += 2; + while (zeroCount-- > 0 && i < bitLength.Length) + { + bitLength.Span[i++] = 0; + } + + i--; + } } else { - N = (Utility.URShift(GetBits(), 9)) + 11; - AddBits(7); + bitLength.Span[i] = (byte)length; } - while (N-- > 0 && i < TableSize) + } + + UnpackUtility.makeDecodeTables(bitLength.Span, 0, BD, PackDef.BC); + + var TableSize = PackDef.HUFF_TABLE_SIZE; + + for (var i = 0; i < TableSize;) + { + if (inAddr > readTop - 5) { - table[i] = table[i - 1]; + if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false)) + { + return false; + } + } + + var Number = this.decodeNumber(BD); + if (Number < 16) + { + table.Span[i] = (byte)((Number + unpOldTable[i]) & 0xf); i++; } - } - else - { - int N; - if (Number == 18) + else if (Number < 18) { - N = (Utility.URShift(GetBits(), 13)) + 3; - AddBits(3); + int N; + if (Number == 16) + { + N = (Utility.URShift(GetBits(), 13)) + 3; + AddBits(3); + } + else + { + N = (Utility.URShift(GetBits(), 9)) + 11; + AddBits(7); + } + + while (N-- > 0 && i < TableSize) + { + table.Span[i] = table.Span[i - 1]; + i++; + } } else { - N = (Utility.URShift(GetBits(), 9)) + 11; - AddBits(7); - } - while (N-- > 0 && i < TableSize) - { - table[i++] = 0; + int N; + if (Number == 18) + { + N = (Utility.URShift(GetBits(), 13)) + 3; + AddBits(3); + } + else + { + N = (Utility.URShift(GetBits(), 9)) + 11; + AddBits(7); + } + + while (N-- > 0 && i < TableSize) + { + table.Span[i++] = 0; + } } } - } - tablesRead = true; - if (inAddr > readTop) - { - return false; - } - UnpackUtility.makeDecodeTables(table, 0, LD, PackDef.NC); - UnpackUtility.makeDecodeTables(table, PackDef.NC, DD, PackDef.DC); - UnpackUtility.makeDecodeTables(table, PackDef.NC + PackDef.DC, LDD, PackDef.LDC); - UnpackUtility.makeDecodeTables( - table, - PackDef.NC + PackDef.DC + PackDef.LDC, - RD, - PackDef.RC - ); - new Span(table).CopyTo(unpOldTable); - return true; + tablesRead = true; + if (inAddr > readTop) + { + return false; + } + + UnpackUtility.makeDecodeTables(table.Span, 0, LD, PackDef.NC); + UnpackUtility.makeDecodeTables(table.Span, PackDef.NC, DD, PackDef.DC); + UnpackUtility.makeDecodeTables(table.Span, PackDef.NC + PackDef.DC, LDD, PackDef.LDC); + UnpackUtility.makeDecodeTables( + table.Span, + PackDef.NC + PackDef.DC + PackDef.LDC, + RD, + PackDef.RC + ); + + table.Span.CopyTo(unpOldTable); + return true; + } + finally + { + ArrayPool.Shared.Return(bitLengthArray); + ArrayPool.Shared.Return(tableArray); + } } private async Task ReadEndOfBlockAsync(CancellationToken cancellationToken = default) From 4f41b6f7937f7b12676f455a6335e449113b8e58 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 10:28:50 +0100 Subject: [PATCH 8/9] review fix --- src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs b/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs index 5135e62c..57aa20a5 100644 --- a/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs +++ b/src/SharpCompress/Common/Rar/AsyncMarkingBinaryReader.cs @@ -54,7 +54,7 @@ internal class AsyncMarkingBinaryReader : IDisposable throw new InvalidFormatException( string.Format( Constants.DefaultCultureInfo, - "Could not read the requested amount of bytes. End of stream reached. Requested: {0}", + "Could not read the requested amount of bytes. End of stream reached. Requested: {0}", count ), ex From c629dc5903645fe719157155f9c9a29d08e18a85 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Apr 2026 11:03:08 +0100 Subject: [PATCH 9/9] format Unpack --- src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs index c5096f47..de4d63e9 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.Async.cs @@ -630,7 +630,7 @@ internal sealed partial class Unpack { unpBlockType = BlockTypes.BLOCK_PPM; return await ppm.DecodeInitAsync(this, PpmEscChar, cancellationToken) - .ConfigureAwait(false); + .ConfigureAwait(false); } unpBlockType = BlockTypes.BLOCK_LZ; @@ -678,7 +678,7 @@ internal sealed partial class Unpack var TableSize = PackDef.HUFF_TABLE_SIZE; - for (var i = 0; i < TableSize;) + for (var i = 0; i < TableSize; ) { if (inAddr > readTop - 5) {