mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-24 07:54:39 +00:00
Merge remote-tracking branch 'origin/master' into adam/lzma-allocations
# Conflicts: # SharpCompress.sln
This commit is contained in:
27
.github/copilot-instructions.md
vendored
Normal file
27
.github/copilot-instructions.md
vendored
Normal 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
12
.github/hooks/rtk-rewrite.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "rtk hook copilot",
|
||||
"cwd": ".",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2
.github/workflows/nuget-release.yml
vendored
2
.github/workflows/nuget-release.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/performance-benchmarks.yml
vendored
2
.github/workflows/performance-benchmarks.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
"version": "10.0.300",
|
||||
"rollForward": "latestPatch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user