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 + } + ] + } +} 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/SharpCompress.sln b/SharpCompress.sln index 65ac7b42..577ddc2c 100644 --- a/SharpCompress.sln +++ b/SharpCompress.sln @@ -30,7 +30,7 @@ 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", "{50C6DA22-2124-4E47-9FB7-96FEC5F59566}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.AotSmoke", "tests\SharpCompress.AotSmoke\SharpCompress.AotSmoke.csproj", "{C9290353-5931-46DE-970A-63143B0BAE41}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -54,10 +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 - {50C6DA22-2124-4E47-9FB7-96FEC5F59566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {50C6DA22-2124-4E47-9FB7-96FEC5F59566}.Debug|Any CPU.Build.0 = Debug|Any CPU - {50C6DA22-2124-4E47-9FB7-96FEC5F59566}.Release|Any CPU.ActiveCfg = Release|Any CPU - {50C6DA22-2124-4E47-9FB7-96FEC5F59566}.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 @@ -66,6 +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} - {50C6DA22-2124-4E47-9FB7-96FEC5F59566} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529} + {C9290353-5931-46DE-970A-63143B0BAE41} = {0F0901FF-E8D9-426A-B5A2-17C7F47C1529} EndGlobalSection EndGlobal 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/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/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/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs index 4be1419f..e9fd9723 100644 --- a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -29,12 +29,33 @@ 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/src/SharpCompress/Common/Tar/TarFilePart.cs b/src/SharpCompress/Common/Tar/TarFilePart.cs index 15b76d14..eb2f4f33 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,14 +36,8 @@ 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) - ); + 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 772a5665..c011abd2 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -44,15 +44,7 @@ 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.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 df71351e..c95efaef 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs @@ -38,11 +38,7 @@ internal static partial class TarHeaderFactory break; case StreamingMode.Streaming: { - header.PackedStream = new TarReadOnlySubStream( - stream, - header.Size, - false - ); + header.PackedStream = new TarReadOnlySubStream(stream, header.Size); } break; default: diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index 987751a3..86594f63 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,9 +9,10 @@ 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) + public TarReadOnlySubStream(Stream stream, long bytesToRead) { _stream = stream; BytesLeftToRead = bytesToRead; @@ -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.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.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/SevenZip/SevenZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs index b4f3c855..d28b6d54 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,29 @@ public class SevenZipArchiveAsyncTests : ArchiveTests VerifyFiles(); } + [Fact] + public async Task SevenZipArchive_Solid_WriteToDirectoryAsync_WithProgress() + { + var progressReports = new System.Collections.Generic.List(); + 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 archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress: progress); + + VerifyFiles(); + Assert.True(progressReports.Count > 0, "Progress reports should be generated"); + } + [Fact] public async Task SevenZipArchive_BZip2_AsyncStreamExtraction() { @@ -336,4 +361,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); + } } 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/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 684f05c2..28947e97 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; @@ -179,6 +180,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() { @@ -394,4 +417,65 @@ public class TarArchiveAsyncTests : ArchiveTests 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 }; + 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/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 1ea5b3f8..7959967e 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; @@ -421,6 +422,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() { diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 5d096c96..d0104cbb 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar; @@ -8,6 +9,7 @@ using SharpCompress.Factories; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; +using SharpCompress.Writers.Tar; using Xunit; namespace SharpCompress.Test.Tar; @@ -347,6 +349,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 34a695c8..39c1ba15 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using SharpCompress.Common; using SharpCompress.Common.Tar; using SharpCompress.Compressors.BZip2; @@ -8,6 +9,7 @@ using SharpCompress.Factories; using SharpCompress.Readers; using SharpCompress.Readers.Tar; using SharpCompress.Test.Mocks; +using SharpCompress.Writers.Tar; using Xunit; namespace SharpCompress.Test.Tar; @@ -379,6 +381,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() { 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() {