gzip using sync generator

This commit is contained in:
Adam Hathcock
2026-08-03 08:15:18 +01:00
parent 187055e673
commit c4fa31d615
20 changed files with 229 additions and 277 deletions

View File

@@ -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);
}
}

View File

@@ -182,7 +182,12 @@ public partial class GZipArchive
var header = ArrayPool<byte>.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)
{

View File

@@ -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<GZipArchiveEntry> oldEntries,
IEnumerable<GZipArchiveEntry> 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<GZipArchiveEntry> LoadEntries(IEnumerable<GZipVolume> volumes)
{
// Kept hand-written: the generator cannot map IAsyncEnumerable.SingleAsync to LINQ Single.
var stream = volumes.Single().Stream;
yield return new GZipArchiveEntry(
this,

View File

@@ -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<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)

View File

@@ -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<int> 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<byte> buffer)
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async ValueTask<int> ReadAsync(
Memory<byte> 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<int> 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<int> ReadAsync(
Memory<byte> 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();

View File

@@ -6,6 +6,7 @@ namespace SharpCompress.Common.GZip;
public partial class GZipEntry
{
[Zomp.SyncMethodGenerator.CreateSyncVersion]
internal static async IAsyncEnumerable<GZipEntry> GetEntriesAsync(
Stream stream,
ReaderOptions options

View File

@@ -46,14 +46,4 @@ public partial class GZipEntry : Entry
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
internal static IEnumerable<GZipEntry> GetEntries(Stream stream, ReaderOptions options)
{
yield return new GZipEntry(
GZipFilePart.Create(stream, options.ArchiveEncoding, options.Providers),
options
);
}
// Async methods moved to GZipEntry.Async.cs
}

View File

@@ -13,6 +13,7 @@ namespace SharpCompress.Common.GZip;
internal sealed partial class GZipFilePart
{
[Zomp.SyncMethodGenerator.CreateSyncVersion]
internal static async ValueTask<GZipFilePart> CreateAsync(
Stream stream,
IArchiveEncoding archiveEncoding,

View File

@@ -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<byte> 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<byte> 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<byte> buf1 = stackalloc byte[1];
var list = new List<byte>();
var done = false;

View File

@@ -9,6 +9,10 @@ namespace SharpCompress.Compressors.Deflate;
public partial class GZipStream
{
/// <summary>
/// Flush the stream.
/// </summary>
[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);
}
/// <summary>
/// Read and decompress data from the source stream.
/// </summary>
///
/// <remarks>
/// With a <c>GZipStream</c>, decompression is done through reading.
/// </remarks>
///
/// <example>
/// <code>
/// 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);
/// }
/// }
/// }
/// }
/// </code>
/// </example>
/// <param name="buffer">The buffer into which the decompressed data should be placed.</param>
/// <param name="offset">the offset within that data array to put the first byte read.</param>
/// <param name="count">the number of bytes to read.</param>
/// <returns>the number of bytes actually read</returns>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async Task<int> 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
/// <summary>
/// Write data to the stream.
/// </summary>
///
/// <remarks>
/// <para>
/// If you wish to use the <c>GZipStream</c> to compress data while writing,
/// you can create a <c>GZipStream</c> with <c>CompressionMode.Compress</c>, and a
/// writable output stream. Then call <c>Write()</c> on that <c>GZipStream</c>,
/// providing uncompressed data as input. The data sent to the output stream
/// will be the compressed form of the data written.
/// </para>
///
/// <para>
/// A <c>GZipStream</c> can be used for <c>Read()</c> or <c>Write()</c>, but not
/// both. Writing implies compression. Reading implies decompression.
/// </para>
///
/// </remarks>
/// <param name="buffer">The buffer holding data to write to the stream.</param>
/// <param name="offset">the offset within that data array to find the first byte to write.</param>
/// <param name="count">the number of bytes to write.</param>
[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

View File

@@ -250,70 +250,6 @@ public partial class GZipStream : Stream
}
}
/// <summary>
/// Flush the stream.
/// </summary>
public override void Flush()
{
if (_disposed)
{
throw new ObjectDisposedException("GZipStream");
}
BaseStream.Flush();
}
/// <summary>
/// Read and decompress data from the source stream.
/// </summary>
///
/// <remarks>
/// With a <c>GZipStream</c>, decompression is done through reading.
/// </remarks>
///
/// <example>
/// <code>
/// 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);
/// }
/// }
/// }
/// }
/// </code>
/// </example>
/// <param name="buffer">The buffer into which the decompressed data should be placed.</param>
/// <param name="offset">the offset within that data array to put the first byte read.</param>
/// <param name="count">the number of bytes to read.</param>
/// <returns>the number of bytes actually read</returns>
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;
}
/// <summary>
/// Calling this method always throws a <see cref="NotImplementedException"/>.
/// </summary>
@@ -328,51 +264,6 @@ public partial class GZipStream : Stream
/// <param name="value">irrelevant; this method will always throw!</param>
public override void SetLength(long value) => throw new NotSupportedException();
/// <summary>
/// Write data to the stream.
/// </summary>
///
/// <remarks>
/// <para>
/// If you wish to use the <c>GZipStream</c> to compress data while writing,
/// you can create a <c>GZipStream</c> with <c>CompressionMode.Compress</c>, and a
/// writable output stream. Then call <c>Write()</c> on that <c>GZipStream</c>,
/// providing uncompressed data as input. The data sent to the output stream
/// will be the compressed form of the data written.
/// </para>
///
/// <para>
/// A <c>GZipStream</c> can be used for <c>Read()</c> or <c>Write()</c>, but not
/// both. Writing implies compression. Reading implies decompression.
/// </para>
///
/// </remarks>
/// <param name="buffer">The buffer holding data to write to the stream.</param>
/// <param name="offset">the offset within that data array to find the first byte to write.</param>
/// <param name="count">the number of bytes to write.</param>
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<int> EmitHeaderAsync(CancellationToken cancellationToken)
{
var header = BuildHeader();

View File

@@ -11,7 +11,7 @@ namespace SharpCompress.Providers.Default;
/// <summary>
/// Provides GZip compression using SharpCompress's internal implementation.
/// </summary>
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<Stream> CreateDecompressStreamAsync(
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public override async ValueTask<Stream> CreateDecompressStreamAsync(
Stream source,
CompressionContext context,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new ValueTask<Stream>(CreateDecompressStream(source, context));
return await Task.FromResult<Stream>(
new GZipStream(
source,
CompressionMode.Decompress,
CompressionLevel.Default,
context.ResolveArchiveEncoding()
)
)
.ConfigureAwait(false);
}
}

View File

@@ -10,6 +10,7 @@ public partial class GZipReader
/// <summary>
/// Returns entries asynchronously for streams that only support async reads.
/// </summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
protected override IAsyncEnumerable<GZipEntry> GetEntriesAsync(Stream stream) =>
GZipEntry.GetEntriesAsync(stream, Options);
}

View File

@@ -11,9 +11,4 @@ public partial class GZipReader : AbstractReader<GZipEntry, GZipVolume>
: base(options, ArchiveType.GZip) => Volume = new GZipVolume(stream, options, 0);
public override GZipVolume Volume { get; }
protected override IEnumerable<GZipEntry> GetEntries(Stream stream) =>
GZipEntry.GetEntries(stream, Options);
// GetEntriesAsync moved to GZipReader.Async.cs
}

View File

@@ -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,

View File

@@ -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.");
}