From b91289d82c732e50c8554d051726027c889cd476 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 13:25:08 +0100 Subject: [PATCH 1/7] add large file tests and GZip/Lzw factory fixes --- src/SharpCompress/Factories/GZipFactory.cs | 12 +- src/SharpCompress/Factories/LzwFactory.cs | 12 +- tests/SharpCompress.Test/LargeArchiveTests.cs | 187 ++++++++++++++++++ tests/TestArchives/Archives/Large/Large.7z | Bin 0 -> 10012 bytes tests/TestArchives/Archives/Large/Large.gz | Bin 0 -> 162850 bytes tests/TestArchives/Archives/Large/Large.rar | Bin 0 -> 3147 bytes .../TestArchives/Archives/Large/Large.tar.gz | Bin 0 -> 163062 bytes tests/TestArchives/Archives/Large/Large.zip | Bin 0 -> 162830 bytes tests/TestArchives/Archives/Large/README.md | 38 ++++ 9 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 tests/SharpCompress.Test/LargeArchiveTests.cs create mode 100644 tests/TestArchives/Archives/Large/Large.7z create mode 100644 tests/TestArchives/Archives/Large/Large.gz create mode 100644 tests/TestArchives/Archives/Large/Large.rar create mode 100644 tests/TestArchives/Archives/Large/Large.tar.gz create mode 100644 tests/TestArchives/Archives/Large/Large.zip create mode 100644 tests/TestArchives/Archives/Large/README.md diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index bda6ef1f..4649f0fc 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -151,13 +151,16 @@ public class GZipFactory CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options) ) ); - if (TarArchive.IsTarFile(testStream)) + var isTarArchive = TarArchive.IsTarFile(testStream); + + // The TAR probe can consume arbitrary compressed input before it rejects a stream. + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); + if (isTarArchive) { - sharpCompressStream.StopRecording(); reader = new TarReader(sharpCompressStream, options, CompressionType.GZip); return true; } - sharpCompressStream.StopRecording(); reader = OpenReader(sharpCompressStream, options); return true; } @@ -185,12 +188,13 @@ public class GZipFactory var tarReader = await new TarFactory() .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); if (tarReader is not null) { return tarReader; } - sharpCompressStream.StopRecording(); return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); } diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index bd5569d1..da475d8a 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -65,14 +65,17 @@ public class LzwFactory : Factory, IReaderFactory ) ) { - if (TarArchive.IsTarFile(testStream)) + var isTarArchive = TarArchive.IsTarFile(testStream); + + // The TAR probe can consume arbitrary compressed input before it rejects a stream. + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); + if (isTarArchive) { - sharpCompressStream.StopRecording(); reader = new TarReader(sharpCompressStream, options, CompressionType.Lzw); return true; } } - sharpCompressStream.StopRecording(); reader = OpenReader(sharpCompressStream, options); return true; } @@ -100,12 +103,13 @@ public class LzwFactory : Factory, IReaderFactory var tarReader = await new TarFactory() .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); + sharpCompressStream.StopRecording(); if (tarReader is not null) { return tarReader; } - sharpCompressStream.StopRecording(); return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) .ConfigureAwait(false); } diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs new file mode 100644 index 00000000..fc94c935 --- /dev/null +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -0,0 +1,187 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Crypto; +using SharpCompress.Readers; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test; + +[Collection(LargeArchiveCollection.Name)] +public class LargeArchiveTests : TestBase +{ + private const long LargeFileSize = 64L * 1024 * 1024; + private const uint LargeFileCrc = 0xF9081EB0; + private const int BufferSize = 64 * 1024; + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.7z")] + public void OpenArchive_ShouldStreamLargeEntry(string fixtureName) + { + using var stream = File.OpenRead(GetArchiveFixturePath(fixtureName)); + using var archive = ArchiveFactory.OpenArchive(stream); + + VerifyArchive(archive); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.7z")] + public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) + { + await using var stream = new AsyncOnlyStream( + File.OpenRead(await GetArchiveFixturePathAsync(fixtureName)) + ); + await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); + + var entry = await GetSingleEntryAsync(archive); + await using var entryStream = await entry.OpenEntryStreamAsync(); + await VerifyContentAsync(entry.Key, entryStream); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.tar.gz")] + public void OpenReader_ShouldStreamLargeEntry(string fixtureName) + { + using var stream = File.OpenRead(GetFixturePath(fixtureName)); + using var reader = ReaderFactory.OpenReader(stream); + + VerifyReader(reader); + } + + [Theory] + [InlineData("Large/Large.zip")] + [InlineData("Large/Large.tar")] + [InlineData("Large/Large.gz")] + [InlineData("Large/Large.rar")] + [InlineData("Large/Large.tar.gz")] + public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) + { + await using var stream = new AsyncOnlyStream(File.OpenRead(GetFixturePath(fixtureName))); + await using var reader = await ReaderFactory.OpenAsyncReader(stream); + + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.False(reader.Entry.IsDirectory); + await using var entryStream = await reader.OpenEntryStreamAsync(); + await VerifyContentAsync(reader.Entry.Key, entryStream); + Assert.False(await reader.MoveToNextEntryAsync()); + } + + private static string GetFixturePath(string fixtureName) => + Path.Combine(TEST_ARCHIVES_PATH, fixtureName); + + private string GetArchiveFixturePath(string fixtureName) => + fixtureName == "Large/Large.tar" ? MaterializeTarFixture() : GetFixturePath(fixtureName); + + private async Task GetArchiveFixturePathAsync(string fixtureName) => + fixtureName == "Large/Large.tar" + ? await MaterializeTarFixtureAsync() + : GetFixturePath(fixtureName); + + private string MaterializeTarFixture() + { + var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar"); + using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var tarStream = File.Create(tarPath); + gzipStream.CopyTo(tarStream); + return tarPath; + } + + private async Task MaterializeTarFixtureAsync() + { + var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar"); + using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var tarStream = File.Create(tarPath); + await gzipStream.CopyToAsync(tarStream); + return tarPath; + } + + private static void VerifyArchive(IArchive archive) + { + var entry = Assert.Single(archive.Entries); + + Assert.False(entry.IsDirectory); + using var entryStream = entry.OpenEntryStream(); + VerifyContent(entry.Key, entryStream); + } + + private static async Task GetSingleEntryAsync(IAsyncArchive archive) + { + IArchiveEntry? entry = null; + await foreach (var candidate in archive.EntriesAsync) + { + Assert.Null(entry); + entry = candidate; + } + + return entry ?? throw new InvalidOperationException("The archive contains no entries."); + } + + private static void VerifyReader(IReader reader) + { + Assert.True(reader.MoveToNextEntry()); + Assert.False(reader.Entry.IsDirectory); + using var entryStream = reader.OpenEntryStream(); + VerifyContent(reader.Entry.Key, entryStream); + Assert.False(reader.MoveToNextEntry()); + } + + private static void VerifyContent(string? key, Stream entryStream) + { + Assert.Equal("large.bin", key); + + using var crcStream = new Crc32Stream(Stream.Null); + var buffer = new byte[BufferSize]; + long length = 0; + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + crcStream.Write(buffer, 0, bytesRead); + length += bytesRead; + } + + Assert.Equal(LargeFileSize, length); + Assert.Equal(LargeFileCrc, crcStream.Crc); + } + + private static async Task VerifyContentAsync(string? key, Stream entryStream) + { + Assert.Equal("large.bin", key); + + using var crcStream = new Crc32Stream(Stream.Null); + var buffer = new byte[BufferSize]; + long length = 0; + int bytesRead; + while ((bytesRead = await entryStream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await crcStream.WriteAsync(buffer, 0, bytesRead); + length += bytesRead; + } + + Assert.Equal(LargeFileSize, length); + Assert.Equal(LargeFileCrc, crcStream.Crc); + } +} + +[CollectionDefinition(LargeArchiveCollection.Name, DisableParallelization = true)] +public sealed class LargeArchiveCollection +{ + public const string Name = "Large archive fixtures"; +} diff --git a/tests/TestArchives/Archives/Large/Large.7z b/tests/TestArchives/Archives/Large/Large.7z new file mode 100644 index 0000000000000000000000000000000000000000..719ea18df023e2174e0eab883ff7aaf94708504b GIT binary patch literal 10012 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 F1ps&6m3ROE literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.rar b/tests/TestArchives/Archives/Large/Large.rar new file mode 100644 index 0000000000000000000000000000000000000000..6f0a65d484263ea26b0589e8bb2b542e390d6d8b GIT binary patch literal 3147 zcmWGaEK-zWXJlYm*eb}&$jZsU!N}ME0t}%urxvO+G5ww1(9poZ*3i(fpn+jYGZVu@ z?zjzd96uXe898$ji_%l|k}~r;S?_WiyM9#u8eqIH(M{jLv(bz5!;~%#g^7DRe>F>M zyjwr{daT~OfAx8B`|t1B`SIoD_UHTU=h@iV{jUG|@!!1raaR8&7#J9&kJk4vFfg$G zGhkp~5dU$2fq^0ZF#`hwMua<%=x literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.tar.gz b/tests/TestArchives/Archives/Large/Large.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..007fef083fe9e4d65cac12d5c3639a5659ff8303 GIT binary patch literal 163062 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 bj+TU@B_UNx!tIred7_sW+AA_}F|YstBaSne literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/Large.zip b/tests/TestArchives/Archives/Large/Large.zip new file mode 100644 index 0000000000000000000000000000000000000000..5b07336fde030aff7517df489d4596cddafca01f GIT binary patch literal 162830 zcmWIWW@Zs#U}E54U|>)T*dWL8b5=DI0|NsC3nv2uLr!8*da7PhX5O2Vr+FI`L>M;g z4&KnMY{AI>M@8x+v+(xgjydaPnHhe}wz?nQdnwAQI7fBu@!Rt?E^m5XaL)4ioZ?Yp zGz>=5!Du!ZEeb|U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>G zEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj& zv?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9 z(UNeqBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXX zM@z!dl8_oDp?r0b2k5{+hA02C-B;YJe!utoC@~raqv>EY8;lkOqb1>JNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)X zS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)n%aX9&)=~w07~rS>Ta`m- zY13#tj)v}NjW$}Njn-(RCE;jEI9d{pmV~1v;b=)XS`vJ zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)X zS`vGEeS_U!qJj&v?LrY2}iTRXh}F)5{{OHqb1>JNjO>(j+TU@CE;jE zI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v z;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1vAuUV7M1MX`&|!cLf9l_I zT&1N=qwzQzx}!DPXpJ^nqm7n?qb1>JNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJ zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v z;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vGEeS_U!qJj&v?LrY2}eu9(UNeq zBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpl5Kqb1>J zNjO>(j+TU@CE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@ zCE;jEI9d{pmV~1v;b=)XS`vJNjO>(j+TU@CE;jEI9d{p zmV~1v;b=)XS`vGEeS_U!qJj&v?LrY2}eu9(UNeq zBpfXXM@z!dl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!d zl5n&n94!e)OTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n94!e) zOTy8TaI_>GEeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@zy%QWDM+5AbGWl4BNOU}E54 zU|>)T*dWL8b5=DI0|NsC3nz$VU|?WmU|?9%2;$@<7Nw`^C1vIXc(byB1Q;0@8O#|N J7!HE9002S{jOhRX literal 0 HcmV?d00001 diff --git a/tests/TestArchives/Archives/Large/README.md b/tests/TestArchives/Archives/Large/README.md new file mode 100644 index 00000000..4992f31b --- /dev/null +++ b/tests/TestArchives/Archives/Large/README.md @@ -0,0 +1,38 @@ +# Large Test Archives + +Each fixture contains one `large.bin` entry with a 67,108,864-byte (64 MiB) +deterministic, repeated text pattern with a 2 KiB `0xFF` prefix. The entry CRC-32 is +`f9081eb0`. + +| Fixture | Archive API | Reader API | +| --- | --- | --- | +| `Large.zip` | Yes | Yes | +| Generated `Large.tar` | Yes | N/A | +| `Large.gz` | Yes | Yes | +| `Large.rar` | Yes | Yes | +| `Large.7z` | Yes | No | +| `Large.tar.gz` | No | Yes | + +The compressible payload keeps the compressed fixtures small while requiring a full +64 MiB decompression to validate each API. The Archive API test expands `Large.tar.gz` +to a scratch `Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is +not committed. + +## Regenerating + +The fixtures were created with `zip`, `tar`, `gzip`, RAR 7.22, and 7-Zip. Run these +commands from a temporary directory after replacing `` with the repository root: + +```sh +yes "SharpCompress large fixture" | head -c 67108864 > large.bin +printf '\377%.0s' {1..2048} > prefix.bin +dd if=prefix.bin of=large.bin bs=2048 count=1 conv=notrunc +touch -t 202001010000 large.bin +mkdir -p /tests/TestArchives/Archives/Large +zip -X -9 -j /tests/TestArchives/Archives/Large/Large.zip large.bin +COPYFILE_DISABLE=1 tar -cf large.tar large.bin +gzip -9 -c large.bin > /tests/TestArchives/Archives/Large/Large.gz +gzip -n -9 -c large.tar > /tests/TestArchives/Archives/Large/Large.tar.gz +rar a -ma5 -m5 -ep /tests/TestArchives/Archives/Large/Large.rar large.bin +7z a -t7z -mx=9 /tests/TestArchives/Archives/Large/Large.7z large.bin +``` From d391cf21e15938731bbc2f68e225e1b9821bee59 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Aug 2026 13:26:48 +0100 Subject: [PATCH 2/7] finalize check in --- tests/SharpCompress.Test/LargeArchiveTests.cs | 14 ++++++++------ tests/TestArchives/Archives/Large/README.md | 7 +++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index fc94c935..761abda6 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -26,7 +26,7 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public void OpenArchive_ShouldStreamLargeEntry(string fixtureName) { - using var stream = File.OpenRead(GetArchiveFixturePath(fixtureName)); + using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName)); using var archive = ArchiveFactory.OpenArchive(stream); VerifyArchive(archive); @@ -41,7 +41,7 @@ public class LargeArchiveTests : TestBase public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { await using var stream = new AsyncOnlyStream( - File.OpenRead(await GetArchiveFixturePathAsync(fixtureName)) + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); @@ -58,7 +58,7 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public void OpenReader_ShouldStreamLargeEntry(string fixtureName) { - using var stream = File.OpenRead(GetFixturePath(fixtureName)); + using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName)); using var reader = ReaderFactory.OpenReader(stream); VerifyReader(reader); @@ -72,7 +72,9 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { - await using var stream = new AsyncOnlyStream(File.OpenRead(GetFixturePath(fixtureName))); + await using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); @@ -85,10 +87,10 @@ public class LargeArchiveTests : TestBase private static string GetFixturePath(string fixtureName) => Path.Combine(TEST_ARCHIVES_PATH, fixtureName); - private string GetArchiveFixturePath(string fixtureName) => + private string GetMaterializedFixturePath(string fixtureName) => fixtureName == "Large/Large.tar" ? MaterializeTarFixture() : GetFixturePath(fixtureName); - private async Task GetArchiveFixturePathAsync(string fixtureName) => + private async Task GetMaterializedFixturePathAsync(string fixtureName) => fixtureName == "Large/Large.tar" ? await MaterializeTarFixtureAsync() : GetFixturePath(fixtureName); diff --git a/tests/TestArchives/Archives/Large/README.md b/tests/TestArchives/Archives/Large/README.md index 4992f31b..0797db5d 100644 --- a/tests/TestArchives/Archives/Large/README.md +++ b/tests/TestArchives/Archives/Large/README.md @@ -7,16 +7,15 @@ deterministic, repeated text pattern with a 2 KiB `0xFF` prefix. The entry CRC-3 | Fixture | Archive API | Reader API | | --- | --- | --- | | `Large.zip` | Yes | Yes | -| Generated `Large.tar` | Yes | N/A | +| Generated `Large.tar` | Yes | Yes | | `Large.gz` | Yes | Yes | | `Large.rar` | Yes | Yes | | `Large.7z` | Yes | No | | `Large.tar.gz` | No | Yes | The compressible payload keeps the compressed fixtures small while requiring a full -64 MiB decompression to validate each API. The Archive API test expands `Large.tar.gz` -to a scratch `Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is -not committed. +64 MiB decompression to validate each API. The tests expand `Large.tar.gz` to a scratch +`Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is not committed. ## Regenerating From 369ab91522f81e14efa90ee368687f2015fc1788 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 3 Aug 2026 16:01:58 +0100 Subject: [PATCH 3/7] fix windows build --- tests/SharpCompress.Test/LargeArchiveTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 761abda6..18b427e2 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -40,13 +40,24 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { +#if LEGACY_DOTNET + using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); +#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); +#endif await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); var entry = await GetSingleEntryAsync(archive); + +#if LEGACY_DOTNET + using var entryStream = await entry.OpenEntryStreamAsync(); +#else await using var entryStream = await entry.OpenEntryStreamAsync(); +#endif await VerifyContentAsync(entry.Key, entryStream); } @@ -72,14 +83,25 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { +#if LEGACY_DOTNET + using var stream = new AsyncOnlyStream( + File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) + ); +#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); +#endif await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); Assert.False(reader.Entry.IsDirectory); + +#if LEGACY_DOTNET + using var entryStream = await reader.OpenEntryStreamAsync(); +#else await using var entryStream = await reader.OpenEntryStreamAsync(); +#endif await VerifyContentAsync(reader.Entry.Key, entryStream); Assert.False(await reader.MoveToNextEntryAsync()); } From 4a71ec898bb06e9536a0c219f722dfa8a6f5b35b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 3 Aug 2026 16:38:27 +0100 Subject: [PATCH 4/7] fixes found by gh and sol --- src/SharpCompress/Factories/GZipFactory.cs | 19 ++++++++++--- src/SharpCompress/Factories/LzwFactory.cs | 18 +++++++++--- tests/SharpCompress.Test/LargeArchiveTests.cs | 16 +++++++++++ .../Tar/TarReaderAsyncTests.cs | 28 +++++++++++++++---- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 4649f0fc..7dfdd313 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -185,14 +185,25 @@ public class GZipFactory } sharpCompressStream.Rewind(); - var tarReader = await new TarFactory() - .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + using var testStream = SharpCompressStream.CreateNonDisposing( + await options + .Providers.CreateDecompressStreamAsync( + CompressionType.GZip, + SharpCompressStream.CreateNonDisposing(sharpCompressStream), + CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options), + cancellationToken + ) + .ConfigureAwait(false) + ); + var isTarArchive = await TarArchive + .IsTarFileAsync(testStream, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); sharpCompressStream.StopRecording(); - if (tarReader is not null) + if (isTarArchive) { - return tarReader; + return new TarReader(sharpCompressStream, options, CompressionType.GZip); } return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index da475d8a..098ec002 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -100,14 +100,24 @@ public class LzwFactory : Factory, IReaderFactory } sharpCompressStream.Rewind(); - var tarReader = await new TarFactory() - .TryOpenReaderAsync(sharpCompressStream, options, cancellationToken) + using var testStream = SharpCompressStream.CreateNonDisposing( + await options + .Providers.CreateDecompressStreamAsync( + CompressionType.Lzw, + SharpCompressStream.CreateNonDisposing(sharpCompressStream), + cancellationToken + ) + .ConfigureAwait(false) + ); + var isTarArchive = await TarArchive + .IsTarFileAsync(testStream, cancellationToken) .ConfigureAwait(false); + sharpCompressStream.Rewind(); sharpCompressStream.StopRecording(); - if (tarReader is not null) + if (isTarArchive) { - return tarReader; + return new TarReader(sharpCompressStream, options, CompressionType.Lzw); } return await OpenAsyncReader(sharpCompressStream, options, cancellationToken) diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 761abda6..09090f6a 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -84,6 +84,22 @@ public class LargeArchiveTests : TestBase Assert.False(await reader.MoveToNextEntryAsync()); } + [Fact] + public async Task OpenAsyncReader_WithGZipExtensionHint_ShouldStreamLargeTarEntry() + { + using var file = File.OpenRead(GetFixturePath("Large/Large.tar.gz")); + await using var stream = new AsyncOnlyStream(new ForwardOnlyStream(file)); + var options = new ReaderOptions { ExtensionHint = "gz" }; + + await using var reader = await ReaderFactory.OpenAsyncReader(stream, options); + + Assert.True(await reader.MoveToNextEntryAsync()); + Assert.False(reader.Entry.IsDirectory); + await using var entryStream = await reader.OpenEntryStreamAsync(); + await VerifyContentAsync(reader.Entry.Key, entryStream); + Assert.False(await reader.MoveToNextEntryAsync()); + } + private static string GetFixturePath(string fixtureName) => Path.Combine(TEST_ARCHIVES_PATH, fixtureName); diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 90bce78e..c329d9d2 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -47,22 +47,38 @@ public class TarReaderAsyncTests : ReaderTests await ReadAsync("Tar.tar.Z", CompressionType.Lzw); [Theory] - [InlineData("Tar.tar.gz", "gz", CompressionType.GZip)] - [InlineData("Tar.tar.Z", "z", CompressionType.Lzw)] + [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, false)] + [InlineData("Tar.tar.gz", "gz", CompressionType.GZip, true)] + [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, false)] + [InlineData("Tar.tar.Z", "z", CompressionType.Lzw, true)] public async ValueTask ReaderFactory_ExtensionHint_PreservesCompressedTarDetection_Async( string archiveName, string extensionHint, - CompressionType compressionType + CompressionType compressionType, + bool useForwardOnlyStream ) { - using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + Stream source = useForwardOnlyStream ? new ForwardOnlyStream(file) : file; + await using var stream = new AsyncOnlyStream(source); var options = ReaderOptions.ForExternalStream.WithExtensionHint(extensionHint); await using var reader = await ReaderFactory.OpenAsyncReader(stream, options); Assert.Equal(ArchiveType.Tar, reader.Type); - Assert.True(await reader.MoveToNextEntryAsync()); - Assert.Equal(compressionType, reader.Entry.CompressionType); + var entryCount = 0; + while (await reader.MoveToNextEntryAsync()) + { + entryCount++; + Assert.Equal(compressionType, reader.Entry.CompressionType); + if (!reader.Entry.IsDirectory) + { + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); + } + } + + Assert.True(entryCount > 0); + VerifyFiles(); } [Fact] From 62d1b5dcc82d32dcc4254f0d1601767e8a04a08b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:11:26 +0100 Subject: [PATCH 5/7] Provide a better way to manage async disposable streams --- .../Archives/IArchiveEntryExtensions.cs | 9 ++--- src/SharpCompress/Common/EntryStream.Async.cs | 4 +-- src/SharpCompress/Common/EntryStream.cs | 2 +- src/SharpCompress/IO/AsyncDisposableStream.cs | 36 +++++++++++++++++++ src/SharpCompress/IO/AsyncDisposeScope.cs | 36 +++++++++++++++++++ .../IO/ConfiguredAsyncDisposeScope.cs | 19 ++++++++++ .../Polyfills/StreamExtensions.cs | 14 ++++++++ .../Readers/AbstractReader.Async.cs | 15 +------- .../Readers/IAsyncReaderExtensions.cs | 6 ---- .../BZip2/BZip2StreamAsyncTests.cs | 14 +------- .../GZip/GZipCrcExtractionTests.cs | 5 +-- tests/SharpCompress.Test/LargeArchiveTests.cs | 23 ++---------- .../Mocks/AsyncOnlyStream.cs | 3 +- .../Mocks/FlushOnDisposeStream.cs | 5 ++- .../Mocks/ForwardOnlyStream.cs | 7 ++-- tests/SharpCompress.Test/Mocks/TestStream.cs | 7 ++-- .../Mocks/ThrowOnFlushStream.cs | 3 +- .../Mocks/TruncatedStream.cs | 3 +- tests/SharpCompress.Test/ReaderTests.cs | 12 ++----- .../Tar/TarArchiveAsyncTests.cs | 11 ++---- .../Tar/TarReaderAsyncTests.cs | 5 --- .../Tar/TarWriterNonSeekableTests.cs | 7 ++-- .../SharpCompress.Test/Zip/Zip64AsyncTests.cs | 11 ------ .../Zip/ZipCrcExtractionTests.cs | 10 ++---- .../Zip/ZipReaderAsyncTests.cs | 8 ----- .../Zip/ZipWriterNonSeekableTests.cs | 7 ++-- 26 files changed, 141 insertions(+), 141 deletions(-) create mode 100644 src/SharpCompress/IO/AsyncDisposableStream.cs create mode 100644 src/SharpCompress/IO/AsyncDisposeScope.cs create mode 100644 src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 502a419e..c0e58656 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -108,15 +108,12 @@ public static class IArchiveEntryExtensions throw new ExtractionException("Entry is a file directory and cannot be extracted."); } -#if LEGACY_DOTNET - using var entryStream = await archiveEntry + var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#else - await using var entryStream = await archiveEntry - .OpenEntryStreamAsync(cancellationToken) + await using var entryStreamScope = entryStream + .DisposeAsyncScope() .ConfigureAwait(false); -#endif var checkedStream = options is null ? entryStream : IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options); diff --git a/src/SharpCompress/Common/EntryStream.Async.cs b/src/SharpCompress/Common/EntryStream.Async.cs index a58afe70..b7112da7 100644 --- a/src/SharpCompress/Common/EntryStream.Async.cs +++ b/src/SharpCompress/Common/EntryStream.Async.cs @@ -17,7 +17,6 @@ public partial class EntryStream _completed = true; } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { if (_isDisposed) @@ -43,9 +42,8 @@ public partial class EntryStream } } await base.DisposeAsync().ConfigureAwait(false); - await _stream.DisposeAsync().ConfigureAwait(false); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } -#endif public override async Task ReadAsync( byte[] buffer, diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs index b8ccf339..ed4e8fc6 100644 --- a/src/SharpCompress/Common/EntryStream.cs +++ b/src/SharpCompress/Common/EntryStream.cs @@ -8,7 +8,7 @@ using SharpCompress.Readers; namespace SharpCompress.Common; -public partial class EntryStream : Stream +public partial class EntryStream : AsyncDisposableStream { private readonly IReader _reader; private readonly Stream _stream; diff --git a/src/SharpCompress/IO/AsyncDisposableStream.cs b/src/SharpCompress/IO/AsyncDisposableStream.cs new file mode 100644 index 00000000..4c7fafbe --- /dev/null +++ b/src/SharpCompress/IO/AsyncDisposableStream.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// A that is guaranteed to be asynchronously disposable on every target framework. +/// +/// +/// +/// On .NET Framework 4.8 and .NET Standard 2.0, has no DisposeAsync. +/// Microsoft.Bcl.AsyncInterfaces supplies the interface on those +/// targets but cannot retrofit it onto the BCL's , and C# will not accept an +/// extension method for the pattern - await using requires a reachable instance +/// DisposeAsync. Deriving from this class instead of therefore makes a type +/// usable with await using uniformly, with no conditional compilation at the call site. +/// +/// +/// The fallback below is the same behaviour as the BCL's own default , +/// so a derived type may call await base.DisposeAsync() unconditionally on any target. +/// +/// +public abstract class AsyncDisposableStream : Stream +#if NO_STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif +{ +#if NO_STREAM_DISPOSEASYNC + public virtual ValueTask DisposeAsync() + { + Dispose(); + return default; + } +#endif +} diff --git a/src/SharpCompress/IO/AsyncDisposeScope.cs b/src/SharpCompress/IO/AsyncDisposeScope.cs new file mode 100644 index 00000000..d8167a6c --- /dev/null +++ b/src/SharpCompress/IO/AsyncDisposeScope.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// Makes any resource usable with await using, disposing it asynchronously when the runtime type +/// supports it and synchronously otherwise. +/// +/// +/// Needed for locals whose static type is (or another type that +/// only sometimes has DisposeAsync), where await using cannot bind directly on +/// .NET Framework 4.8 / .NET Standard 2.0. Unlike a compile-time guard, this picks the asynchronous path +/// based on the runtime type, so a stream that really is asynchronously disposable is disposed that way on +/// every target framework. Prefer deriving from where the type is ours. +/// +internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDisposable +{ + public ValueTask DisposeAsync() + { + if (resource is IAsyncDisposable asyncDisposable) + { + return asyncDisposable.DisposeAsync(); + } + + resource?.Dispose(); + return default; + } + + /// + /// Mirrors ConfiguredAsyncDisposable so await using can specify context capture without + /// boxing this struct through . + /// + public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) => + new(resource, continueOnCapturedContext); +} \ No newline at end of file diff --git a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs new file mode 100644 index 00000000..f9d631c9 --- /dev/null +++ b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +internal readonly struct ConfiguredAsyncDisposeScope(IDisposable? resource, bool continueOnCapturedContext) +{ + public ConfiguredValueTaskAwaitable DisposeAsync() + { + if (resource is IAsyncDisposable asyncDisposable) + { + return asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext); + } + + resource?.Dispose(); + return default(ValueTask).ConfigureAwait(continueOnCapturedContext); + } +} diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index c6e66b82..d4e80630 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -25,6 +25,20 @@ public static class StreamExtensions public void Skip() => stream.CopyTo(Stream.Null); + /// + /// Returns a scope that disposes this stream when awaited, asynchronously where the runtime type + /// supports it. Lets await using be written against a -typed local on + /// every target framework. + /// + internal AsyncDisposeScope DisposeAsyncScope() => new(stream); + + /// + /// Disposes this stream, asynchronously where the runtime type supports it. Use where the static + /// type is , which has no DisposeAsync on .NET Framework 4.8 / + /// .NET Standard 2.0. + /// + internal ValueTask DisposeAsyncCompat() => new AsyncDisposeScope(stream).DisposeAsync(); + public async ValueTask SkipAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 8628c0c2..969c73e6 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,13 +104,8 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip -#if LEGACY_DOTNET - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#endif } public async ValueTask WriteEntryToAsync( @@ -139,19 +134,11 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { -#if LEGACY_DOTNET - using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); var sourceStream = WrapWithProgress(s, Entry); await sourceStream .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) .ConfigureAwait(false); -#else - await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - var sourceStream = WrapWithProgress(s, Entry); - await sourceStream - .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) - .ConfigureAwait(false); -#endif } public async ValueTask OpenEntryStreamAsync( diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 7515b257..5b4e667b 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -108,15 +108,9 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { -#if LEGACY_DOTNET - using var entryStream = await reader - .OpenEntryStreamAsync(cancellationToken) - .ConfigureAwait(false); -#else await using var entryStream = await reader .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#endif var checkedStream = IEntryExtensions.WrapWithChecksumValidation( reader.Entry, entryStream, diff --git a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs index d5b2ef78..fc7e649c 100644 --- a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs +++ b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs @@ -151,19 +151,8 @@ public class BZip2StreamAsyncTests Assert.True(compressed.Length > 0); // Decompress and verify -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using (var readStream = new MemoryStream(compressed)) - { - using ( - var bzip2Stream = await BZip2Stream.CreateAsync( - new AsyncOnlyStream(readStream), - SharpCompress.Compressors.CompressionMode.Decompress, - false - ) - ) - { -#else - await using (var readStream = new MemoryStream(compressed)) { await using ( var bzip2Stream = await BZip2Stream.CreateAsync( @@ -173,7 +162,6 @@ public class BZip2StreamAsyncTests ) ) { -#endif var result = new StringBuilder(); var buffer = new byte[256]; int bytesRead; diff --git a/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs index 2e98c962..acabcb4b 100644 --- a/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipCrcExtractionTests.cs @@ -68,11 +68,8 @@ public class GZipCrcExtractionTests : TestBase [Fact] public async Task GZipArchive_WriteToFileAsync_Throws_On_Crc_Mismatch() { -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true)); -#else - await using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true)); -#endif await using var archive = await GZipArchive.OpenAsyncArchive(stream); var entry = await archive.EntriesAsync.SingleAsync(); var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString()); diff --git a/tests/SharpCompress.Test/LargeArchiveTests.cs b/tests/SharpCompress.Test/LargeArchiveTests.cs index 18b427e2..50f1cfec 100644 --- a/tests/SharpCompress.Test/LargeArchiveTests.cs +++ b/tests/SharpCompress.Test/LargeArchiveTests.cs @@ -40,24 +40,15 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.7z")] public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName) { -#if LEGACY_DOTNET - using var stream = new AsyncOnlyStream( - File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) - ); -#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); -#endif await using var archive = await ArchiveFactory.OpenAsyncArchive(stream); var entry = await GetSingleEntryAsync(archive); -#if LEGACY_DOTNET - using var entryStream = await entry.OpenEntryStreamAsync(); -#else - await using var entryStream = await entry.OpenEntryStreamAsync(); -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using var entryStreamScope = entryStream.DisposeAsyncScope(); await VerifyContentAsync(entry.Key, entryStream); } @@ -83,25 +74,15 @@ public class LargeArchiveTests : TestBase [InlineData("Large/Large.tar.gz")] public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName) { -#if LEGACY_DOTNET - using var stream = new AsyncOnlyStream( - File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) - ); -#else await using var stream = new AsyncOnlyStream( File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName)) ); -#endif await using var reader = await ReaderFactory.OpenAsyncReader(stream); Assert.True(await reader.MoveToNextEntryAsync()); Assert.False(reader.Entry.IsDirectory); -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif await VerifyContentAsync(reader.Entry.Key, entryStream); Assert.False(await reader.MoveToNextEntryAsync()); } diff --git a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs index 232f60ee..9dec5e5a 100644 --- a/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs @@ -2,10 +2,11 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; -public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : Stream +public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : AsyncDisposableStream { private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); diff --git a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs index 63f20702..28dff313 100644 --- a/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs +++ b/tests/SharpCompress.Test/Mocks/FlushOnDisposeStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -8,7 +9,7 @@ namespace SharpCompress.Test.Mocks; // CryptoStream doesn't always trigger the Flush, so this class is used instead // See https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/cryptostream.cs,141 -public class FlushOnDisposeStream(Stream innerStream) : Stream +public class FlushOnDisposeStream(Stream innerStream) : AsyncDisposableStream { public override bool CanRead => innerStream.CanRead; @@ -48,12 +49,10 @@ public class FlushOnDisposeStream(Stream innerStream) : Stream base.Dispose(disposing); } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { await innerStream.FlushAsync(); innerStream.Close(); await base.DisposeAsync(); } -#endif } diff --git a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs index 32961ebc..f6b7ec8e 100644 --- a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks; /// A forward-only stream wrapper that delegates directly to the underlying stream /// without any buffering. Supports reading and writing but not seeking. /// -public class ForwardOnlyStream : Stream +public class ForwardOnlyStream : AsyncDisposableStream { private readonly Stream _stream; private bool _isDisposed; @@ -142,17 +143,15 @@ public class ForwardOnlyStream : Stream } } -#if !LEGACY_DOTNET public override async ValueTask DisposeAsync() { if (!_isDisposed) { - await _stream.DisposeAsync(); + await _stream.DisposeAsyncCompat(); _isDisposed = true; } await base.DisposeAsync(); } -#endif private void ThrowIfDisposed() { diff --git a/tests/SharpCompress.Test/Mocks/TestStream.cs b/tests/SharpCompress.Test/Mocks/TestStream.cs index d3ff48a8..5061afc3 100644 --- a/tests/SharpCompress.Test/Mocks/TestStream.cs +++ b/tests/SharpCompress.Test/Mocks/TestStream.cs @@ -2,10 +2,11 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; -public class TestStream(Stream stream, bool read, bool write, bool seek) : Stream +public class TestStream(Stream stream, bool read, bool write, bool seek) : AsyncDisposableStream { public TestStream(Stream stream) : this(stream, stream.CanRead, stream.CanWrite, stream.CanSeek) { } @@ -50,14 +51,14 @@ public class TestStream(Stream stream, bool read, bool write, bool seek) : Strea Memory buffer, CancellationToken cancellationToken = default ) => stream.ReadAsync(buffer, cancellationToken); +#endif public override async ValueTask DisposeAsync() { await base.DisposeAsync(); - await stream.DisposeAsync(); + await stream.DisposeAsyncCompat(); IsDisposed = true; } -#endif public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); diff --git a/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs index 2cf1a84c..911762a5 100644 --- a/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs +++ b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks; /// A stream wrapper that throws NotSupportedException on Flush() calls. /// This is used to test that archive iteration handles streams that don't support flushing. /// -public class ThrowOnFlushStream : Stream +public class ThrowOnFlushStream : AsyncDisposableStream { private readonly Stream inner; diff --git a/tests/SharpCompress.Test/Mocks/TruncatedStream.cs b/tests/SharpCompress.Test/Mocks/TruncatedStream.cs index 8699a430..55037f2e 100644 --- a/tests/SharpCompress.Test/Mocks/TruncatedStream.cs +++ b/tests/SharpCompress.Test/Mocks/TruncatedStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using SharpCompress.IO; namespace SharpCompress.Test.Mocks; @@ -7,7 +8,7 @@ namespace SharpCompress.Test.Mocks; /// A stream wrapper that truncates the underlying stream after reading a specified number of bytes. /// Used for testing error handling when streams end prematurely. /// -public class TruncatedStream : Stream +public class TruncatedStream : AsyncDisposableStream { private readonly Stream baseStream; private readonly long truncateAfterBytes; diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 4c9024c9..64469219 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -159,18 +159,12 @@ public abstract class ReaderTests : TestBase { using var file = File.OpenRead(testArchive); -#if !LEGACY_DOTNET - await using var protectedStream = SharpCompressStream.CreateNonDisposing( + // SharpCompressStream is not yet an AsyncDisposableStream, so scope its disposal + var protectedStream = SharpCompressStream.CreateNonDisposing( new ForwardOnlyStream(file, options.BufferSize) ); + await using var protectedStreamScope = protectedStream.DisposeAsyncScope(); await using var testStream = new TestStream(protectedStream); -#else - - using var protectedStream = SharpCompressStream.CreateNonDisposing( - new ForwardOnlyStream(file, options.BufferSize) - ); - using var testStream = new TestStream(protectedStream); -#endif await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(testStream), diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 3a1b95cc..dabc0fa2 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -187,10 +187,6 @@ public class TarArchiveAsyncTests : ArchiveTests } } } -#if LEGACY_DOTNET - //add a delay because old .net sucks on DisposeAsync - await Task.Delay(TimeSpan.FromSeconds(1)); -#endif } [Fact] @@ -338,11 +334,8 @@ public class TarArchiveAsyncTests : ArchiveTests { ++numberOfEntries; -#if LEGACY_DOTNET - using var tarEntryStream = await entry.OpenEntryStreamAsync(); -#else - await using var tarEntryStream = await entry.OpenEntryStreamAsync(); -#endif + var tarEntryStream = await entry.OpenEntryStreamAsync(); + await using var tarEntryStreamScope = tarEntryStream.DisposeAsyncScope(); using var testFileStream = new MemoryStream(); await tarEntryStream.CopyToAsync(testFileStream); Assert.Equal(testBytes.Length, testFileStream.Length); diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 90bce78e..9307b7d3 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -337,13 +337,8 @@ public class TarReaderAsyncTests : ReaderTests Assert.True(await reader.MoveToNextEntryAsync()); Assert.Equal("inner.tar.gz", reader.Entry.Key); -#if !LEGACY_DOTNET await using var entryStream = await reader.OpenEntryStreamAsync(); await using var flushingStream = new FlushOnDisposeStream(entryStream); -#else - using var entryStream = await reader.OpenEntryStreamAsync(); - using var flushingStream = new FlushOnDisposeStream(entryStream); -#endif // Extract inner.tar.gz await using var innerReader = await ReaderFactory.OpenAsyncReader(flushingStream); diff --git a/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs b/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs index 454c20a6..218533db 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterNonSeekableTests.cs @@ -89,11 +89,8 @@ public class TarWriterNonSeekableTests { var entry = fileEntries.Single(e => e.Key == name); using var extracted = new MemoryStream(); -#if LEGACY_DOTNET - using (var entryStream = await entry.OpenEntryStreamAsync()) -#else - await using (var entryStream = await entry.OpenEntryStreamAsync()) -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using (entryStream.DisposeAsyncScope()) { await entryStream.CopyToAsync(extracted); } diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs index 5124b6b0..626265cd 100644 --- a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -198,11 +198,7 @@ public class Zip64AsyncTests : WriterTests count++; lastKey = rd.Entry.Key; -#if LEGACY_DOTNET - using var entryStream = await rd.OpenEntryStreamAsync(); -#else await using var entryStream = await rd.OpenEntryStreamAsync(); -#endif if (rd.Entry.Key == "small") { using var ms = new MemoryStream(); @@ -337,17 +333,10 @@ public class Zip64AsyncTests : WriterTests ); while (await rd.MoveToNextEntryAsync()) { -#if LEGACY_DOTNET - using (var entryStream = await rd.OpenEntryStreamAsync()) - { - await entryStream.SkipEntryAsync(); - } -#else await using (var entryStream = await rd.OpenEntryStreamAsync()) { await entryStream.SkipEntryAsync(); } -#endif count++; if (prev != null) { diff --git a/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs index cd0e610c..4a61b150 100644 --- a/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipCrcExtractionTests.cs @@ -122,11 +122,8 @@ public class ZipCrcExtractionTests : ArchiveTests using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false); using var archive = ZipArchive.OpenArchive(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var destination = new MemoryStream(); -#else - await using var destination = new MemoryStream(); -#endif var exception = await Assert.ThrowsAsync(async () => await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = true }) @@ -141,11 +138,8 @@ public class ZipCrcExtractionTests : ArchiveTests using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false); using var archive = ZipArchive.OpenArchive(zipStream); var entry = archive.Entries.Single(e => !e.IsDirectory); -#if LEGACY_DOTNET + // MemoryStream has nothing to dispose asynchronously using var destination = new MemoryStream(); -#else - await using var destination = new MemoryStream(); -#endif await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = false }); diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index 4c7cd627..78f70b28 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -315,11 +315,7 @@ public class ZipReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif // Read some data var buffer = new byte[1024]; await entryStream.ReadAsync(buffer, 0, buffer.Length); @@ -342,11 +338,7 @@ public class ZipReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { -#if LEGACY_DOTNET - using var entryStream = await reader.OpenEntryStreamAsync(); -#else await using var entryStream = await reader.OpenEntryStreamAsync(); -#endif // Read some data var buffer = new byte[1024]; await entryStream.ReadAsync(buffer, 0, buffer.Length); diff --git a/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs b/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs index 4dd10b57..95cdcc56 100644 --- a/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipWriterNonSeekableTests.cs @@ -163,11 +163,8 @@ public class ZipWriterNonSeekableTests { var entry = archive.Entries.Single(e => e.Key == name); using var extracted = new MemoryStream(); -#if LEGACY_DOTNET - using (var entryStream = await entry.OpenEntryStreamAsync()) -#else - await using (var entryStream = await entry.OpenEntryStreamAsync()) -#endif + var entryStream = await entry.OpenEntryStreamAsync(); + await using (entryStream.DisposeAsyncScope()) { await entryStream.CopyToAsync(extracted); } From 693921ec730588fbf5940e02465465334aae6b7c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:12:00 +0100 Subject: [PATCH 6/7] fmt --- src/SharpCompress/IO/AsyncDisposeScope.cs | 2 +- src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/IO/AsyncDisposeScope.cs b/src/SharpCompress/IO/AsyncDisposeScope.cs index d8167a6c..9d4538e4 100644 --- a/src/SharpCompress/IO/AsyncDisposeScope.cs +++ b/src/SharpCompress/IO/AsyncDisposeScope.cs @@ -33,4 +33,4 @@ internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDispos /// public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) => new(resource, continueOnCapturedContext); -} \ No newline at end of file +} diff --git a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs index f9d631c9..9ee5e368 100644 --- a/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs +++ b/src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs @@ -4,7 +4,10 @@ using System.Threading.Tasks; namespace SharpCompress.IO; -internal readonly struct ConfiguredAsyncDisposeScope(IDisposable? resource, bool continueOnCapturedContext) +internal readonly struct ConfiguredAsyncDisposeScope( + IDisposable? resource, + bool continueOnCapturedContext +) { public ConfiguredValueTaskAwaitable DisposeAsync() { From 20ea4a3e03a746a16919931e8ce447dda8393251 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 4 Aug 2026 09:27:48 +0100 Subject: [PATCH 7/7] Do need the csproj change --- src/SharpCompress/SharpCompress.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index 991eccb9..c57cb722 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -31,6 +31,9 @@ $(DefineConstants);LEGACY_DOTNET + + $(DefineConstants);NO_STREAM_DISPOSEASYNC + true true