diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 97f37dcc..9e028ef5 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,11 +3,11 @@
"isRoot": true,
"tools": {
"csharpier": {
- "version": "1.2.6",
+ "version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
-}
\ No newline at end of file
+}
diff --git a/docs/API.md b/docs/API.md
index 6e6efce1..19edd6ed 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -92,7 +92,8 @@ using (var archive = GZipArchive.CreateArchive())
// With fluent options (preferred)
var options = WriterOptions.ForZip()
.WithCompressionLevel(9)
- .WithLeaveStreamOpen(false);
+ .WithLeaveStreamOpen(false)
+ .WithBufferSize(131072);
using (var archive = ZipArchive.CreateArchive())
{
archive.SaveTo("output.zip", options);
@@ -102,10 +103,13 @@ using (var archive = ZipArchive.CreateArchive())
var options2 = new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9,
- LeaveStreamOpen = false
+ LeaveStreamOpen = false,
+ BufferSize = 131072
};
```
+`WriterOptions.BufferSize` controls stream copy buffers used while writing archive entries. If it is not set, SharpCompress falls back to `Constants.BufferSize`.
+
---
## Archive API Methods
@@ -315,6 +319,9 @@ var safeOptions = ExtractionOptions.SafeExtract; // No overwrite
var flatOptions = ExtractionOptions.FlatExtract; // No directory structure
var metadataOptions = ExtractionOptions.PreserveMetadata; // Keep timestamps and attributes
+// Tune extraction copy buffering
+var extractionOptions = new ExtractionOptions { BufferSize = 131072 };
+
// Factory defaults:
// - file path / FileInfo overloads use LeaveStreamOpen = false
// - stream overloads use LeaveStreamOpen = true
@@ -334,6 +341,13 @@ var options = new ReaderOptions
BufferSize = 81920,
RewindableBufferSize = 1_048_576,
};
+
+var extractionOptions = new ExtractionOptions
+{
+ ExtractFullPath = true,
+ Overwrite = true,
+ BufferSize = 131072,
+};
```
### WriterOptions
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 3c91431b..72356614 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -100,7 +100,12 @@ using (var archive = RarArchive.OpenArchive("Test.rar", ReaderOptions.ForFilePat
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
archive.WriteToDirectory(
@"D:\temp",
- new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
+ new ExtractionOptions
+ {
+ ExtractFullPath = true,
+ Overwrite = true,
+ BufferSize = 131072,
+ }
);
}
```
diff --git a/global.json b/global.json
index 50baaf1a..a6dc747f 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.300",
- "rollForward": "latestPatch"
+ "rollForward": "disable"
}
}
diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
index f89ba345..0de4b21c 100644
--- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
@@ -17,7 +17,14 @@ public static class IArchiveEntryExtensions
///
/// The stream to write the entry content to.
/// Optional progress reporter for tracking extraction progress.
- public void WriteTo(Stream streamToWriteTo, IProgress? progress = null)
+ public void WriteTo(Stream streamToWriteTo, IProgress? progress = null) =>
+ archiveEntry.WriteTo(streamToWriteTo, null, progress);
+
+ private void WriteTo(
+ Stream streamToWriteTo,
+ int? bufferSize,
+ IProgress? progress = null
+ )
{
if (archiveEntry.IsDirectory)
{
@@ -26,7 +33,7 @@ public static class IArchiveEntryExtensions
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
- sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
+ sourceStream.CopyTo(streamToWriteTo, bufferSize ?? Constants.BufferSize);
}
///
@@ -40,6 +47,18 @@ public static class IArchiveEntryExtensions
IProgress? progress = null,
CancellationToken cancellationToken = default
)
+ {
+ await archiveEntry
+ .WriteToAsync(streamToWriteTo, Constants.BufferSize, progress, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ private async ValueTask WriteToAsync(
+ Stream streamToWriteTo,
+ int? bufferSize,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
{
if (archiveEntry.IsDirectory)
{
@@ -57,7 +76,7 @@ public static class IArchiveEntryExtensions
#endif
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
await sourceStream
- .CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken)
+ .CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
}
}
@@ -133,16 +152,18 @@ public static class IArchiveEntryExtensions
///
/// Extract to specific file
///
- public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) =>
+ public void WriteToFile(string destinationFileName, ExtractionOptions? options = null)
+ {
entry.WriteEntryToFile(
destinationFileName,
options,
(x, fm) =>
{
- using var fs = File.Open(destinationFileName, fm);
- entry.WriteTo(fs);
+ using var fs = File.Open(x, fm);
+ entry.WriteTo(fs, options?.BufferSize ?? Constants.BufferSize);
}
);
+ }
///
/// Extract to specific file asynchronously
@@ -158,8 +179,10 @@ public static class IArchiveEntryExtensions
options,
async (x, fm, ct) =>
{
- using var fs = File.Open(destinationFileName, fm);
- await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
+ using var fs = File.Open(x, fm);
+ await entry
+ .WriteToAsync(fs, options?.BufferSize, null, ct)
+ .ConfigureAwait(false);
},
cancellationToken
)
diff --git a/src/SharpCompress/Common/Constants.cs b/src/SharpCompress/Common/Constants.cs
index edd81e08..44909ef9 100644
--- a/src/SharpCompress/Common/Constants.cs
+++ b/src/SharpCompress/Common/Constants.cs
@@ -8,6 +8,7 @@ public static class Constants
/// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes.
/// This can be modified globally at runtime.
///
+ // TODO: Revisit remaining non-extraction usages after extraction buffering moves to ExtractionOptions.
public static int BufferSize { get; set; } = 81920;
///
diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs
index 8a3b4859..b99658f8 100644
--- a/src/SharpCompress/Common/ExtractionOptions.cs
+++ b/src/SharpCompress/Common/ExtractionOptions.cs
@@ -38,6 +38,11 @@ public sealed record ExtractionOptions : IExtractionOptions
///
public bool PreserveAttributes { get; set; }
+ ///
+ /// Buffer size for extraction stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Delegate for writing symbolic links to disk.
/// The first parameter is the source path (where the symlink is created).
diff --git a/src/SharpCompress/Common/Options/IExtractionOptions.cs b/src/SharpCompress/Common/Options/IExtractionOptions.cs
index 8aac6a65..767bfb93 100644
--- a/src/SharpCompress/Common/Options/IExtractionOptions.cs
+++ b/src/SharpCompress/Common/Options/IExtractionOptions.cs
@@ -30,6 +30,11 @@ public interface IExtractionOptions
///
bool PreserveAttributes { get; set; }
+ ///
+ /// Buffer size for extraction stream copy operations.
+ ///
+ int BufferSize { get; set; }
+
///
/// Delegate for writing symbolic links to disk.
/// The first parameter is the source path (where the symlink is created).
diff --git a/src/SharpCompress/Common/Options/IWriterOptions.cs b/src/SharpCompress/Common/Options/IWriterOptions.cs
index a7a1d719..37fbb21e 100644
--- a/src/SharpCompress/Common/Options/IWriterOptions.cs
+++ b/src/SharpCompress/Common/Options/IWriterOptions.cs
@@ -19,6 +19,11 @@ public interface IWriterOptions : IStreamOptions, IEncodingOptions, IProgressOpt
///
int CompressionLevel { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ int BufferSize { get; set; }
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom providers, such as
diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs
index 3c3e01df..8628c0c2 100644
--- a/src/SharpCompress/Readers/AbstractReader.Async.cs
+++ b/src/SharpCompress/Readers/AbstractReader.Async.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
-using SharpCompress.IO;
namespace SharpCompress.Readers;
@@ -138,19 +137,19 @@ public abstract partial class AbstractReader
_wroteCurrentEntry = true;
}
- internal async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
+ private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
{
#if LEGACY_DOTNET
using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
var sourceStream = WrapWithProgress(s, Entry);
await sourceStream
- .CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
+ .CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
.ConfigureAwait(false);
#else
await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
var sourceStream = WrapWithProgress(s, Entry);
await sourceStream
- .CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
+ .CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
.ConfigureAwait(false);
#endif
}
@@ -187,9 +186,7 @@ public abstract partial class AbstractReader
///
/// Moves the current async enumerator to the next entry.
///
- internal virtual ValueTask NextEntryForCurrentStreamAsync(
- CancellationToken cancellationToken
- )
+ private ValueTask NextEntryForCurrentStreamAsync(CancellationToken cancellationToken)
{
if (_entriesForCurrentReadStreamAsync is not null)
{
@@ -202,6 +199,9 @@ public abstract partial class AbstractReader
// Async iterator method
protected virtual async IAsyncEnumerable GetEntriesAsync(Stream stream)
{
+#pragma warning disable VSTHRD111
+ await Task.CompletedTask;
+#pragma warning restore VSTHRD111
foreach (var entry in GetEntries(stream))
{
yield return entry;
diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs
index 52410e14..b6a5d32c 100644
--- a/src/SharpCompress/Readers/AbstractReader.cs
+++ b/src/SharpCompress/Readers/AbstractReader.cs
@@ -179,7 +179,10 @@ public abstract partial class AbstractReader : IReader, IAsyncR
s.SkipEntry();
}
- public void WriteEntryTo(Stream writableStream)
+ public void WriteEntryTo(Stream writableStream) =>
+ WriteEntryTo(writableStream, Options.BufferSize);
+
+ private void WriteEntryTo(Stream writableStream, int bufferSize)
{
if (_wroteCurrentEntry)
{
@@ -194,15 +197,17 @@ public abstract partial class AbstractReader : IReader, IAsyncR
);
}
- Write(writableStream);
+ Write(writableStream, bufferSize);
_wroteCurrentEntry = true;
}
- internal void Write(Stream writeStream)
+ internal void Write(Stream writeStream) => Write(writeStream, Options.BufferSize);
+
+ internal void Write(Stream writeStream, int bufferSize)
{
using Stream s = OpenEntryStream();
var sourceStream = WrapWithProgress(s, Entry);
- sourceStream.CopyTo(writeStream, Constants.BufferSize);
+ sourceStream.CopyTo(writeStream, bufferSize);
}
private Stream WrapWithProgress(Stream source, Entry entry)
diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
index 3c1d0408..a982982c 100644
--- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
+++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs
@@ -1,7 +1,9 @@
+using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
+using SharpCompress.IO;
namespace SharpCompress.Readers;
@@ -42,7 +44,8 @@ public static class IAsyncReaderExtensions
async (x, fm, ct) =>
{
using var fs = File.Open(x, fm);
- await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
+ await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
+ .ConfigureAwait(false);
},
cancellationToken
)
@@ -77,7 +80,8 @@ public static class IAsyncReaderExtensions
async (x, fm, ct) =>
{
using var fs = File.Open(x, fm);
- await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
+ await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
+ .ConfigureAwait(false);
},
cancellationToken
)
@@ -92,4 +96,58 @@ public static class IAsyncReaderExtensions
.WriteEntryToAsync(destinationFileInfo.FullName, options, cancellationToken)
.ConfigureAwait(false);
}
+
+ private static async ValueTask CopyEntryToAsync(
+ IAsyncReader reader,
+ Stream writableStream,
+ int? bufferSize,
+ CancellationToken cancellationToken
+ )
+ {
+#if LEGACY_DOTNET
+ using var entryStream = await reader
+ .OpenEntryStreamAsync(cancellationToken)
+ .ConfigureAwait(false);
+#else
+ await using var entryStream = await reader
+ .OpenEntryStreamAsync(cancellationToken)
+ .ConfigureAwait(false);
+#endif
+ var sourceStream = WrapWithProgress(entryStream, reader.Entry);
+ await sourceStream
+ .CopyToAsync(writableStream, bufferSize ?? Constants.BufferSize, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ private static Stream WrapWithProgress(Stream source, IEntry entry)
+ {
+ var progress = entry.Options.Progress;
+ if (progress is null)
+ {
+ return source;
+ }
+
+ var entryPath = entry.Key ?? string.Empty;
+ var totalBytes = GetEntrySizeSafe(entry);
+ return new ProgressReportingStream(
+ source,
+ progress,
+ entryPath,
+ totalBytes,
+ leaveOpen: true
+ );
+ }
+
+ private static long? GetEntrySizeSafe(IEntry entry)
+ {
+ try
+ {
+ var size = entry.Size;
+ return size >= 0 ? size : null;
+ }
+ catch (NotImplementedException)
+ {
+ return null;
+ }
+ }
}
diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs
index 74c708a5..93c391d2 100644
--- a/src/SharpCompress/Readers/IReaderExtensions.cs
+++ b/src/SharpCompress/Readers/IReaderExtensions.cs
@@ -1,5 +1,7 @@
+using System;
using System.IO;
using SharpCompress.Common;
+using SharpCompress.IO;
namespace SharpCompress.Readers;
@@ -58,9 +60,48 @@ public static class IReaderExtensions
options,
(x, fm) =>
{
- using var fs = File.Open(destinationFileName, fm);
- reader.WriteEntryTo(fs);
+ using var fs = File.Open(x, fm);
+ CopyEntryTo(reader, fs, options?.BufferSize ?? Constants.BufferSize);
}
);
}
+
+ private static void CopyEntryTo(IReader reader, Stream writableStream, int bufferSize)
+ {
+ using var entryStream = reader.OpenEntryStream();
+ var sourceStream = WrapWithProgress(entryStream, reader.Entry);
+ sourceStream.CopyTo(writableStream, bufferSize);
+ }
+
+ private static Stream WrapWithProgress(Stream source, IEntry entry)
+ {
+ var progress = entry.Options.Progress;
+ if (progress is null)
+ {
+ return source;
+ }
+
+ var entryPath = entry.Key ?? string.Empty;
+ var totalBytes = GetEntrySizeSafe(entry);
+ return new ProgressReportingStream(
+ source,
+ progress,
+ entryPath,
+ totalBytes,
+ leaveOpen: true
+ );
+ }
+
+ private static long? GetEntrySizeSafe(IEntry entry)
+ {
+ try
+ {
+ var size = entry.Size;
+ return size >= 0 ? size : null;
+ }
+ catch (NotImplementedException)
+ {
+ return null;
+ }
+ }
}
diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs
index 1c11ccc6..df323d69 100644
--- a/src/SharpCompress/Utility.Async.cs
+++ b/src/SharpCompress/Utility.Async.cs
@@ -65,13 +65,14 @@ internal static partial class Utility
public async ValueTask TransferToAsync(
Stream destination,
long maxLength,
+ int? bufferSize = null,
CancellationToken cancellationToken = default
)
{
// Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync
using var limitedStream = new IO.ReadOnlySubStream(source, maxLength);
await limitedStream
- .CopyToAsync(destination, Constants.BufferSize, cancellationToken)
+ .CopyToAsync(destination, bufferSize ?? Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
return limitedStream.Position;
}
diff --git a/src/SharpCompress/Utility.cs b/src/SharpCompress/Utility.cs
index 017bc965..c03c3643 100644
--- a/src/SharpCompress/Utility.cs
+++ b/src/SharpCompress/Utility.cs
@@ -150,11 +150,11 @@ internal static partial class Utility
extension(Stream source)
{
- public long TransferTo(Stream destination, long maxLength)
+ public long TransferTo(Stream destination, long maxLength, int? bufferSize)
{
// Use ReadOnlySubStream to limit reading and leverage framework's CopyTo
using var limitedStream = new IO.ReadOnlySubStream(source, maxLength);
- limitedStream.CopyTo(destination, Constants.BufferSize);
+ limitedStream.CopyTo(destination, bufferSize ?? Constants.BufferSize);
return limitedStream.Position;
}
diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.cs b/src/SharpCompress/Writers/GZip/GZipWriter.cs
index 29c9425c..3c62ff2d 100644
--- a/src/SharpCompress/Writers/GZip/GZipWriter.cs
+++ b/src/SharpCompress/Writers/GZip/GZipWriter.cs
@@ -86,7 +86,7 @@ public sealed partial class GZipWriter : AbstractWriter
}
var progressStream = WrapWithProgress(source, filename);
- progressStream.CopyTo(OutputStream.NotNull(), Constants.BufferSize);
+ progressStream.CopyTo(OutputStream.NotNull(), WriterOptions.BufferSize);
_wroteToStream = true;
}
diff --git a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs
index 42da70e2..ca1b6a9d 100644
--- a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs
+++ b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs
@@ -68,6 +68,11 @@ public sealed record GZipWriterOptions : IWriterOptions
///
public IProgress? Progress { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom implementations, such as
@@ -115,6 +120,7 @@ public sealed record GZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
@@ -128,6 +134,7 @@ public sealed record GZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
}
diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
index 86fa09ff..6ed26a42 100644
--- a/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
+++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriterOptions.cs
@@ -57,6 +57,11 @@ public sealed record SevenZipWriterOptions : IWriterOptions
///
public IProgress? Progress { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom implementations.
@@ -103,6 +108,7 @@ public sealed record SevenZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
@@ -117,6 +123,7 @@ public sealed record SevenZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs
index 7f4b3886..9244608f 100644
--- a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs
+++ b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs
@@ -104,7 +104,12 @@ public partial class TarWriter
await header.WriteAsync(OutputStream.NotNull(), cancellationToken).ConfigureAwait(false);
var progressStream = WrapWithProgress(source, filename);
var written = await progressStream
- .TransferToAsync(OutputStream.NotNull(), realSize, cancellationToken)
+ .TransferToAsync(
+ OutputStream.NotNull(),
+ realSize,
+ WriterOptions.BufferSize,
+ cancellationToken
+ )
.ConfigureAwait(false);
await PadTo512Async(written, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs
index 4ed20335..8136cc58 100644
--- a/src/SharpCompress/Writers/Tar/TarWriter.cs
+++ b/src/SharpCompress/Writers/Tar/TarWriter.cs
@@ -133,7 +133,11 @@ public partial class TarWriter : AbstractWriter
header.Size = realSize;
header.Write(OutputStream.NotNull());
var progressStream = WrapWithProgress(source, filename);
- size = progressStream.TransferTo(OutputStream.NotNull(), realSize);
+ size = progressStream.TransferTo(
+ OutputStream.NotNull(),
+ realSize,
+ WriterOptions.BufferSize
+ );
PadTo512(size.Value);
}
diff --git a/src/SharpCompress/Writers/Tar/TarWriterOptions.cs b/src/SharpCompress/Writers/Tar/TarWriterOptions.cs
index 004d4643..d7aaf22b 100755
--- a/src/SharpCompress/Writers/Tar/TarWriterOptions.cs
+++ b/src/SharpCompress/Writers/Tar/TarWriterOptions.cs
@@ -44,6 +44,11 @@ public sealed record TarWriterOptions : IWriterOptions
///
public IProgress? Progress { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom implementations.
@@ -104,6 +109,7 @@ public sealed record TarWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
@@ -118,6 +124,7 @@ public sealed record TarWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
diff --git a/src/SharpCompress/Writers/WriterOptions.cs b/src/SharpCompress/Writers/WriterOptions.cs
index e4dd4ac0..d70ea6aa 100644
--- a/src/SharpCompress/Writers/WriterOptions.cs
+++ b/src/SharpCompress/Writers/WriterOptions.cs
@@ -56,6 +56,11 @@ public sealed record WriterOptions : IWriterOptions
///
public IProgress? Progress { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom implementations, such as
diff --git a/src/SharpCompress/Writers/WriterOptionsExtensions.cs b/src/SharpCompress/Writers/WriterOptionsExtensions.cs
index 964d9187..2aca567a 100644
--- a/src/SharpCompress/Writers/WriterOptionsExtensions.cs
+++ b/src/SharpCompress/Writers/WriterOptionsExtensions.cs
@@ -53,6 +53,15 @@ public static class WriterOptionsExtensions
),
};
+ ///
+ /// Creates a copy with the specified buffer size.
+ ///
+ public static WriterOptions WithBufferSize(this WriterOptions options, int bufferSize) =>
+ options with
+ {
+ BufferSize = bufferSize,
+ };
+
///
/// Creates a copy with the specified compression level.
///
diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs
index 51695341..128cb9d7 100644
--- a/src/SharpCompress/Writers/Zip/ZipWriter.cs
+++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs
@@ -16,7 +16,6 @@ using SharpCompress.Compressors.PPMd;
using SharpCompress.Compressors.ZStandard;
using SharpCompress.IO;
using SharpCompress.Providers;
-using Constants = SharpCompress.Common.Constants;
namespace SharpCompress.Writers.Zip;
@@ -89,7 +88,7 @@ public partial class ZipWriter : AbstractWriter
{
using var output = WriteToStream(entryPath, zipWriterEntryOptions);
var progressStream = WrapWithProgress(source, entryPath);
- progressStream.CopyTo(output, Constants.BufferSize);
+ progressStream.CopyTo(output, WriterOptions.BufferSize);
}
public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
diff --git a/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs
index e3bf2834..73c92ee6 100644
--- a/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs
+++ b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs
@@ -61,6 +61,11 @@ public sealed record ZipWriterOptions : IWriterOptions
///
public IProgress? Progress { get; set; }
+ ///
+ /// Buffer size for writer stream copy operations.
+ ///
+ public int BufferSize { get; set; } = Constants.BufferSize;
+
///
/// Registry of compression providers.
/// Defaults to but can be replaced with custom implementations.
@@ -128,6 +133,7 @@ public sealed record ZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
@@ -141,6 +147,7 @@ public sealed record ZipWriterOptions : IWriterOptions
LeaveStreamOpen = options.LeaveStreamOpen;
ArchiveEncoding = options.ArchiveEncoding;
Progress = options.Progress;
+ BufferSize = options.BufferSize;
Providers = options.Providers;
}
diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs
index 11376f75..8b08b265 100644
--- a/tests/SharpCompress.Test/OptionsUsabilityTests.cs
+++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs
@@ -1,8 +1,10 @@
+#if !LEGACY_DOTNET
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
@@ -10,6 +12,7 @@ using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using SharpCompress.Writers;
using SharpCompress.Writers.GZip;
+using SharpCompress.Writers.Tar;
using SharpCompress.Writers.Zip;
using Xunit;
@@ -132,11 +135,16 @@ public class OptionsUsabilityTests : TestBase
[Fact]
public void WriterOptions_Fluent_Methods_Modify_Correctly()
{
- var options = WriterOptions.ForZip().WithLeaveStreamOpen(false).WithCompressionLevel(9);
+ var options = WriterOptions
+ .ForZip()
+ .WithLeaveStreamOpen(false)
+ .WithCompressionLevel(9)
+ .WithBufferSize(65536);
Assert.Equal(CompressionType.Deflate, options.CompressionType);
Assert.Equal(9, options.CompressionLevel);
Assert.False(options.LeaveStreamOpen);
+ Assert.Equal(65536, options.BufferSize);
}
[Fact]
@@ -160,6 +168,73 @@ public class OptionsUsabilityTests : TestBase
Assert.Equal(factoryApproach.LeaveStreamOpen, constructorApproach.LeaveStreamOpen);
}
+ [Fact]
+ public void WriterOptions_Default_BufferSize_Uses_Constants_BufferSize()
+ {
+ Assert.Equal(Constants.BufferSize, WriterOptions.ForZip().BufferSize);
+ Assert.Equal(
+ Constants.BufferSize,
+ new ZipWriterOptions(CompressionType.Deflate).BufferSize
+ );
+ Assert.Equal(
+ Constants.BufferSize,
+ new TarWriterOptions(CompressionType.None, true).BufferSize
+ );
+ Assert.Equal(Constants.BufferSize, new GZipWriterOptions().BufferSize);
+ }
+
+ [Fact]
+ public void Format_WriterOptions_Copy_BufferSize()
+ {
+ var options = WriterOptions.ForZip().WithBufferSize(12345);
+
+ Assert.Equal(12345, new ZipWriterOptions(options).BufferSize);
+ Assert.Equal(12345, new TarWriterOptions(options).BufferSize);
+ Assert.Equal(12345, new GZipWriterOptions(options).BufferSize);
+ }
+
+ [Fact]
+ public void ZipWriter_Uses_WriterOptions_BufferSize()
+ {
+ using var source = new TrackingReadStream(new byte[100]);
+ using var destination = new MemoryStream();
+ using var writer = new ZipWriter(
+ destination,
+ new ZipWriterOptions(CompressionType.None) { BufferSize = 17 }
+ );
+
+ writer.Write("buffer-size.txt", source, DateTime.Now);
+
+ Assert.Equal(17, source.CopyBufferSize);
+ }
+
+ [Fact]
+ public void GZipWriter_Uses_WriterOptions_BufferSize()
+ {
+ using var source = new TrackingReadStream(new byte[100]);
+ using var destination = new MemoryStream();
+ using var writer = new GZipWriter(destination, new GZipWriterOptions { BufferSize = 19 });
+
+ writer.Write("buffer-size.txt", source, DateTime.Now);
+
+ Assert.Equal(19, source.CopyBufferSize);
+ }
+
+ [Fact]
+ public void TarWriter_Uses_WriterOptions_BufferSize_ForTransfer()
+ {
+ using var source = new MemoryStream(new byte[100]);
+ using var destination = new MemoryStream();
+ using var writer = new TarWriter(
+ destination,
+ new TarWriterOptions(CompressionType.None, true) { BufferSize = 0 }
+ );
+
+ Assert.Throws(() =>
+ writer.Write("buffer-size.txt", source, DateTime.Now)
+ );
+ }
+
[Fact]
public void ReaderOptions_Fluent_Methods_Modify_Correctly()
{
@@ -229,6 +304,30 @@ public class OptionsUsabilityTests : TestBase
var preserveMetadata = ExtractionOptions.PreserveMetadata;
Assert.True(preserveMetadata.PreserveFileTime);
Assert.True(preserveMetadata.PreserveAttributes);
+
+ Assert.Equal(Constants.BufferSize, new ExtractionOptions().BufferSize);
+ }
+
+ [Fact]
+ public void Reader_WriteEntryToFile_Uses_ExtractionOptions_BufferSize()
+ {
+ using var reader = new TrackingReader();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size.txt");
+
+ reader.WriteEntryToFile(destination, new ExtractionOptions { BufferSize = 11 });
+
+ Assert.Equal(11, reader.EntryStreamCopyBufferSize);
+ }
+
+ [Fact]
+ public async Task Reader_WriteEntryToFileAsync_Uses_ExtractionOptions_BufferSize()
+ {
+ await using var reader = new TrackingReader();
+ var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size-async.txt");
+
+ await reader.WriteEntryToFileAsync(destination, new ExtractionOptions { BufferSize = 13 });
+
+ Assert.Equal(13, reader.EntryStreamCopyBufferSize);
}
[Fact]
@@ -321,4 +420,128 @@ public class OptionsUsabilityTests : TestBase
Assert.Null(noPassword.Password);
Assert.Equal(1_048_576, noPassword.RewindableBufferSize);
}
+
+ private sealed class TestArchiveEntry(Stream source) : IArchiveEntry
+ {
+ public CompressionType CompressionType => CompressionType.None;
+ public DateTime? ArchivedTime => null;
+ public long CompressedSize => source.Length;
+ public long Crc => 0;
+ public DateTime? CreatedTime => null;
+ public string? Key => "buffer-size.txt";
+ public string? LinkTarget => null;
+ public bool IsDirectory => false;
+ public bool IsEncrypted => false;
+ public bool IsSplitAfter => false;
+ public bool IsSolid => false;
+ public int VolumeIndexFirst => 0;
+ public int VolumeIndexLast => 0;
+ public DateTime? LastAccessedTime => null;
+ public DateTime? LastModifiedTime => null;
+ public long Size => source.Length;
+ public int? Attrib => null;
+ public SharpCompress.Common.Options.IReaderOptions Options =>
+ ReaderOptions.ForExternalStream;
+ public bool IsComplete => true;
+ public IArchive Archive => throw new NotSupportedException();
+
+ public Stream OpenEntryStream()
+ {
+ source.Position = 0;
+ return source;
+ }
+
+ public ValueTask OpenEntryStreamAsync(CancellationToken cancellationToken = default)
+ {
+ source.Position = 0;
+ return new ValueTask(source);
+ }
+ }
+
+ private sealed class TrackingReadStream(byte[] data) : MemoryStream(data)
+ {
+ public int? CopyBufferSize { get; private set; }
+
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ CopyBufferSize = bufferSize;
+ base.CopyTo(destination, bufferSize);
+ }
+
+ public override Task CopyToAsync(
+ Stream destination,
+ int bufferSize,
+ CancellationToken cancellationToken
+ )
+ {
+ CopyBufferSize = bufferSize;
+ return base.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+ }
+
+ private sealed class TrackingReader : IReader, IAsyncReader
+ {
+ public ArchiveType Type => ArchiveType.Zip;
+ public MemoryStream Source { get; } = new(new byte[100]);
+ public IEntry Entry => new TestArchiveEntry(Source);
+ public bool Cancelled => false;
+ public int? EntryStreamCopyBufferSize { get; private set; }
+
+ public void Dispose() { }
+
+ public ValueTask DisposeAsync() => default;
+
+ public void WriteEntryTo(Stream writableStream) => throw new NotSupportedException();
+
+ public ValueTask WriteEntryToAsync(
+ Stream writableStream,
+ CancellationToken cancellationToken = default
+ ) => throw new NotSupportedException();
+
+ public void Cancel() { }
+
+ public bool MoveToNextEntry() => false;
+
+ public ValueTask MoveToNextEntryAsync(
+ CancellationToken cancellationToken = default
+ ) => new(false);
+
+ public EntryStream OpenEntryStream()
+ {
+ Source.Position = 0;
+ return new TrackingEntryStream(
+ this,
+ Source,
+ bufferSize => EntryStreamCopyBufferSize = bufferSize
+ );
+ }
+
+ public ValueTask OpenEntryStreamAsync(
+ CancellationToken cancellationToken = default
+ ) => new(OpenEntryStream());
+ }
+
+ private sealed class TrackingEntryStream(
+ IReader reader,
+ Stream stream,
+ Action copyBufferSize
+ ) : EntryStream(reader, stream)
+ {
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ copyBufferSize(bufferSize);
+ base.CopyTo(destination, bufferSize);
+ }
+
+ public override Task CopyToAsync(
+ Stream destination,
+ int bufferSize,
+ CancellationToken cancellationToken
+ )
+ {
+ copyBufferSize(bufferSize);
+ return base.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+ }
}
+#endif
diff --git a/tests/SharpCompress.Test/UtilityTests.cs b/tests/SharpCompress.Test/UtilityTests.cs
index 20d46e6b..4dbe2f83 100644
--- a/tests/SharpCompress.Test/UtilityTests.cs
+++ b/tests/SharpCompress.Test/UtilityTests.cs
@@ -579,7 +579,7 @@ public class UtilityTests
using var source = new MemoryStream(sourceData);
using var destination = new MemoryStream();
- var transferred = source.TransferTo(destination, 5);
+ var transferred = source.TransferTo(destination, 5, null);
Assert.Equal(5, transferred);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, destination.ToArray());
@@ -592,7 +592,7 @@ public class UtilityTests
using var source = new MemoryStream(sourceData);
using var destination = new MemoryStream();
- var transferred = source.TransferTo(destination, 100);
+ var transferred = source.TransferTo(destination, 100, null);
Assert.Equal(3, transferred);
Assert.Equal(sourceData, destination.ToArray());
@@ -604,7 +604,7 @@ public class UtilityTests
using var source = new MemoryStream();
using var destination = new MemoryStream();
- var transferred = source.TransferTo(destination, 100);
+ var transferred = source.TransferTo(destination, 100, null);
Assert.Equal(0, transferred);
Assert.Empty(destination.ToArray());