diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 49f03937..7b2cda51 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -15,9 +15,11 @@ namespace SharpCompress.Archives.GZip; public partial class GZipArchive { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) => SaveToAsync(new FileInfo(filePath), cancellationToken); + [Zomp.SyncMethodGenerator.CreateSyncVersion] public async ValueTask SaveToAsync( FileInfo fileInfo, CancellationToken cancellationToken = default @@ -28,6 +30,7 @@ public partial class GZipArchive .ConfigureAwait(false); } + [Zomp.SyncMethodGenerator.CreateSyncVersion] protected override async ValueTask SaveToAsync( Stream stream, GZipWriterOptions options, @@ -54,6 +57,7 @@ public partial class GZipArchive .WriteAsync( entry.Key.NotNull("Entry Key is null"), entryStream, + entry.LastModifiedTime, cancellationToken ) .ConfigureAwait(false); @@ -65,7 +69,12 @@ public partial class GZipArchive .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); await writer - .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + entry.LastModifiedTime, + cancellationToken + ) .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 2f5e5e33..5f4e7726 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -182,7 +182,12 @@ public partial class GZipArchive var header = ArrayPool.Shared.Rent(10); try { - await stream.ReadFullyAsync(header, 0, 10, cancellationToken).ConfigureAwait(false); + if ( + !await stream.ReadFullyAsync(header, 0, 10, cancellationToken).ConfigureAwait(false) + ) + { + return false; + } if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8) { diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index e47bf7c6..ab3a009d 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -8,7 +8,6 @@ using SharpCompress.Common.Options; using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.GZip; -using SharpCompress.Writers; using SharpCompress.Writers.GZip; namespace SharpCompress.Archives.GZip; @@ -28,14 +27,6 @@ public partial class GZipArchive return sourceStream.Streams.Select(a => new GZipVolume(a, ReaderOptions, 0)); } - public void SaveTo(string filePath) => SaveTo(new FileInfo(filePath)); - - public void SaveTo(FileInfo fileInfo) - { - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); - SaveTo(stream, new GZipWriterOptions(CompressionType.GZip)); - } - protected override GZipArchiveEntry CreateEntryInternal( string key, Stream source, @@ -54,31 +45,9 @@ public partial class GZipArchive protected override GZipArchiveEntry CreateDirectoryEntry(string key, DateTime? modified) => throw new NotSupportedException("GZip archives do not support directory entries."); - protected override void SaveTo( - Stream stream, - GZipWriterOptions options, - IEnumerable oldEntries, - IEnumerable newEntries - ) - { - if (Entries.Count > 1) - { - throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); - } - using var writer = new GZipWriter(stream, options); - foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory)) - { - using var entryStream = entry.OpenEntryStream(); - writer.Write( - entry.Key.NotNull("Entry Key is null"), - entryStream, - entry.LastModifiedTime - ); - } - } - protected override IEnumerable LoadEntries(IEnumerable volumes) { + // Kept hand-written: the generator cannot map IAsyncEnumerable.SingleAsync to LINQ Single. var stream = volumes.Single().Stream; yield return new GZipArchiveEntry( this, diff --git a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs index 9d975788..4c3bed6c 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs @@ -7,23 +7,12 @@ using SharpCompress.Common.Options; namespace SharpCompress.Archives.GZip; -public class GZipArchiveEntry : GZipEntry, IArchiveEntry +public partial class GZipArchiveEntry : GZipEntry, IArchiveEntry { internal GZipArchiveEntry(GZipArchive archive, GZipFilePart? part, IReaderOptions readerOptions) : base(part, readerOptions) => Archive = archive; - public virtual Stream OpenEntryStream() - { - //this is to reset the stream to be read multiple times - var part = (GZipFilePart)Parts.Single(); - var rawStream = part.GetRawStream(); - if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition) - { - rawStream.Position = part.EntryStartPosition; - } - return Parts.Single().GetCompressedStream().NotNull(); - } - + [Zomp.SyncMethodGenerator.CreateSyncVersion] public virtual async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) diff --git a/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs b/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs index 512d7cc2..accf1db4 100644 --- a/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs +++ b/src/SharpCompress/Common/GZip/GZipChecksumValidationStream.cs @@ -7,7 +7,7 @@ using SharpCompress.Crypto; namespace SharpCompress.Common.GZip; -internal sealed class GZipChecksumValidationStream : Stream +internal sealed partial class GZipChecksumValidationStream : Stream { private readonly Stream _source; private readonly Stream _rawStream; @@ -46,23 +46,38 @@ internal sealed class GZipChecksumValidationStream : Stream set => throw new NotSupportedException(); } - public override void Flush() => _source.Flush(); - + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override Task FlushAsync(CancellationToken cancellationToken) => _source.FlushAsync(cancellationToken); - public override int Read(byte[] buffer, int offset, int count) + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) { - var read = _source.Read(buffer, offset, count); + var read = await _source + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read); return read; } #if !LEGACY_DOTNET - public override int Read(Span buffer) + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) { - var read = _source.Read(buffer); + var read = await _source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); +#if SYNC_ONLY UpdateAndValidateAtEof(buffer[..read], read); +#else + UpdateAndValidateAtEof(buffer.Span[..read], read); +#endif return read; } #endif @@ -83,32 +98,6 @@ internal sealed class GZipChecksumValidationStream : Stream return value; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - var read = await _source - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read); - return read; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - var read = await _source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - UpdateAndValidateAtEof(buffer.Span[..read], read); - return read; - } -#endif - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); diff --git a/src/SharpCompress/Common/GZip/GZipEntry.Async.cs b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs index 7bb5f983..a19caa71 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs @@ -6,6 +6,7 @@ namespace SharpCompress.Common.GZip; public partial class GZipEntry { + [Zomp.SyncMethodGenerator.CreateSyncVersion] internal static async IAsyncEnumerable GetEntriesAsync( Stream stream, ReaderOptions options diff --git a/src/SharpCompress/Common/GZip/GZipEntry.cs b/src/SharpCompress/Common/GZip/GZipEntry.cs index 48f08d46..e359198e 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.cs @@ -46,14 +46,4 @@ public partial class GZipEntry : Entry public override bool IsSplitAfter => false; internal override IEnumerable Parts => _filePart.Empty(); - - internal static IEnumerable GetEntries(Stream stream, ReaderOptions options) - { - yield return new GZipEntry( - GZipFilePart.Create(stream, options.ArchiveEncoding, options.Providers), - options - ); - } - - // Async methods moved to GZipEntry.Async.cs } diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs index ae3e118a..e4bd555d 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs @@ -13,6 +13,7 @@ namespace SharpCompress.Common.GZip; internal sealed partial class GZipFilePart { + [Zomp.SyncMethodGenerator.CreateSyncVersion] internal static async ValueTask CreateAsync( Stream stream, IArchiveEncoding archiveEncoding, diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.cs b/src/SharpCompress/Common/GZip/GZipFilePart.cs index 13a1c968..3ce51163 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.cs @@ -16,32 +16,6 @@ internal sealed partial class GZipFilePart : FilePart private readonly Stream _stream; private readonly CompressionProviderRegistry _compressionProviders; - internal static GZipFilePart Create( - Stream stream, - IArchiveEncoding archiveEncoding, - CompressionProviderRegistry compressionProviders - ) - { - var part = new GZipFilePart(stream, archiveEncoding, compressionProviders); - - part.ReadAndValidateGzipHeader(); - if (stream.CanSeek) - { - var position = stream.Position; - stream.Position = stream.Length - 8; - part.ReadTrailer(); - stream.Position = position; - part.EntryStartPosition = position; - } - else - { - // For non-seekable streams, we can't read the trailer or track position. - // Set to 0 since the stream will be read sequentially from its current position. - part.EntryStartPosition = 0; - } - return part; - } - private GZipFilePart( Stream stream, IArchiveEncoding archiveEncoding, @@ -74,6 +48,7 @@ internal sealed partial class GZipFilePart : FilePart private void ReadTrailer() { + // Keep this sync implementation for its stack allocation on the parser hot path. // Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32 Span trailer = stackalloc byte[8]; _stream.ReadFully(trailer); @@ -84,6 +59,7 @@ internal sealed partial class GZipFilePart : FilePart private void ReadAndValidateGzipHeader() { + // Keep this sync implementation for stackalloc and ReadByte-based header parsing. // read the header on the first read Span header = stackalloc byte[10]; var n = _stream.Read(header); @@ -136,6 +112,7 @@ internal sealed partial class GZipFilePart : FilePart private string ReadZeroTerminatedString(Stream stream) { + // Keep this sync implementation for its one-byte stack allocation. Span buf1 = stackalloc byte[1]; var list = new List(); var done = false; diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs index dc2f823c..4a70c519 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs @@ -9,6 +9,10 @@ namespace SharpCompress.Compressors.Deflate; public partial class GZipStream { + /// + /// Flush the stream. + /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task FlushAsync(CancellationToken cancellationToken) { if (_disposed) @@ -18,6 +22,38 @@ public partial class GZipStream await BaseStream.FlushAsync(cancellationToken).ConfigureAwait(false); } + /// + /// Read and decompress data from the source stream. + /// + /// + /// + /// With a GZipStream, decompression is done through reading. + /// + /// + /// + /// + /// byte[] working = new byte[WORKING_BUFFER_SIZE]; + /// using (System.IO.Stream input = System.IO.File.OpenRead(_CompressedFile)) + /// { + /// using (Stream decompressor= new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, true)) + /// { + /// using (var output = System.IO.File.Create(_DecompressedFile)) + /// { + /// int n; + /// while ((n= decompressor.Read(working, 0, working.Length)) !=0) + /// { + /// output.Write(working, 0, n); + /// } + /// } + /// } + /// } + /// + /// + /// The buffer into which the decompressed data should be placed. + /// the offset within that data array to put the first byte read. + /// the number of bytes to read. + /// the number of bytes actually read + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, @@ -33,6 +69,9 @@ public partial class GZipStream .ReadAsync(buffer, offset, count, cancellationToken) .ConfigureAwait(false); + // Console.WriteLine("GZipStream::Read(buffer, off({0}), c({1}) = {2}", offset, count, n); + // Console.WriteLine( Util.FormatByteArray(buffer, offset, n) ); + if (!_firstReadDone) { _firstReadDone = true; @@ -66,6 +105,29 @@ public partial class GZipStream } #endif + /// + /// Write data to the stream. + /// + /// + /// + /// + /// If you wish to use the GZipStream to compress data while writing, + /// you can create a GZipStream with CompressionMode.Compress, and a + /// writable output stream. Then call Write() on that GZipStream, + /// providing uncompressed data as input. The data sent to the output stream + /// will be the compressed form of the data written. + /// + /// + /// + /// A GZipStream can be used for Read() or Write(), but not + /// both. Writing implies compression. Reading implies decompression. + /// + /// + /// + /// The buffer holding data to write to the stream. + /// the offset within that data array to find the first byte to write. + /// the number of bytes to write. + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task WriteAsync( byte[] buffer, int offset, @@ -79,6 +141,7 @@ public partial class GZipStream } if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) { + //Console.WriteLine("GZipStream: First write"); if (BaseStream._wantCompress) { // first write in compression, therefore, emit the GZIP header diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.cs index b0a690d8..0009118d 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.cs @@ -250,70 +250,6 @@ public partial class GZipStream : Stream } } - /// - /// Flush the stream. - /// - public override void Flush() - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - BaseStream.Flush(); - } - - /// - /// Read and decompress data from the source stream. - /// - /// - /// - /// With a GZipStream, decompression is done through reading. - /// - /// - /// - /// - /// byte[] working = new byte[WORKING_BUFFER_SIZE]; - /// using (System.IO.Stream input = System.IO.File.OpenRead(_CompressedFile)) - /// { - /// using (Stream decompressor= new Ionic.Zlib.GZipStream(input, CompressionMode.Decompress, true)) - /// { - /// using (var output = System.IO.File.Create(_DecompressedFile)) - /// { - /// int n; - /// while ((n= decompressor.Read(working, 0, working.Length)) !=0) - /// { - /// output.Write(working, 0, n); - /// } - /// } - /// } - /// } - /// - /// - /// The buffer into which the decompressed data should be placed. - /// the offset within that data array to put the first byte read. - /// the number of bytes to read. - /// the number of bytes actually read - public override int Read(byte[] buffer, int offset, int count) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - var n = BaseStream.Read(buffer, offset, count); - - // Console.WriteLine("GZipStream::Read(buffer, off({0}), c({1}) = {2}", offset, count, n); - // Console.WriteLine( Util.FormatByteArray(buffer, offset, n) ); - - if (!_firstReadDone) - { - _firstReadDone = true; - FileName = BaseStream._GzipFileName; - Comment = BaseStream._GzipComment; - LastModified = BaseStream._GzipMtime; - } - return n; - } - /// /// Calling this method always throws a . /// @@ -328,51 +264,6 @@ public partial class GZipStream : Stream /// irrelevant; this method will always throw! public override void SetLength(long value) => throw new NotSupportedException(); - /// - /// Write data to the stream. - /// - /// - /// - /// - /// If you wish to use the GZipStream to compress data while writing, - /// you can create a GZipStream with CompressionMode.Compress, and a - /// writable output stream. Then call Write() on that GZipStream, - /// providing uncompressed data as input. The data sent to the output stream - /// will be the compressed form of the data written. - /// - /// - /// - /// A GZipStream can be used for Read() or Write(), but not - /// both. Writing implies compression. Reading implies decompression. - /// - /// - /// - /// The buffer holding data to write to the stream. - /// the offset within that data array to find the first byte to write. - /// the number of bytes to write. - public override void Write(byte[] buffer, int offset, int count) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) - { - //Console.WriteLine("GZipStream: First write"); - if (BaseStream._wantCompress) - { - // first write in compression, therefore, emit the GZIP header - _headerByteCount = EmitHeader(); - } - else - { - throw new ArchiveOperationException(); - } - } - - BaseStream.Write(buffer, offset, count); - } - #endregion Stream methods public string? Comment @@ -509,13 +400,7 @@ public partial class GZipStream : Stream return header; } - private int EmitHeader() - { - var header = BuildHeader(); - BaseStream._stream.Write(header, 0, header.Length); - return header.Length; // bytes written - } - + [Zomp.SyncMethodGenerator.CreateSyncVersion] private async ValueTask EmitHeaderAsync(CancellationToken cancellationToken) { var header = BuildHeader(); diff --git a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs index 1365e0a7..be88c40c 100644 --- a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs @@ -11,7 +11,7 @@ namespace SharpCompress.Providers.Default; /// /// Provides GZip compression using SharpCompress's internal implementation. /// -public sealed class GZipCompressionProvider : CompressionProviderBase +public sealed partial class GZipCompressionProvider : CompressionProviderBase { public override CompressionType CompressionType => CompressionType.GZip; public override bool SupportsCompression => true; @@ -28,23 +28,22 @@ public sealed class GZipCompressionProvider : CompressionProviderBase return new GZipStream(source, CompressionMode.Decompress); } - public override Stream CreateDecompressStream(Stream source, CompressionContext context) - { - return new GZipStream( - source, - CompressionMode.Decompress, - CompressionLevel.Default, - context.ResolveArchiveEncoding() - ); - } - - public override ValueTask CreateDecompressStreamAsync( + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public override async ValueTask CreateDecompressStreamAsync( Stream source, CompressionContext context, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); - return new ValueTask(CreateDecompressStream(source, context)); + return await Task.FromResult( + new GZipStream( + source, + CompressionMode.Decompress, + CompressionLevel.Default, + context.ResolveArchiveEncoding() + ) + ) + .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Async.cs b/src/SharpCompress/Readers/GZip/GZipReader.Async.cs index 720cb792..28b10b81 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Async.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Async.cs @@ -10,6 +10,7 @@ public partial class GZipReader /// /// Returns entries asynchronously for streams that only support async reads. /// + [Zomp.SyncMethodGenerator.CreateSyncVersion] protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => GZipEntry.GetEntriesAsync(stream, Options); } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.cs b/src/SharpCompress/Readers/GZip/GZipReader.cs index eb09ab6c..aea23e73 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.cs @@ -11,9 +11,4 @@ public partial class GZipReader : AbstractReader : base(options, ArchiveType.GZip) => Volume = new GZipVolume(stream, options, 0); public override GZipVolume Volume { get; } - - protected override IEnumerable GetEntries(Stream stream) => - GZipEntry.GetEntries(stream, Options); - - // GetEntriesAsync moved to GZipReader.Async.cs } diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs index a1aa613a..de72c0d4 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs @@ -3,11 +3,13 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SharpCompress.Compressors.Deflate; +using SharpCompress.IO; namespace SharpCompress.Writers.GZip; public partial class GZipWriter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async ValueTask WriteAsync( string filename, Stream source, @@ -19,18 +21,28 @@ public partial class GZipWriter { throw new ArgumentException("Can only write a single stream to a GZip file."); } - var stream = (GZipStream)OutputStream.NotNull(); - stream.FileName = filename; - stream.LastModified = modificationTime; + + // Custom providers need not expose SharpCompress's internal GZip stream. + if (OutputStream is GZipStream gzipStream) + { + gzipStream.FileName = filename; + gzipStream.LastModified = modificationTime; + } + var progressStream = WrapWithProgress(source, filename); #if LEGACY_DOTNET - await progressStream.CopyToAsync(stream).ConfigureAwait(false); + await progressStream + .CopyToAsync(OutputStream.NotNull(), WriterOptions.BufferSize) + .ConfigureAwait(false); #else - await progressStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); + await progressStream + .CopyToAsync(OutputStream.NotNull(), WriterOptions.BufferSize, cancellationToken) + .ConfigureAwait(false); #endif _wroteToStream = true; } + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override ValueTask WriteDirectoryAsync( string directoryName, DateTime? modificationTime, diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.cs b/src/SharpCompress/Writers/GZip/GZipWriter.cs index 3c62ff2d..604e7b02 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.cs @@ -70,26 +70,4 @@ public sealed partial class GZipWriter : AbstractWriter } } #pragma warning restore CA2215 - - public override void Write(string filename, Stream source, DateTime? modificationTime) - { - if (_wroteToStream) - { - throw new ArgumentException("Can only write a single stream to a GZip file."); - } - - // Set metadata on the stream if it's the internal GZipStream - if (OutputStream is GZipStream gzipStream) - { - gzipStream.FileName = filename; - gzipStream.LastModified = modificationTime; - } - - var progressStream = WrapWithProgress(source, filename); - progressStream.CopyTo(OutputStream.NotNull(), WriterOptions.BufferSize); - _wroteToStream = true; - } - - public override void WriteDirectory(string directoryName, DateTime? modificationTime) => - throw new NotSupportedException("GZip archives do not support directory entries."); } diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index d7812b57..ba5a7782 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -240,4 +240,35 @@ public class GZipArchiveAsyncTests : ArchiveTests Assert.True(entryStream.IsDisposed); } + + [Fact] + public async ValueTask GZip_Archive_SaveTo_Async_Preserves_Modification_Time() + { + var modificationTime = new DateTime(2024, 1, 2, 3, 4, 5); + using var destination = new MemoryStream(); + await using (var archive = (GZipArchive)await GZipArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync( + "contents.bin", + new MemoryStream(new byte[] { 1, 2, 3, 4 }), + size: 4, + modified: modificationTime + ); + await archive.SaveToAsync(destination, new GZipWriterOptions()); + } + + destination.Position = 0; + await using var result = await GZipArchive.OpenAsyncArchive(destination); + var entry = await result.EntriesAsync.FirstAsync(); + + Assert.Equal(modificationTime, entry.LastModifiedTime); + } + + [Fact] + public async ValueTask GZip_Archive_IsGZipFileAsync_ReturnsFalse_For_Truncated_Header() + { + using var stream = new MemoryStream(new byte[] { 0x1F, 0x8B, 8 }); + + Assert.False(await GZipArchive.IsGZipFileAsync(stream)); + } } diff --git a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs index ba9ecd61..56136e8e 100644 --- a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs @@ -1,6 +1,8 @@ using System.IO; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.Providers; +using SharpCompress.Providers.System; using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.GZip; @@ -91,4 +93,33 @@ public class GZipWriterAsyncTests : WriterTests Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz") ); } + + [Fact] + public async ValueTask GZip_Writer_Async_With_System_Provider() + { + var contents = new byte[] { 1, 2, 3, 4 }; + var providers = CompressionProviderRegistry.Default.With( + new SystemGZipCompressionProvider() + ); + using var source = new MemoryStream(contents); + using var destination = new MemoryStream(); + await using ( + var writer = new GZipWriter( + destination, + new GZipWriterOptions { LeaveStreamOpen = true, Providers = providers } + ) + ) + { + await writer.WriteAsync("contents.bin", source); + } + + destination.Position = 0; + using var compressed = new SystemGZipCompressionProvider().CreateDecompressStream( + destination + ); + using var actual = new MemoryStream(); + compressed.CopyTo(actual); + + Assert.Equal(contents, actual.ToArray()); + } } diff --git a/tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs b/tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs index 5d51c441..9941d256 100644 --- a/tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Writers.GZip; using Xunit; @@ -16,4 +17,15 @@ public class GZipWriterDirectoryTests : TestBase Assert.Throws(() => writer.WriteDirectory("test-dir", DateTime.Now)); } + + [Fact] + public async ValueTask GZipWriter_WriteDirectoryAsync_ThrowsNotSupportedException() + { + using var memoryStream = new MemoryStream(); + using var writer = new GZipWriter(memoryStream, new GZipWriterOptions()); + + await Assert.ThrowsAsync(async () => + await writer.WriteDirectoryAsync("test-dir", DateTime.Now) + ); + } } diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs index 1902e4ab..c8b6ba86 100644 --- a/tests/SharpCompress.Test/OptionsUsabilityTests.cs +++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs @@ -220,6 +220,21 @@ public class OptionsUsabilityTests : TestBase Assert.Equal(19, source.CopyBufferSize); } + [Fact] + public async Task GZipWriter_Uses_WriterOptions_BufferSize_Async() + { + await using var source = new TrackingReadStream(new byte[100]); + await using var destination = new MemoryStream(); + await using var writer = new GZipWriter( + destination, + new GZipWriterOptions { BufferSize = 23 } + ); + + await writer.WriteAsync("buffer-size.txt", source, DateTime.Now); + + Assert.Equal(23, source.CopyBufferSize); + } + [Fact] public void TarWriter_Uses_WriterOptions_BufferSize_ForTransfer() {