Merge remote-tracking branch 'origin/master' into adam/lzma-allocations

# Conflicts:
#	SharpCompress.sln
This commit is contained in:
Adam Hathcock
2026-06-01 13:22:20 +01:00
22 changed files with 511 additions and 64 deletions

27
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,27 @@
<!-- rtk-instructions v2 -->
# 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 <cmd> # Run raw (no filtering) but track usage
```
<!-- /rtk-instructions -->

12
.github/hooks/rtk-rewrite.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "rtk hook copilot",
"cwd": ".",
"timeout": 5
}
]
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
"version": "10.0.300",
"rollForward": "latestPatch"
}
}

View File

@@ -122,6 +122,14 @@ public abstract partial class AbstractWritableArchive<TEntry, TVolume, TOptions>
.ConfigureAwait(false);
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
newEntries.Cast<Entry>().ForEach(x => x.Close());
removedEntries.Cast<Entry>().ForEach(x => x.Close());
modifiedEntries.Cast<Entry>().ForEach(x => x.Close());
}
protected abstract ValueTask SaveToAsync(
Stream stream,
TOptions options,

View File

@@ -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
{

View File

@@ -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<Stream?>(
new TarReadOnlySubStream(_seekableStream, Header.Size, useSyncOverAsync)
);
return new ValueTask<Stream?>(new TarReadOnlySubStream(_seekableStream, Header.Size));
}
return new ValueTask<Stream?>(Header.PackedStream.NotNull());
}

View File

@@ -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:

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>

View File

@@ -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);
}
}

View File

@@ -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<ProgressReport>();
var progress = new SynchronousProgress<ProgressReport>(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<T> : IProgress<T>
{
private readonly Action<T> _handler;
public SynchronousProgress(Action<T> handler) => _handler = handler;
public void Report(T value) => _handler(value);
}
}

View File

@@ -9,6 +9,8 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' ">
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>

View File

@@ -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<string?>();
var openEntryStreams = new List<Stream>();
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
);
}
}

View File

@@ -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<string?>();
var openEntryStreams = new List<Stream>();
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()
{

View File

@@ -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<string?>();
var openEntryStreams = new List<Stream>();
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()
{

View File

@@ -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<IncompleteArchiveException>(() => 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<string?>();
var openEntryStreams = new List<Stream>();
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()
{

View File

@@ -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()
{