Merge remote-tracking branch 'origin/master' into adam/write-async

# Conflicts:
#	src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs
#	src/SharpCompress/Factories/TarFactory.cs
#	src/SharpCompress/Polyfills/StreamExtensions.cs
#	src/SharpCompress/Writers/IWriter.cs
This commit is contained in:
Adam Hathcock
2026-05-14 16:12:13 +01:00
134 changed files with 4378 additions and 4543 deletions

View File

@@ -22,20 +22,16 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
private bool _wroteCurrentEntry;
private readonly bool _disposeVolume;
internal AbstractReader(
ReaderOptions options,
ArchiveType archiveType,
bool disposeVolume = true
)
internal AbstractReader(ReaderOptions options, ArchiveType type, bool disposeVolume = true)
{
ArchiveType = archiveType;
Type = type;
_disposeVolume = disposeVolume;
Options = options;
}
internal ReaderOptions Options { get; }
public ArchiveType ArchiveType { get; }
public ArchiveType Type { get; }
/// <summary>
/// Current volume that the current entry resides in

View File

@@ -19,8 +19,8 @@ public partial class AceReader
/// <returns>An AceReader instance.</returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeAceReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeAceReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -31,19 +31,19 @@ public partial class AceReader
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -61,8 +61,8 @@ public partial class AceReader
ReaderOptions? options = null
)
{
streams.NotNull(nameof(streams));
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -84,6 +84,7 @@ public partial class AceReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -12,7 +12,7 @@ internal class SingleVolumeAceReader : AceReader
internal SingleVolumeAceReader(Stream stream, ReaderOptions options)
: base(options)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
_stream = stream;
}

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arc;
public partial class ArcReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ArcReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -24,8 +24,8 @@ public partial class ArcReader : AbstractReader<ArcEntry, ArcVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new ArcReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new ArcReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
protected override IEnumerable<ArcEntry> GetEntries(Stream stream)

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arj;
public partial class ArjReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ArjReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -31,8 +31,8 @@ public abstract partial class ArjReader : AbstractReader<ArjEntry, ArjVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeArjReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeArjReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -43,8 +43,8 @@ public abstract partial class ArjReader : AbstractReader<ArjEntry, ArjVolume>
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeArjReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeArjReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
protected abstract void ValidateArchive(ArjVolume archive);

View File

@@ -12,7 +12,7 @@ internal class SingleVolumeArjReader : ArjReader
internal SingleVolumeArjReader(Stream stream, ReaderOptions options)
: base(options)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
_stream = stream;
}

View File

@@ -10,14 +10,14 @@ public partial class GZipReader
#endif
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -49,12 +49,13 @@ public partial class GZipReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new GZipReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new GZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
}

View File

@@ -8,7 +8,7 @@ namespace SharpCompress.Readers;
public interface IAsyncReader : IAsyncDisposable
{
ArchiveType ArchiveType { get; }
ArchiveType Type { get; }
IEntry Entry { get; }

View File

@@ -6,7 +6,7 @@ namespace SharpCompress.Readers;
public interface IReader : IDisposable
{
ArchiveType ArchiveType { get; }
ArchiveType Type { get; }
IEntry Entry { get; }

View File

@@ -24,6 +24,6 @@ public interface IReaderFactory : Factories.IFactory
ValueTask<IAsyncReader> OpenAsyncReader(
Stream stream,
ReaderOptions? options,
CancellationToken cancellationToken
CancellationToken cancellationToken = default
);
}

View File

@@ -17,7 +17,7 @@ public interface IReaderOpenable
public static abstract IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null);
public static abstract ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);

View File

@@ -10,14 +10,14 @@ public partial class LzwReader
#endif
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -49,12 +49,13 @@ public partial class LzwReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new LzwReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new LzwReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
}

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Rar;
public partial class RarReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(

View File

@@ -49,7 +49,7 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
@@ -60,7 +60,7 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
public static IReader OpenReader(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
options ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfos.Select(x => x.OpenRead()), options);
}
@@ -72,8 +72,8 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeRarReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeRarReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -84,8 +84,8 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeRarReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeRarReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
protected override IEnumerable<RarReaderEntry> GetEntries(Stream stream)

View File

