From fb76bd82f2d9cde7ecc54a28b1739fa52929cf7f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 26 Nov 2025 08:09:20 +0000 Subject: [PATCH] first commit of async reader --- src/SharpCompress/Common/AsyncBinaryReader.cs | 88 +++++++++++++++++++ .../Common/Zip/Headers/DirectoryEndHeader.cs | 2 +- .../Common/Zip/Headers/LocalEntryHeader.cs | 32 ++++--- .../Headers/Zip64DirectoryEndLocatorHeader.cs | 14 ++- .../Common/Zip/Headers/ZipFileEntry.cs | 13 +-- .../Common/Zip/Headers/ZipHeader.cs | 16 ++-- .../Common/Zip/SeekableZipHeaderFactory.cs | 23 ++--- .../Common/Zip/ZipHeaderFactory.cs | 5 +- .../SharpCompress.Test/Arc/ArcReaderTests.cs | 1 + 9 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 src/SharpCompress/Common/AsyncBinaryReader.cs diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs new file mode 100644 index 00000000..2600f319 --- /dev/null +++ b/src/SharpCompress/Common/AsyncBinaryReader.cs @@ -0,0 +1,88 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common +{ + public sealed class AsyncBinaryReader(Stream stream, bool leaveOpen = false) : IDisposable + { + private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + private readonly byte[] _buffer = new byte[8]; + private bool _disposed; + + public Stream BaseStream => _stream; + + public async ValueTask ReadByteAsync(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false); + return _buffer[0]; + } + + public async ValueTask ReadUInt16Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt16LittleEndian(_buffer); + } + + public async ValueTask ReadUInt32Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt32LittleEndian(_buffer); + } + public async ValueTask ReadUInt64Async(CancellationToken ct = default) + { + await ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt64LittleEndian(_buffer); + } + + public async ValueTask ReadBytesAsync(int count, CancellationToken ct = default) + { + var result = new byte[count]; + await ReadExactAsync(result, 0, count, ct).ConfigureAwait(false); + return result; + } + + private async ValueTask ReadExactAsync(byte[] destination, int offset, int length, CancellationToken ct) + { + var read = 0; + while (read < length) + { + var n = await _stream.ReadAsync(destination, offset + read, length - read, ct).ConfigureAwait(false); + if (n == 0) + { + throw new EndOfStreamException(); + } + + read += n; + } + } + + public void Dispose() + { + if (_disposed || leaveOpen) + { + _disposed = true; + return; + } + + _disposed = true; + _stream.Dispose(); + } + +#if NET6_0_OR_GREATER + public ValueTask DisposeAsync() + { + if (_disposed || leaveOpen) + { + _disposed = true; + return default; + } + + _disposed = true; + return _stream.DisposeAsync(); + } +#endif + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs index 2e54a6dd..71502c0d 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs @@ -7,7 +7,7 @@ internal class DirectoryEndHeader : ZipHeader public DirectoryEndHeader() : base(ZipHeaderType.DirectoryEnd) { } - internal override void Read(BinaryReader reader) + internal override void Read(AsyncBinaryReader reader) { VolumeNumber = reader.ReadUInt16(); FirstVolumeWithDirectory = reader.ReadUInt16(); diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index 1e3dc62d..c1ce5a6d 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -1,27 +1,25 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader : ZipFileEntry +internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) { - public LocalEntryHeader(ArchiveEncoding archiveEncoding) - : base(ZipHeaderType.LocalEntry, archiveEncoding) { } - - internal override void Read(BinaryReader reader) + internal override async ValueTask Read(AsyncBinaryReader reader) { - Version = reader.ReadUInt16(); - Flags = (HeaderFlags)reader.ReadUInt16(); - CompressionMethod = (ZipCompressionMethod)reader.ReadUInt16(); - OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16(); - OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16(); - Crc = reader.ReadUInt32(); - CompressedSize = reader.ReadUInt32(); - UncompressedSize = reader.ReadUInt32(); - var nameLength = reader.ReadUInt16(); - var extraLength = reader.ReadUInt16(); - var name = reader.ReadBytes(nameLength); - var extra = reader.ReadBytes(extraLength); + Version = await reader.ReadUInt16Async(); + Flags = (HeaderFlags)await reader.ReadUInt16Async(); + CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async(); + OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async(); + OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async(); + Crc = await reader.ReadUInt32Async(); + CompressedSize = await reader.ReadUInt32Async(); + UncompressedSize = await reader.ReadUInt32Async(); + var nameLength = await reader.ReadUInt16Async(); + var extraLength = await reader.ReadUInt16Async(); + var name = await reader.ReadBytesAsync(nameLength); + var extra = await reader.ReadBytesAsync(extraLength); // According to .ZIP File Format Specification // diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs index 3020d377..6b44b219 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs @@ -1,17 +1,15 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class Zip64DirectoryEndLocatorHeader : ZipHeader +internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) { - public Zip64DirectoryEndLocatorHeader() - : base(ZipHeaderType.Zip64DirectoryEndLocator) { } - - internal override void Read(BinaryReader reader) + internal override async ValueTask Read(AsyncBinaryReader reader) { - FirstVolumeWithDirectory = reader.ReadUInt32(); - RelativeOffsetOfTheEndOfDirectoryRecord = (long)reader.ReadUInt64(); - TotalNumberOfVolumes = reader.ReadUInt32(); + FirstVolumeWithDirectory = await reader.ReadUInt32Async(); + RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); + TotalNumberOfVolumes = await reader.ReadUInt32Async(); } public uint FirstVolumeWithDirectory { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index f37690d6..5d7f1c32 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -5,15 +5,8 @@ using System.IO; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry : ZipHeader +internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) : ZipHeader(type) { - protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) - : base(type) - { - Extra = new List(); - ArchiveEncoding = archiveEncoding; - } - internal bool IsDirectory { get @@ -30,7 +23,7 @@ internal abstract class ZipFileEntry : ZipHeader internal Stream? PackedStream { get; set; } - internal ArchiveEncoding ArchiveEncoding { get; } + internal ArchiveEncoding ArchiveEncoding { get; } = archiveEncoding; internal string? Name { get; set; } @@ -44,7 +37,7 @@ internal abstract class ZipFileEntry : ZipHeader internal long UncompressedSize { get; set; } - internal List Extra { get; set; } + internal List Extra { get; set; } = new(); public string? Password { get; set; } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs index 36d40a82..ad1714fa 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs @@ -1,18 +1,12 @@ -using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipHeader +internal abstract class ZipHeader(ZipHeaderType type) { - protected ZipHeader(ZipHeaderType type) - { - ZipHeaderType = type; - HasData = true; - } + internal ZipHeaderType ZipHeaderType { get; } = type; - internal ZipHeaderType ZipHeaderType { get; } + internal abstract ValueTask Read(AsyncBinaryReader reader); - internal abstract void Read(BinaryReader reader); - - internal bool HasData { get; set; } + internal bool HasData { get; set; } = true; } diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index 005f6480..be32f8a8 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -18,11 +19,11 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding) : base(StreamingMode.Seekable, password, archiveEncoding) { } - internal IEnumerable ReadSeekableHeader(Stream stream) + internal async IAsyncEnumerable ReadSeekableHeader(Stream stream) { - var reader = new BinaryReader(stream); + var reader = new AsyncBinaryReader(stream); - SeekBackToHeader(stream, reader); + await SeekBackToHeader(stream, reader); var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); @@ -34,24 +35,24 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); - var zip64_locator = reader.ReadUInt32(); + int zip64_locator = await reader.ReadUInt16Async(); if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) { throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); } var zip64Locator = new Zip64DirectoryEndLocatorHeader(); - zip64Locator.Read(reader); + await zip64Locator.Read(reader); stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); - var zip64Signature = reader.ReadUInt32(); + var zip64Signature = await reader.ReadUInt32Async(); if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) { throw new ArchiveException("Failed to locate the Zip64 Header"); } var zip64Entry = new Zip64DirectoryEndHeader(); - zip64Entry.Read(reader); + await zip64Entry.Read(reader); stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); } else @@ -63,8 +64,8 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory while (true) { stream.Position = position; - var signature = reader.ReadUInt32(); - var nextHeader = ReadHeader(signature, reader, _zip64); + var signature = await reader.ReadUInt32Async(); + var nextHeader = await ReadHeader(signature, reader, _zip64); position = stream.Position; if (nextHeader is null) @@ -98,7 +99,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory return true; } - private static void SeekBackToHeader(Stream stream, BinaryReader reader) + private static async ValueTask SeekBackToHeader(Stream stream, AsyncBinaryReader reader) { // Minimum EOCD length if (stream.Length < MINIMUM_EOCD_LENGTH) @@ -117,7 +118,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory stream.Seek(-len, SeekOrigin.End); - var seek = reader.ReadBytes(len); + var seek = await reader.ReadBytesAsync(len); // Search in reverse Array.Reverse(seek); diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index 45869ff6..f461050f 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; @@ -34,14 +35,14 @@ internal class ZipHeaderFactory _archiveEncoding = archiveEncoding; } - protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false) + protected async ValueTask ReadHeader(uint headerBytes, AsyncBinaryReader reader, bool zip64 = false) { switch (headerBytes) { case ENTRY_HEADER_BYTES: { var entryHeader = new LocalEntryHeader(_archiveEncoding); - entryHeader.Read(reader); + await entryHeader.Read(reader); LoadHeader(entryHeader, reader.BaseStream); _lastEntryHeader = entryHeader; diff --git a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs index 5ab79f02..54f169c3 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs @@ -1,3 +1,4 @@ +using System; using SharpCompress.Common; using Xunit;