From f34ed8bd945f86fa5235192013763e48abd00f72 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 11:22:09 +0000 Subject: [PATCH 01/74] first commit for conslidating publci api --- .../Archives/ArchiveFactory.Async.cs | 30 +++---- src/SharpCompress/Archives/ArchiveFactory.cs | 84 ++++++++++++++++--- .../Archives/GZip/GZipArchive.Factory.cs | 28 +++---- .../Archives/IArchiveOpenable.cs | 2 +- .../Archives/IMultiArchiveOpenable.cs | 4 +- .../Archives/IWritableArchiveExtensions.cs | 2 +- .../IWritableAsyncArchiveExtensions.cs | 2 +- .../Archives/IWritableAsyncArchiveFactory.cs | 19 +++++ .../Archives/IWriteableArchiveFactory.cs | 5 +- .../Archives/Rar/RarArchive.Factory.cs | 24 +++--- .../SevenZip/SevenZipArchive.Factory.cs | 33 ++++---- .../Archives/Tar/TarArchive.Factory.cs | 43 +++++----- .../Archives/Zip/ZipArchive.Factory.cs | 20 ++--- src/SharpCompress/Factories/GZipFactory.cs | 14 +++- src/SharpCompress/Factories/TarFactory.cs | 14 +++- src/SharpCompress/Factories/ZipFactory.cs | 14 +++- src/SharpCompress/Readers/AbstractReader.cs | 10 +-- .../Readers/Ace/AceReader.Factory.cs | 7 +- .../Readers/Arc/ArcReader.Factory.cs | 7 +- .../Readers/Arj/ArjReader.Factory.cs | 7 +- .../Readers/GZip/GZipReader.Factory.cs | 7 +- src/SharpCompress/Readers/IAsyncReader.cs | 2 +- src/SharpCompress/Readers/IReader.cs | 2 +- src/SharpCompress/Readers/IReaderFactory.cs | 2 +- src/SharpCompress/Readers/IReaderOpenable.cs | 2 +- .../Readers/Lzw/LzwReader.Factory.cs | 7 +- .../Readers/Rar/RarReader.Factory.cs | 6 +- src/SharpCompress/Readers/Rar/RarReader.cs | 4 +- .../Readers/Tar/TarReader.Factory.cs | 10 +-- .../Readers/Zip/ZipReader.Factory.cs | 7 +- src/SharpCompress/Writers/AbstractWriter.cs | 2 +- .../Writers/GZip/GZipWriter.Factory.cs | 31 +++++-- src/SharpCompress/Writers/IWriter.cs | 4 +- src/SharpCompress/Writers/IWriterOpenable.cs | 20 +++-- .../SevenZip/SevenZipWriter.Factory.cs | 36 ++++++-- .../Writers/Tar/TarWriter.Factory.cs | 31 +++++-- .../Writers/Zip/ZipWriter.Factory.cs | 31 +++++-- tests/SharpCompress.Test/ArchiveTests.cs | 6 +- .../Lzw/LzwReaderAsyncTests.cs | 2 +- .../SharpCompress.Test/Lzw/LzwReaderTests.cs | 4 +- .../Rar/RarArchiveAsyncTests.cs | 9 +- .../SharpCompress.Test/Rar/RarArchiveTests.cs | 5 +- .../SharpCompress.Test/Tar/TarArchiveTests.cs | 2 +- .../Tar/TarReaderAsyncTests.cs | 2 +- .../SharpCompress.Test/Tar/TarReaderTests.cs | 2 +- 45 files changed, 399 insertions(+), 206 deletions(-) create mode 100644 src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 82a40a16..0ae6c910 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -53,20 +53,20 @@ public static partial class ArchiveFactory } public static async ValueTask OpenAsyncArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? options = null, CancellationToken cancellationToken = default ) { fileInfos.NotNull(nameof(fileInfos)); - var filesArray = fileInfos.ToArray(); - if (filesArray.Length == 0) + var filesArray = fileInfos; + if (filesArray.Count == 0) { throw new ArchiveOperationException("No files to open"); } var fileInfo = filesArray[0]; - if (filesArray.Length == 1) + if (filesArray.Count == 1) { return await OpenAsyncArchive(fileInfo, options, cancellationToken) .ConfigureAwait(false); @@ -83,21 +83,21 @@ public static partial class ArchiveFactory } public static async ValueTask OpenAsyncArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? options = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); - var streamsArray = streams.ToArray(); - if (streamsArray.Length == 0) + var streamsArray = streams; + if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } var firstStream = streamsArray[0]; - if (streamsArray.Length == 1) + if (streamsArray.Count == 1) { return await OpenAsyncArchive(firstStream, options, cancellationToken) .ConfigureAwait(false); @@ -114,18 +114,18 @@ public static partial class ArchiveFactory } public static ValueTask FindFactoryAsync( - string path, + string filePath, CancellationToken cancellationToken = default ) where T : IFactory { - path.NotNullOrEmpty(nameof(path)); - return FindFactoryAsync(new FileInfo(path), cancellationToken); + filePath.NotNullOrEmpty(nameof(filePath)); + return FindFactoryAsync(new FileInfo(filePath), cancellationToken); } - private static async ValueTask FindFactoryAsync( + public static async ValueTask FindFactoryAsync( FileInfo finfo, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ) where T : IFactory { @@ -134,9 +134,9 @@ public static partial class ArchiveFactory return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); } - private static async ValueTask FindFactoryAsync( + public static async ValueTask FindFactoryAsync( Stream stream, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ) where T : IFactory { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 85cf0058..879cb999 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Options; using SharpCompress.Factories; @@ -21,7 +23,7 @@ public static partial class ArchiveFactory where TOptions : IWriterOptions { var factory = Factory - .Factories.OfType>() + .Factories.OfType>() .FirstOrDefault(); if (factory != null) @@ -32,6 +34,24 @@ public static partial class ArchiveFactory throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions)); } + public static ValueTask> CreateAsyncArchive( + CancellationToken cancellationToken = default + ) + where TOptions : IWriterOptions + { + cancellationToken.ThrowIfCancellationRequested(); + var factory = Factory + .Factories.OfType>() + .FirstOrDefault(); + + if (factory is not null) + { + return factory.CreateAsyncArchive(cancellationToken); + } + + throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions)); + } + public static IArchive OpenArchive(string filePath, ReaderOptions? options = null) { filePath.NotNullOrEmpty(nameof(filePath)); @@ -46,19 +66,19 @@ public static partial class ArchiveFactory } public static IArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? options = null ) { fileInfos.NotNull(nameof(fileInfos)); - var filesArray = fileInfos.ToArray(); - if (filesArray.Length == 0) + var filesArray = fileInfos; + if (filesArray.Count == 0) { throw new ArchiveOperationException("No files to open"); } var fileInfo = filesArray[0]; - if (filesArray.Length == 1) + if (filesArray.Count == 1) { return OpenArchive(fileInfo, options); } @@ -69,17 +89,17 @@ public static partial class ArchiveFactory return FindFactory(fileInfo).OpenArchive(filesArray, options); } - public static IArchive OpenArchive(IEnumerable streams, ReaderOptions? options = null) + public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) { streams.NotNull(nameof(streams)); - var streamsArray = streams.ToArray(); - if (streamsArray.Length == 0) + var streamsArray = streams; + if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } var firstStream = streamsArray[0]; - if (streamsArray.Length == 1) + if (streamsArray.Count == 1) { return OpenArchive(firstStream, options); } @@ -100,11 +120,11 @@ public static partial class ArchiveFactory archive.WriteToDirectory(destinationDirectory, options); } - public static T FindFactory(string path) + public static T FindFactory(string filePath) where T : IFactory { - path.NotNullOrEmpty(nameof(path)); - using Stream stream = File.OpenRead(path); + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); return FindFactory(stream); } @@ -182,6 +202,46 @@ public static partial class ArchiveFactory return false; } + public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + string filePath, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return IsArchiveAsync(stream, cancellationToken); + } + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanRead || !stream.CanSeek) + { + throw new ArgumentException("Stream should be readable and seekable"); + } + + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + var isArchive = await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); + stream.Position = startPosition; + + if (isArchive) + { + return (true, factory.KnownArchiveType); + } + } + + return (false, null); + } + public static IEnumerable GetFileParts(string part1) { part1.NotNullOrEmpty(nameof(part1)); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 6fe3fe60..9dad1ae8 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -21,14 +21,14 @@ public partial class GZipArchive #endif { public static ValueTask> OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - path.NotNullOrEmpty(nameof(path)); - return OpenAsyncArchive(new FileInfo(path), readerOptions, cancellationToken); + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken); } public static IWritableArchive OpenArchive( @@ -37,7 +37,7 @@ public partial class GZipArchive ) { filePath.NotNullOrEmpty(nameof(filePath)); - return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions()); + return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath); } public static IWritableArchive OpenArchive( @@ -50,39 +50,39 @@ public partial class GZipArchive new SourceStream( fileInfo, i => ArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } public static IWritableArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ) { fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; return new GZipArchive( new SourceStream( files[0], - i => i < files.Length ? files[i] : null, - readerOptions ?? new ReaderOptions() + i => i < files.Count ? files[i] : null, + readerOptions ?? ReaderOptions.ForFilePath ) ); } public static IWritableArchive OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ) { streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; return new GZipArchive( new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ) ); } @@ -100,7 +100,7 @@ public partial class GZipArchive } return new GZipArchive( - new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions()) + new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) ); } diff --git a/src/SharpCompress/Archives/IArchiveOpenable.cs b/src/SharpCompress/Archives/IArchiveOpenable.cs index 5539e16e..22c0ed34 100644 --- a/src/SharpCompress/Archives/IArchiveOpenable.cs +++ b/src/SharpCompress/Archives/IArchiveOpenable.cs @@ -20,7 +20,7 @@ public interface IArchiveOpenable public static abstract TSync OpenArchive(Stream stream, ReaderOptions? readerOptions = null); public static abstract ValueTask OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ); diff --git a/src/SharpCompress/Archives/IMultiArchiveOpenable.cs b/src/SharpCompress/Archives/IMultiArchiveOpenable.cs index 6a927297..4c208ec1 100644 --- a/src/SharpCompress/Archives/IMultiArchiveOpenable.cs +++ b/src/SharpCompress/Archives/IMultiArchiveOpenable.cs @@ -12,12 +12,12 @@ public interface IMultiArchiveOpenable where TASync : IAsyncArchive { public static abstract TSync OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ); public static abstract TSync OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ); diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 6012dda5..f6ce3ff2 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -22,7 +22,7 @@ public static class IWritableArchiveExtensions var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) ) { - var fileInfo = new FileInfo(path); + var fileInfo = new FileInfo(filePath); writableArchive.AddEntry( path.Substring(filePath.Length), fileInfo.OpenRead(), diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 4bdc792f..6e848b21 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -24,7 +24,7 @@ public static class IWritableAsyncArchiveExtensions var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) ) { - var fileInfo = new FileInfo(path); + var fileInfo = new FileInfo(filePath); await writableArchive .AddEntryAsync( path.Substring(filePath.Length), diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs new file mode 100644 index 00000000..0de3a7da --- /dev/null +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Options; + +namespace SharpCompress.Archives; + +/// +/// Decorator for used to declare an archive format as able to create async writable archives. +/// +public interface IWritableAsyncArchiveFactory : Factories.IFactory + where TOptions : IWriterOptions +{ + /// + /// Creates a new, empty async archive, ready to be written. + /// + ValueTask> CreateAsyncArchive( + CancellationToken cancellationToken = default + ); +} diff --git a/src/SharpCompress/Archives/IWriteableArchiveFactory.cs b/src/SharpCompress/Archives/IWriteableArchiveFactory.cs index 40a01fac..7c7ace2c 100644 --- a/src/SharpCompress/Archives/IWriteableArchiveFactory.cs +++ b/src/SharpCompress/Archives/IWriteableArchiveFactory.cs @@ -3,7 +3,7 @@ using SharpCompress.Common.Options; namespace SharpCompress.Archives; /// -/// Decorator for used to declare an archive format as able to create writeable archives +/// Decorator for used to declare an archive format as able to create writable archives. /// /// /// Implemented by:
@@ -12,7 +12,8 @@ namespace SharpCompress.Archives; /// /// /// -public interface IWriteableArchiveFactory : Factories.IFactory +///
+public interface IWritableArchiveFactory : Factories.IFactory where TOptions : IWriterOptions { /// diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 707049d3..432bf54f 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -21,14 +21,14 @@ public partial class RarArchive #endif { public static ValueTask OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - path.NotNullOrEmpty(nameof(path)); - return new((IRarAsyncArchive)OpenArchive(new FileInfo(path), readerOptions)); + filePath.NotNullOrEmpty(nameof(filePath)); + return new((IRarAsyncArchive)OpenArchive(new FileInfo(filePath), readerOptions)); } public static IRarArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null) @@ -39,7 +39,7 @@ public partial class RarArchive new SourceStream( fileInfo, i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } @@ -71,33 +71,33 @@ public partial class RarArchive } public static IRarArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ) { fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; return new RarArchive( new SourceStream( files[0], - i => i < files.Length ? files[i] : null, - readerOptions ?? new ReaderOptions() + i => i < files.Count ? files[i] : null, + readerOptions ?? ReaderOptions.ForFilePath ) ); } public static IRarArchive OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ) { streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; return new RarArchive( new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ) ); } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 30adbaf9..df7fa8c4 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -17,22 +17,25 @@ public partial class SevenZipArchive #endif { public static ValueTask OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - path.NotNullOrEmpty("path"); + filePath.NotNullOrEmpty(nameof(filePath)); return new( - (IAsyncArchive)OpenArchive(new FileInfo(path), readerOptions ?? new ReaderOptions()) + (IAsyncArchive)OpenArchive( + new FileInfo(filePath), + readerOptions ?? ReaderOptions.ForFilePath + ) ); } public static IArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null) { - filePath.NotNullOrEmpty("filePath"); - return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions()); + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath); } public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) @@ -42,39 +45,39 @@ public partial class SevenZipArchive new SourceStream( fileInfo, i => ArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } public static IArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ) { fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; return new SevenZipArchive( new SourceStream( files[0], - i => i < files.Length ? files[i] : null, - readerOptions ?? new ReaderOptions() + i => i < files.Count ? files[i] : null, + readerOptions ?? ReaderOptions.ForFilePath ) ); } public static IArchive OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ) { streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; return new SevenZipArchive( new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ) ); } @@ -89,7 +92,7 @@ public partial class SevenZipArchive } return new SevenZipArchive( - new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions()) + new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) ); } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 3c3caa3f..e1464dbf 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -38,23 +38,20 @@ public partial class TarArchive ) { fileInfo.NotNull(nameof(fileInfo)); - return OpenArchive( - [fileInfo], - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } - ); + return OpenArchive([fileInfo], readerOptions ?? ReaderOptions.ForFilePath); } public static IWritableArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ) { fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; var sourceStream = new SourceStream( files[0], - i => i < files.Length ? files[i] : null, - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + i => i < files.Count ? files[i] : null, + readerOptions ?? ReaderOptions.ForFilePath ); var compressionType = TarFactory.GetCompressionType( sourceStream, @@ -65,16 +62,16 @@ public partial class TarArchive } public static IWritableArchive OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ) { streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; var sourceStream = new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ); var compressionType = TarFactory.GetCompressionType( sourceStream, @@ -109,7 +106,7 @@ public partial class TarArchive var sourceStream = new SourceStream( stream, i => null, - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForExternalStream ); var compressionType = await TarFactory .GetCompressionTypeAsync( @@ -123,14 +120,14 @@ public partial class TarArchive } public static ValueTask> OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - path.NotNullOrEmpty(nameof(path)); - return OpenAsyncArchive(new FileInfo(path), readerOptions, cancellationToken); + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken); } public static async ValueTask> OpenAsyncArchive( @@ -141,7 +138,7 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); fileInfo.NotNull(nameof(fileInfo)); - readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false }; + readerOptions ??= ReaderOptions.ForFilePath; var sourceStream = new SourceStream(fileInfo, i => null, readerOptions); var compressionType = await TarFactory .GetCompressionTypeAsync( @@ -162,11 +159,11 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; var sourceStream = new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ); var compressionType = await TarFactory .GetCompressionTypeAsync( @@ -187,11 +184,11 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; var sourceStream = new SourceStream( files[0], - i => i < files.Length ? files[i] : null, - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + i => i < files.Count ? files[i] : null, + readerOptions ?? ReaderOptions.ForFilePath ); var compressionType = await TarFactory .GetCompressionTypeAsync( diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index dc4aad38..db0b8489 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -47,33 +47,33 @@ public partial class ZipArchive } public static IWritableArchive OpenArchive( - IEnumerable fileInfos, + IReadOnlyList fileInfos, ReaderOptions? readerOptions = null ) { fileInfos.NotNull(nameof(fileInfos)); - var files = fileInfos.ToArray(); + var files = fileInfos; return new ZipArchive( new SourceStream( files[0], - i => i < files.Length ? files[i] : null, + i => i < files.Count ? files[i] : null, readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } ) ); } public static IWritableArchive OpenArchive( - IEnumerable streams, + IReadOnlyList streams, ReaderOptions? readerOptions = null ) { streams.NotNull(nameof(streams)); - var strms = streams.ToArray(); + var strms = streams; return new ZipArchive( new SourceStream( strms[0], - i => i < strms.Length ? strms[i] : null, - readerOptions ?? new ReaderOptions() + i => i < strms.Count ? strms[i] : null, + readerOptions ?? ReaderOptions.ForExternalStream ) ); } @@ -91,18 +91,18 @@ public partial class ZipArchive } return new ZipArchive( - new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions()) + new SourceStream(stream, i => null, readerOptions ?? ReaderOptions.ForExternalStream) ); } public static ValueTask> OpenAsyncArchive( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new((IWritableAsyncArchive)OpenArchive(path, readerOptions)); + return new((IWritableAsyncArchive)OpenArchive(filePath, readerOptions)); } public static ValueTask> OpenAsyncArchive( diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 70a2ad85..5278871e 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -27,7 +27,8 @@ public class GZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWritableArchiveFactory, + IWritableAsyncArchiveFactory { #region IFactory @@ -218,10 +219,19 @@ public class GZipFactory #endregion - #region IWriteableArchiveFactory + #region IWritableArchiveFactory /// public IWritableArchive CreateArchive() => GZipArchive.CreateArchive(); + /// + public ValueTask> CreateAsyncArchive( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return GZipArchive.CreateAsyncArchive(); + } + #endregion } diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index c92501ba..fda80346 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -25,7 +25,8 @@ public class TarFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWritableArchiveFactory, + IWritableAsyncArchiveFactory { #region IFactory @@ -421,10 +422,19 @@ public class TarFactory #endregion - #region IWriteableArchiveFactory + #region IWritableArchiveFactory /// public IWritableArchive CreateArchive() => TarArchive.CreateArchive(); + /// + public ValueTask> CreateAsyncArchive( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return TarArchive.CreateAsyncArchive(); + } + #endregion } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 057bfb65..67c1100a 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -24,7 +24,8 @@ public class ZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWritableArchiveFactory, + IWritableAsyncArchiveFactory { #region IFactory @@ -246,10 +247,19 @@ public class ZipFactory #endregion - #region IWriteableArchiveFactory + #region IWritableArchiveFactory /// public IWritableArchive CreateArchive() => ZipArchive.CreateArchive(); + /// + public ValueTask> CreateAsyncArchive( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return ZipArchive.CreateAsyncArchive(); + } + #endregion } diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index e6d0966d..52410e14 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -22,20 +22,16 @@ public abstract partial class AbstractReader : 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; } /// /// Current volume that the current entry resides in diff --git a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs index d3adddd0..a4ae43bb 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs @@ -36,14 +36,14 @@ public partial class AceReader } public static ValueTask 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 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); } } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs b/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs index 0ca8478c..e6bbfe9e 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs @@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arc; public partial class ArcReader : IReaderOpenable { public static ValueTask 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 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); } } diff --git a/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs b/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs index 6072045d..1e5689d3 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs @@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arj; public partial class ArjReader : IReaderOpenable { public static ValueTask 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 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); } } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs index 2a05e323..8baf715d 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs @@ -10,14 +10,14 @@ public partial class GZipReader #endif { public static ValueTask 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 OpenAsyncReader( @@ -49,6 +49,7 @@ 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); } diff --git a/src/SharpCompress/Readers/IAsyncReader.cs b/src/SharpCompress/Readers/IAsyncReader.cs index bd82ee48..c3cc8de0 100644 --- a/src/SharpCompress/Readers/IAsyncReader.cs +++ b/src/SharpCompress/Readers/IAsyncReader.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Readers; public interface IAsyncReader : IAsyncDisposable { - ArchiveType ArchiveType { get; } + ArchiveType Type { get; } IEntry Entry { get; } diff --git a/src/SharpCompress/Readers/IReader.cs b/src/SharpCompress/Readers/IReader.cs index a38d61d6..f7642ffc 100644 --- a/src/SharpCompress/Readers/IReader.cs +++ b/src/SharpCompress/Readers/IReader.cs @@ -6,7 +6,7 @@ namespace SharpCompress.Readers; public interface IReader : IDisposable { - ArchiveType ArchiveType { get; } + ArchiveType Type { get; } IEntry Entry { get; } diff --git a/src/SharpCompress/Readers/IReaderFactory.cs b/src/SharpCompress/Readers/IReaderFactory.cs index 538dacfb..9b3c533c 100644 --- a/src/SharpCompress/Readers/IReaderFactory.cs +++ b/src/SharpCompress/Readers/IReaderFactory.cs @@ -24,6 +24,6 @@ public interface IReaderFactory : Factories.IFactory ValueTask OpenAsyncReader( Stream stream, ReaderOptions? options, - CancellationToken cancellationToken + CancellationToken cancellationToken = default ); } diff --git a/src/SharpCompress/Readers/IReaderOpenable.cs b/src/SharpCompress/Readers/IReaderOpenable.cs index 32c7bdf3..58604211 100644 --- a/src/SharpCompress/Readers/IReaderOpenable.cs +++ b/src/SharpCompress/Readers/IReaderOpenable.cs @@ -17,7 +17,7 @@ public interface IReaderOpenable public static abstract IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null); public static abstract ValueTask OpenAsyncReader( - string path, + string filePath, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default ); diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs index 6e17e222..b62e7b5f 100644 --- a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs @@ -10,14 +10,14 @@ public partial class LzwReader #endif { public static ValueTask 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 OpenAsyncReader( @@ -49,6 +49,7 @@ 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); } diff --git a/src/SharpCompress/Readers/Rar/RarReader.Factory.cs b/src/SharpCompress/Readers/Rar/RarReader.Factory.cs index b18fd3c7..20ec829a 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Factory.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Factory.cs @@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Rar; public partial class RarReader : IReaderOpenable { public static ValueTask 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 OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index 24235308..a6a8ede7 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -48,7 +48,7 @@ public abstract partial class RarReader : AbstractReader fileInfos, ReaderOptions? options = null) { - options ??= new ReaderOptions { LeaveStreamOpen = false }; + options ??= ReaderOptions.ForFilePath; return OpenReader(fileInfos.Select(x => x.OpenRead()), options); } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index 13c8453c..f94a89dc 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -71,13 +71,13 @@ public partial class TarReader } public static ValueTask 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 OpenAsyncReader( @@ -139,7 +139,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); @@ -154,7 +154,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); } diff --git a/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs b/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs index 65f179cf..7058937d 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs @@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Zip; public partial class ZipReader : IReaderOpenable { public static ValueTask 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 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); } } diff --git a/src/SharpCompress/Writers/AbstractWriter.cs b/src/SharpCompress/Writers/AbstractWriter.cs index 60ddb1b1..6cd32c7f 100644 --- a/src/SharpCompress/Writers/AbstractWriter.cs +++ b/src/SharpCompress/Writers/AbstractWriter.cs @@ -20,7 +20,7 @@ public abstract partial class AbstractWriter(ArchiveType type, IWriterOptions wr protected Stream? OutputStream { get; private set; } - public ArchiveType WriterType { get; } = type; + public ArchiveType Type { get; } = type; protected IWriterOptions WriterOptions { get; } = writerOptions; diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs index 01e6edb5..f4f2b902 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs @@ -1,5 +1,7 @@ #if NET8_0_OR_GREATER using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Writers.GZip; @@ -15,7 +17,7 @@ public partial class GZipWriter : IWriterOpenable public static IWriter OpenWriter(FileInfo fileInfo, GZipWriterOptions writerOptions) { fileInfo.NotNull(nameof(fileInfo)); - return new GZipWriter(fileInfo.OpenWrite(), writerOptions); + return new GZipWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false }); } public static IWriter OpenWriter(Stream stream, GZipWriterOptions writerOptions) @@ -24,19 +26,34 @@ public partial class GZipWriter : IWriterOpenable return new GZipWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter(string stream, GZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + string filePath, + GZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(filePath, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(Stream stream, GZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + Stream stream, + GZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(stream, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, GZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + FileInfo fileInfo, + GZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions)); } } #endif diff --git a/src/SharpCompress/Writers/IWriter.cs b/src/SharpCompress/Writers/IWriter.cs index 648c0fe0..c64166df 100644 --- a/src/SharpCompress/Writers/IWriter.cs +++ b/src/SharpCompress/Writers/IWriter.cs @@ -8,14 +8,14 @@ namespace SharpCompress.Writers; public interface IWriter : IDisposable { - ArchiveType WriterType { get; } + ArchiveType Type { get; } void Write(string filename, Stream source, DateTime? modificationTime); void WriteDirectory(string directoryName, DateTime? modificationTime); } public interface IAsyncWriter : IDisposable, IAsyncDisposable { - ArchiveType WriterType { get; } + ArchiveType Type { get; } ValueTask WriteAsync( string filename, Stream source, diff --git a/src/SharpCompress/Writers/IWriterOpenable.cs b/src/SharpCompress/Writers/IWriterOpenable.cs index 828b0b7c..1c584f2f 100644 --- a/src/SharpCompress/Writers/IWriterOpenable.cs +++ b/src/SharpCompress/Writers/IWriterOpenable.cs @@ -1,6 +1,7 @@ #if NET8_0_OR_GREATER using System.IO; using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Options; namespace SharpCompress.Writers; @@ -17,22 +18,25 @@ public interface IWriterOpenable /// Opens a Writer asynchronously. /// /// The stream to write to. - /// The archive type. /// Writer options. - /// A task that returns an IWriter. - public static abstract IAsyncWriter OpenAsyncWriter( + /// Cancellation token. + /// A task that returns an async writer. + public static abstract ValueTask OpenAsyncWriter( Stream stream, - TWriterOptions writerOptions + TWriterOptions writerOptions, + CancellationToken cancellationToken = default ); - public static abstract IAsyncWriter OpenAsyncWriter( + public static abstract ValueTask OpenAsyncWriter( string filePath, - TWriterOptions writerOptions + TWriterOptions writerOptions, + CancellationToken cancellationToken = default ); - public static abstract IAsyncWriter OpenAsyncWriter( + public static abstract ValueTask OpenAsyncWriter( FileInfo fileInfo, - TWriterOptions writerOptions + TWriterOptions writerOptions, + CancellationToken cancellationToken = default ); } #endif diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs index 89e80414..e84d7401 100644 --- a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs @@ -1,5 +1,7 @@ #if NET8_0_OR_GREATER using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Writers.SevenZip; @@ -20,7 +22,13 @@ public partial class SevenZipWriter : IWriterOpenable public static IWriter OpenWriter(FileInfo fileInfo, SevenZipWriterOptions writerOptions) { fileInfo.NotNull(nameof(fileInfo)); - return new SevenZipWriter(fileInfo.OpenWrite(), writerOptions); + return new SevenZipWriter( + fileInfo.OpenWrite(), + writerOptions with + { + LeaveStreamOpen = false, + } + ); } /// @@ -35,28 +43,40 @@ public partial class SevenZipWriter : IWriterOpenable /// /// Opens a new async SevenZipWriter for the specified file path. /// - public static IAsyncWriter OpenAsyncWriter(string filePath, SevenZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + string filePath, + SevenZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(filePath, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(filePath, writerOptions)); } /// /// Opens a new async SevenZipWriter for the specified stream. /// - public static IAsyncWriter OpenAsyncWriter(Stream stream, SevenZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + Stream stream, + SevenZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(stream, writerOptions)); } /// /// Opens a new async SevenZipWriter for the specified file. /// - public static IAsyncWriter OpenAsyncWriter( + public static ValueTask OpenAsyncWriter( FileInfo fileInfo, - SevenZipWriterOptions writerOptions + SevenZipWriterOptions writerOptions, + CancellationToken cancellationToken = default ) { - return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions)); } } #endif diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs index a3c6bf6f..b00ed13e 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs @@ -1,5 +1,7 @@ #if NET8_0_OR_GREATER using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Writers.Tar; @@ -15,7 +17,7 @@ public partial class TarWriter : IWriterOpenable public static IWriter OpenWriter(FileInfo fileInfo, TarWriterOptions writerOptions) { fileInfo.NotNull(nameof(fileInfo)); - return new TarWriter(fileInfo.OpenWrite(), writerOptions); + return new TarWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false }); } public static IWriter OpenWriter(Stream stream, TarWriterOptions writerOptions) @@ -24,19 +26,34 @@ public partial class TarWriter : IWriterOpenable return new TarWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter(string stream, TarWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + string filePath, + TarWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(filePath, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(Stream stream, TarWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + Stream stream, + TarWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(stream, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, TarWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + FileInfo fileInfo, + TarWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions)); } } #endif diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs index d21ab7e2..b6667842 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs @@ -1,5 +1,7 @@ #if NET8_0_OR_GREATER using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Writers.Zip; @@ -15,7 +17,7 @@ public partial class ZipWriter : IWriterOpenable public static IWriter OpenWriter(FileInfo fileInfo, ZipWriterOptions writerOptions) { fileInfo.NotNull(nameof(fileInfo)); - return new ZipWriter(fileInfo.OpenWrite(), writerOptions); + return new ZipWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false }); } public static IWriter OpenWriter(Stream stream, ZipWriterOptions writerOptions) @@ -24,19 +26,34 @@ public partial class ZipWriter : IWriterOpenable return new ZipWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter(string stream, ZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + string filePath, + ZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(filePath, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(Stream stream, ZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + Stream stream, + ZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(stream, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(stream, writerOptions)); } - public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, ZipWriterOptions writerOptions) + public static ValueTask OpenAsyncWriter( + FileInfo fileInfo, + ZipWriterOptions writerOptions, + CancellationToken cancellationToken = default + ) { - return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); + cancellationToken.ThrowIfCancellationRequested(); + return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions)); } } #endif diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 270900b9..492cc85e 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -179,7 +179,7 @@ public class ArchiveTests : ReaderTests { using ( var archive = ArchiveFactory.OpenArchive( - testArchives.Select(a => new FileInfo(a)), + testArchives.Select(a => new FileInfo(a)).ToArray(), readerOptions ) ) @@ -208,7 +208,7 @@ public class ArchiveTests : ReaderTests { using ( var archive = ArchiveFactory.OpenArchive( - testArchives.Select(f => new FileInfo(f)), + testArchives.Select(f => new FileInfo(f)).ToArray(), readerOptions ) ) @@ -240,7 +240,7 @@ public class ArchiveTests : ReaderTests { var src = testArchives.ToArray(); using var archive = ArchiveFactory.OpenArchive( - src.Select(f => new FileInfo(f)), + src.Select(f => new FileInfo(f)).ToArray(), readerOptions ); var idx = 0; diff --git a/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs b/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs index 6ecc6047..0b09298b 100644 --- a/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs @@ -23,7 +23,7 @@ public class LzwReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "large_test.txt.Z")); using var reader = LzwReader.OpenReader(stream); - Assert.Equal(ArchiveType.Lzw, reader.ArchiveType); + Assert.Equal(ArchiveType.Lzw, reader.Type); Assert.True(reader.MoveToNextEntry()); var entry = reader.Entry; diff --git a/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs index f75fd666..8e94481f 100644 --- a/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs +++ b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs @@ -41,7 +41,7 @@ public class LzwReaderTests : ReaderTests ); // Should detect as Tar archive with Lzw compression - Assert.Equal(ArchiveType.Tar, reader.ArchiveType); + Assert.Equal(ArchiveType.Tar, reader.Type); Assert.True(reader.MoveToNextEntry()); Assert.NotNull(reader.Entry); Assert.Equal(CompressionType.Lzw, reader.Entry.CompressionType); @@ -80,7 +80,7 @@ public class LzwReaderTests : ReaderTests using var reader = ReaderFactory.OpenReader(stream); // Should detect as Lzw archive (not Tar) - Assert.Equal(ArchiveType.Lzw, reader.ArchiveType); + Assert.Equal(ArchiveType.Lzw, reader.Type); Assert.True(reader.MoveToNextEntry()); Assert.NotNull(reader.Entry); Assert.Equal(CompressionType.Lzw, reader.Entry.CompressionType); diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index bba174d6..3f48b976 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -253,7 +253,10 @@ public class RarArchiveAsyncTests : ArchiveTests private async ValueTask DoRar_Multi_ArchiveStreamReadAsync(string[] archives, bool isSolid) { using var archive = RarArchive.OpenArchive( - archives.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)).Select(File.OpenRead) + archives + .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)) + .Select(File.OpenRead) + .ToArray() ); Assert.Equal(archive.IsSolid, isSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) @@ -672,7 +675,7 @@ public class RarArchiveAsyncTests : ArchiveTests { var paths = testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)); using var archive = ArchiveFactory.OpenArchive( - paths.Select(a => new FileInfo(a)), + paths.Select(a => new FileInfo(a)).ToArray(), readerOptions ); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) @@ -730,7 +733,7 @@ public class RarArchiveAsyncTests : ArchiveTests { var paths = testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x)); using var archive = ArchiveFactory.OpenArchive( - paths.Select(f => new FileInfo(f)), + paths.Select(f => new FileInfo(f)).ToArray(), readerOptions ); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index e4fef73f..fdb8d56b 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -242,7 +242,10 @@ public class RarArchiveTests : ArchiveTests private void DoRar_Multi_ArchiveStreamRead(string[] archives, bool isSolid) { using var archive = RarArchive.OpenArchive( - archives.Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)).Select(File.OpenRead) + archives + .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)) + .Select(File.OpenRead) + .ToArray() ); Assert.Equal(archive.IsSolid, isSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 008bb145..086ca02c 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -336,7 +336,7 @@ public class TarArchiveTests : ArchiveTests using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); using var reader = ReaderFactory.OpenReader(stream); - Assert.Equal(ArchiveType.Tar, reader.ArchiveType); + Assert.Equal(ArchiveType.Tar, reader.Type); Assert.True(reader.MoveToNextEntry()); } } diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 34ceac02..07c0b15b 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -156,7 +156,7 @@ public class TarReaderAsyncTests : ReaderTests var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsRar.tar"); using Stream stream = File.OpenRead(archiveFullPath); using var reader = ReaderFactory.OpenReader(stream); - Assert.True(reader.ArchiveType == ArchiveType.Tar); + Assert.True(reader.Type == ArchiveType.Tar); } [Fact] diff --git a/tests/SharpCompress.Test/Tar/TarReaderTests.cs b/tests/SharpCompress.Test/Tar/TarReaderTests.cs index d76792cf..21c62f83 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderTests.cs @@ -147,7 +147,7 @@ public class TarReaderTests : ReaderTests var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsRar.tar"); using Stream stream = File.OpenRead(archiveFullPath); using var reader = ReaderFactory.OpenReader(stream); - Assert.True(reader.ArchiveType == ArchiveType.Tar); + Assert.True(reader.Type == ArchiveType.Tar); } [Fact] From 9050a7a64ae70b9a86345df7b3485fc736fb7d6d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 11:43:44 +0000 Subject: [PATCH 02/74] remove extra creates --- src/SharpCompress/Archives/ArchiveFactory.cs | 22 +-- .../Archives/IWritableAsyncArchiveFactory.cs | 19 --- src/SharpCompress/Factories/GZipFactory.cs | 12 +- src/SharpCompress/Factories/TarFactory.cs | 12 +- src/SharpCompress/Factories/ZipFactory.cs | 12 +- .../SharpCompress.Test/ArchiveFactoryTests.cs | 133 ++++++++++++++++++ 6 files changed, 138 insertions(+), 72 deletions(-) delete mode 100644 src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs create mode 100644 tests/SharpCompress.Test/ArchiveFactoryTests.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 879cb999..e4973f0f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -34,24 +34,6 @@ public static partial class ArchiveFactory throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions)); } - public static ValueTask> CreateAsyncArchive( - CancellationToken cancellationToken = default - ) - where TOptions : IWriterOptions - { - cancellationToken.ThrowIfCancellationRequested(); - var factory = Factory - .Factories.OfType>() - .FirstOrDefault(); - - if (factory is not null) - { - return factory.CreateAsyncArchive(cancellationToken); - } - - throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions)); - } - public static IArchive OpenArchive(string filePath, ReaderOptions? options = null) { filePath.NotNullOrEmpty(nameof(filePath)); @@ -202,14 +184,14 @@ public static partial class ArchiveFactory return false; } - public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return IsArchiveAsync(stream, cancellationToken); + return await IsArchiveAsync(stream, cancellationToken).ConfigureAwait(false); } public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs deleted file mode 100644 index 0de3a7da..00000000 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.Common.Options; - -namespace SharpCompress.Archives; - -/// -/// Decorator for used to declare an archive format as able to create async writable archives. -/// -public interface IWritableAsyncArchiveFactory : Factories.IFactory - where TOptions : IWriterOptions -{ - /// - /// Creates a new, empty async archive, ready to be written. - /// - ValueTask> CreateAsyncArchive( - CancellationToken cancellationToken = default - ); -} diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index 5278871e..c49b8f8e 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -27,8 +27,7 @@ public class GZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWritableArchiveFactory, - IWritableAsyncArchiveFactory + IWritableArchiveFactory { #region IFactory @@ -224,14 +223,5 @@ public class GZipFactory /// public IWritableArchive CreateArchive() => GZipArchive.CreateArchive(); - /// - public ValueTask> CreateAsyncArchive( - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return GZipArchive.CreateAsyncArchive(); - } - #endregion } diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index fda80346..28380587 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -25,8 +25,7 @@ public class TarFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWritableArchiveFactory, - IWritableAsyncArchiveFactory + IWritableArchiveFactory { #region IFactory @@ -427,14 +426,5 @@ public class TarFactory /// public IWritableArchive CreateArchive() => TarArchive.CreateArchive(); - /// - public ValueTask> CreateAsyncArchive( - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return TarArchive.CreateAsyncArchive(); - } - #endregion } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index 67c1100a..b5dbdff3 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -24,8 +24,7 @@ public class ZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWritableArchiveFactory, - IWritableAsyncArchiveFactory + IWritableArchiveFactory { #region IFactory @@ -252,14 +251,5 @@ public class ZipFactory /// public IWritableArchive CreateArchive() => ZipArchive.CreateArchive(); - /// - public ValueTask> CreateAsyncArchive( - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return ZipArchive.CreateAsyncArchive(); - } - #endregion } diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs new file mode 100644 index 00000000..96249f8e --- /dev/null +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -0,0 +1,133 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Factories; +using Xunit; + +namespace SharpCompress.Test; + +public class ArchiveFactoryTests : TestBase +{ + [Theory] + [InlineData("Zip.deflate.zip", typeof(ZipFactory))] + [InlineData("Tar.noEmptyDirs.tar", typeof(TarFactory))] + [InlineData("Rar.rar", typeof(RarFactory))] + [InlineData("7Zip.nonsolid.7z", typeof(SevenZipFactory))] + public async ValueTask FindFactoryAsync_String_ReturnsExpectedFactory( + string archiveName, + System.Type expectedFactoryType + ) + { + var factory = await ArchiveFactory.FindFactoryAsync( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.IsType(expectedFactoryType, factory); + } + + [Theory] + [InlineData("Zip.deflate.zip", typeof(ZipFactory))] + [InlineData("Tar.noEmptyDirs.tar", typeof(TarFactory))] + [InlineData("Rar.rar", typeof(RarFactory))] + [InlineData("7Zip.nonsolid.7z", typeof(SevenZipFactory))] + public async ValueTask FindFactoryAsync_FileInfo_ReturnsExpectedFactory( + string archiveName, + System.Type expectedFactoryType + ) + { + var factory = await ArchiveFactory.FindFactoryAsync( + new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, archiveName)) + ); + + Assert.IsType(expectedFactoryType, factory); + } + + [Theory] + [InlineData("Zip.deflate.zip", typeof(ZipFactory))] + [InlineData("Tar.noEmptyDirs.tar", typeof(TarFactory))] + public async ValueTask FindFactoryAsync_Stream_PreservesPosition( + string archiveName, + System.Type expectedFactoryType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 7); + var startPosition = stream.Position; + + var factory = await ArchiveFactory.FindFactoryAsync(stream); + + Assert.IsType(expectedFactoryType, factory); + Assert.Equal(startPosition, stream.Position); + } + + [Fact] + public async ValueTask FindFactoryAsync_InvalidData_ThrowsArchiveOperationException() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + await Assert.ThrowsAsync(async () => + await ArchiveFactory.FindFactoryAsync(stream) + ); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + [InlineData("Rar.rar", ArchiveType.Rar)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)] + public async ValueTask IsArchiveAsync_String_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = await ArchiveFactory.IsArchiveAsync( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.True(result.IsArchive); + Assert.Equal(expectedType, result.Type); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public async ValueTask IsArchiveAsync_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 11); + var startPosition = stream.Position; + + var result = await ArchiveFactory.IsArchiveAsync(stream); + + Assert.True(result.IsArchive); + Assert.Equal(expectedType, result.Type); + Assert.Equal(startPosition, stream.Position); + } + + [Fact] + public async ValueTask IsArchiveAsync_InvalidData_ReturnsFalseAndNullType() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var result = await ArchiveFactory.IsArchiveAsync(stream); + + Assert.False(result.IsArchive); + Assert.Null(result.Type); + Assert.Equal(0, stream.Position); + } + + private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) + { + var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + var buffer = new byte[prefixLength + archiveBytes.Length]; + + archiveBytes.CopyTo(buffer, prefixLength); + + var stream = new MemoryStream(buffer); + stream.Position = prefixLength; + return stream; + } +} From bf32c85933ea00587007dffeaf94ce102d7cacba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:48:55 +0000 Subject: [PATCH 03/74] Initial plan From b0e97360051d3776042ba9304278404224b4e35c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:52:06 +0000 Subject: [PATCH 04/74] Rename IWriteableArchiveFactory.cs to IWritableArchiveFactory.cs to match interface name Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- ...eArchiveFactory.cs => IWritableArchiveFactory.cs} | 0 src/SharpCompress/packages.lock.json | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) rename src/SharpCompress/Archives/{IWriteableArchiveFactory.cs => IWritableArchiveFactory.cs} (100%) diff --git a/src/SharpCompress/Archives/IWriteableArchiveFactory.cs b/src/SharpCompress/Archives/IWritableArchiveFactory.cs similarity index 100% rename from src/SharpCompress/Archives/IWriteableArchiveFactory.cs rename to src/SharpCompress/Archives/IWritableArchiveFactory.cs diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 5059aafe..b44de28b 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.23, )", + "resolved": "8.0.23", + "contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From ebd784cfb2c00192291d652aeadb1f438b565265 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 15:16:28 +0000 Subject: [PATCH 05/74] code review changes --- src/SharpCompress/Archives/ArchiveFactory.Async.cs | 3 +-- src/SharpCompress/Archives/IWritableArchiveExtensions.cs | 8 +++----- .../Archives/IWritableAsyncArchiveExtensions.cs | 8 +++----- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 0ae6c910..67d71b70 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Factories; -using SharpCompress.IO; using SharpCompress.Readers; namespace SharpCompress.Archives; @@ -123,7 +122,7 @@ public static partial class ArchiveFactory return FindFactoryAsync(new FileInfo(filePath), cancellationToken); } - public static async ValueTask FindFactoryAsync( + private static async ValueTask FindFactoryAsync( FileInfo finfo, CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index f6ce3ff2..f349f06d 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -1,8 +1,6 @@ using System; using System.IO; -using SharpCompress.Common; using SharpCompress.Common.Options; -using SharpCompress.Writers; namespace SharpCompress.Archives; @@ -11,7 +9,7 @@ public static class IWritableArchiveExtensions extension(IWritableArchive writableArchive) { public void AddAllFromDirectory( - string filePath, + string directoryPath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories ) @@ -19,12 +17,12 @@ public static class IWritableArchiveExtensions using (writableArchive.PauseEntryRebuilding()) { foreach ( - var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) + var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, searchOption) ) { var fileInfo = new FileInfo(filePath); writableArchive.AddEntry( - path.Substring(filePath.Length), + Path.GetFileName(filePath), fileInfo.OpenRead(), true, fileInfo.Length, diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 6e848b21..72110966 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -2,9 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; -using SharpCompress.Common; using SharpCompress.Common.Options; -using SharpCompress.Writers; namespace SharpCompress.Archives; @@ -13,7 +11,7 @@ public static class IWritableAsyncArchiveExtensions extension(IWritableAsyncArchive writableArchive) { public async ValueTask AddAllFromDirectoryAsync( - string filePath, + string directoryPath, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories ) @@ -21,13 +19,13 @@ public static class IWritableAsyncArchiveExtensions using (writableArchive.PauseEntryRebuilding()) { foreach ( - var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) + var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, searchOption) ) { var fileInfo = new FileInfo(filePath); await writableArchive .AddEntryAsync( - path.Substring(filePath.Length), + Path.GetFileName(filePath), fileInfo.OpenRead(), true, fileInfo.Length, From f13ab3a0e7d511745d644c7fbd181554b9e0338a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 15:33:30 +0000 Subject: [PATCH 06/74] FindFactoryAsync shouldn't be public --- src/SharpCompress/Archives/ArchiveFactory.Async.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 67d71b70..03595554 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -112,7 +112,7 @@ public static partial class ArchiveFactory .ConfigureAwait(false); } - public static ValueTask FindFactoryAsync( + internal static ValueTask FindFactoryAsync( string filePath, CancellationToken cancellationToken = default ) @@ -122,7 +122,7 @@ public static partial class ArchiveFactory return FindFactoryAsync(new FileInfo(filePath), cancellationToken); } - private static async ValueTask FindFactoryAsync( + internal static async ValueTask FindFactoryAsync( FileInfo finfo, CancellationToken cancellationToken = default ) @@ -133,7 +133,7 @@ public static partial class ArchiveFactory return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); } - public static async ValueTask FindFactoryAsync( + internal static async ValueTask FindFactoryAsync( Stream stream, CancellationToken cancellationToken = default ) From e6a179bdb5be7ff1aae0c121408b0743f0b3b343 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 15:33:57 +0000 Subject: [PATCH 07/74] fmt --- src/SharpCompress/Archives/IWritableArchiveExtensions.cs | 6 +++++- .../Archives/IWritableAsyncArchiveExtensions.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index f349f06d..9f249837 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -17,7 +17,11 @@ public static class IWritableArchiveExtensions using (writableArchive.PauseEntryRebuilding()) { foreach ( - var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, searchOption) + var filePath in Directory.EnumerateFiles( + directoryPath, + searchPattern, + searchOption + ) ) { var fileInfo = new FileInfo(filePath); diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 72110966..07a37945 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -19,7 +19,11 @@ public static class IWritableAsyncArchiveExtensions using (writableArchive.PauseEntryRebuilding()) { foreach ( - var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, searchOption) + var filePath in Directory.EnumerateFiles( + directoryPath, + searchPattern, + searchOption + ) ) { var fileInfo = new FileInfo(filePath); From b05a16d007f3115f50131e7bedba30f001108f71 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 6 Mar 2026 15:39:07 +0000 Subject: [PATCH 08/74] oops, wrong change made --- src/SharpCompress/Archives/IWritableArchiveExtensions.cs | 2 +- src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 9f249837..f9f35018 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -26,7 +26,7 @@ public static class IWritableArchiveExtensions { var fileInfo = new FileInfo(filePath); writableArchive.AddEntry( - Path.GetFileName(filePath), + filePath.Substring(directoryPath.Length), fileInfo.OpenRead(), true, fileInfo.Length, diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 07a37945..dcfee164 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -29,7 +29,7 @@ public static class IWritableAsyncArchiveExtensions var fileInfo = new FileInfo(filePath); await writableArchive .AddEntryAsync( - Path.GetFileName(filePath), + filePath.Substring(directoryPath.Length), fileInfo.OpenRead(), true, fileInfo.Length, From 265d99095bf9f45dd9b2d64bfa9c2cc400b9e1e5 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Mar 2026 13:19:57 -0400 Subject: [PATCH 09/74] Fix three BLAKE2sp correctness bugs and eliminate allocations in hot path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix off-by-one in Update(BLAKE2SP): pos was masked with 448 (64×7) instead of 511 (64×8−1), causing incorrect leaf assignment when update chunks are not multiples of 64 bytes. This produced wrong hashes whenever streaming reads didn't align to 64-byte boundaries. 2. Fix double-finalization when Read() is called again after returning 0. Final() was called unconditionally on each EOF read, re-running compression on an already-finalized state and corrupting the hash. EnsureHash() guards with a null-check and is idempotent; both sync and async Read paths share the fix. _blake2sp is set to null after finalization so any erroneous post-final calls fail fast rather than silently corrupting state. 3. Fix false-positive CRC check when stream is not fully drained. _hash was initialized to fileHeader.FileCrc in the constructor, so GetCrc() returned the expected CRC rather than the computed one if the stream was abandoned early. The check would then compare the expected hash against itself and always succeed, silently accepting a corrupt or incomplete file. _hash is now null until the stream is fully read; GetCrc() throws if called early rather than returning a misleading value. Additional changes: - Compress: stackalloc m/v arrays instead of heap; LE fast path via MemoryMarshal.Cast replaces 16 BitConverter.ToUInt32 calls per block - Final(BLAKE2S): write directly to Span, eliminating MemoryStream and BitConverter.GetBytes allocations; 8 leaf digests now stackalloc'd - Inner classes and fields: private, sealed, readonly where applicable --- .../Rar/RarBLAKE2spStream.Async.cs | 13 +- .../Compressors/Rar/RarBLAKE2spStream.cs | 241 ++++++++++-------- 2 files changed, 135 insertions(+), 119 deletions(-) diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs index 6b44acb3..351ad688 100644 --- a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; @@ -32,12 +31,12 @@ internal partial class RarBLAKE2spStream : RarStream .ConfigureAwait(false); if (result != 0) { - Update(_blake2sp, new ReadOnlySpan(buffer, offset, result), result); + Update(_blake2sp!, new ReadOnlySpan(buffer, offset, result)); } else { - _hash = Final(_blake2sp); - if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0) + EnsureHash(); + if (!disableCRCCheck && !GetCrc().SequenceEqual(readStream.CurrentCrc) && count != 0) { // NOTE: we use the last FileHeader in a multipart volume to check CRC throw new InvalidFormatException("file crc mismatch"); @@ -56,14 +55,14 @@ internal partial class RarBLAKE2spStream : RarStream var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); if (result != 0) { - Update(_blake2sp, buffer.Span.Slice(0, result), result); + Update(_blake2sp!, buffer.Span.Slice(0, result)); } else { - _hash = Final(_blake2sp); + EnsureHash(); if ( !disableCRCCheck - && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) + && !GetCrc().SequenceEqual(readStream.CurrentCrc) && buffer.Length != 0 ) { diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs index 41ae7a49..cbf35b57 100644 --- a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs @@ -1,8 +1,5 @@ using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using SharpCompress.Common; using SharpCompress.Common.Rar.Headers; @@ -13,14 +10,14 @@ internal partial class RarBLAKE2spStream : RarStream private readonly MultiVolumeReadOnlyStreamBase readStream; private readonly bool disableCRCCheck; - const uint BLAKE2S_NUM_ROUNDS = 10; - const uint BLAKE2S_FINAL_FLAG = (~(uint)0); - const int BLAKE2S_BLOCK_SIZE = 64; - const int BLAKE2S_DIGEST_SIZE = 32; - const int BLAKE2SP_PARALLEL_DEGREE = 8; - const uint BLAKE2S_INIT_IV_SIZE = 8; + private const int BLAKE2S_NUM_ROUNDS = 10; + private const uint BLAKE2S_FINAL_FLAG = ~(uint)0; + private const int BLAKE2S_BLOCK_SIZE = 64; + private const int BLAKE2S_DIGEST_SIZE = 32; + private const int BLAKE2SP_PARALLEL_DEGREE = 8; + private const int BLAKE2S_INIT_IV_SIZE = 8; - static readonly UInt32[] k_BLAKE2S_IV = + private static readonly uint[] k_BLAKE2S_IV = { 0x6A09E667U, 0xBB67AE85U, @@ -32,7 +29,7 @@ internal partial class RarBLAKE2spStream : RarStream 0x5BE0CD19U, }; - static readonly byte[][] k_BLAKE2S_Sigma = + private static readonly byte[][] k_BLAKE2S_Sigma = { new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, new byte[] { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, @@ -46,14 +43,14 @@ internal partial class RarBLAKE2spStream : RarStream new byte[] { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 }, }; - internal class BLAKE2S + private sealed class BLAKE2S { - internal UInt32[] h; - internal UInt32[] t; - internal UInt32[] f; - internal byte[] b; + internal readonly uint[] h; + internal readonly uint[] t; + internal readonly uint[] f; + internal readonly byte[] b; internal int bufferPosition; - internal UInt32 lastNodeFlag; + internal uint lastNodeFlag; public BLAKE2S() { @@ -64,9 +61,9 @@ internal partial class RarBLAKE2spStream : RarStream } }; - internal class BLAKE2SP + private sealed class BLAKE2SP { - internal BLAKE2S[] S; + internal readonly BLAKE2S[] S; internal int bufferPosition; public BLAKE2SP() @@ -79,9 +76,8 @@ internal partial class RarBLAKE2spStream : RarStream } }; - BLAKE2SP _blake2sp; - - byte[] _hash = []; + private BLAKE2SP? _blake2sp; + private byte[]? _hash; private RarBLAKE2spStream( IRarUnpack unpack, @@ -92,10 +88,9 @@ internal partial class RarBLAKE2spStream : RarStream { this.readStream = readStream; + // TODO: rar uses a modified hash xor'ed with encryption key? disableCRCCheck = fileHeader.IsEncrypted; - _hash = fileHeader.FileCrc.NotNull(); - _blake2sp = new BLAKE2SP(); - ResetCrc(); + this._blake2sp = CreateBlake2sp(); } public static RarBLAKE2spStream Create( @@ -111,19 +106,15 @@ internal partial class RarBLAKE2spStream : RarStream // Async methods moved to RarBLAKE2spStream.Async.cs - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - } + public byte[] GetCrc() => + this._hash + ?? throw new InvalidOperationException( + "hash not computed, has the stream been fully drained?" + ); - public byte[] GetCrc() => _hash; - - internal void ResetCrc(BLAKE2S hash) + private static void ResetCrc(BLAKE2S hash) { - for (UInt32 j = 0; j < BLAKE2S_INIT_IV_SIZE; j++) - { - hash.h[j] = k_BLAKE2S_IV[j]; - } + k_BLAKE2S_IV.AsSpan().CopyTo(hash.h); hash.t[0] = 0; hash.t[1] = 0; hash.f[0] = 0; @@ -132,14 +123,14 @@ internal partial class RarBLAKE2spStream : RarStream hash.lastNodeFlag = 0; } - internal void G( - ref UInt32[] m, - ref byte[] sigma, + private static void G( + Span m, + byte[] sigma, int i, - ref UInt32 a, - ref UInt32 b, - ref UInt32 c, - ref UInt32 d + ref uint a, + ref uint b, + ref uint c, + ref uint d ) { a += b + m[sigma[2 * i]]; @@ -157,16 +148,22 @@ internal partial class RarBLAKE2spStream : RarStream b = (b >> 7) | (b << 25); } - internal void Compress(BLAKE2S hash) + private static void Compress(BLAKE2S hash) { - var m = new UInt32[16]; - var v = new UInt32[16]; - - for (var i = 0; i < 16; i++) + Span m = stackalloc uint[16]; + if (BitConverter.IsLittleEndian) { - m[i] = BitConverter.ToUInt32(hash.b, i * 4); + MemoryMarshal.Cast(hash.b).CopyTo(m); + } + else + { + for (var i = 0; i < 16; i++) + { + m[i] = BitConverter.ToUInt32(hash.b, i * 4); + } } + Span v = stackalloc uint[16]; for (var i = 0; i < 8; i++) { v[i] = hash.h[i]; @@ -184,16 +181,15 @@ internal partial class RarBLAKE2spStream : RarStream for (var r = 0; r < BLAKE2S_NUM_ROUNDS; r++) { - ref byte[] sigma = ref k_BLAKE2S_Sigma[r]; - - G(ref m, ref sigma, 0, ref v[0], ref v[4], ref v[8], ref v[12]); - G(ref m, ref sigma, 1, ref v[1], ref v[5], ref v[9], ref v[13]); - G(ref m, ref sigma, 2, ref v[2], ref v[6], ref v[10], ref v[14]); - G(ref m, ref sigma, 3, ref v[3], ref v[7], ref v[11], ref v[15]); - G(ref m, ref sigma, 4, ref v[0], ref v[5], ref v[10], ref v[15]); - G(ref m, ref sigma, 5, ref v[1], ref v[6], ref v[11], ref v[12]); - G(ref m, ref sigma, 6, ref v[2], ref v[7], ref v[8], ref v[13]); - G(ref m, ref sigma, 7, ref v[3], ref v[4], ref v[9], ref v[14]); + var sigma = k_BLAKE2S_Sigma[r]; + G(m, sigma, 0, ref v[0], ref v[4], ref v[8], ref v[12]); + G(m, sigma, 1, ref v[1], ref v[5], ref v[9], ref v[13]); + G(m, sigma, 2, ref v[2], ref v[6], ref v[10], ref v[14]); + G(m, sigma, 3, ref v[3], ref v[7], ref v[11], ref v[15]); + G(m, sigma, 4, ref v[0], ref v[5], ref v[10], ref v[15]); + G(m, sigma, 5, ref v[1], ref v[6], ref v[11], ref v[12]); + G(m, sigma, 6, ref v[2], ref v[7], ref v[8], ref v[13]); + G(m, sigma, 7, ref v[3], ref v[4], ref v[9], ref v[14]); } for (var i = 0; i < 8; i++) @@ -202,103 +198,124 @@ internal partial class RarBLAKE2spStream : RarStream } } - internal void Update(BLAKE2S hash, ReadOnlySpan data, int size) + private static void Update(BLAKE2S hash, ReadOnlySpan data) { - var i = 0; - while (size != 0) + while (data.Length != 0) { var pos = hash.bufferPosition; - var reminder = BLAKE2S_BLOCK_SIZE - pos; - - if (size <= reminder) + var chunkSize = BLAKE2S_BLOCK_SIZE - pos; + if (data.Length <= chunkSize) { - data.Slice(i, size).CopyTo(new Span(hash.b, pos, size)); - hash.bufferPosition += size; + data.CopyTo(hash.b.AsSpan(pos)); + hash.bufferPosition += data.Length; return; } - data.Slice(i, reminder).CopyTo(new Span(hash.b, pos, reminder)); + data.Slice(0, chunkSize).CopyTo(hash.b.AsSpan(pos)); hash.t[0] += BLAKE2S_BLOCK_SIZE; hash.t[1] += hash.t[0] < BLAKE2S_BLOCK_SIZE ? 1U : 0U; Compress(hash); hash.bufferPosition = 0; - i += reminder; - size -= reminder; + data = data.Slice(chunkSize); } } - internal byte[] Final(BLAKE2S hash) + private static void Final(BLAKE2S hash, Span output) { hash.t[0] += (uint)hash.bufferPosition; hash.t[1] += hash.t[0] < hash.bufferPosition ? 1U : 0U; hash.f[0] = BLAKE2S_FINAL_FLAG; hash.f[1] = hash.lastNodeFlag; - Array.Clear(hash.b, hash.bufferPosition, BLAKE2S_BLOCK_SIZE - hash.bufferPosition); + hash.b.AsSpan(hash.bufferPosition).Clear(); Compress(hash); - var mem = new MemoryStream(); - - for (var i = 0; i < 8; i++) + if (BitConverter.IsLittleEndian) { - mem.Write(BitConverter.GetBytes(hash.h[i]), 0, 4); + MemoryMarshal.Cast(hash.h).CopyTo(output); + } + else + { + for (var i = 0; i < 8; i++) + { + var v = hash.h[i]; + output[i * 4] = (byte)v; + output[i * 4 + 1] = (byte)(v >> 8); + output[i * 4 + 2] = (byte)(v >> 16); + output[i * 4 + 3] = (byte)(v >> 24); + } } - - return mem.ToArray(); } - public void ResetCrc() + private static BLAKE2SP CreateBlake2sp() { - _blake2sp.bufferPosition = 0; + var blake2sp = new BLAKE2SP(); - for (UInt32 i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++) + for (var i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++) { - _blake2sp.S[i].bufferPosition = 0; - ResetCrc(_blake2sp.S[i]); - _blake2sp.S[i].h[0] ^= (BLAKE2S_DIGEST_SIZE | BLAKE2SP_PARALLEL_DEGREE << 16 | 2 << 24); - _blake2sp.S[i].h[2] ^= i; - _blake2sp.S[i].h[3] ^= (BLAKE2S_DIGEST_SIZE << 24); + var blake2S = blake2sp.S[i]; + ResetCrc(blake2S); + + var h = blake2S.h; + // word[0]: digest_length | (fanout<<16) | (depth<<24) + h[0] ^= BLAKE2S_DIGEST_SIZE | (BLAKE2SP_PARALLEL_DEGREE << 16) | (2 << 24); + // word[2]: node_offset = leaf index + h[2] ^= (uint)i; + // word[3]: inner_length in bits 24-31 + h[3] ^= BLAKE2S_DIGEST_SIZE << 24; } - _blake2sp.S[BLAKE2SP_PARALLEL_DEGREE - 1].lastNodeFlag = BLAKE2S_FINAL_FLAG; + blake2sp.S[BLAKE2SP_PARALLEL_DEGREE - 1].lastNodeFlag = BLAKE2S_FINAL_FLAG; + return blake2sp; } - internal void Update(BLAKE2SP hash, ReadOnlySpan data, int size) + private static void Update(BLAKE2SP hash, ReadOnlySpan data) { - var i = 0; var pos = hash.bufferPosition; - while (size != 0) + while (data.Length != 0) { var index = pos / BLAKE2S_BLOCK_SIZE; - var reminder = BLAKE2S_BLOCK_SIZE - (pos & (BLAKE2S_BLOCK_SIZE - 1)); - if (reminder > size) + var chunkSize = BLAKE2S_BLOCK_SIZE - (pos & (BLAKE2S_BLOCK_SIZE - 1)); + if (chunkSize > data.Length) { - reminder = size; + chunkSize = data.Length; } - // Update(hash.S[index], data, size); - Update(hash.S[index], data.Slice(i, reminder), reminder); - size -= reminder; - i += reminder; - pos += reminder; - pos &= (BLAKE2S_BLOCK_SIZE * (BLAKE2SP_PARALLEL_DEGREE - 1)); + Update(hash.S[index], data.Slice(0, chunkSize)); + data = data.Slice(chunkSize); + pos = (pos + chunkSize) & (BLAKE2S_BLOCK_SIZE * BLAKE2SP_PARALLEL_DEGREE - 1); } hash.bufferPosition = pos; } - internal byte[] Final(BLAKE2SP hash) + private static byte[] Final(BLAKE2SP blake2sp) { - var h = new BLAKE2S(); + var blake2s = new BLAKE2S(); + ResetCrc(blake2s); - ResetCrc(h); - h.h[0] ^= (BLAKE2S_DIGEST_SIZE | BLAKE2SP_PARALLEL_DEGREE << 16 | 2 << 24); - h.h[3] ^= (1 << 16 | BLAKE2S_DIGEST_SIZE << 24); - h.lastNodeFlag = BLAKE2S_FINAL_FLAG; + var h = blake2s.h; + // word[0]: digest_length | (fanout<<16) | (depth<<24) — same as leaves + h[0] ^= BLAKE2S_DIGEST_SIZE | (BLAKE2SP_PARALLEL_DEGREE << 16) | (2 << 24); + // word[3]: node_depth=1 (bits 16-23), inner_length=32 (bits 24-31) + h[3] ^= (1 << 16) | (BLAKE2S_DIGEST_SIZE << 24); + blake2s.lastNodeFlag = BLAKE2S_FINAL_FLAG; + Span digest = stackalloc byte[BLAKE2S_DIGEST_SIZE]; for (var i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++) { - var digest = Final(_blake2sp.S[i]); - Update(h, digest, BLAKE2S_DIGEST_SIZE); + Final(blake2sp.S[i], digest); + Update(blake2s, digest); } - return Final(h); + Final(blake2s, digest); + return digest.ToArray(); + } + + private void EnsureHash() + { + if (this._hash == null) + { + this._hash = Final(this._blake2sp!); + // prevent incorrect usage past hash finality by failing fast + this._blake2sp = null; + } } public override int Read(byte[] buffer, int offset, int count) @@ -306,12 +323,12 @@ internal partial class RarBLAKE2spStream : RarStream var result = base.Read(buffer, offset, count); if (result != 0) { - Update(_blake2sp, new ReadOnlySpan(buffer, offset, result), result); + Update(this._blake2sp!, new ReadOnlySpan(buffer, offset, result)); } else { - _hash = Final(_blake2sp); - if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0) + EnsureHash(); + if (!disableCRCCheck && !GetCrc().SequenceEqual(readStream.CurrentCrc) && count != 0) { // NOTE: we use the last FileHeader in a multipart volume to check CRC throw new InvalidFormatException("file crc mismatch"); From 817bd2a95e0355e912e6a256d2ce8e3166cc51cc Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 4 Apr 2026 10:44:23 +0100 Subject: [PATCH 10/74] First pass of implementation --- src/SharpCompress/IO/PooledMemoryStream.cs | 992 ++++++++++++++++++ src/SharpCompress/ThrowHelper.cs | 9 + .../Streams/PooledMemoryStreamTests.cs | 149 +++ 3 files changed, 1150 insertions(+) create mode 100644 src/SharpCompress/IO/PooledMemoryStream.cs create mode 100644 tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs new file mode 100644 index 00000000..58bc748f --- /dev/null +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -0,0 +1,992 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// MemoryStream implementation backed by pooled byte arrays. +/// +public sealed class PooledMemoryStream : MemoryStream +{ + public const int DefaultBlockSize = 81920; + + private const int MaxStreamLength = int.MaxValue; + + private enum StorageMode + { + Segmented, + Contiguous, + } + + private readonly ArrayPool _arrayPool; + private readonly int _blockSize; + + private readonly List _detachedExposedBuffers = new(); + + private StorageMode _mode; + private List? _blocks; + private byte[]? _contiguousBuffer; + + private bool _contiguousBufferExposed; + private bool _isOpen; + private bool _writable; + private bool _expandable; + private bool _exposable; + + private int _origin; + private int _position; + private int _length; + private int _capacity; + private int _allocatedCapacity; + + public PooledMemoryStream() + : this(0) { } + + public PooledMemoryStream(int capacity) + : this(capacity, DefaultBlockSize, ArrayPool.Shared) { } + + public PooledMemoryStream(int capacity, int blockSize) + : this(capacity, blockSize, ArrayPool.Shared) { } + + public PooledMemoryStream(int capacity, int blockSize, ArrayPool arrayPool) + { + ThrowHelper.ThrowIfNull(arrayPool, nameof(arrayPool)); + ThrowHelper.ThrowIfNegative(capacity, nameof(capacity)); + ThrowHelper.ThrowIfNegativeOrZero(blockSize, nameof(blockSize)); + + _arrayPool = arrayPool; + _blockSize = blockSize; + + _mode = StorageMode.Segmented; + _blocks = new List(); + _isOpen = true; + _writable = true; + _expandable = true; + _exposable = true; + _origin = 0; + _position = 0; + _length = 0; + _capacity = capacity; + + EnsureSegmentedAllocated(capacity); + } + + public PooledMemoryStream(byte[] buffer) + : this(buffer, writable: true) { } + + public PooledMemoryStream(byte[] buffer, bool writable) + : this(buffer, 0, buffer?.Length ?? 0, writable, publiclyVisible: false) { } + + public PooledMemoryStream(byte[] buffer, int index, int count) + : this(buffer, index, count, writable: true, publiclyVisible: false) { } + + public PooledMemoryStream(byte[] buffer, int index, int count, bool writable) + : this(buffer, index, count, writable, publiclyVisible: false) { } + + public PooledMemoryStream( + byte[] buffer, + int index, + int count, + bool writable, + bool publiclyVisible + ) + { + ThrowHelper.ThrowIfNull(buffer, nameof(buffer)); + ThrowHelper.ThrowIfNegative(index, nameof(index)); + ThrowHelper.ThrowIfNegative(count, nameof(count)); + if (buffer.Length - index < count) + { + throw new ArgumentException("Offset and length are out of bounds."); + } + + _arrayPool = ArrayPool.Shared; + _blockSize = DefaultBlockSize; + + _mode = StorageMode.Segmented; + _blocks = new List(); + _origin = 0; + _position = 0; + _length = count; + _capacity = count; + _writable = writable; + _expandable = false; + _exposable = publiclyVisible; + _isOpen = true; + + EnsureSegmentedAllocated(_capacity); + if (count > 0) + { + CopyToSegmented(0, buffer, index, count); + } + } + + public override bool CanRead => _isOpen; + + public override bool CanSeek => _isOpen; + + public override bool CanWrite => _writable; + + public override long Length + { + get + { + EnsureNotClosed(); + return _length - _origin; + } + } + + public override long Position + { + get + { + EnsureNotClosed(); + return _position - _origin; + } + set + { + ThrowHelper.ThrowIfNegative(value, nameof(value)); + EnsureNotClosed(); + ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength - _origin, nameof(value)); + + _position = _origin + (int)value; + } + } + + public override int Capacity + { + get + { + EnsureNotClosed(); + return _capacity - _origin; + } + set + { + if (value < Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + EnsureNotClosed(); + + if (!_expandable && value != Capacity) + { + throw new NotSupportedException("Memory stream is not expandable."); + } + + var target = _origin + value; + if (target == _capacity) + { + return; + } + + SetCapacityAbsolute(target); + } + } + + public override void Flush() { } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + public override long Seek(long offset, SeekOrigin loc) + { + EnsureNotClosed(); + + var anchor = loc switch + { + SeekOrigin.Begin => _origin, + SeekOrigin.Current => _position, + SeekOrigin.End => _length, + _ => throw new ArgumentException("Invalid seek origin.", nameof(loc)), + }; + + var target = anchor + offset; + if (target < _origin) + { + throw new IOException("Attempted to seek before the beginning of the stream."); + } + + if (target > MaxStreamLength) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + _position = (int)target; + return _position - _origin; + } + + public override void SetLength(long value) + { + ThrowHelper.ThrowIfNegative(value, nameof(value)); + ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value)); + + EnsureWritable(); + + var newLength = _origin + (int)value; + if (newLength > _capacity) + { + EnsureCapacityForAppend(newLength); + } + + if (newLength > _length) + { + ClearRange(_length, newLength - _length); + } + + _length = newLength; + if (_position > newLength) + { + _position = newLength; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValidateReadWriteBufferArguments(buffer, offset, count); + EnsureNotClosed(); + + var available = _length - _position; + if (available <= 0) + { + return 0; + } + + if (count > available) + { + count = available; + } + + switch (_mode) + { + case StorageMode.Contiguous: + Buffer.BlockCopy(_contiguousBuffer!, _position, buffer, offset, count); + break; + case StorageMode.Segmented: + CopyFromSegmented(_position, buffer, offset, count); + break; + } + + _position += count; + return count; + } + + public override int ReadByte() + { + EnsureNotClosed(); + if (_position >= _length) + { + return -1; + } + + byte value; + switch (_mode) + { + case StorageMode.Contiguous: + value = _contiguousBuffer![_position]; + break; + default: + { + var blockIndex = _position / _blockSize; + var blockOffset = _position % _blockSize; + value = _blocks![blockIndex][blockOffset]; + break; + } + } + + _position++; + return value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + ValidateReadWriteBufferArguments(buffer, offset, count); + EnsureWritable(); + + if (count == 0) + { + return; + } + + var endPosition = _position + count; + if (endPosition < 0) + { + throw new IOException("Stream is too long."); + } + + if (endPosition > _capacity) + { + EnsureCapacityForAppend(endPosition); + } + + if (_position > _length) + { + ClearRange(_length, _position - _length); + } + + switch (_mode) + { + case StorageMode.Contiguous: + Buffer.BlockCopy(buffer, offset, _contiguousBuffer!, _position, count); + break; + case StorageMode.Segmented: + CopyToSegmented(_position, buffer, offset, count); + break; + } + + _position = endPosition; + if (_position > _length) + { + _length = _position; + } + } + + public override void WriteByte(byte value) + { + EnsureWritable(); + + var endPosition = _position + 1; + if (endPosition < 0) + { + throw new IOException("Stream is too long."); + } + + if (endPosition > _capacity) + { + EnsureCapacityForAppend(endPosition); + } + + if (_position > _length) + { + ClearRange(_length, _position - _length); + } + + switch (_mode) + { + case StorageMode.Contiguous: + _contiguousBuffer![_position] = value; + break; + default: + { + var blockIndex = _position / _blockSize; + var blockOffset = _position % _blockSize; + _blocks![blockIndex][blockOffset] = value; + break; + } + } + + _position = endPosition; + if (_position > _length) + { + _length = _position; + } + } + + public override byte[] GetBuffer() + { + EnsureNotClosed(); + if (!_exposable) + { + throw new UnauthorizedAccessException("Memory stream buffer is not publicly visible."); + } + + EnsureContiguous(); + _contiguousBufferExposed = true; + return _contiguousBuffer!; + } + + public override bool TryGetBuffer(out ArraySegment buffer) + { + if (!_exposable) + { + buffer = default; + return false; + } + + EnsureNotClosed(); + + EnsureContiguous(); + _contiguousBufferExposed = true; + buffer = new ArraySegment(_contiguousBuffer!, 0, _length); + return true; + } + + public override byte[] ToArray() + { + EnsureNotClosed(); + + var count = _length - _origin; + if (count == 0) + { + return Array.Empty(); + } + + var copy = new byte[count]; + switch (_mode) + { + case StorageMode.Contiguous: + Buffer.BlockCopy(_contiguousBuffer!, _origin, copy, 0, count); + break; + case StorageMode.Segmented: + CopyFromSegmented(_origin, copy, 0, count); + break; + } + + return copy; + } + + public override void WriteTo(Stream stream) + { + ThrowHelper.ThrowIfNull(stream, nameof(stream)); + EnsureNotClosed(); + + var count = _length - _origin; + if (count == 0) + { + return; + } + + switch (_mode) + { + case StorageMode.Contiguous: + stream.Write(_contiguousBuffer!, _origin, count); + break; + case StorageMode.Segmented: + { + var position = _origin; + var remaining = count; + while (remaining > 0) + { + var blockIndex = position / _blockSize; + var blockOffset = position % _blockSize; + var toWrite = Math.Min(remaining, _blockSize - blockOffset); + stream.Write(_blocks![blockIndex], blockOffset, toWrite); + position += toWrite; + remaining -= toWrite; + } + + break; + } + } + } + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + +#if !LEGACY_DOTNET + public override int Read(Span buffer) + { + EnsureNotClosed(); + + var available = _length - _position; + if (available <= 0) + { + return 0; + } + + var count = Math.Min(available, buffer.Length); + switch (_mode) + { + case StorageMode.Contiguous: + _contiguousBuffer.AsSpan(_position, count).CopyTo(buffer); + break; + case StorageMode.Segmented: + { + var sourcePosition = _position; + var destinationOffset = 0; + var remaining = count; + + while (remaining > 0) + { + var blockIndex = sourcePosition / _blockSize; + var blockOffset = sourcePosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); + _blocks! + [blockIndex] + .AsSpan(blockOffset, toCopy) + .CopyTo(buffer.Slice(destinationOffset, toCopy)); + + sourcePosition += toCopy; + destinationOffset += toCopy; + remaining -= toCopy; + } + + break; + } + } + + _position += count; + return count; + } + + public override void Write(ReadOnlySpan buffer) + { + EnsureWritable(); + if (buffer.Length == 0) + { + return; + } + + var endPosition = _position + buffer.Length; + if (endPosition < 0) + { + throw new IOException("Stream is too long."); + } + + if (endPosition > _capacity) + { + EnsureCapacityForAppend(endPosition); + } + + if (_position > _length) + { + ClearRange(_length, _position - _length); + } + + switch (_mode) + { + case StorageMode.Contiguous: + buffer.CopyTo(_contiguousBuffer.AsSpan(_position, buffer.Length)); + break; + case StorageMode.Segmented: + { + var sourceOffset = 0; + var destinationPosition = _position; + var remaining = buffer.Length; + + while (remaining > 0) + { + var blockIndex = destinationPosition / _blockSize; + var blockOffset = destinationPosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); + + buffer + .Slice(sourceOffset, toCopy) + .CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy)); + + sourceOffset += toCopy; + destinationPosition += toCopy; + remaining -= toCopy; + } + + break; + } + } + + _position = endPosition; + if (_position > _length) + { + _length = _position; + } + } + + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + return ValueTask.FromResult(Read(buffer.Span)); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + + public override ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + Write(buffer.Span); + return ValueTask.CompletedTask; + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } +#endif + + protected override void Dispose(bool disposing) + { + if (_isOpen) + { + _isOpen = false; + _writable = false; + _expandable = false; + + if (disposing) + { + ReturnPooledBuffers(); + } + } + + base.Dispose(disposing); + } + + private void EnsureNotClosed() + { + if (!_isOpen) + { + throw new ObjectDisposedException(nameof(PooledMemoryStream)); + } + } + + private void EnsureWritable() + { + EnsureNotClosed(); + if (!_writable) + { + throw new NotSupportedException("Stream does not support writing."); + } + } + + private void EnsureCapacityForAppend(int requiredLength) + { + if (requiredLength < 0) + { + throw new IOException("Stream is too long."); + } + + if (requiredLength <= _capacity) + { + return; + } + + if (!_expandable) + { + throw new NotSupportedException("Memory stream is not expandable."); + } + + var nextCapacity = RoundUpToBlockBoundary(requiredLength); + SetCapacityAbsolute(nextCapacity); + } + + private void SetCapacityAbsolute(int newCapacity) + { + ThrowHelper.ThrowIfLessThan(newCapacity, _length, nameof(newCapacity)); + + switch (_mode) + { + case StorageMode.Contiguous: + if (newCapacity > _allocatedCapacity) + { + DemoteContiguousToSegmented(); + EnsureSegmentedAllocated(newCapacity); + } + break; + + case StorageMode.Segmented: + EnsureSegmentedAllocated(newCapacity); + break; + } + + _capacity = newCapacity; + if (_length > _capacity) + { + _length = _capacity; + } + if (_position > _capacity) + { + _position = _capacity; + } + } + + private void EnsureContiguous() + { + if (_mode == StorageMode.Contiguous) + { + return; + } + + var requested = Math.Max(_capacity, 1); + var contiguous = _arrayPool.Rent(requested); + if (_length > 0) + { + CopyFromSegmented(0, contiguous, 0, _length); + } + + ReturnSegmentedBlocks(); + + _mode = StorageMode.Contiguous; + _contiguousBuffer = contiguous; + _contiguousBufferExposed = false; + _allocatedCapacity = contiguous.Length; + } + + private void DemoteContiguousToSegmented() + { + var contiguous = _contiguousBuffer; + if (contiguous is null) + { + return; + } + + var requiredCapacity = Math.Max(_capacity, _length); + _mode = StorageMode.Segmented; + _blocks = new List(); + _contiguousBuffer = null; + EnsureSegmentedAllocated(requiredCapacity); + + if (_length > 0) + { + CopyToSegmented(0, contiguous, 0, _length); + } + + if (_contiguousBufferExposed) + { + _detachedExposedBuffers.Add(contiguous); + _contiguousBufferExposed = false; + } + else + { + _arrayPool.Return(contiguous); + } + } + + private void EnsureSegmentedAllocated(int capacity) + { + if (_mode != StorageMode.Segmented) + { + throw new InvalidOperationException( + "Segmented allocation requested while not in segmented mode." + ); + } + + var requiredAllocated = RoundUpToBlockBoundary(capacity); + var requiredBlocks = requiredAllocated == 0 ? 0 : requiredAllocated / _blockSize; + + _blocks ??= new List(); + + while (_blocks.Count < requiredBlocks) + { + _blocks.Add(_arrayPool.Rent(_blockSize)); + } + + while (_blocks.Count > requiredBlocks) + { + var index = _blocks.Count - 1; + var block = _blocks[index]; + _blocks.RemoveAt(index); + _arrayPool.Return(block); + } + + _allocatedCapacity = requiredAllocated; + } + + private int RoundUpToBlockBoundary(int value) + { + if (value <= 0) + { + return 0; + } + + var rounded = ((long)value + _blockSize - 1) / _blockSize * _blockSize; + if (rounded > MaxStreamLength) + { + throw new IOException("Stream is too long."); + } + + return (int)rounded; + } + + private void ClearRange(int absoluteStart, int count) + { + if (count <= 0) + { + return; + } + + switch (_mode) + { + case StorageMode.Contiguous: + Array.Clear(_contiguousBuffer!, absoluteStart, count); + break; + case StorageMode.Segmented: + { + var position = absoluteStart; + var remaining = count; + while (remaining > 0) + { + var blockIndex = position / _blockSize; + var blockOffset = position % _blockSize; + var toClear = Math.Min(remaining, _blockSize - blockOffset); + Array.Clear(_blocks![blockIndex], blockOffset, toClear); + position += toClear; + remaining -= toClear; + } + + break; + } + } + } + + private void CopyFromSegmented( + int absoluteSourcePosition, + byte[] destination, + int offset, + int count + ) + { + var sourcePosition = absoluteSourcePosition; + var destinationOffset = offset; + var remaining = count; + + while (remaining > 0) + { + var blockIndex = sourcePosition / _blockSize; + var blockOffset = sourcePosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); + Buffer.BlockCopy( + _blocks![blockIndex], + blockOffset, + destination, + destinationOffset, + toCopy + ); + + sourcePosition += toCopy; + destinationOffset += toCopy; + remaining -= toCopy; + } + } + + private void CopyToSegmented( + int absoluteDestinationPosition, + byte[] source, + int offset, + int count + ) + { + var sourceOffset = offset; + var destinationPosition = absoluteDestinationPosition; + var remaining = count; + + while (remaining > 0) + { + var blockIndex = destinationPosition / _blockSize; + var blockOffset = destinationPosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); + Buffer.BlockCopy(source, sourceOffset, _blocks![blockIndex], blockOffset, toCopy); + + sourceOffset += toCopy; + destinationPosition += toCopy; + remaining -= toCopy; + } + } + + private void ReturnSegmentedBlocks() + { + if (_blocks is null) + { + return; + } + + for (var i = 0; i < _blocks.Count; i++) + { + _arrayPool.Return(_blocks[i]); + } + + _blocks.Clear(); + } + + private void ReturnPooledBuffers() + { + if (_mode == StorageMode.Segmented) + { + ReturnSegmentedBlocks(); + _blocks = null; + } + + if (_mode == StorageMode.Contiguous && _contiguousBuffer is not null) + { + _arrayPool.Return(_contiguousBuffer); + _contiguousBuffer = null; + } + + for (var i = 0; i < _detachedExposedBuffers.Count; i++) + { + _arrayPool.Return(_detachedExposedBuffers[i]); + } + + _detachedExposedBuffers.Clear(); + } + + private static void ValidateReadWriteBufferArguments(byte[] buffer, int offset, int count) + { + ThrowHelper.ThrowIfNull(buffer, nameof(buffer)); + ThrowHelper.ThrowIfNegative(offset, nameof(offset)); + ThrowHelper.ThrowIfNegative(count, nameof(count)); + if (buffer.Length - offset < count) + { + throw new ArgumentException("Offset and length are out of bounds."); + } + } +} diff --git a/src/SharpCompress/ThrowHelper.cs b/src/SharpCompress/ThrowHelper.cs index 9d598107..c202d87c 100644 --- a/src/SharpCompress/ThrowHelper.cs +++ b/src/SharpCompress/ThrowHelper.cs @@ -60,6 +60,15 @@ internal static class ThrowHelper } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfGreaterThan(long value, long other, string? paramName = null) + { + if (value > other) + { + throw new ArgumentOutOfRangeException(paramName); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ThrowIfGreaterThan(uint value, uint other, string? paramName = null) { diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs new file mode 100644 index 00000000..28af7f1c --- /dev/null +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Buffers; +using System.IO; +using System.Linq; +using SharpCompress.IO; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class PooledMemoryStreamTests +{ + [Fact] + public void GrowsUsingFixedSizeBlocks() + { + var pool = new TrackingArrayPool(); + + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); + stream.Write(new byte[20], 0, 20); + + Assert.Equal(3, pool.RentRequests.Count); + Assert.All(pool.RentRequests, requested => Assert.Equal(8, requested)); + } + + [Fact] + public void DisposeReturnsRentedBlocksToPool() + { + var pool = new TrackingArrayPool(); + var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); + + stream.Write(new byte[17], 0, 17); + stream.Dispose(); + + Assert.Equal(pool.RentRequests.Count, pool.ReturnedLengths.Count); + Assert.All(pool.ReturnedLengths, length => Assert.Equal(8, length)); + } + + [Fact] + public void GetBufferPromotesToContiguousAndLaterGrowthUsesBlocks() + { + var pool = new TrackingArrayPool(); + + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); + stream.Write(new byte[10], 0, 10); + + var contiguous = stream.GetBuffer(); + Assert.Equal(16, contiguous.Length); + Assert.Contains(16, pool.RentRequests); + + stream.Position = stream.Length; + stream.Write(new byte[10], 0, 10); // grow to 20, forcing demotion + block growth + + var totalBlockRents = pool.RentRequests.Count(size => size == 8); + Assert.True(totalBlockRents >= 3); + } + + [Fact] + public void BufferConstructorCopiesDataAndIsNonExpandable() + { + var backing = Enumerable.Range(0, 10).Select(i => (byte)i).ToArray(); + using var stream = new PooledMemoryStream( + backing, + 2, + 4, + writable: true, + publiclyVisible: true + ); + + Assert.Equal(4, stream.Length); + Assert.Equal(4, stream.Capacity); + + backing[2] = 255; + Assert.Equal(new byte[] { 2, 3, 4, 5 }, stream.ToArray()); + + Assert.Throws(() => stream.Capacity = 5); + } + + [Fact] + public void BufferConstructorTryGetBufferRespectsVisibilityFlag() + { + var backing = new byte[10]; + using var hidden = new PooledMemoryStream( + backing, + 1, + 5, + writable: true, + publiclyVisible: false + ); + Assert.False(hidden.TryGetBuffer(out _)); + + using var visible = new PooledMemoryStream( + backing, + 1, + 5, + writable: true, + publiclyVisible: true + ); + Assert.True(visible.TryGetBuffer(out var segment)); + Assert.Equal(0, segment.Offset); + Assert.Equal(5, segment.Count); + } + + [Fact] + public void SetLengthExtendingClearsGap() + { + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); + stream.Position = 5; + stream.WriteByte(42); + stream.Position = 0; + + var data = stream.ToArray(); + Assert.Equal(6, data.Length); + Assert.Equal(0, data[0]); + Assert.Equal(0, data[4]); + Assert.Equal(42, data[5]); + } + + [Fact] + public void MethodsThrowAfterDispose() + { + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); + stream.WriteByte(1); + stream.Dispose(); + + Assert.Throws(() => stream.ReadByte()); + Assert.Throws(() => stream.ToArray()); + Assert.Throws(() => stream.GetBuffer()); + } + + private sealed class TrackingArrayPool : ArrayPool + { + public readonly System.Collections.Generic.List RentRequests = new(); + public readonly System.Collections.Generic.List ReturnedLengths = new(); + + public override byte[] Rent(int minimumLength) + { + RentRequests.Add(minimumLength); + return new byte[minimumLength]; + } + + public override void Return(byte[] array, bool clearArray = false) + { + ReturnedLengths.Add(array.Length); + if (clearArray) + { + Array.Clear(array, 0, array.Length); + } + } + } +} From 9db27dfc2b5833f0147c4a4bdd9693b5ce08e182 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 4 Apr 2026 10:53:12 +0100 Subject: [PATCH 11/74] fix build --- src/SharpCompress/packages.lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 7d5f0448..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From a26198a6b0488bd730bb942e3cf226ea9f0b0646 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 4 Apr 2026 11:04:51 +0100 Subject: [PATCH 12/74] use pooled memory streams for better performance --- src/SharpCompress/Archives/Tar/TarArchive.Async.cs | 2 +- src/SharpCompress/Archives/Tar/TarArchive.cs | 2 +- src/SharpCompress/Common/SevenZip/ArchiveReader.cs | 2 +- .../Common/SevenZip/SevenZipFilesInfo.cs | 3 ++- .../Compressors/LZMA/Lzma2EncoderStream.cs | 5 +++-- src/SharpCompress/Compressors/PPMd/PpmdStream.cs | 3 ++- .../Compressors/Squeezed/SqueezedStream.Async.cs | 7 ++++--- .../Compressors/Squeezed/SqueezedStream.cs | 5 +++-- src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs | 4 ++-- src/SharpCompress/packages.lock.json | 12 ++++++------ 10 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index ac584e1b..45e5b1ca 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -141,7 +141,7 @@ public partial class TarArchive using (var entryStream = entry.OpenEntryStream()) { - using var memoryStream = new MemoryStream(); + using var memoryStream = new PooledMemoryStream(); await entryStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; var bytes = memoryStream.ToArray(); diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index fdd8ac3a..44238549 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -151,7 +151,7 @@ public partial class TarArchive using (var entryStream = entry.OpenEntryStream()) { - using var memoryStream = new MemoryStream(); + using var memoryStream = new PooledMemoryStream(); entryStream.CopyTo(memoryStream, Constants.BufferSize); memoryStream.Position = 0; var bytes = memoryStream.ToArray(); diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs index 3a4e5622..b75d123c 100644 --- a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs @@ -1182,7 +1182,7 @@ internal partial class ArchiveReader } else { - _stream = new MemoryStream(); + _stream = new PooledMemoryStream(); } _rem = _db._files[index].Size; } diff --git a/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs b/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs index f5fd410e..9162474b 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipFilesInfo.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text; using SharpCompress.Compressors.LZMA.Utilities; +using SharpCompress.IO; namespace SharpCompress.Common.SevenZip; @@ -215,7 +216,7 @@ internal sealed class SevenZipFilesInfoWriter Action writeData ) { - using var dataStream = new MemoryStream(); + using var dataStream = new PooledMemoryStream(); writeData(dataStream); stream.WriteByte((byte)propertyId); diff --git a/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs index 1642be44..84bd3f00 100644 --- a/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA; @@ -158,7 +159,7 @@ internal sealed class Lzma2EncoderStream : Stream } using var inputMs = new MemoryStream(data.ToArray(), writable: false); - using var outputMs = new MemoryStream(); + using var outputMs = new PooledMemoryStream(); encoder.Code(inputMs, outputMs, data.Length, -1, null); @@ -190,7 +191,7 @@ internal sealed class Lzma2EncoderStream : Stream decoder.SetDecoderProperties(props); using var input = new MemoryStream(compressedData); - using var output = new MemoryStream(); + using var output = new PooledMemoryStream(); decoder.Code(input, output, compressedData.Length, uncompressedSize, null); return (int)input.Position; diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index 902246d7..210f5aa5 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using SharpCompress.Compressors.LZMA.RangeCoder; using SharpCompress.Compressors.PPMd.H; using SharpCompress.Compressors.PPMd.I1; +using SharpCompress.IO; namespace SharpCompress.Compressors.PPMd; @@ -179,7 +180,7 @@ public class PpmdStream : Stream { if (_compress) { - _model.EncodeBlock(_stream, new MemoryStream(), true); + _model.EncodeBlock(_stream, new PooledMemoryStream(), true); } } base.Dispose(disposing); diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs index d8090dca..6a0cfcda 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Compressors.RLE90; +using SharpCompress.IO; namespace SharpCompress.Compressors.Squeezed; @@ -54,14 +55,14 @@ public partial class SqueezeStream if (bytesRead != 2) { - return new MemoryStream(Array.Empty()); + return new PooledMemoryStream(); } int numnodes = numNodesBytes[0] | (numNodesBytes[1] << 8); if (numnodes >= NUMVALS || numnodes == 0) { - return new MemoryStream(Array.Empty()); + return new PooledMemoryStream(); } var dnode = new int[numnodes, 2]; @@ -82,7 +83,7 @@ public partial class SqueezeStream } var bitReader = new BitReader(_stream); - var huffmanDecoded = new MemoryStream(); + var huffmanDecoded = new PooledMemoryStream(); int i = 0; while (true) diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs index 09aa8b2c..f6a40f1c 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using SharpCompress.Common; using SharpCompress.Compressors.RLE90; +using SharpCompress.IO; namespace SharpCompress.Compressors.Squeezed; @@ -67,7 +68,7 @@ public partial class SqueezeStream : Stream if (numnodes >= NUMVALS || numnodes == 0) { - return new MemoryStream(Array.Empty()); + return new PooledMemoryStream(); } var dnode = new int[numnodes, 2]; @@ -78,7 +79,7 @@ public partial class SqueezeStream : Stream } var bitReader = new BitReader(_stream); - var huffmanDecoded = new MemoryStream(); + var huffmanDecoded = new PooledMemoryStream(); int i = 0; while (true) diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs index a785aaf2..501959ee 100644 --- a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs +++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs @@ -171,7 +171,7 @@ public partial class SevenZipWriter : AbstractWriter var filesInfo = new SevenZipFilesInfoWriter { Entries = entries.ToArray() }; // Write header to a temporary stream first - using var headerStream = new MemoryStream(); + using var headerStream = new PooledMemoryStream(); ArchiveHeaderWriter.WriteRawHeader(headerStream, mainStreamsInfo, filesInfo); // Optionally compress the header @@ -212,7 +212,7 @@ public partial class SevenZipWriter : AbstractWriter }; // Write encoded header to a second temporary stream - using var encodedHeaderStream = new MemoryStream(); + using var encodedHeaderStream = new PooledMemoryStream(); ArchiveHeaderWriter.WriteEncodedHeader(encodedHeaderStream, headerStreamsInfo); // Write the encoded header to the output diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index b44de28b..5059aafe 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.2, )", - "resolved": "10.0.2", - "contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.23, )", - "resolved": "8.0.23", - "contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 2bf007542a5bd4cdd74093134f0abdd50bbf0d03 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 4 Apr 2026 11:10:43 +0100 Subject: [PATCH 13/74] cleaned up PooledMemoryStream.cs --- src/SharpCompress/IO/PooledMemoryStream.cs | 157 ++++-------------- .../Streams/PooledMemoryStreamTests.cs | 51 ++---- 2 files changed, 49 insertions(+), 159 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 58bc748f..3bbdb330 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common; namespace SharpCompress.IO; @@ -12,8 +13,6 @@ namespace SharpCompress.IO; /// public sealed class PooledMemoryStream : MemoryStream { - public const int DefaultBlockSize = 81920; - private const int MaxStreamLength = int.MaxValue; private enum StorageMode @@ -33,11 +32,6 @@ public sealed class PooledMemoryStream : MemoryStream private bool _contiguousBufferExposed; private bool _isOpen; - private bool _writable; - private bool _expandable; - private bool _exposable; - - private int _origin; private int _position; private int _length; private int _capacity; @@ -47,7 +41,7 @@ public sealed class PooledMemoryStream : MemoryStream : this(0) { } public PooledMemoryStream(int capacity) - : this(capacity, DefaultBlockSize, ArrayPool.Shared) { } + : this(capacity, Constants.BufferSize, ArrayPool.Shared) { } public PooledMemoryStream(int capacity, int blockSize) : this(capacity, blockSize, ArrayPool.Shared) { } @@ -64,10 +58,6 @@ public sealed class PooledMemoryStream : MemoryStream _mode = StorageMode.Segmented; _blocks = new List(); _isOpen = true; - _writable = true; - _expandable = true; - _exposable = true; - _origin = 0; _position = 0; _length = 0; _capacity = capacity; @@ -75,67 +65,18 @@ public sealed class PooledMemoryStream : MemoryStream EnsureSegmentedAllocated(capacity); } - public PooledMemoryStream(byte[] buffer) - : this(buffer, writable: true) { } - - public PooledMemoryStream(byte[] buffer, bool writable) - : this(buffer, 0, buffer?.Length ?? 0, writable, publiclyVisible: false) { } - - public PooledMemoryStream(byte[] buffer, int index, int count) - : this(buffer, index, count, writable: true, publiclyVisible: false) { } - - public PooledMemoryStream(byte[] buffer, int index, int count, bool writable) - : this(buffer, index, count, writable, publiclyVisible: false) { } - - public PooledMemoryStream( - byte[] buffer, - int index, - int count, - bool writable, - bool publiclyVisible - ) - { - ThrowHelper.ThrowIfNull(buffer, nameof(buffer)); - ThrowHelper.ThrowIfNegative(index, nameof(index)); - ThrowHelper.ThrowIfNegative(count, nameof(count)); - if (buffer.Length - index < count) - { - throw new ArgumentException("Offset and length are out of bounds."); - } - - _arrayPool = ArrayPool.Shared; - _blockSize = DefaultBlockSize; - - _mode = StorageMode.Segmented; - _blocks = new List(); - _origin = 0; - _position = 0; - _length = count; - _capacity = count; - _writable = writable; - _expandable = false; - _exposable = publiclyVisible; - _isOpen = true; - - EnsureSegmentedAllocated(_capacity); - if (count > 0) - { - CopyToSegmented(0, buffer, index, count); - } - } - public override bool CanRead => _isOpen; public override bool CanSeek => _isOpen; - public override bool CanWrite => _writable; + public override bool CanWrite => _isOpen; public override long Length { get { EnsureNotClosed(); - return _length - _origin; + return _length; } } @@ -144,15 +85,15 @@ public sealed class PooledMemoryStream : MemoryStream get { EnsureNotClosed(); - return _position - _origin; + return _position; } set { ThrowHelper.ThrowIfNegative(value, nameof(value)); EnsureNotClosed(); - ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength - _origin, nameof(value)); + ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value)); - _position = _origin + (int)value; + _position = (int)value; } } @@ -161,23 +102,15 @@ public sealed class PooledMemoryStream : MemoryStream get { EnsureNotClosed(); - return _capacity - _origin; + return _capacity; } set { - if (value < Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } + ThrowHelper.ThrowIfLessThan(value, _length, nameof(value)); EnsureNotClosed(); - if (!_expandable && value != Capacity) - { - throw new NotSupportedException("Memory stream is not expandable."); - } - - var target = _origin + value; + var target = value; if (target == _capacity) { return; @@ -205,14 +138,14 @@ public sealed class PooledMemoryStream : MemoryStream var anchor = loc switch { - SeekOrigin.Begin => _origin, + SeekOrigin.Begin => 0, SeekOrigin.Current => _position, SeekOrigin.End => _length, _ => throw new ArgumentException("Invalid seek origin.", nameof(loc)), }; var target = anchor + offset; - if (target < _origin) + if (target < 0) { throw new IOException("Attempted to seek before the beginning of the stream."); } @@ -223,7 +156,7 @@ public sealed class PooledMemoryStream : MemoryStream } _position = (int)target; - return _position - _origin; + return _position; } public override void SetLength(long value) @@ -233,7 +166,7 @@ public sealed class PooledMemoryStream : MemoryStream EnsureWritable(); - var newLength = _origin + (int)value; + var newLength = (int)value; if (newLength > _capacity) { EnsureCapacityForAppend(newLength); @@ -395,11 +328,6 @@ public sealed class PooledMemoryStream : MemoryStream public override byte[] GetBuffer() { EnsureNotClosed(); - if (!_exposable) - { - throw new UnauthorizedAccessException("Memory stream buffer is not publicly visible."); - } - EnsureContiguous(); _contiguousBufferExposed = true; return _contiguousBuffer!; @@ -407,12 +335,6 @@ public sealed class PooledMemoryStream : MemoryStream public override bool TryGetBuffer(out ArraySegment buffer) { - if (!_exposable) - { - buffer = default; - return false; - } - EnsureNotClosed(); EnsureContiguous(); @@ -425,7 +347,7 @@ public sealed class PooledMemoryStream : MemoryStream { EnsureNotClosed(); - var count = _length - _origin; + var count = _length; if (count == 0) { return Array.Empty(); @@ -435,10 +357,10 @@ public sealed class PooledMemoryStream : MemoryStream switch (_mode) { case StorageMode.Contiguous: - Buffer.BlockCopy(_contiguousBuffer!, _origin, copy, 0, count); + Buffer.BlockCopy(_contiguousBuffer!, 0, copy, 0, count); break; case StorageMode.Segmented: - CopyFromSegmented(_origin, copy, 0, count); + CopyFromSegmented(0, copy, 0, count); break; } @@ -450,7 +372,7 @@ public sealed class PooledMemoryStream : MemoryStream ThrowHelper.ThrowIfNull(stream, nameof(stream)); EnsureNotClosed(); - var count = _length - _origin; + var count = _length; if (count == 0) { return; @@ -459,11 +381,11 @@ public sealed class PooledMemoryStream : MemoryStream switch (_mode) { case StorageMode.Contiguous: - stream.Write(_contiguousBuffer!, _origin, count); + stream.Write(_contiguousBuffer!, 0, count); break; case StorageMode.Segmented: { - var position = _origin; + var position = 0; var remaining = count; while (remaining > 0) { @@ -526,7 +448,7 @@ public sealed class PooledMemoryStream : MemoryStream } #if !LEGACY_DOTNET - public override int Read(Span buffer) + public override int Read(Span destination) { EnsureNotClosed(); @@ -536,11 +458,11 @@ public sealed class PooledMemoryStream : MemoryStream return 0; } - var count = Math.Min(available, buffer.Length); + var count = Math.Min(available, destination.Length); switch (_mode) { case StorageMode.Contiguous: - _contiguousBuffer.AsSpan(_position, count).CopyTo(buffer); + _contiguousBuffer.AsSpan(_position, count).CopyTo(destination); break; case StorageMode.Segmented: { @@ -556,7 +478,7 @@ public sealed class PooledMemoryStream : MemoryStream _blocks! [blockIndex] .AsSpan(blockOffset, toCopy) - .CopyTo(buffer.Slice(destinationOffset, toCopy)); + .CopyTo(destination.Slice(destinationOffset, toCopy)); sourcePosition += toCopy; destinationOffset += toCopy; @@ -571,15 +493,15 @@ public sealed class PooledMemoryStream : MemoryStream return count; } - public override void Write(ReadOnlySpan buffer) + public override void Write(ReadOnlySpan source) { EnsureWritable(); - if (buffer.Length == 0) + if (source.Length == 0) { return; } - var endPosition = _position + buffer.Length; + var endPosition = _position + source.Length; if (endPosition < 0) { throw new IOException("Stream is too long."); @@ -598,13 +520,13 @@ public sealed class PooledMemoryStream : MemoryStream switch (_mode) { case StorageMode.Contiguous: - buffer.CopyTo(_contiguousBuffer.AsSpan(_position, buffer.Length)); + source.CopyTo(_contiguousBuffer.AsSpan(_position, source.Length)); break; case StorageMode.Segmented: { var sourceOffset = 0; var destinationPosition = _position; - var remaining = buffer.Length; + var remaining = source.Length; while (remaining > 0) { @@ -612,7 +534,7 @@ public sealed class PooledMemoryStream : MemoryStream var blockOffset = destinationPosition % _blockSize; var toCopy = Math.Min(remaining, _blockSize - blockOffset); - buffer + source .Slice(sourceOffset, toCopy) .CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy)); @@ -633,7 +555,7 @@ public sealed class PooledMemoryStream : MemoryStream } public override ValueTask ReadAsync( - Memory buffer, + Memory destination, CancellationToken cancellationToken = default ) { @@ -644,7 +566,7 @@ public sealed class PooledMemoryStream : MemoryStream try { - return ValueTask.FromResult(Read(buffer.Span)); + return ValueTask.FromResult(Read(destination.Span)); } catch (Exception ex) { @@ -653,7 +575,7 @@ public sealed class PooledMemoryStream : MemoryStream } public override ValueTask WriteAsync( - ReadOnlyMemory buffer, + ReadOnlyMemory source, CancellationToken cancellationToken = default ) { @@ -664,7 +586,7 @@ public sealed class PooledMemoryStream : MemoryStream try { - Write(buffer.Span); + Write(source.Span); return ValueTask.CompletedTask; } catch (Exception ex) @@ -679,8 +601,6 @@ public sealed class PooledMemoryStream : MemoryStream if (_isOpen) { _isOpen = false; - _writable = false; - _expandable = false; if (disposing) { @@ -702,10 +622,6 @@ public sealed class PooledMemoryStream : MemoryStream private void EnsureWritable() { EnsureNotClosed(); - if (!_writable) - { - throw new NotSupportedException("Stream does not support writing."); - } } private void EnsureCapacityForAppend(int requiredLength) @@ -720,11 +636,6 @@ public sealed class PooledMemoryStream : MemoryStream return; } - if (!_expandable) - { - throw new NotSupportedException("Memory stream is not expandable."); - } - var nextCapacity = RoundUpToBlockBoundary(requiredLength); SetCapacityAbsolute(nextCapacity); } diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index 28af7f1c..c628821d 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -54,49 +54,28 @@ public class PooledMemoryStreamTests } [Fact] - public void BufferConstructorCopiesDataAndIsNonExpandable() + public void TryGetBufferReturnsSegmentWhenOpen() { - var backing = Enumerable.Range(0, 10).Select(i => (byte)i).ToArray(); - using var stream = new PooledMemoryStream( - backing, - 2, - 4, - writable: true, - publiclyVisible: true - ); + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); + stream.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); - Assert.Equal(4, stream.Length); - Assert.Equal(4, stream.Capacity); - - backing[2] = 255; - Assert.Equal(new byte[] { 2, 3, 4, 5 }, stream.ToArray()); - - Assert.Throws(() => stream.Capacity = 5); + Assert.True(stream.TryGetBuffer(out var segment)); + Assert.Equal(0, segment.Offset); + Assert.Equal(4, segment.Count); + Assert.Equal(1, segment.Array![0]); } [Fact] - public void BufferConstructorTryGetBufferRespectsVisibilityFlag() + public void CapacitySetterCanGrowAndShrinkWithinLength() { - var backing = new byte[10]; - using var hidden = new PooledMemoryStream( - backing, - 1, - 5, - writable: true, - publiclyVisible: false - ); - Assert.False(hidden.TryGetBuffer(out _)); + using var stream = new PooledMemoryStream(capacity: 16, blockSize: 8); + stream.Write(new byte[6], 0, 6); - using var visible = new PooledMemoryStream( - backing, - 1, - 5, - writable: true, - publiclyVisible: true - ); - Assert.True(visible.TryGetBuffer(out var segment)); - Assert.Equal(0, segment.Offset); - Assert.Equal(5, segment.Count); + stream.Capacity = 24; + Assert.Equal(24, stream.Capacity); + + stream.Capacity = 8; + Assert.Equal(8, stream.Capacity); } [Fact] From db9bdb20bd931219ac20e0521d8508a5c0eca523 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:09:58 +0100 Subject: [PATCH 14/74] Update src/SharpCompress/IO/PooledMemoryStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 3bbdb330..9cd19696 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -325,21 +325,31 @@ public sealed class PooledMemoryStream : MemoryStream } } + private byte[] GetExposableContiguousBuffer() + { + EnsureContiguous(); + + if (_capacity > _length) + { + ClearRange(_length, _capacity - _length); + } + + _contiguousBufferExposed = true; + return _contiguousBuffer!; + } + public override byte[] GetBuffer() { EnsureNotClosed(); - EnsureContiguous(); - _contiguousBufferExposed = true; - return _contiguousBuffer!; + return GetExposableContiguousBuffer(); } public override bool TryGetBuffer(out ArraySegment buffer) { EnsureNotClosed(); - EnsureContiguous(); - _contiguousBufferExposed = true; - buffer = new ArraySegment(_contiguousBuffer!, 0, _length); + var exposableBuffer = GetExposableContiguousBuffer(); + buffer = new ArraySegment(exposableBuffer, 0, _length); return true; } From 6b9a3f8b69a57f999b48b5af3a089045bb28488b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:10:17 +0100 Subject: [PATCH 15/74] Update src/SharpCompress/Compressors/PPMd/PpmdStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/Compressors/PPMd/PpmdStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index 210f5aa5..6f1e7fcb 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -180,7 +180,7 @@ public class PpmdStream : Stream { if (_compress) { - _model.EncodeBlock(_stream, new PooledMemoryStream(), true); + _model.EncodeBlock(_stream, Stream.Null, true); } } base.Dispose(disposing); From 5e7644bd1857840ef876bbf3317aa7c0a91dfe59 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:11:48 +0100 Subject: [PATCH 16/74] Update src/SharpCompress/IO/PooledMemoryStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 9cd19696..a832bf69 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -892,11 +892,10 @@ public sealed class PooledMemoryStream : MemoryStream _contiguousBuffer = null; } - for (var i = 0; i < _detachedExposedBuffers.Count; i++) - { - _arrayPool.Return(_detachedExposedBuffers[i]); - } - + // Buffers tracked here have been exposed to callers. Returning them to the + // shared pool would allow unrelated code to rent and mutate arrays that may + // still be referenced after the stream is disposed, which breaks + // MemoryStream-compatible expectations for GetBuffer/TryGetBuffer. _detachedExposedBuffers.Clear(); } From c01dd5cd90d21a3ede9e18a0305149d30b1429e0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:16:47 +0100 Subject: [PATCH 17/74] Update tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Streams/PooledMemoryStreamTests.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index c628821d..d5e9a192 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -107,13 +107,22 @@ public class PooledMemoryStreamTests private sealed class TrackingArrayPool : ArrayPool { + private const byte RentedBufferFillValue = 0x5A; + public readonly System.Collections.Generic.List RentRequests = new(); public readonly System.Collections.Generic.List ReturnedLengths = new(); public override byte[] Rent(int minimumLength) { RentRequests.Add(minimumLength); - return new byte[minimumLength]; + + var array = new byte[minimumLength]; + for (var i = 0; i < array.Length; i++) + { + array[i] = RentedBufferFillValue; + } + + return array; } public override void Return(byte[] array, bool clearArray = false) From 8c4fed373b8de60f9650271f7c7a48746fb092b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:21:54 +0000 Subject: [PATCH 18/74] Remove try/catch from async overloads and fix CA1725 parameter naming in PooledMemoryStream Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/1f2c3c48-1112-43f1-82ce-efb157fbe0d5 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 64 ++++++---------------- 1 file changed, 18 insertions(+), 46 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index a832bf69..cf94ad1e 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -424,14 +424,7 @@ public sealed class PooledMemoryStream : MemoryStream return Task.FromCanceled(cancellationToken); } - try - { - return Task.FromResult(Read(buffer, offset, count)); - } - catch (Exception ex) - { - return Task.FromException(ex); - } + return Task.FromResult(Read(buffer, offset, count)); } public override Task WriteAsync( @@ -446,19 +439,12 @@ public sealed class PooledMemoryStream : MemoryStream return Task.FromCanceled(cancellationToken); } - try - { - Write(buffer, offset, count); - return Task.CompletedTask; - } - catch (Exception ex) - { - return Task.FromException(ex); - } + Write(buffer, offset, count); + return Task.CompletedTask; } #if !LEGACY_DOTNET - public override int Read(Span destination) + public override int Read(Span buffer) { EnsureNotClosed(); @@ -468,11 +454,11 @@ public sealed class PooledMemoryStream : MemoryStream return 0; } - var count = Math.Min(available, destination.Length); + var count = Math.Min(available, buffer.Length); switch (_mode) { case StorageMode.Contiguous: - _contiguousBuffer.AsSpan(_position, count).CopyTo(destination); + _contiguousBuffer.AsSpan(_position, count).CopyTo(buffer); break; case StorageMode.Segmented: { @@ -488,7 +474,7 @@ public sealed class PooledMemoryStream : MemoryStream _blocks! [blockIndex] .AsSpan(blockOffset, toCopy) - .CopyTo(destination.Slice(destinationOffset, toCopy)); + .CopyTo(buffer.Slice(destinationOffset, toCopy)); sourcePosition += toCopy; destinationOffset += toCopy; @@ -503,15 +489,15 @@ public sealed class PooledMemoryStream : MemoryStream return count; } - public override void Write(ReadOnlySpan source) + public override void Write(ReadOnlySpan buffer) { EnsureWritable(); - if (source.Length == 0) + if (buffer.Length == 0) { return; } - var endPosition = _position + source.Length; + var endPosition = _position + buffer.Length; if (endPosition < 0) { throw new IOException("Stream is too long."); @@ -530,13 +516,13 @@ public sealed class PooledMemoryStream : MemoryStream switch (_mode) { case StorageMode.Contiguous: - source.CopyTo(_contiguousBuffer.AsSpan(_position, source.Length)); + buffer.CopyTo(_contiguousBuffer.AsSpan(_position, buffer.Length)); break; case StorageMode.Segmented: { var sourceOffset = 0; var destinationPosition = _position; - var remaining = source.Length; + var remaining = buffer.Length; while (remaining > 0) { @@ -544,7 +530,7 @@ public sealed class PooledMemoryStream : MemoryStream var blockOffset = destinationPosition % _blockSize; var toCopy = Math.Min(remaining, _blockSize - blockOffset); - source + buffer .Slice(sourceOffset, toCopy) .CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy)); @@ -565,7 +551,7 @@ public sealed class PooledMemoryStream : MemoryStream } public override ValueTask ReadAsync( - Memory destination, + Memory buffer, CancellationToken cancellationToken = default ) { @@ -574,18 +560,11 @@ public sealed class PooledMemoryStream : MemoryStream return ValueTask.FromCanceled(cancellationToken); } - try - { - return ValueTask.FromResult(Read(destination.Span)); - } - catch (Exception ex) - { - return ValueTask.FromException(ex); - } + return ValueTask.FromResult(Read(buffer.Span)); } public override ValueTask WriteAsync( - ReadOnlyMemory source, + ReadOnlyMemory buffer, CancellationToken cancellationToken = default ) { @@ -594,15 +573,8 @@ public sealed class PooledMemoryStream : MemoryStream return ValueTask.FromCanceled(cancellationToken); } - try - { - Write(source.Span); - return ValueTask.CompletedTask; - } - catch (Exception ex) - { - return ValueTask.FromException(ex); - } + Write(buffer.Span); + return ValueTask.CompletedTask; } #endif From c25fec313eba26b7c9d0c1758c18acd3c2d7b686 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:39:15 +0100 Subject: [PATCH 19/74] fix PooledMemoryStream.cs --- src/SharpCompress/IO/PooledMemoryStream.cs | 11 +++++++-- .../Streams/PooledMemoryStreamTests.cs | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index cf94ad1e..aadc2507 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -10,7 +10,15 @@ namespace SharpCompress.IO; /// /// MemoryStream implementation backed by pooled byte arrays. +/// Uses to reduce GC pressure for temporary buffers. /// +/// +/// This implementation is not thread-safe. Use appropriate synchronization for concurrent access. +/// Buffers exposed via or will not be +/// returned to the pool on dispose to maintain MemoryStream-compatible semantics. +/// The stream dynamically switches between segmented (multiple blocks) and contiguous storage modes +/// based on usage patterns, optimizing for both memory efficiency and performance. +/// public sealed class PooledMemoryStream : MemoryStream { private const int MaxStreamLength = int.MaxValue; @@ -857,8 +865,7 @@ public sealed class PooledMemoryStream : MemoryStream ReturnSegmentedBlocks(); _blocks = null; } - - if (_mode == StorageMode.Contiguous && _contiguousBuffer is not null) + else if (_mode == StorageMode.Contiguous && _contiguousBuffer is not null) { _arrayPool.Return(_contiguousBuffer); _contiguousBuffer = null; diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index d5e9a192..6b12f2a3 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -105,6 +105,30 @@ public class PooledMemoryStreamTests Assert.Throws(() => stream.GetBuffer()); } + [Fact] + public void MultipleGetBufferCallsReturnSameArray() + { + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + + var buffer1 = stream.GetBuffer(); + var buffer2 = stream.GetBuffer(); + + Assert.Same(buffer1, buffer2); + Assert.Equal(1, buffer1[0]); + Assert.Equal(2, buffer1[1]); + Assert.Equal(3, buffer1[2]); + } + + [Fact] + public void SeekBeyondMaxLengthThrows() + { + using var stream = new PooledMemoryStream(); + Assert.Throws(() => + stream.Seek(int.MaxValue + 1L, SeekOrigin.Begin) + ); + } + private sealed class TrackingArrayPool : ArrayPool { private const byte RentedBufferFillValue = 0x5A; From bfe78249fc73470ae3997a1d5ea40d12c380c748 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:47:41 +0100 Subject: [PATCH 20/74] remove net5 from target frameworks to avoid issues --- src/SharpCompress/Common/Rar/CryptKey5.cs | 4 +- .../Common/Tar/Headers/TarHeader.Async.cs | 2 +- .../Common/Tar/Headers/TarHeader.cs | 2 +- .../ZStandard/Unsafe/ZstdInternal.cs | 4 +- .../Compressors/ZStandard/Unsafe/ZstdLazy.cs | 6 +-- .../Polyfills/StreamExtensions.cs | 2 +- src/SharpCompress/SharpCompress.csproj | 2 +- src/SharpCompress/packages.lock.json | 42 ------------------- 8 files changed, 11 insertions(+), 53 deletions(-) diff --git a/src/SharpCompress/Common/Rar/CryptKey5.cs b/src/SharpCompress/Common/Rar/CryptKey5.cs index 66f08aa3..490a3c54 100644 --- a/src/SharpCompress/Common/Rar/CryptKey5.cs +++ b/src/SharpCompress/Common/Rar/CryptKey5.cs @@ -35,7 +35,7 @@ internal class CryptKey5 : ICryptKey ) { var passwordBytes = Encoding.UTF8.GetBytes(password); -#if LEGACY_DOTNET || NET5_0 +#if LEGACY_DOTNET using var hmac = new HMACSHA256(passwordBytes); var block = hmac.ComputeHash(salt); #else @@ -50,7 +50,7 @@ internal class CryptKey5 : ICryptKey { for (var i = 1; i < loop[x]; i++) { -#if LEGACY_DOTNET || NET5_0 +#if LEGACY_DOTNET block = hmac.ComputeHash(block); #else block = HMACSHA256.HashData(passwordBytes, block); diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs index 870c472c..084cd83d 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs @@ -59,7 +59,7 @@ internal sealed partial class TarHeader int splitIndex = -1; for (int i = 0; i < dirSeps.Count; i++) { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER int count = ArchiveEncoding .GetEncoding() .GetByteCount(fullName.AsSpan(0, dirSeps[i])); diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs index 2c538067..233970b2 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs @@ -89,7 +89,7 @@ internal sealed partial class TarHeader int splitIndex = -1; for (int i = 0; i < dirSeps.Count; i++) { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER int count = ArchiveEncoding .GetEncoding() .GetByteCount(fullName.AsSpan(0, dirSeps[i])); diff --git a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdInternal.cs b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdInternal.cs index c8b9f15d..4180d6b1 100644 --- a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdInternal.cs +++ b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdInternal.cs @@ -5,7 +5,7 @@ using static SharpCompress.Compressors.ZStandard.UnsafeHelper; #if NETCOREAPP3_0_OR_GREATER using System.Runtime.Intrinsics.X86; #endif -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Runtime.Intrinsics.Arm; #endif @@ -554,7 +554,7 @@ public static unsafe partial class Methods [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ZSTD_copy16(void* dst, void* src) { -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER if (AdvSimd.IsSupported) { AdvSimd.Store((byte*)dst, AdvSimd.LoadVector128((byte*)src)); diff --git a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdLazy.cs b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdLazy.cs index 178160b1..fdf26282 100644 --- a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdLazy.cs +++ b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdLazy.cs @@ -6,7 +6,7 @@ using static SharpCompress.Compressors.ZStandard.UnsafeHelper; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; #endif -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER using System.Runtime.Intrinsics.Arm; #endif @@ -1172,7 +1172,7 @@ public static unsafe partial class Methods { assert(rowEntries == 16 || rowEntries == 32 || rowEntries == 64); assert(rowEntries <= 64); -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER if (AdvSimd.IsSupported && BitConverter.IsLittleEndian) { if (rowEntries == 16) @@ -1272,7 +1272,7 @@ public static unsafe partial class Methods } #endif -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER if (AdvSimd.IsSupported && BitConverter.IsLittleEndian) { if (rowEntries == 16) diff --git a/src/SharpCompress/Polyfills/StreamExtensions.cs b/src/SharpCompress/Polyfills/StreamExtensions.cs index 18c5b593..e3caa24f 100644 --- a/src/SharpCompress/Polyfills/StreamExtensions.cs +++ b/src/SharpCompress/Polyfills/StreamExtensions.cs @@ -28,7 +28,7 @@ public static class StreamExtensions public Task SkipAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER return stream.CopyToAsync(Stream.Null, cancellationToken); #else return stream.CopyToAsync(Stream.Null); diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index 0a9c440c..bcde6224 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -6,7 +6,7 @@ 0.0.0.0 0.0.0.0 Adam Hathcock - net48;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0 + net48;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0 SharpCompress ../../SharpCompress.snk true diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 5059aafe..a401c702 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -313,48 +313,6 @@ "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" } }, - ".NETCoreApp,Version=v5.0": { - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" - } - }, - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[10.0.102, )", - "resolved": "10.0.102", - "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.102", - "Microsoft.SourceLink.Common": "10.0.102" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" - }, - "Microsoft.NETFramework.ReferenceAssemblies.net461": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" - }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" - } - }, "net6.0": { "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 4f09c6dc0009062ca976397e8524cf42c0e18de0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:57:04 +0100 Subject: [PATCH 21/74] Update tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Streams/PooledMemoryStreamTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index 6b12f2a3..91f64a40 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -129,6 +129,59 @@ public class PooledMemoryStreamTests ); } + [Fact] + public void DisposeAfterGetBufferDoesNotReturnExposedArrayToPool() + { + var pool = new TrackingArrayPool(); + byte[] buffer; + + using (var stream = new PooledMemoryStream(pool, capacity: 0, blockSize: 8)) + { + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + buffer = stream.GetBuffer(); + + Assert.NotNull(buffer); + Assert.NotEmpty(pool.RentRequests); + } + + Assert.Empty(pool.ReturnedLengths); + Assert.Equal(1, buffer[0]); + Assert.Equal(2, buffer[1]); + Assert.Equal(3, buffer[2]); + } + + [Fact] + public void DisposeAfterTryGetBufferDoesNotReturnExposedArrayToPool() + { + var pool = new TrackingArrayPool(); + ArraySegment segment; + + using (var stream = new PooledMemoryStream(pool, capacity: 0, blockSize: 8)) + { + stream.Write(new byte[] { 1, 2, 3 }, 0, 3); + + Assert.True(stream.TryGetBuffer(out segment)); + Assert.NotNull(segment.Array); + Assert.NotEmpty(pool.RentRequests); + } + + Assert.Empty(pool.ReturnedLengths); + Assert.Equal(1, segment.Array![segment.Offset]); + Assert.Equal(2, segment.Array[segment.Offset + 1]); + Assert.Equal(3, segment.Array[segment.Offset + 2]); + } + + [Fact] + public void SetLengthNearIntMaxValueDoesNotThrowIOException() + { + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); + var length = int.MaxValue - 1L; + + var exception = Record.Exception(() => stream.SetLength(length)); + + Assert.Null(exception); + Assert.Equal(length, stream.Length); + } private sealed class TrackingArrayPool : ArrayPool { private const byte RentedBufferFillValue = 0x5A; From ec46584e545736338c2b9ccd3a52881d9ecac62f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 14:57:36 +0100 Subject: [PATCH 22/74] Update src/SharpCompress/IO/PooledMemoryStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index aadc2507..5bfce1dc 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -680,6 +680,7 @@ public sealed class PooledMemoryStream : MemoryStream _contiguousBuffer = contiguous; _contiguousBufferExposed = false; _allocatedCapacity = contiguous.Length; + _capacity = _allocatedCapacity; } private void DemoteContiguousToSegmented() From fb785985163ab979f5287bba0b637516fafdf499 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 15:16:00 +0100 Subject: [PATCH 23/74] implement GetBuffer with non-pooled array creation --- src/SharpCompress/IO/PooledMemoryStream.cs | 26 ++++-- .../Streams/PooledMemoryStreamTests.cs | 90 +++++++++++++++---- 2 files changed, 91 insertions(+), 25 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index aadc2507..e2e7cf6d 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -333,30 +333,38 @@ public sealed class PooledMemoryStream : MemoryStream } } - private byte[] GetExposableContiguousBuffer() + private byte[] CreateExposableBuffer() { - EnsureContiguous(); - - if (_capacity > _length) + var exposable = new byte[_capacity]; + if (_length == 0) { - ClearRange(_length, _capacity - _length); + return exposable; } - _contiguousBufferExposed = true; - return _contiguousBuffer!; + switch (_mode) + { + case StorageMode.Contiguous: + Buffer.BlockCopy(_contiguousBuffer!, 0, exposable, 0, _length); + break; + case StorageMode.Segmented: + CopyFromSegmented(0, exposable, 0, _length); + break; + } + + return exposable; } public override byte[] GetBuffer() { EnsureNotClosed(); - return GetExposableContiguousBuffer(); + return CreateExposableBuffer(); } public override bool TryGetBuffer(out ArraySegment buffer) { EnsureNotClosed(); - var exposableBuffer = GetExposableContiguousBuffer(); + var exposableBuffer = CreateExposableBuffer(); buffer = new ArraySegment(exposableBuffer, 0, _length); return true; } diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index 6b12f2a3..e7b770e7 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.IO; -using System.Linq; using SharpCompress.IO; using Xunit; @@ -35,22 +34,28 @@ public class PooledMemoryStreamTests } [Fact] - public void GetBufferPromotesToContiguousAndLaterGrowthUsesBlocks() + public void GetBufferReturnsArraySizedToCapacityWithoutTouchingPool() { - var pool = new TrackingArrayPool(); + var pool = new OverRentingArrayPool(extraLength: 8); using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); - stream.Write(new byte[10], 0, 10); + stream.Write(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 0, 10); - var contiguous = stream.GetBuffer(); - Assert.Equal(16, contiguous.Length); - Assert.Contains(16, pool.RentRequests); + var rentsBefore = pool.RentRequests.Count; + var returnsBefore = pool.ReturnedLengths.Count; - stream.Position = stream.Length; - stream.Write(new byte[10], 0, 10); // grow to 20, forcing demotion + block growth + var buffer = stream.GetBuffer(); + Assert.Equal(16, buffer.Length); + Assert.Equal(1, buffer[0]); + Assert.Equal(10, buffer[9]); + Assert.Equal(0, buffer[10]); + Assert.Equal(0, buffer[15]); + Assert.Equal(rentsBefore, pool.RentRequests.Count); + Assert.Equal(returnsBefore, pool.ReturnedLengths.Count); - var totalBlockRents = pool.RentRequests.Count(size => size == 8); - Assert.True(totalBlockRents >= 3); + buffer[0] = 255; + stream.Position = 0; + Assert.Equal(1, stream.ReadByte()); } [Fact] @@ -65,6 +70,33 @@ public class PooledMemoryStreamTests Assert.Equal(1, segment.Array![0]); } + [Fact] + public void TryGetBufferReturnsArraySizedToCapacityWithoutTouchingPool() + { + var pool = new OverRentingArrayPool(extraLength: 8); + + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); + stream.Write(new byte[] { 1, 2, 3, 4 }, 0, 4); + + var rentsBefore = pool.RentRequests.Count; + var returnsBefore = pool.ReturnedLengths.Count; + + Assert.True(stream.TryGetBuffer(out var segment)); + Assert.Equal(0, segment.Offset); + Assert.Equal(4, segment.Count); + Assert.Equal(8, segment.Array!.Length); + Assert.Equal(1, segment.Array[0]); + Assert.Equal(4, segment.Array[3]); + Assert.Equal(0, segment.Array[4]); + Assert.Equal(0, segment.Array[7]); + Assert.Equal(rentsBefore, pool.RentRequests.Count); + Assert.Equal(returnsBefore, pool.ReturnedLengths.Count); + + segment.Array[0] = 255; + stream.Position = 0; + Assert.Equal(1, stream.ReadByte()); + } + [Fact] public void CapacitySetterCanGrowAndShrinkWithinLength() { @@ -106,7 +138,7 @@ public class PooledMemoryStreamTests } [Fact] - public void MultipleGetBufferCallsReturnSameArray() + public void MultipleGetBufferCallsReturnDifferentArrays() { using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); stream.Write(new byte[] { 1, 2, 3 }, 0, 3); @@ -114,10 +146,8 @@ public class PooledMemoryStreamTests var buffer1 = stream.GetBuffer(); var buffer2 = stream.GetBuffer(); - Assert.Same(buffer1, buffer2); - Assert.Equal(1, buffer1[0]); - Assert.Equal(2, buffer1[1]); - Assert.Equal(3, buffer1[2]); + Assert.NotSame(buffer1, buffer2); + Assert.Equal(buffer1, buffer2); } [Fact] @@ -158,4 +188,32 @@ public class PooledMemoryStreamTests } } } + + private sealed class OverRentingArrayPool : ArrayPool + { + private readonly int _extraLength; + + public OverRentingArrayPool(int extraLength) + { + _extraLength = extraLength; + } + + public readonly System.Collections.Generic.List RentRequests = new(); + public readonly System.Collections.Generic.List ReturnedLengths = new(); + + public override byte[] Rent(int minimumLength) + { + RentRequests.Add(minimumLength); + return new byte[minimumLength + _extraLength]; + } + + public override void Return(byte[] array, bool clearArray = false) + { + ReturnedLengths.Add(array.Length); + if (clearArray) + { + Array.Clear(array, 0, array.Length); + } + } + } } From e7cacdd7ee78d8b2559bbf8c78485041541b6a99 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 15:19:44 +0100 Subject: [PATCH 24/74] don't resize ring buffer --- src/SharpCompress/IO/SharpCompressStream.cs | 10 ++++++-- .../Streams/SharpCompressStreamSeekTest.cs | 23 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 26560024..f2bda765 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -203,11 +203,17 @@ public partial class SharpCompressStream : Stream, IStreamStack // Allocate ring buffer with the requested minimum size (at least the global default). if (_ringBuffer is null) { - var size = + var requiredSize = minBufferSize.GetValueOrDefault() > Constants.RewindableBufferSize ? minBufferSize.GetValueOrDefault() : Constants.RewindableBufferSize; - _ringBuffer = new RingBuffer(size); + _ringBuffer = new RingBuffer(requiredSize); + } + else if (minBufferSize.HasValue && minBufferSize.Value > _ringBuffer.Capacity) + { + throw new ArchiveOperationException( + $"StartRecording requires a ring buffer of at least {minBufferSize.Value} bytes, but the stream was created with capacity {_ringBuffer.Capacity}." + ); } // Mark current position as recording anchor diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs index 8840a2ad..04c86284 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamSeekTest.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Test.Mocks; using Xunit; @@ -134,10 +135,10 @@ public class SharpCompressStreamSeekTest // Simulates the BZip2 scenario: the ring buffer must be large enough // from the moment StartRecording is called so that a large probe read // (up to 900 KB for BZip2) can be rewound without buffer overflow. - const int largeSize = 100; - const int largeReadSize = 80; + const int largeSize = 100_000; + const int largeReadSize = 80_000; - var data = new byte[100]; + var data = new byte[largeSize]; for (var i = 0; i < data.Length; i++) { data[i] = (byte)(i + 1); @@ -181,4 +182,20 @@ public class SharpCompressStreamSeekTest Assert.Equal(1, readBuffer[0]); Assert.Equal(5, readBuffer[4]); } + + [Fact] + public void StartRecording_WithExistingSmallerRingBuffer_Throws() + { + var ms = new MemoryStream(new byte[131_072]); + var nonSeekableMs = new NonSeekableStreamWrapper(ms); + var stream = SharpCompressStream.Create(nonSeekableMs, 32_768); + + var exception = Assert.Throws(() => + stream.StartRecording(131_072) + ); + + Assert.Contains("ring buffer", exception.Message); + Assert.Contains("131072", exception.Message); + Assert.Contains("32768", exception.Message); + } } From d1e6173b506bb9ca4cc03da593133d34ae4cf5fa Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 5 Apr 2026 15:25:33 +0100 Subject: [PATCH 25/74] remove contiguous buffer tracking and added overrenting array pool test --- src/SharpCompress/IO/PooledMemoryStream.cs | 325 ++++-------------- .../Streams/PooledMemoryStreamTests.cs | 83 +++++ 2 files changed, 145 insertions(+), 263 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index e2e7cf6d..8caadd31 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -14,36 +14,21 @@ namespace SharpCompress.IO; /// /// /// This implementation is not thread-safe. Use appropriate synchronization for concurrent access. -/// Buffers exposed via or will not be -/// returned to the pool on dispose to maintain MemoryStream-compatible semantics. -/// The stream dynamically switches between segmented (multiple blocks) and contiguous storage modes -/// based on usage patterns, optimizing for both memory efficiency and performance. +/// Buffers exposed via or are allocated as +/// fresh non-pooled arrays to avoid exposing pooled memory. /// public sealed class PooledMemoryStream : MemoryStream { private const int MaxStreamLength = int.MaxValue; - private enum StorageMode - { - Segmented, - Contiguous, - } - private readonly ArrayPool _arrayPool; private readonly int _blockSize; - private readonly List _detachedExposedBuffers = new(); - - private StorageMode _mode; private List? _blocks; - private byte[]? _contiguousBuffer; - - private bool _contiguousBufferExposed; private bool _isOpen; private int _position; private int _length; private int _capacity; - private int _allocatedCapacity; public PooledMemoryStream() : this(0) { } @@ -63,7 +48,6 @@ public sealed class PooledMemoryStream : MemoryStream _arrayPool = arrayPool; _blockSize = blockSize; - _mode = StorageMode.Segmented; _blocks = new List(); _isOpen = true; _position = 0; @@ -208,15 +192,7 @@ public sealed class PooledMemoryStream : MemoryStream count = available; } - switch (_mode) - { - case StorageMode.Contiguous: - Buffer.BlockCopy(_contiguousBuffer!, _position, buffer, offset, count); - break; - case StorageMode.Segmented: - CopyFromSegmented(_position, buffer, offset, count); - break; - } + CopyFromSegmented(_position, buffer, offset, count); _position += count; return count; @@ -230,20 +206,9 @@ public sealed class PooledMemoryStream : MemoryStream return -1; } - byte value; - switch (_mode) - { - case StorageMode.Contiguous: - value = _contiguousBuffer![_position]; - break; - default: - { - var blockIndex = _position / _blockSize; - var blockOffset = _position % _blockSize; - value = _blocks![blockIndex][blockOffset]; - break; - } - } + var blockIndex = _position / _blockSize; + var blockOffset = _position % _blockSize; + var value = _blocks![blockIndex][blockOffset]; _position++; return value; @@ -275,15 +240,7 @@ public sealed class PooledMemoryStream : MemoryStream ClearRange(_length, _position - _length); } - switch (_mode) - { - case StorageMode.Contiguous: - Buffer.BlockCopy(buffer, offset, _contiguousBuffer!, _position, count); - break; - case StorageMode.Segmented: - CopyToSegmented(_position, buffer, offset, count); - break; - } + CopyToSegmented(_position, buffer, offset, count); _position = endPosition; if (_position > _length) @@ -312,19 +269,9 @@ public sealed class PooledMemoryStream : MemoryStream ClearRange(_length, _position - _length); } - switch (_mode) - { - case StorageMode.Contiguous: - _contiguousBuffer![_position] = value; - break; - default: - { - var blockIndex = _position / _blockSize; - var blockOffset = _position % _blockSize; - _blocks![blockIndex][blockOffset] = value; - break; - } - } + var blockIndex = _position / _blockSize; + var blockOffset = _position % _blockSize; + _blocks![blockIndex][blockOffset] = value; _position = endPosition; if (_position > _length) @@ -341,15 +288,7 @@ public sealed class PooledMemoryStream : MemoryStream return exposable; } - switch (_mode) - { - case StorageMode.Contiguous: - Buffer.BlockCopy(_contiguousBuffer!, 0, exposable, 0, _length); - break; - case StorageMode.Segmented: - CopyFromSegmented(0, exposable, 0, _length); - break; - } + CopyFromSegmented(0, exposable, 0, _length); return exposable; } @@ -380,15 +319,7 @@ public sealed class PooledMemoryStream : MemoryStream } var copy = new byte[count]; - switch (_mode) - { - case StorageMode.Contiguous: - Buffer.BlockCopy(_contiguousBuffer!, 0, copy, 0, count); - break; - case StorageMode.Segmented: - CopyFromSegmented(0, copy, 0, count); - break; - } + CopyFromSegmented(0, copy, 0, count); return copy; } @@ -404,27 +335,16 @@ public sealed class PooledMemoryStream : MemoryStream return; } - switch (_mode) + var position = 0; + var remaining = count; + while (remaining > 0) { - case StorageMode.Contiguous: - stream.Write(_contiguousBuffer!, 0, count); - break; - case StorageMode.Segmented: - { - var position = 0; - var remaining = count; - while (remaining > 0) - { - var blockIndex = position / _blockSize; - var blockOffset = position % _blockSize; - var toWrite = Math.Min(remaining, _blockSize - blockOffset); - stream.Write(_blocks![blockIndex], blockOffset, toWrite); - position += toWrite; - remaining -= toWrite; - } - - break; - } + var blockIndex = position / _blockSize; + var blockOffset = position % _blockSize; + var toWrite = Math.Min(remaining, _blockSize - blockOffset); + stream.Write(_blocks![blockIndex], blockOffset, toWrite); + position += toWrite; + remaining -= toWrite; } } @@ -471,34 +391,23 @@ public sealed class PooledMemoryStream : MemoryStream } var count = Math.Min(available, buffer.Length); - switch (_mode) + var sourcePosition = _position; + var destinationOffset = 0; + var remaining = count; + + while (remaining > 0) { - case StorageMode.Contiguous: - _contiguousBuffer.AsSpan(_position, count).CopyTo(buffer); - break; - case StorageMode.Segmented: - { - var sourcePosition = _position; - var destinationOffset = 0; - var remaining = count; + var blockIndex = sourcePosition / _blockSize; + var blockOffset = sourcePosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); + _blocks! + [blockIndex] + .AsSpan(blockOffset, toCopy) + .CopyTo(buffer.Slice(destinationOffset, toCopy)); - while (remaining > 0) - { - var blockIndex = sourcePosition / _blockSize; - var blockOffset = sourcePosition % _blockSize; - var toCopy = Math.Min(remaining, _blockSize - blockOffset); - _blocks! - [blockIndex] - .AsSpan(blockOffset, toCopy) - .CopyTo(buffer.Slice(destinationOffset, toCopy)); - - sourcePosition += toCopy; - destinationOffset += toCopy; - remaining -= toCopy; - } - - break; - } + sourcePosition += toCopy; + destinationOffset += toCopy; + remaining -= toCopy; } _position += count; @@ -529,34 +438,23 @@ public sealed class PooledMemoryStream : MemoryStream ClearRange(_length, _position - _length); } - switch (_mode) + var sourceOffset = 0; + var destinationPosition = _position; + var remaining = buffer.Length; + + while (remaining > 0) { - case StorageMode.Contiguous: - buffer.CopyTo(_contiguousBuffer.AsSpan(_position, buffer.Length)); - break; - case StorageMode.Segmented: - { - var sourceOffset = 0; - var destinationPosition = _position; - var remaining = buffer.Length; + var blockIndex = destinationPosition / _blockSize; + var blockOffset = destinationPosition % _blockSize; + var toCopy = Math.Min(remaining, _blockSize - blockOffset); - while (remaining > 0) - { - var blockIndex = destinationPosition / _blockSize; - var blockOffset = destinationPosition % _blockSize; - var toCopy = Math.Min(remaining, _blockSize - blockOffset); + buffer + .Slice(sourceOffset, toCopy) + .CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy)); - buffer - .Slice(sourceOffset, toCopy) - .CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy)); - - sourceOffset += toCopy; - destinationPosition += toCopy; - remaining -= toCopy; - } - - break; - } + sourceOffset += toCopy; + destinationPosition += toCopy; + remaining -= toCopy; } _position = endPosition; @@ -642,20 +540,7 @@ public sealed class PooledMemoryStream : MemoryStream { ThrowHelper.ThrowIfLessThan(newCapacity, _length, nameof(newCapacity)); - switch (_mode) - { - case StorageMode.Contiguous: - if (newCapacity > _allocatedCapacity) - { - DemoteContiguousToSegmented(); - EnsureSegmentedAllocated(newCapacity); - } - break; - - case StorageMode.Segmented: - EnsureSegmentedAllocated(newCapacity); - break; - } + EnsureSegmentedAllocated(newCapacity); _capacity = newCapacity; if (_length > _capacity) @@ -668,67 +553,8 @@ public sealed class PooledMemoryStream : MemoryStream } } - private void EnsureContiguous() - { - if (_mode == StorageMode.Contiguous) - { - return; - } - - var requested = Math.Max(_capacity, 1); - var contiguous = _arrayPool.Rent(requested); - if (_length > 0) - { - CopyFromSegmented(0, contiguous, 0, _length); - } - - ReturnSegmentedBlocks(); - - _mode = StorageMode.Contiguous; - _contiguousBuffer = contiguous; - _contiguousBufferExposed = false; - _allocatedCapacity = contiguous.Length; - } - - private void DemoteContiguousToSegmented() - { - var contiguous = _contiguousBuffer; - if (contiguous is null) - { - return; - } - - var requiredCapacity = Math.Max(_capacity, _length); - _mode = StorageMode.Segmented; - _blocks = new List(); - _contiguousBuffer = null; - EnsureSegmentedAllocated(requiredCapacity); - - if (_length > 0) - { - CopyToSegmented(0, contiguous, 0, _length); - } - - if (_contiguousBufferExposed) - { - _detachedExposedBuffers.Add(contiguous); - _contiguousBufferExposed = false; - } - else - { - _arrayPool.Return(contiguous); - } - } - private void EnsureSegmentedAllocated(int capacity) { - if (_mode != StorageMode.Segmented) - { - throw new InvalidOperationException( - "Segmented allocation requested while not in segmented mode." - ); - } - var requiredAllocated = RoundUpToBlockBoundary(capacity); var requiredBlocks = requiredAllocated == 0 ? 0 : requiredAllocated / _blockSize; @@ -746,8 +572,6 @@ public sealed class PooledMemoryStream : MemoryStream _blocks.RemoveAt(index); _arrayPool.Return(block); } - - _allocatedCapacity = requiredAllocated; } private int RoundUpToBlockBoundary(int value) @@ -773,27 +597,16 @@ public sealed class PooledMemoryStream : MemoryStream return; } - switch (_mode) + var position = absoluteStart; + var remaining = count; + while (remaining > 0) { - case StorageMode.Contiguous: - Array.Clear(_contiguousBuffer!, absoluteStart, count); - break; - case StorageMode.Segmented: - { - var position = absoluteStart; - var remaining = count; - while (remaining > 0) - { - var blockIndex = position / _blockSize; - var blockOffset = position % _blockSize; - var toClear = Math.Min(remaining, _blockSize - blockOffset); - Array.Clear(_blocks![blockIndex], blockOffset, toClear); - position += toClear; - remaining -= toClear; - } - - break; - } + var blockIndex = position / _blockSize; + var blockOffset = position % _blockSize; + var toClear = Math.Min(remaining, _blockSize - blockOffset); + Array.Clear(_blocks![blockIndex], blockOffset, toClear); + position += toClear; + remaining -= toClear; } } @@ -868,22 +681,8 @@ public sealed class PooledMemoryStream : MemoryStream private void ReturnPooledBuffers() { - if (_mode == StorageMode.Segmented) - { - ReturnSegmentedBlocks(); - _blocks = null; - } - else if (_mode == StorageMode.Contiguous && _contiguousBuffer is not null) - { - _arrayPool.Return(_contiguousBuffer); - _contiguousBuffer = null; - } - - // Buffers tracked here have been exposed to callers. Returning them to the - // shared pool would allow unrelated code to rent and mutate arrays that may - // still be referenced after the stream is disposed, which breaks - // MemoryStream-compatible expectations for GetBuffer/TryGetBuffer. - _detachedExposedBuffers.Clear(); + ReturnSegmentedBlocks(); + _blocks = null; } private static void ValidateReadWriteBufferArguments(byte[] buffer, int offset, int count) diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index e7b770e7..9c9571ba 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -33,6 +33,53 @@ public class PooledMemoryStreamTests Assert.All(pool.ReturnedLengths, length => Assert.Equal(8, length)); } + [Fact] + public void OverRentedBlocksUseLogicalBlockSize() + { + var pool = new FilledOverRentingArrayPool(extraLength: 8, fillValue: 0x5A); + + using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, arrayPool: pool); + stream.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); + + stream.Position = 10; + stream.Write(new byte[] { 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 }, 0, 10); + + Assert.Equal(3, pool.RentRequests.Count); + Assert.All(pool.RentRequests, requested => Assert.Equal(8, requested)); + Assert.All(pool.RentedLengths, length => Assert.Equal(16, length)); + + var expected = new byte[] + { + 1, + 2, + 3, + 4, + 5, + 0, + 0, + 0, + 0, + 0, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + }; + + Assert.Equal(expected, stream.ToArray()); + + stream.Position = 0; + var roundTrip = new byte[expected.Length]; + Assert.Equal(expected.Length, stream.Read(roundTrip, 0, roundTrip.Length)); + Assert.Equal(expected, roundTrip); + } + [Fact] public void GetBufferReturnsArraySizedToCapacityWithoutTouchingPool() { @@ -216,4 +263,40 @@ public class PooledMemoryStreamTests } } } + + private sealed class FilledOverRentingArrayPool : ArrayPool + { + private readonly int _extraLength; + private readonly byte _fillValue; + + public FilledOverRentingArrayPool(int extraLength, byte fillValue) + { + _extraLength = extraLength; + _fillValue = fillValue; + } + + public readonly System.Collections.Generic.List RentRequests = new(); + public readonly System.Collections.Generic.List RentedLengths = new(); + + public override byte[] Rent(int minimumLength) + { + RentRequests.Add(minimumLength); + + var array = new byte[minimumLength + _extraLength]; + RentedLengths.Add(array.Length); + for (var i = 0; i < array.Length; i++) + { + array[i] = _fillValue; + } + return array; + } + + public override void Return(byte[] array, bool clearArray = false) + { + if (clearArray) + { + Array.Clear(array, 0, array.Length); + } + } + } } From 4d774c6a7a756111e0f6a651f04d0253be7b988e Mon Sep 17 00:00:00 2001 From: aromaa Date: Fri, 10 Apr 2026 15:56:48 +0300 Subject: [PATCH 26/74] Fix setting invalid access time fails extraction --- src/SharpCompress/Common/IEntry.Extensions.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Common/IEntry.Extensions.cs b/src/SharpCompress/Common/IEntry.Extensions.cs index 7e9b79a3..7f0cda43 100644 --- a/src/SharpCompress/Common/IEntry.Extensions.cs +++ b/src/SharpCompress/Common/IEntry.Extensions.cs @@ -23,17 +23,38 @@ internal static class EntryExtensions { if (entry.CreatedTime.HasValue) { - nf.CreationTime = entry.CreatedTime.Value; + try + { + nf.CreationTime = entry.CreatedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } } if (entry.LastModifiedTime.HasValue) { - nf.LastWriteTime = entry.LastModifiedTime.Value; + try + { + nf.LastWriteTime = entry.LastModifiedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } } if (entry.LastAccessedTime.HasValue) { - nf.LastAccessTime = entry.LastAccessedTime.Value; + try + { + nf.LastAccessTime = entry.LastAccessedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } } } From ef67c0c3bdbd04ecbb8a40d59ac9d7e6aa5cac07 Mon Sep 17 00:00:00 2001 From: Dirk Lemstra Date: Fri, 10 Apr 2026 18:34:51 +0200 Subject: [PATCH 27/74] Corrected async examples. (#1277) --- docs/USAGE.md | 110 ++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index e28e50e4..f299f755 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -236,49 +236,37 @@ The registry also exposes `GetCompressingProvider` (now returning `ICompressionP **Extract single entry asynchronously:** ```C# -using (Stream stream = File.OpenRead("archive.zip")) -using (var reader = ReaderFactory.OpenReader(stream)) +using Stream stream = File.OpenRead("archive.zip"); +await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken); +while (await reader.MoveToNextEntryAsync(cancellationToken)) { - while (reader.MoveToNextEntry()) + if (!reader.Entry.IsDirectory) { - if (!reader.Entry.IsDirectory) - { - using (var entryStream = reader.OpenEntryStream()) - { - using (var outputStream = File.Create("output.bin")) - { - await reader.WriteEntryToAsync(outputStream, cancellationToken); - } - } - } + using var entryStream = await reader.OpenEntryStreamAsync(cancellationToken); + using var outputStream = File.Create("output.bin"); + await reader.WriteEntryToAsync(outputStream, cancellationToken); } } ``` **Extract all entries asynchronously:** ```C# -using (Stream stream = File.OpenRead("archive.tar.gz")) -using (var reader = ReaderFactory.OpenReader(stream)) -{ - await reader.WriteAllToDirectoryAsync( - @"D:\temp", - cancellationToken: cancellationToken - ); -} +using Stream stream = File.OpenRead("archive.tar.gz"); +await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken); +await reader.WriteAllToDirectoryAsync( + @"D:\temp", + cancellationToken: cancellationToken +); ``` **Open and process entry stream asynchronously:** ```C# -using (var archive = ZipArchive.OpenArchive("archive.zip")) +using var archive = ZipArchive.OpenArchive("archive.zip"); +foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) { - foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) - { - using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken)) - { - // Process the decompressed stream asynchronously - await ProcessStreamAsync(entryStream, cancellationToken); - } - } + using var entryStream = await entry.OpenEntryStreamAsync(cancellationToken); + // Process the decompressed stream asynchronously + await ProcessStreamAsync(entryStream, cancellationToken); } ``` @@ -286,28 +274,22 @@ using (var archive = ZipArchive.OpenArchive("archive.zip")) **Write single file asynchronously:** ```C# -using (Stream archiveStream = File.OpenWrite("output.zip")) -using (var writer = WriterFactory.OpenWriter(archiveStream, ArchiveType.Zip, CompressionType.Deflate)) -{ - using (Stream fileStream = File.OpenRead("input.txt")) - { - await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken); - } -} +using Stream archiveStream = File.OpenWrite("output.zip"); +await using var writer = await WriterFactory.OpenAsyncWriter(archiveStream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken); +using Stream fileStream = File.OpenRead("input.txt"); +await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken); ``` **Write entire directory asynchronously:** ```C# -using (Stream stream = File.OpenWrite("backup.tar.gz")) -using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip))) -{ - await writer.WriteAllAsync( - @"D:\files", - "*", - SearchOption.AllDirectories, - cancellationToken - ); -} +using Stream stream = File.OpenWrite("backup.tar.gz"); +await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip), cancellationToken); +await writer.WriteAllAsync( + @"D:\files", + "*", + SearchOption.AllDirectories, + cancellationToken +); ``` **Write with progress tracking and cancellation:** @@ -317,17 +299,15 @@ var cts = new CancellationTokenSource(); // Set timeout or cancel from UI cts.CancelAfter(TimeSpan.FromMinutes(5)); -using (Stream stream = File.OpenWrite("archive.zip")) -using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate)) +using Stream stream = File.OpenWrite("archive.zip"); +await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cts.Token); +try { - try - { - await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token); - } - catch (OperationCanceledException) - { - Console.WriteLine("Operation was cancelled"); - } + await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Operation was cancelled"); } ``` @@ -335,14 +315,12 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio **Extract from archive asynchronously:** ```C# -using (var archive = ZipArchive.OpenArchive("archive.zip")) -{ - // Simple async extraction - works for all archive types - await archive.WriteToDirectoryAsync( - @"C:\output", - cancellationToken: cancellationToken - ); -} +await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken); +// Simple async extraction - works for all archive types +await archive.WriteToDirectoryAsync( + @"C:\output", + cancellationToken: cancellationToken +); ``` **Benefits of Async Operations:** From da30b5c805a96270b559eaaf53b68f95b1eb4b2d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:47:33 +0100 Subject: [PATCH 28/74] Fix incorrect code examples in docs for sync/async usage (#1280) --- docs/API.md | 38 +++++++++++++++++++++++++------------- docs/USAGE.md | 14 ++++++++------ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/API.md b/docs/API.md index fdef287a..f730746d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -84,8 +84,8 @@ using (var archive = ZipArchive.OpenArchive("file.zip")) archive.WriteToDirectory(@"C:\output"); // Extract single entry - var entry = archive.Entries.First(); - entry.WriteToFile(@"C:\output\file.txt"); + var firstEntry = archive.Entries.First(); + firstEntry.WriteToFile(@"C:\output\file.txt"); // Get entry stream using (var stream = entry.OpenEntryStream()) @@ -97,14 +97,23 @@ using (var archive = ZipArchive.OpenArchive("file.zip")) // Async extraction (requires IAsyncArchive) await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip")) { + // Extract all entries asynchronously await asyncArchive.WriteToDirectoryAsync( @"C:\output", cancellationToken: cancellationToken ); } -using (var stream = await entry.OpenEntryStreamAsync(cancellationToken)) + +// Open a specific entry stream asynchronously +await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip")) { - // ... + await foreach (var entry in asyncArchive.EntriesAsync) + { + using (var stream = await entry.OpenEntryStreamAsync(cancellationToken)) + { + // ... + } + } } ``` @@ -214,15 +223,18 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio // Write directory writer.WriteAll("C:\\source", "*", SearchOption.AllDirectories); writer.WriteAll("C:\\source", "*.txt", SearchOption.TopDirectoryOnly); - - // Async variants - using (var fileStream = File.OpenRead("source.txt")) - { - await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken); - } - - await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken); } + +// Async variants: use OpenAsyncWriter to get IAsyncWriter +await using var stream = File.Create("output.zip"); +await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken); + +using (var fileStream = File.OpenRead("source.txt")) +{ + await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken); +} + +await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken); ``` --- @@ -390,7 +402,7 @@ ArchiveType.ZStandard ```csharp try { - using (var archive = ZipArchive.Open("archive.zip", + using (var archive = ZipArchive.OpenArchive("archive.zip", ReaderOptions.ForEncryptedArchive("password"))) { archive.WriteToDirectory(@"C:\output"); diff --git a/docs/USAGE.md b/docs/USAGE.md index f299f755..41f05cbf 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -242,7 +242,6 @@ while (await reader.MoveToNextEntryAsync(cancellationToken)) { if (!reader.Entry.IsDirectory) { - using var entryStream = await reader.OpenEntryStreamAsync(cancellationToken); using var outputStream = File.Create("output.bin"); await reader.WriteEntryToAsync(outputStream, cancellationToken); } @@ -261,12 +260,15 @@ await reader.WriteAllToDirectoryAsync( **Open and process entry stream asynchronously:** ```C# -using var archive = ZipArchive.OpenArchive("archive.zip"); -foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) +await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken); +await foreach (var entry in archive.EntriesAsync) { - using var entryStream = await entry.OpenEntryStreamAsync(cancellationToken); - // Process the decompressed stream asynchronously - await ProcessStreamAsync(entryStream, cancellationToken); + if (!entry.IsDirectory) + { + using var entryStream = await entry.OpenEntryStreamAsync(cancellationToken); + // Process the decompressed stream asynchronously + await ProcessStreamAsync(entryStream, cancellationToken); + } } ``` From 23e72452e3fae999a73be10e8f1bd04cb81bcb3f Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:45:36 +0900 Subject: [PATCH 29/74] Replace APPNOTE.TXT contents with reference link note --- reference/APPNOTE.TXT | 3501 +---------------------------------------- 1 file changed, 4 insertions(+), 3497 deletions(-) diff --git a/reference/APPNOTE.TXT b/reference/APPNOTE.TXT index c89d8a8e..5ddac229 100644 --- a/reference/APPNOTE.TXT +++ b/reference/APPNOTE.TXT @@ -1,3497 +1,4 @@ -File: APPNOTE.TXT - .ZIP File Format Specification -Version: 6.3.4 -Status: Final - replaces version 6.3.3 -Revised: October 1, 2014 -Copyright (c) 1989 - 2014 PKWARE Inc., All Rights Reserved. - -1.0 Introduction ---------------- - -1.1 Purpose ------------ - - 1.1.1 This specification is intended to define a cross-platform, - interoperable file storage and transfer format. Since its - first publication in 1989, PKWARE, Inc. ("PKWARE") has remained - committed to ensuring the interoperability of the .ZIP file - format through periodic publication and maintenance of this - specification. We trust that all .ZIP compatible vendors and - application developers that use and benefit from this format - will share and support this commitment to interoperability. - -1.2 Scope ---------- - - 1.2.1 ZIP is one of the most widely used compressed file formats. It is - universally used to aggregate, compress, and encrypt files into a single - interoperable container. No specific use or application need is - defined by this format and no specific implementation guidance is - provided. This document provides details on the storage format for - creating ZIP files. Information is provided on the records and - fields that describe what a ZIP file is. - -1.3 Trademarks --------------- - - 1.3.1 PKWARE, PKZIP, SecureZIP, and PKSFX are registered trademarks of - PKWARE, Inc. in the United States and elsewhere. PKPatchMaker, - Deflate64, and ZIP64 are trademarks of PKWARE, Inc. Other marks - referenced within this document appear for identification - purposes only and are the property of their respective owners. - - -1.4 Permitted Use ------------------ - - 1.4.1 This document, "APPNOTE.TXT - .ZIP File Format Specification" is the - exclusive property of PKWARE. Use of the information contained in this - document is permitted solely for the purpose of creating products, - programs and processes that read and write files in the ZIP format - subject to the terms and conditions herein. - - 1.4.2 Use of the content of this document within other publications is - permitted only through reference to this document. Any reproduction - or distribution of this document in whole or in part without prior - written permission from PKWARE is strictly prohibited. - - 1.4.3 Certain technological components provided in this document are the - patented proprietary technology of PKWARE and as such require a - separate, executed license agreement from PKWARE. Applicable - components are marked with the following, or similar, statement: - 'Refer to the section in this document entitled "Incorporating - PKWARE Proprietary Technology into Your Product" for more information'. - -1.5 Contacting PKWARE ---------------------- - - 1.5.1 If you have questions on this format, its use, or licensing, or if you - wish to report defects, request changes or additions, please contact: - - PKWARE, Inc. - 201 E. Pittsburgh Avenue, Suite 400 - Milwaukee, WI 53204 - +1-414-289-9788 - +1-414-289-9789 FAX - zipformat@pkware.com - - 1.5.2 Information about this format and copies of this document are publicly - available at: - - http://www.pkware.com/appnote - -1.6 Disclaimer --------------- - - 1.6.1 Although PKWARE will attempt to supply current and accurate - information relating to its file formats, algorithms, and the - subject programs, the possibility of error or omission cannot - be eliminated. PKWARE therefore expressly disclaims any warranty - that the information contained in the associated materials relating - to the subject programs and/or the format of the files created or - accessed by the subject programs and/or the algorithms used by - the subject programs, or any other matter, is current, correct or - accurate as delivered. Any risk of damage due to any possible - inaccurate information is assumed by the user of the information. - Furthermore, the information relating to the subject programs - and/or the file formats created or accessed by the subject - programs and/or the algorithms used by the subject programs is - subject to change without notice. - -2.0 Revisions --------------- - -2.1 Document Status --------------------- - - 2.1.1 If the STATUS of this file is marked as DRAFT, the content - defines proposed revisions to this specification which may consist - of changes to the ZIP format itself, or that may consist of other - content changes to this document. Versions of this document and - the format in DRAFT form may be subject to modification prior to - publication STATUS of FINAL. DRAFT versions are published periodically - to provide notification to the ZIP community of pending changes and to - provide opportunity for review and comment. - - 2.1.2 Versions of this document having a STATUS of FINAL are - considered to be in the final form for that version of the document - and are not subject to further change until a new, higher version - numbered document is published. Newer versions of this format - specification are intended to remain interoperable with with all prior - versions whenever technically possible. - -2.2 Change Log --------------- - - Version Change Description Date - ------- ------------------ ---------- - 5.2 -Single Password Symmetric Encryption 07/16/2003 - storage - - 6.1.0 -Smartcard compatibility 01/20/2004 - -Documentation on certificate storage - - 6.2.0 -Introduction of Central Directory 04/26/2004 - Encryption for encrypting metadata - -Added OS X to Version Made By values - - 6.2.1 -Added Extra Field placeholder for 04/01/2005 - POSZIP using ID 0x4690 - - -Clarified size field on - "zip64 end of central directory record" - - 6.2.2 -Documented Final Feature Specification 01/06/2006 - for Strong Encryption - - -Clarifications and typographical - corrections - - 6.3.0 -Added tape positioning storage 09/29/2006 - parameters - - -Expanded list of supported hash algorithms - - -Expanded list of supported compression - algorithms - - -Expanded list of supported encryption - algorithms - - -Added option for Unicode filename - storage - - -Clarifications for consistent use - of Data Descriptor records - - -Added additional "Extra Field" - definitions - - 6.3.1 -Corrected standard hash values for 04/11/2007 - SHA-256/384/512 - - 6.3.2 -Added compression method 97 09/28/2007 - - -Documented InfoZIP "Extra Field" - values for UTF-8 file name and - file comment storage - - 6.3.3 -Formatting changes to support 09/01/2012 - easier referencing of this APPNOTE - from other documents and standards - - 6.3.4 -Address change 10/01/2014 - - -3.0 Notations -------------- - - 3.1 Use of the term MUST or SHALL indicates a required element. - - 3.2 MAY NOT or SHALL NOT indicates an element is prohibited from use. - - 3.3 SHOULD indicates a RECOMMENDED element. - - 3.4 SHOULD NOT indicates an element NOT RECOMMENDED for use. - - 3.5 MAY indicates an OPTIONAL element. - - -4.0 ZIP Files -------------- - -4.1 What is a ZIP file ----------------------- - - 4.1.1 ZIP files MAY be identified by the standard .ZIP file extension - although use of a file extension is not required. Use of the - extension .ZIPX is also recognized and MAY be used for ZIP files. - Other common file extensions using the ZIP format include .JAR, .WAR, - .DOCX, .XLXS, .PPTX, .ODT, .ODS, .ODP and others. Programs reading or - writing ZIP files SHOULD rely on internal record signatures described - in this document to identify files in this format. - - 4.1.2 ZIP files SHOULD contain at least one file and MAY contain - multiple files. - - 4.1.3 Data compression MAY be used to reduce the size of files - placed into a ZIP file, but is not required. This format supports the - use of multiple data compression algorithms. When compression is used, - one of the documented compression algorithms MUST be used. Implementors - are advised to experiment with their data to determine which of the - available algorithms provides the best compression for their needs. - Compression method 8 (Deflate) is the method used by default by most - ZIP compatible application programs. - - - 4.1.4 Data encryption MAY be used to protect files within a ZIP file. - Keying methods supported for encryption within this format include - passwords and public/private keys. Either MAY be used individually - or in combination. Encryption MAY be applied to individual files. - Additional security MAY be used through the encryption of ZIP file - metadata stored within the Central Directory. See the section on the - Strong Encryption Specification for information. Refer to the section - in this document entitled "Incorporating PKWARE Proprietary Technology - into Your Product" for more information. - - 4.1.5 Data integrity MUST be provided for each file using CRC32. - - 4.1.6 Additional data integrity MAY be included through the use of - digital signatures. Individual files MAY be signed with one or more - digital signatures. The Central Directory, if signed, MUST use a - single signature. - - 4.1.7 Files MAY be placed within a ZIP file uncompressed or stored. - The term "stored" as used in the context of this document means the file - is copied into the ZIP file uncompressed. - - 4.1.8 Each data file placed into a ZIP file MAY be compressed, stored, - encrypted or digitally signed independent of how other data files in the - same ZIP file are archived. - - 4.1.9 ZIP files MAY be streamed, split into segments (on fixed or on - removable media) or "self-extracting". Self-extracting ZIP - files MUST include extraction code for a target platform within - the ZIP file. - - 4.1.10 Extensibility is provided for platform or application specific - needs through extra data fields that MAY be defined for custom - purposes. Extra data definitions MUST NOT conflict with existing - documented record definitions. - - 4.1.11 Common uses for ZIP MAY also include the use of manifest files. - Manifest files store application specific information within a file stored - within the ZIP file. This manifest file SHOULD be the first file in the - ZIP file. This specification does not provide any information or guidance on - the use of manifest files within ZIP files. Refer to the application developer - for information on using manifest files and for any additional profile - information on using ZIP within an application. - - 4.1.12 ZIP files MAY be placed within other ZIP files. - -4.2 ZIP Metadata ----------------- - - 4.2.1 ZIP files are identified by metadata consisting of defined record types - containing the storage information necessary for maintaining the files - placed into a ZIP file. Each record type MUST be identified using a header - signature that identifies the record type. Signature values begin with the - two byte constant marker of 0x4b50, representing the characters "PK". - - -4.3 General Format of a .ZIP file ---------------------------------- - - 4.3.1 A ZIP file MUST contain an "end of central directory record". A ZIP - file containing only an "end of central directory record" is considered an - empty ZIP file. Files may be added or replaced within a ZIP file, or deleted. - A ZIP file MUST have only one "end of central directory record". Other - records defined in this specification MAY be used as needed to support - storage requirements for individual ZIP files. - - 4.3.2 Each file placed into a ZIP file MUST be preceeded by a "local - file header" record for that file. Each "local file header" MUST be - accompanied by a corresponding "central directory header" record within - the central directory section of the ZIP file. - - 4.3.3 Files MAY be stored in arbitrary order within a ZIP file. A ZIP - file MAY span multiple volumes or it MAY be split into user-defined - segment sizes. All values MUST be stored in little-endian byte order unless - otherwise specified in this document for a specific data element. - - 4.3.4 Compression MUST NOT be applied to a "local file header", an "encryption - header", or an "end of central directory record". Individual "central - directory records" must not be compressed, but the aggregate of all central - directory records MAY be compressed. - - 4.3.5 File data MAY be followed by a "data descriptor" for the file. Data - descriptors are used to facilitate ZIP file streaming. - - - 4.3.6 Overall .ZIP file format: - - [local file header 1] - [encryption header 1] - [file data 1] - [data descriptor 1] - . - . - . - [local file header n] - [encryption header n] - [file data n] - [data descriptor n] - [archive decryption header] - [archive extra data record] - [central directory header 1] - . - . - . - [central directory header n] - [zip64 end of central directory record] - [zip64 end of central directory locator] - [end of central directory record] - - - 4.3.7 Local file header: - - local file header signature 4 bytes (0x04034b50) - version needed to extract 2 bytes - general purpose bit flag 2 bytes - compression method 2 bytes - last mod file time 2 bytes - last mod file date 2 bytes - crc-32 4 bytes - compressed size 4 bytes - uncompressed size 4 bytes - file name length 2 bytes - extra field length 2 bytes - - file name (variable size) - extra field (variable size) - - 4.3.8 File data - - Immediately following the local header for a file - SHOULD be placed the compressed or stored data for the file. - If the file is encrypted, the encryption header for the file - SHOULD be placed after the local header and before the file - data. The series of [local file header][encryption header] - [file data][data descriptor] repeats for each file in the - .ZIP archive. - - Zero-byte files, directories, and other file types that - contain no content MUST not include file data. - - 4.3.9 Data descriptor: - - crc-32 4 bytes - compressed size 4 bytes - uncompressed size 4 bytes - - 4.3.9.1 This descriptor MUST exist if bit 3 of the general - purpose bit flag is set (see below). It is byte aligned - and immediately follows the last byte of compressed data. - This descriptor SHOULD be used only when it was not possible to - seek in the output .ZIP file, e.g., when the output .ZIP file - was standard output or a non-seekable device. For ZIP64(tm) format - archives, the compressed and uncompressed sizes are 8 bytes each. - - 4.3.9.2 When compressing files, compressed and uncompressed sizes - should be stored in ZIP64 format (as 8 byte values) when a - file's size exceeds 0xFFFFFFFF. However ZIP64 format may be - used regardless of the size of a file. When extracting, if - the zip64 extended information extra field is present for - the file the compressed and uncompressed sizes will be 8 - byte values. - - 4.3.9.3 Although not originally assigned a signature, the value - 0x08074b50 has commonly been adopted as a signature value - for the data descriptor record. Implementers should be - aware that ZIP files may be encountered with or without this - signature marking data descriptors and SHOULD account for - either case when reading ZIP files to ensure compatibility. - - 4.3.9.4 When writing ZIP files, implementors SHOULD include the - signature value marking the data descriptor record. When - the signature is used, the fields currently defined for - the data descriptor record will immediately follow the - signature. - - 4.3.9.5 An extensible data descriptor will be released in a - future version of this APPNOTE. This new record is intended to - resolve conflicts with the use of this record going forward, - and to provide better support for streamed file processing. - - 4.3.9.6 When the Central Directory Encryption method is used, - the data descriptor record is not required, but MAY be used. - If present, and bit 3 of the general purpose bit field is set to - indicate its presence, the values in fields of the data descriptor - record MUST be set to binary zeros. See the section on the Strong - Encryption Specification for information. Refer to the section in - this document entitled "Incorporating PKWARE Proprietary Technology - into Your Product" for more information. - - - 4.3.10 Archive decryption header: - - 4.3.10.1 The Archive Decryption Header is introduced in version 6.2 - of the ZIP format specification. This record exists in support - of the Central Directory Encryption Feature implemented as part of - the Strong Encryption Specification as described in this document. - When the Central Directory Structure is encrypted, this decryption - header MUST precede the encrypted data segment. - - 4.3.10.2 The encrypted data segment SHALL consist of the Archive - extra data record (if present) and the encrypted Central Directory - Structure data. The format of this data record is identical to the - Decryption header record preceding compressed file data. If the - central directory structure is encrypted, the location of the start of - this data record is determined using the Start of Central Directory - field in the Zip64 End of Central Directory record. See the - section on the Strong Encryption Specification for information - on the fields used in the Archive Decryption Header record. - Refer to the section in this document entitled "Incorporating - PKWARE Proprietary Technology into Your Product" for more information. - - - 4.3.11 Archive extra data record: - - archive extra data signature 4 bytes (0x08064b50) - extra field length 4 bytes - extra field data (variable size) - - 4.3.11.1 The Archive Extra Data Record is introduced in version 6.2 - of the ZIP format specification. This record MAY be used in support - of the Central Directory Encryption Feature implemented as part of - the Strong Encryption Specification as described in this document. - When present, this record MUST immediately precede the central - directory data structure. - - 4.3.11.2 The size of this data record SHALL be included in the - Size of the Central Directory field in the End of Central - Directory record. If the central directory structure is compressed, - but not encrypted, the location of the start of this data record is - determined using the Start of Central Directory field in the Zip64 - End of Central Directory record. Refer to the section in this document - entitled "Incorporating PKWARE Proprietary Technology into Your - Product" for more information. - - 4.3.12 Central directory structure: - - [central directory header 1] - . - . - . - [central directory header n] - [digital signature] - - File header: - - central file header signature 4 bytes (0x02014b50) - version made by 2 bytes - version needed to extract 2 bytes - general purpose bit flag 2 bytes - compression method 2 bytes - last mod file time 2 bytes - last mod file date 2 bytes - crc-32 4 bytes - compressed size 4 bytes - uncompressed size 4 bytes - file name length 2 bytes - extra field length 2 bytes - file comment length 2 bytes - disk number start 2 bytes - internal file attributes 2 bytes - external file attributes 4 bytes - relative offset of local header 4 bytes - - file name (variable size) - extra field (variable size) - file comment (variable size) - - 4.3.13 Digital signature: - - header signature 4 bytes (0x05054b50) - size of data 2 bytes - signature data (variable size) - - With the introduction of the Central Directory Encryption - feature in version 6.2 of this specification, the Central - Directory Structure MAY be stored both compressed and encrypted. - Although not required, it is assumed when encrypting the - Central Directory Structure, that it will be compressed - for greater storage efficiency. Information on the - Central Directory Encryption feature can be found in the section - describing the Strong Encryption Specification. The Digital - Signature record will be neither compressed nor encrypted. - - 4.3.14 Zip64 end of central directory record - - zip64 end of central dir - signature 4 bytes (0x06064b50) - size of zip64 end of central - directory record 8 bytes - version made by 2 bytes - version needed to extract 2 bytes - number of this disk 4 bytes - number of the disk with the - start of the central directory 4 bytes - total number of entries in the - central directory on this disk 8 bytes - total number of entries in the - central directory 8 bytes - size of the central directory 8 bytes - offset of start of central - directory with respect to - the starting disk number 8 bytes - zip64 extensible data sector (variable size) - - 4.3.14.1 The value stored into the "size of zip64 end of central - directory record" should be the size of the remaining - record and should not include the leading 12 bytes. - - Size = SizeOfFixedFields + SizeOfVariableData - 12. - - 4.3.14.2 The above record structure defines Version 1 of the - zip64 end of central directory record. Version 1 was - implemented in versions of this specification preceding - 6.2 in support of the ZIP64 large file feature. The - introduction of the Central Directory Encryption feature - implemented in version 6.2 as part of the Strong Encryption - Specification defines Version 2 of this record structure. - Refer to the section describing the Strong Encryption - Specification for details on the version 2 format for - this record. Refer to the section in this document entitled - "Incorporating PKWARE Proprietary Technology into Your Product" - for more information applicable to use of Version 2 of this - record. - - 4.3.14.3 Special purpose data MAY reside in the zip64 extensible - data sector field following either a V1 or V2 version of this - record. To ensure identification of this special purpose data - it must include an identifying header block consisting of the - following: - - Header ID - 2 bytes - Data Size - 4 bytes - - The Header ID field indicates the type of data that is in the - data block that follows. - - Data Size identifies the number of bytes that follow for this - data block type. - - 4.3.14.4 Multiple special purpose data blocks MAY be present. - Each MUST be preceded by a Header ID and Data Size field. Current - mappings of Header ID values supported in this field are as - defined in APPENDIX C. - - 4.3.15 Zip64 end of central directory locator - - zip64 end of central dir locator - signature 4 bytes (0x07064b50) - number of the disk with the - start of the zip64 end of - central directory 4 bytes - relative offset of the zip64 - end of central directory record 8 bytes - total number of disks 4 bytes - - 4.3.16 End of central directory record: - - end of central dir signature 4 bytes (0x06054b50) - number of this disk 2 bytes - number of the disk with the - start of the central directory 2 bytes - total number of entries in the - central directory on this disk 2 bytes - total number of entries in - the central directory 2 bytes - size of the central directory 4 bytes - offset of start of central - directory with respect to - the starting disk number 4 bytes - .ZIP file comment length 2 bytes - .ZIP file comment (variable size) - -4.4 Explanation of fields --------------------------- - - 4.4.1 General notes on fields - - 4.4.1.1 All fields unless otherwise noted are unsigned and stored - in Intel low-byte:high-byte, low-word:high-word order. - - 4.4.1.2 String fields are not null terminated, since the length - is given explicitly. - - 4.4.1.3 The entries in the central directory may not necessarily - be in the same order that files appear in the .ZIP file. - - 4.4.1.4 If one of the fields in the end of central directory - record is too small to hold required data, the field should be - set to -1 (0xFFFF or 0xFFFFFFFF) and the ZIP64 format record - should be created. - - 4.4.1.5 The end of central directory record and the Zip64 end - of central directory locator record MUST reside on the same - disk when splitting or spanning an archive. - - 4.4.2 version made by (2 bytes) - - 4.4.2.1 The upper byte indicates the compatibility of the file - attribute information. If the external file attributes - are compatible with MS-DOS and can be read by PKZIP for - DOS version 2.04g then this value will be zero. If these - attributes are not compatible, then this value will - identify the host system on which the attributes are - compatible. Software can use this information to determine - the line record format for text files etc. - - 4.4.2.2 The current mappings are: - - 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) - 1 - Amiga 2 - OpenVMS - 3 - UNIX 4 - VM/CMS - 5 - Atari ST 6 - OS/2 H.P.F.S. - 7 - Macintosh 8 - Z-System - 9 - CP/M 10 - Windows NTFS - 11 - MVS (OS/390 - Z/OS) 12 - VSE - 13 - Acorn Risc 14 - VFAT - 15 - alternate MVS 16 - BeOS - 17 - Tandem 18 - OS/400 - 19 - OS X (Darwin) 20 thru 255 - unused - - 4.4.2.3 The lower byte indicates the ZIP specification version - (the version of this document) supported by the software - used to encode the file. The value/10 indicates the major - version number, and the value mod 10 is the minor version - number. - - 4.4.3 version needed to extract (2 bytes) - - 4.4.3.1 The minimum supported ZIP specification version needed - to extract the file, mapped as above. This value is based on - the specific format features a ZIP program MUST support to - be able to extract the file. If multiple features are - applied to a file, the minimum version MUST be set to the - feature having the highest value. New features or feature - changes affecting the published format specification will be - implemented using higher version numbers than the last - published value to avoid conflict. - - 4.4.3.2 Current minimum feature versions are as defined below: - - 1.0 - Default value - 1.1 - File is a volume label - 2.0 - File is a folder (directory) - 2.0 - File is compressed using Deflate compression - 2.0 - File is encrypted using traditional PKWARE encryption - 2.1 - File is compressed using Deflate64(tm) - 2.5 - File is compressed using PKWARE DCL Implode - 2.7 - File is a patch data set - 4.5 - File uses ZIP64 format extensions - 4.6 - File is compressed using BZIP2 compression* - 5.0 - File is encrypted using DES - 5.0 - File is encrypted using 3DES - 5.0 - File is encrypted using original RC2 encryption - 5.0 - File is encrypted using RC4 encryption - 5.1 - File is encrypted using AES encryption - 5.1 - File is encrypted using corrected RC2 encryption** - 5.2 - File is encrypted using corrected RC2-64 encryption** - 6.1 - File is encrypted using non-OAEP key wrapping*** - 6.2 - Central directory encryption - 6.3 - File is compressed using LZMA - 6.3 - File is compressed using PPMd+ - 6.3 - File is encrypted using Blowfish - 6.3 - File is encrypted using Twofish - - 4.4.3.3 Notes on version needed to extract - - * Early 7.x (pre-7.2) versions of PKZIP incorrectly set the - version needed to extract for BZIP2 compression to be 50 - when it should have been 46. - - ** Refer to the section on Strong Encryption Specification - for additional information regarding RC2 corrections. - - *** Certificate encryption using non-OAEP key wrapping is the - intended mode of operation for all versions beginning with 6.1. - Support for OAEP key wrapping MUST only be used for - backward compatibility when sending ZIP files to be opened by - versions of PKZIP older than 6.1 (5.0 or 6.0). - - + Files compressed using PPMd MUST set the version - needed to extract field to 6.3, however, not all ZIP - programs enforce this and may be unable to decompress - data files compressed using PPMd if this value is set. - - When using ZIP64 extensions, the corresponding value in the - zip64 end of central directory record MUST also be set. - This field should be set appropriately to indicate whether - Version 1 or Version 2 format is in use. - - - 4.4.4 general purpose bit flag: (2 bytes) - - Bit 0: If set, indicates that the file is encrypted. - - (For Method 6 - Imploding) - Bit 1: If the compression method used was type 6, - Imploding, then this bit, if set, indicates - an 8K sliding dictionary was used. If clear, - then a 4K sliding dictionary was used. - - Bit 2: If the compression method used was type 6, - Imploding, then this bit, if set, indicates - 3 Shannon-Fano trees were used to encode the - sliding dictionary output. If clear, then 2 - Shannon-Fano trees were used. - - (For Methods 8 and 9 - Deflating) - Bit 2 Bit 1 - 0 0 Normal (-en) compression option was used. - 0 1 Maximum (-exx/-ex) compression option was used. - 1 0 Fast (-ef) compression option was used. - 1 1 Super Fast (-es) compression option was used. - - (For Method 14 - LZMA) - Bit 1: If the compression method used was type 14, - LZMA, then this bit, if set, indicates - an end-of-stream (EOS) marker is used to - mark the end of the compressed data stream. - If clear, then an EOS marker is not present - and the compressed data size must be known - to extract. - - Note: Bits 1 and 2 are undefined if the compression - method is any other. - - Bit 3: If this bit is set, the fields crc-32, compressed - size and uncompressed size are set to zero in the - local header. The correct values are put in the - data descriptor immediately following the compressed - data. (Note: PKZIP version 2.04g for DOS only - recognizes this bit for method 8 compression, newer - versions of PKZIP recognize this bit for any - compression method.) - - Bit 4: Reserved for use with method 8, for enhanced - deflating. - - Bit 5: If this bit is set, this indicates that the file is - compressed patched data. (Note: Requires PKZIP - version 2.70 or greater) - - Bit 6: Strong encryption. If this bit is set, you MUST - set the version needed to extract value to at least - 50 and you MUST also set bit 0. If AES encryption - is used, the version needed to extract value MUST - be at least 51. See the section describing the Strong - Encryption Specification for details. Refer to the - section in this document entitled "Incorporating PKWARE - Proprietary Technology into Your Product" for more - information. - - Bit 7: Currently unused. - - Bit 8: Currently unused. - - Bit 9: Currently unused. - - Bit 10: Currently unused. - - Bit 11: Language encoding flag (EFS). If this bit is set, - the filename and comment fields for this file - MUST be encoded using UTF-8. (see APPENDIX D) - - Bit 12: Reserved by PKWARE for enhanced compression. - - Bit 13: Set when encrypting the Central Directory to indicate - selected data values in the Local Header are masked to - hide their actual values. See the section describing - the Strong Encryption Specification for details. Refer - to the section in this document entitled "Incorporating - PKWARE Proprietary Technology into Your Product" for - more information. - - Bit 14: Reserved by PKWARE. - - Bit 15: Reserved by PKWARE. - - 4.4.5 compression method: (2 bytes) - - 0 - The file is stored (no compression) - 1 - The file is Shrunk - 2 - The file is Reduced with compression factor 1 - 3 - The file is Reduced with compression factor 2 - 4 - The file is Reduced with compression factor 3 - 5 - The file is Reduced with compression factor 4 - 6 - The file is Imploded - 7 - Reserved for Tokenizing compression algorithm - 8 - The file is Deflated - 9 - Enhanced Deflating using Deflate64(tm) - 10 - PKWARE Data Compression Library Imploding (old IBM TERSE) - 11 - Reserved by PKWARE - 12 - File is compressed using BZIP2 algorithm - 13 - Reserved by PKWARE - 14 - LZMA (EFS) - 15 - Reserved by PKWARE - 16 - Reserved by PKWARE - 17 - Reserved by PKWARE - 18 - File is compressed using IBM TERSE (new) - 19 - IBM LZ77 z Architecture (PFS) - 97 - WavPack compressed data - 98 - PPMd version I, Rev 1 - - - 4.4.6 date and time fields: (2 bytes each) - - The date and time are encoded in standard MS-DOS format. - If input came from standard input, the date and time are - those at which compression was started for this data. - If encrypting the central directory and general purpose bit - flag 13 is set indicating masking, the value stored in the - Local Header will be zero. - - 4.4.7 CRC-32: (4 bytes) - - The CRC-32 algorithm was generously contributed by - David Schwaderer and can be found in his excellent - book "C Programmers Guide to NetBIOS" published by - Howard W. Sams & Co. Inc. The 'magic number' for - the CRC is 0xdebb20e3. The proper CRC pre and post - conditioning is used, meaning that the CRC register - is pre-conditioned with all ones (a starting value - of 0xffffffff) and the value is post-conditioned by - taking the one's complement of the CRC residual. - If bit 3 of the general purpose flag is set, this - field is set to zero in the local header and the correct - value is put in the data descriptor and in the central - directory. When encrypting the central directory, if the - local header is not in ZIP64 format and general purpose - bit flag 13 is set indicating masking, the value stored - in the Local Header will be zero. - - 4.4.8 compressed size: (4 bytes) - 4.4.9 uncompressed size: (4 bytes) - - The size of the file compressed (4.4.8) and uncompressed, - (4.4.9) respectively. When a decryption header is present it - will be placed in front of the file data and the value of the - compressed file size will include the bytes of the decryption - header. If bit 3 of the general purpose bit flag is set, - these fields are set to zero in the local header and the - correct values are put in the data descriptor and - in the central directory. If an archive is in ZIP64 format - and the value in this field is 0xFFFFFFFF, the size will be - in the corresponding 8 byte ZIP64 extended information - extra field. When encrypting the central directory, if the - local header is not in ZIP64 format and general purpose bit - flag 13 is set indicating masking, the value stored for the - uncompressed size in the Local Header will be zero. - - 4.4.10 file name length: (2 bytes) - 4.4.11 extra field length: (2 bytes) - 4.4.12 file comment length: (2 bytes) - - The length of the file name, extra field, and comment - fields respectively. The combined length of any - directory record and these three fields should not - generally exceed 65,535 bytes. If input came from standard - input, the file name length is set to zero. - - - 4.4.13 disk number start: (2 bytes) - - The number of the disk on which this file begins. If an - archive is in ZIP64 format and the value in this field is - 0xFFFF, the size will be in the corresponding 4 byte zip64 - extended information extra field. - - 4.4.14 internal file attributes: (2 bytes) - - Bits 1 and 2 are reserved for use by PKWARE. - - 4.4.14.1 The lowest bit of this field indicates, if set, - that the file is apparently an ASCII or text file. If not - set, that the file apparently contains binary data. - The remaining bits are unused in version 1.0. - - 4.4.14.2 The 0x0002 bit of this field indicates, if set, that - a 4 byte variable record length control field precedes each - logical record indicating the length of the record. The - record length control field is stored in little-endian byte - order. This flag is independent of text control characters, - and if used in conjunction with text data, includes any - control characters in the total length of the record. This - value is provided for mainframe data transfer support. - - 4.4.15 external file attributes: (4 bytes) - - The mapping of the external attributes is - host-system dependent (see 'version made by'). For - MS-DOS, the low order byte is the MS-DOS directory - attribute byte. If input came from standard input, this - field is set to zero. - - 4.4.16 relative offset of local header: (4 bytes) - - This is the offset from the start of the first disk on - which this file appears, to where the local header should - be found. If an archive is in ZIP64 format and the value - in this field is 0xFFFFFFFF, the size will be in the - corresponding 8 byte zip64 extended information extra field. - - 4.4.17 file name: (Variable) - - 4.4.17.1 The name of the file, with optional relative path. - The path stored MUST not contain a drive or - device letter, or a leading slash. All slashes - MUST be forward slashes '/' as opposed to - backwards slashes '\' for compatibility with Amiga - and UNIX file systems etc. If input came from standard - input, there is no file name field. - - 4.4.17.2 If using the Central Directory Encryption Feature and - general purpose bit flag 13 is set indicating masking, the file - name stored in the Local Header will not be the actual file name. - A masking value consisting of a unique hexadecimal value will - be stored. This value will be sequentially incremented for each - file in the archive. See the section on the Strong Encryption - Specification for details on retrieving the encrypted file name. - Refer to the section in this document entitled "Incorporating PKWARE - Proprietary Technology into Your Product" for more information. - - - 4.4.18 file comment: (Variable) - - The comment for this file. - - 4.4.19 number of this disk: (2 bytes) - - The number of this disk, which contains central - directory end record. If an archive is in ZIP64 format - and the value in this field is 0xFFFF, the size will - be in the corresponding 4 byte zip64 end of central - directory field. - - - 4.4.20 number of the disk with the start of the central - directory: (2 bytes) - - The number of the disk on which the central - directory starts. If an archive is in ZIP64 format - and the value in this field is 0xFFFF, the size will - be in the corresponding 4 byte zip64 end of central - directory field. - - 4.4.21 total number of entries in the central dir on - this disk: (2 bytes) - - The number of central directory entries on this disk. - If an archive is in ZIP64 format and the value in - this field is 0xFFFF, the size will be in the - corresponding 8 byte zip64 end of central - directory field. - - 4.4.22 total number of entries in the central dir: (2 bytes) - - The total number of files in the .ZIP file. If an - archive is in ZIP64 format and the value in this field - is 0xFFFF, the size will be in the corresponding 8 byte - zip64 end of central directory field. - - 4.4.23 size of the central directory: (4 bytes) - - The size (in bytes) of the entire central directory. - If an archive is in ZIP64 format and the value in - this field is 0xFFFFFFFF, the size will be in the - corresponding 8 byte zip64 end of central - directory field. - - 4.4.24 offset of start of central directory with respect to - the starting disk number: (4 bytes) - - Offset of the start of the central directory on the - disk on which the central directory starts. If an - archive is in ZIP64 format and the value in this - field is 0xFFFFFFFF, the size will be in the - corresponding 8 byte zip64 end of central - directory field. - - 4.4.25 .ZIP file comment length: (2 bytes) - - The length of the comment for this .ZIP file. - - 4.4.26 .ZIP file comment: (Variable) - - The comment for this .ZIP file. ZIP file comment data - is stored unsecured. No encryption or data authentication - is applied to this area at this time. Confidential information - should not be stored in this section. - - 4.4.27 zip64 extensible data sector (variable size) - - (currently reserved for use by PKWARE) - - - 4.4.28 extra field: (Variable) - - This SHOULD be used for storage expansion. If additional - information needs to be stored within a ZIP file for special - application or platform needs, it SHOULD be stored here. - Programs supporting earlier versions of this specification can - then safely skip the file, and find the next file or header. - This field will be 0 length in version 1.0. - - Existing extra fields are defined in the section - Extensible data fields that follows. - -4.5 Extensible data fields --------------------------- - - 4.5.1 In order to allow different programs and different types - of information to be stored in the 'extra' field in .ZIP - files, the following structure MUST be used for all - programs storing data in this field: - - header1+data1 + header2+data2 . . . - - Each header should consist of: - - Header ID - 2 bytes - Data Size - 2 bytes - - Note: all fields stored in Intel low-byte/high-byte order. - - The Header ID field indicates the type of data that is in - the following data block. - - Header IDs of 0 thru 31 are reserved for use by PKWARE. - The remaining IDs can be used by third party vendors for - proprietary usage. - - 4.5.2 The current Header ID mappings defined by PKWARE are: - - 0x0001 Zip64 extended information extra field - 0x0007 AV Info - 0x0008 Reserved for extended language encoding data (PFS) - (see APPENDIX D) - 0x0009 OS/2 - 0x000a NTFS - 0x000c OpenVMS - 0x000d UNIX - 0x000e Reserved for file stream and fork descriptors - 0x000f Patch Descriptor - 0x0014 PKCS#7 Store for X.509 Certificates - 0x0015 X.509 Certificate ID and Signature for - individual file - 0x0016 X.509 Certificate ID for Central Directory - 0x0017 Strong Encryption Header - 0x0018 Record Management Controls - 0x0019 PKCS#7 Encryption Recipient Certificate List - 0x0065 IBM S/390 (Z390), AS/400 (I400) attributes - - uncompressed - 0x0066 Reserved for IBM S/390 (Z390), AS/400 (I400) - attributes - compressed - 0x4690 POSZIP 4690 (reserved) - - - 4.5.3 -Zip64 Extended Information Extra Field (0x0001): - - The following is the layout of the zip64 extended - information "extra" block. If one of the size or - offset fields in the Local or Central directory - record is too small to hold the required data, - a Zip64 extended information record is created. - The order of the fields in the zip64 extended - information record is fixed, but the fields MUST - only appear if the corresponding Local or Central - directory record field is set to 0xFFFF or 0xFFFFFFFF. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(ZIP64) 0x0001 2 bytes Tag for this "extra" block type - Size 2 bytes Size of this "extra" block - Original - Size 8 bytes Original uncompressed file size - Compressed - Size 8 bytes Size of compressed data - Relative Header - Offset 8 bytes Offset of local header record - Disk Start - Number 4 bytes Number of the disk on which - this file starts - - This entry in the Local header MUST include BOTH original - and compressed file size fields. If encrypting the - central directory and bit 13 of the general purpose bit - flag is set indicating masking, the value stored in the - Local Header for the original file size will be zero. - - - 4.5.4 -OS/2 Extra Field (0x0009): - - The following is the layout of the OS/2 attributes "extra" - block. (Last Revision 09/05/95) - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(OS/2) 0x0009 2 bytes Tag for this "extra" block type - TSize 2 bytes Size for the following data block - BSize 4 bytes Uncompressed Block Size - CType 2 bytes Compression type - EACRC 4 bytes CRC value for uncompress block - (var) variable Compressed block - - The OS/2 extended attribute structure (FEA2LIST) is - compressed and then stored in its entirety within this - structure. There will only ever be one "block" of data in - VarFields[]. - - 4.5.5 -NTFS Extra Field (0x000a): - - The following is the layout of the NTFS attributes - "extra" block. (Note: At this time the Mtime, Atime - and Ctime values MAY be used on any WIN32 system.) - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(NTFS) 0x000a 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of the total "extra" block - Reserved 4 bytes Reserved for future use - Tag1 2 bytes NTFS attribute tag value #1 - Size1 2 bytes Size of attribute #1, in bytes - (var) Size1 Attribute #1 data - . - . - . - TagN 2 bytes NTFS attribute tag value #N - SizeN 2 bytes Size of attribute #N, in bytes - (var) SizeN Attribute #N data - - For NTFS, values for Tag1 through TagN are as follows: - (currently only one set of attributes is defined for NTFS) - - Tag Size Description - ----- ---- ----------- - 0x0001 2 bytes Tag for attribute #1 - Size1 2 bytes Size of attribute #1, in bytes - Mtime 8 bytes File last modification time - Atime 8 bytes File last access time - Ctime 8 bytes File creation time - - 4.5.6 -OpenVMS Extra Field (0x000c): - - The following is the layout of the OpenVMS attributes - "extra" block. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- - (VMS) 0x000c 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of the total "extra" block - CRC 4 bytes 32-bit CRC for remainder of the block - Tag1 2 bytes OpenVMS attribute tag value #1 - Size1 2 bytes Size of attribute #1, in bytes - (var) Size1 Attribute #1 data - . - . - . - TagN 2 bytes OpenVMS attribute tag value #N - SizeN 2 bytes Size of attribute #N, in bytes - (var) SizeN Attribute #N data - - OpenVMS Extra Field Rules: - - 4.5.6.1. There will be one or more attributes present, which - will each be preceded by the above TagX & SizeX values. - These values are identical to the ATR$C_XXXX and ATR$S_XXXX - constants which are defined in ATR.H under OpenVMS C. Neither - of these values will ever be zero. - - 4.5.6.2. No word alignment or padding is performed. - - 4.5.6.3. A well-behaved PKZIP/OpenVMS program should never produce - more than one sub-block with the same TagX value. Also, there will - never be more than one "extra" block of type 0x000c in a particular - directory record. - - 4.5.7 -UNIX Extra Field (0x000d): - - The following is the layout of the UNIX "extra" block. - Note: all fields are stored in Intel low-byte/high-byte - order. - - Value Size Description - ----- ---- ----------- -(UNIX) 0x000d 2 bytes Tag for this "extra" block type - TSize 2 bytes Size for the following data block - Atime 4 bytes File last access time - Mtime 4 bytes File last modification time - Uid 2 bytes File user ID - Gid 2 bytes File group ID - (var) variable Variable length data field - - The variable length data field will contain file type - specific data. Currently the only values allowed are - the original "linked to" file names for hard or symbolic - links, and the major and minor device node numbers for - character and block device nodes. Since device nodes - cannot be either symbolic or hard links, only one set of - variable length data is stored. Link files will have the - name of the original file stored. This name is NOT NULL - terminated. Its size can be determined by checking TSize - - 12. Device entries will have eight bytes stored as two 4 - byte entries (in little endian format). The first entry - will be the major device number, and the second the minor - device number. - - 4.5.8 -PATCH Descriptor Extra Field (0x000f): - - 4.5.8.1 The following is the layout of the Patch Descriptor - "extra" block. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(Patch) 0x000f 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of the total "extra" block - Version 2 bytes Version of the descriptor - Flags 4 bytes Actions and reactions (see below) - OldSize 4 bytes Size of the file about to be patched - OldCRC 4 bytes 32-bit CRC of the file to be patched - NewSize 4 bytes Size of the resulting file - NewCRC 4 bytes 32-bit CRC of the resulting file - - 4.5.8.2 Actions and reactions - - Bits Description - ---- ---------------- - 0 Use for auto detection - 1 Treat as a self-patch - 2-3 RESERVED - 4-5 Action (see below) - 6-7 RESERVED - 8-9 Reaction (see below) to absent file - 10-11 Reaction (see below) to newer file - 12-13 Reaction (see below) to unknown file - 14-15 RESERVED - 16-31 RESERVED - - 4.5.8.2.1 Actions - - Action Value - ------ ----- - none 0 - add 1 - delete 2 - patch 3 - - 4.5.8.2.2 Reactions - - Reaction Value - -------- ----- - ask 0 - skip 1 - ignore 2 - fail 3 - - 4.5.8.3 Patch support is provided by PKPatchMaker(tm) technology - and is covered under U.S. Patents and Patents Pending. The use or - implementation in a product of certain technological aspects set - forth in the current APPNOTE, including those with regard to - strong encryption or patching requires a license from PKWARE. - Refer to the section in this document entitled "Incorporating - PKWARE Proprietary Technology into Your Product" for more - information. - - 4.5.9 -PKCS#7 Store for X.509 Certificates (0x0014): - - This field MUST contain information about each of the certificates - files may be signed with. When the Central Directory Encryption - feature is enabled for a ZIP file, this record will appear in - the Archive Extra Data Record, otherwise it will appear in the - first central directory record and will be ignored in any - other record. - - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(Store) 0x0014 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of the store data - TData TSize Data about the store - - - 4.5.10 -X.509 Certificate ID and Signature for individual file (0x0015): - - This field contains the information about which certificate in - the PKCS#7 store was used to sign a particular file. It also - contains the signature data. This field can appear multiple - times, but can only appear once per certificate. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(CID) 0x0015 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of data that follows - TData TSize Signature Data - - 4.5.11 -X.509 Certificate ID and Signature for central directory (0x0016): - - This field contains the information about which certificate in - the PKCS#7 store was used to sign the central directory structure. - When the Central Directory Encryption feature is enabled for a - ZIP file, this record will appear in the Archive Extra Data Record, - otherwise it will appear in the first central directory record. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(CDID) 0x0016 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of data that follows - TData TSize Data - - 4.5.12 -Strong Encryption Header (0x0017): - - Value Size Description - ----- ---- ----------- - 0x0017 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of data that follows - Format 2 bytes Format definition for this record - AlgID 2 bytes Encryption algorithm identifier - Bitlen 2 bytes Bit length of encryption key - Flags 2 bytes Processing flags - CertData TSize-8 Certificate decryption extra field data - (refer to the explanation for CertData - in the section describing the - Certificate Processing Method under - the Strong Encryption Specification) - - See the section describing the Strong Encryption Specification - for details. Refer to the section in this document entitled - "Incorporating PKWARE Proprietary Technology into Your Product" - for more information. - - 4.5.13 -Record Management Controls (0x0018): - - Value Size Description - ----- ---- ----------- -(Rec-CTL) 0x0018 2 bytes Tag for this "extra" block type - CSize 2 bytes Size of total extra block data - Tag1 2 bytes Record control attribute 1 - Size1 2 bytes Size of attribute 1, in bytes - Data1 Size1 Attribute 1 data - . - . - . - TagN 2 bytes Record control attribute N - SizeN 2 bytes Size of attribute N, in bytes - DataN SizeN Attribute N data - - - 4.5.14 -PKCS#7 Encryption Recipient Certificate List (0x0019): - - This field MAY contain information about each of the certificates - used in encryption processing and it can be used to identify who is - allowed to decrypt encrypted files. This field should only appear - in the archive extra data record. This field is not required and - serves only to aid archive modifications by preserving public - encryption key data. Individual security requirements may dictate - that this data be omitted to deter information exposure. - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- -(CStore) 0x0019 2 bytes Tag for this "extra" block type - TSize 2 bytes Size of the store data - TData TSize Data about the store - - TData: - - Value Size Description - ----- ---- ----------- - Version 2 bytes Format version number - must 0x0001 at this time - CStore (var) PKCS#7 data blob - - See the section describing the Strong Encryption Specification - for details. Refer to the section in this document entitled - "Incorporating PKWARE Proprietary Technology into Your Product" - for more information. - - 4.5.15 -MVS Extra Field (0x0065): - - The following is the layout of the MVS "extra" block. - Note: Some fields are stored in Big Endian format. - All text is in EBCDIC format unless otherwise specified. - - Value Size Description - ----- ---- ----------- -(MVS) 0x0065 2 bytes Tag for this "extra" block type - TSize 2 bytes Size for the following data block - ID 4 bytes EBCDIC "Z390" 0xE9F3F9F0 or - "T4MV" for TargetFour - (var) TSize-4 Attribute data (see APPENDIX B) - - - 4.5.16 -OS/400 Extra Field (0x0065): - - The following is the layout of the OS/400 "extra" block. - Note: Some fields are stored in Big Endian format. - All text is in EBCDIC format unless otherwise specified. - - Value Size Description - ----- ---- ----------- -(OS400) 0x0065 2 bytes Tag for this "extra" block type - TSize 2 bytes Size for the following data block - ID 4 bytes EBCDIC "I400" 0xC9F4F0F0 or - "T4MV" for TargetFour - (var) TSize-4 Attribute data (see APPENDIX A) - -4.6 Third Party Mappings ------------------------- - - 4.6.1 Third party mappings commonly used are: - - 0x07c8 Macintosh - 0x2605 ZipIt Macintosh - 0x2705 ZipIt Macintosh 1.3.5+ - 0x2805 ZipIt Macintosh 1.3.5+ - 0x334d Info-ZIP Macintosh - 0x4341 Acorn/SparkFS - 0x4453 Windows NT security descriptor (binary ACL) - 0x4704 VM/CMS - 0x470f MVS - 0x4b46 FWKCS MD5 (see below) - 0x4c41 OS/2 access control list (text ACL) - 0x4d49 Info-ZIP OpenVMS - 0x4f4c Xceed original location extra field - 0x5356 AOS/VS (ACL) - 0x5455 extended timestamp - 0x554e Xceed unicode extra field - 0x5855 Info-ZIP UNIX (original, also OS/2, NT, etc) - 0x6375 Info-ZIP Unicode Comment Extra Field - 0x6542 BeOS/BeBox - 0x7075 Info-ZIP Unicode Path Extra Field - 0x756e ASi UNIX - 0x7855 Info-ZIP UNIX (new) - 0xa220 Microsoft Open Packaging Growth Hint - 0xfd4a SMS/QDOS - - Detailed descriptions of Extra Fields defined by third - party mappings will be documented as information on - these data structures is made available to PKWARE. - PKWARE does not guarantee the accuracy of any published - third party data. - - 4.6.2 Third-party Extra Fields must include a Header ID using - the format defined in the section of this document - titled Extensible Data Fields (section 4.5). - - The Data Size field indicates the size of the following - data block. Programs can use this value to skip to the - next header block, passing over any data blocks that are - not of interest. - - Note: As stated above, the size of the entire .ZIP file - header, including the file name, comment, and extra - field should not exceed 64K in size. - - 4.6.3 In case two different programs should appropriate the same - Header ID value, it is strongly recommended that each - program SHOULD place a unique signature of at least two bytes in - size (and preferably 4 bytes or bigger) at the start of - each data area. Every program SHOULD verify that its - unique signature is present, in addition to the Header ID - value being correct, before assuming that it is a block of - known type. - - Third-party Mappings: - - 4.6.4 -ZipIt Macintosh Extra Field (long) (0x2605): - - The following is the layout of the ZipIt extra block - for Macintosh. The local-header and central-header versions - are identical. This block must be present if the file is - stored MacBinary-encoded and it should not be used if the file - is not stored MacBinary-encoded. - - Value Size Description - ----- ---- ----------- - (Mac2) 0x2605 Short tag for this extra block type - TSize Short total data size for this block - "ZPIT" beLong extra-field signature - FnLen Byte length of FileName - FileName variable full Macintosh filename - FileType Byte[4] four-byte Mac file type string - Creator Byte[4] four-byte Mac creator string - - - 4.6.5 -ZipIt Macintosh Extra Field (short, for files) (0x2705): - - The following is the layout of a shortened variant of the - ZipIt extra block for Macintosh (without "full name" entry). - This variant is used by ZipIt 1.3.5 and newer for entries of - files (not directories) that do not have a MacBinary encoded - file. The local-header and central-header versions are identical. - - Value Size Description - ----- ---- ----------- - (Mac2b) 0x2705 Short tag for this extra block type - TSize Short total data size for this block (12) - "ZPIT" beLong extra-field signature - FileType Byte[4] four-byte Mac file type string - Creator Byte[4] four-byte Mac creator string - fdFlags beShort attributes from FInfo.frFlags, - may be omitted - 0x0000 beShort reserved, may be omitted - - - 4.6.6 -ZipIt Macintosh Extra Field (short, for directories) (0x2805): - - The following is the layout of a shortened variant of the - ZipIt extra block for Macintosh used only for directory - entries. This variant is used by ZipIt 1.3.5 and newer to - save some optional Mac-specific information about directories. - The local-header and central-header versions are identical. - - Value Size Description - ----- ---- ----------- - (Mac2c) 0x2805 Short tag for this extra block type - TSize Short total data size for this block (12) - "ZPIT" beLong extra-field signature - frFlags beShort attributes from DInfo.frFlags, may - be omitted - View beShort ZipIt view flag, may be omitted - - - The View field specifies ZipIt-internal settings as follows: - - Bits of the Flags: - bit 0 if set, the folder is shown expanded (open) - when the archive contents are viewed in ZipIt. - bits 1-15 reserved, zero; - - - 4.6.7 -FWKCS MD5 Extra Field (0x4b46): - - The FWKCS Contents_Signature System, used in - automatically identifying files independent of file name, - optionally adds and uses an extra field to support the - rapid creation of an enhanced contents_signature: - - Header ID = 0x4b46 - Data Size = 0x0013 - Preface = 'M','D','5' - followed by 16 bytes containing the uncompressed file's - 128_bit MD5 hash(1), low byte first. - - When FWKCS revises a .ZIP file central directory to add - this extra field for a file, it also replaces the - central directory entry for that file's uncompressed - file length with a measured value. - - FWKCS provides an option to strip this extra field, if - present, from a .ZIP file central directory. In adding - this extra field, FWKCS preserves .ZIP file Authenticity - Verification; if stripping this extra field, FWKCS - preserves all versions of AV through PKZIP version 2.04g. - - FWKCS, and FWKCS Contents_Signature System, are - trademarks of Frederick W. Kantor. - - (1) R. Rivest, RFC1321.TXT, MIT Laboratory for Computer - Science and RSA Data Security, Inc., April 1992. - ll.76-77: "The MD5 algorithm is being placed in the - public domain for review and possible adoption as a - standard." - - - 4.6.8 -Info-ZIP Unicode Comment Extra Field (0x6375): - - Stores the UTF-8 version of the file comment as stored in the - central directory header. (Last Revision 20070912) - - Value Size Description - ----- ---- ----------- - (UCom) 0x6375 Short tag for this extra block type ("uc") - TSize Short total data size for this block - Version 1 byte version of this extra field, currently 1 - ComCRC32 4 bytes Comment Field CRC32 Checksum - UnicodeCom Variable UTF-8 version of the entry comment - - Currently Version is set to the number 1. If there is a need - to change this field, the version will be incremented. Changes - may not be backward compatible so this extra field should not be - used if the version is not recognized. - - The ComCRC32 is the standard zip CRC32 checksum of the File Comment - field in the central directory header. This is used to verify that - the comment field has not changed since the Unicode Comment extra field - was created. This can happen if a utility changes the File Comment - field but does not update the UTF-8 Comment extra field. If the CRC - check fails, this Unicode Comment extra field should be ignored and - the File Comment field in the header should be used instead. - - The UnicodeCom field is the UTF-8 version of the File Comment field - in the header. As UnicodeCom is defined to be UTF-8, no UTF-8 byte - order mark (BOM) is used. The length of this field is determined by - subtracting the size of the previous fields from TSize. If both the - File Name and Comment fields are UTF-8, the new General Purpose Bit - Flag, bit 11 (Language encoding flag (EFS)), can be used to indicate - both the header File Name and Comment fields are UTF-8 and, in this - case, the Unicode Path and Unicode Comment extra fields are not - needed and should not be created. Note that, for backward - compatibility, bit 11 should only be used if the native character set - of the paths and comments being zipped up are already in UTF-8. It is - expected that the same file comment storage method, either general - purpose bit 11 or extra fields, be used in both the Local and Central - Directory Header for a file. - - - 4.6.9 -Info-ZIP Unicode Path Extra Field (0x7075): - - Stores the UTF-8 version of the file name field as stored in the - local header and central directory header. (Last Revision 20070912) - - Value Size Description - ----- ---- ----------- - (UPath) 0x7075 Short tag for this extra block type ("up") - TSize Short total data size for this block - Version 1 byte version of this extra field, currently 1 - NameCRC32 4 bytes File Name Field CRC32 Checksum - UnicodeName Variable UTF-8 version of the entry File Name - - Currently Version is set to the number 1. If there is a need - to change this field, the version will be incremented. Changes - may not be backward compatible so this extra field should not be - used if the version is not recognized. - - The NameCRC32 is the standard zip CRC32 checksum of the File Name - field in the header. This is used to verify that the header - File Name field has not changed since the Unicode Path extra field - was created. This can happen if a utility renames the File Name but - does not update the UTF-8 path extra field. If the CRC check fails, - this UTF-8 Path Extra Field should be ignored and the File Name field - in the header should be used instead. - - The UnicodeName is the UTF-8 version of the contents of the File Name - field in the header. As UnicodeName is defined to be UTF-8, no UTF-8 - byte order mark (BOM) is used. The length of this field is determined - by subtracting the size of the previous fields from TSize. If both - the File Name and Comment fields are UTF-8, the new General Purpose - Bit Flag, bit 11 (Language encoding flag (EFS)), can be used to - indicate that both the header File Name and Comment fields are UTF-8 - and, in this case, the Unicode Path and Unicode Comment extra fields - are not needed and should not be created. Note that, for backward - compatibility, bit 11 should only be used if the native character set - of the paths and comments being zipped up are already in UTF-8. It is - expected that the same file name storage method, either general - purpose bit 11 or extra fields, be used in both the Local and Central - Directory Header for a file. - - - 4.6.10 -Microsoft Open Packaging Growth Hint (0xa220): - - Value Size Description - ----- ---- ----------- - 0xa220 Short tag for this extra block type - TSize Short size of Sig + PadVal + Padding - Sig Short verification signature (A028) - PadVal Short Initial padding value - Padding variable filled with NULL characters - -4.7 Manifest Files ------------------- - - 4.7.1 Applications using ZIP files may have a need for additional - information that must be included with the files placed into - a ZIP file. Application specific information that cannot be - stored using the defined ZIP storage records SHOULD be stored - using the extensible Extra Field convention defined in this - document. However, some applications may use a manifest - file as a means for storing additional information. One - example is the META-INF/MANIFEST.MF file used in ZIP formatted - files having the .JAR extension (JAR files). - - 4.7.2 A manifest file is a file created for the application process - that requires this information. A manifest file MAY be of any - file type required by the defining application process. It is - placed within the same ZIP file as files to which this information - applies. By convention, this file is typically the first file placed - into the ZIP file and it may include a defined directory path. - - 4.7.3 Manifest files may be compressed or encrypted as needed for - application processing of the files inside the ZIP files. - - Manifest files are outside of the scope of this specification. - - -5.0 Explanation of compression methods --------------------------------------- - - -5.1 UnShrinking - Method 1 --------------------------- - - 5.1.1 Shrinking is a Dynamic Ziv-Lempel-Welch compression algorithm - with partial clearing. The initial code size is 9 bits, and the - maximum code size is 13 bits. Shrinking differs from conventional - Dynamic Ziv-Lempel-Welch implementations in several respects: - - 5.1.2 The code size is controlled by the compressor, and is - not automatically increased when codes larger than the current - code size are created (but not necessarily used). When - the decompressor encounters the code sequence 256 - (decimal) followed by 1, it should increase the code size - read from the input stream to the next bit size. No - blocking of the codes is performed, so the next code at - the increased size should be read from the input stream - immediately after where the previous code at the smaller - bit size was read. Again, the decompressor should not - increase the code size used until the sequence 256,1 is - encountered. - - 5.1.3 When the table becomes full, total clearing is not - performed. Rather, when the compressor emits the code - sequence 256,2 (decimal), the decompressor should clear - all leaf nodes from the Ziv-Lempel tree, and continue to - use the current code size. The nodes that are cleared - from the Ziv-Lempel tree are then re-used, with the lowest - code value re-used first, and the highest code value - re-used last. The compressor can emit the sequence 256,2 - at any time. - -5.2 Expanding - Methods 2-5 ---------------------------- - - 5.2.1 The Reducing algorithm is actually a combination of two - distinct algorithms. The first algorithm compresses repeated - byte sequences, and the second algorithm takes the compressed - stream from the first algorithm and applies a probabilistic - compression method. - - 5.2.2 The probabilistic compression stores an array of 'follower - sets' S(j), for j=0 to 255, corresponding to each possible - ASCII character. Each set contains between 0 and 32 - characters, to be denoted as S(j)[0],...,S(j)[m], where m<32. - The sets are stored at the beginning of the data area for a - Reduced file, in reverse order, with S(255) first, and S(0) - last. - - 5.2.3 The sets are encoded as { N(j), S(j)[0],...,S(j)[N(j)-1] }, - where N(j) is the size of set S(j). N(j) can be 0, in which - case the follower set for S(j) is empty. Each N(j) value is - encoded in 6 bits, followed by N(j) eight bit character values - corresponding to S(j)[0] to S(j)[N(j)-1] respectively. If - N(j) is 0, then no values for S(j) are stored, and the value - for N(j-1) immediately follows. - - 5.2.4 Immediately after the follower sets, is the compressed data - stream. The compressed data stream can be interpreted for the - probabilistic decompression as follows: - - let Last-Character <- 0. - loop until done - if the follower set S(Last-Character) is empty then - read 8 bits from the input stream, and copy this - value to the output stream. - otherwise if the follower set S(Last-Character) is non-empty then - read 1 bit from the input stream. - if this bit is not zero then - read 8 bits from the input stream, and copy this - value to the output stream. - otherwise if this bit is zero then - read B(N(Last-Character)) bits from the input - stream, and assign this value to I. - Copy the value of S(Last-Character)[I] to the - output stream. - - assign the last value placed on the output stream to - Last-Character. - end loop - - B(N(j)) is defined as the minimal number of bits required to - encode the value N(j)-1. - - 5.2.5 The decompressed stream from above can then be expanded to - re-create the original file as follows: - - let State <- 0. - - loop until done - read 8 bits from the input stream into C. - case State of - 0: if C is not equal to DLE (144 decimal) then - copy C to the output stream. - otherwise if C is equal to DLE then - let State <- 1. - - 1: if C is non-zero then - let V <- C. - let Len <- L(V) - let State <- F(Len). - otherwise if C is zero then - copy the value 144 (decimal) to the output stream. - let State <- 0 - - 2: let Len <- Len + C - let State <- 3. - - 3: move backwards D(V,C) bytes in the output stream - (if this position is before the start of the output - stream, then assume that all the data before the - start of the output stream is filled with zeros). - copy Len+3 bytes from this position to the output stream. - let State <- 0. - end case - end loop - - The functions F,L, and D are dependent on the 'compression - factor', 1 through 4, and are defined as follows: - - For compression factor 1: - L(X) equals the lower 7 bits of X. - F(X) equals 2 if X equals 127 otherwise F(X) equals 3. - D(X,Y) equals the (upper 1 bit of X) * 256 + Y + 1. - For compression factor 2: - L(X) equals the lower 6 bits of X. - F(X) equals 2 if X equals 63 otherwise F(X) equals 3. - D(X,Y) equals the (upper 2 bits of X) * 256 + Y + 1. - For compression factor 3: - L(X) equals the lower 5 bits of X. - F(X) equals 2 if X equals 31 otherwise F(X) equals 3. - D(X,Y) equals the (upper 3 bits of X) * 256 + Y + 1. - For compression factor 4: - L(X) equals the lower 4 bits of X. - F(X) equals 2 if X equals 15 otherwise F(X) equals 3. - D(X,Y) equals the (upper 4 bits of X) * 256 + Y + 1. - -5.3 Imploding - Method 6 ------------------------- - - 5.3.1 The Imploding algorithm is actually a combination of two - distinct algorithms. The first algorithm compresses repeated byte - sequences using a sliding dictionary. The second algorithm is - used to compress the encoding of the sliding dictionary output, - using multiple Shannon-Fano trees. - - 5.3.2 The Imploding algorithm can use a 4K or 8K sliding dictionary - size. The dictionary size used can be determined by bit 1 in the - general purpose flag word; a 0 bit indicates a 4K dictionary - while a 1 bit indicates an 8K dictionary. - - 5.3.3 The Shannon-Fano trees are stored at the start of the - compressed file. The number of trees stored is defined by bit 2 in - the general purpose flag word; a 0 bit indicates two trees stored, - a 1 bit indicates three trees are stored. If 3 trees are stored, - the first Shannon-Fano tree represents the encoding of the - Literal characters, the second tree represents the encoding of - the Length information, the third represents the encoding of the - Distance information. When 2 Shannon-Fano trees are stored, the - Length tree is stored first, followed by the Distance tree. - - 5.3.4 The Literal Shannon-Fano tree, if present is used to represent - the entire ASCII character set, and contains 256 values. This - tree is used to compress any data not compressed by the sliding - dictionary algorithm. When this tree is present, the Minimum - Match Length for the sliding dictionary is 3. If this tree is - not present, the Minimum Match Length is 2. - - 5.3.5 The Length Shannon-Fano tree is used to compress the Length - part of the (length,distance) pairs from the sliding dictionary - output. The Length tree contains 64 values, ranging from the - Minimum Match Length, to 63 plus the Minimum Match Length. - - 5.3.6 The Distance Shannon-Fano tree is used to compress the Distance - part of the (length,distance) pairs from the sliding dictionary - output. The Distance tree contains 64 values, ranging from 0 to - 63, representing the upper 6 bits of the distance value. The - distance values themselves will be between 0 and the sliding - dictionary size, either 4K or 8K. - - 5.3.7 The Shannon-Fano trees themselves are stored in a compressed - format. The first byte of the tree data represents the number of - bytes of data representing the (compressed) Shannon-Fano tree - minus 1. The remaining bytes represent the Shannon-Fano tree - data encoded as: - - High 4 bits: Number of values at this bit length + 1. (1 - 16) - Low 4 bits: Bit Length needed to represent value + 1. (1 - 16) - - 5.3.8 The Shannon-Fano codes can be constructed from the bit lengths - using the following algorithm: - - 1) Sort the Bit Lengths in ascending order, while retaining the - order of the original lengths stored in the file. - - 2) Generate the Shannon-Fano trees: - - Code <- 0 - CodeIncrement <- 0 - LastBitLength <- 0 - i <- number of Shannon-Fano codes - 1 (either 255 or 63) - - loop while i >= 0 - Code = Code + CodeIncrement - if BitLength(i) <> LastBitLength then - LastBitLength=BitLength(i) - CodeIncrement = 1 shifted left (16 - LastBitLength) - ShannonCode(i) = Code - i <- i - 1 - end loop - - 3) Reverse the order of all the bits in the above ShannonCode() - vector, so that the most significant bit becomes the least - significant bit. For example, the value 0x1234 (hex) would - become 0x2C48 (hex). - - 4) Restore the order of Shannon-Fano codes as originally stored - within the file. - - Example: - - This example will show the encoding of a Shannon-Fano tree - of size 8. Notice that the actual Shannon-Fano trees used - for Imploding are either 64 or 256 entries in size. - - Example: 0x02, 0x42, 0x01, 0x13 - - The first byte indicates 3 values in this table. Decoding the - bytes: - 0x42 = 5 codes of 3 bits long - 0x01 = 1 code of 2 bits long - 0x13 = 2 codes of 4 bits long - - This would generate the original bit length array of: - (3, 3, 3, 3, 3, 2, 4, 4) - - There are 8 codes in this table for the values 0 thru 7. Using - the algorithm to obtain the Shannon-Fano codes produces: - - Reversed Order Original - Val Sorted Constructed Code Value Restored Length - --- ------ ----------------- -------- -------- ------ - 0: 2 1100000000000000 11 101 3 - 1: 3 1010000000000000 101 001 3 - 2: 3 1000000000000000 001 110 3 - 3: 3 0110000000000000 110 010 3 - 4: 3 0100000000000000 010 100 3 - 5: 3 0010000000000000 100 11 2 - 6: 4 0001000000000000 1000 1000 4 - 7: 4 0000000000000000 0000 0000 4 - - The values in the Val, Order Restored and Original Length columns - now represent the Shannon-Fano encoding tree that can be used for - decoding the Shannon-Fano encoded data. How to parse the - variable length Shannon-Fano values from the data stream is beyond - the scope of this document. (See the references listed at the end of - this document for more information.) However, traditional decoding - schemes used for Huffman variable length decoding, such as the - Greenlaw algorithm, can be successfully applied. - - 5.3.9 The compressed data stream begins immediately after the - compressed Shannon-Fano data. The compressed data stream can be - interpreted as follows: - - loop until done - read 1 bit from input stream. - - if this bit is non-zero then (encoded data is literal data) - if Literal Shannon-Fano tree is present - read and decode character using Literal Shannon-Fano tree. - otherwise - read 8 bits from input stream. - copy character to the output stream. - otherwise (encoded data is sliding dictionary match) - if 8K dictionary size - read 7 bits for offset Distance (lower 7 bits of offset). - otherwise - read 6 bits for offset Distance (lower 6 bits of offset). - - using the Distance Shannon-Fano tree, read and decode the - upper 6 bits of the Distance value. - - using the Length Shannon-Fano tree, read and decode - the Length value. - - Length <- Length + Minimum Match Length - - if Length = 63 + Minimum Match Length - read 8 bits from the input stream, - add this value to Length. - - move backwards Distance+1 bytes in the output stream, and - copy Length characters from this position to the output - stream. (if this position is before the start of the output - stream, then assume that all the data before the start of - the output stream is filled with zeros). - end loop - -5.4 Tokenizing - Method 7 -------------------------- - - 5.4.1 This method is not used by PKZIP. - -5.5 Deflating - Method 8 ------------------------- - - 5.5.1 The Deflate algorithm is similar to the Implode algorithm using - a sliding dictionary of up to 32K with secondary compression - from Huffman/Shannon-Fano codes. - - 5.5.2 The compressed data is stored in blocks with a header describing - the block and the Huffman codes used in the data block. The header - format is as follows: - - Bit 0: Last Block bit This bit is set to 1 if this is the last - compressed block in the data. - Bits 1-2: Block type - 00 (0) - Block is stored - All stored data is byte aligned. - Skip bits until next byte, then next word = block - length, followed by the ones compliment of the block - length word. Remaining data in block is the stored - data. - - 01 (1) - Use fixed Huffman codes for literal and distance codes. - Lit Code Bits Dist Code Bits - --------- ---- --------- ---- - 0 - 143 8 0 - 31 5 - 144 - 255 9 - 256 - 279 7 - 280 - 287 8 - - Literal codes 286-287 and distance codes 30-31 are - never used but participate in the huffman construction. - - 10 (2) - Dynamic Huffman codes. (See expanding Huffman codes) - - 11 (3) - Reserved - Flag a "Error in compressed data" if seen. - - 5.5.3 Expanding Huffman Codes - - If the data block is stored with dynamic Huffman codes, the Huffman - codes are sent in the following compressed format: - - 5 Bits: # of Literal codes sent - 256 (256 - 286) - All other codes are never sent. - 5 Bits: # of Dist codes - 1 (1 - 32) - 4 Bits: # of Bit Length codes - 3 (3 - 19) - - The Huffman codes are sent as bit lengths and the codes are built as - described in the implode algorithm. The bit lengths themselves are - compressed with Huffman codes. There are 19 bit length codes: - - 0 - 15: Represent bit lengths of 0 - 15 - 16: Copy the previous bit length 3 - 6 times. - The next 2 bits indicate repeat length (0 = 3, ... ,3 = 6) - Example: Codes 8, 16 (+2 bits 11), 16 (+2 bits 10) will - expand to 12 bit lengths of 8 (1 + 6 + 5) - 17: Repeat a bit length of 0 for 3 - 10 times. (3 bits of length) - 18: Repeat a bit length of 0 for 11 - 138 times (7 bits of length) - - The lengths of the bit length codes are sent packed 3 bits per value - (0 - 7) in the following order: - - 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 - - The Huffman codes should be built as described in the Implode algorithm - except codes are assigned starting at the shortest bit length, i.e. the - shortest code should be all 0's rather than all 1's. Also, codes with - a bit length of zero do not participate in the tree construction. The - codes are then used to decode the bit lengths for the literal and - distance tables. - - The bit lengths for the literal tables are sent first with the number - of entries sent described by the 5 bits sent earlier. There are up - to 286 literal characters; the first 256 represent the respective 8 - bit character, code 256 represents the End-Of-Block code, the remaining - 29 codes represent copy lengths of 3 thru 258. There are up to 30 - distance codes representing distances from 1 thru 32k as described - below. - - Length Codes - ------------ - Extra Extra Extra Extra - Code Bits Length Code Bits Lengths Code Bits Lengths Code Bits Length(s) - ---- ---- ------ ---- ---- ------- ---- ---- ------- ---- ---- --------- - 257 0 3 265 1 11,12 273 3 35-42 281 5 131-162 - 258 0 4 266 1 13,14 274 3 43-50 282 5 163-194 - 259 0 5 267 1 15,16 275 3 51-58 283 5 195-226 - 260 0 6 268 1 17,18 276 3 59-66 284 5 227-257 - 261 0 7 269 2 19-22 277 4 67-82 285 0 258 - 262 0 8 270 2 23-26 278 4 83-98 - 263 0 9 271 2 27-30 279 4 99-114 - 264 0 10 272 2 31-34 280 4 115-130 - - Distance Codes - -------------- - Extra Extra Extra Extra - Code Bits Dist Code Bits Dist Code Bits Distance Code Bits Distance - ---- ---- ---- ---- ---- ------ ---- ---- -------- ---- ---- -------- - 0 0 1 8 3 17-24 16 7 257-384 24 11 4097-6144 - 1 0 2 9 3 25-32 17 7 385-512 25 11 6145-8192 - 2 0 3 10 4 33-48 18 8 513-768 26 12 8193-12288 - 3 0 4 11 4 49-64 19 8 769-1024 27 12 12289-16384 - 4 1 5,6 12 5 65-96 20 9 1025-1536 28 13 16385-24576 - 5 1 7,8 13 5 97-128 21 9 1537-2048 29 13 24577-32768 - 6 2 9-12 14 6 129-192 22 10 2049-3072 - 7 2 13-16 15 6 193-256 23 10 3073-4096 - - 5.5.4 The compressed data stream begins immediately after the - compressed header data. The compressed data stream can be - interpreted as follows: - - do - read header from input stream. - - if stored block - skip bits until byte aligned - read count and 1's compliment of count - copy count bytes data block - otherwise - loop until end of block code sent - decode literal character from input stream - if literal < 256 - copy character to the output stream - otherwise - if literal = end of block - break from loop - otherwise - decode distance from input stream - - move backwards distance bytes in the output stream, and - copy length characters from this position to the output - stream. - end loop - while not last block - - if data descriptor exists - skip bits until byte aligned - read crc and sizes - endif - -5.6 Enhanced Deflating - Method 9 ---------------------------------- - - 5.6.1 The Enhanced Deflating algorithm is similar to Deflate but uses - a sliding dictionary of up to 64K. Deflate64(tm) is supported - by the Deflate extractor. - -5.7 BZIP2 - Method 12 ---------------------- - - 5.7.1 BZIP2 is an open-source data compression algorithm developed by - Julian Seward. Information and source code for this algorithm - can be found on the internet. - -5.8 LZMA - Method 14 ---------------------- - - 5.8.1 LZMA is a block-oriented, general purpose data compression - algorithm developed and maintained by Igor Pavlov. It is a derivative - of LZ77 that utilizes Markov chains and a range coder. Information and - source code for this algorithm can be found on the internet. Consult - with the author of this algorithm for information on terms or - restrictions on use. - - Support for LZMA within the ZIP format is defined as follows: - - 5.8.2 The Compression method field within the ZIP Local and Central - Header records will be set to the value 14 to indicate data was - compressed using LZMA. - - 5.8.3 The Version needed to extract field within the ZIP Local and - Central Header records will be set to 6.3 to indicate the minimum - ZIP format version supporting this feature. - - 5.8.4 File data compressed using the LZMA algorithm must be placed - immediately following the Local Header for the file. If a standard - ZIP encryption header is required, it will follow the Local Header - and will precede the LZMA compressed file data segment. The location - of LZMA compressed data segment within the ZIP format will be as shown: - - [local header file 1] - [encryption header file 1] - [LZMA compressed data segment for file 1] - [data descriptor 1] - [local header file 2] - - 5.8.5 The encryption header and data descriptor records may - be conditionally present. The LZMA Compressed Data Segment - will consist of an LZMA Properties Header followed by the - LZMA Compressed Data as shown: - - [LZMA properties header for file 1] - [LZMA compressed data for file 1] - - 5.8.6 The LZMA Compressed Data will be stored as provided by the - LZMA compression library. Compressed size, uncompressed size and - other file characteristics about the file being compressed must be - stored in standard ZIP storage format. - - 5.8.7 The LZMA Properties Header will store specific data required - to decompress the LZMA compressed Data. This data is set by the - LZMA compression engine using the function WriteCoderProperties() - as documented within the LZMA SDK. - - 5.8.8 Storage fields for the property information within the LZMA - Properties Header are as follows: - - LZMA Version Information 2 bytes - LZMA Properties Size 2 bytes - LZMA Properties Data variable, defined by "LZMA Properties Size" - - 5.8.8.1 LZMA Version Information - this field identifies which version - of the LZMA SDK was used to compress a file. The first byte will - store the major version number of the LZMA SDK and the second - byte will store the minor number. - - 5.8.8.2 LZMA Properties Size - this field defines the size of the - remaining property data. Typically this size should be determined by - the version of the SDK. This size field is included as a convenience - and to help avoid any ambiguity should it arise in the future due - to changes in this compression algorithm. - - 5.8.8.3 LZMA Property Data - this variable sized field records the - required values for the decompressor as defined by the LZMA SDK. - The data stored in this field should be obtained using the - WriteCoderProperties() in the version of the SDK defined by - the "LZMA Version Information" field. - - 5.8.8.4 The layout of the "LZMA Properties Data" field is a function of - the LZMA compression algorithm. It is possible that this layout may be - changed by the author over time. The data layout in version 4.3 of the - LZMA SDK defines a 5 byte array that uses 4 bytes to store the dictionary - size in little-endian order. This is preceded by a single packed byte as - the first element of the array that contains the following fields: - - PosStateBits - LiteralPosStateBits - LiteralContextBits - - Refer to the LZMA documentation for a more detailed explanation of - these fields. - - 5.8.9 Data compressed with method 14, LZMA, may include an end-of-stream - (EOS) marker ending the compressed data stream. This marker is not - required, but its use is highly recommended to facilitate processing - and implementers should include the EOS marker whenever possible. - When the EOS marker is used, general purpose bit 1 must be set. If - general purpose bit 1 is not set, the EOS marker is not present. - -5.9 WavPack - Method 97 ------------------------ - - 5.9.1 Information describing the use of compression method 97 is - provided by WinZIP International, LLC. This method relies on the - open source WavPack audio compression utility developed by David Bryant. - Information on WavPack is available at www.wavpack.com. Please consult - with the author of this algorithm for information on terms and - restrictions on use. - - 5.9.2 WavPack data for a file begins immediately after the end of the - local header data. This data is the output from WavPack compression - routines. Within the ZIP file, the use of WavPack compression is - indicated by setting the compression method field to a value of 97 - in both the local header and the central directory header. The Version - needed to extract and version made by fields use the same values as are - used for data compressed using the Deflate algorithm. - - 5.9.3 An implementation note for storing digital sample data when using - WavPack compression within ZIP files is that all of the bytes of - the sample data should be compressed. This includes any unused - bits up to the byte boundary. An example is a 2 byte sample that - uses only 12 bits for the sample data with 4 unused bits. If only - 12 bits are passed as the sample size to the WavPack routines, the 4 - unused bits will be set to 0 on extraction regardless of their original - state. To avoid this, the full 16 bits of the sample data size - should be provided. - -5.10 PPMd - Method 98 ---------------------- - - 5.10.1 PPMd is a data compression algorithm developed by Dmitry Shkarin - which includes a carryless rangecoder developed by Dmitry Subbotin. - This algorithm is based on predictive phrase matching on multiple - order contexts. Information and source code for this algorithm - can be found on the internet. Consult with the author of this - algorithm for information on terms or restrictions on use. - - 5.10.2 Support for PPMd within the ZIP format currently is provided only - for version I, revision 1 of the algorithm. Storage requirements - for using this algorithm are as follows: - - 5.10.3 Parameters needed to control the algorithm are stored in the two - bytes immediately preceding the compressed data. These bytes are - used to store the following fields: - - Model order - sets the maximum model order, default is 8, possible - values are from 2 to 16 inclusive - - Sub-allocator size - sets the size of sub-allocator in MB, default is 50, - possible values are from 1MB to 256MB inclusive - - Model restoration method - sets the method used to restart context - model at memory insufficiency, values are: - - 0 - restarts model from scratch - default - 1 - cut off model - decreases performance by as much as 2x - 2 - freeze context tree - not recommended - - 5.10.4 An example for packing these fields into the 2 byte storage field is - illustrated below. These values are stored in Intel low-byte/high-byte - order. - - wPPMd = (Model order - 1) + - ((Sub-allocator size - 1) << 4) + - (Model restoration method << 12) - - -6.0 Traditional PKWARE Encryption ----------------------------------- - - 6.0.1 The following information discusses the decryption steps - required to support traditional PKWARE encryption. This - form of encryption is considered weak by today's standards - and its use is recommended only for situations with - low security needs or for compatibility with older .ZIP - applications. - -6.1 Traditional PKWARE Decryption ---------------------------------- - - 6.1.1 PKWARE is grateful to Mr. Roger Schlafly for his expert - contribution towards the development of PKWARE's traditional - encryption. - - 6.1.2 PKZIP encrypts the compressed data stream. Encrypted files - must be decrypted before they can be extracted to their original - form. - - 6.1.3 Each encrypted file has an extra 12 bytes stored at the start - of the data area defining the encryption header for that file. The - encryption header is originally set to random values, and then - itself encrypted, using three, 32-bit keys. The key values are - initialized using the supplied encryption password. After each byte - is encrypted, the keys are then updated using pseudo-random number - generation techniques in combination with the same CRC-32 algorithm - used in PKZIP and described elsewhere in this document. - - 6.1.4 The following are the basic steps required to decrypt a file: - - 1) Initialize the three 32-bit keys with the password. - 2) Read and decrypt the 12-byte encryption header, further - initializing the encryption keys. - 3) Read and decrypt the compressed data stream using the - encryption keys. - - 6.1.5 Initializing the encryption keys - - Key(0) <- 305419896 - Key(1) <- 591751049 - Key(2) <- 878082192 - - loop for i <- 0 to length(password)-1 - update_keys(password(i)) - end loop - - Where update_keys() is defined as: - - update_keys(char): - Key(0) <- crc32(key(0),char) - Key(1) <- Key(1) + (Key(0) & 000000ffH) - Key(1) <- Key(1) * 134775813 + 1 - Key(2) <- crc32(key(2),key(1) >> 24) - end update_keys - - Where crc32(old_crc,char) is a routine that given a CRC value and a - character, returns an updated CRC value after applying the CRC-32 - algorithm described elsewhere in this document. - - 6.1.6 Decrypting the encryption header - - The purpose of this step is to further initialize the encryption - keys, based on random data, to render a plaintext attack on the - data ineffective. - - Read the 12-byte encryption header into Buffer, in locations - Buffer(0) thru Buffer(11). - - loop for i <- 0 to 11 - C <- buffer(i) ^ decrypt_byte() - update_keys(C) - buffer(i) <- C - end loop - - Where decrypt_byte() is defined as: - - unsigned char decrypt_byte() - local unsigned short temp - temp <- Key(2) | 2 - decrypt_byte <- (temp * (temp ^ 1)) >> 8 - end decrypt_byte - - After the header is decrypted, the last 1 or 2 bytes in Buffer - should be the high-order word/byte of the CRC for the file being - decrypted, stored in Intel low-byte/high-byte order. Versions of - PKZIP prior to 2.0 used a 2 byte CRC check; a 1 byte CRC check is - used on versions after 2.0. This can be used to test if the password - supplied is correct or not. - - 6.1.7 Decrypting the compressed data stream - - The compressed data stream can be decrypted as follows: - - loop until done - read a character into C - Temp <- C ^ decrypt_byte() - update_keys(temp) - output Temp - end loop - - -7.0 Strong Encryption Specification ------------------------------------ - - 7.0.1 Portions of the Strong Encryption technology defined in this - specification are covered under patents and pending patent applications. - Refer to the section in this document entitled "Incorporating - PKWARE Proprietary Technology into Your Product" for more information. - -7.1 Strong Encryption Overview ------------------------------- - - 7.1.1 Version 5.x of this specification introduced support for strong - encryption algorithms. These algorithms can be used with either - a password or an X.509v3 digital certificate to encrypt each file. - This format specification supports either password or certificate - based encryption to meet the security needs of today, to enable - interoperability between users within both PKI and non-PKI - environments, and to ensure interoperability between different - computing platforms that are running a ZIP program. - - 7.1.2 Password based encryption is the most common form of encryption - people are familiar with. However, inherent weaknesses with - passwords (e.g. susceptibility to dictionary/brute force attack) - as well as password management and support issues make certificate - based encryption a more secure and scalable option. Industry - efforts and support are defining and moving towards more advanced - security solutions built around X.509v3 digital certificates and - Public Key Infrastructures(PKI) because of the greater scalability, - administrative options, and more robust security over traditional - password based encryption. - - 7.1.3 Most standard encryption algorithms are supported with this - specification. Reference implementations for many of these - algorithms are available from either commercial or open source - distributors. Readily available cryptographic toolkits make - implementation of the encryption features straight-forward. - This document is not intended to provide a treatise on data - encryption principles or theory. Its purpose is to document the - data structures required for implementing interoperable data - encryption within the .ZIP format. It is strongly recommended that - you have a good understanding of data encryption before reading - further. - - 7.1.4 The algorithms introduced in Version 5.0 of this specification - include: - - RC2 40 bit, 64 bit, and 128 bit - RC4 40 bit, 64 bit, and 128 bit - DES - 3DES 112 bit and 168 bit - - Version 5.1 adds support for the following: - - AES 128 bit, 192 bit, and 256 bit - - - 7.1.5 Version 6.1 introduces encryption data changes to support - interoperability with Smartcard and USB Token certificate storage - methods which do not support the OAEP strengthening standard. - - 7.1.6 Version 6.2 introduces support for encrypting metadata by compressing - and encrypting the central directory data structure to reduce information - leakage. Information leakage can occur in legacy ZIP applications - through exposure of information about a file even though that file is - stored encrypted. The information exposed consists of file - characteristics stored within the records and fields defined by this - specification. This includes data such as a file's name, its original - size, timestamp and CRC32 value. - - 7.1.7 Version 6.3 introduces support for encrypting data using the Blowfish - and Twofish algorithms. These are symmetric block ciphers developed - by Bruce Schneier. Blowfish supports using a variable length key from - 32 to 448 bits. Block size is 64 bits. Implementations should use 16 - rounds and the only mode supported within ZIP files is CBC. Twofish - supports key sizes 128, 192 and 256 bits. Block size is 128 bits. - Implementations should use 16 rounds and the only mode supported within - ZIP files is CBC. Information and source code for both Blowfish and - Twofish algorithms can be found on the internet. Consult with the author - of these algorithms for information on terms or restrictions on use. - - 7.1.8 Central Directory Encryption provides greater protection against - information leakage by encrypting the Central Directory structure and - by masking key values that are replicated in the unencrypted Local - Header. ZIP compatible programs that cannot interpret an encrypted - Central Directory structure cannot rely on the data in the corresponding - Local Header for decompression information. - - 7.1.9 Extra Field records that may contain information about a file that should - not be exposed should not be stored in the Local Header and should only - be written to the Central Directory where they can be encrypted. This - design currently does not support streaming. Information in the End of - Central Directory record, the Zip64 End of Central Directory Locator, - and the Zip64 End of Central Directory records are not encrypted. Access - to view data on files within a ZIP file with an encrypted Central Directory - requires the appropriate password or private key for decryption prior to - viewing any files, or any information about the files, in the archive. - - 7.1.10 Older ZIP compatible programs not familiar with the Central Directory - Encryption feature will no longer be able to recognize the Central - Directory and may assume the ZIP file is corrupt. Programs that - attempt streaming access using Local Headers will see invalid - information for each file. Central Directory Encryption need not be - used for every ZIP file. Its use is recommended for greater security. - ZIP files not using Central Directory Encryption should operate as - in the past. - - 7.1.11 This strong encryption feature specification is intended to provide for - scalable, cross-platform encryption needs ranging from simple password - encryption to authenticated public/private key encryption. - - 7.1.12 Encryption provides data confidentiality and privacy. It is - recommended that you combine X.509 digital signing with encryption - to add authentication and non-repudiation. - - -7.2 Single Password Symmetric Encryption Method ------------------------------------------------ - - 7.2.1 The Single Password Symmetric Encryption Method using strong - encryption algorithms operates similarly to the traditional - PKWARE encryption defined in this format. Additional data - structures are added to support the processing needs of the - strong algorithms. - - The Strong Encryption data structures are: - - 7.2.2 General Purpose Bits - Bits 0 and 6 of the General Purpose bit - flag in both local and central header records. Both bits set - indicates strong encryption. Bit 13, when set indicates the Central - Directory is encrypted and that selected fields in the Local Header - are masked to hide their actual value. - - - 7.2.3 Extra Field 0x0017 in central header only. - - Fields to consider in this record are: - - 7.2.3.1 Format - the data format identifier for this record. The only - value allowed at this time is the integer value 2. - - 7.2.3.2 AlgId - integer identifier of the encryption algorithm from the - following range - - 0x6601 - DES - 0x6602 - RC2 (version needed to extract < 5.2) - 0x6603 - 3DES 168 - 0x6609 - 3DES 112 - 0x660E - AES 128 - 0x660F - AES 192 - 0x6610 - AES 256 - 0x6702 - RC2 (version needed to extract >= 5.2) - 0x6720 - Blowfish - 0x6721 - Twofish - 0x6801 - RC4 - 0xFFFF - Unknown algorithm - - 7.2.3.3 Bitlen - Explicit bit length of key - - 32 - 448 bits - - 7.2.3.4 Flags - Processing flags needed for decryption - - 0x0001 - Password is required to decrypt - 0x0002 - Certificates only - 0x0003 - Password or certificate required to decrypt - - Values > 0x0003 reserved for certificate processing - - - 7.2.4 Decryption header record preceding compressed file data. - - -Decryption Header: - - Value Size Description - ----- ---- ----------- - IVSize 2 bytes Size of initialization vector (IV) - IVData IVSize Initialization vector for this file - Size 4 bytes Size of remaining decryption header data - Format 2 bytes Format definition for this record - AlgID 2 bytes Encryption algorithm identifier - Bitlen 2 bytes Bit length of encryption key - Flags 2 bytes Processing flags - ErdSize 2 bytes Size of Encrypted Random Data - ErdData ErdSize Encrypted Random Data - Reserved1 4 bytes Reserved certificate processing data - Reserved2 (var) Reserved for certificate processing data - VSize 2 bytes Size of password validation data - VData VSize-4 Password validation data - VCRC32 4 bytes Standard ZIP CRC32 of password validation data - - 7.2.4.1 IVData - The size of the IV should match the algorithm block size. - The IVData can be completely random data. If the size of - the randomly generated data does not match the block size - it should be complemented with zero's or truncated as - necessary. If IVSize is 0,then IV = CRC32 + Uncompressed - File Size (as a 64 bit little-endian, unsigned integer value). - - 7.2.4.2 Format - the data format identifier for this record. The only - value allowed at this time is the integer value 3. - - 7.2.4.3 AlgId - integer identifier of the encryption algorithm from the - following range - - 0x6601 - DES - 0x6602 - RC2 (version needed to extract < 5.2) - 0x6603 - 3DES 168 - 0x6609 - 3DES 112 - 0x660E - AES 128 - 0x660F - AES 192 - 0x6610 - AES 256 - 0x6702 - RC2 (version needed to extract >= 5.2) - 0x6720 - Blowfish - 0x6721 - Twofish - 0x6801 - RC4 - 0xFFFF - Unknown algorithm - - 7.2.4.4 Bitlen - Explicit bit length of key - - 32 - 448 bits - - 7.2.4.5 Flags - Processing flags needed for decryption - - 0x0001 - Password is required to decrypt - 0x0002 - Certificates only - 0x0003 - Password or certificate required to decrypt - - Values > 0x0003 reserved for certificate processing - - 7.2.4.6 ErdData - Encrypted random data is used to store random data that - is used to generate a file session key for encrypting - each file. SHA1 is used to calculate hash data used to - derive keys. File session keys are derived from a master - session key generated from the user-supplied password. - If the Flags field in the decryption header contains - the value 0x4000, then the ErdData field must be - decrypted using 3DES. If the value 0x4000 is not set, - then the ErdData field must be decrypted using AlgId. - - - 7.2.4.7 Reserved1 - Reserved for certificate processing, if value is - zero, then Reserved2 data is absent. See the explanation - under the Certificate Processing Method for details on - this data structure. - - 7.2.4.8 Reserved2 - If present, the size of the Reserved2 data structure - is located by skipping the first 4 bytes of this field - and using the next 2 bytes as the remaining size. See - the explanation under the Certificate Processing Method - for details on this data structure. - - 7.2.4.9 VSize - This size value will always include the 4 bytes of the - VCRC32 data and will be greater than 4 bytes. - - 7.2.4.10 VData - Random data for password validation. This data is VSize - in length and VSize must be a multiple of the encryption - block size. VCRC32 is a checksum value of VData. - VData and VCRC32 are stored encrypted and start the - stream of encrypted data for a file. - - - 7.2.5 Useful Tips - - 7.2.5.1 Strong Encryption is always applied to a file after compression. The - block oriented algorithms all operate in Cypher Block Chaining (CBC) - mode. The block size used for AES encryption is 16. All other block - algorithms use a block size of 8. Two IDs are defined for RC2 to - account for a discrepancy found in the implementation of the RC2 - algorithm in the cryptographic library on Windows XP SP1 and all - earlier versions of Windows. It is recommended that zero length files - not be encrypted, however programs should be prepared to extract them - if they are found within a ZIP file. - - 7.2.5.2 A pseudo-code representation of the encryption process is as follows: - - Password = GetUserPassword() - MasterSessionKey = DeriveKey(SHA1(Password)) - RD = CryptographicStrengthRandomData() - For Each File - IV = CryptographicStrengthRandomData() - VData = CryptographicStrengthRandomData() - VCRC32 = CRC32(VData) - FileSessionKey = DeriveKey(SHA1(IV + RD) - ErdData = Encrypt(RD,MasterSessionKey,IV) - Encrypt(VData + VCRC32 + FileData, FileSessionKey,IV) - Done - - 7.2.5.3 The function names and parameter requirements will depend on - the choice of the cryptographic toolkit selected. Almost any - toolkit supporting the reference implementations for each - algorithm can be used. The RSA BSAFE(r), OpenSSL, and Microsoft - CryptoAPI libraries are all known to work well. - - - 7.3 Single Password - Central Directory Encryption - -------------------------------------------------- - - 7.3.1 Central Directory Encryption is achieved within the .ZIP format by - encrypting the Central Directory structure. This encapsulates the metadata - most often used for processing .ZIP files. Additional metadata is stored for - redundancy in the Local Header for each file. The process of concealing - metadata by encrypting the Central Directory does not protect the data within - the Local Header. To avoid information leakage from the exposed metadata - in the Local Header, the fields containing information about a file are masked. - - 7.3.2 Local Header - - Masking replaces the true content of the fields for a file in the Local - Header with false information. When masked, the Local Header is not - suitable for streaming access and the options for data recovery of damaged - archives is reduced. Extra Data fields that may contain confidential - data should not be stored within the Local Header. The value set into - the Version needed to extract field should be the correct value needed to - extract the file without regard to Central Directory Encryption. The fields - within the Local Header targeted for masking when the Central Directory is - encrypted are: - - Field Name Mask Value - ------------------ --------------------------- - compression method 0 - last mod file time 0 - last mod file date 0 - crc-32 0 - compressed size 0 - uncompressed size 0 - file name (variable size) Base 16 value from the - range 1 - 0xFFFFFFFFFFFFFFFF - represented as a string whose - size will be set into the - file name length field - - The Base 16 value assigned as a masked file name is simply a sequentially - incremented value for each file starting with 1 for the first file. - Modifications to a ZIP file may cause different values to be stored for - each file. For compatibility, the file name field in the Local Header - should never be left blank. As of Version 6.2 of this specification, - the Compression Method and Compressed Size fields are not yet masked. - Fields having a value of 0xFFFF or 0xFFFFFFFF for the ZIP64 format - should not be masked. - - 7.3.3 Encrypting the Central Directory - - Encryption of the Central Directory does not include encryption of the - Central Directory Signature data, the Zip64 End of Central Directory - record, the Zip64 End of Central Directory Locator, or the End - of Central Directory record. The ZIP file comment data is never - encrypted. - - Before encrypting the Central Directory, it may optionally be compressed. - Compression is not required, but for storage efficiency it is assumed - this structure will be compressed before encrypting. Similarly, this - specification supports compressing the Central Directory without - requiring that it also be encrypted. Early implementations of this - feature will assume the encryption method applied to files matches the - encryption applied to the Central Directory. - - Encryption of the Central Directory is done in a manner similar to - that of file encryption. The encrypted data is preceded by a - decryption header. The decryption header is known as the Archive - Decryption Header. The fields of this record are identical to - the decryption header preceding each encrypted file. The location - of the Archive Decryption Header is determined by the value in the - Start of the Central Directory field in the Zip64 End of Central - Directory record. When the Central Directory is encrypted, the - Zip64 End of Central Directory record will always be present. - - The layout of the Zip64 End of Central Directory record for all - versions starting with 6.2 of this specification will follow the - Version 2 format. The Version 2 format is as follows: - - The leading fixed size fields within the Version 1 format for this - record remain unchanged. The record signature for both Version 1 - and Version 2 will be 0x06064b50. Immediately following the last - byte of the field known as the Offset of Start of Central - Directory With Respect to the Starting Disk Number will begin the - new fields defining Version 2 of this record. - - 7.3.4 New fields for Version 2 - - Note: all fields stored in Intel low-byte/high-byte order. - - Value Size Description - ----- ---- ----------- - Compression Method 2 bytes Method used to compress the - Central Directory - Compressed Size 8 bytes Size of the compressed data - Original Size 8 bytes Original uncompressed size - AlgId 2 bytes Encryption algorithm ID - BitLen 2 bytes Encryption key length - Flags 2 bytes Encryption flags - HashID 2 bytes Hash algorithm identifier - Hash Length 2 bytes Length of hash data - Hash Data (variable) Hash data - - The Compression Method accepts the same range of values as the - corresponding field in the Central Header. - - The Compressed Size and Original Size values will not include the - data of the Central Directory Signature which is compressed or - encrypted. - - The AlgId, BitLen, and Flags fields accept the same range of values - the corresponding fields within the 0x0017 record. - - Hash ID identifies the algorithm used to hash the Central Directory - data. This data does not have to be hashed, in which case the - values for both the HashID and Hash Length will be 0. Possible - values for HashID are: - - Value Algorithm - ------ --------- - 0x0000 none - 0x0001 CRC32 - 0x8003 MD5 - 0x8004 SHA1 - 0x8007 RIPEMD160 - 0x800C SHA256 - 0x800D SHA384 - 0x800E SHA512 - - 7.3.5 When the Central Directory data is signed, the same hash algorithm - used to hash the Central Directory for signing should be used. - This is recommended for processing efficiency, however, it is - permissible for any of the above algorithms to be used independent - of the signing process. - - The Hash Data will contain the hash data for the Central Directory. - The length of this data will vary depending on the algorithm used. - - The Version Needed to Extract should be set to 62. - - The value for the Total Number of Entries on the Current Disk will - be 0. These records will no longer support random access when - encrypting the Central Directory. - - 7.3.6 When the Central Directory is compressed and/or encrypted, the - End of Central Directory record will store the value 0xFFFFFFFF - as the value for the Total Number of Entries in the Central - Directory. The value stored in the Total Number of Entries in - the Central Directory on this Disk field will be 0. The actual - values will be stored in the equivalent fields of the Zip64 - End of Central Directory record. - - 7.3.7 Decrypting and decompressing the Central Directory is accomplished - in the same manner as decrypting and decompressing a file. - - 7.4 Certificate Processing Method - --------------------------------- - - The Certificate Processing Method for ZIP file encryption - defines the following additional data fields: - - 7.4.1 Certificate Flag Values - - Additional processing flags that can be present in the Flags field of both - the 0x0017 field of the central directory Extra Field and the Decryption - header record preceding compressed file data are: - - 0x0007 - reserved for future use - 0x000F - reserved for future use - 0x0100 - Indicates non-OAEP key wrapping was used. If this - this field is set, the version needed to extract must - be at least 61. This means OAEP key wrapping is not - used when generating a Master Session Key using - ErdData. - 0x4000 - ErdData must be decrypted using 3DES-168, otherwise use the - same algorithm used for encrypting the file contents. - 0x8000 - reserved for future use - - - 7.4.2 CertData - Extra Field 0x0017 record certificate data structure - - The data structure used to store certificate data within the section - of the Extra Field defined by the CertData field of the 0x0017 - record are as shown: - - Value Size Description - ----- ---- ----------- - RCount 4 bytes Number of recipients. - HashAlg 2 bytes Hash algorithm identifier - HSize 2 bytes Hash size - SRList (var) Simple list of recipients hashed public keys - - - RCount This defines the number intended recipients whose - public keys were used for encryption. This identifies - the number of elements in the SRList. - - HashAlg This defines the hash algorithm used to calculate - the public key hash of each public key used - for encryption. This field currently supports - only the following value for SHA-1 - - 0x8004 - SHA1 - - HSize This defines the size of a hashed public key. - - SRList This is a variable length list of the hashed - public keys for each intended recipient. Each - element in this list is HSize. The total size of - SRList is determined using RCount * HSize. - - - 7.4.3 Reserved1 - Certificate Decryption Header Reserved1 Data - - Value Size Description - ----- ---- ----------- - RCount 4 bytes Number of recipients. - - RCount This defines the number intended recipients whose - public keys were used for encryption. This defines - the number of elements in the REList field defined below. - - - 7.4.4 Reserved2 - Certificate Decryption Header Reserved2 Data Structures - - - Value Size Description - ----- ---- ----------- - HashAlg 2 bytes Hash algorithm identifier - HSize 2 bytes Hash size - REList (var) List of recipient data elements - - - HashAlg This defines the hash algorithm used to calculate - the public key hash of each public key used - for encryption. This field currently supports - only the following value for SHA-1 - - 0x8004 - SHA1 - - HSize This defines the size of a hashed public key - defined in REHData. - - REList This is a variable length of list of recipient data. - Each element in this list consists of a Recipient - Element data structure as follows: - - - Recipient Element (REList) Data Structure: - - Value Size Description - ----- ---- ----------- - RESize 2 bytes Size of REHData + REKData - REHData HSize Hash of recipients public key - REKData (var) Simple key blob - - - RESize This defines the size of an individual REList - element. This value is the combined size of the - REHData field + REKData field. REHData is defined by - HSize. REKData is variable and can be calculated - for each REList element using RESize and HSize. - - REHData Hashed public key for this recipient. - - REKData Simple Key Blob. The format of this data structure - is identical to that defined in the Microsoft - CryptoAPI and generated using the CryptExportKey() - function. The version of the Simple Key Blob - supported at this time is 0x02 as defined by - Microsoft. - -7.5 Certificate Processing - Central Directory Encryption ---------------------------------------------------------- - - 7.5.1 Central Directory Encryption using Digital Certificates will - operate in a manner similar to that of Single Password Central - Directory Encryption. This record will only be present when there - is data to place into it. Currently, data is placed into this - record when digital certificates are used for either encrypting - or signing the files within a ZIP file. When only password - encryption is used with no certificate encryption or digital - signing, this record is not currently needed. When present, this - record will appear before the start of the actual Central Directory - data structure and will be located immediately after the Archive - Decryption Header if the Central Directory is encrypted. - - 7.5.2 The Archive Extra Data record will be used to store the following - information. Additional data may be added in future versions. - - Extra Data Fields: - - 0x0014 - PKCS#7 Store for X.509 Certificates - 0x0016 - X.509 Certificate ID and Signature for central directory - 0x0019 - PKCS#7 Encryption Recipient Certificate List - - The 0x0014 and 0x0016 Extra Data records that otherwise would be - located in the first record of the Central Directory for digital - certificate processing. When encrypting or compressing the Central - Directory, the 0x0014 and 0x0016 records must be located in the - Archive Extra Data record and they should not remain in the first - Central Directory record. The Archive Extra Data record will also - be used to store the 0x0019 data. - - 7.5.3 When present, the size of the Archive Extra Data record will be - included in the size of the Central Directory. The data of the - Archive Extra Data record will also be compressed and encrypted - along with the Central Directory data structure. - -7.6 Certificate Processing Differences --------------------------------------- - - 7.6.1 The Certificate Processing Method of encryption differs from the - Single Password Symmetric Encryption Method as follows. Instead - of using a user-defined password to generate a master session key, - cryptographically random data is used. The key material is then - wrapped using standard key-wrapping techniques. This key material - is wrapped using the public key of each recipient that will need - to decrypt the file using their corresponding private key. - - 7.6.2 This specification currently assumes digital certificates will follow - the X.509 V3 format for 1024 bit and higher RSA format digital - certificates. Implementation of this Certificate Processing Method - requires supporting logic for key access and management. This logic - is outside the scope of this specification. - -7.7 OAEP Processing with Certificate-based Encryption ------------------------------------------------------ - - 7.7.1 OAEP stands for Optimal Asymmetric Encryption Padding. It is a - strengthening technique used for small encoded items such as decryption - keys. This is commonly applied in cryptographic key-wrapping techniques - and is supported by PKCS #1. Versions 5.0 and 6.0 of this specification - were designed to support OAEP key-wrapping for certificate-based - decryption keys for additional security. - - 7.7.2 Support for private keys stored on Smartcards or Tokens introduced - a conflict with this OAEP logic. Most card and token products do - not support the additional strengthening applied to OAEP key-wrapped - data. In order to resolve this conflict, versions 6.1 and above of this - specification will no longer support OAEP when encrypting using - digital certificates. - - 7.7.3 Versions of PKZIP available during initial development of the - certificate processing method set a value of 61 into the - version needed to extract field for a file. This indicates that - non-OAEP key wrapping is used. This affects certificate encryption - only, and password encryption functions should not be affected by - this value. This means values of 61 may be found on files encrypted - with certificates only, or on files encrypted with both password - encryption and certificate encryption. Files encrypted with both - methods can safely be decrypted using the password methods documented. - -8.0 Splitting and Spanning ZIP files -------------------------------------- - - 8.1 Spanned ZIP files - - 8.1.1 Spanning is the process of segmenting a ZIP file across - multiple removable media. This support has typically only - been provided for DOS formatted floppy diskettes. - - 8.2 Split ZIP files - - 8.2.1 File splitting is a newer derivation of spanning. - Splitting follows the same segmentation process as - spanning, however, it does not require writing each - segment to a unique removable medium and instead supports - placing all pieces onto local or non-removable locations - such as file systems, local drives, folders, etc. - - 8.3 File Naming Differences - - 8.3.1 A key difference between spanned and split ZIP files is - that all pieces of a spanned ZIP file have the same name. - Since each piece is written to a separate volume, no name - collisions occur and each segment can reuse the original - .ZIP file name given to the archive. - - 8.3.2 Sequence ordering for DOS spanned archives uses the DOS - volume label to determine segment numbers. Volume labels - for each segment are written using the form PKBACK#xxx, - where xxx is the segment number written as a decimal - value from 001 - nnn. - - 8.3.3 Split ZIP files are typically written to the same location - and are subject to name collisions if the spanned name - format is used since each segment will reside on the same - drive. To avoid name collisions, split archives are named - as follows. - - Segment 1 = filename.z01 - Segment n-1 = filename.z(n-1) - Segment n = filename.zip - - 8.3.4 The .ZIP extension is used on the last segment to support - quickly reading the central directory. The segment number - n should be a decimal value. - - 8.4 Spanned Self-extracting ZIP Files - - 8.4.1 Spanned ZIP files may be PKSFX Self-extracting ZIP files. - PKSFX files may also be split, however, in this case - the first segment must be named filename.exe. The first - segment of a split PKSFX archive must be large enough to - include the entire executable program. - - 8.5 Capacities and Markers - - 8.5.1 Capacities for split archives are as follows: - - Maximum number of segments = 4,294,967,295 - 1 - Maximum .ZIP segment size = 4,294,967,295 bytes - Minimum segment size = 64K - Maximum PKSFX segment size = 2,147,483,647 bytes - - 8.5.2 Segment sizes may be different however by convention, all - segment sizes should be the same with the exception of the - last, which may be smaller. Local and central directory - header records must never be split across a segment boundary. - When writing a header record, if the number of bytes remaining - within a segment is less than the size of the header record, - end the current segment and write the header at the start - of the next segment. The central directory may span segment - boundaries, but no single record in the central directory - should be split across segments. - - 8.5.3 Spanned/Split archives created using PKZIP for Windows - (V2.50 or greater), PKZIP Command Line (V2.50 or greater), - or PKZIP Explorer will include a special spanning - signature as the first 4 bytes of the first segment of - the archive. This signature (0x08074b50) will be - followed immediately by the local header signature for - the first file in the archive. - - 8.5.4 A special spanning marker may also appear in spanned/split - archives if the spanning or splitting process starts but - only requires one segment. In this case the 0x08074b50 - signature will be replaced with the temporary spanning - marker signature of 0x30304b50. Split archives can - only be uncompressed by other versions of PKZIP that - know how to create a split archive. - - 8.5.5 The signature value 0x08074b50 is also used by some - ZIP implementations as a marker for the Data Descriptor - record. Conflict in this alternate assignment can be - avoided by ensuring the position of the signature - within the ZIP file to determine the use for which it - is intended. - -9.0 Change Process ------------------- - - 9.1 In order for the .ZIP file format to remain a viable technology, this - specification should be considered as open for periodic review and - revision. Although this format was originally designed with a - certain level of extensibility, not all changes in technology - (present or future) were or will be necessarily considered in its - design. - - 9.2 If your application requires new definitions to the - extensible sections in this format, or if you would like to - submit new data structures or new capabilities, please forward - your request to zipformat@pkware.com. All submissions will be - reviewed by the ZIP File Specification Committee for possible - inclusion into future versions of this specification. - - 9.3 Periodic revisions to this specification will be published as - DRAFT or as FINAL status to ensure interoperability. We encourage - comments and feedback that may help improve clarity or content. - - -10.0 Incorporating PKWARE Proprietary Technology into Your Product ------------------------------------------------------------------- - - 10.1 The Use or Implementation in a product of APPNOTE technological - components pertaining to either strong encryption or patching requires - a separate, executed license agreement from PKWARE. Please contact - PKWARE at zipformat@pkware.com or +1-414-289-9788 with regard to - acquiring such a license. - - 10.2 Additional information regarding PKWARE proprietray technology is - available at http://www.pkware.com/appnote. - -11.0 Acknowledgements ---------------------- - - In addition to the above mentioned contributors to PKZIP and PKUNZIP, - PKWARE would like to extend special thanks to Robert Mahoney for - suggesting the extension .ZIP for this software. - -12.0 References ---------------- - - Fiala, Edward R., and Greene, Daniel H., "Data compression with - finite windows", Communications of the ACM, Volume 32, Number 4, - April 1989, pages 490-505. - - Held, Gilbert, "Data Compression, Techniques and Applications, - Hardware and Software Considerations", John Wiley & Sons, 1987. - - Huffman, D.A., "A method for the construction of minimum-redundancy - codes", Proceedings of the IRE, Volume 40, Number 9, September 1952, - pages 1098-1101. - - Nelson, Mark, "LZW Data Compression", Dr. Dobbs Journal, Volume 14, - Number 10, October 1989, pages 29-37. - - Nelson, Mark, "The Data Compression Book", M&T Books, 1991. - - Storer, James A., "Data Compression, Methods and Theory", - Computer Science Press, 1988 - - Welch, Terry, "A Technique for High-Performance Data Compression", - IEEE Computer, Volume 17, Number 6, June 1984, pages 8-19. - - Ziv, J. and Lempel, A., "A universal algorithm for sequential data - compression", Communications of the ACM, Volume 30, Number 6, - June 1987, pages 520-540. - - Ziv, J. and Lempel, A., "Compression of individual sequences via - variable-rate coding", IEEE Transactions on Information Theory, - Volume 24, Number 5, September 1978, pages 530-536. - - -APPENDIX A - AS/400 Extra Field (0x0065) Attribute Definitions --------------------------------------------------------------- - -A.1 Field Definition Structure: - - a. field length including length 2 bytes - b. field code 2 bytes - c. data x bytes - -A.2 Field Code Description - - 4001 Source type i.e. CLP etc - 4002 The text description of the library - 4003 The text description of the file - 4004 The text description of the member - 4005 x'F0' or 0 is PF-DTA, x'F1' or 1 is PF_SRC - 4007 Database Type Code 1 byte - 4008 Database file and fields definition - 4009 GZIP file type 2 bytes - 400B IFS code page 2 bytes - 400C IFS Creation Time 4 bytes - 400D IFS Access Time 4 bytes - 400E IFS Modification time 4 bytes - 005C Length of the records in the file 2 bytes - 0068 GZIP two words 8 bytes - -APPENDIX B - z/OS Extra Field (0x0065) Attribute Definitions ------------------------------------------------------------- - -B.1 Field Definition Structure: - - a. field length including length 2 bytes - b. field code 2 bytes - c. data x bytes - -B.2 Field Code Description - - 0001 File Type 2 bytes - 0002 NonVSAM Record Format 1 byte - 0003 Reserved - 0004 NonVSAM Block Size 2 bytes Big Endian - 0005 Primary Space Allocation 3 bytes Big Endian - 0006 Secondary Space Allocation 3 bytes Big Endian - 0007 Space Allocation Type1 byte flag - 0008 Modification Date Retired with PKZIP 5.0 + - 0009 Expiration Date Retired with PKZIP 5.0 + - 000A PDS Directory Block Allocation 3 bytes Big Endian binary value - 000B NonVSAM Volume List variable - 000C UNIT Reference Retired with PKZIP 5.0 + - 000D DF/SMS Management Class 8 bytes EBCDIC Text Value - 000E DF/SMS Storage Class 8 bytes EBCDIC Text Value - 000F DF/SMS Data Class 8 bytes EBCDIC Text Value - 0010 PDS/PDSE Member Info. 30 bytes - 0011 VSAM sub-filetype 2 bytes - 0012 VSAM LRECL 13 bytes EBCDIC "(num_avg num_max)" - 0013 VSAM Cluster Name Retired with PKZIP 5.0 + - 0014 VSAM KSDS Key Information 13 bytes EBCDIC "(num_length num_position)" - 0015 VSAM Average LRECL 5 bytes EBCDIC num_value padded with blanks - 0016 VSAM Maximum LRECL 5 bytes EBCDIC num_value padded with blanks - 0017 VSAM KSDS Key Length 5 bytes EBCDIC num_value padded with blanks - 0018 VSAM KSDS Key Position 5 bytes EBCDIC num_value padded with blanks - 0019 VSAM Data Name 1-44 bytes EBCDIC text string - 001A VSAM KSDS Index Name 1-44 bytes EBCDIC text string - 001B VSAM Catalog Name 1-44 bytes EBCDIC text string - 001C VSAM Data Space Type 9 bytes EBCDIC text string - 001D VSAM Data Space Primary 9 bytes EBCDIC num_value left-justified - 001E VSAM Data Space Secondary 9 bytes EBCDIC num_value left-justified - 001F VSAM Data Volume List variable EBCDIC text list of 6-character Volume IDs - 0020 VSAM Data Buffer Space 8 bytes EBCDIC num_value left-justified - 0021 VSAM Data CISIZE 5 bytes EBCDIC num_value left-justified - 0022 VSAM Erase Flag 1 byte flag - 0023 VSAM Free CI % 3 bytes EBCDIC num_value left-justified - 0024 VSAM Free CA % 3 bytes EBCDIC num_value left-justified - 0025 VSAM Index Volume List variable EBCDIC text list of 6-character Volume IDs - 0026 VSAM Ordered Flag 1 byte flag - 0027 VSAM REUSE Flag 1 byte flag - 0028 VSAM SPANNED Flag 1 byte flag - 0029 VSAM Recovery Flag 1 byte flag - 002A VSAM WRITECHK Flag 1 byte flag - 002B VSAM Cluster/Data SHROPTS 3 bytes EBCDIC "n,y" - 002C VSAM Index SHROPTS 3 bytes EBCDIC "n,y" - 002D VSAM Index Space Type 9 bytes EBCDIC text string - 002E VSAM Index Space Primary 9 bytes EBCDIC num_value left-justified - 002F VSAM Index Space Secondary 9 bytes EBCDIC num_value left-justified - 0030 VSAM Index CISIZE 5 bytes EBCDIC num_value left-justified - 0031 VSAM Index IMBED 1 byte flag - 0032 VSAM Index Ordered Flag 1 byte flag - 0033 VSAM REPLICATE Flag 1 byte flag - 0034 VSAM Index REUSE Flag 1 byte flag - 0035 VSAM Index WRITECHK Flag 1 byte flag Retired with PKZIP 5.0 + - 0036 VSAM Owner 8 bytes EBCDIC text string - 0037 VSAM Index Owner 8 bytes EBCDIC text string - 0038 Reserved - 0039 Reserved - 003A Reserved - 003B Reserved - 003C Reserved - 003D Reserved - 003E Reserved - 003F Reserved - 0040 Reserved - 0041 Reserved - 0042 Reserved - 0043 Reserved - 0044 Reserved - 0045 Reserved - 0046 Reserved - 0047 Reserved - 0048 Reserved - 0049 Reserved - 004A Reserved - 004B Reserved - 004C Reserved - 004D Reserved - 004E Reserved - 004F Reserved - 0050 Reserved - 0051 Reserved - 0052 Reserved - 0053 Reserved - 0054 Reserved - 0055 Reserved - 0056 Reserved - 0057 Reserved - 0058 PDS/PDSE Member TTR Info. 6 bytes Big Endian - 0059 PDS 1st LMOD Text TTR 3 bytes Big Endian - 005A PDS LMOD EP Rec # 4 bytes Big Endian - 005B Reserved - 005C Max Length of records 2 bytes Big Endian - 005D PDSE Flag 1 byte flag - 005E Reserved - 005F Reserved - 0060 Reserved - 0061 Reserved - 0062 Reserved - 0063 Reserved - 0064 Reserved - 0065 Last Date Referenced 4 bytes Packed Hex "yyyymmdd" - 0066 Date Created 4 bytes Packed Hex "yyyymmdd" - 0068 GZIP two words 8 bytes - 0071 Extended NOTE Location 12 bytes Big Endian - 0072 Archive device UNIT 6 bytes EBCDIC - 0073 Archive 1st Volume 6 bytes EBCDIC - 0074 Archive 1st VOL File Seq# 2 bytes Binary - -APPENDIX C - Zip64 Extensible Data Sector Mappings ---------------------------------------------------- - - -Z390 Extra Field: - - The following is the general layout of the attributes for the - ZIP 64 "extra" block for extended tape operations. - - Note: some fields stored in Big Endian format. All text is - in EBCDIC format unless otherwise specified. - - Value Size Description - ----- ---- ----------- - (Z390) 0x0065 2 bytes Tag for this "extra" block type - Size 4 bytes Size for the following data block - Tag 4 bytes EBCDIC "Z390" - Length71 2 bytes Big Endian - Subcode71 2 bytes Enote type code - FMEPos 1 byte - Length72 2 bytes Big Endian - Subcode72 2 bytes Unit type code - Unit 1 byte Unit - Length73 2 bytes Big Endian - Subcode73 2 bytes Volume1 type code - FirstVol 1 byte Volume - Length74 2 bytes Big Endian - Subcode74 2 bytes FirstVol file sequence - FileSeq 2 bytes Sequence - -APPENDIX D - Language Encoding (EFS) ------------------------------------- - -D.1 The ZIP format has historically supported only the original IBM PC character -encoding set, commonly referred to as IBM Code Page 437. This limits storing -file name characters to only those within the original MS-DOS range of values -and does not properly support file names in other character encodings, or -languages. To address this limitation, this specification will support the -following change. - -D.2 If general purpose bit 11 is unset, the file name and comment should conform -to the original ZIP character encoding. If general purpose bit 11 is set, the -filename and comment must support The Unicode Standard, Version 4.1.0 or -greater using the character encoding form defined by the UTF-8 storage -specification. The Unicode Standard is published by the The Unicode -Consortium (www.unicode.org). UTF-8 encoded data stored within ZIP files -is expected to not include a byte order mark (BOM). - -D.3 Applications may choose to supplement this file name storage through the use -of the 0x0008 Extra Field. Storage for this optional field is currently -undefined, however it will be used to allow storing extended information -on source or target encoding that may further assist applications with file -name, or file content encoding tasks. Please contact PKWARE with any -requirements on how this field should be used. - -D.4 The 0x0008 Extra Field storage may be used with either setting for general -purpose bit 11. Examples of the intended usage for this field is to store -whether "modified-UTF-8" (JAVA) is used, or UTF-8-MAC. Similarly, other -commonly used character encoding (code page) designations can be indicated -through this field. Formalized values for use of the 0x0008 record remain -undefined at this time. The definition for the layout of the 0x0008 field -will be published when available. Use of the 0x0008 Extra Field provides -for storing data within a ZIP file in an encoding other than IBM Code -Page 437 or UTF-8. - -D.5 General purpose bit 11 will not imply any encoding of file content or -password. Values defining character encoding for file content or -password must be stored within the 0x0008 Extended Language Encoding -Extra Field. - -D.6 Ed Gordon of the Info-ZIP group has defined a pair of "extra field" records -that can be used to store UTF-8 file name and file comment fields. These -records can be used for cases when the general purpose bit 11 method -for storing UTF-8 data in the standard file name and comment fields is -not desirable. A common case for this alternate method is if backward -compatibility with older programs is required. - -D.7 Definitions for the record structure of these fields are included above -in the section on 3rd party mappings for "extra field" records. These -records are identified by Header ID's 0x6375 (Info-ZIP Unicode Comment -Extra Field) and 0x7075 (Info-ZIP Unicode Path Extra Field). - -D.8 The choice of which storage method to use when writing a ZIP file is left -to the implementation. Developers should expect that a ZIP file may -contain either method and should provide support for reading data in -either format. Use of general purpose bit 11 reduces storage requirements -for file name data by not requiring additional "extra field" data for -each file, but can result in older ZIP programs not being able to extract -files. Use of the 0x6375 and 0x7075 records will result in a ZIP file -that should always be readable by older ZIP programs, but requires more -storage per file to write file name and/or file comment fields. +APPNOTE.TXT is the ZIP file format specification. +Section 1.4 states that PKWARE permits use of this document only for creating products, programs, and processes that read and write ZIP files, and that any reproduction or distribution of the document in whole or in part without prior written permission from PKWARE is strictly prohibited. +This file has therefore been replaced with a reference link. +https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT From f77f372d9f96160b07cf0d4e7d4108b4876ba61a Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:48:26 +0900 Subject: [PATCH 30/74] Add specification link label to APPNOTE reference --- reference/APPNOTE.TXT | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reference/APPNOTE.TXT b/reference/APPNOTE.TXT index 5ddac229..e74b9e0e 100644 --- a/reference/APPNOTE.TXT +++ b/reference/APPNOTE.TXT @@ -1,4 +1,6 @@ APPNOTE.TXT is the ZIP file format specification. Section 1.4 states that PKWARE permits use of this document only for creating products, programs, and processes that read and write ZIP files, and that any reproduction or distribution of the document in whole or in part without prior written permission from PKWARE is strictly prohibited. This file has therefore been replaced with a reference link. + +Specification Link: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT From b36689af20e0e0d101dc1b21e9d69e0ccf952c42 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 19 Apr 2026 11:48:54 +0100 Subject: [PATCH 31/74] format --- tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index 9cb8f2ca..2864209d 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -259,6 +259,7 @@ public class PooledMemoryStreamTests Assert.Null(exception); Assert.Equal(length, stream.Length); } + private sealed class TrackingArrayPool : ArrayPool { private const byte RentedBufferFillValue = 0x5A; From ad3c5dafd70e8794da3160462931e74662d38ee4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 19 Apr 2026 12:11:50 +0100 Subject: [PATCH 32/74] update tests --- src/SharpCompress/packages.lock.json | 12 +++++----- .../Streams/PooledMemoryStreamTests.cs | 22 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index a401c702..e82196f8 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs index 2864209d..d2b0996c 100644 --- a/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/PooledMemoryStreamTests.cs @@ -209,10 +209,10 @@ public class PooledMemoryStreamTests [Fact] public void DisposeAfterGetBufferDoesNotReturnExposedArrayToPool() { - var pool = new TrackingArrayPool(); + var pool = new OverRentingArrayPool(extraLength: 8); byte[] buffer; - using (var stream = new PooledMemoryStream(pool, capacity: 0, blockSize: 8)) + using (var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, pool)) { stream.Write(new byte[] { 1, 2, 3 }, 0, 3); buffer = stream.GetBuffer(); @@ -221,7 +221,7 @@ public class PooledMemoryStreamTests Assert.NotEmpty(pool.RentRequests); } - Assert.Empty(pool.ReturnedLengths); + Assert.DoesNotContain(buffer, pool.ReturnedArrays); Assert.Equal(1, buffer[0]); Assert.Equal(2, buffer[1]); Assert.Equal(3, buffer[2]); @@ -230,10 +230,10 @@ public class PooledMemoryStreamTests [Fact] public void DisposeAfterTryGetBufferDoesNotReturnExposedArrayToPool() { - var pool = new TrackingArrayPool(); + var pool = new OverRentingArrayPool(extraLength: 8); ArraySegment segment; - using (var stream = new PooledMemoryStream(pool, capacity: 0, blockSize: 8)) + using (var stream = new PooledMemoryStream(capacity: 0, blockSize: 8, pool)) { stream.Write(new byte[] { 1, 2, 3 }, 0, 3); @@ -242,22 +242,20 @@ public class PooledMemoryStreamTests Assert.NotEmpty(pool.RentRequests); } - Assert.Empty(pool.ReturnedLengths); + Assert.DoesNotContain(segment.Array!, pool.ReturnedArrays); Assert.Equal(1, segment.Array![segment.Offset]); Assert.Equal(2, segment.Array[segment.Offset + 1]); Assert.Equal(3, segment.Array[segment.Offset + 2]); } [Fact] - public void SetLengthNearIntMaxValueDoesNotThrowIOException() + public void SetLengthNearIntMaxValueThrowsIOExceptionWhenBlockRoundingOverflows() { using var stream = new PooledMemoryStream(capacity: 0, blockSize: 8); var length = int.MaxValue - 1L; - var exception = Record.Exception(() => stream.SetLength(length)); - - Assert.Null(exception); - Assert.Equal(length, stream.Length); + Assert.Throws(() => stream.SetLength(length)); + Assert.Equal(0, stream.Length); } private sealed class TrackingArrayPool : ArrayPool @@ -301,6 +299,7 @@ public class PooledMemoryStreamTests public readonly System.Collections.Generic.List RentRequests = new(); public readonly System.Collections.Generic.List ReturnedLengths = new(); + public readonly System.Collections.Generic.List ReturnedArrays = new(); public override byte[] Rent(int minimumLength) { @@ -311,6 +310,7 @@ public class PooledMemoryStreamTests public override void Return(byte[] array, bool clearArray = false) { ReturnedLengths.Add(array.Length); + ReturnedArrays.Add(array); if (clearArray) { Array.Clear(array, 0, array.Length); From a59f4f330d9dbf907432712c0ffad20aacb26314 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 19 Apr 2026 12:17:39 +0100 Subject: [PATCH 33/74] update docs for tar gap analysis --- docs/FORMATS.md | 2 +- docs/TAR_GAP_ANALYSIS.md | 340 +++++++++++++++++++++++++++++++ docs/TAR_SPEC.md | 430 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 771 insertions(+), 1 deletion(-) create mode 100644 docs/TAR_GAP_ANALYSIS.md create mode 100644 docs/TAR_SPEC.md diff --git a/docs/FORMATS.md b/docs/FORMATS.md index a0c66705..d086f92c 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -20,7 +20,7 @@ | Tar.BZip2 | BZip2 | Both | TarArchive | TarReader | TarWriter (3) | | Tar.Zstandard | ZStandard | Decompress | TarArchive | TarReader | N/A | | Tar.LZip | LZMA | Both | TarArchive | TarReader | TarWriter (3) | -| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | TarWriter (3) | +| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | N/A | | GZip (single file) | DEFLATE | Both | GZipArchive | GZipReader | GZipWriter | | 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Both | SevenZipArchive | N/A | SevenZipWriter | diff --git a/docs/TAR_GAP_ANALYSIS.md b/docs/TAR_GAP_ANALYSIS.md new file mode 100644 index 00000000..3e46db52 --- /dev/null +++ b/docs/TAR_GAP_ANALYSIS.md @@ -0,0 +1,340 @@ +# Tar Gap Analysis + +## Scope + +This document compares the current Tar documentation, tests, and code paths in SharpCompress. + +It is intentionally implementation-focused. The goal is to identify mismatches, omissions, and incomplete areas in the current SharpCompress Tar support. + +Primary references: + +- `docs/FORMATS.md` +- `src/SharpCompress/Factories/TarFactory.cs` +- `src/SharpCompress/Factories/TarWrapper.cs` +- `src/SharpCompress/Archives/Tar/` +- `src/SharpCompress/Readers/Tar/` +- `src/SharpCompress/Writers/Tar/` +- `src/SharpCompress/Common/Tar/` +- `tests/SharpCompress.Test/Tar/` + +## Claimed vs Actual Support + +### `Tar.XZ` is read-only + +`tar.xz` is supported for reading, but not for writing. + +Actual implementation in `src/SharpCompress/Writers/Tar/TarWriter.cs` does not support `CompressionType.Xz`. The writer throws `InvalidFormatException` for any compression type outside: + +- `None` +- `GZip` +- `BZip2` +- `LZip` + +Impact: + +- Tar write support is narrower than Tar read support +- `tar.xz` creation is not available through the built-in Tar writer + +Recommended action: + +- keep the format table marked `N/A` for Tar.XZ writer support + +## Read-Path Gaps + +### PAX headers are not implemented + +There is no explicit support for POSIX PAX local extended headers. + +Evidence: + +- `EntryType` does not define the usual local PAX header type value +- `TarHeader.Read` handles `LongName` and `LongLink`, but does not implement PAX record parsing +- there are no tests or test archives covering PAX behavior + +Impact: + +- archives relying on PAX for long names, metadata, or timestamps may not be interpreted correctly + +Recommended action: + +- decide whether PAX is intentionally unsupported or should be implemented +- document that decision explicitly + +### Sparse files are not semantically implemented + +`EntryType` defines `SparseFile`, but the read path does not contain sparse map handling or sparse reconstruction logic. + +Evidence: + +- `src/SharpCompress/Common/Tar/Headers/EntryType.cs` +- no sparse-specific code in `TarHeader`, `TarEntry`, `TarFilePart`, or `TarArchive` +- no sparse tests + +Impact: + +- sparse entries may be treated as ordinary entries rather than sparse files with holes + +Recommended action: + +- document sparse support as unsupported or partial +- add explicit tests if future support is added + +### Global extended headers are not semantically implemented + +`EntryType` defines `GlobalExtendedHeader`, but no semantic handling exists in the read pipeline. + +Evidence: + +- `TarHeader.Read` does not special-case `GlobalExtendedHeader` +- `TarEntry` does not surface a global-header model +- no tests cover this case + +Impact: + +- global metadata records are not applied in a defined way + +Recommended action: + +- document as unsupported until explicit behavior exists + +### Device and FIFO semantics are not surfaced + +The entry type enum includes `CharDevice`, `BlockDevice`, and `Fifo`, but the public tar model does not expose device metadata semantics. + +Impact: + +- such entries may not round-trip meaningfully through the API +- behavior is undocumented and untested + +Recommended action: + +- either document them as raw/unmodeled entry types or add dedicated support + +## Write-Path Gaps + +### `HeaderFormat` is not honored consistently + +`TarWriterOptions.HeaderFormat` exists and defaults to `GNU_TAR_LONG_LINK`, but the configured value is not consistently applied. + +### Sync directory write path + +`TarWriter.WriteDirectory` creates headers using: + +- `new TarHeader(WriterOptions.ArchiveEncoding)` + +This uses the default tar header format rather than the writer's configured `headerFormat` field. + +Impact: + +- directory entries written through the sync path do not follow `TarWriterOptions.HeaderFormat` + +### Async write path + +`TarWriter.WriteAsync` and `WriteDirectoryAsync` also create headers using the default constructor rather than the configured header format. + +Impact: + +- async writes ignore `TarWriterOptions.HeaderFormat` for both file and directory entries + +Recommended action: + +- pass the configured header format to all `TarHeader` constructions in sync and async write paths +- add tests for both `GNU_TAR_LONG_LINK` and `USTAR` + +### No public link-writing support + +The read path supports symbolic and hard link targets through `TarEntry.LinkTarget`, but the write API exposes only regular file and directory creation. + +Impact: + +- symlink and hardlink tar archives cannot be created through the current public Tar writer API + +Recommended action: + +- either document this as a deliberate limitation or add link-writing APIs + +### Metadata round-trip support is incomplete + +The writer does not round-trip rich tar metadata beyond the basic fields needed for file and directory entries. + +Current write behavior sets fixed defaults for some fields such as mode, owner id, and group id. + +Impact: + +- modified or newly created tar archives may lose metadata fidelity relative to the original archive + +Recommended action: + +- document current metadata write behavior clearly +- expand metadata support only if needed by consumers + +### No write support for some detected wrappers + +The detection and read path supports wrappers that the write path does not support. + +| Wrapper | Read support | Write support | +| ------- | ------------ | ------------- | +| `tar.xz` | Yes | No | +| `tar.zst` | Yes | No | +| `tar.Z` | Yes | No | + +This is not inherently wrong, but it should be clearly documented everywhere support is summarized. + +## Sync and Async API Inconsistencies + +### Seekability requirements differ at the API boundary + +Synchronous `TarArchive.OpenArchive(Stream)` explicitly throws if the stream is not seekable. + +Asynchronous `TarArchive.OpenAsyncArchive(Stream)` does not perform the same public guard. + +Impact: + +- callers do not see the same contract from sync and async overloads +- behavior is harder to reason about from API docs alone + +Recommended action: + +- either align the contracts or document the difference explicitly + +### Async and sync write behavior do not align on header format handling + +This is the most visible sync/async inconsistency in the current Tar writer implementation. + +Recommended action: + +- fix the implementation first +- add matching sync and async tests to keep the behavior aligned + +## Test Coverage Gaps + +### Symlink coverage exists in test data but not in assertions + +There is a tar archive containing symlinks: + +- `tests/TestArchives/Archives/TarWithSymlink.tar.gz` + +Current Tar tests do not assert tar symlink behavior against that fixture. + +Impact: + +- the code claims practical read support for link targets, but coverage does not verify it + +Recommended action: + +- add reader and archive tests asserting `EntryType`-derived behavior and `LinkTarget` + +### No tests for `HeaderFormat` + +There are currently no tests covering: + +- `TarWriterOptions.HeaderFormat = USTAR` +- `TarWriterOptions.HeaderFormat = GNU_TAR_LONG_LINK` +- long-name failures in USTAR mode +- long-name success in GNU mode through the async writer path + +Impact: + +- the current header-format regressions were able to exist without test coverage + +Recommended action: + +- add dedicated sync and async writer tests for header format selection + +### No tests for PAX, sparse, or global headers + +There is no evidence of coverage for: + +- PAX local headers +- global extended headers +- sparse tar entries + +Impact: + +- unsupported or partial behavior is neither documented by tests nor protected from regression + +Recommended action: + +- either add fixtures and tests or document these as unsupported with no test coverage + +### No tests for unsupported write wrappers + +There are negative tests for an invalid `Rar` compression type, but not for unsupported tar wrappers that a user might reasonably infer from read support. + +Missing negative cases include: + +- `CompressionType.Xz` +- `CompressionType.ZStandard` +- `CompressionType.Lzw` + +Recommended action: + +- add explicit negative tests so the supported write matrix stays intentional + +## Documentation Gaps + +### Current format documentation is too coarse for Tar + +`docs/FORMATS.md` summarizes support at the wrapper level, but Tar behavior depends on more than wrapper compression. + +Missing implementation-specific details include: + +- GNU long-name and long-link support +- USTAR prefix handling +- oldgnu numeric quirk handling +- missing PAX support +- missing sparse support +- reader vs archive behavior differences for compressed tar +- file-size requirements for writing from non-seekable sources + +Recommended action: + +- keep `docs/FORMATS.md` high-level +- add and maintain a dedicated Tar spec document for details + +### The current docs do not call out partial support clearly + +The codebase supports some tar dialect features and not others, but the docs do not separate: + +- fully supported +- partially supported +- unsupported + +Recommended action: + +- use an explicit feature matrix in the Tar documentation + +## Recommended Follow-Ups + +### Priority 0 + +- Correct `docs/FORMATS.md` for `Tar.XZ` write support + +### Priority 1 + +- Fix `TarWriterOptions.HeaderFormat` handling in sync and async writer paths +- Add tests for header-format behavior +- Add symlink coverage using `TarWithSymlink.tar.gz` + +### Priority 2 + +- Decide and document the support position for PAX headers +- Decide and document the support position for sparse files +- Decide and document the support position for global extended headers + +### Priority 3 + +- Add negative writer tests for unsupported wrapper compressions +- Evaluate whether sync and async archive open contracts should match exactly +- Improve metadata round-trip behavior only if there is a consumer need + +## Summary + +The SharpCompress Tar implementation is strong on common read scenarios and basic write scenarios, but the current gaps fall into four categories: + +- documentation overstating or under-describing support +- incomplete feature coverage for less common tar dialect features +- sync/async and file/directory inconsistencies in writer header-format handling +- test coverage holes around links and advanced tar metadata features + +`docs/TAR_SPEC.md` should be treated as the implementation baseline. This document identifies where that baseline is incomplete, inconsistent, or incorrectly reflected elsewhere in the repository. diff --git a/docs/TAR_SPEC.md b/docs/TAR_SPEC.md new file mode 100644 index 00000000..0d371332 --- /dev/null +++ b/docs/TAR_SPEC.md @@ -0,0 +1,430 @@ +# Tar Spec + +## Scope + +This document describes the Tar implementation that exists in SharpCompress today. + +It is intentionally SharpCompress-specific. It documents actual behavior in the current codebase, including partial support and limitations. It is not a general tar format reference. + +Primary implementation files: + +- `src/SharpCompress/Factories/TarFactory.cs` +- `src/SharpCompress/Factories/TarWrapper.cs` +- `src/SharpCompress/Archives/Tar/TarArchive.cs` +- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs` +- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs` +- `src/SharpCompress/Readers/Tar/TarReader.cs` +- `src/SharpCompress/Readers/Tar/TarReader.Async.cs` +- `src/SharpCompress/Writers/Tar/TarWriter.cs` +- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs` +- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs` +- `src/SharpCompress/Common/Tar/Headers/TarHeader.cs` +- `src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs` +- `src/SharpCompress/Common/Tar/TarHeaderFactory.cs` +- `src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs` + +## API Surface + +SharpCompress exposes Tar support through four main entry points. + +| Type | Role | +| ---- | ---- | +| `TarFactory` | Format detection and factory entry point for archive, reader, and writer APIs | +| `TarArchive` | Archive API for enumerating and rewriting tar archives | +| `TarReader` | Forward-only reader API for streaming tar extraction | +| `TarWriter` | Forward-only writer API for creating tar archives | + +`TarWriterOptions` controls output compression, stream ownership, archive finalization, encoding, and header write format. + +## Supported Wrapper Formats + +Tar wrapper detection is defined by `TarWrapper.Wrappers` in `src/SharpCompress/Factories/TarWrapper.cs`. + +### Supported Extensions + +| Wrapper | Extensions | +| ------- | ---------- | +| Plain tar | `tar` | +| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` | +| Tar + GZip | `tar.gz`, `taz`, `tgz` | +| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` | +| Tar + LZip | `tar.lz` | +| Tar + XZ | `tar.xz`, `txz` | +| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` | + +### API Support Matrix + +| Wrapper | Detection | `TarArchive` read | `TarReader` read | `TarWriter` write | +| ------- | --------- | ----------------- | ---------------- | ----------------- | +| Plain tar | Yes | Yes | Yes | Yes | +| Tar + GZip | Yes | Yes | Yes | Yes | +| Tar + BZip2 | Yes | Yes | Yes | Yes | +| Tar + LZip | Yes | Yes | Yes | Yes | +| Tar + XZ | Yes | Yes | Yes | No | +| Tar + ZStandard | Yes | Yes | Yes | No | +| Tar + LZW compress | Yes | Yes | Yes | No | + +Write support is implemented in `src/SharpCompress/Writers/Tar/TarWriter.cs` and currently accepts only `CompressionType.None`, `CompressionType.GZip`, `CompressionType.BZip2`, and `CompressionType.LZip`. + +## Detection Behavior + +Tar detection is implemented in `TarFactory.IsArchive`, `TarFactory.IsArchiveAsync`, `TarFactory.GetCompressionType`, and `TarFactory.GetCompressionTypeAsync`. + +Detection behavior is: + +1. Wrap the incoming stream in `SharpCompressStream`. +2. Start recording with a rewind buffer sized from `TarWrapper.MaximumRewindBufferSize`. +3. Probe each registered wrapper in order. +4. If a wrapper matches, create a decompression stream for that wrapper. +5. Call `TarArchive.IsTarFile` or `TarArchive.IsTarFileAsync` on the decompressed stream. +6. If the tar probe succeeds, treat the stream as tar with that wrapper compression. + +Implications: + +- Tar detection is content-based, not extension-based. +- Wrapper detection is not sufficient by itself. The decompressed payload must also parse as tar. +- Non-seekable detection is supported through the recording and rewind mechanism. +- The largest rewind requirement currently comes from BZip2, which declares a larger minimum probe buffer in `TarWrapper`. + +`TarArchive.IsTarFile` and `TarArchive.IsTarFileAsync` attempt to read a single tar header and return `false` on any exception. They also treat an all-zero empty archive block as a valid empty tar archive when the entry type is defined. + +## Reader Behavior + +`TarReader` is the forward-only streaming API. + +Implementation files: + +- `src/SharpCompress/Readers/Tar/TarReader.cs` +- `src/SharpCompress/Readers/Tar/TarReader.Async.cs` +- `src/SharpCompress/Common/Tar/TarEntry.cs` +- `src/SharpCompress/Common/Tar/TarEntry.Async.cs` +- `src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs` + +Reader behavior: + +- The reader always enumerates entries in streaming mode. +- It works with non-seekable input streams. +- It applies decompression based on the detected wrapper compression type before parsing tar headers. +- Entry streams are backed by `TarReadOnlySubStream`. + +`TarReadOnlySubStream` has an important behavior: disposing an entry stream consumes any unread entry bytes and any required 512-byte padding so that the next header can be read correctly. This is what makes skipping entries work in streaming mode. + +### Reader Compression Mapping + +`TarReader.RequestInitialStream` and `RequestInitialStreamAsync` map the detected wrapper to the corresponding decompression stream: + +- `None` +- `BZip2` +- `GZip` +- `ZStandard` +- `LZip` +- `Xz` +- `Lzw` + +### Reader Entry Semantics + +For each entry, SharpCompress exposes: + +- `Key` from the parsed tar name +- `LinkTarget` for symbolic and hard links +- `Size` +- `CompressedSize` +- `LastModifiedTime` +- `IsDirectory` +- `Mode` +- `UserID` +- `GroupId` + +Tar entries are always reported as unencrypted and CRC is always `0`. + +## Archive Behavior + +`TarArchive` is the archive API. + +Implementation files: + +- `src/SharpCompress/Archives/Tar/TarArchive.cs` +- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs` +- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs` + +### Open Behavior + +Synchronous `TarArchive.OpenArchive(Stream)` requires a seekable stream and throws `ArgumentException` when `CanSeek` is `false`. + +`TarArchive.OpenArchive(FileInfo)` and the list-based overloads use `SourceStream` and determine wrapper compression by calling `TarFactory.GetCompressionType`. + +Asynchronous `OpenAsyncArchive` overloads use `TarFactory.GetCompressionTypeAsync` and do not enforce the same explicit seekability check at the public API boundary. + +### Entry Loading + +`TarArchive.LoadEntries` and `LoadEntriesAsync` parse entries differently depending on wrapper compression: + +- Uncompressed tar uses `StreamingMode.Seekable`. +- Wrapped tar uses `StreamingMode.Streaming` because the decompressed stream is not treated as random-access. + +When seekable mode is used, the header stores `DataStartPosition`, and entries reopen data through `TarFilePart` by seeking back to the data position. + +When streaming mode is used, the header stores a `PackedStream`, and entry access follows streaming semantics over the decompressed stream. + +### Archive Rewrite Behavior + +`TarArchive` supports creating and modifying archives through `AbstractWritableArchive`: + +- add file entries +- add directory entries +- remove entries +- save to a new stream or path + +Archive rewrite is implemented by enumerating the existing and new entries and writing them back out through `TarWriter`. + +## Writer Behavior + +`TarWriter` is the forward-only tar writer. + +Implementation files: + +- `src/SharpCompress/Writers/Tar/TarWriter.cs` +- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs` +- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs` + +### Supported Output Compression + +The writer supports these output compression types: + +- `CompressionType.None` +- `CompressionType.GZip` +- `CompressionType.BZip2` +- `CompressionType.LZip` + +Any other compression type causes `InvalidFormatException`. + +### Stream Ownership + +If `LeaveStreamOpen` is `true`, `TarWriter` wraps the destination in a non-disposing stream. + +### File Writing + +`TarWriter.Write` and `WriteAsync` write a tar header followed by file contents, then pad the payload to the next 512-byte boundary. + +If the source stream is non-seekable and the caller does not supply `size`, the writer throws `ArgumentException` because tar requires the file size in the header. + +### Directory Writing + +`WriteDirectory` and `WriteDirectoryAsync` normalize the directory name to use forward slashes and ensure the key ends with `/`. + +Empty or root-equivalent directory names are skipped. + +### Archive Finalization + +If `FinalizeArchiveOnClose` is `true`, disposing the writer writes two 512-byte zero blocks to terminate the archive. + +If the output stream implements `IFinishable`, dispose also calls `Finish()`. + +## Header Write Formats + +`TarHeaderWriteFormat` is defined in `src/SharpCompress/Common/Tar/Headers/TarHeaderWriteFormat.cs`. + +Supported write formats: + +- `GNU_TAR_LONG_LINK` +- `USTAR` + +`TarWriterOptions.HeaderFormat` defaults to `GNU_TAR_LONG_LINK`. + +Current implementation behavior is narrower than the option surface suggests: + +- sync file writes use the configured `HeaderFormat` +- sync directory writes currently construct the default tar header format +- async file writes currently construct the default tar header format +- async directory writes currently construct the default tar header format + +In practice, this means the configured `HeaderFormat` is currently honored only by the synchronous file write path. + +### GNU Long Name Write Behavior + +In GNU mode, when a file name exceeds the 100-byte field, `TarHeader.WriteGnuTarLongLink` writes a synthetic long-name header using `././@LongLink` and `EntryType.LongName`, then writes the long name payload, and finally writes the actual file entry. + +GNU mode also writes large file sizes using binary size encoding when the size does not fit the standard octal field. + +### USTAR Write Behavior + +When the synchronous file write path is configured for `USTAR`, `TarHeader.WriteUstar` attempts to split a long path into: + +- the main `name` field +- the `prefix` field + +If the name cannot be represented in USTAR field limits, the writer throws `InvalidFormatException` and instructs the caller to use GNU Tar format instead. + +## Header Read Behavior + +Tar header parsing is implemented in `TarHeader.Read` and `TarHeader.ReadAsync`. + +### Implemented Read Features + +| Feature | Read support | +| ------- | ------------ | +| Regular file entries | Yes | +| Directory entries | Yes | +| Symbolic link target reading | Yes | +| Hard link target reading | Yes | +| GNU long name (`L`) | Yes | +| GNU long link (`K`) | Yes | +| USTAR prefix reconstruction | Yes | +| Binary size field parsing | Yes | +| oldgnu uid/gid numeric quirk parsing | Yes | +| POSIX and signed checksum validation | Yes | + +### Entry Types Recognized by the Code + +`EntryType` currently declares these values in `src/SharpCompress/Common/Tar/Headers/EntryType.cs`: + +- `File` +- `OldFile` +- `HardLink` +- `SymLink` +- `CharDevice` +- `BlockDevice` +- `Directory` +- `Fifo` +- `LongLink` +- `LongName` +- `SparseFile` +- `VolumeHeader` +- `GlobalExtendedHeader` + +SharpCompress currently has explicit handling for only a subset of those values during read and write. + +### Long Name and Long Link Reads + +When `TarHeader.Read` encounters `EntryType.LongName` or `EntryType.LongLink`, it reads the payload and applies it to the next real header. + +Long-name payload reads are capped at `32768` bytes to avoid memory exhaustion from malformed archives. + +### Name Reconstruction + +For USTAR headers, if the magic field is `ustar` and the prefix field is populated, SharpCompress reconstructs the entry name as `prefix + "/" + name`. + +## Name and Metadata Handling + +### Path Normalization + +Writer path normalization is implemented in `TarWriter.NormalizeFilename` and `NormalizeDirectoryName`. + +Behavior: + +- backslashes are converted to `/` +- drive prefixes before `:` are removed +- leading and trailing `/` are trimmed for file entries +- directory entries are normalized to end with `/` + +### Encoding + +Tar name encoding and decoding is controlled by `IArchiveEncoding`. + +- reader APIs decode names with `ReaderOptions.ArchiveEncoding` +- writer APIs encode names with `TarWriterOptions.ArchiveEncoding` + +The tests include UTF-8 and code page coverage for tar name handling. + +### Metadata Surface + +Tar metadata currently surfaced through `TarEntry` includes: + +- name +- link target +- mode +- uid +- gid +- size +- last modified time + +Writer metadata is narrower. The writer sets: + +- `LastModifiedTime` +- `Name` +- `Size` +- entry type for file or directory + +The current writer writes fixed mode, owner id, and group id defaults rather than round-tripping full metadata. + +## Async Behavior + +Async tar support is provided by: + +- `TarArchive.OpenAsyncArchive` +- `TarReader.OpenAsyncReader` +- `TarWriter.WriteAsync` +- `TarWriter.WriteDirectoryAsync` +- `TarHeader.ReadAsync` +- `TarHeader.WriteAsync` + +The async implementations generally mirror the sync implementations while using async header parsing, decompression, and stream copy paths. The most important current exception is `TarWriterOptions.HeaderFormat`, which is not consistently honored outside the synchronous file write path. + +## Known Limitations + +This section documents current implementation limits, not desired future behavior. + +### Write limitations + +- No write support for `tar.xz` +- No write support for `tar.zst` +- No write support for `tar.Z` +- No public API for writing symbolic links or hard links +- No PAX write support +- No sparse file write support +- No device or FIFO write support + +### Read limitations or partial support + +- No explicit PAX local header support +- No semantic sparse file handling beyond recognizing the entry type enum value +- No semantic global extended header handling beyond recognizing the entry type enum value +- No special device or FIFO object model beyond the raw entry type information available internally + +### Archive behavior limitations + +- Sync archive open requires a seekable input stream +- Compressed tar archive access is not full random-access in the same sense as uncompressed seekable tar + +## Test Coverage Map + +Tar tests live in `tests/SharpCompress.Test/Tar/`. + +Representative coverage: + +| Area | Tests | +| ---- | ----- | +| Wrapper detection and reading | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` | +| Archive open and rewrite | `TarArchiveTests.cs`, `TarArchiveAsyncTests.cs` | +| Writer behavior | `TarWriterTests.cs`, `TarWriterAsyncTests.cs` | +| Directory entry behavior | `TarWriterDirectoryTests.cs`, `TarArchiveDirectoryTests.cs` | +| Long-name behavior | `TarArchiveTests.cs`, `TarReaderTests.cs` | +| Corruption and broken stream handling | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` | + +Representative tar test archives in `tests/TestArchives/Archives/`: + +- `Tar.tar` +- `Tar.tar.gz` +- `Tar.tar.bz2` +- `Tar.tar.lz` +- `Tar.tar.xz` +- `Tar.tar.zst` +- `Tar.tar.Z` +- `Tar.oldgnu.tar.gz` +- `very long filename.tar` +- `ustar with long names.tar` +- `Tar.LongPathsWithLongNameExtension.tar` +- `Tar.Empty.tar` +- `TarCorrupted.tar` +- `TarWithSymlink.tar.gz` + +## Summary + +SharpCompress Tar support is centered around: + +- broad read support for common tar wrappers +- forward-only reader behavior for streamed extraction +- seekable archive support for uncompressed tar and archive rewrite workflows +- narrower write support than read support +- GNU long-name and USTAR write support +- partial coverage for less common tar dialect features From 891d1268fb6bb03ce3f22cc592fd36552baf4e54 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 19 Apr 2026 12:18:47 +0100 Subject: [PATCH 34/74] Update src/SharpCompress/IO/PooledMemoryStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 8caadd31..3fabeb0b 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -81,8 +81,8 @@ public sealed class PooledMemoryStream : MemoryStream } set { - ThrowHelper.ThrowIfNegative(value, nameof(value)); EnsureNotClosed(); + ThrowHelper.ThrowIfNegative(value, nameof(value)); ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value)); _position = (int)value; From e0ccbbb7c7c108b70099ac85b9a117ad4cfb178c Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 19 Apr 2026 12:19:06 +0100 Subject: [PATCH 35/74] Update src/SharpCompress/IO/PooledMemoryStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 3fabeb0b..8111945e 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -153,11 +153,10 @@ public sealed class PooledMemoryStream : MemoryStream public override void SetLength(long value) { - ThrowHelper.ThrowIfNegative(value, nameof(value)); - ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value)); - EnsureWritable(); + ThrowHelper.ThrowIfNegative(value, nameof(value)); + ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value)); var newLength = (int)value; if (newLength > _capacity) { From 49a7e15a8b3d669dd4f2bb375f35855f5325b4cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:20:33 +0000 Subject: [PATCH 36/74] Add EnsureNotClosed() to Flush and FlushAsync in PooledMemoryStream Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/0ce9533f-f529-4c07-b641-6bf0883a0643 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/IO/PooledMemoryStream.cs | 6 +++++- src/SharpCompress/packages.lock.json | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/SharpCompress/IO/PooledMemoryStream.cs b/src/SharpCompress/IO/PooledMemoryStream.cs index 8111945e..3116386f 100644 --- a/src/SharpCompress/IO/PooledMemoryStream.cs +++ b/src/SharpCompress/IO/PooledMemoryStream.cs @@ -112,7 +112,10 @@ public sealed class PooledMemoryStream : MemoryStream } } - public override void Flush() { } + public override void Flush() + { + EnsureNotClosed(); + } public override Task FlushAsync(CancellationToken cancellationToken) { @@ -121,6 +124,7 @@ public sealed class PooledMemoryStream : MemoryStream return Task.FromCanceled(cancellationToken); } + EnsureNotClosed(); return Task.CompletedTask; } diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e82196f8..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.26, )", - "resolved": "8.0.26", - "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From f8485b0e8c15a13067f03f672ba5bba80a0b5e7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:33:40 +0000 Subject: [PATCH 37/74] Compute CRC without GetBuffer() in SevenZipWriter to avoid allocation Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/77f37cc4-3538-4df1-a816-aa7391065878 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Writers/SevenZip/SevenZipWriter.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs index 501959ee..7e2a058e 100644 --- a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs +++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.cs @@ -220,12 +220,10 @@ public partial class SevenZipWriter : AbstractWriter encodedHeaderStream.Position = 0; encodedHeaderStream.CopyTo(output); - // Compute CRC of the encoded header - var headerCrc = Crc32Stream.Compute( - Crc32Stream.DEFAULT_POLYNOMIAL, - Crc32Stream.DEFAULT_SEED, - encodedHeaderStream.GetBuffer().AsSpan(0, (int)encodedHeaderStream.Length) - ); + // Compute CRC of the encoded header without allocating a contiguous buffer + var encodedHeaderCrcSink = new Crc32Stream(Stream.Null); + encodedHeaderStream.WriteTo(encodedHeaderCrcSink); + var headerCrc = encodedHeaderCrcSink.Crc; // Back-patch signature header var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize); @@ -251,12 +249,10 @@ public partial class SevenZipWriter : AbstractWriter rawHeaderStream.Position = 0; rawHeaderStream.CopyTo(output); - // Compute CRC of the raw header - var headerCrc = Crc32Stream.Compute( - Crc32Stream.DEFAULT_POLYNOMIAL, - Crc32Stream.DEFAULT_SEED, - rawHeaderStream.GetBuffer().AsSpan(0, (int)rawHeaderStream.Length) - ); + // Compute CRC of the raw header without allocating a contiguous buffer + var rawHeaderCrcSink = new Crc32Stream(Stream.Null); + rawHeaderStream.WriteTo(rawHeaderCrcSink); + var headerCrc = rawHeaderCrcSink.Crc; // Back-patch signature header var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize); From 37c0f5aec575f7dcdc06e76f75e9ca9707bbf0ee Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:32:36 +0900 Subject: [PATCH 38/74] fix: Change LeaveStreamOpen default from true to false As of v0.21, the library is documented to close wrapped streams by default. However, the ReaderOptions.LeaveStreamOpen property defaulted to true, which contradicted the documented behavior and caused file locks when deleting archives after extraction. Changes: - Set LeaveStreamOpen = false as the default (consistent with v0.21 design) - Updated ReaderOptions.cs with comprehensive documentation explaining: * File-based vs caller-provided stream handling * When to use LeaveStreamOpen = true * Usage examples for both scenarios - Updated AGENTS.md Stream Handling Rules section with: * Clear explanation of default disposal behavior * Overload differences (file-based vs stream-based) * Guidance on using ForExternalStream preset This ensures the implementation matches the documented behavior and prevents unexpected file locking issues during archive deletion. Fixes: File deletion failures after archive extraction --- AGENTS.md | 11 +++++--- src/SharpCompress/Readers/ReaderOptions.cs | 31 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 32bbb942..3c960eea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,10 +126,15 @@ SharpCompress supports multiple archive and compression formats: - See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix ### Stream Handling Rules -- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default -- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal +- **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) + - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically + - Stream-based overloads (e.g., `OpenArchive(Stream stream)`) use `ReaderOptions.ForExternalStream` by default, which sets `LeaveStreamOpen = true` +- **For caller-provided streams**: Use `ReaderOptions` with `LeaveStreamOpen = true` or the `ForExternalStream` preset to prevent automatic disposal + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` +- **For file paths**: SharpCompress manages the stream lifecycle; no manual disposal needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal -- Always dispose of readers, writers, and archives in `using` blocks +- Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs ### Async/Await Patterns diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 6a063611..ed496101 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -24,9 +24,36 @@ namespace SharpCompress.Readers; public sealed record ReaderOptions : IReaderOptions { /// - /// SharpCompress will keep the supplied streams open. Default is true. + /// SharpCompress will keep the supplied streams open. + /// Default is false as of v0.21: streams are closed when the archive/reader is disposed. + /// Set to true when passing caller-owned streams that should not be disposed. /// - public bool LeaveStreamOpen { get; init; } = true; + /// + /// + /// Default behavior (LeaveStreamOpen = false): + /// When you open an archive from a file path (e.g., GZipArchive.OpenArchive(filePath)), + /// SharpCompress manages the stream lifetime and closes it on Dispose. + /// + /// + /// Caller-provided streams (LeaveStreamOpen = true): + /// When you pass a stream you created (FileStream, MemoryStream, NetworkStream, etc.), + /// set LeaveStreamOpen = true to prevent SharpCompress from disposing it. + /// Use preset for convenience. + /// + /// + /// Example: + /// + /// // 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); + /// + /// + /// + public bool LeaveStreamOpen { get; init; } = false; /// /// Encoding to use for archive entry names. From 23840b7a0ff0f7eb8048249c75fa038ab14490cd Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:15:56 +0900 Subject: [PATCH 39/74] fix(zip): stop overriding LeaveStreamOpen in OpenArchive --- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index db0b8489..aeb2686b 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -41,7 +41,7 @@ public partial class ZipArchive new SourceStream( fileInfo, i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + readerOptions ?? new ReaderOptions() ) ); } @@ -57,7 +57,7 @@ public partial class ZipArchive new SourceStream( files[0], i => i < files.Count ? files[i] : null, - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + readerOptions ?? new ReaderOptions() ) ); } From ea950457f51b4ac8f076bf4110f15f0e354f963b Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:16:16 +0900 Subject: [PATCH 40/74] fix(docs): clarify stream handling rules and ownership in AGENTS.md --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3c960eea..0d91e218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,11 +128,11 @@ SharpCompress supports multiple archive and compression formats: ### Stream Handling Rules - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically - - Stream-based overloads (e.g., `OpenArchive(Stream stream)`) use `ReaderOptions.ForExternalStream` by default, which sets `LeaveStreamOpen = true` -- **For caller-provided streams**: Use `ReaderOptions` with `LeaveStreamOpen = true` or the `ForExternalStream` preset to prevent automatic disposal - - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - - Or: `var options = ReaderOptions.ForExternalStream;` -- **For file paths**: SharpCompress manages the stream lifecycle; no manual disposal needed beyond the archive/reader itself + - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly + - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` + - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is neededbeyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From 8d193db721319d86c581c2927fa94743fb5dbcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:32:27 +0900 Subject: [PATCH 41/74] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/Readers/ReaderOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index ed496101..4f6788c3 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -24,7 +24,7 @@ namespace SharpCompress.Readers; public sealed record ReaderOptions : IReaderOptions { /// - /// SharpCompress will keep the supplied streams open. + /// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed. /// Default is false as of v0.21: streams are closed when the archive/reader is disposed. /// Set to true when passing caller-owned streams that should not be disposed. /// From 1bd7bf2169937f4c87632c2709a15bfb0004944d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:40:13 +0900 Subject: [PATCH 42/74] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0d91e218..0284f9e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,7 +132,7 @@ SharpCompress supports multiple archive and compression formats: - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - Or: `var options = ReaderOptions.ForExternalStream;` - - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is neededbeyond the archive/reader itself + - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From f5e27faefc8e3b237776ab6a8d1b75d0a7e2cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:48:34 +0900 Subject: [PATCH 43/74] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0284f9e4..ce2bdacc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,10 +129,10 @@ SharpCompress supports multiple archive and compression formats: - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly - - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - - Or: `var options = ReaderOptions.ForExternalStream;` - - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself +- **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` +- **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From 6a7e0b2f10e6c0c19eb9338926ec9a158e1be5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:09:10 +0900 Subject: [PATCH 44/74] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ce2bdacc..d8bc7c85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,8 @@ SharpCompress supports multiple archive and compression formats: ### Stream Handling Rules - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically + - File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader + - Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` From a351b56737939888c95d88efb2d2c065b35f1a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:26:31 +0900 Subject: [PATCH 45/74] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 7 ++++--- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 2 +- src/SharpCompress/Readers/ReaderOptions.cs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d8bc7c85..a47121a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,11 +126,12 @@ SharpCompress supports multiple archive and compression formats: - See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix ### Stream Handling Rules -- **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) +- **Disposal semantics**: The default `ReaderOptions.LeaveStreamOpen` value is `false`, but effective stream ownership depends on which API overload you call - File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader - Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics - - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly -- **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Several high-level overloads that accept a caller-provided `Stream` use external-stream semantics by default (for example, `ReaderFactory.OpenReader(Stream)` / `ArchiveFactory.OpenArchive(Stream)`), so the caller's stream is typically left open unless you opt into different ownership behavior + - Do **not** assume every stream-based overload behaves identically; some APIs require you to pass stream ownership options explicitly +- **For caller-provided streams**: When the overload accepts `ReaderOptions`, pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - Or: `var options = ReaderOptions.ForExternalStream;` - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index aeb2686b..9d46d370 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -57,7 +57,7 @@ public partial class ZipArchive new SourceStream( files[0], i => i < files.Count ? files[i] : null, - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 4f6788c3..cc90224a 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -25,7 +25,7 @@ public sealed record ReaderOptions : IReaderOptions { /// /// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed. - /// Default is false as of v0.21: streams are closed when the archive/reader 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. /// /// From d99b406e09d097ca82e701b1b39bab4860875d4d Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:02:30 +0900 Subject: [PATCH 46/74] fix: change default reader options in OpenArchive method for FileInfo --- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index 9d46d370..bdd18669 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -41,7 +41,7 @@ public partial class ZipArchive new SourceStream( fileInfo, i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } From 3af023a4a88818c7ebdf35d9d0b612c3a483b0d4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 21 Apr 2026 10:43:43 +0100 Subject: [PATCH 47/74] Use explicit helpers for leaving streams open or not. --- .../Archives/Rar/RarArchive.Factory.cs | 4 ++-- .../Archives/Zip/ZipArchive.Async.cs | 2 +- src/SharpCompress/Archives/Zip/ZipArchive.cs | 2 +- src/SharpCompress/Common/GZip/GZipVolume.cs | 2 +- src/SharpCompress/Common/Lzw/LzwVolume.cs | 2 +- src/SharpCompress/Common/Volume.cs | 4 ++-- src/SharpCompress/Factories/TarFactory.cs | 4 ++-- .../Readers/Ace/AceReader.Factory.cs | 6 +++--- src/SharpCompress/Readers/Arc/ArcReader.cs | 2 +- src/SharpCompress/Readers/Arj/ArjReader.cs | 4 ++-- .../Readers/GZip/GZipReader.Factory.cs | 2 +- .../Readers/Lzw/LzwReader.Factory.cs | 2 +- src/SharpCompress/Readers/Rar/RarReader.cs | 4 ++-- src/SharpCompress/Readers/ReaderOptions.cs | 15 +++++++-------- .../Readers/Tar/TarReader.Factory.cs | 4 ++-- src/SharpCompress/Readers/Zip/ZipReader.cs | 4 ++-- 16 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 432bf54f..df820e2f 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -51,7 +51,7 @@ public partial class RarArchive new SourceStream( fileInfo, i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } @@ -66,7 +66,7 @@ public partial class RarArchive } return new RarArchive( - new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions()) + new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) ); } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index 75255b25..97c412e6 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -45,7 +45,7 @@ public partial class ZipArchive s = new SourceStream( v[0].Stream, i => i < v.Length ? v[i].Stream : null, - new ReaderOptions() { LeaveStreamOpen = true } + ReaderOptions.ForExternalStream ); } else diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 20604814..3b5fd5b2 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -86,7 +86,7 @@ public partial class ZipArchive s = new SourceStream( v[0].Stream, i => i < v.Length ? v[i].Stream : null, - new ReaderOptions() { LeaveStreamOpen = true } + ReaderOptions.ForExternalStream ); } else diff --git a/src/SharpCompress/Common/GZip/GZipVolume.cs b/src/SharpCompress/Common/GZip/GZipVolume.cs index cb821acf..f26c4037 100644 --- a/src/SharpCompress/Common/GZip/GZipVolume.cs +++ b/src/SharpCompress/Common/GZip/GZipVolume.cs @@ -5,7 +5,7 @@ namespace SharpCompress.Common.GZip; public class GZipVolume : Volume { - public GZipVolume(Stream stream, ReaderOptions? options, int index) + public GZipVolume(Stream stream, ReaderOptions options, int index) : base(stream, options, index) { } public GZipVolume(FileInfo fileInfo, ReaderOptions options) diff --git a/src/SharpCompress/Common/Lzw/LzwVolume.cs b/src/SharpCompress/Common/Lzw/LzwVolume.cs index 7ded1d26..b771a19a 100644 --- a/src/SharpCompress/Common/Lzw/LzwVolume.cs +++ b/src/SharpCompress/Common/Lzw/LzwVolume.cs @@ -5,7 +5,7 @@ namespace SharpCompress.Common.Lzw; public class LzwVolume : Volume { - public LzwVolume(Stream stream, ReaderOptions? options, int index) + public LzwVolume(Stream stream, ReaderOptions options, int index) : base(stream, options, index) { } public LzwVolume(FileInfo fileInfo, ReaderOptions options) diff --git a/src/SharpCompress/Common/Volume.cs b/src/SharpCompress/Common/Volume.cs index d00ed4f0..ffca18c3 100644 --- a/src/SharpCompress/Common/Volume.cs +++ b/src/SharpCompress/Common/Volume.cs @@ -11,10 +11,10 @@ public abstract partial class Volume : IVolume, IAsyncDisposable private readonly Stream _baseStream; private readonly Stream _actualStream; - internal Volume(Stream stream, ReaderOptions? readerOptions, int index = 0) + internal Volume(Stream stream, ReaderOptions readerOptions, int index = 0) { Index = index; - ReaderOptions = readerOptions ?? new ReaderOptions(); + ReaderOptions = readerOptions; _baseStream = stream; // Only rewind if it's a buffered SharpCompressStream (not passthrough) diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 0a47d3b7..56d0a73b 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -317,7 +317,7 @@ public class TarFactory /// public IReader OpenReader(Stream stream, ReaderOptions? options) { - options ??= new ReaderOptions(); + options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) @@ -350,7 +350,7 @@ public class TarFactory ) { cancellationToken.ThrowIfCancellationRequested(); - options ??= new ReaderOptions(); + options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) diff --git a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs index a4ae43bb..5977e8de 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs @@ -20,7 +20,7 @@ public partial class AceReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new SingleVolumeAceReader(stream, readerOptions ?? new ReaderOptions()); + return new SingleVolumeAceReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } /// @@ -32,7 +32,7 @@ public partial class AceReader public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? new ReaderOptions()); + return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( @@ -62,7 +62,7 @@ public partial class AceReader ) { streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? new ReaderOptions()); + return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs index 376d598e..c1b53366 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -25,7 +25,7 @@ public partial class ArcReader : AbstractReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new ArcReader(stream, readerOptions ?? new ReaderOptions()); + return new ArcReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } protected override IEnumerable GetEntries(Stream stream) diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs index d5a0ae74..a0c4899c 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -32,7 +32,7 @@ public abstract partial class ArjReader : AbstractReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new SingleVolumeArjReader(stream, readerOptions ?? new ReaderOptions()); + return new SingleVolumeArjReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } /// @@ -44,7 +44,7 @@ public abstract partial class ArjReader : AbstractReader public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { streams.NotNull(nameof(streams)); - return new MultiVolumeArjReader(streams, options ?? new ReaderOptions()); + return new MultiVolumeArjReader(streams, options ?? ReaderOptions.ForExternalStream); } protected abstract void ValidateArchive(ArjVolume archive); diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs index 8baf715d..bd593f20 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs @@ -56,6 +56,6 @@ public partial class GZipReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new GZipReader(stream, readerOptions ?? new ReaderOptions()); + return new GZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs index b62e7b5f..e2166e37 100644 --- a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs @@ -56,6 +56,6 @@ public partial class LzwReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new LzwReader(stream, readerOptions ?? new ReaderOptions()); + return new LzwReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index a6a8ede7..81a5457c 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -72,7 +72,7 @@ public abstract partial class RarReader : AbstractReader @@ -84,7 +84,7 @@ public abstract partial class RarReader : AbstractReader streams, ReaderOptions? options = null) { streams.NotNull(nameof(streams)); - return new MultiVolumeRarReader(streams, options ?? new ReaderOptions()); + return new MultiVolumeRarReader(streams, options ?? ReaderOptions.ForExternalStream); } protected override IEnumerable GetEntries(Stream stream) diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index cc90224a..86d05f42 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -151,35 +151,34 @@ public sealed record ReaderOptions : IReaderOptions /// /// Gets ReaderOptions configured for caller-provided streams. /// - public static ReaderOptions ForExternalStream => new() { LeaveStreamOpen = true }; + internal static ReaderOptions Default => new(); + + public static ReaderOptions ForExternalStream => Default with { LeaveStreamOpen = true }; /// /// Gets ReaderOptions configured for file-based overloads that open their own stream. /// - public static ReaderOptions ForFilePath => new() { LeaveStreamOpen = false }; + public static ReaderOptions ForFilePath => Default; /// /// Creates ReaderOptions for reading encrypted archives. /// /// The password for encrypted archives. public static ReaderOptions ForEncryptedArchive(string? password = null) => - new ReaderOptions().WithPassword(password); + Default.WithPassword(password); /// /// Creates ReaderOptions for archives with custom character encoding. /// /// The encoding for archive entry names. public static ReaderOptions ForEncoding(IArchiveEncoding encoding) => - new ReaderOptions().WithArchiveEncoding(encoding); + Default.WithArchiveEncoding(encoding); /// /// Creates ReaderOptions for self-extracting archives that require header search. /// 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: diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index e6e5a703..aae20bb4 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -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( @@ -171,7 +171,7 @@ public partial class TarReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - readerOptions ??= new ReaderOptions(); + readerOptions ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( stream, bufferSize: Math.Max( diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 91dff5bf..e2019de6 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -48,7 +48,7 @@ public partial class ZipReader : AbstractReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { stream.NotNull(nameof(stream)); - return new ZipReader(stream, readerOptions ?? new ReaderOptions()); + return new ZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } public static IReader OpenReader( @@ -58,7 +58,7 @@ public partial class ZipReader : AbstractReader ) { stream.NotNull(nameof(stream)); - return new ZipReader(stream, options ?? new ReaderOptions(), entries); + return new ZipReader(stream, options ?? ReaderOptions.ForExternalStream, entries); } #endregion Open From 4f1f32b508e76cbd6ca9310615a9754830102641 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 21 Apr 2026 10:55:59 +0100 Subject: [PATCH 48/74] Fix usages of ReaderOptions --- .../Archives/ArchiveFactory.Async.cs | 6 +- src/SharpCompress/Archives/ArchiveFactory.cs | 2 +- .../Readers/ReaderFactory.Async.cs | 6 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- .../Ace/AceReaderAsyncTests.cs | 7 ++- .../Arj/ArjReaderAsyncTests.cs | 7 ++- .../CompressionProviderTests.cs | 25 +++++--- .../SharpCompress.Test/Lzw/LzwReaderTests.cs | 5 +- .../OptionsUsabilityTests.cs | 12 ++-- .../SharpCompress.Test/ProgressReportTests.cs | 10 +-- .../Rar/RarArchiveAsyncTests.cs | 25 ++++++-- .../SharpCompress.Test/Rar/RarArchiveTests.cs | 27 ++++++-- .../Rar/RarHeaderFactoryTest.cs | 5 +- .../Rar/RarReaderAsyncTests.cs | 32 +++++++--- .../SharpCompress.Test/Rar/RarReaderTests.cs | 42 ++++++++++--- tests/SharpCompress.Test/ReaderTests.cs | 9 ++- .../SevenZip/SevenZipArchiveTests.cs | 34 ++++++++-- .../Streams/DisposalTests.cs | 5 +- .../Tar/TarArchiveAsyncTests.cs | 6 +- .../SharpCompress.Test/Tar/TarArchiveTests.cs | 2 +- tests/SharpCompress.Test/TestBase.cs | 2 +- tests/SharpCompress.Test/WriterTests.cs | 4 +- .../SharpCompress.Test/Zip/Zip64AsyncTests.cs | 5 +- tests/SharpCompress.Test/Zip/Zip64Tests.cs | 10 ++- .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 62 +++++++++++++++---- .../Zip/ZipReaderAsyncTests.cs | 22 +++++-- .../SharpCompress.Test/Zip/ZipReaderTests.cs | 45 ++++++++++++-- .../Zip/ZipShortReadTests.cs | 5 +- 28 files changed, 328 insertions(+), 96 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 03595554..fde9a93d 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -33,7 +33,11 @@ public static partial class ArchiveFactory ) { filePath.NotNullOrEmpty(nameof(filePath)); - return OpenAsyncArchive(new FileInfo(filePath), options, cancellationToken); + return OpenAsyncArchive( + new FileInfo(filePath), + options ?? ReaderOptions.ForFilePath, + cancellationToken + ); } public static async ValueTask OpenAsyncArchive( diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index e4973f0f..ec044e0e 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -37,7 +37,7 @@ public static partial class ArchiveFactory public static IArchive OpenArchive(string filePath, ReaderOptions? options = null) { filePath.NotNullOrEmpty(nameof(filePath)); - return OpenArchive(new FileInfo(filePath), options); + return OpenArchive(new FileInfo(filePath), options ?? ReaderOptions.ForFilePath); } public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 0119bd52..80388577 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -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 + ); } /// diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index a1ff1775..2a84913c 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -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) diff --git a/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs b/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs index 62d10265..332ca652 100644 --- a/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs @@ -73,7 +73,7 @@ public class AceReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(testArchive); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions() + ReaderOptions.ForExternalStream ); while (await reader.MoveToNextEntryAsync()) { @@ -95,7 +95,10 @@ public class AceReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(testArchive); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (await reader.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs index 17dfa9cf..c2418e4b 100644 --- a/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs @@ -94,7 +94,7 @@ public class ArjReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(testArchive); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions() + ReaderOptions.ForExternalStream ); while (await reader.MoveToNextEntryAsync()) { @@ -119,7 +119,10 @@ public class ArjReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(testArchive); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions() { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (await reader.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/CompressionProviderTests.cs b/tests/SharpCompress.Test/CompressionProviderTests.cs index 8578de9d..31c04d3a 100644 --- a/tests/SharpCompress.Test/CompressionProviderTests.cs +++ b/tests/SharpCompress.Test/CompressionProviderTests.cs @@ -332,7 +332,10 @@ public class CompressionProviderTests ); compressedStream.Position = 0; - var readerOptions = new ReaderOptions { ArchiveEncoding = archiveEncoding }; + var readerOptions = ReaderOptions.ForExternalStream with + { + ArchiveEncoding = archiveEncoding, + }; var context = CompressionContext.FromStream(compressedStream) with { ReaderOptions = readerOptions, @@ -433,7 +436,7 @@ public class CompressionProviderTests archiveStream.Position = 0; var customProvider = new GZipCompressionProvider(); var registry = CompressionProviderRegistry.Default.With(customProvider); - var readOptions = new ReaderOptions { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; using var reader = TarReader.OpenReader(archiveStream, readOptions); reader.MoveToNextEntry().Should().BeTrue(); @@ -462,7 +465,7 @@ public class CompressionProviderTests archiveStream.Position = 0; var registry = CompressionProviderRegistry.Default.With(new ContextRequiredGZipProvider()); - var readOptions = new ReaderOptions { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; using var reader = TarReader.OpenReader(archiveStream, readOptions); reader.MoveToNextEntry().Should().BeTrue(); @@ -504,7 +507,11 @@ public class CompressionProviderTests var customProvider = new DeflateCompressionProvider(); var registry = CompressionProviderRegistry.Default.With(customProvider); - var original = new ReaderOptions { Providers = registry, LeaveStreamOpen = false }; + var original = ReaderOptions.ForExternalStream with + { + Providers = registry, + LeaveStreamOpen = false, + }; // Clone using 'with' expression var clone = original with @@ -533,7 +540,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var readOptions = new ReaderOptions { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; archiveStream.Position = 0; using var archive = TarArchive.OpenArchive(archiveStream, readOptions); @@ -562,7 +569,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var readOptions = new ReaderOptions { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; archiveStream.Position = 0; await using var archive = await TarArchive.OpenAsyncArchive(archiveStream, readOptions); @@ -600,7 +607,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new DeflateCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var options = new ReaderOptions { Providers = registry }; + var options = ReaderOptions.ForExternalStream with { Providers = registry }; zipStream.Position = 0; await using var reader = await ReaderFactory.OpenAsyncReader(zipStream, options); @@ -618,7 +625,7 @@ public class CompressionProviderTests var archivePath = Path.Combine(TestBase.TEST_ARCHIVES_PATH, "Tar.tar.Z"); var trackingProvider = new TrackingCompressionProvider(new LzwCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var options = new ReaderOptions { Providers = registry }; + var options = ReaderOptions.ForExternalStream with { Providers = registry }; using var stream = File.OpenRead(archivePath); using var reader = ReaderFactory.OpenReader(stream, options); @@ -809,7 +816,7 @@ public class CompressionProviderTests // Read back using internal provider (should be compatible) archiveStream.Position = 0; - var readOptions = new ReaderOptions(); + var readOptions = ReaderOptions.ForExternalStream; using var reader = TarReader.OpenReader(archiveStream, readOptions); reader.MoveToNextEntry().Should().BeTrue(); using var entryStream = reader.OpenEntryStream(); diff --git a/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs index 8e94481f..7a74967e 100644 --- a/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs +++ b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs @@ -37,7 +37,10 @@ public class LzwReaderTests : ReaderTests using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.Z")); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LeaveStreamOpen = false } + ReaderOptions.ForExternalStream with + { + LeaveStreamOpen = false, + } ); // Should detect as Tar archive with Lzw compression diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs index 70b73cb7..f9f05aed 100644 --- a/tests/SharpCompress.Test/OptionsUsabilityTests.cs +++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs @@ -160,8 +160,8 @@ public class OptionsUsabilityTests : TestBase [Fact] public void ReaderOptions_Fluent_Methods_Modify_Correctly() { - var options = new ReaderOptions() - .WithLeaveStreamOpen(false) + var options = ReaderOptions + .ForExternalStream.WithLeaveStreamOpen(false) .WithPassword("secret") .WithLookForHeader(true) .WithBufferSize(65536); @@ -176,15 +176,15 @@ public class OptionsUsabilityTests : TestBase public void ReaderOptions_Fluent_And_Initializer_Equivalent() { // Fluent approach - var fluentApproach = new ReaderOptions() - .WithLeaveStreamOpen(false) + var fluentApproach = ReaderOptions + .ForExternalStream.WithLeaveStreamOpen(false) .WithPassword("secret") .WithLookForHeader(true) .WithBufferSize(65536) .WithDisableCheckIncomplete(true); - // Object initializer approach - var initializerApproach = new ReaderOptions + // Preset + with-expression approach + var initializerApproach = ReaderOptions.ForExternalStream with { LeaveStreamOpen = false, Password = "secret", diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index a26e96cd..b1d896c9 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -108,7 +108,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = new ReaderOptions { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -234,7 +234,7 @@ public class ProgressReportTests : TestBase // Read without progress archiveStream.Position = 0; - var readerOptions = new ReaderOptions(); + var readerOptions = ReaderOptions.ForExternalStream; Assert.Null(readerOptions.Progress); using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) @@ -324,7 +324,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = new ReaderOptions { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -445,7 +445,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = new ReaderOptions { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -538,7 +538,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = new ReaderOptions { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; await using ( var reader = await ReaderFactory.OpenAsyncReader( diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 3f48b976..8051875a 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -71,7 +71,11 @@ public class RarArchiveAsyncTests : ArchiveTests await using ( var archive = await RarArchive.OpenAsyncArchive( stream, - new ReaderOptions { Password = password, LeaveStreamOpen = true } + ReaderOptions.ForExternalStream with + { + Password = password, + LeaveStreamOpen = true, + } ) ) { @@ -98,7 +102,11 @@ public class RarArchiveAsyncTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, archiveName), - new ReaderOptions { Password = password, LeaveStreamOpen = true } + ReaderOptions.ForFilePath with + { + Password = password, + LeaveStreamOpen = true, + } ) ) { @@ -144,7 +152,13 @@ public class RarArchiveAsyncTests : ArchiveTests { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg")); using ( - var archive = RarArchive.OpenArchive(stream, new ReaderOptions { LookForHeader = true }) + var archive = RarArchive.OpenArchive( + stream, + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } + ) ) { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) @@ -316,7 +330,10 @@ public class RarArchiveAsyncTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ) ) { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index fdb8d56b..b80cfcfa 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -66,7 +66,11 @@ public class RarArchiveTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( stream, - new ReaderOptions { Password = password, LeaveStreamOpen = true } + ReaderOptions.ForExternalStream with + { + Password = password, + LeaveStreamOpen = true, + } ) ) { @@ -93,7 +97,11 @@ public class RarArchiveTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, archiveName), - new ReaderOptions { Password = password, LeaveStreamOpen = true } + ReaderOptions.ForFilePath with + { + Password = password, + LeaveStreamOpen = true, + } ) ) { @@ -136,7 +144,13 @@ public class RarArchiveTests : ArchiveTests { using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg")); using ( - var archive = RarArchive.OpenArchive(stream, new ReaderOptions { LookForHeader = true }) + var archive = RarArchive.OpenArchive( + stream, + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } + ) ) { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) @@ -302,7 +316,10 @@ public class RarArchiveTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ) ) { @@ -751,7 +768,7 @@ public class RarArchiveTests : ArchiveTests public void Rar_MalformedArchive_NoInfiniteLoop() { var testFile = "Rar.malformed_512byte.rar"; - var readerOptions = new ReaderOptions { LookForHeader = true }; + var readerOptions = ReaderOptions.ForExternalStream with { LookForHeader = true }; // This should throw InvalidOperationException, not hang in an infinite loop var exception = Assert.Throws(() => diff --git a/tests/SharpCompress.Test/Rar/RarHeaderFactoryTest.cs b/tests/SharpCompress.Test/Rar/RarHeaderFactoryTest.cs index 70015fcf..6fd42ba9 100644 --- a/tests/SharpCompress.Test/Rar/RarHeaderFactoryTest.cs +++ b/tests/SharpCompress.Test/Rar/RarHeaderFactoryTest.cs @@ -16,7 +16,10 @@ public class RarHeaderFactoryTest : TestBase public RarHeaderFactoryTest() => _rarHeaderFactory = new RarHeaderFactory( StreamingMode.Seekable, - new ReaderOptions { LeaveStreamOpen = true } + ReaderOptions.ForExternalStream with + { + LeaveStreamOpen = true, + } ); [Fact] diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index d6d82e26..188784c5 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -73,7 +73,10 @@ public class RarReaderAsyncTests : ReaderTests archives .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)) .Select(p => File.OpenRead(p)), - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { @@ -187,7 +190,10 @@ public class RarReaderAsyncTests : ReaderTests await ReadAsync( testArchive, CompressionType.Rar, - new ReaderOptions { Password = password } + ReaderOptions.ForFilePath with + { + Password = password, + } ); [Fact] @@ -248,7 +254,10 @@ public class RarReaderAsyncTests : ReaderTests await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ) ) { @@ -271,7 +280,10 @@ public class RarReaderAsyncTests : ReaderTests using ( IReader baseReader = RarReader.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ) ) { @@ -314,7 +326,10 @@ public class RarReaderAsyncTests : ReaderTests using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (await reader.MoveToNextEntryAsync()) { @@ -337,7 +352,10 @@ public class RarReaderAsyncTests : ReaderTests using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (await reader.MoveToNextEntryAsync()) { @@ -359,7 +377,7 @@ public class RarReaderAsyncTests : ReaderTests using Stream stream = File.OpenRead(testArchive); await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForExternalStream ); while (await reader.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/Rar/RarReaderTests.cs b/tests/SharpCompress.Test/Rar/RarReaderTests.cs index 008db7fd..a037f9bb 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderTests.cs @@ -71,7 +71,10 @@ public class RarReaderTests : ReaderTests archives .Select(s => Path.Combine(TEST_ARCHIVES_PATH, s)) .Select(p => File.OpenRead(p)), - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { @@ -173,7 +176,14 @@ public class RarReaderTests : ReaderTests public void Rar5_Encrypted_Reader() => ReadRar("Rar5.encrypted_filesOnly.rar", "test"); private void ReadRar(string testArchive, string password) => - Read(testArchive, CompressionType.Rar, new ReaderOptions { Password = password }); + Read( + testArchive, + CompressionType.Rar, + ReaderOptions.ForFilePath with + { + Password = password, + } + ); [Fact] public void Rar_Entry_Stream() => DoRar_Entry_Stream("Rar.rar"); @@ -222,7 +232,10 @@ public class RarReaderTests : ReaderTests using ( var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ) ) { @@ -243,7 +256,13 @@ public class RarReaderTests : ReaderTests { using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"))) using ( - var reader = RarReader.OpenReader(stream, new ReaderOptions { LookForHeader = true }) + var reader = RarReader.OpenReader( + stream, + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } + ) ) { while (reader.MoveToNextEntry()) @@ -278,7 +297,10 @@ public class RarReaderTests : ReaderTests using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (reader.MoveToNextEntry()) { @@ -301,7 +323,10 @@ public class RarReaderTests : ReaderTests using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename)); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (reader.MoveToNextEntry()) { @@ -321,7 +346,10 @@ public class RarReaderTests : ReaderTests ); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (reader.MoveToNextEntry()) { diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index e4fedc58..c30b58e1 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -32,7 +32,7 @@ public abstract class ReaderTests : TestBase ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); - options ??= new ReaderOptions { BufferSize = 0x20000 }; + options ??= ReaderOptions.ForFilePath with { BufferSize = 0x20000 }; var optionsWithStreamOpen = options with { LeaveStreamOpen = true }; readImpl(testArchive, optionsWithStreamOpen); @@ -128,7 +128,7 @@ public abstract class ReaderTests : TestBase { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); - options ??= new ReaderOptions() { BufferSize = 0x20000 }; + options ??= ReaderOptions.ForExternalStream with { BufferSize = 0x20000 }; var optionsWithStreamOpen = options with { LeaveStreamOpen = true }; await ReadImplAsync( @@ -215,7 +215,10 @@ public abstract class ReaderTests : TestBase using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, fileName)); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LookForHeader = true } + ReaderOptions.ForExternalStream with + { + LookForHeader = true, + } ); while (reader.MoveToNextEntry()) diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index ccb3596e..c06fee78 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -27,16 +27,28 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMAAES_StreamRead() => - ArchiveStreamRead("7Zip.LZMA.Aes.7z", new ReaderOptions { Password = "testpassword" }); + ArchiveStreamRead( + "7Zip.LZMA.Aes.7z", + ReaderOptions.ForExternalStream with + { + Password = "testpassword", + } + ); [Fact] public void SevenZipArchive_LZMAAES_PathRead() => - ArchiveFileRead("7Zip.LZMA.Aes.7z", new ReaderOptions { Password = "testpassword" }); + ArchiveFileRead( + "7Zip.LZMA.Aes.7z", + ReaderOptions.ForFilePath with + { + Password = "testpassword", + } + ); [Fact] public void SevenZipArchive_LZMAAES_NoPasswordExceptionTest() => Assert.Throws(() => - ArchiveFileRead("7Zip.LZMA.Aes.7z", new ReaderOptions { Password = null }) + ArchiveFileRead("7Zip.LZMA.Aes.7z", ReaderOptions.ForFilePath with { Password = null }) ); //was failing with ArgumentNullException not CryptographicException like rar [Fact] @@ -65,11 +77,23 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMA2AES_StreamRead() => - ArchiveStreamRead("7Zip.LZMA2.Aes.7z", new ReaderOptions { Password = "testpassword" }); + ArchiveStreamRead( + "7Zip.LZMA2.Aes.7z", + ReaderOptions.ForExternalStream with + { + Password = "testpassword", + } + ); [Fact] public void SevenZipArchive_LZMA2AES_PathRead() => - ArchiveFileRead("7Zip.LZMA2.Aes.7z", new ReaderOptions { Password = "testpassword" }); + ArchiveFileRead( + "7Zip.LZMA2.Aes.7z", + ReaderOptions.ForFilePath with + { + Password = "testpassword", + } + ); [Fact] public void SevenZipArchive_BZip2_StreamRead() => ArchiveStreamRead("7Zip.BZip2.7z"); diff --git a/tests/SharpCompress.Test/Streams/DisposalTests.cs b/tests/SharpCompress.Test/Streams/DisposalTests.cs index 75d6c69e..b80d6943 100644 --- a/tests/SharpCompress.Test/Streams/DisposalTests.cs +++ b/tests/SharpCompress.Test/Streams/DisposalTests.cs @@ -76,7 +76,10 @@ public class DisposalTests new SourceStream( stream, i => null, - new ReaderOptions { LeaveStreamOpen = leaveOpen } + ReaderOptions.ForExternalStream with + { + LeaveStreamOpen = leaveOpen, + } ) ); } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index b9b7d6eb..923d5bf2 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -57,7 +57,7 @@ public class TarArchiveAsyncTests : ArchiveTests await using ( var archive2 = await TarArchive.OpenAsyncArchive( new AsyncOnlyStream(File.OpenRead(unmodified)), - new ReaderOptions() { LeaveStreamOpen = false } + ReaderOptions.ForExternalStream.WithLeaveStreamOpen(false) ) ) { @@ -115,7 +115,7 @@ public class TarArchiveAsyncTests : ArchiveTests await using ( var archive2 = await TarArchive.OpenAsyncArchive( new AsyncOnlyStream(File.OpenRead(unmodified)), - new ReaderOptions() { LeaveStreamOpen = false } + ReaderOptions.ForExternalStream.WithLeaveStreamOpen(false) ) ) { @@ -213,7 +213,7 @@ public class TarArchiveAsyncTests : ArchiveTests } using (var inputMemory = new MemoryStream(mstm.ToArray())) { - var tropt = new ReaderOptions { ArchiveEncoding = enc }; + var tropt = ReaderOptions.ForExternalStream with { ArchiveEncoding = enc }; await using ( var tr = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(inputMemory), diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 086ca02c..8db1ca16 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -262,7 +262,7 @@ public class TarArchiveTests : ArchiveTests } using (var inputMemory = new MemoryStream(mstm.ToArray())) { - var tropt = new ReaderOptions { ArchiveEncoding = enc }; + var tropt = ReaderOptions.ForExternalStream with { ArchiveEncoding = enc }; using (var tr = TarReader.OpenReader(inputMemory, tropt)) { while (tr.MoveToNextEntry()) diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index 1b4656d6..e39fbe8c 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -255,7 +255,7 @@ public class TestBase : IAsyncDisposable protected void CompareArchivesByPath(string file1, string file2, Encoding? encoding = null) { - var readerOptions = new ReaderOptions { LeaveStreamOpen = false }; + var readerOptions = ReaderOptions.ForExternalStream with { LeaveStreamOpen = false }; readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; //don't compare the order. OS X reads files from the file system in a different order therefore makes the archive ordering different diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 25f6e406..1142f7ac 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -39,7 +39,7 @@ public class WriterTests : TestBase using (Stream stream = File.OpenRead(Path.Combine(SCRATCH2_FILES_PATH, archive))) { - var readerOptions = new ReaderOptions(); + var readerOptions = ReaderOptions.ForExternalStream; readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; @@ -90,7 +90,7 @@ public class WriterTests : TestBase using (Stream stream = File.OpenRead(Path.Combine(SCRATCH2_FILES_PATH, archive))) { - var readerOptions = new ReaderOptions(); + var readerOptions = ReaderOptions.ForExternalStream; readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; diff --git a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs index 2d7a0c7b..32e666b4 100644 --- a/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs @@ -201,7 +201,10 @@ public class Zip64AsyncTests : WriterTests { await using var rd = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(fs), - new ReaderOptions { LookForHeader = false } + ReaderOptions.ForExternalStream with + { + LookForHeader = false, + } ); while (await rd.MoveToNextEntryAsync()) { diff --git a/tests/SharpCompress.Test/Zip/Zip64Tests.cs b/tests/SharpCompress.Test/Zip/Zip64Tests.cs index 68d27625..e1bac5bd 100644 --- a/tests/SharpCompress.Test/Zip/Zip64Tests.cs +++ b/tests/SharpCompress.Test/Zip/Zip64Tests.cs @@ -182,7 +182,15 @@ public class Zip64Tests : WriterTests long size = 0; IEntry? prev = null; using (var fs = File.OpenRead(filename)) - using (var rd = ZipReader.OpenReader(fs, new ReaderOptions { LookForHeader = false })) + using ( + var rd = ZipReader.OpenReader( + fs, + ReaderOptions.ForExternalStream with + { + LookForHeader = false, + } + ) + ) { while (rd.MoveToNextEntry()) { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 92073da4..d50a05e1 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -465,7 +465,10 @@ public class ZipArchiveTests : ArchiveTests using ( var reader = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ) ) { @@ -482,7 +485,10 @@ public class ZipArchiveTests : ArchiveTests { using var reader = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES2.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); foreach (var entry in reader.Entries.Where(x => !x.IsDirectory)) { @@ -499,7 +505,10 @@ public class ZipArchiveTests : ArchiveTests // Test that WinZip AES encrypted entries correctly report their compression type using var deflateArchive = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); foreach (var entry in deflateArchive.Entries.Where(x => !x.IsDirectory)) { @@ -509,7 +518,10 @@ public class ZipArchiveTests : ArchiveTests using var lzmaArchive = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); foreach (var entry in lzmaArchive.Entries.Where(x => !x.IsDirectory)) { @@ -523,7 +535,10 @@ public class ZipArchiveTests : ArchiveTests { using var archive = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); VerifyMixedZstandardArchive(archive); @@ -535,7 +550,13 @@ public class ZipArchiveTests : ArchiveTests using var stream = File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip") ); - using var archive = ZipArchive.OpenArchive(stream, new ReaderOptions { Password = "test" }); + using var archive = ZipArchive.OpenArchive( + stream, + ReaderOptions.ForExternalStream with + { + Password = "test", + } + ); VerifyMixedZstandardArchive(archive); } @@ -571,7 +592,10 @@ public class ZipArchiveTests : ArchiveTests // Test that Pkware encrypted entries correctly report their compression type using var deflateArchive = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.pkware.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); foreach (var entry in deflateArchive.Entries.Where(x => !x.IsDirectory)) { @@ -581,7 +605,10 @@ public class ZipArchiveTests : ArchiveTests using var bzip2Archive = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); foreach (var entry in bzip2Archive.Entries.Where(x => !x.IsDirectory)) { @@ -595,7 +622,10 @@ public class ZipArchiveTests : ArchiveTests { using var reader = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.zip64.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ); var isComplete = reader.IsComplete; Assert.Equal(1, reader.Volumes.Count()); @@ -611,7 +641,10 @@ public class ZipArchiveTests : ArchiveTests using ( var reader = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ) ) { @@ -673,7 +706,10 @@ public class ZipArchiveTests : ArchiveTests using var fileStream = File.OpenRead(zipFile); using var archive = ArchiveFactory.OpenArchive( fileStream, - new ReaderOptions { Password = "12345678" } + ReaderOptions.ForExternalStream with + { + Password = "12345678", + } ); var entries = archive.Entries.Where(entry => !entry.IsDirectory); foreach (var entry in entries) @@ -881,7 +917,7 @@ public class ZipArchiveTests : ArchiveTests { var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions + ReaderOptions.ForExternalStream with { ArchiveEncoding = new ArchiveEncoding { @@ -896,7 +932,7 @@ public class ZipArchiveTests : ArchiveTests { var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions + ReaderOptions.ForExternalStream with { ArchiveEncoding = new ArchiveEncoding { diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index a9525ede..4c7cd627 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -143,7 +143,10 @@ public class ZipReaderAsyncTests : ReaderTests using ( IReader baseReader = ZipReader.OpenReader( stream, - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { @@ -169,7 +172,7 @@ public class ZipReaderAsyncTests : ReaderTests await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(stream), - new ReaderOptions().WithLeaveStreamOpen(false) + ReaderOptions.ForExternalStream.WithLeaveStreamOpen(false) ) ) { @@ -215,7 +218,10 @@ public class ZipReaderAsyncTests : ReaderTests using ( IReader baseReader = ZipReader.OpenReader( stream, - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { @@ -244,7 +250,10 @@ public class ZipReaderAsyncTests : ReaderTests await using ( var reader = await ReaderFactory.OpenAsyncReader( stream, - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { @@ -272,7 +281,10 @@ public class ZipReaderAsyncTests : ReaderTests await using ( var reader = await ReaderFactory.OpenAsyncReader( stream, - new ReaderOptions { Password = "test" } + ReaderOptions.ForExternalStream with + { + Password = "test", + } ) ) { diff --git a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs index af4bd3ad..473c862e 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs @@ -129,7 +129,15 @@ public class ZipReaderTests : ReaderTests using ( Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip")) ) - using (var reader = ZipReader.OpenReader(stream, new ReaderOptions { Password = "test" })) + using ( + var reader = ZipReader.OpenReader( + stream, + ReaderOptions.ForExternalStream with + { + Password = "test", + } + ) + ) { while (reader.MoveToNextEntry()) { @@ -152,7 +160,7 @@ public class ZipReaderTests : ReaderTests using ( var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions().WithLeaveStreamOpen(false) + ReaderOptions.ForExternalStream.WithLeaveStreamOpen(false) ) ) { @@ -194,7 +202,13 @@ public class ZipReaderTests : ReaderTests ) ) using ( - var reader = ZipReader.OpenReader(stream, new ReaderOptions { Password = "test" }) + var reader = ZipReader.OpenReader( + stream, + ReaderOptions.ForExternalStream with + { + Password = "test", + } + ) ) { while (reader.MoveToNextEntry()) @@ -217,7 +231,15 @@ public class ZipReaderTests : ReaderTests Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip") ) ) - using (var reader = ZipReader.OpenReader(stream, new ReaderOptions { Password = "test" })) + using ( + var reader = ZipReader.OpenReader( + stream, + ReaderOptions.ForExternalStream with + { + Password = "test", + } + ) + ) { while (reader.MoveToNextEntry()) { @@ -236,7 +258,15 @@ public class ZipReaderTests : ReaderTests { var count = 0; using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip"))) - using (var reader = ZipReader.OpenReader(stream, new ReaderOptions { Password = "test" })) + using ( + var reader = ZipReader.OpenReader( + stream, + ReaderOptions.ForExternalStream with + { + Password = "test", + } + ) + ) { while (reader.MoveToNextEntry()) { @@ -402,7 +432,10 @@ public class ZipReaderTests : ReaderTests { using var reader = ReaderFactory.OpenReader( Path.Combine(TEST_ARCHIVES_PATH, "Zip.none.encrypted.zip"), - new ReaderOptions { Password = "test" } + ReaderOptions.ForFilePath with + { + Password = "test", + } ); reader.MoveToNextEntry(); Assert.Equal("first.txt", reader.Entry.Key); diff --git a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs index 34c2777b..f9f1d72a 100644 --- a/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipShortReadTests.cs @@ -99,7 +99,10 @@ public class ZipShortReadTests : ReaderTests var names = new List(); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions { LeaveStreamOpen = true } + ReaderOptions.ForExternalStream with + { + LeaveStreamOpen = true, + } ); while (reader.MoveToNextEntry()) From 1f699f35522e76f7fc1ab73f99130d7c36c2d677 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 21 Apr 2026 10:59:01 +0100 Subject: [PATCH 49/74] manual clean up --- src/SharpCompress/Archives/AbstractArchive.cs | 2 +- src/SharpCompress/packages.lock.json | 12 ++++++------ .../Benchmarks/TarBenchmarks.cs | 4 ++-- .../Benchmarks/ZipBenchmarks.cs | 4 ++-- .../SevenZip/SevenZipArchiveTests.cs | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 7d89067f..1e4b353b 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -40,7 +40,7 @@ public abstract partial class AbstractArchive : IArchive, IAsyn internal AbstractArchive(ArchiveType type) { Type = type; - ReaderOptions = new(); + ReaderOptions = ReaderOptions.Default; _lazyVolumes = new LazyReadOnlyCollection(Enumerable.Empty()); _lazyEntries = new LazyReadOnlyCollection(Enumerable.Empty()); _lazyVolumesAsync = new LazyAsyncReadOnlyCollection( diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 03c03a9a..e82196f8 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs index 67abff6a..634f3940 100644 --- a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs @@ -70,7 +70,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase using var stream = new MemoryStream(_tarBytes); using var archive = TarArchive.OpenArchive( stream, - new ReaderOptions().WithProviders( + ReaderOptions.ForExternalStream.WithProviders( CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider()) ) ); @@ -87,7 +87,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase using var stream = new MemoryStream(_tarBytes); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions().WithProviders( + ReaderOptions.ForExternalStream.WithProviders( CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider()) ) ); diff --git a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs index bd818853..c5b3e090 100644 --- a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs +++ b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs @@ -32,7 +32,7 @@ public class ZipBenchmarks : ArchiveBenchmarkBase using var stream = new MemoryStream(_archiveBytes); using var archive = ZipArchive.OpenArchive( stream, - new ReaderOptions().WithProviders( + ReaderOptions.ForExternalStream.WithProviders( CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider()) ) ); @@ -73,7 +73,7 @@ public class ZipBenchmarks : ArchiveBenchmarkBase using var stream = new MemoryStream(_archiveBytes); using var reader = ReaderFactory.OpenReader( stream, - new ReaderOptions().WithProviders( + ReaderOptions.ForExternalStream.WithProviders( CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider()) ) ); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index c06fee78..59c6b004 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -69,11 +69,11 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMA2_EXE_StreamRead() => - ArchiveStreamRead(new SevenZipFactory(), "7Zip.LZMA2.exe", new() { LookForHeader = true }); + ArchiveStreamRead(new SevenZipFactory(), "7Zip.LZMA2.exe", ReaderOptions.ForExternalStream.WithLookForHeader(true)); [Fact] public void SevenZipArchive_LZMA2_EXE_PathRead() => - ArchiveFileRead("7Zip.LZMA2.exe", new() { LookForHeader = true }, new SevenZipFactory()); + ArchiveFileRead("7Zip.LZMA2.exe", ReaderOptions.ForFilePath.WithLookForHeader(true), new SevenZipFactory()); [Fact] public void SevenZipArchive_LZMA2AES_StreamRead() => From 6a402a3b5053bea0274658d099708ab650801899 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 21 Apr 2026 11:04:14 +0100 Subject: [PATCH 50/74] use with methods --- .../Archives/Rar/FileInfoRarArchiveVolume.cs | 13 ++++++------- src/SharpCompress/Common/GZip/GZipVolume.cs | 2 +- src/SharpCompress/Common/Lzw/LzwVolume.cs | 2 +- src/SharpCompress/Readers/ReaderOptions.cs | 2 +- .../SharpCompress.Test/CompressionProviderTests.cs | 12 ++++++------ tests/SharpCompress.Test/ProgressReportTests.cs | 8 ++++---- tests/SharpCompress.Test/Rar/RarArchiveTests.cs | 2 +- tests/SharpCompress.Test/ReaderTests.cs | 12 ++++++------ .../SevenZip/SevenZipArchiveTests.cs | 14 +++++++++++--- .../SharpCompress.Test/Tar/TarArchiveAsyncTests.cs | 2 +- tests/SharpCompress.Test/Tar/TarArchiveTests.cs | 2 +- tests/SharpCompress.Test/TestBase.cs | 2 +- 12 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/SharpCompress/Archives/Rar/FileInfoRarArchiveVolume.cs b/src/SharpCompress/Archives/Rar/FileInfoRarArchiveVolume.cs index 788d1f39..54c99a93 100644 --- a/src/SharpCompress/Archives/Rar/FileInfoRarArchiveVolume.cs +++ b/src/SharpCompress/Archives/Rar/FileInfoRarArchiveVolume.cs @@ -15,18 +15,17 @@ namespace SharpCompress.Archives.Rar; internal class FileInfoRarArchiveVolume : RarVolume { internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index) - : base(StreamingMode.Seekable, fileInfo.OpenRead(), FixOptions(options), index) + : base( + StreamingMode.Seekable, + fileInfo.OpenRead(), + options.WithLeaveStreamOpen(false), + index + ) { FileInfo = fileInfo; FileParts = GetVolumeFileParts().ToArray().ToReadOnly(); } - private static ReaderOptions FixOptions(ReaderOptions options) - { - //make sure we're closing streams with fileinfo - return options with { LeaveStreamOpen = false }; - } - internal ReadOnlyCollection FileParts { get; } internal FileInfo FileInfo { get; } diff --git a/src/SharpCompress/Common/GZip/GZipVolume.cs b/src/SharpCompress/Common/GZip/GZipVolume.cs index f26c4037..b753c3ad 100644 --- a/src/SharpCompress/Common/GZip/GZipVolume.cs +++ b/src/SharpCompress/Common/GZip/GZipVolume.cs @@ -9,7 +9,7 @@ public class GZipVolume : Volume : base(stream, options, index) { } public GZipVolume(FileInfo fileInfo, ReaderOptions options) - : base(fileInfo.OpenRead(), options with { LeaveStreamOpen = false }) { } + : base(fileInfo.OpenRead(), options.WithLeaveStreamOpen(false)) { } public override bool IsFirstVolume => true; diff --git a/src/SharpCompress/Common/Lzw/LzwVolume.cs b/src/SharpCompress/Common/Lzw/LzwVolume.cs index b771a19a..be50f106 100644 --- a/src/SharpCompress/Common/Lzw/LzwVolume.cs +++ b/src/SharpCompress/Common/Lzw/LzwVolume.cs @@ -9,7 +9,7 @@ public class LzwVolume : Volume : base(stream, options, index) { } public LzwVolume(FileInfo fileInfo, ReaderOptions options) - : base(fileInfo.OpenRead(), options with { LeaveStreamOpen = false }) { } + : base(fileInfo.OpenRead(), options.WithLeaveStreamOpen(false)) { } public override bool IsFirstVolume => true; diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 86d05f42..109e3e93 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -153,7 +153,7 @@ public sealed record ReaderOptions : IReaderOptions /// internal static ReaderOptions Default => new(); - public static ReaderOptions ForExternalStream => Default with { LeaveStreamOpen = true }; + public static ReaderOptions ForExternalStream => Default.WithLeaveStreamOpen(true); /// /// Gets ReaderOptions configured for file-based overloads that open their own stream. diff --git a/tests/SharpCompress.Test/CompressionProviderTests.cs b/tests/SharpCompress.Test/CompressionProviderTests.cs index 31c04d3a..1b043efd 100644 --- a/tests/SharpCompress.Test/CompressionProviderTests.cs +++ b/tests/SharpCompress.Test/CompressionProviderTests.cs @@ -436,7 +436,7 @@ public class CompressionProviderTests archiveStream.Position = 0; var customProvider = new GZipCompressionProvider(); var registry = CompressionProviderRegistry.Default.With(customProvider); - var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream.WithProviders(registry); using var reader = TarReader.OpenReader(archiveStream, readOptions); reader.MoveToNextEntry().Should().BeTrue(); @@ -465,7 +465,7 @@ public class CompressionProviderTests archiveStream.Position = 0; var registry = CompressionProviderRegistry.Default.With(new ContextRequiredGZipProvider()); - var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream.WithProviders(registry); using var reader = TarReader.OpenReader(archiveStream, readOptions); reader.MoveToNextEntry().Should().BeTrue(); @@ -540,7 +540,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream.WithProviders(registry); archiveStream.Position = 0; using var archive = TarArchive.OpenArchive(archiveStream, readOptions); @@ -569,7 +569,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new GZipCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var readOptions = ReaderOptions.ForExternalStream with { Providers = registry }; + var readOptions = ReaderOptions.ForExternalStream.WithProviders(registry); archiveStream.Position = 0; await using var archive = await TarArchive.OpenAsyncArchive(archiveStream, readOptions); @@ -607,7 +607,7 @@ public class CompressionProviderTests var trackingProvider = new TrackingCompressionProvider(new DeflateCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var options = ReaderOptions.ForExternalStream with { Providers = registry }; + var options = ReaderOptions.ForExternalStream.WithProviders(registry); zipStream.Position = 0; await using var reader = await ReaderFactory.OpenAsyncReader(zipStream, options); @@ -625,7 +625,7 @@ public class CompressionProviderTests var archivePath = Path.Combine(TestBase.TEST_ARCHIVES_PATH, "Tar.tar.Z"); var trackingProvider = new TrackingCompressionProvider(new LzwCompressionProvider()); var registry = CompressionProviderRegistry.Default.With(trackingProvider); - var options = ReaderOptions.ForExternalStream with { Providers = registry }; + var options = ReaderOptions.ForExternalStream.WithProviders(registry); using var stream = File.OpenRead(archivePath); using var reader = ReaderFactory.OpenReader(stream, options); diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index b1d896c9..ff79a41c 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -108,7 +108,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream.WithProgress(progress); using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -324,7 +324,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream.WithProgress(progress); using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -445,7 +445,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream.WithProgress(progress); using (var reader = ReaderFactory.OpenReader(archiveStream, readerOptions)) { @@ -538,7 +538,7 @@ public class ProgressReportTests : TestBase // Now read it with progress reporting archiveStream.Position = 0; - var readerOptions = ReaderOptions.ForExternalStream with { Progress = progress }; + var readerOptions = ReaderOptions.ForExternalStream.WithProgress(progress); await using ( var reader = await ReaderFactory.OpenAsyncReader( diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index b80cfcfa..1d57bd6e 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -768,7 +768,7 @@ public class RarArchiveTests : ArchiveTests public void Rar_MalformedArchive_NoInfiniteLoop() { var testFile = "Rar.malformed_512byte.rar"; - var readerOptions = ReaderOptions.ForExternalStream with { LookForHeader = true }; + var readerOptions = ReaderOptions.ForExternalStream.WithLookForHeader(true); // This should throw InvalidOperationException, not hang in an infinite loop var exception = Assert.Throws(() => diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index c30b58e1..72ab5f47 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -32,12 +32,12 @@ public abstract class ReaderTests : TestBase ) { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); - options ??= ReaderOptions.ForFilePath with { BufferSize = 0x20000 }; + options ??= ReaderOptions.ForFilePath.WithBufferSize(0x20000); - var optionsWithStreamOpen = options with { LeaveStreamOpen = true }; + var optionsWithStreamOpen = options.WithLeaveStreamOpen(true); readImpl(testArchive, optionsWithStreamOpen); - var optionsWithStreamClosed = options with { LeaveStreamOpen = false }; + var optionsWithStreamClosed = options.WithLeaveStreamOpen(false); readImpl(testArchive, optionsWithStreamClosed); VerifyFiles(); @@ -128,9 +128,9 @@ public abstract class ReaderTests : TestBase { testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); - options ??= ReaderOptions.ForExternalStream with { BufferSize = 0x20000 }; + options ??= ReaderOptions.ForExternalStream.WithBufferSize(0x20000); - var optionsWithStreamOpen = options with { LeaveStreamOpen = true }; + var optionsWithStreamOpen = options.WithLeaveStreamOpen(true); await ReadImplAsync( testArchive, expectedCompression, @@ -138,7 +138,7 @@ public abstract class ReaderTests : TestBase cancellationToken ); - var optionsWithStreamClosed = options with { LeaveStreamOpen = false }; + var optionsWithStreamClosed = options.WithLeaveStreamOpen(false); await ReadImplAsync( testArchive, expectedCompression, diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index 59c6b004..e3016d96 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -48,7 +48,7 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMAAES_NoPasswordExceptionTest() => Assert.Throws(() => - ArchiveFileRead("7Zip.LZMA.Aes.7z", ReaderOptions.ForFilePath with { Password = null }) + ArchiveFileRead("7Zip.LZMA.Aes.7z", ReaderOptions.ForFilePath.WithPassword(null)) ); //was failing with ArgumentNullException not CryptographicException like rar [Fact] @@ -69,11 +69,19 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMA2_EXE_StreamRead() => - ArchiveStreamRead(new SevenZipFactory(), "7Zip.LZMA2.exe", ReaderOptions.ForExternalStream.WithLookForHeader(true)); + ArchiveStreamRead( + new SevenZipFactory(), + "7Zip.LZMA2.exe", + ReaderOptions.ForExternalStream.WithLookForHeader(true) + ); [Fact] public void SevenZipArchive_LZMA2_EXE_PathRead() => - ArchiveFileRead("7Zip.LZMA2.exe", ReaderOptions.ForFilePath.WithLookForHeader(true), new SevenZipFactory()); + ArchiveFileRead( + "7Zip.LZMA2.exe", + ReaderOptions.ForFilePath.WithLookForHeader(true), + new SevenZipFactory() + ); [Fact] public void SevenZipArchive_LZMA2AES_StreamRead() => diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 923d5bf2..30b511fa 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -213,7 +213,7 @@ public class TarArchiveAsyncTests : ArchiveTests } using (var inputMemory = new MemoryStream(mstm.ToArray())) { - var tropt = ReaderOptions.ForExternalStream with { ArchiveEncoding = enc }; + var tropt = ReaderOptions.ForExternalStream.WithArchiveEncoding(enc); await using ( var tr = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(inputMemory), diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index 8db1ca16..ebb721f5 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -262,7 +262,7 @@ public class TarArchiveTests : ArchiveTests } using (var inputMemory = new MemoryStream(mstm.ToArray())) { - var tropt = ReaderOptions.ForExternalStream with { ArchiveEncoding = enc }; + var tropt = ReaderOptions.ForExternalStream.WithArchiveEncoding(enc); using (var tr = TarReader.OpenReader(inputMemory, tropt)) { while (tr.MoveToNextEntry()) diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index e39fbe8c..0cdd6a07 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -255,7 +255,7 @@ public class TestBase : IAsyncDisposable protected void CompareArchivesByPath(string file1, string file2, Encoding? encoding = null) { - var readerOptions = ReaderOptions.ForExternalStream with { LeaveStreamOpen = false }; + var readerOptions = ReaderOptions.ForExternalStream.WithLeaveStreamOpen(false); readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default; //don't compare the order. OS X reads files from the file system in a different order therefore makes the archive ordering different From d10b93242c5dbda5a32323dbc37beed44c65597b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 21 Apr 2026 11:10:53 +0100 Subject: [PATCH 51/74] clean up for other options --- .../Common/ExtractionMethods.Async.cs | 10 +--------- src/SharpCompress/Common/ExtractionMethods.cs | 10 +--------- src/SharpCompress/Common/ExtractionOptions.cs | 15 +-------------- src/SharpCompress/Writers/WriterOptions.cs | 14 +++----------- 4 files changed, 6 insertions(+), 43 deletions(-) diff --git a/src/SharpCompress/Common/ExtractionMethods.Async.cs b/src/SharpCompress/Common/ExtractionMethods.Async.cs index 8792e06b..053e62f7 100644 --- a/src/SharpCompress/Common/ExtractionMethods.Async.cs +++ b/src/SharpCompress/Common/ExtractionMethods.Async.cs @@ -90,15 +90,7 @@ internal static partial class ExtractionMethods options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (options.SymbolicLinkHandler is not null) - { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - else - { - ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - return; + options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget); } else { diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 526c6e26..f7980fa8 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -99,15 +99,7 @@ internal static partial class ExtractionMethods options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (options.SymbolicLinkHandler is not null) - { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - else - { - ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - return; + options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget); } else { diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs index 84a6bffe..0074f13c 100644 --- a/src/SharpCompress/Common/ExtractionOptions.cs +++ b/src/SharpCompress/Common/ExtractionOptions.cs @@ -58,10 +58,7 @@ public sealed record ExtractionOptions : IExtractionOptions /// Creates a new ExtractionOptions instance with the specified overwrite behavior. /// /// Whether to overwrite existing files. - public ExtractionOptions(bool overwrite) - { - Overwrite = overwrite; - } + public ExtractionOptions(bool overwrite) => Overwrite = overwrite; /// /// Creates a new ExtractionOptions instance with the specified extraction path and overwrite behavior. @@ -102,14 +99,4 @@ public sealed record ExtractionOptions : IExtractionOptions /// public static ExtractionOptions PreserveMetadata => new() { PreserveFileTime = true, PreserveAttributes = true }; - - /// - /// Default symbolic link handler that logs a warning message. - /// - public static void DefaultSymbolicLinkHandler(string sourcePath, string targetPath) - { - Console.WriteLine( - $"Could not write symlink {sourcePath} -> {targetPath}, for more information please see https://github.com/dotnet/runtime/issues/24271" - ); - } } diff --git a/src/SharpCompress/Writers/WriterOptions.cs b/src/SharpCompress/Writers/WriterOptions.cs index cc0e0370..6415ca3e 100644 --- a/src/SharpCompress/Writers/WriterOptions.cs +++ b/src/SharpCompress/Writers/WriterOptions.cs @@ -1,7 +1,6 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; -using SharpCompress.Compressors; using SharpCompress.Providers; using D = SharpCompress.Compressors.Deflate; @@ -18,17 +17,10 @@ namespace SharpCompress.Writers; /// public sealed record WriterOptions : IWriterOptions { - private CompressionType _compressionType; - private int _compressionLevel; - /// /// The compression type to use for the archive. /// - public CompressionType CompressionType - { - get => _compressionType; - init => _compressionType = value; - } + public CompressionType CompressionType { get; init; } /// /// The compression level to be used when the compression type supports variable levels. @@ -40,11 +32,11 @@ public sealed record WriterOptions : IWriterOptions /// public int CompressionLevel { - get => _compressionLevel; + get; init { CompressionLevelValidation.Validate(CompressionType, value); - _compressionLevel = value; + field = value; } } From 5d75f8c36dc3db971bb4fa82cd5fe354845a40cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:11:31 +0000 Subject: [PATCH 52/74] =?UTF-8?q?Fix=20ForExternalStream=E2=86=92ForFilePa?= =?UTF-8?q?th=20for=20file-path=20overloads=20and=20update=20SymbolicLinkH?= =?UTF-8?q?andler=20remarks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/41bf7d12-f140-47cf-96f9-3620eeb0eb32 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Common/ExtractionOptions.cs | 2 +- src/SharpCompress/packages.lock.json | 12 ++++++------ tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs | 2 +- tests/SharpCompress.Test/Rar/RarArchiveTests.cs | 2 +- tests/SharpCompress.Test/Zip/ZipArchiveTests.cs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs index 0074f13c..ea84ed53 100644 --- a/src/SharpCompress/Common/ExtractionOptions.cs +++ b/src/SharpCompress/Common/ExtractionOptions.cs @@ -45,7 +45,7 @@ public sealed record ExtractionOptions : IExtractionOptions /// /// /// Breaking change: Changed from field to init-only property in version 0.40.0. - /// The default handler logs a warning message. + /// If no handler is provided, symbolic links are silently skipped during extraction. /// public Action? SymbolicLinkHandler { get; init; } diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e82196f8..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.26, )", - "resolved": "8.0.26", - "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 8051875a..eebdd38d 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -330,7 +330,7 @@ public class RarArchiveAsyncTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"), - ReaderOptions.ForExternalStream with + ReaderOptions.ForFilePath with { LookForHeader = true, } diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index 1d57bd6e..20b5faa0 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -316,7 +316,7 @@ public class RarArchiveTests : ArchiveTests using ( var archive = RarArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Rar.jpeg.jpg"), - ReaderOptions.ForExternalStream with + ReaderOptions.ForFilePath with { LookForHeader = true, } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index d50a05e1..82192b22 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -622,7 +622,7 @@ public class ZipArchiveTests : ArchiveTests { using var reader = ZipArchive.OpenArchive( Path.Combine(TEST_ARCHIVES_PATH, "Zip.zip64.zip"), - ReaderOptions.ForExternalStream with + ReaderOptions.ForFilePath with { Password = "test", } From f4935b042fc32cecb89cc44f4b75d1ae9f14367a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:15:28 +0000 Subject: [PATCH 53/74] Fix ReaderOptions preset usage for file-path overloads and stale SymbolicLinkHandler docs Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/41bf7d12-f140-47cf-96f9-3620eeb0eb32 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/packages.lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 03c03a9a..a401c702 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 3ed94dd462158ceb8dcf8e25cd48ad18dea0ae85 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 09:56:26 +0100 Subject: [PATCH 54/74] add seekable checks --- .../Archives/ArchiveFactory.Async.cs | 2 ++ src/SharpCompress/Archives/ArchiveFactory.cs | 18 +++++++++++++++ .../Archives/GZip/GZipArchive.Factory.cs | 1 + .../Archives/Rar/RarArchive.Factory.cs | 1 + .../SevenZip/SevenZipArchive.Factory.cs | 1 + .../Archives/Tar/TarArchive.Factory.cs | 6 +++++ .../Archives/Zip/ZipArchive.Factory.cs | 2 ++ .../SharpCompress.Test/ArchiveFactoryTests.cs | 22 +++++++++++++++++++ .../GZip/GZipArchiveTests.cs | 10 +++++++++ .../SharpCompress.Test/Rar/RarArchiveTests.cs | 9 ++++++++ .../SevenZip/SevenZipArchiveTests.cs | 12 ++++++++++ .../Tar/TarArchiveAsyncTests.cs | 10 +++++++++ .../SharpCompress.Test/Tar/TarArchiveTests.cs | 9 ++++++++ .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 9 ++++++++ 14 files changed, 112 insertions(+) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index fde9a93d..0d5415e1 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -99,6 +99,8 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } + EnsureSeekable(streamsArray); + var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index ec044e0e..308aaa63 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -13,6 +13,22 @@ namespace SharpCompress.Archives; public static partial class ArchiveFactory { + internal static void EnsureSeekable(Stream stream) + { + if (stream is null || !stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } + } + + internal static void EnsureSeekable(IReadOnlyList streams) + { + foreach (var stream in streams) + { + EnsureSeekable(stream); + } + } + public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= ReaderOptions.ForExternalStream; @@ -80,6 +96,8 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } + EnsureSeekable(streamsArray); + var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 9dad1ae8..6a0b6bac 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -77,6 +77,7 @@ public partial class GZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new GZipArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index df820e2f..79d89718 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -92,6 +92,7 @@ public partial class RarArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new RarArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index df7fa8c4..011b9a86 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -72,6 +72,7 @@ public partial class SevenZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new SevenZipArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index e1464dbf..37abd21c 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -67,6 +67,7 @@ public partial class TarArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; var sourceStream = new SourceStream( strms[0], @@ -103,6 +104,10 @@ public partial class TarArchive ) { stream.NotNull(nameof(stream)); + if (!stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } var sourceStream = new SourceStream( stream, i => null, @@ -159,6 +164,7 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; var sourceStream = new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index bdd18669..18d28114 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -68,6 +68,7 @@ public partial class ZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new ZipArchive( new SourceStream( @@ -132,6 +133,7 @@ public partial class ZipArchive ) { cancellationToken.ThrowIfCancellationRequested(); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 96249f8e..af5ab692 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -1,9 +1,11 @@ +using System; using System.IO; using System.Text; using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test; @@ -61,6 +63,26 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(startPosition, stream.Position); } + [Fact] + public void OpenArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => ArchiveFactory.OpenArchive([nonSeekable, seekable])); + } + + [Fact] + public async ValueTask OpenAsyncArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + await Assert.ThrowsAsync(() => + ArchiveFactory.OpenAsyncArchive([nonSeekable, seekable]).AsTask() + ); + } + [Fact] public async ValueTask FindFactoryAsync_InvalidData_ThrowsArchiveOperationException() { diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs index a33256a6..59e8b4fe 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -5,6 +5,7 @@ using SharpCompress.Archives; using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; using SharpCompress.Common; +using SharpCompress.Test.Mocks; using SharpCompress.Writers.GZip; using Xunit; @@ -127,6 +128,15 @@ public class GZipArchiveTests : ArchiveTests Assert.Equal(archive.Type, ArchiveType.GZip); } + [Fact] + public void GZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => GZipArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void GZip_Archive_NonSeekableStream() { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index 20b5faa0..d13660c6 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -122,6 +122,15 @@ public class RarArchiveTests : ArchiveTests [Fact] public void Rar_ArchiveStreamRead() => ArchiveStreamRead("Rar.rar"); + [Fact] + public void RarArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => RarArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Rar5_ArchiveStreamRead() => ArchiveStreamRead("Rar5.rar"); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index e3016d96..f16036df 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Common; using SharpCompress.Common.SevenZip; using SharpCompress.Factories; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.SevenZip; @@ -25,6 +26,17 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMA_PathRead() => ArchiveFileRead("7Zip.LZMA.7z"); + [Fact] + public void SevenZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => + SevenZipArchive.OpenArchive([nonSeekable, seekable]) + ); + } + [Fact] public void SevenZipArchive_LZMAAES_StreamRead() => ArchiveStreamRead( diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 30b511fa..0ab211f4 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -22,6 +22,16 @@ public class TarArchiveAsyncTests : ArchiveTests [Fact] public async ValueTask TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar"); + [Fact] + public async ValueTask TarArchiveOpenAsyncStream_Throws_On_NonSeekable_Stream() + { + using var stream = new ForwardOnlyStream(new MemoryStream()); + + await Assert.ThrowsAsync(() => + TarArchive.OpenAsyncArchive(stream).AsTask() + ); + } + [Fact] public async ValueTask Tar_FileName_Exactly_100_Characters_Async() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index ebb721f5..a0dcd87a 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -34,6 +34,15 @@ public class TarArchiveTests : ArchiveTests Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); } + [Fact] + public void TarArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => TarArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Tar_FileName_Exactly_100_Characters() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 82192b22..8f4d4a20 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -28,6 +28,15 @@ public class ZipArchiveTests : ArchiveTests [Fact] public void Zip_BZip2_ArchiveStreamRead() => ArchiveStreamRead("Zip.bzip2.zip"); + [Fact] + public void ZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new NonSeekableMemoryStream(); + using var seekable = new MemoryStream(); + + Assert.Throws(() => ZipArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Zip_Deflate_Streamed2_ArchiveStreamRead() => ArchiveStreamRead("Zip.deflate.dd-.zip"); From be4b6cdd7f7892e57cf19fb8723610f7b3ed2fbb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 09:56:53 +0100 Subject: [PATCH 55/74] ignore opencode stuff --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 66147659..80f4154a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ profiler-snapshots/ .DS_Store *.snupkg benchmark-results/ +/.opencode From 2bae46e28aa1f9fcbd30bffbf9c034a7971cb816 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 10:11:54 +0100 Subject: [PATCH 56/74] add extension methods for checks --- .../Archives/ArchiveFactory.Async.cs | 9 +-- src/SharpCompress/Archives/ArchiveFactory.cs | 41 +++---------- .../Archives/GZip/GZipArchive.Factory.cs | 9 +-- .../Archives/Rar/RarArchive.Factory.cs | 9 +-- .../SevenZip/SevenZipArchive.Factory.cs | 9 +-- .../Archives/Tar/TarArchive.Factory.cs | 16 ++--- .../Archives/Zip/ZipArchive.Factory.cs | 11 +--- .../Readers/Ace/AceReader.Factory.cs | 10 ++-- .../Readers/Ace/SingleVolumeAceReader.cs | 2 +- src/SharpCompress/Readers/Arc/ArcReader.cs | 2 +- src/SharpCompress/Readers/Arj/ArjReader.cs | 6 +- .../Readers/Arj/SingleVolumeArjReader.cs | 2 +- .../Readers/GZip/GZipReader.Factory.cs | 2 +- .../Readers/Lzw/LzwReader.Factory.cs | 2 +- src/SharpCompress/Readers/Rar/RarReader.cs | 6 +- .../Readers/ReaderFactory.Async.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- .../Readers/Tar/TarReader.Factory.cs | 2 +- src/SharpCompress/Readers/Zip/ZipReader.cs | 4 +- .../StreamValidationExtensions.cs | 58 +++++++++++++++++++ .../SharpCompress.Test/ReaderFactoryTests.cs | 39 +++++++++++++ 21 files changed, 141 insertions(+), 102 deletions(-) create mode 100644 src/SharpCompress/StreamValidationExtensions.cs create mode 100644 tests/SharpCompress.Test/ReaderFactoryTests.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 0d5415e1..39ac2795 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -99,7 +99,7 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } - EnsureSeekable(streamsArray); + streamsArray.RequireSeekable(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) @@ -145,11 +145,8 @@ public static partial class ArchiveFactory ) where T : IFactory { - stream.NotNull(nameof(stream)); - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var factories = Factory.Factories.OfType(); diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 308aaa63..eb7a6815 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -13,22 +13,6 @@ namespace SharpCompress.Archives; public static partial class ArchiveFactory { - internal static void EnsureSeekable(Stream stream) - { - if (stream is null || !stream.CanSeek) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } - } - - internal static void EnsureSeekable(IReadOnlyList streams) - { - foreach (var stream in streams) - { - EnsureSeekable(stream); - } - } - public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= ReaderOptions.ForExternalStream; @@ -96,7 +80,7 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } - EnsureSeekable(streamsArray); + streamsArray.RequireSeekable(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) @@ -139,11 +123,8 @@ public static partial class ArchiveFactory public static T FindFactory(Stream stream) where T : IFactory { - stream.NotNull(nameof(stream)); - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var factories = Factory.Factories.OfType(); @@ -178,12 +159,8 @@ public static partial class ArchiveFactory public static bool IsArchive(Stream stream, out ArchiveType? type) { type = null; - stream.NotNull(nameof(stream)); - - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var startPosition = stream.Position; @@ -217,12 +194,8 @@ public static partial class ArchiveFactory CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); - - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var startPosition = stream.Position; diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 6a0b6bac..c7f57f88 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -77,7 +77,7 @@ public partial class GZipArchive ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new GZipArchive( new SourceStream( @@ -93,12 +93,7 @@ public partial class GZipArchive ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new GZipArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 79d89718..b97e7497 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -58,12 +58,7 @@ public partial class RarArchive public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new RarArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) @@ -92,7 +87,7 @@ public partial class RarArchive ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new RarArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 011b9a86..e15354b1 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -72,7 +72,7 @@ public partial class SevenZipArchive ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new SevenZipArchive( new SourceStream( @@ -85,12 +85,7 @@ public partial class SevenZipArchive public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new SevenZipArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 37abd21c..5eacb117 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -67,7 +67,7 @@ public partial class TarArchive ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; var sourceStream = new SourceStream( strms[0], @@ -87,12 +87,7 @@ public partial class TarArchive ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return OpenArchive([stream], readerOptions); } @@ -104,10 +99,7 @@ public partial class TarArchive ) { stream.NotNull(nameof(stream)); - if (!stream.CanSeek) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); var sourceStream = new SourceStream( stream, i => null, @@ -164,7 +156,7 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; var sourceStream = new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index 18d28114..d83d00e1 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -68,7 +68,7 @@ public partial class ZipArchive ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new ZipArchive( new SourceStream( @@ -84,12 +84,7 @@ public partial class ZipArchive ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new ZipArchive( new SourceStream(stream, i => null, readerOptions ?? ReaderOptions.ForExternalStream) @@ -133,7 +128,7 @@ public partial class ZipArchive ) { cancellationToken.ThrowIfCancellationRequested(); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs index 5977e8de..98c3124a 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs @@ -19,7 +19,7 @@ public partial class AceReader /// An AceReader instance. public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeAceReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -31,8 +31,8 @@ public partial class AceReader /// public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( @@ -61,8 +61,8 @@ public partial class AceReader ReaderOptions? options = null ) { - streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs index ce42c6e1..ba782d23 100644 --- a/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs +++ b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs @@ -12,7 +12,7 @@ internal class SingleVolumeAceReader : AceReader internal SingleVolumeAceReader(Stream stream, ReaderOptions options) : base(options) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); _stream = stream; } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs index c1b53366..e2a8303d 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -24,7 +24,7 @@ public partial class ArcReader : AbstractReader /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ArcReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs index a0c4899c..79389b87 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -31,7 +31,7 @@ public abstract partial class ArjReader : AbstractReader /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeArjReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -43,8 +43,8 @@ public abstract partial class ArjReader : AbstractReader /// public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeArjReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeArjReader(streamArray, options ?? ReaderOptions.ForExternalStream); } protected abstract void ValidateArchive(ArjVolume archive); diff --git a/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs index 71eb46e4..0128f60e 100644 --- a/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs +++ b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs @@ -12,7 +12,7 @@ internal class SingleVolumeArjReader : ArjReader internal SingleVolumeArjReader(Stream stream, ReaderOptions options) : base(options) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); _stream = stream; } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs index bd593f20..a6bca9f6 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs @@ -55,7 +55,7 @@ public partial class GZipReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new GZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs index e2166e37..a1535c89 100644 --- a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs @@ -55,7 +55,7 @@ public partial class LzwReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new LzwReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index 81a5457c..43925d64 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -71,7 +71,7 @@ public abstract partial class RarReader : AbstractReader public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeRarReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -83,8 +83,8 @@ public abstract partial class RarReader : AbstractReader public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeRarReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeRarReader(streamArray, options ?? ReaderOptions.ForExternalStream); } protected override IEnumerable GetEntries(Stream stream) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 80388577..72bd7b09 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -56,7 +56,7 @@ public static partial class ReaderFactory CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 2a84913c..72c4dd71 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -29,7 +29,7 @@ public static partial class ReaderFactory /// public static IReader OpenReader(Stream stream, ReaderOptions? options = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index aae20bb4..aa13c779 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -170,7 +170,7 @@ public partial class TarReader /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); readerOptions ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( stream, diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index e2019de6..a9ca5c50 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -47,7 +47,7 @@ public partial class ZipReader : AbstractReader /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -57,7 +57,7 @@ public partial class ZipReader : AbstractReader IEnumerable entries ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ZipReader(stream, options ?? ReaderOptions.ForExternalStream, entries); } diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs new file mode 100644 index 00000000..7420bcec --- /dev/null +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SharpCompress; + +internal static class StreamValidationExtensions +{ + internal static Stream RequireReadable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanRead) + { + throw new ArgumentException("Stream must be readable", nameof(stream)); + } + + return stream; + } + + internal static Stream RequireSeekable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } + + return stream; + } + + internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) + { + streams.NotNull(nameof(streams)); + + foreach (var stream in streams) + { + stream.RequireSeekable(); + } + + return streams; + } + + internal static IReadOnlyList RequireReadable(this IEnumerable streams) + { + streams.NotNull(nameof(streams)); + + var streamArray = streams as IReadOnlyList ?? streams.ToArray(); + foreach (var stream in streamArray) + { + stream.RequireReadable(); + } + + return streamArray; + } +} diff --git a/tests/SharpCompress.Test/ReaderFactoryTests.cs b/tests/SharpCompress.Test/ReaderFactoryTests.cs new file mode 100644 index 00000000..f77fb2f9 --- /dev/null +++ b/tests/SharpCompress.Test/ReaderFactoryTests.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Readers; +using SharpCompress.Readers.Rar; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test; + +public class ReaderFactoryTests +{ + [Fact] + public void OpenReader_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ReaderFactory.OpenReader(unreadable)); + } + + [Fact] + public async ValueTask OpenAsyncReader_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + await Assert.ThrowsAsync(() => + ReaderFactory.OpenAsyncReader(unreadable).AsTask() + ); + } + + [Fact] + public void RarReader_StreamCollection_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + using var readable = new MemoryStream(); + + Assert.Throws(() => RarReader.OpenReader([unreadable, readable])); + } +} From 5d14c96fb03699f2bfdcc44dbe4a2597995cbcdb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 10:19:37 +0100 Subject: [PATCH 57/74] clean up seekable checks --- .../Archives/ArchiveFactory.Async.cs | 6 ++---- src/SharpCompress/Archives/ArchiveFactory.cs | 6 ++---- .../Archives/GZip/GZipArchive.Factory.cs | 6 +++--- .../Archives/Rar/RarArchive.Factory.cs | 6 +++--- .../SevenZip/SevenZipArchive.Factory.cs | 6 +++--- .../Archives/Tar/TarArchive.Factory.cs | 13 ++++++------- .../Archives/Zip/ZipArchive.Factory.cs | 7 +++---- .../SharpCompress.Test/ArchiveFactoryTests.cs | 18 ++++++++++++++++++ .../GZip/GZipArchiveTests.cs | 8 ++++++++ .../SharpCompress.Test/Rar/RarArchiveTests.cs | 8 ++++++++ .../SevenZip/SevenZipArchiveTests.cs | 8 ++++++++ .../Tar/TarArchiveAsyncTests.cs | 10 ++++++++++ .../SharpCompress.Test/Tar/TarArchiveTests.cs | 13 +++++++++++++ .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 9 +++++++++ 14 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 39ac2795..6518952f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -92,15 +92,13 @@ public static partial class ArchiveFactory ) { cancellationToken.ThrowIfCancellationRequested(); - streams.NotNull(nameof(streams)); - var streamsArray = streams; + var streamsArray = streams.RequireReadable(); + streamsArray.RequireSeekable(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } - streamsArray.RequireSeekable(); - var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index eb7a6815..7f1834d1 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -73,15 +73,13 @@ public static partial class ArchiveFactory public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - var streamsArray = streams; + var streamsArray = streams.RequireReadable(); + streamsArray.RequireSeekable(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } - streamsArray.RequireSeekable(); - var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index c7f57f88..9caa068e 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -76,9 +76,8 @@ public partial class GZipArchive ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new GZipArchive( new SourceStream( strms[0], @@ -93,6 +92,7 @@ public partial class GZipArchive ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return new GZipArchive( diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index b97e7497..31d53b0a 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -58,6 +58,7 @@ public partial class RarArchive public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { + stream.RequireReadable(); stream.RequireSeekable(); return new RarArchive( @@ -86,9 +87,8 @@ public partial class RarArchive ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new RarArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index e15354b1..161b0ed2 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -71,9 +71,8 @@ public partial class SevenZipArchive ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new SevenZipArchive( new SourceStream( strms[0], @@ -85,6 +84,7 @@ public partial class SevenZipArchive public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { + stream.RequireReadable(); stream.RequireSeekable(); return new SevenZipArchive( diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 5eacb117..9d60a6e8 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -66,9 +66,8 @@ public partial class TarArchive ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, @@ -87,6 +86,7 @@ public partial class TarArchive ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return OpenArchive([stream], readerOptions); @@ -98,7 +98,7 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); stream.RequireSeekable(); var sourceStream = new SourceStream( stream, @@ -155,9 +155,8 @@ public partial class TarArchive ) { cancellationToken.ThrowIfCancellationRequested(); - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index d83d00e1..a015272a 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -67,9 +67,8 @@ public partial class ZipArchive ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new ZipArchive( new SourceStream( strms[0], @@ -84,6 +83,7 @@ public partial class ZipArchive ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return new ZipArchive( @@ -128,7 +128,6 @@ public partial class ZipArchive ) { cancellationToken.ThrowIfCancellationRequested(); - streams.RequireSeekable(); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index af5ab692..d6326bad 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -93,6 +93,24 @@ public class ArchiveFactoryTests : TestBase ); } + [Fact] + public void OpenArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ArchiveFactory.OpenArchive(unreadable)); + } + + [Fact] + public async ValueTask OpenAsyncArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + await Assert.ThrowsAsync(() => + ArchiveFactory.OpenAsyncArchive(unreadable).AsTask() + ); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs index 59e8b4fe..5f9f5094 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -137,6 +137,14 @@ public class GZipArchiveTests : ArchiveTests Assert.Throws(() => GZipArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void GZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => GZipArchive.OpenArchive(unreadable)); + } + [Fact] public void GZip_Archive_NonSeekableStream() { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index d13660c6..c4ef386c 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -131,6 +131,14 @@ public class RarArchiveTests : ArchiveTests Assert.Throws(() => RarArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void RarArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => RarArchive.OpenArchive(unreadable)); + } + [Fact] public void Rar5_ArchiveStreamRead() => ArchiveStreamRead("Rar5.rar"); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index f16036df..0fc86c3e 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -37,6 +37,14 @@ public class SevenZipArchiveTests : ArchiveTests ); } + [Fact] + public void SevenZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => SevenZipArchive.OpenArchive(unreadable)); + } + [Fact] public void SevenZipArchive_LZMAAES_StreamRead() => ArchiveStreamRead( diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 0ab211f4..a2c06516 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -32,6 +32,16 @@ public class TarArchiveAsyncTests : ArchiveTests ); } + [Fact] + public async ValueTask TarArchiveOpenAsyncStream_Throws_On_Unreadable_Stream() + { + using var stream = new TestStream(new MemoryStream(), false, true, true); + + await Assert.ThrowsAsync(() => + TarArchive.OpenAsyncArchive(stream).AsTask() + ); + } + [Fact] public async ValueTask Tar_FileName_Exactly_100_Characters_Async() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index a0dcd87a..5f522e7c 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -34,6 +34,19 @@ public class TarArchiveTests : ArchiveTests Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); } + [Fact] + public void TarArchiveStreamRead_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")), + false, + true, + true + ); + + Assert.Throws(() => TarArchive.OpenArchive(unreadable)); + } + [Fact] public void TarArchive_StreamCollection_Throws_On_NonSeekable_Stream() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 8f4d4a20..ca80917f 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Zip; using Xunit; @@ -37,6 +38,14 @@ public class ZipArchiveTests : ArchiveTests Assert.Throws(() => ZipArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void ZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ZipArchive.OpenArchive(unreadable)); + } + [Fact] public void Zip_Deflate_Streamed2_ArchiveStreamRead() => ArchiveStreamRead("Zip.deflate.dd-.zip"); From c9a68593ea9ce2b0880e53203b640b501e212aae Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 11:19:09 +0100 Subject: [PATCH 58/74] add writable checks --- .../StreamValidationExtensions.cs | 12 +++++++ .../Writers/GZip/GZipWriter.Factory.cs | 2 +- .../SevenZip/SevenZipWriter.Factory.cs | 2 +- .../Writers/Tar/TarWriter.Factory.cs | 2 +- src/SharpCompress/Writers/WriterFactory.cs | 4 +++ .../Writers/Zip/ZipWriter.Factory.cs | 2 +- .../SharpCompress.Test/WriterFactoryTests.cs | 34 +++++++++++++++++++ 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/SharpCompress.Test/WriterFactoryTests.cs diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs index 7420bcec..8c2b87db 100644 --- a/src/SharpCompress/StreamValidationExtensions.cs +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -31,6 +31,18 @@ internal static class StreamValidationExtensions return stream; } + internal static Stream RequireWritable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanWrite) + { + throw new ArgumentException("Stream must be writable", nameof(stream)); + } + + return stream; + } + internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) { streams.NotNull(nameof(streams)); diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs index f4f2b902..cae0b91d 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs @@ -22,7 +22,7 @@ public partial class GZipWriter : IWriterOpenable public static IWriter OpenWriter(Stream stream, GZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new GZipWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs index e84d7401..1191a83a 100644 --- a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs @@ -36,7 +36,7 @@ public partial class SevenZipWriter : IWriterOpenable /// public static IWriter OpenWriter(Stream stream, SevenZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new SevenZipWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs index b00ed13e..7613374b 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs @@ -22,7 +22,7 @@ public partial class TarWriter : IWriterOpenable public static IWriter OpenWriter(Stream stream, TarWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new TarWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 48ef1d47..847766f9 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -75,6 +75,8 @@ public static class WriterFactory IWriterOptions writerOptions ) { + stream.RequireWritable(); + var factory = Factories .Factory.Factories.OfType() .FirstOrDefault(item => item.KnownArchiveType == archiveType); @@ -102,6 +104,8 @@ public static class WriterFactory CancellationToken cancellationToken = default ) { + stream.RequireWritable(); + var factory = Factories .Factory.Factories.OfType() .FirstOrDefault(item => item.KnownArchiveType == archiveType); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs index b6667842..825de870 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs @@ -22,7 +22,7 @@ public partial class ZipWriter : IWriterOpenable public static IWriter OpenWriter(Stream stream, ZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new ZipWriter(stream, writerOptions); } diff --git a/tests/SharpCompress.Test/WriterFactoryTests.cs b/tests/SharpCompress.Test/WriterFactoryTests.cs new file mode 100644 index 00000000..ca7a45a5 --- /dev/null +++ b/tests/SharpCompress.Test/WriterFactoryTests.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Test.Mocks; +using SharpCompress.Writers; +using Xunit; + +namespace SharpCompress.Test; + +public class WriterFactoryTests +{ + [Fact] + public void OpenWriter_Stream_Throws_On_Unwritable_Stream() + { + using var unwritable = new TestStream(new MemoryStream(), true, false, true); + + Assert.Throws(() => + WriterFactory.OpenWriter(unwritable, ArchiveType.Zip, WriterOptions.ForZip()) + ); + } + + [Fact] + public async ValueTask OpenAsyncWriter_Stream_Throws_On_Unwritable_Stream() + { + using var unwritable = new TestStream(new MemoryStream(), true, false, true); + + await Assert.ThrowsAsync(() => + WriterFactory + .OpenAsyncWriter(unwritable, ArchiveType.Zip, WriterOptions.ForZip()) + .AsTask() + ); + } +} From 41432ab0624bbf19dc5497fd37430df4c49d24f7 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 11:43:10 +0100 Subject: [PATCH 59/74] human fix --- .../Archives/ArchiveFactory.Async.cs | 8 +---- src/SharpCompress/Archives/ArchiveFactory.cs | 3 +- .../Archives/GZip/GZipArchive.Factory.cs | 3 +- .../Archives/Rar/RarArchive.Factory.cs | 3 +- .../SevenZip/SevenZipArchive.Factory.cs | 3 +- .../Archives/Tar/TarArchive.Factory.cs | 6 ++-- .../Archives/Zip/ZipArchive.Factory.cs | 3 +- .../StreamValidationExtensions.cs | 30 +++++-------------- src/SharpCompress/packages.lock.json | 12 ++++---- .../SharpCompress.Test/ReaderFactoryTests.cs | 4 ++- 10 files changed, 25 insertions(+), 50 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 6518952f..84bb671d 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -92,13 +92,7 @@ public static partial class ArchiveFactory ) { cancellationToken.ThrowIfCancellationRequested(); - var streamsArray = streams.RequireReadable(); - streamsArray.RequireSeekable(); - if (streamsArray.Count == 0) - { - throw new ArchiveOperationException("No streams"); - } - + var streamsArray = streams.RequireReadable().RequireSeekable().ToList(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 7f1834d1..34cc5bb1 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -73,8 +73,7 @@ public static partial class ArchiveFactory public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) { - var streamsArray = streams.RequireReadable(); - streamsArray.RequireSeekable(); + var streamsArray = streams.RequireReadable().RequireSeekable().ToList(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 9caa068e..2f5e5e33 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -76,8 +76,7 @@ public partial class GZipArchive ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new GZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 31d53b0a..2c333c5c 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -87,8 +87,7 @@ public partial class RarArchive ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new RarArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 161b0ed2..a338e363 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -71,8 +71,7 @@ public partial class SevenZipArchive ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new SevenZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 9d60a6e8..d166f8c8 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -66,8 +66,7 @@ public partial class TarArchive ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, @@ -155,8 +154,7 @@ public partial class TarArchive ) { cancellationToken.ThrowIfCancellationRequested(); - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index a015272a..1e02ad6f 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -67,8 +67,7 @@ public partial class ZipArchive ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new ZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs index 8c2b87db..28bd9116 100644 --- a/src/SharpCompress/StreamValidationExtensions.cs +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; namespace SharpCompress; internal static class StreamValidationExtensions { - internal static Stream RequireReadable(this Stream stream) + internal static void RequireReadable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -15,11 +14,9 @@ internal static class StreamValidationExtensions { throw new ArgumentException("Stream must be readable", nameof(stream)); } - - return stream; } - internal static Stream RequireSeekable(this Stream stream) + internal static void RequireSeekable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -27,11 +24,9 @@ internal static class StreamValidationExtensions { throw new ArgumentException("Stream must be seekable", nameof(stream)); } - - return stream; } - internal static Stream RequireWritable(this Stream stream) + internal static void RequireWritable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -39,32 +34,23 @@ internal static class StreamValidationExtensions { throw new ArgumentException("Stream must be writable", nameof(stream)); } - - return stream; } - internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) + internal static IEnumerable RequireSeekable(this IEnumerable streams) { - streams.NotNull(nameof(streams)); - foreach (var stream in streams) { stream.RequireSeekable(); + yield return stream; } - - return streams; } - internal static IReadOnlyList RequireReadable(this IEnumerable streams) + internal static IEnumerable RequireReadable(this IEnumerable streams) { - streams.NotNull(nameof(streams)); - - var streamArray = streams as IReadOnlyList ?? streams.ToArray(); - foreach (var stream in streamArray) + foreach (var stream in streams) { stream.RequireReadable(); + yield return stream; } - - return streamArray; } } diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index a401c702..e82196f8 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/ReaderFactoryTests.cs b/tests/SharpCompress.Test/ReaderFactoryTests.cs index f77fb2f9..2c5236c8 100644 --- a/tests/SharpCompress.Test/ReaderFactoryTests.cs +++ b/tests/SharpCompress.Test/ReaderFactoryTests.cs @@ -34,6 +34,8 @@ public class ReaderFactoryTests using var unreadable = new TestStream(new MemoryStream(), false, true, true); using var readable = new MemoryStream(); - Assert.Throws(() => RarReader.OpenReader([unreadable, readable])); + Assert.Throws(() => + RarReader.OpenReader([unreadable, readable]).MoveToNextEntry() + ); } } From 76300b9c970fba1874b13bbe19d5dde1a054df02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:38:36 +0000 Subject: [PATCH 60/74] Initial plan From a176ffee20fc2d6f4e683d49c0174ab0a436796a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:42:21 +0000 Subject: [PATCH 61/74] initial plan Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/324eaee6-e733-4193-b619-6eb75b435ec2 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/packages.lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index e82196f8..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.6, )", - "resolved": "10.0.6", - "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.26, )", - "resolved": "8.0.26", - "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From 6c59326628bd33f1c3438bfd4d9e8402c0682fef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 08:47:12 +0000 Subject: [PATCH 62/74] Add ArchiveInformation record and GetArchiveInformation API for consolidated archive detection Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/324eaee6-e733-4193-b619-6eb75b435ec2 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/ArchiveFactory.Async.cs | 48 +++++++++ src/SharpCompress/Archives/ArchiveFactory.cs | 38 +++++++ .../Archives/ArchiveInformation.cs | 22 ++++ src/SharpCompress/packages.lock.json | 12 +-- .../SharpCompress.Test/ArchiveFactoryTests.cs | 100 ++++++++++++++++++ 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/SharpCompress/Archives/ArchiveInformation.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 84bb671d..f2c945c6 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -110,6 +110,54 @@ public static partial class ArchiveFactory .ConfigureAwait(false); } + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + var isArchive = await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); + stream.Position = startPosition; + + if (isArchive) + { + return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + } + + return null; + } + internal static ValueTask FindFactoryAsync( string filePath, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 34cc5bb1..3767c7c9 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -176,6 +176,44 @@ public static partial class ArchiveFactory return false; } + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + public static ArchiveInformation? GetArchiveInformation(string filePath) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return GetArchiveInformation(stream); + } + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + public static ArchiveInformation? GetArchiveInformation(Stream stream) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + var isArchive = factory.IsArchive(stream); + stream.Position = startPosition; + + if (isArchive) + { + return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + } + + return null; + } + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/ArchiveInformation.cs b/src/SharpCompress/Archives/ArchiveInformation.cs new file mode 100644 index 00000000..adc0dfa2 --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveInformation.cs @@ -0,0 +1,22 @@ +using SharpCompress.Common; + +namespace SharpCompress.Archives; + +/// +/// Contains information about a detected archive, including its type and supported capabilities. +/// +/// +/// Use or +/// +/// to obtain an instance of this record. +/// +/// +/// The type of archive detected, or when the format is not a registered well-known type. +/// +/// +/// when this archive format supports random access via the API, +/// meaning the full file listing can be retrieved without decompressing the entire archive. +/// when only the API is available, +/// which reads entries sequentially and can only report per-entry progress. +/// +public record ArchiveInformation(ArchiveType? Type, bool SupportsRandomAccess); diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 03c03a9a..a401c702 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.22, )", + "resolved": "8.0.22", + "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index d6326bad..867561d9 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -159,6 +159,106 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(0, stream.Position); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public void GetArchiveInformation_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + public async ValueTask GetArchiveInformationAsync_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + Path.Combine(TEST_ARCHIVES_PATH, archiveName) + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Fact] + public void GetArchiveInformation_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.Null(info); + } + + [Fact] + public async ValueTask GetArchiveInformationAsync_ReturnsNull_ForNonArchive() + { + using var stream = new MemoryStream(Encoding.ASCII.GetBytes("not an archive")); + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.Null(info); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void GetArchiveInformation_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = ArchiveFactory.GetArchiveInformation(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public async ValueTask GetArchiveInformationAsync_Stream_PreservesPosition( + string archiveName, + ArchiveType expectedType + ) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 13); + var startPosition = stream.Position; + + var info = await ArchiveFactory.GetArchiveInformationAsync(stream); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(startPosition, stream.Position); + } + private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); From f118935bb8c895e132c4e48c77753b95d9291771 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:13:00 +0000 Subject: [PATCH 63/74] Consolidate archive detection into shared TryFindFactory helpers Agent-Logs-Url: https://github.com/adamhathcock/sharpcompress/sessions/9c5bf06d-3e2f-4bb5-9130-f5b7cdd1d0a0 Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/ArchiveFactory.Async.cs | 59 +++++++----- src/SharpCompress/Archives/ArchiveFactory.cs | 92 +++++++++---------- src/SharpCompress/packages.lock.json | 12 +-- 3 files changed, 81 insertions(+), 82 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index f2c945c6..a96bbefa 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -140,22 +140,10 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - stream.Position = startPosition; - - if (isArchive) - { - return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - } - - return null; + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); } internal static ValueTask FindFactoryAsync( @@ -188,14 +176,39 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var factories = Factory.Factories.OfType(); + // Use the shared async detection loop over all factories. If the matched factory + // implements T we return it; otherwise (or if nothing matched) we fall through + // to the same "unsupported format" exception that the original code produced, + // listing the T-typed factories as the hint for the caller. + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + if (factory is T typedFactory) + { + return typedFactory; + } + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); + + throw new ArchiveOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + + /// + /// Async counterpart of . + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + private static async ValueTask TryFindFactoryAsync( + Stream stream, + CancellationToken cancellationToken + ) + { var startPosition = stream.Position; - foreach (var factory in factories) + foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if ( await factory .IsArchiveAsync(stream, cancellationToken: cancellationToken) @@ -203,15 +216,11 @@ public static partial class ArchiveFactory ) { stream.Seek(startPosition, SeekOrigin.Begin); - return factory; } } - var extensions = string.Join(", ", factories.Select(item => item.Name)); - - throw new ArchiveOperationException( - $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" - ); + stream.Seek(startPosition, SeekOrigin.Begin); + return null; } } diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 3767c7c9..0f236a0e 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -123,23 +123,17 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var factories = Factory.Factories.OfType(); - - var startPosition = stream.Position; - - foreach (var factory in factories) + // Use the shared detection loop over all factories. If the matched factory + // implements T we return it; otherwise (or if nothing matched) we fall through + // to the same "unsupported format" exception that the original code produced, + // listing the T-typed factories as the hint for the caller. + var factory = TryFindFactory(stream); + if (factory is T typedFactory) { - stream.Seek(startPosition, SeekOrigin.Begin); - - if (factory.IsArchive(stream)) - { - stream.Seek(startPosition, SeekOrigin.Begin); - - return factory; - } + return typedFactory; } - var extensions = string.Join(", ", factories.Select(item => item.Name)); + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); throw new ArchiveOperationException( $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" @@ -155,25 +149,12 @@ public static partial class ArchiveFactory public static bool IsArchive(Stream stream, out ArchiveType? type) { - type = null; stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = factory.IsArchive(stream); - stream.Position = startPosition; - - if (isArchive) - { - type = factory.KnownArchiveType; - return true; - } - } - - return false; + var factory = TryFindFactory(stream); + type = factory?.KnownArchiveType; + return factory is not null; } /// @@ -198,19 +179,42 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); + var factory = TryFindFactory(stream); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + /// + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + /// + /// This is the shared, seekable-stream detection core used by + /// , , + /// and . + /// + /// uses a separate code path + /// based on rewindable buffering, which supports + /// non-seekable streams and is therefore not unified with this helper. + /// + /// + private static IFactory? TryFindFactory(Stream stream) + { var startPosition = stream.Position; foreach (var factory in Factory.Factories) { - var isArchive = factory.IsArchive(stream); - stream.Position = startPosition; - - if (isArchive) + stream.Seek(startPosition, SeekOrigin.Begin); + if (factory.IsArchive(stream)) { - return new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; } } + stream.Seek(startPosition, SeekOrigin.Begin); return null; } @@ -232,22 +236,8 @@ public static partial class ArchiveFactory stream.RequireReadable(); stream.RequireSeekable(); - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - var isArchive = await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - stream.Position = startPosition; - - if (isArchive) - { - return (true, factory.KnownArchiveType); - } - } - - return (false, null); + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return (factory is not null, factory?.KnownArchiveType); } public static IEnumerable GetFileParts(string part1) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index a401c702..03c03a9a 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", From d2c06e95d6b1d805907a5582def96bfe886abbf4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:07:11 +0100 Subject: [PATCH 64/74] Clean up and move detection to new file --- .../Archives/ArchiveFactory.Async.cs | 116 ----------- .../Archives/ArchiveFactory.Detection.cs | 187 ++++++++++++++++++ src/SharpCompress/Archives/ArchiveFactory.cs | 61 ------ 3 files changed, 187 insertions(+), 177 deletions(-) create mode 100644 src/SharpCompress/Archives/ArchiveFactory.Detection.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index a96bbefa..6c45cb1f 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -1,11 +1,9 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; -using SharpCompress.Factories; using SharpCompress.Readers; namespace SharpCompress.Archives; @@ -109,118 +107,4 @@ public static partial class ArchiveFactory .OpenAsyncArchive(streamsArray, options, cancellationToken) .ConfigureAwait(false); } - - /// - /// Returns information about the archive at the given file path asynchronously, - /// or if the file is not a recognized archive. - /// - /// Path to the archive file. - /// Cancellation token. - public static async ValueTask GetArchiveInformationAsync( - string filePath, - CancellationToken cancellationToken = default - ) - { - filePath.NotNullOrEmpty(nameof(filePath)); - using Stream stream = File.OpenRead(filePath); - return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); - } - - /// - /// Returns information about the archive in the given stream asynchronously, - /// or if the stream is not a recognized archive. - /// - /// A readable and seekable stream positioned at the start of the archive. - /// Cancellation token. - public static async ValueTask GetArchiveInformationAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - stream.RequireReadable(); - stream.RequireSeekable(); - - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - return factory is null - ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - - internal static ValueTask FindFactoryAsync( - string filePath, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - filePath.NotNullOrEmpty(nameof(filePath)); - return FindFactoryAsync(new FileInfo(filePath), cancellationToken); - } - - internal static async ValueTask FindFactoryAsync( - FileInfo finfo, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - finfo.NotNull(nameof(finfo)); - using Stream stream = finfo.OpenRead(); - return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - } - - internal static async ValueTask FindFactoryAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - where T : IFactory - { - stream.RequireReadable(); - stream.RequireSeekable(); - - // Use the shared async detection loop over all factories. If the matched factory - // implements T we return it; otherwise (or if nothing matched) we fall through - // to the same "unsupported format" exception that the original code produced, - // listing the T-typed factories as the hint for the caller. - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); - if (factory is T typedFactory) - { - return typedFactory; - } - - var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); - - throw new ArchiveOperationException( - $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" - ); - } - - /// - /// Async counterpart of . - /// Iterates all registered factories and returns the first one whose - /// recognises the stream, or . - /// Stream position is restored to its value at entry on both success and failure. - /// - private static async ValueTask TryFindFactoryAsync( - Stream stream, - CancellationToken cancellationToken - ) - { - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - stream.Seek(startPosition, SeekOrigin.Begin); - if ( - await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) - .ConfigureAwait(false) - ) - { - stream.Seek(startPosition, SeekOrigin.Begin); - return factory; - } - } - - stream.Seek(startPosition, SeekOrigin.Begin); - return null; - } } diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs new file mode 100644 index 00000000..ae04e09f --- /dev/null +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -0,0 +1,187 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Factories; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public static partial class ArchiveFactory +{ + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + CancellationToken cancellationToken = default + ) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + } + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + internal static ValueTask FindFactoryAsync( + string filePath, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + filePath.NotNullOrEmpty(nameof(filePath)); + return FindFactoryAsync(new FileInfo(filePath), cancellationToken); + } + + internal static async ValueTask FindFactoryAsync( + FileInfo fileInfo, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + fileInfo.NotNull(nameof(fileInfo)); + using Stream stream = fileInfo.OpenRead(); + return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + } + + internal static async ValueTask FindFactoryAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + where T : IFactory + { + stream.RequireReadable(); + stream.RequireSeekable(); + + // Use the shared async detection loop over all factories. If the matched factory + // implements T we return it; otherwise (or if nothing matched) we fall through + // to the same "unsupported format" exception that the original code produced, + // listing the T-typed factories as the hint for the caller. + var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + if (factory is T typedFactory) + { + return typedFactory; + } + + var extensions = string.Join(", ", Factory.Factories.OfType().Select(item => item.Name)); + + throw new ArchiveOperationException( + $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}" + ); + } + + /// + /// Async counterpart of . + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + private static async ValueTask TryFindFactoryAsync( + Stream stream, + CancellationToken cancellationToken + ) + { + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + if ( + await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false) + ) + { + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + public static ArchiveInformation? GetArchiveInformation(string filePath) + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return GetArchiveInformation(stream); + } + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + public static ArchiveInformation? GetArchiveInformation(Stream stream) + { + stream.RequireReadable(); + stream.RequireSeekable(); + + var factory = TryFindFactory(stream); + return factory is null + ? null + : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); + } + + /// + /// Iterates all registered factories and returns the first one whose + /// recognises the stream, or . + /// Stream position is restored to its value at entry on both success and failure. + /// + /// + /// This is the shared, seekable-stream detection core used by + /// , , + /// and . + /// + /// uses a separate code path + /// based on rewindable buffering, which supports + /// non-seekable streams and is therefore not unified with this helper. + /// + /// + private static IFactory? TryFindFactory(Stream stream) + { + var startPosition = stream.Position; + + foreach (var factory in Factory.Factories) + { + stream.Seek(startPosition, SeekOrigin.Begin); + if (factory.IsArchive(stream)) + { + stream.Seek(startPosition, SeekOrigin.Begin); + return factory; + } + } + + stream.Seek(startPosition, SeekOrigin.Begin); + return null; + } +} diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 0f236a0e..d6f08cc6 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -157,67 +157,6 @@ public static partial class ArchiveFactory return factory is not null; } - /// - /// Returns information about the archive at the given file path, - /// or if the file is not a recognized archive. - /// - /// Path to the archive file. - public static ArchiveInformation? GetArchiveInformation(string filePath) - { - filePath.NotNullOrEmpty(nameof(filePath)); - using Stream stream = File.OpenRead(filePath); - return GetArchiveInformation(stream); - } - - /// - /// Returns information about the archive in the given stream, - /// or if the stream is not a recognized archive. - /// - /// A readable and seekable stream positioned at the start of the archive. - public static ArchiveInformation? GetArchiveInformation(Stream stream) - { - stream.RequireReadable(); - stream.RequireSeekable(); - - var factory = TryFindFactory(stream); - return factory is null - ? null - : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); - } - - /// - /// Iterates all registered factories and returns the first one whose - /// recognises the stream, or . - /// Stream position is restored to its value at entry on both success and failure. - /// - /// - /// This is the shared, seekable-stream detection core used by - /// , , - /// and . - /// - /// uses a separate code path - /// based on rewindable buffering, which supports - /// non-seekable streams and is therefore not unified with this helper. - /// - /// - private static IFactory? TryFindFactory(Stream stream) - { - var startPosition = stream.Position; - - foreach (var factory in Factory.Factories) - { - stream.Seek(startPosition, SeekOrigin.Begin); - if (factory.IsArchive(stream)) - { - stream.Seek(startPosition, SeekOrigin.Begin); - return factory; - } - } - - stream.Seek(startPosition, SeekOrigin.Begin); - return null; - } - public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default From 4fc08534c2c902a93d854b554d5e732f8ec96a58 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:18:27 +0100 Subject: [PATCH 65/74] add detection test --- .../SharpCompress.Test/ArchiveFactoryTests.cs | 180 +++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 867561d9..26a62acd 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -203,6 +203,173 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] + [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] + [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] + [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] + [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] + [InlineData("WinZip26.zip", ArchiveType.Zip, true)] + [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public async ValueTask GetArchiveInformationAsync_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + [Fact] public void GetArchiveInformation_ReturnsNull_ForNonArchive() { @@ -261,7 +428,7 @@ public class ArchiveFactoryTests : TestBase private MemoryStream CreatePrefixedArchiveStream(string archiveName, int prefixLength) { - var archiveBytes = File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, archiveName)); + var archiveBytes = File.ReadAllBytes(GetTestArchivePath(archiveName)); var buffer = new byte[prefixLength + archiveBytes.Length]; archiveBytes.CopyTo(buffer, prefixLength); @@ -270,4 +437,15 @@ public class ArchiveFactoryTests : TestBase stream.Position = prefixLength; return stream; } + + private static string GetTestArchivePath(string archiveName) + { + var archivesPath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); + if (File.Exists(archivesPath)) + { + return archivesPath; + } + + return Path.GetFullPath(Path.Combine(TEST_ARCHIVES_PATH, "..", archiveName)); + } } From ac8203e957d65ab18795a3c051df5c9ec50ce122 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 26 Apr 2026 11:30:56 +0100 Subject: [PATCH 66/74] add sync tests --- .../SharpCompress.Test/ArchiveFactoryTests.cs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 26a62acd..f19496ee 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -181,6 +181,22 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public void GetArchiveInformation_WithLookForHeader_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName), true); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] @@ -203,6 +219,192 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); } + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] + public async ValueTask GetArchiveInformationAsync_WithLookForHeader_ReturnsExpectedInfo( + string archiveName, + ArchiveType expectedType, + bool expectedRandomAccess + ) + { + var info = await ArchiveFactory.GetArchiveInformationAsync( + GetTestArchivePath(archiveName), + true + ); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedRandomAccess, info.SupportsRandomAccess); + } + + [Theory] + [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARM64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ARMT.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BCJ2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.BZip2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Copy.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.EmptyStream.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Filters.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.IA64.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.LZMA2.Aes.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.PPMd.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.RISCV.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.SPARC.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.Tar.tar", ArchiveType.Tar, true)] + [InlineData("7Zip.Tar.tar.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.ZSTD.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.delta.distance.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.encryptedFiles.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.eos.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.1block.7z", ArchiveType.SevenZip, true)] + [InlineData("7Zip.solid.7z", ArchiveType.SevenZip, true)] + [InlineData("Ace.encrypted.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method1.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2-solid.ace", ArchiveType.Ace, false)] + [InlineData("Ace.method2.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.ace", ArchiveType.Ace, false)] + [InlineData("Ace.store.largefile.ace", ArchiveType.Ace, false)] + [InlineData("Arc.crunched.arc", ArchiveType.Arc, false)] + [InlineData("Arc.crunched.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squashed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.squeezed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.arc", ArchiveType.Arc, false)] + [InlineData("Arc.uncompressed.largefile.arc", ArchiveType.Arc, false)] + [InlineData("Arj.encrypted.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method1.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method2.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method3.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.arj", ArchiveType.Arj, false)] + [InlineData("Arj.method4.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.arj", ArchiveType.Arj, false)] + [InlineData("Arj.store.largefile.arj", ArchiveType.Arj, false)] + [InlineData("Issue_685.zip", ArchiveType.Zip, true)] + [InlineData("PrePostHeaders.zip", ArchiveType.Zip, true)] + [InlineData("Rar.Audio_program.rar", ArchiveType.Rar, true)] + [InlineData("Rar.Encrypted.rar", ArchiveType.Rar, true)] + [InlineData("Rar.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar.issue1050.rar", ArchiveType.Rar, true)] + [InlineData("Rar.malformed_512byte.rar", ArchiveType.Rar, true)] + [InlineData("Rar.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar.rar", ArchiveType.Rar, true)] + [InlineData("Rar.solid.rar", ArchiveType.Rar, true)] + [InlineData("Rar.test_invalid_exttime.rar", ArchiveType.Rar, true)] + [InlineData("Rar15.rar", ArchiveType.Rar, true)] + [InlineData("Rar2.rar", ArchiveType.Rar, true)] + [InlineData("Rar4.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.comment.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.crc_blake2.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesAndHeader.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.encrypted_filesOnly.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.none.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.rar", ArchiveType.Rar, true)] + [InlineData("Rar5.solid.rar", ArchiveType.Rar, true)] + [InlineData("Tar.ContainsRar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.ContainsTarGz.tar", ArchiveType.Tar, true)] + [InlineData("Tar.Empty.tar", ArchiveType.Tar, true)] + [InlineData("Tar.LongPathsWithLongNameExtension.tar", ArchiveType.Tar, true)] + [InlineData("Tar.mod.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.noEmptyDirs.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.oldgnu.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar", ArchiveType.Tar, true)] + [InlineData("Tar.tar.Z", ArchiveType.Tar, true)] + [InlineData("Tar.tar.bz2", ArchiveType.Tar, true)] + [InlineData("Tar.tar.gz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.lz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.xz", ArchiveType.Tar, true)] + [InlineData("Tar.tar.zst", ArchiveType.Tar, true)] + [InlineData("TarCorrupted.tar", ArchiveType.Tar, true)] + [InlineData("TarWithSymlink.tar.gz", ArchiveType.Tar, true)] + [InlineData("WinZip26.zip", ArchiveType.Zip, true)] + [InlineData("WinZip26_BZip2.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip26_LZMA.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_XZ.zipx", ArchiveType.Zip, true)] + [InlineData("WinZip27_ZSTD.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.644.zip", ArchiveType.Zip, true)] + [InlineData("Zip.EntryComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.Evil.zip", ArchiveType.Zip, true)] + [InlineData("Zip.LongComment.zip", ArchiveType.Zip, true)] + [InlineData("Zip.UnicodePathExtra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.badlocalextra.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.bzip2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.WinzipAES2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd-.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.mod2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.pkware.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate.zip", ArchiveType.Zip, true)] + [InlineData("Zip.deflate64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.implode.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.WinzipAES.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.empty.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.lzma.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.datadescriptors.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.encrypted.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.issue86.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.none.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.dd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.noEmptyDirs.zip", ArchiveType.Zip, true)] + [InlineData("Zip.ppmd.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce1.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce2.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce3.zip", ArchiveType.Zip, true)] + [InlineData("Zip.reduce4.zip", ArchiveType.Zip, true)] + [InlineData("Zip.shrink.zip", ArchiveType.Zip, true)] + [InlineData("Zip.uncompressed.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.compressedonly.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zip64.zip", ArchiveType.Zip, true)] + [InlineData("Zip.zipx", ArchiveType.Zip, true)] + [InlineData("Zip.zstd.WinzipAES.mixed.zip", ArchiveType.Zip, true)] + [InlineData("large_test.txt.Z", ArchiveType.Lzw, false)] + [InlineData("test_477.zip", ArchiveType.Zip, true)] + [InlineData("ustar with long names.tar", ArchiveType.Tar, true)] + [InlineData("very long filename.tar", ArchiveType.Tar, true)] + [InlineData("zipcrypto.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.AES.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted.zip", ArchiveType.Zip, true)] + [InlineData("SharpCompress.Encrypted2.zip", ArchiveType.Zip, true)] + public void GetArchiveInformation_DetectsSingleFileTestArchives( + string archiveName, + ArchiveType expectedType, + bool expectedSeekable + ) + { + var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName)); + + Assert.NotNull(info); + Assert.Equal(expectedType, info.Type); + Assert.Equal(expectedSeekable, info.SupportsRandomAccess); + } + [Theory] [InlineData("64bitstream.zip.7z", ArchiveType.SevenZip, true)] [InlineData("7Zip.ARM.7z", ArchiveType.SevenZip, true)] From aae3502a8be7856da367636f73661652a2fb0b08 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 27 Apr 2026 16:43:06 +0100 Subject: [PATCH 67/74] intermediate commit --- .../Archives/ArchiveFactory.Detection.cs | 107 ++++++++++++++++-- .../Archives/Rar/RarArchive.Factory.cs | 4 +- .../SevenZip/SevenZipArchive.Factory.cs | 71 ++++++++++-- src/SharpCompress/Factories/Factory.cs | 9 ++ src/SharpCompress/Factories/RarFactory.cs | 9 ++ .../Factories/SevenZipFactory.cs | 9 ++ .../SharpCompress.Test/ArchiveFactoryTests.cs | 12 +- 7 files changed, 194 insertions(+), 27 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index ae04e09f..c8b9f37d 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -19,11 +19,31 @@ public static partial class ArchiveFactory public static async ValueTask GetArchiveInformationAsync( string filePath, CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive at the given file path asynchronously, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return await GetArchiveInformationAsync(stream, cancellationToken).ConfigureAwait(false); + return await GetArchiveInformationAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); } /// @@ -35,12 +55,32 @@ public static partial class ArchiveFactory public static async ValueTask GetArchiveInformationAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + /// + /// Returns information about the archive in the given stream asynchronously, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + /// Cancellation token. + public static async ValueTask GetArchiveInformationAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); return factory is null ? null : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); @@ -102,6 +142,14 @@ public static partial class ArchiveFactory private static async ValueTask TryFindFactoryAsync( Stream stream, CancellationToken cancellationToken + ) => + await TryFindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + private static async ValueTask TryFindFactoryAsync( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken ) { var startPosition = stream.Position; @@ -109,11 +157,15 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if ( - await factory - .IsArchiveAsync(stream, cancellationToken: cancellationToken) + var isArchive = factory is Factory concreteFactory + ? await concreteFactory + .IsArchiveAsyncWithOptions(stream, readerOptions, cancellationToken) .ConfigureAwait(false) - ) + : await factory + .IsArchiveAsync(stream, readerOptions.Password, cancellationToken) + .ConfigureAwait(false); + + if (isArchive) { stream.Seek(startPosition, SeekOrigin.Begin); return factory; @@ -129,11 +181,23 @@ public static partial class ArchiveFactory /// or if the file is not a recognized archive. /// /// Path to the archive file. - public static ArchiveInformation? GetArchiveInformation(string filePath) + public static ArchiveInformation? GetArchiveInformation(string filePath) => + GetArchiveInformation(filePath, ReaderOptions.ForFilePath); + + /// + /// Returns information about the archive at the given file path, + /// or if the file is not a recognized archive. + /// + /// Path to the archive file. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + string filePath, + ReaderOptions? readerOptions + ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return GetArchiveInformation(stream); + return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath); } /// @@ -141,12 +205,24 @@ public static partial class ArchiveFactory /// or if the stream is not a recognized archive. /// /// A readable and seekable stream positioned at the start of the archive. - public static ArchiveInformation? GetArchiveInformation(Stream stream) + public static ArchiveInformation? GetArchiveInformation(Stream stream) => + GetArchiveInformation(stream, ReaderOptions.ForExternalStream); + + /// + /// Returns information about the archive in the given stream, + /// or if the stream is not a recognized archive. + /// + /// A readable and seekable stream positioned at the start of the archive. + /// Options controlling archive detection. + public static ArchiveInformation? GetArchiveInformation( + Stream stream, + ReaderOptions? readerOptions + ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = TryFindFactory(stream); + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); return factory is null ? null : new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory); @@ -167,14 +243,21 @@ public static partial class ArchiveFactory /// non-seekable streams and is therefore not unified with this helper. /// /// - private static IFactory? TryFindFactory(Stream stream) + private static IFactory? TryFindFactory(Stream stream) => + TryFindFactory(stream, ReaderOptions.ForExternalStream); + + private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions) { var startPosition = stream.Position; foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - if (factory.IsArchive(stream)) + var isArchive = factory is Factory concreteFactory + ? concreteFactory.IsArchiveWithOptions(stream, readerOptions) + : factory.IsArchive(stream, readerOptions.Password); + + if (isArchive) { stream.Seek(startPosition, SeekOrigin.Begin); return factory; diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 2c333c5c..a8a0448a 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -153,7 +153,7 @@ public partial class RarArchive { try { - MarkHeader.Read(stream, true, false); + MarkHeader.Read(stream, true, options?.LookForHeader ?? false); return true; } catch @@ -172,7 +172,7 @@ public partial class RarArchive try { await MarkHeader - .ReadAsync(stream, true, false, cancellationToken) + .ReadAsync(stream, true, options?.LookForHeader ?? false, cancellationToken) .ConfigureAwait(false); return true; } diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index a338e363..8c177a0f 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -143,11 +143,14 @@ public partial class SevenZipArchive return IsSevenZipFile(stream); } - public static bool IsSevenZipFile(Stream stream) + public static bool IsSevenZipFile(Stream stream) => + IsSevenZipFile(stream, ReaderOptions.ForExternalStream); + + public static bool IsSevenZipFile(Stream stream, ReaderOptions? readerOptions) { try { - return SignatureMatch(stream); + return SignatureMatch(stream, readerOptions?.LookForHeader ?? false); } catch { @@ -158,12 +161,25 @@ public partial class SevenZipArchive public static async ValueTask IsSevenZipFileAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsSevenZipFileAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask IsSevenZipFileAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); try { - return await SignatureMatchAsync(stream, cancellationToken).ConfigureAwait(false); + return await SignatureMatchAsync( + stream, + readerOptions?.LookForHeader ?? false, + cancellationToken + ) + .ConfigureAwait(false); } catch { @@ -173,13 +189,29 @@ public partial class SevenZipArchive private static ReadOnlySpan Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C]; - private static bool SignatureMatch(Stream stream) + private static bool SignatureMatch(Stream stream, bool lookForHeader) { var buffer = ArrayPool.Shared.Rent(6); try { - stream.ReadExact(buffer, 0, 6); - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) + { + stream.ReadExact(buffer, 0, 6); + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; + } + + return false; } finally { @@ -189,18 +221,39 @@ public partial class SevenZipArchive private static async ValueTask SignatureMatchAsync( Stream stream, + bool lookForHeader, CancellationToken cancellationToken ) { var buffer = ArrayPool.Shared.Rent(6); try { - if (!await stream.ReadFullyAsync(buffer, 0, 6, cancellationToken).ConfigureAwait(false)) + var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0; + for (var offset = 0; offset <= maxScanOffset; offset++) { - return false; + if ( + !await stream + .ReadFullyAsync(buffer, 0, 6, cancellationToken) + .ConfigureAwait(false) + ) + { + return false; + } + + if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature)) + { + return true; + } + + if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6) + { + return false; + } + + stream.Position -= 5; } - return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature); + return false; } finally { diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 8c8cbde2..633d3056 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -56,12 +56,21 @@ public abstract class Factory : IFactory /// public abstract bool IsArchive(Stream stream, string? password = null); + internal virtual bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + IsArchive(stream, readerOptions.Password); + public abstract ValueTask IsArchiveAsync( Stream stream, string? password = null, CancellationToken cancellationToken = default ); + internal virtual ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => IsArchiveAsync(stream, readerOptions.Password, cancellationToken); + /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 615f7fb4..2d7f1f86 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -34,6 +34,9 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade public override bool IsArchive(Stream stream, string? password = null) => RarArchive.IsRarFile(stream); + internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + RarArchive.IsRarFile(stream, readerOptions); + /// public override ValueTask IsArchiveAsync( Stream stream, @@ -41,6 +44,12 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade CancellationToken cancellationToken = default ) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken); + internal override ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => RarArchive.IsRarFileAsync(stream, readerOptions, cancellationToken); + /// public override FileInfo? GetFilePart(int index, FileInfo part1) => RarArchiveVolumeFactory.GetFilePart(index, part1); diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index adc26074..a2eb46c5 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -37,6 +37,9 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I public override bool IsArchive(Stream stream, string? password = null) => SevenZipArchive.IsSevenZipFile(stream); + internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + SevenZipArchive.IsSevenZipFile(stream, readerOptions); + /// public override ValueTask IsArchiveAsync( Stream stream, @@ -44,6 +47,12 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I CancellationToken cancellationToken = default ) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken); + internal override ValueTask IsArchiveAsyncWithOptions( + Stream stream, + ReaderOptions readerOptions, + CancellationToken cancellationToken = default + ) => SevenZipArchive.IsSevenZipFileAsync(stream, readerOptions, cancellationToken); + #endregion #region IArchiveFactory diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index f19496ee..314e0cff 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.Readers; using SharpCompress.Test.Mocks; using Xunit; @@ -184,13 +185,16 @@ public class ArchiveFactoryTests : TestBase [Theory] [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] - public void GetArchiveInformation_WithLookForHeader_ReturnsExpectedInfo( + public void GetArchiveInformation_WithReaderOptions_ReturnsExpectedInfo( string archiveName, ArchiveType expectedType, bool expectedRandomAccess ) { - var info = ArchiveFactory.GetArchiveInformation(GetTestArchivePath(archiveName), true); + var info = ArchiveFactory.GetArchiveInformation( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); Assert.NotNull(info); Assert.Equal(expectedType, info.Type); @@ -222,7 +226,7 @@ public class ArchiveFactoryTests : TestBase [Theory] [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip, true)] [InlineData("Rar.jpeg.jpg", ArchiveType.Rar, true)] - public async ValueTask GetArchiveInformationAsync_WithLookForHeader_ReturnsExpectedInfo( + public async ValueTask GetArchiveInformationAsync_WithReaderOptions_ReturnsExpectedInfo( string archiveName, ArchiveType expectedType, bool expectedRandomAccess @@ -230,7 +234,7 @@ public class ArchiveFactoryTests : TestBase { var info = await ArchiveFactory.GetArchiveInformationAsync( GetTestArchivePath(archiveName), - true + ReaderOptions.ForFilePath.WithLookForHeader(true) ); Assert.NotNull(info); From 472765b5e325530d9d8324cfc940be434c057cc2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Mon, 27 Apr 2026 17:17:31 +0100 Subject: [PATCH 68/74] fix up overloads and usages --- AGENTS.md | 1 + .../Archives/ArchiveFactory.Detection.cs | 14 ++-- src/SharpCompress/Archives/ArchiveFactory.cs | 48 ++++++++++++-- src/SharpCompress/Factories/AceFactory.cs | 4 +- src/SharpCompress/Factories/ArcFactory.cs | 4 +- src/SharpCompress/Factories/ArjFactory.cs | 4 +- src/SharpCompress/Factories/Factory.cs | 21 ++---- src/SharpCompress/Factories/GZipFactory.cs | 4 +- src/SharpCompress/Factories/IFactory.cs | 8 +-- src/SharpCompress/Factories/LzwFactory.cs | 4 +- src/SharpCompress/Factories/RarFactory.cs | 11 +--- .../Factories/SevenZipFactory.cs | 11 +--- src/SharpCompress/Factories/TarFactory.cs | 8 +-- .../Factories/ZStandardFactory.cs | 4 +- src/SharpCompress/Factories/ZipFactory.cs | 12 ++-- .../SharpCompress.Test/ArchiveFactoryTests.cs | 66 +++++++++++++++++++ tests/SharpCompress.Test/ReaderTests.cs | 3 +- 17 files changed, 149 insertions(+), 78 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a47121a0..79480854 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,6 +190,7 @@ SharpCompress supports multiple archive and compression formats: ### Validation Expectations - Run targeted tests for the changed area first. +- On non-Windows machines, avoid net48 test runs unless Mono is installed; use framework-specific validation such as `--framework net10.0` instead. - Run `dotnet csharpier format .` after code edits. - Run `dotnet csharpier check .` before handing off changes. diff --git a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs index c8b9f37d..a0084914 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Detection.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Detection.cs @@ -157,13 +157,9 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - var isArchive = factory is Factory concreteFactory - ? await concreteFactory - .IsArchiveAsyncWithOptions(stream, readerOptions, cancellationToken) - .ConfigureAwait(false) - : await factory - .IsArchiveAsync(stream, readerOptions.Password, cancellationToken) - .ConfigureAwait(false); + var isArchive = await factory + .IsArchiveAsync(stream, readerOptions, cancellationToken) + .ConfigureAwait(false); if (isArchive) { @@ -253,9 +249,7 @@ public static partial class ArchiveFactory foreach (var factory in Factory.Factories) { stream.Seek(startPosition, SeekOrigin.Begin); - var isArchive = factory is Factory concreteFactory - ? concreteFactory.IsArchiveWithOptions(stream, readerOptions) - : factory.IsArchive(stream, readerOptions.Password); + var isArchive = factory.IsArchive(stream, readerOptions); if (isArchive) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index d6f08cc6..49e2f9b4 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -141,18 +141,32 @@ public static partial class ArchiveFactory } public static bool IsArchive(string filePath, out ArchiveType? type) + { + return IsArchive(filePath, ReaderOptions.ForFilePath, out type); + } + + public static bool IsArchive( + string filePath, + ReaderOptions? readerOptions, + out ArchiveType? type + ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream s = File.OpenRead(filePath); - return IsArchive(s, out type); + return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type); } public static bool IsArchive(Stream stream, out ArchiveType? type) + { + return IsArchive(stream, ReaderOptions.ForExternalStream, out type); + } + + public static bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = TryFindFactory(stream); + var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream); type = factory?.KnownArchiveType; return factory is not null; } @@ -160,22 +174,48 @@ public static partial class ArchiveFactory public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( string filePath, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + string filePath, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { filePath.NotNullOrEmpty(nameof(filePath)); using Stream stream = File.OpenRead(filePath); - return await IsArchiveAsync(stream, cancellationToken).ConfigureAwait(false); + return await IsArchiveAsync( + stream, + readerOptions ?? ReaderOptions.ForFilePath, + cancellationToken + ) + .ConfigureAwait(false); } public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( Stream stream, CancellationToken cancellationToken = default + ) => + await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken) + .ConfigureAwait(false); + + public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync( + Stream stream, + ReaderOptions? readerOptions, + CancellationToken cancellationToken = default ) { stream.RequireReadable(); stream.RequireSeekable(); - var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); + var factory = await TryFindFactoryAsync( + stream, + readerOptions ?? ReaderOptions.ForExternalStream, + cancellationToken + ) + .ConfigureAwait(false); return (factory is not null, factory?.KnownArchiveType); } diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs index f2fdfaa6..5117045e 100644 --- a/src/SharpCompress/Factories/AceFactory.cs +++ b/src/SharpCompress/Factories/AceFactory.cs @@ -23,12 +23,12 @@ public class AceFactory : Factory, IReaderFactory yield return "ace"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => AceHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => AceHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 46c12666..cbc95e01 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -25,7 +25,7 @@ public class ArcFactory : Factory, IReaderFactory yield return "arc"; } - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { //You may have to use some(paranoid) checks to ensure that you actually are //processing an ARC file, since other archivers also adopted the idea of putting @@ -63,7 +63,7 @@ public class ArcFactory : Factory, IReaderFactory public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index d599d81d..de1a8382 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -23,12 +23,12 @@ public class ArjFactory : Factory, IReaderFactory yield return "arj"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ArjHeader.IsArchive(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ArjHeader.IsArchiveAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 633d3056..115c6e49 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -54,22 +54,12 @@ public abstract class Factory : IFactory public abstract IEnumerable GetSupportedExtensions(); /// - public abstract bool IsArchive(Stream stream, string? password = null); - - internal virtual bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => - IsArchive(stream, readerOptions.Password); - + public abstract bool IsArchive(Stream stream, ReaderOptions readerOptions); public abstract ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ); - - internal virtual ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default - ) => IsArchiveAsync(stream, readerOptions.Password, cancellationToken); + ); /// public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null; @@ -95,7 +85,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if (IsArchive(stream, options.Password)) + if (IsArchive(stream, options)) { stream.Rewind(true); reader = readerFactory.OpenReader(stream, options); @@ -115,10 +105,7 @@ public abstract class Factory : IFactory if (this is IReaderFactory readerFactory) { stream.Rewind(); - if ( - await IsArchiveAsync(stream, options.Password, cancellationToken) - .ConfigureAwait(false) - ) + if (await IsArchiveAsync(stream, options, cancellationToken).ConfigureAwait(false)) { stream.Rewind(true); return await readerFactory diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index c49b8f8e..dd870d51 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -44,13 +44,13 @@ public class GZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => GZipArchive.IsGZipFile(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs index 2f0b1ab0..76bddb64 100644 --- a/src/SharpCompress/Factories/IFactory.cs +++ b/src/SharpCompress/Factories/IFactory.cs @@ -37,18 +37,18 @@ public interface IFactory /// Returns true if the stream represents an archive of the format defined by this type. /// /// A stream, pointing to the beginning of the archive. - /// optional password - bool IsArchive(Stream stream, string? password = null); + /// Options controlling archive detection. + bool IsArchive(Stream stream, ReaderOptions readerOptions); /// /// Returns true if the stream represents an archive of the format defined by this type asynchronously. /// /// A stream, pointing to the beginning of the archive. - /// optional password + /// Options controlling archive detection. /// cancellation token ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ); diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs index cbfabb60..5bc333c6 100644 --- a/src/SharpCompress/Factories/LzwFactory.cs +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -32,13 +32,13 @@ public class LzwFactory : Factory, IReaderFactory } /// - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => LzwStream.IsLzwStream(stream); /// public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => LzwStream.IsLzwStreamAsync(stream, cancellationToken); diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 2d7f1f86..2efc920d 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -31,20 +31,11 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade } /// - public override bool IsArchive(Stream stream, string? password = null) => - RarArchive.IsRarFile(stream); - - internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => RarArchive.IsRarFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken); - - internal override ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index a2eb46c5..bac7b533 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -34,20 +34,11 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I } /// - public override bool IsArchive(Stream stream, string? password = null) => - SevenZipArchive.IsSevenZipFile(stream); - - internal override bool IsArchiveWithOptions(Stream stream, ReaderOptions readerOptions) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => SevenZipArchive.IsSevenZipFile(stream, readerOptions); /// public override ValueTask IsArchiveAsync( - Stream stream, - string? password = null, - CancellationToken cancellationToken = default - ) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken); - - internal override ValueTask IsArchiveAsyncWithOptions( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 56d0a73b..eaa6e706 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -48,9 +48,9 @@ public class TarFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) @@ -78,11 +78,11 @@ public class TarFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { - var providers = CompressionProviderRegistry.Default; + var providers = readerOptions.Providers; var sharpCompressStream = new SharpCompressStream(stream); sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize); foreach (var wrapper in TarWrapper.Wrappers) diff --git a/src/SharpCompress/Factories/ZStandardFactory.cs b/src/SharpCompress/Factories/ZStandardFactory.cs index e45b2454..6ee920dc 100644 --- a/src/SharpCompress/Factories/ZStandardFactory.cs +++ b/src/SharpCompress/Factories/ZStandardFactory.cs @@ -21,12 +21,12 @@ internal class ZStandardFactory : Factory yield return "zstd"; } - public override bool IsArchive(Stream stream, string? password = null) => + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) => ZStandardStream.IsZStandard(stream); public override ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) => ZStandardStream.IsZStandardAsync(stream, cancellationToken); } diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index b5dbdff3..a9f97052 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -43,10 +43,10 @@ public class ZipFactory } /// - public override bool IsArchive(Stream stream, string? password = null) + public override bool IsArchive(Stream stream, ReaderOptions readerOptions) { var startPosition = stream.CanSeek ? stream.Position : -1; - if (ZipArchive.IsZipFile(stream, password)) + if (ZipArchive.IsZipFile(stream, readerOptions.Password)) { return true; } @@ -61,7 +61,7 @@ public class ZipFactory stream.Position = startPosition; //test the zip (last) file of a multipart zip - if (ZipArchive.IsZipMulti(stream, password)) + if (ZipArchive.IsZipMulti(stream, readerOptions.Password)) { return true; } @@ -74,7 +74,7 @@ public class ZipFactory /// public override async ValueTask IsArchiveAsync( Stream stream, - string? password = null, + ReaderOptions readerOptions, CancellationToken cancellationToken = default ) { @@ -84,7 +84,7 @@ public class ZipFactory // probe for single volume zip if ( await ZipArchive - .IsZipFileAsync(stream, password, cancellationToken) + .IsZipFileAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { @@ -102,7 +102,7 @@ public class ZipFactory //test the zip (last) file of a multipart zip if ( await ZipArchive - .IsZipMultiAsync(stream, password, cancellationToken) + .IsZipMultiAsync(stream, readerOptions.Password, cancellationToken) .ConfigureAwait(false) ) { diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 314e0cff..ba9acd7e 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -112,6 +112,72 @@ public class ArchiveFactoryTests : TestBase ); } + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + [InlineData("Rar.rar", ArchiveType.Rar)] + [InlineData("7Zip.nonsolid.7z", ArchiveType.SevenZip)] + public void IsArchive_String_ReturnsExpectedType(string archiveName, ArchiveType expectedType) + { + var result = ArchiveFactory.IsArchive( + Path.Combine(TEST_ARCHIVES_PATH, archiveName), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("Zip.deflate.zip", ArchiveType.Zip)] + [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] + public void IsArchive_Stream_PreservesPosition(string archiveName, ArchiveType expectedType) + { + using var stream = CreatePrefixedArchiveStream(archiveName, 11); + var startPosition = stream.Position; + + var result = ArchiveFactory.IsArchive(stream, out var type); + + Assert.True(result); + Assert.Equal(expectedType, type); + Assert.Equal(startPosition, stream.Position); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public void IsArchive_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = ArchiveFactory.IsArchive( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true), + out var type + ); + + Assert.True(result); + Assert.Equal(expectedType, type); + } + + [Theory] + [InlineData("7Zip.LZMA2.exe", ArchiveType.SevenZip)] + [InlineData("Rar.jpeg.jpg", ArchiveType.Rar)] + public async ValueTask IsArchiveAsync_WithReaderOptions_ReturnsExpectedType( + string archiveName, + ArchiveType expectedType + ) + { + var result = await ArchiveFactory.IsArchiveAsync( + GetTestArchivePath(archiveName), + ReaderOptions.ForFilePath.WithLookForHeader(true) + ); + + Assert.True(result.IsArchive); + Assert.Equal(expectedType, result.Type); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 72ab5f47..4c9024c9 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -103,7 +103,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), - null, + ReaderOptions.ForExternalStream, cancellationToken ) ) @@ -112,6 +112,7 @@ public abstract class ReaderTests : TestBase ( await factory.IsArchiveAsync( new FileInfo(testArchive).OpenRead(), + ReaderOptions.ForExternalStream, cancellationToken: cancellationToken ) ) From 69ef2b2a3b6ca51a06c88f9b66af102cbd71e379 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 6 May 2026 10:10:02 +0100 Subject: [PATCH 69/74] merge fixes --- src/SharpCompress/packages.lock.json | 12 ++++---- tests/SharpCompress.Test/packages.lock.json | 31 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 5059aafe..7d5f0448 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -442,9 +442,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index 9a219922..ca6ffc35 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -309,6 +309,30 @@ } } }, + ".NETFramework,Version=v4.8/win-x86": { + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + } + }, "net10.0": { "AwesomeAssertions": { "type": "Direct", @@ -521,6 +545,13 @@ "resolved": "8.0.0", "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" } + }, + "net10.0/win-x86": { + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + } } } } \ No newline at end of file From 005c269bbeec6b13b25a212c08b487750446dd26 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 6 May 2026 10:17:36 +0100 Subject: [PATCH 70/74] Merge fixes --- src/SharpCompress/Common/IEntry.Extensions.cs | 71 ----------------- .../Common/IEntryExtensions.Async.cs | 13 +-- src/SharpCompress/Common/IEntryExtensions.cs | 79 ++++++++++++++++--- 3 files changed, 67 insertions(+), 96 deletions(-) delete mode 100644 src/SharpCompress/Common/IEntry.Extensions.cs diff --git a/src/SharpCompress/Common/IEntry.Extensions.cs b/src/SharpCompress/Common/IEntry.Extensions.cs deleted file mode 100644 index 7f0cda43..00000000 --- a/src/SharpCompress/Common/IEntry.Extensions.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.IO; - -namespace SharpCompress.Common; - -internal static class EntryExtensions -{ - internal static void PreserveExtractionOptions( - this IEntry entry, - string destinationFileName, - ExtractionOptions options - ) - { - if (options.PreserveFileTime || options.PreserveAttributes) - { - var nf = new FileInfo(destinationFileName); - if (!nf.Exists) - { - return; - } - - // update file time to original packed time - if (options.PreserveFileTime) - { - if (entry.CreatedTime.HasValue) - { - try - { - nf.CreationTime = entry.CreatedTime.Value; - } - catch - { - // Invalid time or the OS rejected - } - } - - if (entry.LastModifiedTime.HasValue) - { - try - { - nf.LastWriteTime = entry.LastModifiedTime.Value; - } - catch - { - // Invalid time or the OS rejected - } - } - - if (entry.LastAccessedTime.HasValue) - { - try - { - nf.LastAccessTime = entry.LastAccessedTime.Value; - } - catch - { - // Invalid time or the OS rejected - } - } - } - - if (options.PreserveAttributes) - { - if (entry.Attrib.HasValue) - { - nf.Attributes = (FileAttributes) - System.Enum.ToObject(typeof(FileAttributes), entry.Attrib.Value); - } - } - } - } -} diff --git a/src/SharpCompress/Common/IEntryExtensions.Async.cs b/src/SharpCompress/Common/IEntryExtensions.Async.cs index fabf4334..7b42f1c8 100644 --- a/src/SharpCompress/Common/IEntryExtensions.Async.cs +++ b/src/SharpCompress/Common/IEntryExtensions.Async.cs @@ -86,18 +86,7 @@ internal static partial class IEntryExtensions options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (options.SymbolicLinkHandler is not null) - { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - else - { - ExtractionOptions.DefaultSymbolicLinkHandler( - destinationFileName, - entry.LinkTarget - ); - } - return; + options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget); } else { diff --git a/src/SharpCompress/Common/IEntryExtensions.cs b/src/SharpCompress/Common/IEntryExtensions.cs index 813ebfe3..feaf8e51 100644 --- a/src/SharpCompress/Common/IEntryExtensions.cs +++ b/src/SharpCompress/Common/IEntryExtensions.cs @@ -107,19 +107,7 @@ internal static partial class IEntryExtensions options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (options.SymbolicLinkHandler is not null) - { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); - } - else - { - ExtractionOptions.DefaultSymbolicLinkHandler( - destinationFileName, - entry.LinkTarget - ); - } - - return; + options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget); } else { @@ -134,5 +122,70 @@ internal static partial class IEntryExtensions entry.PreserveExtractionOptions(destinationFileName, options); } } + + + internal void PreserveExtractionOptions( + string destinationFileName, + ExtractionOptions options + ) + { + if (options.PreserveFileTime || options.PreserveAttributes) + { + var nf = new FileInfo(destinationFileName); + if (!nf.Exists) + { + return; + } + + // update file time to original packed time + if (options.PreserveFileTime) + { + if (entry.CreatedTime.HasValue) + { + try + { + nf.CreationTime = entry.CreatedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } + } + + if (entry.LastModifiedTime.HasValue) + { + try + { + nf.LastWriteTime = entry.LastModifiedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } + } + + if (entry.LastAccessedTime.HasValue) + { + try + { + nf.LastAccessTime = entry.LastAccessedTime.Value; + } + catch + { + // Invalid time or the OS rejected + } + } + } + + if (options.PreserveAttributes) + { + if (entry.Attrib.HasValue) + { + nf.Attributes = (FileAttributes) + Enum.ToObject(typeof(FileAttributes), entry.Attrib.Value); + } + } + } + } } } From f92d86fbf340283c83f6772ad4bdcadc5caedeb1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 6 May 2026 10:18:27 +0100 Subject: [PATCH 71/74] remove net 7 and 9 --- src/SharpCompress/Common/IEntryExtensions.cs | 1 - src/SharpCompress/SharpCompress.csproj | 2 +- src/SharpCompress/packages.lock.json | 84 -------------------- 3 files changed, 1 insertion(+), 86 deletions(-) diff --git a/src/SharpCompress/Common/IEntryExtensions.cs b/src/SharpCompress/Common/IEntryExtensions.cs index feaf8e51..77c5207c 100644 --- a/src/SharpCompress/Common/IEntryExtensions.cs +++ b/src/SharpCompress/Common/IEntryExtensions.cs @@ -123,7 +123,6 @@ internal static partial class IEntryExtensions } } - internal void PreserveExtractionOptions( string destinationFileName, ExtractionOptions options diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index bcde6224..991eccb9 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -6,7 +6,7 @@ 0.0.0.0 0.0.0.0 Adam Hathcock - net48;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0;net9.0;net10.0 + net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net10.0 SharpCompress ../../SharpCompress.snk true diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 03c03a9a..fdba2fb1 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -355,48 +355,6 @@ "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" } }, - "net7.0": { - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" - } - }, - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[10.0.102, )", - "resolved": "10.0.102", - "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.102", - "Microsoft.SourceLink.Common": "10.0.102" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" - }, - "Microsoft.NETFramework.ReferenceAssemblies.net461": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" - }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" - } - }, "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", @@ -444,48 +402,6 @@ "resolved": "10.0.102", "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" } - }, - "net9.0": { - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3" - } - }, - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[10.0.102, )", - "resolved": "10.0.102", - "contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.102", - "Microsoft.SourceLink.Common": "10.0.102" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" - }, - "Microsoft.NETFramework.ReferenceAssemblies.net461": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA==" - }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "10.0.102", - "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" - } } } } \ No newline at end of file From a05465a13d667e9e5ba0629db4dd393ac64398d0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 13 May 2026 11:49:26 +0100 Subject: [PATCH 72/74] Some clean up of unused vars --- .gitignore | 2 +- .../Common/IEntryExtensions.Async.cs | 2 +- src/SharpCompress/Common/IEntryExtensions.cs | 2 +- .../Rar/UnpackV1/Unpack50.Async.cs | 4 +- .../Compressors/Rar/UnpackV1/Unpack50.cs | 4 +- .../Compressors/Rar/UnpackV2017/unpack_hpp.cs | 48 +++---------------- .../Compressors/Rar/VM/BitInput.cs | 3 +- src/SharpCompress/packages.lock.json | 12 ++--- tests/SharpCompress.Test/packages.lock.json | 31 ------------ 9 files changed, 21 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 80f4154a..6a1f0613 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ profiler-snapshots/ .DS_Store *.snupkg benchmark-results/ -/.opencode +.opencode/ diff --git a/src/SharpCompress/Common/IEntryExtensions.Async.cs b/src/SharpCompress/Common/IEntryExtensions.Async.cs index 7b42f1c8..4ef697e0 100644 --- a/src/SharpCompress/Common/IEntryExtensions.Async.cs +++ b/src/SharpCompress/Common/IEntryExtensions.Async.cs @@ -9,7 +9,7 @@ internal static partial class IEntryExtensions { extension(IEntry entry) { - public async ValueTask WriteEntryToDirectoryAsync( + internal async ValueTask WriteEntryToDirectoryAsync( string destinationDirectory, ExtractionOptions? options, Func writeAsync, diff --git a/src/SharpCompress/Common/IEntryExtensions.cs b/src/SharpCompress/Common/IEntryExtensions.cs index 77c5207c..d3158f03 100644 --- a/src/SharpCompress/Common/IEntryExtensions.cs +++ b/src/SharpCompress/Common/IEntryExtensions.cs @@ -10,7 +10,7 @@ internal static partial class IEntryExtensions /// /// Extract to specific directory, retaining filename /// - public void WriteEntryToDirectory( + internal void WriteEntryToDirectory( string destinationDirectory, ExtractionOptions? options, Action write diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs index 42f7d439..ecc56576 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs @@ -239,7 +239,7 @@ internal partial class Unpack { Header.HeaderSize = 0; - if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 7) + if (Inp.InAddr > ReadTop - 7) { if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false)) { @@ -292,7 +292,7 @@ internal partial class Unpack CancellationToken cancellationToken = default ) { - if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 16) + if ( Inp.InAddr > ReadTop - 16) { if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false)) { diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs index e3352cfc..eef8882f 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs @@ -372,7 +372,7 @@ internal partial class Unpack private bool ReadFilter(UnpackFilter Filter) { - if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 16) + if ( Inp.InAddr > ReadTop - 16) { if (!UnpReadBuf()) { @@ -762,7 +762,7 @@ internal partial class Unpack { Header.HeaderSize = 0; - if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 7) + if (Inp.InAddr > ReadTop - 7) { if (!UnpReadBuf()) { diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs index cdd584a0..a37a867c 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs @@ -156,40 +156,25 @@ if (Decoded!=NULL) //struct UnpackFilter internal class UnpackFilter { - public byte Type; - public uint BlockStart; - public uint BlockLength; - public byte Channels; + internal byte Type; + internal uint BlockStart; + internal uint BlockLength; + internal byte Channels; // uint Width; // byte PosR; - public bool NextWindow; + internal bool NextWindow; }; -//struct UnpackFilter30 -internal class UnpackFilter30 -{ - public uint BlockStart; - public uint BlockLength; - public bool NextWindow; - - // Position of parent filter in Filters array used as prototype for filter - // in PrgStack array. Not defined for filters in Filters array. - public uint ParentFilter; - - /*#if !RarV2017_RAR5ONLY - public VM_PreparedProgram Prg; - #endif*/ -}; internal class AudioVariables // For RAR 2.0 archives only. { - public int K1, + internal int K1, K2, K3, K4, K5; - public int D1, + internal int D1, D2, D3, D4; @@ -369,7 +354,6 @@ internal partial class Unpack #endif*/ private int PPMEscChar; - private readonly byte[] UnpOldTable = new byte[HUFF_TABLE_SIZE30]; // If we already read decoding tables for Unpack v2,v3,v5. // We should not use a single variable for all algorithm versions, @@ -379,25 +363,7 @@ internal partial class Unpack private bool TablesRead2, TablesRead5; - // Virtual machine to execute filters code. - /*#if !RarV2017_RAR5ONLY - RarVM VM; - #endif*/ - // Buffer to read VM filters code. We moved it here from AddVMCode - // function to reduce time spent in BitInput constructor. - private readonly BitInput VMCodeInp = new(true); - - // Filters code, one entry per filter. - private readonly List Filters30 = new(); - - // Filters stack, several entrances of same filter are possible. - private readonly List PrgStack = new(); - - // Lengths of preceding data blocks, one length of one last block - // for every filter. Used to reduce the size required to write - // the data block length if lengths are repeating. - private readonly List OldFilterLengths = new(); /*#if RarV2017_RAR_SMP // More than 8 threads are unlikely to provide a noticeable gain diff --git a/src/SharpCompress/Compressors/Rar/VM/BitInput.cs b/src/SharpCompress/Compressors/Rar/VM/BitInput.cs index 8cd82056..a35e05fb 100644 --- a/src/SharpCompress/Compressors/Rar/VM/BitInput.cs +++ b/src/SharpCompress/Compressors/Rar/VM/BitInput.cs @@ -22,8 +22,7 @@ internal class BitInput : IDisposable get => inBit; set => inBit = value; } - public bool ExternalBuffer; - private byte[] _privateBuffer = ArrayPool.Shared.Rent(MAX_SIZE); + private readonly byte[] _privateBuffer = ArrayPool.Shared.Rent(MAX_SIZE); private bool _disposed; /// diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index fdba2fb1..f522434d 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.5, )", - "resolved": "10.0.5", - "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -358,9 +358,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index ca6ffc35..9a219922 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -309,30 +309,6 @@ } } }, - ".NETFramework,Version=v4.8/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - } - }, "net10.0": { "AwesomeAssertions": { "type": "Direct", @@ -545,13 +521,6 @@ "resolved": "8.0.0", "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" } - }, - "net10.0/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" - } } } } \ No newline at end of file From ccb3aafb30ac123a4af0c16ae0bfb4fd1e25480d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 13 May 2026 13:16:35 +0100 Subject: [PATCH 73/74] AI clean up for tests --- tests/SharpCompress.Test/TempDirectory.cs | 83 +++++++++++++++++++++++ tests/SharpCompress.Test/TestBase.cs | 67 +++++------------- 2 files changed, 101 insertions(+), 49 deletions(-) create mode 100644 tests/SharpCompress.Test/TempDirectory.cs diff --git a/tests/SharpCompress.Test/TempDirectory.cs b/tests/SharpCompress.Test/TempDirectory.cs new file mode 100644 index 00000000..e155210f --- /dev/null +++ b/tests/SharpCompress.Test/TempDirectory.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.Test; + +internal sealed class TempDirectory : IAsyncDisposable +{ + private const int MaxDeleteAttempts = 5; + private static readonly TimeSpan DeleteRetryDelay = TimeSpan.FromMilliseconds(100); + + public TempDirectory(string prefix) + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"{prefix}.{Guid.NewGuid():N}"); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public string GetDirectory(string name) + { + var path = System.IO.Path.Combine(Path, name); + Directory.CreateDirectory(path); + return path; + } + + public string CreateDirectory(string name) + { + var path = System.IO.Path.Combine(Path, name, System.IO.Path.GetRandomFileName()); + Directory.CreateDirectory(path); + return path; + } + + public void ResetDirectory(string name) + { + DeleteDirectory(System.IO.Path.Combine(Path, name)); + Directory.CreateDirectory(System.IO.Path.Combine(Path, name)); + } + + public async ValueTask DisposeAsync() + { + for (var attempt = 1; attempt <= MaxDeleteAttempts; attempt++) + { + try + { + DeleteDirectory(Path); + if (Directory.Exists(Path)) + { + throw new IOException( + $"Temp test directory '{Path}' still exists after deletion." + ); + } + + return; + } + catch (Exception ex) + when (IsRetryableDeleteException(ex) && attempt < MaxDeleteAttempts) + { + await Task.Delay(DeleteRetryDelay).ConfigureAwait(false); + } + catch (Exception ex) when (IsRetryableDeleteException(ex)) + { + throw new InvalidOperationException( + $"Failed to clean up temp test directory '{Path}'.", + ex + ); + } + } + + throw new InvalidOperationException($"Temp test directory '{Path}' was not cleaned up."); + } + + private static void DeleteDirectory(string path) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + + private static bool IsRetryableDeleteException(Exception ex) => + ex is IOException or UnauthorizedAccessException; +} diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs index 0cdd6a07..1c244ac8 100644 --- a/tests/SharpCompress.Test/TestBase.cs +++ b/tests/SharpCompress.Test/TestBase.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using SharpCompress.Readers; using Xunit; @@ -31,70 +30,40 @@ public class TestBase : IAsyncDisposable MISC_TEST_FILES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "MiscTest"); } - private readonly Guid _testGuid = Guid.NewGuid(); - private readonly string _testTempDirectory; + private readonly TempDirectory _tempDirectory; protected readonly string SCRATCH_FILES_PATH; protected readonly string SCRATCH2_FILES_PATH; protected TestBase() { - _testTempDirectory = Path.Combine(Path.GetTempPath(), $"SharpCompress.Test.{_testGuid:N}"); - SCRATCH_FILES_PATH = Path.Combine(_testTempDirectory, "Scratch"); - SCRATCH2_FILES_PATH = Path.Combine(_testTempDirectory, "Scratch2"); - - Directory.CreateDirectory(SCRATCH_FILES_PATH); - Directory.CreateDirectory(SCRATCH2_FILES_PATH); + _tempDirectory = new TempDirectory("SharpCompress.Test"); + SCRATCH_FILES_PATH = _tempDirectory.GetDirectory("Scratch"); + SCRATCH2_FILES_PATH = _tempDirectory.GetDirectory("Scratch2"); } - //always use async dispose since we have I/O and sync Dispose doesn't wait when using xunit - public async ValueTask DisposeAsync() - { - await Task.CompletedTask; - DeleteScratchDirectory(_testTempDirectory); - } + // Always use async dispose since we have I/O and sync Dispose doesn't wait when using xunit. + public ValueTask DisposeAsync() => _tempDirectory.DisposeAsync(); public void CleanScratch() { - ResetScratchDirectory(SCRATCH_FILES_PATH); - ResetScratchDirectory(SCRATCH2_FILES_PATH); + _tempDirectory.ResetDirectory("Scratch"); + _tempDirectory.ResetDirectory("Scratch2"); } - private static void ResetScratchDirectory(string path) - { - if (Directory.Exists(path)) - { - Directory.Delete(path, true); - } + protected string CreateScratchDirectory(string name) => + _tempDirectory.CreateDirectory(Path.Combine("Scratch", name)); - Directory.CreateDirectory(path); - } + protected string CreateScratch2Directory(string name) => + _tempDirectory.CreateDirectory(Path.Combine("Scratch2", name)); - private static void DeleteScratchDirectory(string path) - { - if (!Directory.Exists(path)) - { - return; - } + protected string GetScratchPath(params string[] parts) => + CombinePath(SCRATCH_FILES_PATH, parts); - try - { - Directory.Delete(path, true); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) - { - throw new InvalidOperationException( - $"Failed to clean up temp test directory '{path}'.", - ex - ); - } + protected string GetScratch2Path(params string[] parts) => + CombinePath(SCRATCH2_FILES_PATH, parts); - if (Directory.Exists(path)) - { - throw new InvalidOperationException( - $"Temp test directory '{path}' was not cleaned up." - ); - } - } + private static string CombinePath(string root, string[] parts) => + parts.Length == 0 ? root : Path.Combine(root, Path.Combine(parts)); public void VerifyFiles() { From 2352f7918ce6a769c08d94a2944c17c6641e4497 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 13 May 2026 13:16:54 +0100 Subject: [PATCH 74/74] format --- src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs | 2 +- src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs | 2 +- src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs index ecc56576..5ade0c6b 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.Async.cs @@ -292,7 +292,7 @@ internal partial class Unpack CancellationToken cancellationToken = default ) { - if ( Inp.InAddr > ReadTop - 16) + if (Inp.InAddr > ReadTop - 16) { if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false)) { diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs index eef8882f..496b6204 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack50.cs @@ -372,7 +372,7 @@ internal partial class Unpack private bool ReadFilter(UnpackFilter Filter) { - if ( Inp.InAddr > ReadTop - 16) + if (Inp.InAddr > ReadTop - 16) { if (!UnpReadBuf()) { diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs index a37a867c..6bc3e1d5 100644 --- a/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs +++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/unpack_hpp.cs @@ -166,7 +166,6 @@ internal class UnpackFilter internal bool NextWindow; }; - internal class AudioVariables // For RAR 2.0 archives only. { internal int K1, @@ -354,7 +353,6 @@ internal partial class Unpack #endif*/ private int PPMEscChar; - // If we already read decoding tables for Unpack v2,v3,v5. // We should not use a single variable for all algorithm versions, // because we can have a corrupt archive with one algorithm file @@ -363,8 +361,6 @@ internal partial class Unpack private bool TablesRead2, TablesRead5; - - /*#if RarV2017_RAR_SMP // More than 8 threads are unlikely to provide a noticeable gain // for unpacking, but would use the additional memory.