diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs index cb9dc542..fa5f1a0d 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs @@ -152,7 +152,11 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc; long inputSize; { +#if LEGACY_DOTNET using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); +#else + await using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); +#endif properties = lzmaStream.Properties; (inputCrc, inputSize) = await CopyWithCrcAsync( diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs index a606e2a7..454ba022 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs @@ -8,8 +8,11 @@ namespace SharpCompress.Compressors.Deflate; public partial class DeflateStream { -#if !LEGACY_DOTNET +#if !LEGACY_DOTNET || NETSTANDARD2_1 public override async ValueTask DisposeAsync() +#else + public async ValueTask DisposeAsync() +#endif { if (!_disposed) { @@ -19,9 +22,12 @@ public partial class DeflateStream } _disposed = true; } +#if !LEGACY_DOTNET || NETSTANDARD2_1 await base.DisposeAsync().ConfigureAwait(false); - } +#else + await Task.CompletedTask.ConfigureAwait(false); #endif + } public override async Task FlushAsync(CancellationToken cancellationToken) { diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index ea96f065..c82ca78a 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -550,15 +550,20 @@ internal class ZlibBaseStream : Stream, IStreamStack } } -#if !LEGACY_DOTNET +#if !LEGACY_DOTNET || NETSTANDARD2_1 public override async ValueTask DisposeAsync() +#else + public async ValueTask DisposeAsync() +#endif { if (isDisposed) { return; } isDisposed = true; +#if !LEGACY_DOTNET || NETSTANDARD2_1 await base.DisposeAsync().ConfigureAwait(false); +#endif if (_stream is null) { return; @@ -574,13 +579,19 @@ internal class ZlibBaseStream : Stream, IStreamStack { if (!_leaveOpen) { - await _stream.DisposeAsync().ConfigureAwait(false); + if (_stream is IAsyncDisposable asyncDisposableStream) + { + await asyncDisposableStream.DisposeAsync().ConfigureAwait(false); + } + else + { + _stream.Dispose(); + } } _stream = null; } } } -#endif public override void Flush() { diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 0bfde315..5e506696 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -473,21 +473,30 @@ public class TarFactory stream = writerOptions.CompressionType switch { CompressionType.None => stream, - CompressionType.BZip2 => await providers.CreateCompressStreamAsync( - CompressionType.BZip2, - stream, - writerOptions.CompressionLevel, - cancellationToken).ConfigureAwait(false), - CompressionType.GZip => await providers.CreateCompressStreamAsync( - CompressionType.GZip, - stream, - writerOptions.CompressionLevel, - cancellationToken).ConfigureAwait(false), - CompressionType.LZip => await providers.CreateCompressStreamAsync( - CompressionType.LZip, - stream, - writerOptions.CompressionLevel, - cancellationToken).ConfigureAwait(false), + CompressionType.BZip2 => await providers + .CreateCompressStreamAsync( + CompressionType.BZip2, + stream, + writerOptions.CompressionLevel, + cancellationToken + ) + .ConfigureAwait(false), + CompressionType.GZip => await providers + .CreateCompressStreamAsync( + CompressionType.GZip, + stream, + writerOptions.CompressionLevel, + cancellationToken + ) + .ConfigureAwait(false), + CompressionType.LZip => await providers + .CreateCompressStreamAsync( + CompressionType.LZip, + stream, + writerOptions.CompressionLevel, + cancellationToken + ) + .ConfigureAwait(false), _ => throw new InvalidFormatException( "Tar does not support compression: " + writerOptions.CompressionType ), diff --git a/src/SharpCompress/Factories/TarWrapper.cs b/src/SharpCompress/Factories/TarWrapper.cs index 2568cb3e..ba0a5fbd 100644 --- a/src/SharpCompress/Factories/TarWrapper.cs +++ b/src/SharpCompress/Factories/TarWrapper.cs @@ -99,8 +99,11 @@ public class TarWrapper( LZipStream.IsLZipFile, LZipStream.IsLZipFileAsync, (stream) => LZipStream.Create(stream, CompressionMode.Decompress), - async (stream, _) =>await LZipStream.CreateAsync(stream, CompressionMode.Decompress).ConfigureAwait(false), - ["tar.lz"] + async (stream, _) => + await LZipStream + .CreateAsync(stream, CompressionMode.Decompress) + .ConfigureAwait(false), + ["tar.lz"] ), new( CompressionType.Xz, diff --git a/src/SharpCompress/IO/CountingStream.cs b/src/SharpCompress/IO/CountingStream.cs index ecc36cf7..5d1263b3 100644 --- a/src/SharpCompress/IO/CountingStream.cs +++ b/src/SharpCompress/IO/CountingStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.IO; @@ -37,6 +39,9 @@ internal class CountingStream : Stream public override void Flush() => _stream.Flush(); + public override async Task FlushAsync(CancellationToken cancellationToken) => + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); @@ -56,6 +61,28 @@ internal class CountingStream : Stream _bytesWritten++; } + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + _bytesWritten += count; + } + +#if !LEGACY_DOTNET + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + _bytesWritten += buffer.Length; + } +#endif + protected override void Dispose(bool disposing) { if (disposing) diff --git a/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs index 61a5f92d..74022dd5 100644 --- a/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/LZipCompressionProvider.cs @@ -39,11 +39,11 @@ public sealed class LZipCompressionProvider : CompressionProviderBase return LZipStream.Create(source, CompressionMode.Decompress); } - public override async ValueTask CreateDecompressStreamAsync(Stream source, CancellationToken cancellationToken = default) => await LZipStream - .CreateAsync( - source, - CompressionMode.Decompress, - cancellationToken: cancellationToken - ) - .ConfigureAwait(false); + public override async ValueTask CreateDecompressStreamAsync( + Stream source, + CancellationToken cancellationToken = default + ) => + await LZipStream + .CreateAsync(source, CompressionMode.Decompress, cancellationToken: cancellationToken) + .ConfigureAwait(false); } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index 636d1b12..d456ef2b 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -17,7 +17,6 @@ public partial class TarWriter : AbstractWriter _finalizeArchiveOnClose = options.FinalizeArchiveOnClose; _headerFormat = options.HeaderFormat; - InitializeStream(destination); } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs index 8d9bf9c7..194fb7ed 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -87,7 +87,12 @@ public partial class ZipWriter ) { cancellationToken.ThrowIfCancellationRequested(); + options.ValidateWithFallback(compressionType, compressionLevel); var compression = ToZipCompressionMethod(options.CompressionType ?? compressionType); + + entryPath = NormalizeFilename(entryPath); + options.ModificationDateTime ??= DateTime.Now; + options.EntryComment ??= string.Empty; var entry = new ZipCentralDirectoryEntry( compression, entryPath, @@ -109,13 +114,16 @@ public partial class ZipWriter await WriteHeaderAsync(entryPath, options, entry, useZip64, cancellationToken) .ConfigureAwait(false); streamPosition += headersize; - return new ZipWritingStream( - this, - OutputStream.NotNull(), - entry, - compression, - options.CompressionLevel ?? compressionLevel - ); + return await ZipWritingStream + .CreateAsync( + this, + OutputStream.NotNull(), + entry, + compression, + options.CompressionLevel ?? compressionLevel, + cancellationToken + ) + .ConfigureAwait(false); } private async Task WriteHeaderAsync( diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index 56aaa09a..a7cd0784 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -392,13 +392,14 @@ public partial class ZipWriter : AbstractWriter private readonly CRC32 crc = new(); private readonly ZipCentralDirectoryEntry entry; private readonly Stream originalStream; - private readonly Stream writeStream; + private Stream writeStream; private readonly ZipWriter writer; private readonly ZipCompressionMethod zipCompressionMethod; private readonly int compressionLevel; private ICompressionProviderHooks? compressionProviderHooks; private CompressionContext? compressionContext; private CountingStream? counting; + private MemoryStream? asyncCompressionBuffer; private ulong decompressed; // Flag to prevent throwing exceptions on Dispose @@ -410,7 +411,8 @@ public partial class ZipWriter : AbstractWriter Stream originalStream, ZipCentralDirectoryEntry entry, ZipCompressionMethod zipCompressionMethod, - int compressionLevel + int compressionLevel, + Stream? compressionStream = null ) { this.writer = writer; @@ -419,7 +421,42 @@ public partial class ZipWriter : AbstractWriter this.entry = entry; this.zipCompressionMethod = zipCompressionMethod; this.compressionLevel = compressionLevel; - writeStream = GetWriteStream(originalStream); + writeStream = GetWriteStream(compressionStream ?? originalStream); + } + + internal static async Task CreateAsync( + ZipWriter writer, + Stream originalStream, + ZipCentralDirectoryEntry entry, + ZipCompressionMethod zipCompressionMethod, + int compressionLevel, + CancellationToken cancellationToken + ) + { + var compressionStream = originalStream; + MemoryStream? asyncCompressionBuffer = null; + if ( + zipCompressionMethod + is ZipCompressionMethod.BZip2 + or ZipCompressionMethod.LZMA + or ZipCompressionMethod.PPMd + ) + { + asyncCompressionBuffer = new MemoryStream(); + compressionStream = asyncCompressionBuffer; + } + + var stream = new ZipWritingStream( + writer, + originalStream, + entry, + zipCompressionMethod, + compressionLevel, + compressionStream + ); + stream.asyncCompressionBuffer = asyncCompressionBuffer; + await Task.CompletedTask.ConfigureAwait(false); + return stream; } public override bool CanRead => false; @@ -674,43 +711,98 @@ public partial class ZipWriter : AbstractWriter public override void Write(byte[] buffer, int offset, int count) { - // We check the limits first, because we can keep the archive consistent - // if we can prevent the writes from happening - if (entry.Zip64HeaderOffset == 0) - { - var countingCount = counting?.BytesWritten ?? 0; - // Pre-check, the counting.Count is not exact, as we do not know the size before having actually compressed it - if ( - limitsExceeded - || ((decompressed + (uint)count) > uint.MaxValue) - || (countingCount + (uint)count) > uint.MaxValue - ) - { - throw new NotSupportedException( - "Attempted to write a stream that is larger than 4GiB without setting the zip64 option" - ); - } - } + CheckWriteLimits(count); decompressed += (uint)count; crc.SlurpBlock(buffer, offset, count); writeStream.Write(buffer, offset, count); - if (entry.Zip64HeaderOffset == 0) + CheckPostWriteLimits(); + } + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + CheckWriteLimits(count); + + decompressed += (uint)count; + crc.SlurpBlock(buffer, offset, count); + await writeStream + .WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + CheckPostWriteLimits(); + } + +#if !LEGACY_DOTNET + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + CheckWriteLimits(buffer.Length); + + decompressed += (uint)buffer.Length; + if (System.Runtime.InteropServices.MemoryMarshal.TryGetArray(buffer, out var segment)) { - var countingCount = counting?.BytesWritten ?? 0; - // Post-check, this is accurate - if ((decompressed > uint.MaxValue) || countingCount > uint.MaxValue) - { - // We have written the data, so the archive is now broken - // Throwing the exception here, allows us to avoid - // throwing an exception in Dispose() which is discouraged - // as it can mask other errors - limitsExceeded = true; - throw new NotSupportedException( - "Attempted to write a stream that is larger than 4GiB without setting the zip64 option" - ); - } + crc.SlurpBlock(segment.Array!, segment.Offset, segment.Count); + } + else + { + var array = buffer.ToArray(); + crc.SlurpBlock(array, 0, array.Length); + } + await writeStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + + CheckPostWriteLimits(); + } +#endif + + private void CheckWriteLimits(int count) + { + // We check the limits first, because we can keep the archive consistent + // if we can prevent the writes from happening. The compressed byte count + // is only an estimate until compression has actually happened. + if (entry.Zip64HeaderOffset != 0) + { + return; + } + + var countingCount = counting?.BytesWritten ?? 0; + if ( + limitsExceeded + || ((decompressed + (uint)count) > uint.MaxValue) + || (countingCount + (uint)count) > uint.MaxValue + ) + { + throw new NotSupportedException( + "Attempted to write a stream that is larger than 4GiB without setting the zip64 option" + ); + } + } + + private void CheckPostWriteLimits() + { + if (entry.Zip64HeaderOffset != 0) + { + return; + } + + var countingCount = counting?.BytesWritten ?? 0; + if ((decompressed > uint.MaxValue) || countingCount > uint.MaxValue) + { + // We have written the data, so the archive is now broken. Throwing + // here avoids throwing from Dispose(), which can mask other errors. + limitsExceeded = true; + throw new NotSupportedException( + "Attempted to write a stream that is larger than 4GiB without setting the zip64 option" + ); } } @@ -777,20 +869,26 @@ public partial class ZipWriter : AbstractWriter isDisposed = true; -#if NET48 || NETSTANDARD2_0 - writeStream.Dispose(); -#else - await writeStream.DisposeAsync().ConfigureAwait(false); -#endif + if (writeStream is IAsyncDisposable asyncDisposableWriteStream) + { + await asyncDisposableWriteStream.DisposeAsync().ConfigureAwait(false); + } + else + { + writeStream.Dispose(); + } if (limitsExceeded) { // We have written invalid data into the archive, so destroy it -#if NET48 || NETSTANDARD2_0 - originalStream.Dispose(); -#else - await originalStream.DisposeAsync().ConfigureAwait(false); -#endif + if (originalStream is IAsyncDisposable asyncDisposableOriginalStream) + { + await asyncDisposableOriginalStream.DisposeAsync().ConfigureAwait(false); + } + else + { + originalStream.Dispose(); + } return; } @@ -805,6 +903,14 @@ public partial class ZipWriter : AbstractWriter var compressedvalue = zip64 ? uint.MaxValue : (uint)countingCount; var decompressedvalue = zip64 ? uint.MaxValue : (uint)entry.Decompressed; + if (asyncCompressionBuffer is not null) + { + asyncCompressionBuffer.Position = 0; + await asyncCompressionBuffer + .CopyToAsync(originalStream, 81920, CancellationToken.None) + .ConfigureAwait(false); + } + if (originalStream.CanSeek) { originalStream.Position = (long)(entry.HeaderOffset + 6); diff --git a/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs b/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs index 9aee962b..f78cd989 100644 --- a/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs +++ b/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs @@ -130,7 +130,9 @@ public class LeaveOpenBehaviorTests public void LZipStream_Compress_LeaveOpen_False() { using var innerStream = new TestStream(new MemoryStream()); - using (var lzip = LZipStream.Create(innerStream, CompressionMode.Compress, leaveOpen: false)) + using ( + var lzip = LZipStream.Create(innerStream, CompressionMode.Compress, leaveOpen: false) + ) { lzip.Write(CreateTestData(), 0, CreateTestData().Length); lzip.Finish(); @@ -177,7 +179,9 @@ public class LeaveOpenBehaviorTests using var innerStream = new TestStream(memStream); var decompressed = new byte[CreateTestData().Length]; - using (var lzip = LZipStream.Create(innerStream, CompressionMode.Decompress, leaveOpen: false)) + using ( + var lzip = LZipStream.Create(innerStream, CompressionMode.Decompress, leaveOpen: false) + ) { lzip.Read(decompressed, 0, decompressed.Length); } @@ -201,7 +205,9 @@ public class LeaveOpenBehaviorTests using var innerStream = new TestStream(memStream); var decompressed = new byte[CreateTestData().Length]; - using (var lzip = LZipStream.Create(innerStream, CompressionMode.Decompress, leaveOpen: true)) + using ( + var lzip = LZipStream.Create(innerStream, CompressionMode.Decompress, leaveOpen: true) + ) { lzip.Read(decompressed, 0, decompressed.Length); }