@@ -25,7 +25,11 @@ public static partial class ReaderFactory
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncReader(new FileInfo(filePath), options, cancellationToken);
return OpenAsyncReader(
new FileInfo(filePath),
options ?? ReaderOptions.ForFilePath,
cancellationToken
);
}
/// <summary>
@@ -52,7 +56,7 @@ public static partial class ReaderFactory
CancellationToken cancellationToken = default
)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(

View File

@@ -12,7 +12,7 @@ public static partial class ReaderFactory
public static IReader OpenReader(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenReader(new FileInfo(filePath), options);
return OpenReader(new FileInfo(filePath), options ?? ReaderOptions.ForFilePath);
}
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? options = null)
@@ -29,7 +29,7 @@ public static partial class ReaderFactory
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(

View File

@@ -24,9 +24,36 @@ namespace SharpCompress.Readers;
public sealed record ReaderOptions : IReaderOptions
{
/// <summary>
/// SharpCompress will keep the supplied streams open. Default is true.
/// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed.
/// As of v0.21, the library is documented to close streams by default; this option now defaults to false.
/// Set to true when passing caller-owned streams that should not be disposed.
/// </summary>
public bool LeaveStreamOpen { get; init; } = true;
/// <remarks>
/// <para>
/// <b>Default behavior (LeaveStreamOpen = false):</b>
/// When you open an archive from a file path (e.g., <c>GZipArchive.OpenArchive(filePath)</c>),
/// SharpCompress manages the stream lifetime and closes it on Dispose.
/// </para>
/// <para>
/// <b>Caller-provided streams (LeaveStreamOpen = true):</b>
/// When you pass a stream you created (FileStream, MemoryStream, NetworkStream, etc.),
/// set LeaveStreamOpen = true to prevent SharpCompress from disposing it.
/// Use <see cref="ForExternalStream"/> preset for convenience.
/// </para>
/// <para>
/// <b>Example:</b>
/// <code>
/// // File-based: stream managed by library
/// using var archive = GZipArchive.OpenArchive(filePath); // LeaveStreamOpen = false
///
/// // Caller-provided stream: caller manages lifetime
/// using var stream = File.OpenRead(filePath);
/// var options = new ReaderOptions { LeaveStreamOpen = true };
/// using var archive = GZipArchive.OpenArchive(stream, options);
/// </code>
/// </para>
/// </remarks>
public bool LeaveStreamOpen { get; init; } = false;
/// <summary>
/// Encoding to use for archive entry names.
@@ -124,35 +151,34 @@ public sealed record ReaderOptions : IReaderOptions
/// <summary>
/// Gets ReaderOptions configured for caller-provided streams.
/// </summary>
public static ReaderOptions ForExternalStream => new() { LeaveStreamOpen = true };
internal static ReaderOptions Default => new();
public static ReaderOptions ForExternalStream => Default.WithLeaveStreamOpen(true);
/// <summary>
/// Gets ReaderOptions configured for file-based overloads that open their own stream.
/// </summary>
public static ReaderOptions ForFilePath => new() { LeaveStreamOpen = false };
public static ReaderOptions ForFilePath => Default;
/// <summary>
/// Creates ReaderOptions for reading encrypted archives.
/// </summary>
/// <param name="password">The password for encrypted archives.</param>
public static ReaderOptions ForEncryptedArchive(string? password = null) =>
new ReaderOptions().WithPassword(password);
Default.WithPassword(password);
/// <summary>
/// Creates ReaderOptions for archives with custom character encoding.
/// </summary>
/// <param name="encoding">The encoding for archive entry names.</param>
public static ReaderOptions ForEncoding(IArchiveEncoding encoding) =>
new ReaderOptions().WithArchiveEncoding(encoding);
Default.WithArchiveEncoding(encoding);
/// <summary>
/// Creates ReaderOptions for self-extracting archives that require header search.
/// </summary>
public static ReaderOptions ForSelfExtractingArchive(string? password = null) =>
new ReaderOptions()
.WithLookForHeader(true)
.WithPassword(password)
.WithRewindableBufferSize(1_048_576); // 1MB for SFX archives
Default.WithLookForHeader(true).WithPassword(password).WithRewindableBufferSize(1_048_576); // 1MB for SFX archives
// Note: Parameterized constructors have been removed.
// Use fluent With*() helpers or object initializers instead:

View File

@@ -72,13 +72,13 @@ public partial class TarReader
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
path.NotNullOrEmpty(nameof(path));
return OpenAsyncReader(new FileInfo(path), readerOptions, cancellationToken);
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncReader(new FileInfo(filePath), readerOptions, cancellationToken);
}
public static async ValueTask<IAsyncReader> OpenAsyncReader(
@@ -89,7 +89,7 @@ public partial class TarReader
{
cancellationToken.ThrowIfCancellationRequested();
stream.NotNull(nameof(stream));
readerOptions ??= new ReaderOptions();
readerOptions ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(
stream,
bufferSize: Math.Max(
@@ -143,7 +143,7 @@ public partial class TarReader
CancellationToken cancellationToken = default
)
{
readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
var stream = fileInfo.OpenAsyncReadStream(cancellationToken);
return await OpenAsyncReader(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
@@ -158,7 +158,7 @@ public partial class TarReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
@@ -170,8 +170,8 @@ public partial class TarReader
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
readerOptions ??= new ReaderOptions();
stream.RequireReadable();
readerOptions ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(
stream,
bufferSize: Math.Max(

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Zip;
public partial class ZipReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ZipReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -47,8 +47,8 @@ public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new ZipReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new ZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
public static IReader OpenReader(
@@ -57,8 +57,8 @@ public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>
IEnumerable<ZipEntry> entries
)
{
stream.NotNull(nameof(stream));
return new ZipReader(stream, options ?? new ReaderOptions(), entries);
stream.RequireReadable();
return new ZipReader(stream, options ?? ReaderOptions.ForExternalStream, entries);
}
#endregion Open