diff --git a/docs/API.md b/docs/API.md index 5696961d7..fdef287a2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -190,7 +190,7 @@ await using (var reader = await ReaderFactory.OpenAsyncReader(stream)) // Async extraction of all entries await reader.WriteAllToDirectoryAsync( @"C:\output", - cancellationToken + cancellationToken: cancellationToken ); } ``` @@ -231,11 +231,11 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio ### ReaderOptions -Use factory presets and fluent helpers for common configurations: +Use preset properties and fluent helpers for common configurations: ```csharp // External stream with password and custom encoding -var options = ReaderOptions.ForExternalStream() +var options = ReaderOptions.ForExternalStream .WithPassword("password") .WithArchiveEncoding(new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); @@ -244,9 +244,14 @@ using (var archive = ZipArchive.OpenArchive("file.zip", options)) // ... } -// Common presets -var safeOptions = ReaderOptions.SafeExtract; // No overwrite -var flatOptions = ReaderOptions.FlatExtract; // No directory structure +// Open-time presets +var external = ReaderOptions.ForExternalStream; +var owned = ReaderOptions.ForFilePath; + +// Extraction presets +var safeOptions = ExtractionOptions.SafeExtract; // No overwrite +var flatOptions = ExtractionOptions.FlatExtract; // No directory structure +var metadataOptions = ExtractionOptions.PreserveMetadata; // Keep timestamps and attributes // Factory defaults: // - file path / FileInfo overloads use LeaveStreamOpen = false @@ -297,23 +302,24 @@ archive.SaveTo("output.zip", options); ### Extraction behavior ```csharp -var options = new ReaderOptions +var options = new ExtractionOptions { ExtractFullPath = true, // Recreate directory structure Overwrite = true, // Overwrite existing files PreserveFileTime = true // Keep original timestamps }; -using (var archive = ZipArchive.OpenArchive("file.zip", options)) +using (var archive = ZipArchive.OpenArchive("file.zip")) { - archive.WriteToDirectory(@"C:\output"); + archive.WriteToDirectory(@"C:\output", options); } ``` ### Options matrix ```text -ReaderOptions: open-time behavior (password, encoding, stream ownership, extraction defaults) +ReaderOptions: open-time behavior (password, encoding, stream ownership) +ExtractionOptions: extract-time behavior (overwrite, paths, timestamps, attributes, symlinks) WriterOptions: write-time behavior (compression type/level, encoding, stream ownership) ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, comments, zip64) ``` @@ -324,7 +330,7 @@ ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, ```csharp var registry = CompressionProviderRegistry.Default.With(new SystemGZipCompressionProvider()); -var readerOptions = ReaderOptions.ForOwnedFile().WithProviders(registry); +var readerOptions = ReaderOptions.ForFilePath.WithProviders(registry); var writerOptions = new WriterOptions(CompressionType.GZip) { CompressionLevel = 6, @@ -412,7 +418,7 @@ var progress = new Progress(report => Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%"); }); -var options = ReaderOptions.ForOwnedFile().WithProgress(progress); +var options = ReaderOptions.ForFilePath.WithProgress(progress); using (var archive = ZipArchive.OpenArchive("archive.zip", options)) { archive.WriteToDirectory(@"C:\output"); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9db75bbcb..c463bf8e2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -90,7 +90,8 @@ Common types, options, and enumerations used across formats. - `ArchiveType.cs` - Enum for archive formats - `CompressionType.cs` - Enum for compression methods - `ArchiveEncoding.cs` - Character encoding configuration -- `IExtractionOptions.cs` - Extraction configuration exposed through `ReaderOptions` +- `IExtractionOptions.cs` - Interface for extraction configuration +- `ExtractionOptions.cs` - Extraction behavior options for file extraction APIs - Format-specific headers: `Zip/Headers/`, `Tar/Headers/`, `Rar/Headers/`, etc. #### `Compressors/` - Compression Algorithms diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 604fd2cf4..05b12d16a 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -333,7 +333,7 @@ using (var archive = ZipArchive.OpenArchive("archive.zip")) { await archive.WriteToDirectoryAsync( @"C:\output", - cancellationToken + cancellationToken: cancellationToken ); } // Thread can handle other work while I/O happens @@ -369,7 +369,7 @@ try { await archive.WriteToDirectoryAsync( @"C:\output", - cts.Token + cancellationToken: cts.Token ); } } diff --git a/docs/USAGE.md b/docs/USAGE.md index 772bf90d5..e28e50e46 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -6,7 +6,7 @@ SharpCompress now provides full async/await support for all I/O operations. All **Key Async Methods:** - `reader.WriteEntryToAsync(stream, cancellationToken)` - Extract entry asynchronously -- `reader.WriteAllToDirectoryAsync(path, cancellationToken)` - Extract all asynchronously +- `reader.WriteAllToDirectoryAsync(path, cancellationToken: cancellationToken)` - Extract all asynchronously - `writer.WriteAsync(filename, stream, modTime, cancellationToken)` - Write entry asynchronously - `writer.WriteAllAsync(directory, pattern, searchOption, cancellationToken)` - Write directory asynchronously - `entry.OpenEntryStreamAsync(cancellationToken)` - Open entry stream asynchronously @@ -94,14 +94,14 @@ Note: Extracting a solid rar or 7z file needs to be done in sequential order to `ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead. ```C# -// Using fluent factory method for extraction options -using (var archive = RarArchive.OpenArchive("Test.rar", - ReaderOptions.ForOwnedFile() - .WithExtractFullPath(true) - .WithOverwrite(true))) +// Use ReaderOptions for open-time behavior and ExtractionOptions for extract-time behavior +using (var archive = RarArchive.OpenArchive("Test.rar", ReaderOptions.ForFilePath)) { // Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types - archive.WriteToDirectory(@"D:\temp"); + archive.WriteToDirectory( + @"D:\temp", + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); } ``` @@ -131,12 +131,13 @@ var progress = new Progress(report => }); using (var archive = RarArchive.OpenArchive("archive.rar", - ReaderOptions.ForOwnedFile() - .WithProgress(progress) - .WithExtractFullPath(true) - .WithOverwrite(true))) // Must be solid Rar or 7Zip + ReaderOptions.ForFilePath + .WithProgress(progress))) // Must be solid Rar or 7Zip { - archive.WriteToDirectory(@"D:\output"); + archive.WriteToDirectory( + @"D:\output", + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); } ``` @@ -218,7 +219,7 @@ To replace a specific algorithm (for example to use `System.IO.Compression` for var systemGZip = new SystemGZipCompressionProvider(); var customRegistry = CompressionProviderRegistry.Default.With(systemGZip); -var readerOptions = ReaderOptions.ForOwnedFile() +var readerOptions = ReaderOptions.ForFilePath .WithProviders(customRegistry); using var reader = ReaderFactory.OpenReader(stream, readerOptions); @@ -261,7 +262,7 @@ using (var reader = ReaderFactory.OpenReader(stream)) { await reader.WriteAllToDirectoryAsync( @"D:\temp", - cancellationToken + cancellationToken: cancellationToken ); } ``` @@ -339,7 +340,7 @@ using (var archive = ZipArchive.OpenArchive("archive.zip")) // Simple async extraction - works for all archive types await archive.WriteToDirectoryAsync( @"C:\output", - cancellationToken + cancellationToken: cancellationToken ); } ``` diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 8e5f7d90a..82a40a164 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -43,7 +43,7 @@ public static async ValueTask OpenAsyncArchive( CancellationToken cancellationToken = default ) { - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; var factory = await FindFactoryAsync(fileInfo, cancellationToken) .ConfigureAwait(false); @@ -73,7 +73,7 @@ public static async ValueTask OpenAsyncArchive( } fileInfo.NotNull(nameof(fileInfo)); - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; var factory = await FindFactoryAsync(fileInfo, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 79d187b93..85cf00583 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -40,7 +40,7 @@ public static IArchive OpenArchive(string filePath, ReaderOptions? options = nul public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null) { - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; return FindFactory(fileInfo).OpenArchive(fileInfo, options); } @@ -64,7 +64,7 @@ public static IArchive OpenArchive( } fileInfo.NotNull(nameof(fileInfo)); - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; return FindFactory(fileInfo).OpenArchive(filesArray, options); } @@ -93,11 +93,11 @@ public static IArchive OpenArchive(IEnumerable streams, ReaderOptions? o public static void WriteToDirectory( string sourceArchive, string destinationDirectory, - ReaderOptions? options = null + ExtractionOptions? options = null ) { - using var archive = OpenArchive(sourceArchive, options); - archive.WriteToDirectory(destinationDirectory); + using var archive = OpenArchive(sourceArchive); + archive.WriteToDirectory(destinationDirectory, options); } public static T FindFactory(string path) diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index 725480730..ba0f74a5c 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -13,7 +13,7 @@ public interface IArchive : IDisposable ArchiveType Type { get; } /// - /// The options used when opening this archive, including extraction behavior settings. + /// The options used when opening this archive. /// ReaderOptions ReaderOptions { get; } diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 28ef61c7c..f60105a52 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -102,11 +102,15 @@ private static Stream WrapWithProgress( /// /// Extract to specific directory, retaining filename /// - public void WriteToDirectory(string destinationDirectory) => + public void WriteToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) => ExtractionMethods.WriteEntryToDirectory( entry, destinationDirectory, - (path) => entry.WriteToFile(path) + options, + (path) => entry.WriteToFile(path, options) ); /// @@ -114,14 +118,16 @@ public void WriteToDirectory(string destinationDirectory) => /// public async ValueTask WriteToDirectoryAsync( string destinationDirectory, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToDirectoryAsync( entry, destinationDirectory, + options, async (path, ct) => - await entry.WriteToFileAsync(path, ct).ConfigureAwait(false), + await entry.WriteToFileAsync(path, options, ct).ConfigureAwait(false), cancellationToken ) .ConfigureAwait(false); @@ -129,10 +135,11 @@ await entry.WriteToFileAsync(path, ct).ConfigureAwait(false), /// /// Extract to specific file /// - public void WriteToFile(string destinationFileName) => + public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) => ExtractionMethods.WriteEntryToFile( entry, destinationFileName, + options, (x, fm) => { using var fs = File.Open(destinationFileName, fm); @@ -145,12 +152,14 @@ public void WriteToFile(string destinationFileName) => /// public async ValueTask WriteToFileAsync( string destinationFileName, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToFileAsync( entry, destinationFileName, + options, async (x, fm, ct) => { using var fs = File.Open(destinationFileName, fm); diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs index a5bfd0b76..80857a25c 100644 --- a/src/SharpCompress/Archives/IArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveExtensions.cs @@ -14,25 +14,28 @@ public static class IArchiveExtensions /// Extract to specific directory with progress reporting /// /// The folder to extract into. + /// Extraction options. /// Optional progress reporter for tracking extraction progress. public void WriteToDirectory( string destinationDirectory, + ExtractionOptions? options = null, IProgress? progress = null ) { if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) { using var reader = archive.ExtractAllEntries(); - reader.WriteAllToDirectory(destinationDirectory); + reader.WriteAllToDirectory(destinationDirectory, options); } else { - archive.WriteToDirectoryInternal(destinationDirectory, progress); + archive.WriteToDirectoryInternal(destinationDirectory, options, progress); } } private void WriteToDirectoryInternal( string destinationDirectory, + ExtractionOptions? options, IProgress? progress ) { @@ -58,7 +61,7 @@ private void WriteToDirectoryInternal( continue; } - entry.WriteToDirectory(destinationDirectory); + entry.WriteToDirectory(destinationDirectory, options); bytesRead += entry.Size; progress?.Report( diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs index bb00afcf8..9ba599776 100644 --- a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -17,10 +17,12 @@ public static class IAsyncArchiveExtensions /// /// The archive to extract. /// The folder to extract into. + /// Extraction options. /// Optional progress reporter for tracking extraction progress. /// Optional cancellation token. public async ValueTask WriteToDirectoryAsync( string destinationDirectory, + ExtractionOptions? options = null, IProgress? progress = null, CancellationToken cancellationToken = default ) @@ -34,7 +36,7 @@ await archive.IsSolidAsync().ConfigureAwait(false) .ExtractAllEntriesAsync() .ConfigureAwait(false); await reader - .WriteAllToDirectoryAsync(destinationDirectory, cancellationToken) + .WriteAllToDirectoryAsync(destinationDirectory, options, cancellationToken) .ConfigureAwait(false); } else @@ -42,6 +44,7 @@ await reader await archive .WriteToDirectoryAsyncInternal( destinationDirectory, + options, progress, cancellationToken ) @@ -51,6 +54,7 @@ await archive private async ValueTask WriteToDirectoryAsyncInternal( string destinationDirectory, + ExtractionOptions? options, IProgress? progress, CancellationToken cancellationToken ) @@ -80,7 +84,7 @@ CancellationToken cancellationToken } await entry - .WriteToDirectoryAsync(destinationDirectory, cancellationToken) + .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) .ConfigureAwait(false); bytesRead += entry.Size; diff --git a/src/SharpCompress/Common/ExtractionMethods.Async.cs b/src/SharpCompress/Common/ExtractionMethods.Async.cs index 75fede3d2..8792e06bb 100644 --- a/src/SharpCompress/Common/ExtractionMethods.Async.cs +++ b/src/SharpCompress/Common/ExtractionMethods.Async.cs @@ -2,7 +2,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using SharpCompress.Readers; namespace SharpCompress.Common; @@ -11,12 +10,14 @@ internal static partial class ExtractionMethods public static async ValueTask WriteEntryToDirectoryAsync( IEntry entry, string destinationDirectory, + ExtractionOptions? options, Func writeAsync, CancellationToken cancellationToken = default ) { string destinationFileName; var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory); + options ??= new ExtractionOptions(); //check for trailing slash. if ( @@ -36,7 +37,7 @@ public static async ValueTask WriteEntryToDirectoryAsync( var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); file = Utility.ReplaceInvalidFileNameChars(file); - if (entry.Options.ExtractFullPath) + if (options.ExtractFullPath) { var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) .NotNull("Directory is null"); @@ -72,7 +73,7 @@ public static async ValueTask WriteEntryToDirectoryAsync( } await writeAsync(destinationFileName, cancellationToken).ConfigureAwait(false); } - else if (entry.Options.ExtractFullPath && !Directory.Exists(destinationFileName)) + else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) { Directory.CreateDirectory(destinationFileName); } @@ -81,19 +82,21 @@ public static async ValueTask WriteEntryToDirectoryAsync( public static async ValueTask WriteEntryToFileAsync( IEntry entry, string destinationFileName, + ExtractionOptions? options, Func openAndWriteAsync, CancellationToken cancellationToken = default ) { + options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (entry.Options.SymbolicLinkHandler is not null) + if (options.SymbolicLinkHandler is not null) { - entry.Options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); + options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); } else { - ReaderOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); + ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); } return; } @@ -101,14 +104,14 @@ public static async ValueTask WriteEntryToFileAsync( { var fm = FileMode.Create; - if (!entry.Options.Overwrite) + if (!options.Overwrite) { fm = FileMode.CreateNew; } await openAndWriteAsync(destinationFileName, fm, cancellationToken) .ConfigureAwait(false); - entry.PreserveExtractionOptions(destinationFileName); + entry.PreserveExtractionOptions(destinationFileName, options); } } } diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 6c6ced80e..526c6e263 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -1,9 +1,6 @@ using System; using System.IO; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using SharpCompress.Readers; namespace SharpCompress.Common; @@ -24,11 +21,13 @@ internal static partial class ExtractionMethods public static void WriteEntryToDirectory( IEntry entry, string destinationDirectory, + ExtractionOptions? options, Action write ) { string destinationFileName; var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory); + options ??= new ExtractionOptions(); //check for trailing slash. if ( @@ -48,7 +47,7 @@ Action write var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); file = Utility.ReplaceInvalidFileNameChars(file); - if (entry.Options.ExtractFullPath) + if (options.ExtractFullPath) { var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) .NotNull("Directory is null"); @@ -84,7 +83,7 @@ Action write } write(destinationFileName); } - else if (entry.Options.ExtractFullPath && !Directory.Exists(destinationFileName)) + else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) { Directory.CreateDirectory(destinationFileName); } @@ -93,18 +92,20 @@ Action write public static void WriteEntryToFile( IEntry entry, string destinationFileName, + ExtractionOptions? options, Action openAndWrite ) { + options ??= new ExtractionOptions(); if (entry.LinkTarget != null) { - if (entry.Options.SymbolicLinkHandler is not null) + if (options.SymbolicLinkHandler is not null) { - entry.Options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); + options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); } else { - ReaderOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); + ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); } return; } @@ -112,13 +113,13 @@ Action openAndWrite { var fm = FileMode.Create; - if (!entry.Options.Overwrite) + if (!options.Overwrite) { fm = FileMode.CreateNew; } openAndWrite(destinationFileName, fm); - entry.PreserveExtractionOptions(destinationFileName); + entry.PreserveExtractionOptions(destinationFileName, options); } } } diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs new file mode 100644 index 000000000..84a6bffec --- /dev/null +++ b/src/SharpCompress/Common/ExtractionOptions.cs @@ -0,0 +1,115 @@ +using System; +using SharpCompress.Common.Options; + +namespace SharpCompress.Common; + +/// +/// Options for configuring extraction behavior when extracting archive entries. +/// +/// +/// This class is immutable. Use the with expression to create modified copies: +/// +/// var options = new ExtractionOptions { Overwrite = false }; +/// options = options with { PreserveFileTime = true }; +/// +/// +public sealed record ExtractionOptions : IExtractionOptions +{ + /// + /// Overwrite target if it exists. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool Overwrite { get; init; } = true; + + /// + /// Extract with internal directory structure. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool ExtractFullPath { get; init; } = true; + + /// + /// Preserve file time. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool PreserveFileTime { get; init; } = true; + + /// + /// Preserve windows file attributes. + /// + public bool PreserveAttributes { get; init; } + + /// + /// Delegate for writing symbolic links to disk. + /// The first parameter is the source path (where the symlink is created). + /// The second parameter is the target path (what the symlink refers to). + /// + /// + /// Breaking change: Changed from field to init-only property in version 0.40.0. + /// The default handler logs a warning message. + /// + public Action? SymbolicLinkHandler { get; init; } + + /// + /// Creates a new ExtractionOptions instance with default values. + /// + public ExtractionOptions() { } + + /// + /// Creates a new ExtractionOptions instance with the specified overwrite behavior. + /// + /// Whether to overwrite existing files. + public ExtractionOptions(bool overwrite) + { + Overwrite = overwrite; + } + + /// + /// Creates a new ExtractionOptions instance with the specified extraction path and overwrite behavior. + /// + /// Whether to preserve directory structure. + /// Whether to overwrite existing files. + public ExtractionOptions(bool extractFullPath, bool overwrite) + { + ExtractFullPath = extractFullPath; + Overwrite = overwrite; + } + + /// + /// Creates a new ExtractionOptions instance with the specified extraction path, overwrite behavior, and file time preservation. + /// + /// Whether to preserve directory structure. + /// Whether to overwrite existing files. + /// Whether to preserve file modification times. + public ExtractionOptions(bool extractFullPath, bool overwrite, bool preserveFileTime) + { + ExtractFullPath = extractFullPath; + Overwrite = overwrite; + PreserveFileTime = preserveFileTime; + } + + /// + /// Gets an ExtractionOptions instance configured for safe extraction (no overwrite). + /// + public static ExtractionOptions SafeExtract => new(overwrite: false); + + /// + /// Gets an ExtractionOptions instance configured for flat extraction (no directory structure). + /// + public static ExtractionOptions FlatExtract => new(extractFullPath: false, overwrite: true); + + /// + /// Gets an ExtractionOptions instance configured to preserve timestamps and attributes. + /// + 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/Common/IEntry.Extensions.cs b/src/SharpCompress/Common/IEntry.Extensions.cs index d73f8a683..7e9b79a34 100644 --- a/src/SharpCompress/Common/IEntry.Extensions.cs +++ b/src/SharpCompress/Common/IEntry.Extensions.cs @@ -4,9 +4,13 @@ namespace SharpCompress.Common; internal static class EntryExtensions { - internal static void PreserveExtractionOptions(this IEntry entry, string destinationFileName) + internal static void PreserveExtractionOptions( + this IEntry entry, + string destinationFileName, + ExtractionOptions options + ) { - if (entry.Options.PreserveFileTime || entry.Options.PreserveAttributes) + if (options.PreserveFileTime || options.PreserveAttributes) { var nf = new FileInfo(destinationFileName); if (!nf.Exists) @@ -15,7 +19,7 @@ internal static void PreserveExtractionOptions(this IEntry entry, string destina } // update file time to original packed time - if (entry.Options.PreserveFileTime) + if (options.PreserveFileTime) { if (entry.CreatedTime.HasValue) { @@ -33,7 +37,7 @@ internal static void PreserveExtractionOptions(this IEntry entry, string destina } } - if (entry.Options.PreserveAttributes) + if (options.PreserveAttributes) { if (entry.Attrib.HasValue) { diff --git a/src/SharpCompress/Common/Options/IReaderOptions.cs b/src/SharpCompress/Common/Options/IReaderOptions.cs index f054e31f4..74d00de58 100644 --- a/src/SharpCompress/Common/Options/IReaderOptions.cs +++ b/src/SharpCompress/Common/Options/IReaderOptions.cs @@ -3,11 +3,7 @@ namespace SharpCompress.Common.Options; -public interface IReaderOptions - : IStreamOptions, - IEncodingOptions, - IProgressOptions, - IExtractionOptions +public interface IReaderOptions : IStreamOptions, IEncodingOptions, IProgressOptions { /// /// Look for RarArchive (Check for self-extracting archives or cases where RarArchive isn't at the start of the file) diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 07a7db8dd..120e2f69e 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -14,14 +14,16 @@ public static class IAsyncReaderExtensions /// public async ValueTask WriteEntryToDirectoryAsync( string destinationDirectory, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToDirectoryAsync( reader.Entry, destinationDirectory, + options, async (path, ct) => - await reader.WriteEntryToFileAsync(path, ct).ConfigureAwait(false), + await reader.WriteEntryToFileAsync(path, options, ct).ConfigureAwait(false), cancellationToken ) .ConfigureAwait(false); @@ -31,12 +33,14 @@ await reader.WriteEntryToFileAsync(path, ct).ConfigureAwait(false), /// public async ValueTask WriteEntryToFileAsync( string destinationFileName, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToFileAsync( reader.Entry, destinationFileName, + options, async (x, fm, ct) => { using var fs = File.Open(destinationFileName, fm); @@ -51,25 +55,28 @@ await ExtractionMethods /// public async ValueTask WriteAllToDirectoryAsync( string destinationDirectory, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) { while (await reader.MoveToNextEntryAsync(cancellationToken).ConfigureAwait(false)) { await reader - .WriteEntryToDirectoryAsync(destinationDirectory, cancellationToken) + .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) .ConfigureAwait(false); } } public async ValueTask WriteEntryToAsync( string destinationFileName, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToFileAsync( reader.Entry, destinationFileName, + options, async (x, fm, ct) => { using var fs = File.Open(destinationFileName, fm); @@ -81,10 +88,11 @@ await ExtractionMethods public async ValueTask WriteEntryToAsync( FileInfo destinationFileInfo, + ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await reader - .WriteEntryToAsync(destinationFileInfo.FullName, cancellationToken) + .WriteEntryToAsync(destinationFileInfo.FullName, options, cancellationToken) .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 436696bf6..54dfa14fc 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -22,31 +22,42 @@ public void WriteEntryTo(FileInfo filePath) /// /// Extract all remaining unread entries to specific directory, retaining filename /// - public void WriteAllToDirectory(string destinationDirectory) + public void WriteAllToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) { while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory(destinationDirectory); + reader.WriteEntryToDirectory(destinationDirectory, options); } } /// /// Extract to specific directory, retaining filename /// - public void WriteEntryToDirectory(string destinationDirectory) => + public void WriteEntryToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) => ExtractionMethods.WriteEntryToDirectory( reader.Entry, destinationDirectory, - (path) => reader.WriteEntryToFile(path) + options, + (path) => reader.WriteEntryToFile(path, options) ); /// /// Extract to specific file /// - public void WriteEntryToFile(string destinationFileName) => + public void WriteEntryToFile( + string destinationFileName, + ExtractionOptions? options = null + ) => ExtractionMethods.WriteEntryToFile( reader.Entry, destinationFileName, + options, (x, fm) => { using var fs = File.Open(destinationFileName, fm); diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index e3f3a4be3..0119bd52e 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -41,7 +41,7 @@ public static async ValueTask OpenAsyncReader( CancellationToken cancellationToken = default ) { - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; var stream = fileInfo.OpenAsyncReadStream(cancellationToken); return await OpenAsyncReader(stream, options, cancellationToken).ConfigureAwait(false); } diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 6a729b554..a1ff17752 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -17,7 +17,7 @@ public static IReader OpenReader(string filePath, ReaderOptions? options = null) public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? options = null) { - options ??= ReaderOptions.ForOwnedFile; + options ??= ReaderOptions.ForFilePath; return OpenReader(fileInfo.OpenRead(), options); } diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 41423c532..dd384d2d7 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -10,9 +10,9 @@ namespace SharpCompress.Readers; /// Options for configuring reader behavior when opening archives. /// /// -/// This class is immutable. Use factory presets and fluent helpers for common configurations: +/// This class is immutable. Use preset properties and fluent helpers for common configurations: /// -/// var options = ReaderOptions.ForExternalStream() +/// var options = ReaderOptions.ForExternalStream /// .WithPassword("secret") /// .WithLookForHeader(true); /// @@ -107,40 +107,6 @@ public sealed record ReaderOptions : IReaderOptions /// public int? RewindableBufferSize { get; init; } - /// - /// Overwrite target if it exists. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool Overwrite { get; init; } = true; - - /// - /// Extract with internal directory structure. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool ExtractFullPath { get; init; } = true; - - /// - /// Preserve file time. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool PreserveFileTime { get; init; } = true; - - /// - /// Preserve windows file attributes. - /// - public bool PreserveAttributes { get; init; } - - /// - /// Delegate for writing symbolic links to disk. - /// The first parameter is the source path (where the symlink is created). - /// The second parameter is the target path (what the symlink refers to). - /// - /// - /// Breaking change: Changed from field to init-only property in version 0.40.0. - /// The default handler logs a warning message. - /// - public Action? SymbolicLinkHandler { get; init; } - /// /// Registry of compression providers. /// Defaults to but can be replaced with custom implementations, such as @@ -162,17 +128,7 @@ public ReaderOptions() { } /// /// Gets ReaderOptions configured for file-based overloads that open their own stream. /// - public static ReaderOptions ForOwnedFile => new() { LeaveStreamOpen = false }; - - /// - /// Gets a ReaderOptions instance configured for safe extraction (no overwrite). - /// - public static ReaderOptions SafeExtract => new() { Overwrite = false }; - - /// - /// Gets a ReaderOptions instance configured for flat extraction (no directory structure). - /// - public static ReaderOptions FlatExtract => new() { ExtractFullPath = false, Overwrite = true }; + public static ReaderOptions ForFilePath => new() { LeaveStreamOpen = false }; /// /// Creates ReaderOptions for reading encrypted archives. @@ -197,16 +153,6 @@ public static ReaderOptions ForSelfExtractingArchive(string? password = null) => .WithPassword(password) .WithRewindableBufferSize(1_048_576); // 1MB for SFX archives - /// - /// 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" - ); - } - // Note: Parameterized constructors have been removed. // Use fluent With*() helpers or object initializers instead: // new ReaderOptions().WithPassword("secret").WithLookForHeader(true) diff --git a/src/SharpCompress/Readers/ReaderOptionsExtensions.cs b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs index 33f248079..2415b5447 100644 --- a/src/SharpCompress/Readers/ReaderOptionsExtensions.cs +++ b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs @@ -86,47 +86,6 @@ public static ReaderOptions WithRewindableBufferSize( int? rewindableBufferSize ) => options with { RewindableBufferSize = rewindableBufferSize }; - /// - /// Creates a copy with the specified overwrite setting. - /// - public static ReaderOptions WithOverwrite(this ReaderOptions options, bool overwrite) => - options with - { - Overwrite = overwrite, - }; - - /// - /// Creates a copy with the specified extract full path setting. - /// - public static ReaderOptions WithExtractFullPath( - this ReaderOptions options, - bool extractFullPath - ) => options with { ExtractFullPath = extractFullPath }; - - /// - /// Creates a copy with the specified preserve file time setting. - /// - public static ReaderOptions WithPreserveFileTime( - this ReaderOptions options, - bool preserveFileTime - ) => options with { PreserveFileTime = preserveFileTime }; - - /// - /// Creates a copy with the specified preserve attributes setting. - /// - public static ReaderOptions WithPreserveAttributes( - this ReaderOptions options, - bool preserveAttributes - ) => options with { PreserveAttributes = preserveAttributes }; - - /// - /// Creates a copy with the specified symbolic link handler. - /// - public static ReaderOptions WithSymbolicLinkHandler( - this ReaderOptions options, - Action? handler - ) => options with { SymbolicLinkHandler = handler }; - /// /// Creates a copy with the specified compression provider registry. /// diff --git a/tests/SharpCompress.Test/ExtractionTests.cs b/tests/SharpCompress.Test/ExtractionTests.cs index cf2330424..24ed6bf47 100644 --- a/tests/SharpCompress.Test/ExtractionTests.cs +++ b/tests/SharpCompress.Test/ExtractionTests.cs @@ -47,7 +47,12 @@ public void Extraction_ShouldHandleCaseInsensitivePathsOnWindows() // This should not throw an exception even if Path.GetFullPath returns // a path with different casing than the actual directory - var exception = Record.Exception(() => reader.WriteAllToDirectory(extractPath)); + var exception = Record.Exception(() => + reader.WriteAllToDirectory( + extractPath, + new ExtractionOptions { ExtractFullPath = false, Overwrite = true } + ) + ); Assert.Null(exception); } @@ -90,7 +95,10 @@ public void Extraction_ShouldPreventPathTraversalAttacks() using var reader = ReaderFactory.OpenReader(stream); var exception = Assert.Throws(() => - reader.WriteAllToDirectory(extractPath) + reader.WriteAllToDirectory( + extractPath, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ) ); Assert.Contains("outside of the destination", exception.Message); diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index cf334ef94..0722255c2 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -144,7 +144,7 @@ public async ValueTask Async_With_Cancellation_Token() cancellationToken: cts.Token ); - await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cts.Token); + await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken: cts.Token); // Just verify some files were extracted var extractedFiles = Directory.GetFiles( diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs index 603007c9b..70b73cb75 100644 --- a/tests/SharpCompress.Test/OptionsUsabilityTests.cs +++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs @@ -180,7 +180,8 @@ public void ReaderOptions_Fluent_And_Initializer_Equivalent() .WithLeaveStreamOpen(false) .WithPassword("secret") .WithLookForHeader(true) - .WithOverwrite(false); + .WithBufferSize(65536) + .WithDisableCheckIncomplete(true); // Object initializer approach var initializerApproach = new ReaderOptions @@ -188,13 +189,18 @@ public void ReaderOptions_Fluent_And_Initializer_Equivalent() LeaveStreamOpen = false, Password = "secret", LookForHeader = true, - Overwrite = false, + BufferSize = 65536, + DisableCheckIncomplete = true, }; Assert.Equal(fluentApproach.LeaveStreamOpen, initializerApproach.LeaveStreamOpen); Assert.Equal(fluentApproach.Password, initializerApproach.Password); Assert.Equal(fluentApproach.LookForHeader, initializerApproach.LookForHeader); - Assert.Equal(fluentApproach.Overwrite, initializerApproach.Overwrite); + Assert.Equal(fluentApproach.BufferSize, initializerApproach.BufferSize); + Assert.Equal( + fluentApproach.DisableCheckIncomplete, + initializerApproach.DisableCheckIncomplete + ); } [Fact] @@ -203,15 +209,23 @@ public void ReaderOptions_Presets_Have_Correct_Defaults() var external = ReaderOptions.ForExternalStream; Assert.True(external.LeaveStreamOpen); - var owned = ReaderOptions.ForOwnedFile; + var owned = ReaderOptions.ForFilePath; Assert.False(owned.LeaveStreamOpen); + } - var safe = ReaderOptions.SafeExtract; + [Fact] + public void ExtractionOptions_Presets_Have_Correct_Defaults() + { + var safe = ExtractionOptions.SafeExtract; Assert.False(safe.Overwrite); - var flat = ReaderOptions.FlatExtract; + var flat = ExtractionOptions.FlatExtract; Assert.False(flat.ExtractFullPath); Assert.True(flat.Overwrite); + + var preserveMetadata = ExtractionOptions.PreserveMetadata; + Assert.True(preserveMetadata.PreserveFileTime); + Assert.True(preserveMetadata.PreserveAttributes); } [Fact] diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 56ae3c31d..e4fedc587 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -202,7 +202,10 @@ public async ValueTask UseReaderAsync( Assert.Equal(expectedCompression, reader.Entry.CompressionType); } - await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken); + await reader.WriteEntryToDirectoryAsync( + SCRATCH_FILES_PATH, + cancellationToken: cancellationToken + ); } } } diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index 8ceefa9b0..25f6e4068 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -99,7 +99,10 @@ await writer.WriteAllAsync( readerOptions, cancellationToken ); - await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken); + await reader.WriteAllToDirectoryAsync( + SCRATCH_FILES_PATH, + cancellationToken: cancellationToken + ); } VerifyFiles(); } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index c40365d2c..86b97c054 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -242,7 +242,7 @@ public async ValueTask Zip_Deflate_Archive_WriteToDirectoryAsync_WithProgress() await using IAsyncArchive archive = await ZipArchive.OpenAsyncArchive( new AsyncOnlyStream(stream) ); - await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress); + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress: progress); } await Task.Delay(1000);