From 9dbe3c7aca4eceb3f21b421a667c2a5cb3cd47a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 15:51:19 +0000 Subject: [PATCH 01/17] Initial plan From 2d5c2210e7a74299af195493c18e65c512d7af22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 16:06:09 +0000 Subject: [PATCH 02/17] Fix tar archive enumeration after fully reading entry streams Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/e5d578fd-e9cd-4c00-bbe7-2f77a394926c Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Common/Tar/TarReadOnlySubStream.cs | 116 ++++++++++++++---- .../SharpCompress.Test/Tar/TarArchiveTests.cs | 57 +++++++++ 2 files changed, 146 insertions(+), 27 deletions(-) diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index 987751a3..bc3408df 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Tar; @@ -8,6 +9,7 @@ internal class TarReadOnlySubStream : Stream private readonly Stream _stream; private bool _isDisposed; + private bool _isPositionedAtNextEntry; private long _amountRead; public TarReadOnlySubStream(Stream stream, long bytesToRead, bool useSyncOverAsyncDispose) @@ -27,27 +29,17 @@ internal class TarReadOnlySubStream : Stream _isDisposed = true; if (disposing) { - // Ensure we read all remaining blocks for this entry. - _stream.Skip(BytesLeftToRead); - _amountRead += BytesLeftToRead; - - // If the last block wasn't a full 512 bytes, skip the remaining padding bytes. - var bytesInLastBlock = _amountRead % 512; - - if (bytesInLastBlock != 0) + if (Utility.UseSyncOverAsyncDispose()) { - if (Utility.UseSyncOverAsyncDispose()) - { #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits #pragma warning disable CA2012 - _stream.SkipAsync(512 - bytesInLastBlock).GetAwaiter().GetResult(); + AdvanceToNextHeaderAsync().GetAwaiter().GetResult(); #pragma warning restore CA2012 #pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - } - else - { - _stream.Skip(512 - bytesInLastBlock); - } + } + else + { + AdvanceToNextHeader(); } } base.Dispose(disposing); @@ -63,17 +55,7 @@ internal class TarReadOnlySubStream : Stream } _isDisposed = true; - // Ensure we read all remaining blocks for this entry. - await _stream.SkipAsync(BytesLeftToRead).ConfigureAwait(false); - _amountRead += BytesLeftToRead; - - // If the last block wasn't a full 512 bytes, skip the remaining padding bytes. - var bytesInLastBlock = _amountRead % 512; - - if (bytesInLastBlock != 0) - { - await _stream.SkipAsync(512 - bytesInLastBlock).ConfigureAwait(false); - } + await AdvanceToNextHeaderAsync().ConfigureAwait(false); GC.SuppressFinalize(this); await base.DisposeAsync().ConfigureAwait(false); @@ -82,6 +64,54 @@ internal class TarReadOnlySubStream : Stream private long BytesLeftToRead { get; set; } + private void AdvanceToNextHeader() + { + if (_isPositionedAtNextEntry) + { + return; + } + + if (BytesLeftToRead > 0) + { + _stream.Skip(BytesLeftToRead); + _amountRead += BytesLeftToRead; + BytesLeftToRead = 0; + } + + // Tar entry data is padded to 512-byte blocks, so callers that read to EOF + // should still leave the shared archive stream positioned at the next header. + var bytesInLastBlock = _amountRead % 512; + if (bytesInLastBlock != 0) + { + _stream.Skip(512 - bytesInLastBlock); + } + + _isPositionedAtNextEntry = true; + } + + private async ValueTask AdvanceToNextHeaderAsync() + { + if (_isPositionedAtNextEntry) + { + return; + } + + if (BytesLeftToRead > 0) + { + await _stream.SkipAsync(BytesLeftToRead).ConfigureAwait(false); + _amountRead += BytesLeftToRead; + BytesLeftToRead = 0; + } + + var bytesInLastBlock = _amountRead % 512; + if (bytesInLastBlock != 0) + { + await _stream.SkipAsync(512 - bytesInLastBlock).ConfigureAwait(false); + } + + _isPositionedAtNextEntry = true; + } + public override bool CanRead => true; public override bool CanSeek => false; @@ -104,6 +134,11 @@ internal class TarReadOnlySubStream : Stream public override int Read(byte[] buffer, int offset, int count) { + if (BytesLeftToRead <= 0) + { + AdvanceToNextHeader(); + return 0; + } if (BytesLeftToRead < count) { count = (int)BytesLeftToRead; @@ -113,6 +148,10 @@ internal class TarReadOnlySubStream : Stream { BytesLeftToRead -= read; _amountRead += read; + if (BytesLeftToRead == 0) + { + AdvanceToNextHeader(); + } } return read; } @@ -121,6 +160,7 @@ internal class TarReadOnlySubStream : Stream { if (BytesLeftToRead <= 0) { + AdvanceToNextHeader(); return -1; } var value = _stream.ReadByte(); @@ -128,6 +168,10 @@ internal class TarReadOnlySubStream : Stream { --BytesLeftToRead; ++_amountRead; + if (BytesLeftToRead == 0) + { + AdvanceToNextHeader(); + } } return value; } @@ -139,6 +183,11 @@ internal class TarReadOnlySubStream : Stream System.Threading.CancellationToken cancellationToken ) { + if (BytesLeftToRead <= 0) + { + await AdvanceToNextHeaderAsync().ConfigureAwait(false); + return 0; + } if (BytesLeftToRead < count) { count = (int)BytesLeftToRead; @@ -150,6 +199,10 @@ internal class TarReadOnlySubStream : Stream { BytesLeftToRead -= read; _amountRead += read; + if (BytesLeftToRead == 0) + { + await AdvanceToNextHeaderAsync().ConfigureAwait(false); + } } return read; } @@ -160,6 +213,11 @@ internal class TarReadOnlySubStream : Stream System.Threading.CancellationToken cancellationToken = default ) { + if (BytesLeftToRead <= 0) + { + await AdvanceToNextHeaderAsync().ConfigureAwait(false); + return 0; + } if (BytesLeftToRead < buffer.Length) { buffer = buffer.Slice(0, (int)BytesLeftToRead); @@ -169,6 +227,10 @@ internal class TarReadOnlySubStream : Stream { BytesLeftToRead -= read; _amountRead += read; + if (BytesLeftToRead == 0) + { + await AdvanceToNextHeaderAsync().ConfigureAwait(false); + } } return read; } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 008bb145..e516e11f 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -312,6 +313,62 @@ public class TarArchiveTests : ArchiveTests Assert.Equal(2, numberOfEntries); } + [Fact] + public void Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream() + { + var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; + var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) + { + ArchiveEncoding = archiveEncoding, + }; + var testBytes = Encoding.UTF8.GetBytes("This is a test."); + + using var memoryStream = new MemoryStream(); + using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions)) + using (var testFileStream = new MemoryStream(testBytes)) + { + tarWriter.Write("file0.txt", testFileStream); + testFileStream.Position = 0; + tarWriter.Write("file1.txt", testFileStream); + tarWriter.WriteDirectory("folder0", null); + testFileStream.Position = 0; + tarWriter.Write("folder0/file_in_folder0.txt", testFileStream); + } + + memoryStream.Position = 0; + + var entryKeys = new List(); + var openEntryStreams = new List(); + + using (var archive = ArchiveFactory.OpenArchive(memoryStream)) + { + foreach (var entry in archive.Entries) + { + entryKeys.Add(entry.Key); + if (entry.IsDirectory) + { + continue; + } + + var tarEntryStream = entry.OpenEntryStream(); + openEntryStreams.Add(tarEntryStream); + + using var testFileStream = new MemoryStream(); + tarEntryStream.CopyTo(testFileStream); + Assert.Equal(testBytes.Length, testFileStream.Length); + } + + Assert.Equal(4, archive.Entries.Count()); + } + + openEntryStreams.ForEach(stream => stream.Dispose()); + + Assert.Equal( + ["file0.txt", "file1.txt", "folder0/", "folder0/file_in_folder0.txt"], + entryKeys + ); + } + [Fact] public void Tar_Detect_Test() { From 76ed18fc76a99b06e93348f7d0ad428762a489e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 12:59:15 +0000 Subject: [PATCH 03/17] Initial plan From 5adae487c7af9d3f137187efb0446c421b3ef15e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 13:12:11 +0000 Subject: [PATCH 04/17] Fix writable archive async disposal and add regression tests --- .../Archives/AbstractWritableArchive.Async.cs | 8 ++++++ src/SharpCompress/packages.lock.json | 12 ++++---- .../SharpCompress.AotSmoke/packages.lock.json | 28 ++++--------------- .../GZip/GZipArchiveAsyncTests.cs | 19 +++++++++++++ .../Tar/TarArchiveAsyncTests.cs | 22 +++++++++++++++ .../Zip/ZipArchiveAsyncTests.cs | 22 +++++++++++++++ 6 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs index a8012896..67b8dffc 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs @@ -122,6 +122,14 @@ public abstract partial class AbstractWritableArchive .ConfigureAwait(false); } + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync().ConfigureAwait(false); + newEntries.Cast().ForEach(x => x.Close()); + removedEntries.Cast().ForEach(x => x.Close()); + modifiedEntries.Cast().ForEach(x => x.Close()); + } + protected abstract ValueTask SaveToAsync( Stream stream, TOptions options, diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e1e7afa5..05ccd77c 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -286,9 +286,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.8, )", - "resolved": "10.0.8", - "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -388,9 +388,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.27, )", - "resolved": "8.0.27", - "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 74a5d802..33d459fa 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -4,15 +4,15 @@ "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "f9u8fMRROe2lS5MOOLutK6iSNTK9pC3kqd90FIn8Sk29fbZ0QDjZrBbwUkhouk/8dppC71SIEQaag0lGRTxvfA==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -63,22 +63,6 @@ "sharpcompress": { "type": "Project" } - }, - "net10.0/osx-arm64": { - "Microsoft.DotNet.ILCompiler": { - "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==", - "dependencies": { - "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "10.0.6" - } - }, - "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": { - "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "+yovwOAlIpfIcH+ZWmLYXWTSWYJ93wcQxF/RVk+X4MXgLASeosCJYVLqP20g0cufKjoRqvCmnklR6y9Su3ORtA==" - } } } } \ No newline at end of file diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index e8ce2a9d..d7812b57 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -221,4 +221,23 @@ public class GZipArchiveAsyncTests : ArchiveTests Path.Combine(scratchPath2, "Tar.tar") ); } + + [Fact] + public async ValueTask GZip_Async_Dispose_Closes_New_Entry_Stream() + { + var entryStream = new TestStream(new MemoryStream(new byte[] { 1, 2, 3 })); + + await using (var archive = await GZipArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync( + "test.bin", + entryStream, + closeStream: true, + size: entryStream.Length + ); + await archive.SaveToAsync(new MemoryStream(), new GZipWriterOptions()); + } + + Assert.True(entryStream.IsDisposed); + } } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 684f05c2..1324212f 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -179,6 +179,28 @@ public class TarArchiveAsyncTests : ArchiveTests CompareArchivesByPath(unmodified, scratchPath); } + [Fact] + public async ValueTask Tar_Async_Dispose_Closes_New_Entry_Stream() + { + var entryStream = new TestStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + await using (var archive = await TarArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync( + "test.txt", + entryStream, + closeStream: true, + size: entryStream.Length + ); + await archive.SaveToAsync( + new MemoryStream(), + new TarWriterOptions(CompressionType.None, true) + ); + } + + Assert.True(entryStream.IsDisposed); + } + [Fact] public async ValueTask Tar_Random_Write_Add_Async() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index 86b97c05..420ce481 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -186,6 +186,28 @@ public class ZipArchiveAsyncTests : ArchiveTests CompareArchivesByPath(unmodified, scratchPath); } + [Fact] + public async ValueTask Zip_Async_Dispose_Closes_New_Entry_Stream() + { + var entryStream = new TestStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + + await using (var archive = await ZipArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync( + "test.txt", + entryStream, + closeStream: true, + size: entryStream.Length + ); + await archive.SaveToAsync( + new MemoryStream(), + new ZipWriterOptions(CompressionType.Deflate) + ); + } + + Assert.True(entryStream.IsDisposed); + } + [Fact] public async ValueTask Zip_Deflate_Entry_Stream_Async() { From 22e32f2a10c25ca6f727cb37e523a77be9d86637 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 13:12:33 +0000 Subject: [PATCH 05/17] Revert unintended lockfile updates --- src/SharpCompress/packages.lock.json | 12 ++++---- .../SharpCompress.AotSmoke/packages.lock.json | 28 +++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 05ccd77c..e1e7afa5 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -286,9 +286,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -388,9 +388,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.27, )", + "resolved": "8.0.27", + "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 33d459fa..74a5d802 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -4,15 +4,15 @@ "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "f9u8fMRROe2lS5MOOLutK6iSNTK9pC3kqd90FIn8Sk29fbZ0QDjZrBbwUkhouk/8dppC71SIEQaag0lGRTxvfA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -63,6 +63,22 @@ "sharpcompress": { "type": "Project" } + }, + "net10.0/osx-arm64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==", + "dependencies": { + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "10.0.6" + } + }, + "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.6", + "contentHash": "+yovwOAlIpfIcH+ZWmLYXWTSWYJ93wcQxF/RVk+X4MXgLASeosCJYVLqP20g0cufKjoRqvCmnklR6y9Su3ORtA==" + } } } } \ No newline at end of file From 3f79edd31e37d74b31ebb9d505d654a8f7f27a1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 07:53:28 +0000 Subject: [PATCH 06/17] Initial plan From d5a8f98500267dbf0467c086405761749520ad17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 08:02:33 +0000 Subject: [PATCH 07/17] Fix async progress reporting for solid 7z extraction --- .../Archives/IAsyncArchiveExtensions.cs | 23 ++++++++++++++++--- .../SevenZip/SevenZipArchiveAsyncTests.cs | 22 ++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs index 4be1419f..534ef1ec 100644 --- a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -29,12 +29,29 @@ public static class IAsyncArchiveExtensions || archive.Type == ArchiveType.SevenZip ) { + var totalBytes = await archive.TotalUncompressedSizeAsync().ConfigureAwait(false); + var bytesRead = 0L; await using var reader = await archive .ExtractAllEntriesAsync() .ConfigureAwait(false); - await reader - .WriteAllToDirectoryAsync(destinationDirectory, options, cancellationToken) - .ConfigureAwait(false); + while (await reader.MoveToNextEntryAsync(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + + await reader + .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + + if (reader.Entry.IsDirectory) + { + continue; + } + + bytesRead += reader.Entry.Size; + progress?.Report( + new ProgressReport(reader.Entry.Key ?? string.Empty, bytesRead, totalBytes) + ); + } } else { diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs index b4f3c855..6c89c1db 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs @@ -1,9 +1,11 @@ +using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.SevenZip; +using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Test.Mocks; using Xunit; @@ -141,6 +143,26 @@ public class SevenZipArchiveAsyncTests : ArchiveTests VerifyFiles(); } + [Fact] + public async Task SevenZipArchive_Solid_WriteToDirectoryAsync_WithProgress() + { + var progressReports = new System.Collections.Generic.List(); + var progress = new Progress(report => progressReports.Add(report)); + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); +#if NETFRAMEWORK + using var stream = File.OpenRead(testArchive); +#else + await using var stream = File.OpenRead(testArchive); +#endif + await using var archive = await ArchiveFactory.OpenAsyncArchive(new AsyncOnlyStream(stream)); + + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress: progress); + + await Task.Delay(1000); + VerifyFiles(); + Assert.True(progressReports.Count > 0, "Progress reports should be generated"); + } + [Fact] public async Task SevenZipArchive_BZip2_AsyncStreamExtraction() { From afea217934389d50e4c48844aaef0f1068cbacd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 08:05:22 +0000 Subject: [PATCH 08/17] chore: finalize security verification --- src/SharpCompress/packages.lock.json | 12 ++++---- .../SharpCompress.AotSmoke/packages.lock.json | 28 ++++--------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e1e7afa5..05ccd77c 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -286,9 +286,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.8, )", - "resolved": "10.0.8", - "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -388,9 +388,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.27, )", - "resolved": "8.0.27", - "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 74a5d802..33d459fa 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -4,15 +4,15 @@ "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "f9u8fMRROe2lS5MOOLutK6iSNTK9pC3kqd90FIn8Sk29fbZ0QDjZrBbwUkhouk/8dppC71SIEQaag0lGRTxvfA==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -63,22 +63,6 @@ "sharpcompress": { "type": "Project" } - }, - "net10.0/osx-arm64": { - "Microsoft.DotNet.ILCompiler": { - "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "nBOzxOys8OeyJ+Nsi/uYlI/5TSsvwjaM/p5m4dTL6khCLx9UuP3b2ec3HeuBw/+F7hHCAZG1yFx8VBeoRAX+EQ==", - "dependencies": { - "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": "10.0.6" - } - }, - "runtime.osx-arm64.Microsoft.DotNet.ILCompiler": { - "type": "Transitive", - "resolved": "10.0.6", - "contentHash": "+yovwOAlIpfIcH+ZWmLYXWTSWYJ93wcQxF/RVk+X4MXgLASeosCJYVLqP20g0cufKjoRqvCmnklR6y9Su3ORtA==" - } } } } \ No newline at end of file From ae8d983b722d63c8c87bf108bd6c97718e499604 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 08:13:41 +0000 Subject: [PATCH 09/17] Fix flaky progress test by using synchronous IProgress implementation --- src/SharpCompress/packages.lock.json | 12 ++++++------ .../SevenZip/SevenZipArchiveAsyncTests.cs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 05ccd77c..e1e7afa5 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -286,9 +286,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -388,9 +388,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.27, )", + "resolved": "8.0.27", + "contentHash": "rQi9TxifHRnXP7lVRZH05DxD2/XGbJp12q0ozcbrlBlBnyyzssFTH/2vLhtKWUp2CT1qVscTrcYTFiwTyKPKRg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs index 6c89c1db..859910f2 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs @@ -147,7 +147,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests public async Task SevenZipArchive_Solid_WriteToDirectoryAsync_WithProgress() { var progressReports = new System.Collections.Generic.List(); - var progress = new Progress(report => progressReports.Add(report)); + var progress = new SynchronousProgress(report => progressReports.Add(report)); var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); #if NETFRAMEWORK using var stream = File.OpenRead(testArchive); @@ -158,7 +158,6 @@ public class SevenZipArchiveAsyncTests : ArchiveTests await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress: progress); - await Task.Delay(1000); VerifyFiles(); Assert.True(progressReports.Count > 0, "Progress reports should be generated"); } @@ -358,4 +357,13 @@ public class SevenZipArchiveAsyncTests : ArchiveTests // The critical check: within a single folder, the stream should NEVER be recreated Assert.Equal(0, streamRecreationsWithinFolder); // Folder stream should remain the same for all entries in the same folder } + + private sealed class SynchronousProgress : IProgress + { + private readonly Action _handler; + + public SynchronousProgress(Action handler) => _handler = handler; + + public void Report(T value) => _handler(value); + } } From 7c5a7dccb1f0218f589bd0a4119d3b1891974dff Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 09:25:14 +0100 Subject: [PATCH 10/17] Try to fix global.json to avoid churn in locks --- global.json | 4 +-- tests/SharpCompress.Test/packages.lock.json | 31 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 512142d2..50baaf1a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100", - "rollForward": "latestFeature" + "version": "10.0.300", + "rollForward": "latestPatch" } } diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index 9f666ea6..d8e93f08 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -315,6 +315,30 @@ } } }, + ".NETFramework,Version=v4.8/win-x86": { + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + } + }, "net10.0": { "AwesomeAssertions": { "type": "Direct", @@ -533,6 +557,13 @@ "resolved": "8.0.0", "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" } + }, + "net10.0/win-x86": { + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + } } } } \ No newline at end of file From d6efe32cda9ca43edf18c370dbc84a2a734805cf Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 09:25:52 +0100 Subject: [PATCH 11/17] fmt --- src/SharpCompress/Archives/IAsyncArchiveExtensions.cs | 6 +++++- .../SevenZip/SevenZipArchiveAsyncTests.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs index 534ef1ec..e9fd9723 100644 --- a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -39,7 +39,11 @@ public static class IAsyncArchiveExtensions cancellationToken.ThrowIfCancellationRequested(); await reader - .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .WriteEntryToDirectoryAsync( + destinationDirectory, + options, + cancellationToken + ) .ConfigureAwait(false); if (reader.Entry.IsDirectory) diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs index 859910f2..d28b6d54 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs @@ -147,14 +147,18 @@ public class SevenZipArchiveAsyncTests : ArchiveTests public async Task SevenZipArchive_Solid_WriteToDirectoryAsync_WithProgress() { var progressReports = new System.Collections.Generic.List(); - var progress = new SynchronousProgress(report => progressReports.Add(report)); + var progress = new SynchronousProgress(report => + progressReports.Add(report) + ); var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z"); #if NETFRAMEWORK using var stream = File.OpenRead(testArchive); #else await using var stream = File.OpenRead(testArchive); #endif - await using var archive = await ArchiveFactory.OpenAsyncArchive(new AsyncOnlyStream(stream)); + await using var archive = await ArchiveFactory.OpenAsyncArchive( + new AsyncOnlyStream(stream) + ); await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress: progress); From bb7ab319c8d058827aeb49056aaf7452175d7728 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 10:00:59 +0100 Subject: [PATCH 12/17] add rtk gubbins to ignore things --- .github/copilot-instructions.md | 27 +++++++++++++++++++++++++++ .github/hooks/rtk-rewrite.json | 12 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/hooks/rtk-rewrite.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..96094115 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,27 @@ + +# RTK — Token-Optimized CLI + +**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens. + +## Rule + +Always prefix shell commands with `rtk`: + +```bash +# Instead of: Use: +git status rtk git status +git log -10 rtk git log -10 +cargo test rtk cargo test +docker ps rtk docker ps +kubectl get pods rtk kubectl pods +``` + +## Meta commands (use directly) + +```bash +rtk gain # Token savings dashboard +rtk gain --history # Per-command savings history +rtk discover # Find missed rtk opportunities +rtk proxy # Run raw (no filtering) but track usage +``` + diff --git a/.github/hooks/rtk-rewrite.json b/.github/hooks/rtk-rewrite.json new file mode 100644 index 00000000..eb2a5a7f --- /dev/null +++ b/.github/hooks/rtk-rewrite.json @@ -0,0 +1,12 @@ +{ + "hooks": { + "PreToolUse": [ + { + "type": "command", + "command": "rtk hook copilot", + "cwd": ".", + "timeout": 5 + } + ] + } +} From e7eb1097fa56e5a8afcff626ff3cbb4e172ccc26 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 10:07:10 +0100 Subject: [PATCH 13/17] update settings to avoid package lock churn --- .github/workflows/nuget-release.yml | 2 +- .github/workflows/performance-benchmarks.yml | 2 +- build/Program.cs | 4 ++- .../SharpCompress.AotSmoke.csproj | 1 + .../SharpCompress.AotSmoke/packages.lock.json | 28 +++++++++++++---- .../SharpCompress.Test.csproj | 2 ++ tests/SharpCompress.Test/packages.lock.json | 31 ------------------- 7 files changed, 30 insertions(+), 40 deletions(-) diff --git a/.github/workflows/nuget-release.yml b/.github/workflows/nuget-release.yml index ccc626cc..345b82c4 100644 --- a/.github/workflows/nuget-release.yml +++ b/.github/workflows/nuget-release.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x + global-json-file: global.json # Determine version using C# build target - name: Determine Version diff --git a/.github/workflows/performance-benchmarks.yml b/.github/workflows/performance-benchmarks.yml index 7f5acdbb..3adf01aa 100644 --- a/.github/workflows/performance-benchmarks.yml +++ b/.github/workflows/performance-benchmarks.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Build Performance Project run: dotnet build tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release diff --git a/build/Program.cs b/build/Program.cs index 1c36e0a9..4f9d313c 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -12,6 +12,7 @@ using static SimpleExec.Command; const string Clean = "clean"; const string Restore = "restore"; +const string UpdateLocks = "update-locks"; const string Build = "build"; const string Test = "test"; const string Format = "format"; @@ -66,7 +67,8 @@ Target( Run("dotnet", "csharpier check ."); } ); -Target(Restore, [CheckFormat], () => Run("dotnet", "restore")); +Target(Restore, [CheckFormat], () => Run("dotnet", "restore --locked-mode")); +Target(UpdateLocks, [CheckFormat], () => Run("dotnet", "restore --force-evaluate")); Target( Build, diff --git a/tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj b/tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj index c6f6314b..aed0b97d 100644 --- a/tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj +++ b/tests/SharpCompress.AotSmoke/SharpCompress.AotSmoke.csproj @@ -2,6 +2,7 @@ Exe net10.0 + linux-x64 true true full diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 33d459fa..776cf41a 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -4,15 +4,15 @@ "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "f9u8fMRROe2lS5MOOLutK6iSNTK9pC3kqd90FIn8Sk29fbZ0QDjZrBbwUkhouk/8dppC71SIEQaag0lGRTxvfA==" + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "RJxitcN5CCyZDcPNXKLsecwKvACzmy8C1z8hGM9+hFcnPhv1jDysJFFIeUHIPWaZ6wDAfYtZcgKEtegvL2Nz8A==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -63,6 +63,22 @@ "sharpcompress": { "type": "Project" } + }, + "net10.0/linux-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "RJxitcN5CCyZDcPNXKLsecwKvACzmy8C1z8hGM9+hFcnPhv1jDysJFFIeUHIPWaZ6wDAfYtZcgKEtegvL2Nz8A==", + "dependencies": { + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.8" + } + }, + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "0jxyi69frgaqADCnEpHE+f65NoiRTAjfjvNDMOxWV77BumQ56eMDL4ECw29DcJTqwaYJQ92PqDS6y6CiLf7kgw==" + } } } } \ No newline at end of file diff --git a/tests/SharpCompress.Test/SharpCompress.Test.csproj b/tests/SharpCompress.Test/SharpCompress.Test.csproj index 9d56d13f..68ff64ae 100644 --- a/tests/SharpCompress.Test/SharpCompress.Test.csproj +++ b/tests/SharpCompress.Test/SharpCompress.Test.csproj @@ -9,6 +9,8 @@ $(DefineConstants);LEGACY_DOTNET + AnyCPU + false $(DefineConstants);WINDOWS diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index d8e93f08..9f666ea6 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -315,30 +315,6 @@ } } }, - ".NETFramework,Version=v4.8/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - } - }, "net10.0": { "AwesomeAssertions": { "type": "Direct", @@ -557,13 +533,6 @@ "resolved": "8.0.0", "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" } - }, - "net10.0/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" - } } } } \ No newline at end of file From ee64868919129a7910a219d7c174b9b5660ccd76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 11:58:38 +0000 Subject: [PATCH 14/17] Add tar reader regression tests --- .../Tar/TarArchiveAsyncTests.cs | 62 +++++++++++++++++++ .../Tar/TarReaderAsyncTests.cs | 58 +++++++++++++++++ .../SharpCompress.Test/Tar/TarReaderTests.cs | 56 +++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index b9b7d6eb..c6decdd5 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -275,4 +276,65 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(2, numberOfEntries); } + + [Fact] + public async ValueTask Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream_Async() + { + var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; + var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) + { + ArchiveEncoding = archiveEncoding, + }; + var testBytes = Encoding.UTF8.GetBytes("This is a test."); + + using var memoryStream = new MemoryStream(); + using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions)) + using (var testFileStream = new MemoryStream(testBytes)) + { + await tarWriter.WriteAsync("file0.txt", testFileStream, null); + testFileStream.Position = 0; + await tarWriter.WriteAsync("file1.txt", testFileStream, null); + tarWriter.WriteDirectory("folder0", null); + testFileStream.Position = 0; + await tarWriter.WriteAsync("folder0/file_in_folder0.txt", testFileStream, null); + } + + memoryStream.Position = 0; + + var entryKeys = new List(); + var openEntryStreams = new List(); + + await using ( + var archive = await TarArchive.OpenAsyncArchive( + new AsyncOnlyStream(memoryStream), + ReaderOptions.ForExternalStream + ) + ) + { + await foreach (var entry in archive.EntriesAsync) + { + entryKeys.Add(entry.Key); + if (entry.IsDirectory) + { + continue; + } + + var tarEntryStream = await entry.OpenEntryStreamAsync(); + openEntryStreams.Add(tarEntryStream); + + using var testFileStream = new MemoryStream(); + await tarEntryStream.CopyToAsync(testFileStream); + Assert.Equal(testBytes.Length, testFileStream.Length); + } + + Assert.Equal(4, await archive.EntriesAsync.CountAsync()); + } + + openEntryStreams.ForEach(stream => stream.Dispose()); + + Assert.Equal( + ["file0.txt", "file1.txt", "folder0/", "folder0/file_in_folder0.txt"], + entryKeys + ); + } } diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 34ceac02..e42bcc13 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Factories; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; +using SharpCompress.Writers.Tar; using Xunit; namespace SharpCompress.Test.Tar; @@ -199,6 +201,62 @@ public class TarReaderAsyncTests : ReaderTests ); } + [Fact] + public async ValueTask Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream_Async() + { + var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; + var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) + { + ArchiveEncoding = archiveEncoding, + }; + var testBytes = Encoding.UTF8.GetBytes("This is a test."); + + using var memoryStream = new MemoryStream(); + using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions)) + using (var testFileStream = new MemoryStream(testBytes)) + { + await tarWriter.WriteAsync("file0.txt", testFileStream, null); + testFileStream.Position = 0; + await tarWriter.WriteAsync("file1.txt", testFileStream, null); + tarWriter.WriteDirectory("folder0", null); + testFileStream.Position = 0; + await tarWriter.WriteAsync("folder0/file_in_folder0.txt", testFileStream, null); + } + + memoryStream.Position = 0; + + var entryKeys = new List(); + var openEntryStreams = new List(); + + await using ( + var reader = await TarReader.OpenAsyncReader(new AsyncOnlyStream(memoryStream)) + ) + { + while (await reader.MoveToNextEntryAsync()) + { + entryKeys.Add(reader.Entry.Key); + if (reader.Entry.IsDirectory) + { + continue; + } + + var entryStream = await reader.OpenEntryStreamAsync(); + openEntryStreams.Add(entryStream); + + using var testFileStream = new MemoryStream(); + await entryStream.CopyToAsync(testFileStream); + Assert.Equal(testBytes.Length, testFileStream.Length); + } + } + + openEntryStreams.ForEach(stream => stream.Dispose()); + + Assert.Equal( + ["file0.txt", "file1.txt", "folder0/", "folder0/file_in_folder0.txt"], + entryKeys + ); + } + [Fact] public async ValueTask Tar_Corrupted_Async() { diff --git a/tests/SharpCompress.Test/Tar/TarReaderTests.cs b/tests/SharpCompress.Test/Tar/TarReaderTests.cs index d34cc68b..819155a5 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderTests.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using SharpCompress.Common; using SharpCompress.Compressors.BZip2; using SharpCompress.Factories; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; +using SharpCompress.Writers.Tar; using Xunit; namespace SharpCompress.Test.Tar; @@ -231,6 +233,60 @@ public class TarReaderTests : ReaderTests Assert.Throws(() => reader.MoveToNextEntry()); } + [Fact] + public void Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream() + { + var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; + var tarWriterOptions = new TarWriterOptions(CompressionType.None, true) + { + ArchiveEncoding = archiveEncoding, + }; + var testBytes = Encoding.UTF8.GetBytes("This is a test."); + + using var memoryStream = new MemoryStream(); + using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions)) + using (var testFileStream = new MemoryStream(testBytes)) + { + tarWriter.Write("file0.txt", testFileStream, null); + testFileStream.Position = 0; + tarWriter.Write("file1.txt", testFileStream, null); + tarWriter.WriteDirectory("folder0", null); + testFileStream.Position = 0; + tarWriter.Write("folder0/file_in_folder0.txt", testFileStream, null); + } + + memoryStream.Position = 0; + + var entryKeys = new List(); + var openEntryStreams = new List(); + + using (var reader = TarReader.OpenReader(memoryStream)) + { + while (reader.MoveToNextEntry()) + { + entryKeys.Add(reader.Entry.Key); + if (reader.Entry.IsDirectory) + { + continue; + } + + var entryStream = reader.OpenEntryStream(); + openEntryStreams.Add(entryStream); + + using var testFileStream = new MemoryStream(); + entryStream.CopyTo(testFileStream); + Assert.Equal(testBytes.Length, testFileStream.Length); + } + } + + openEntryStreams.ForEach(stream => stream.Dispose()); + + Assert.Equal( + ["file0.txt", "file1.txt", "folder0/", "folder0/file_in_folder0.txt"], + entryKeys + ); + } + [Fact] public void Tar_Corrupted() { From 7a6a8d53f984e098e3003338c989b2059c94cd9c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 15:11:39 +0100 Subject: [PATCH 15/17] Fix merge --- src/SharpCompress/Common/Tar/TarFilePart.cs | 8 ++------ src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs | 7 +------ src/SharpCompress/Common/Tar/TarHeaderFactory.cs | 3 +-- src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs | 2 +- tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs | 3 +++ 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs index 15b76d14..4c32865c 100644 --- a/src/SharpCompress/Common/Tar/TarFilePart.cs +++ b/src/SharpCompress/Common/Tar/TarFilePart.cs @@ -25,7 +25,7 @@ internal sealed class TarFilePart : FilePart if (_seekableStream != null) { _seekableStream.Position = Header.DataStartPosition ?? 0; - return new TarReadOnlySubStream(_seekableStream, Header.Size, false); + return new TarReadOnlySubStream(_seekableStream, Header.Size); } return Header.PackedStream.NotNull(); } @@ -36,13 +36,9 @@ internal sealed class TarFilePart : FilePart { if (_seekableStream != null) { - var useSyncOverAsync = false; -#if LEGACY_DOTNET - useSyncOverAsync = true; -#endif _seekableStream.Position = Header.DataStartPosition ?? 0; return new ValueTask( - new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync) + new TarReadOnlySubStream(_seekableStream, Header.Size) ); } return new ValueTask(Header.PackedStream.NotNull()); diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs index 772a5665..460be1cc 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -44,14 +44,9 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - var useSyncOverAsync = false; -#if LEGACY_DOTNET - useSyncOverAsync = true; -#endif header.PackedStream = new TarReadOnlySubStream( stream, - header.Size, - useSyncOverAsync + header.Size ); } break; diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs index df71351e..6053fa10 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -40,8 +40,7 @@ internal static partial class TarHeaderFactory { header.PackedStream = new TarReadOnlySubStream( stream, - header.Size, - false + header.Size ); } break; diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index bc3408df..86594f63 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -12,7 +12,7 @@ internal class TarReadOnlySubStream : Stream private bool _isPositionedAtNextEntry; private long _amountRead; - public TarReadOnlySubStream(Stream stream, long bytesToRead, bool useSyncOverAsyncDispose) + public TarReadOnlySubStream(Stream stream, long bytesToRead) { _stream = stream; BytesLeftToRead = bytesToRead; diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index d199e09d..28947e97 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -416,6 +416,9 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(4100, localOverrideLink.UserID); Assert.Equal(5100, localOverrideLink.GroupId); Assert.Equal(Convert.ToInt64("777", 8), localOverrideLink.Mode); + } + + [Fact] public async ValueTask Tar_Read_One_At_A_Time_Without_Disposing_Entry_Stream_Async() { var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 }; From 762580caf01137dbf4e5624823f71ead96f80444 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 15:13:05 +0100 Subject: [PATCH 16/17] add aot smoke to sln --- SharpCompress.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/SharpCompress.sln b/SharpCompress.sln index 3609183a..577ddc2c 100644 --- a/SharpCompress.sln +++ b/SharpCompress.sln @@ -30,6 +30,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.AotSmoke", "tests\SharpCompress.AotSmoke\SharpCompress.AotSmoke.csproj", "{C9290353-5931-46DE-970A-63143B0BAE41}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,10 @@ Global {5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Debug|Any CPU.Build.0 = Debug|Any CPU {5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}.Release|Any CPU.Build.0 = Release|Any CPU + {C9290353-5931-46DE-970A-63143B0BAE41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9290353-5931-46DE-970A-63143B0BAE41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9290353-5931-46DE-970A-63143B0BAE41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9290353-5931-46DE-970A-63143B0BAE41}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,5 +66,6 @@ Global {FD19DDD8-72B2-4024-8665-0D1F7A2AA998} = {3C5BE746-03E5-4895-9988-0B57F162F86C} {F2B1A1EB-0FA6-40D0-8908-E13247C7226F} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529} {5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529} + {C9290353-5931-46DE-970A-63143B0BAE41} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529} EndGlobalSection EndGlobal From 0f6f1f2b53d0253788fec2d1d7da9dc089cbc2fe Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 29 May 2026 15:37:14 +0100 Subject: [PATCH 17/17] fmt --- src/SharpCompress/Common/Tar/TarFilePart.cs | 4 +--- src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs | 5 +---- src/SharpCompress/Common/Tar/TarHeaderFactory.cs | 5 +---- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs index 4c32865c..eb2f4f33 100644 --- a/src/SharpCompress/Common/Tar/TarFilePart.cs +++ b/src/SharpCompress/Common/Tar/TarFilePart.cs @@ -37,9 +37,7 @@ internal sealed class TarFilePart : FilePart if (_seekableStream != null) { _seekableStream.Position = Header.DataStartPosition ?? 0; - return new ValueTask( - new TarReadOnlySubStream(_seekableStream, Header.Size) - ); + return new ValueTask(new TarReadOnlySubStream(_seekableStream, Header.Size)); } return new ValueTask(Header.PackedStream.NotNull()); } diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs index 460be1cc..c011abd2 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -44,10 +44,7 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream( - stream, - header.Size - ); + header.PackedStream = new TarReadOnlySubStream(stream, header.Size); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs index 6053fa10..c95efaef 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -38,10 +38,7 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream( - stream, - header.Size - ); + header.PackedStream = new TarReadOnlySubStream(stream, header.Size); } break; default: