From dea635e39e12f5564c4e3059d32ab4a888dd8f4a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 18 Jan 2026 16:29:03 +0000 Subject: [PATCH] Intermediate commit of rar async reading --- .../Common/Rar/AsyncRarCrcBinaryReader.cs | 49 +++ .../Common/Rar/AsyncRarCryptoBinaryReader.cs | 98 +++++ .../Common/Rar/Headers/ArchiveCryptHeader.cs | 13 + .../Common/Rar/Headers/ArchiveHeader.cs | 34 ++ .../Common/Rar/Headers/EndArchiveHeader.cs | 27 +- .../Common/Rar/Headers/FileHeader.cs | 385 ++++++++++++++++++ .../Common/Rar/Headers/ProtectHeader.cs | 14 + .../Common/Rar/Headers/RarHeader.cs | 137 ++++++- .../Common/Rar/Headers/RarHeaderFactory.cs | 314 +++++++++++++- .../Common/Rar/Rar5CryptoInfo.cs | 62 +++ .../IO/AsyncMarkingBinaryReader.cs | 186 +++++++++ 11 files changed, 1309 insertions(+), 10 deletions(-) create mode 100644 src/SharpCompress/Common/Rar/AsyncRarCrcBinaryReader.cs create mode 100644 src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs create mode 100644 src/SharpCompress/IO/AsyncMarkingBinaryReader.cs diff --git a/src/SharpCompress/Common/Rar/AsyncRarCrcBinaryReader.cs b/src/SharpCompress/Common/Rar/AsyncRarCrcBinaryReader.cs new file mode 100644 index 00000000..438da7fc --- /dev/null +++ b/src/SharpCompress/Common/Rar/AsyncRarCrcBinaryReader.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar; + +internal class AsyncRarCrcBinaryReader : AsyncMarkingBinaryReader +{ + private uint _currentCrc; + + public AsyncRarCrcBinaryReader(Stream stream, CancellationToken ct = default) + : base(stream, ct) { } + + public uint GetCrc32() => ~_currentCrc; + + public void ResetCrc() => _currentCrc = 0xffffffff; + + protected void UpdateCrc(byte b) => _currentCrc = RarCRC.CheckCrc(_currentCrc, b); + + protected void UpdateCrc(byte[] bytes, int offset, int count) => + _currentCrc = RarCRC.CheckCrc(_currentCrc, bytes, offset, count); + + protected async ValueTask ReadBytesNoCrcAsync(int count, CancellationToken ct = default) + { + CurrentReadByteCount += count; + var buffer = new byte[count]; + await BaseStream.ReadExactAsync(buffer, 0, count, ct).ConfigureAwait(false); + return buffer; + } + + public override async ValueTask ReadByteAsync(CancellationToken ct = default) + { + var b = await base.ReadByteAsync(ct).ConfigureAwait(false); + _currentCrc = RarCRC.CheckCrc(_currentCrc, b); + return b; + } + + public override async ValueTask ReadBytesAsync( + int count, + CancellationToken ct = default + ) + { + var result = await base.ReadBytesAsync(count, ct).ConfigureAwait(false); + _currentCrc = RarCRC.CheckCrc(_currentCrc, result, 0, result.Length); + return result; + } +} diff --git a/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs b/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs new file mode 100644 index 00000000..188f375e --- /dev/null +++ b/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Rar; + +internal sealed class AsyncRarCryptoBinaryReader : AsyncRarCrcBinaryReader +{ + private readonly Queue _data = new(); + private long _readCount; + private BlockTransformer? _rijndael; + + public AsyncRarCryptoBinaryReader( + Stream stream, + ICryptKey cryptKey, + CancellationToken ct = default + ) + : base(stream, ct) + { + var salt = base.ReadBytesNoCrcAsync(EncryptionConstV5.SIZE_SALT30, ct) + .GetAwaiter() + .GetResult(); + _readCount += EncryptionConstV5.SIZE_SALT30; + _rijndael = new BlockTransformer(cryptKey.Transformer(salt)); + } + + public AsyncRarCryptoBinaryReader(Stream stream, ICryptKey cryptKey, byte[] salt) + : base(stream) => _rijndael = new BlockTransformer(cryptKey.Transformer(salt)); + + public override long CurrentReadByteCount + { + get => _readCount; + protected set + { + // ignore + } + } + + public override void Mark() => _readCount = 0; + + public override async ValueTask ReadByteAsync(CancellationToken ct = default) + { + var result = await ReadAndDecryptBytesAsync(1, ct).ConfigureAwait(false); + return result[0]; + } + + public override async ValueTask ReadBytesAsync( + int count, + CancellationToken ct = default + ) => await ReadAndDecryptBytesAsync(count, ct).ConfigureAwait(false); + + private async ValueTask ReadAndDecryptBytesAsync( + int count, + CancellationToken ct = default + ) + { + var queueSize = _data.Count; + var sizeToRead = count - queueSize; + + if (sizeToRead > 0) + { + var alignedSize = sizeToRead + ((~sizeToRead + 1) & 0xf); + for (var i = 0; i < alignedSize / 16; i++) + { + var cipherText = await ReadBytesNoCrcAsync(16, ct).ConfigureAwait(false); + var readBytes = _rijndael!.ProcessBlock(cipherText); + foreach (var readByte in readBytes) + { + _data.Enqueue(readByte); + } + } + } + + var decryptedBytes = new byte[count]; + + for (var i = 0; i < count; i++) + { + var b = _data.Dequeue(); + decryptedBytes[i] = b; + UpdateCrc(b); + } + + _readCount += count; + return decryptedBytes; + } + + public void ClearQueue() => _data.Clear(); + + public void SkipQueue() + { + var position = BaseStream.Position; + BaseStream.Position = position + _data.Count; + ClearQueue(); + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs index f819b478..b8370cbe 100644 --- a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs @@ -1,5 +1,8 @@ #nullable disable +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; @@ -13,4 +16,14 @@ internal class ArchiveCryptHeader : RarHeader protected override void ReadFinish(MarkingBinaryReader reader) => CryptInfo = new Rar5CryptoInfo(reader, false); + + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + CryptInfo = await Rar5CryptoInfo + .CreateAsync(reader, false, cancellationToken) + .ConfigureAwait(false); + } } diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs index e8f344ad..ecce262d 100644 --- a/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs @@ -1,3 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; @@ -33,6 +36,37 @@ internal sealed class ArchiveHeader : RarHeader } } + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + if (IsRar5) + { + Flags = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + if (HasFlag(ArchiveFlagsV5.HAS_VOLUME_NUMBER)) + { + VolumeNumber = (int)await reader.ReadRarVIntUInt32Async().ConfigureAwait(false); + } + // later: we may have a locator record if we need it + //if (ExtraSize != 0) { + // ReadLocator(reader); + //} + } + else + { + Flags = HeaderFlags; + HighPosAv = await reader.ReadInt16Async(cancellationToken).ConfigureAwait(false); + PosAv = await reader.ReadInt32Async(cancellationToken).ConfigureAwait(false); + if (HasFlag(ArchiveFlagsV4.ENCRYPT_VER)) + { + EncryptionVersion = await reader + .ReadByteAsync(cancellationToken) + .ConfigureAwait(false); + } + } + } + private ushort Flags { get; set; } private bool HasFlag(ushort flag) => (Flags & flag) == flag; diff --git a/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs index f5bc4523..e54d02ca 100644 --- a/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs @@ -1,4 +1,6 @@ -using SharpCompress.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; @@ -27,6 +29,29 @@ internal class EndArchiveHeader : RarHeader } } + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + if (IsRar5) + { + Flags = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + } + else + { + Flags = HeaderFlags; + if (HasFlag(EndArchiveFlagsV4.DATA_CRC)) + { + ArchiveCrc = await reader.ReadInt32Async(cancellationToken).ConfigureAwait(false); + } + if (HasFlag(EndArchiveFlagsV4.VOLUME_NUMBER)) + { + VolumeNumber = await reader.ReadInt16Async(cancellationToken).ConfigureAwait(false); + } + } + } + private ushort Flags { get; set; } private bool HasFlag(ushort flag) => (Flags & flag) == flag; diff --git a/src/SharpCompress/Common/Rar/Headers/FileHeader.cs b/src/SharpCompress/Common/Rar/Headers/FileHeader.cs index 0aa9fc0d..b33936dc 100644 --- a/src/SharpCompress/Common/Rar/Headers/FileHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/FileHeader.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; #if !Rar2017_64bit using size_t = System.UInt32; @@ -32,6 +34,33 @@ internal class FileHeader : RarHeader } } + public static new async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + HeaderType headerType, + CancellationToken cancellationToken = default + ) + { + var fh = new FileHeader(header, reader, headerType); + await fh.ReadFinishAsync(reader, cancellationToken).ConfigureAwait(false); + return fh; + } + + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + if (IsRar5) + { + await ReadFromReaderV5Async(reader, cancellationToken).ConfigureAwait(false); + } + else + { + await ReadFromReaderV4Async(reader, cancellationToken).ConfigureAwait(false); + } + } + private void ReadFromReaderV5(MarkingBinaryReader reader) { Flags = reader.ReadRarVIntUInt16(); @@ -205,6 +234,162 @@ internal class FileHeader : RarHeader } } + private async Task ReadFromReaderV5Async( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + Flags = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + + var lvalue = checked((long)await reader.ReadRarVIntAsync().ConfigureAwait(false)); + + UncompressedSize = HasFlag(FileFlagsV5.UNPACKED_SIZE_UNKNOWN) ? long.MaxValue : lvalue; + + FileAttributes = await reader.ReadRarVIntUInt32Async().ConfigureAwait(false); + + if (HasFlag(FileFlagsV5.HAS_MOD_TIME)) + { + var value = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + FileLastModifiedTime = Utility.UnixTimeToDateTime(value); + } + + if (HasFlag(FileFlagsV5.HAS_CRC32)) + { + FileCrc = await reader.ReadBytesAsync(4, cancellationToken).ConfigureAwait(false); + } + + var compressionInfo = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + + CompressionAlgorithm = (byte)((compressionInfo & 0x3f) + 50); + IsSolid = (compressionInfo & 0x40) == 0x40; + CompressionMethod = (byte)((compressionInfo >> 7) & 0x7); + WindowSize = IsDirectory ? 0 : ((size_t)0x20000) << ((compressionInfo >> 10) & 0xf); + + HostOs = await reader.ReadRarVIntByteAsync().ConfigureAwait(false); + + var nameSize = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + + var b = await reader.ReadBytesAsync(nameSize, cancellationToken).ConfigureAwait(false); + FileName = ConvertPathV5(Encoding.UTF8.GetString(b, 0, b.Length)); + + if (ExtraSize != RemainingHeaderBytes(reader)) + { + throw new InvalidFormatException("rar5 header size / extra size inconsistency"); + } + + const ushort FHEXTRA_CRYPT = 0x01; + const ushort FHEXTRA_HASH = 0x02; + const ushort FHEXTRA_HTIME = 0x03; + const ushort FHEXTRA_REDIR = 0x05; + + while (RemainingHeaderBytes(reader) > 0) + { + var size = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + var n = RemainingHeaderBytes(reader); + var type = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + switch (type) + { + case FHEXTRA_CRYPT: + { + Rar5CryptoInfo = new Rar5CryptoInfo(reader, true); + if (Rar5CryptoInfo.PswCheck.All(singleByte => singleByte == 0)) + { + Rar5CryptoInfo = null; + } + } + break; + case FHEXTRA_HASH: + { + const uint FHEXTRA_HASH_BLAKE2 = 0x0; + const int BLAKE2_DIGEST_SIZE = 0x20; + if ( + (uint)await reader.ReadRarVIntAsync().ConfigureAwait(false) + == FHEXTRA_HASH_BLAKE2 + ) + { + _hash = await reader + .ReadBytesAsync(BLAKE2_DIGEST_SIZE, cancellationToken) + .ConfigureAwait(false); + } + } + break; + case FHEXTRA_HTIME: + { + var flags = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + var isWindowsTime = (flags & 1) == 0; + if ((flags & 0x2) == 0x2) + { + FileLastModifiedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ); + } + if ((flags & 0x4) == 0x4) + { + FileCreatedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ); + } + if ((flags & 0x8) == 0x8) + { + FileLastAccessedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ); + } + } + break; + case FHEXTRA_REDIR: + { + RedirType = await reader.ReadRarVIntByteAsync().ConfigureAwait(false); + RedirFlags = await reader.ReadRarVIntByteAsync().ConfigureAwait(false); + var nn = await reader.ReadRarVIntUInt16Async().ConfigureAwait(false); + var bb = await reader + .ReadBytesAsync(nn, cancellationToken) + .ConfigureAwait(false); + RedirTargetName = ConvertPathV5(Encoding.UTF8.GetString(bb, 0, bb.Length)); + } + break; + default: + break; + } + + var did = n - RemainingHeaderBytes(reader); + var drain = size - did; + if (drain > 0) + { + await reader.ReadBytesAsync(drain, cancellationToken).ConfigureAwait(false); + } + } + + if (AdditionalDataSize != 0) + { + CompressedSize = AdditionalDataSize; + } + } + + private static async ValueTask ReadExtendedTimeV5Async( + AsyncMarkingBinaryReader reader, + bool isWindowsTime, + CancellationToken cancellationToken = default + ) + { + if (isWindowsTime) + { + var value = await reader.ReadInt64Async(cancellationToken).ConfigureAwait(false); + return DateTime.FromFileTime(value); + } + else + { + var value = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + return Utility.UnixTimeToDateTime(value); + } + } + private static DateTime ReadExtendedTimeV5(MarkingBinaryReader reader, bool isWindowsTime) { if (isWindowsTime) @@ -217,6 +402,169 @@ internal class FileHeader : RarHeader } } + private async Task ReadFromReaderV4Async( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + Flags = HeaderFlags; + IsSolid = HasFlag(FileFlagsV4.SOLID); + WindowSize = IsDirectory + ? 0U + : ((size_t)0x10000) << ((Flags & FileFlagsV4.WINDOW_MASK) >> 5); + + var lowUncompressedSize = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + + HostOs = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + + FileCrc = await reader.ReadBytesAsync(4, cancellationToken).ConfigureAwait(false); + + var value = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + FileLastModifiedTime = Utility.DosDateToDateTime(value); + + CompressionAlgorithm = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + var value1 = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + CompressionMethod = (byte)(value1 - 0x30); + + var nameSize = await reader.ReadInt16Async(cancellationToken).ConfigureAwait(false); + + FileAttributes = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + + uint highCompressedSize = 0; + uint highUncompressedkSize = 0; + if (HasFlag(FileFlagsV4.LARGE)) + { + highCompressedSize = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + highUncompressedkSize = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + } + else + { + if (lowUncompressedSize == 0xffffffff) + { + lowUncompressedSize = 0xffffffff; + highUncompressedkSize = int.MaxValue; + } + } + CompressedSize = UInt32To64(highCompressedSize, checked((uint)AdditionalDataSize)); + UncompressedSize = UInt32To64(highUncompressedkSize, lowUncompressedSize); + + nameSize = nameSize > 4 * 1024 ? (short)(4 * 1024) : nameSize; + + var fileNameBytes = await reader + .ReadBytesAsync(nameSize, cancellationToken) + .ConfigureAwait(false); + + const int newLhdSize = 32; + + switch (HeaderCode) + { + case HeaderCodeV.RAR4_FILE_HEADER: + { + if (HasFlag(FileFlagsV4.UNICODE)) + { + var length = 0; + while (length < fileNameBytes.Length && fileNameBytes[length] != 0) + { + length++; + } + if (length != nameSize) + { + length++; + } + FileName = FileNameDecoder.Decode(fileNameBytes, length); + } + else + { + FileName = ArchiveEncoding.Decode(fileNameBytes); + } + FileName = ConvertPathV4(FileName); + } + break; + case HeaderCodeV.RAR4_NEW_SUB_HEADER: + { + var datasize = HeaderSize - newLhdSize - nameSize; + if (HasFlag(FileFlagsV4.SALT)) + { + datasize -= EncryptionConstV5.SIZE_SALT30; + } + if (datasize > 0) + { + SubData = await reader + .ReadBytesAsync(datasize, cancellationToken) + .ConfigureAwait(false); + } + + if (NewSubHeaderType.SUBHEAD_TYPE_RR.Equals(fileNameBytes)) + { + if (SubData is null) + { + throw new InvalidFormatException(); + } + RecoverySectors = + SubData[8] + + (SubData[9] << 8) + + (SubData[10] << 16) + + (SubData[11] << 24); + } + } + break; + } + + if (HasFlag(FileFlagsV4.SALT)) + { + R4Salt = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_SALT30, cancellationToken) + .ConfigureAwait(false); + } + if (HasFlag(FileFlagsV4.EXT_TIME)) + { + if (RemainingHeaderBytes(reader) >= 2) + { + var extendedFlags = await reader + .ReadUInt16Async(cancellationToken) + .ConfigureAwait(false); + if (FileLastModifiedTime is null) + { + FileLastModifiedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + FileLastModifiedTime, + reader, + 0, + cancellationToken + ); + } + + FileCreatedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 1, + cancellationToken + ); + FileLastAccessedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 2, + cancellationToken + ); + FileArchivedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 3, + cancellationToken + ); + } + } + } + private static string ConvertPathV5(string path) { if (Path.DirectorySeparatorChar == '\\') @@ -365,6 +713,43 @@ internal class FileHeader : RarHeader return l + y; } + private static async ValueTask ProcessExtendedTimeV4Async( + ushort extendedFlags, + DateTime? time, + AsyncMarkingBinaryReader reader, + int i, + CancellationToken cancellationToken = default + ) + { + var rmode = (uint)extendedFlags >> ((3 - i) * 4); + if ((rmode & 8) == 0) + { + return null; + } + if (i != 0) + { + var dosTime = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + time = Utility.DosDateToDateTime(dosTime); + } + if ((rmode & 4) == 0 && time is not null) + { + time = time.Value.AddSeconds(1); + } + uint nanosecondHundreds = 0; + var count = (int)rmode & 3; + for (var j = 0; j < count; j++) + { + var b = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + nanosecondHundreds |= (((uint)b) << ((j + 3 - count) * 8)); + } + + if (time is not null) + { + return time.Value.AddMilliseconds(nanosecondHundreds * Math.Pow(10, -4)); + } + return null; + } + private static DateTime? ProcessExtendedTimeV4( ushort extendedFlags, DateTime? time, diff --git a/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs index cf8db3c0..e1ee470e 100644 --- a/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs @@ -1,3 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; @@ -22,6 +25,17 @@ internal sealed class ProtectHeader : RarHeader Mark = reader.ReadBytes(8); } + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + Version = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + RecSectors = await reader.ReadUInt16Async(cancellationToken).ConfigureAwait(false); + TotalBlocks = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + Mark = await reader.ReadBytesAsync(8, cancellationToken).ConfigureAwait(false); + } + internal uint DataSize => checked((uint)AdditionalDataSize); internal byte Version { get; private set; } internal ushort RecSectors { get; private set; } diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs index 3ca76ab9..83f8686c 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs @@ -1,4 +1,7 @@ using System; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; @@ -26,6 +29,81 @@ internal class RarHeader : IRarHeader } } + internal static async ValueTask TryReadBaseAsync( + AsyncRarCrcBinaryReader reader, + bool isRar5, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken = default + ) + { + try + { + return await CreateBaseAsync(reader, isRar5, archiveEncoding, cancellationToken) + .ConfigureAwait(false); + } + catch (InvalidFormatException) + { + return null; + } + } + + private static async ValueTask CreateBaseAsync( + AsyncRarCrcBinaryReader reader, + bool isRar5, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken = default + ) + { + var header = new RarHeader(HeaderType.Null, isRar5) { ArchiveEncoding = archiveEncoding }; + if (isRar5) + { + header.HeaderCrc = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + reader.ResetCrc(); + header.HeaderSize = (int) + await reader.ReadRarVIntUInt32Async(3, cancellationToken).ConfigureAwait(false); + reader.Mark(); + header.HeaderCode = await reader.ReadRarVIntByteAsync(2).ConfigureAwait(false); + header.HeaderFlags = await reader + .ReadRarVIntUInt16Async(2, cancellationToken) + .ConfigureAwait(false); + + if (header.HasHeaderFlag(HeaderFlagsV5.HAS_EXTRA)) + { + header.ExtraSize = await reader.ReadRarVIntUInt32Async().ConfigureAwait(false); + } + if (header.HasHeaderFlag(HeaderFlagsV5.HAS_DATA)) + { + header.AdditionalDataSize = (long) + await reader.ReadRarVIntAsync().ConfigureAwait(false); + } + } + else + { + reader.Mark(); + header.HeaderCrc = await reader + .ReadUInt16Async(cancellationToken) + .ConfigureAwait(false); + reader.ResetCrc(); + header.HeaderCode = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + header.HeaderFlags = await reader + .ReadUInt16Async(cancellationToken) + .ConfigureAwait(false); + header.HeaderSize = await reader + .ReadInt16Async(cancellationToken) + .ConfigureAwait(false); + if (header.HasHeaderFlag(HeaderFlagsV4.HAS_DATA)) + { + header.AdditionalDataSize = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + } + } + + return header; + } + private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding) { _headerType = HeaderType.Null; @@ -64,6 +142,13 @@ internal class RarHeader : IRarHeader } } + private RarHeader(HeaderType headerType, bool isRar5) + { + _headerType = headerType; + _isRar5 = isRar5; + ArchiveEncoding = null!; + } + protected RarHeader(RarHeader header, RarCrcBinaryReader reader, HeaderType headerType) { _headerType = headerType; @@ -86,12 +171,50 @@ internal class RarHeader : IRarHeader VerifyHeaderCrc(reader.GetCrc32()); } + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + HeaderType headerType, + CancellationToken cancellationToken = default + ) + { + var result = new RarHeader(headerType, header.IsRar5) + { + HeaderCrc = header.HeaderCrc, + HeaderCode = header.HeaderCode, + HeaderFlags = header.HeaderFlags, + HeaderSize = header.HeaderSize, + ExtraSize = header.ExtraSize, + AdditionalDataSize = header.AdditionalDataSize, + ArchiveEncoding = header.ArchiveEncoding, + }; + + await result.ReadFinishAsync(reader, cancellationToken).ConfigureAwait(false); + + var n = result.RemainingHeaderBytes(reader); + if (n > 0) + { + await reader.ReadBytesAsync(n, cancellationToken).ConfigureAwait(false); + } + + result.VerifyHeaderCrc(reader.GetCrc32()); + return result; + } + + protected int RemainingHeaderBytes(AsyncMarkingBinaryReader reader) => + checked(HeaderSize - (int)reader.CurrentReadByteCount); + protected int RemainingHeaderBytes(MarkingBinaryReader reader) => checked(HeaderSize - (int)reader.CurrentReadByteCount); protected virtual void ReadFinish(MarkingBinaryReader reader) => throw new NotImplementedException(); + protected virtual async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) => throw new NotImplementedException(); + private void VerifyHeaderCrc(uint crc32) { var b = (IsRar5 ? crc32 : (ushort)crc32) == HeaderCrc; @@ -105,25 +228,25 @@ internal class RarHeader : IRarHeader protected bool IsRar5 => _isRar5; - protected uint HeaderCrc { get; } + protected uint HeaderCrc { get; private set; } - internal byte HeaderCode { get; } + internal byte HeaderCode { get; private set; } - protected ushort HeaderFlags { get; } + protected ushort HeaderFlags { get; private set; } protected bool HasHeaderFlag(ushort flag) => (HeaderFlags & flag) == flag; - protected int HeaderSize { get; } + protected int HeaderSize { get; private set; } - internal IArchiveEncoding ArchiveEncoding { get; } + internal IArchiveEncoding ArchiveEncoding { get; private set; } /// /// Extra header size. /// - protected uint ExtraSize { get; } + protected uint ExtraSize { get; private set; } /// /// Size of additional data (eg file contents) /// - protected long AdditionalDataSize { get; } + protected long AdditionalDataSize { get; private set; } } diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs index 902e595f..65772072 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; using SharpCompress.Readers; @@ -40,6 +43,35 @@ public class RarHeaderFactory } } + public async IAsyncEnumerable ReadHeadersAsync( + Stream stream, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var markHeader = await MarkHeader + .ReadAsync(stream, Options.LeaveStreamOpen, Options.LookForHeader, cancellationToken) + .ConfigureAwait(false); + _isRar5 = markHeader.IsRar5; + yield return markHeader; + + RarHeader? header; + while ( + (header = await TryReadNextHeaderAsync(stream, cancellationToken).ConfigureAwait(false)) + != null + ) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return header; + if (header.HeaderType == HeaderType.EndArchive) + { + // End of archive marker. RAR does not read anything after this header letting to use third + // party tools to add extra information such as a digital signature to archive. + yield break; + } + } + } + private RarHeader? TryReadNextHeader(Stream stream) { RarCrcBinaryReader reader; @@ -92,7 +124,7 @@ public class RarHeaderFactory case HeaderCodeV.RAR4_PROTECT_HEADER: { var ph = new ProtectHeader(header, reader); - // skip the recovery record data, we do not use it. + // skip for recovery record data, we do not use it. switch (StreamingMode) { case StreamingMode.Seekable: @@ -198,6 +230,181 @@ public class RarHeaderFactory } } + private async ValueTask TryReadNextHeaderAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + AsyncRarCrcBinaryReader reader; + if (!IsEncrypted) + { + reader = new AsyncRarCrcBinaryReader(stream); + } + else + { + if (Options.Password is null) + { + throw new CryptographicException( + "Encrypted Rar archive has no password specified." + ); + } + + if (_isRar5 && _cryptInfo != null) + { + var markingReader = new MarkingBinaryReader(stream); + _cryptInfo.ReadInitV(markingReader); + var _headerKey = new CryptKey5(Options.Password!, _cryptInfo); + + reader = new AsyncRarCrcBinaryReader(stream, _headerKey, _cryptInfo.Salt); + } + else + { + var key = new CryptKey3(Options.Password); + reader = new AsyncRarCrcBinaryReader(stream, key); + } + } + + var header = await RarHeader + .TryReadBaseAsync(reader, _isRar5, Options.ArchiveEncoding, cancellationToken) + .ConfigureAwait(false); + if (header is null) + { + return null; + } + + switch (header.HeaderCode) + { + case HeaderCodeV.RAR5_ARCHIVE_HEADER: + case HeaderCodeV.RAR4_ARCHIVE_HEADER: + { + var ah = await ArchiveHeader + .CreateAsync(header, reader, cancellationToken) + .ConfigureAwait(false); + if (ah.IsEncrypted == true) + { + //!!! rar5 we don't know yet + return await CreateRarHeaderAsync(header, ah, reader, cancellationToken) + .ConfigureAwait(false); + } + return ah; + } + + case HeaderCodeV.RAR4_PROTECT_HEADER: + { + var ph = await ProtectHeader + .CreateAsync(header, reader, cancellationToken) + .ConfigureAwait(false); + await SkipProtectHeaderDataAsync(ph, reader, cancellationToken) + .ConfigureAwait(false); + return await CreateRarHeaderAsync(header, ph, reader, cancellationToken) + .ConfigureAwait(false); + } + + case HeaderCodeV.RAR5_SERVICE_HEADER: + { + var fh = await FileHeader + .CreateAsync(header, reader, HeaderType.Service, cancellationToken) + .ConfigureAwait(false); + if (fh.FileName == "CMT") + { + fh.PackedStream = new ReadOnlySubStream(reader.BaseStream, fh.CompressedSize); + } + else + { + await SkipDataAsync(fh, reader, cancellationToken).ConfigureAwait(false); + } + return fh; + } + + case HeaderCodeV.RAR4_NEW_SUB_HEADER: + { + var fh = await FileHeader + .CreateAsync(header, reader, HeaderType.NewSub, cancellationToken) + .ConfigureAwait(false); + await SkipDataAsync(fh, reader, cancellationToken).ConfigureAwait(false); + return fh; + } + + case HeaderCodeV.RAR5_FILE_HEADER: + case HeaderCodeV.RAR4_FILE_HEADER: + { + var fh = await FileHeader + .CreateAsync(header, reader, HeaderType.File, cancellationToken) + .ConfigureAwait(false); + await ProcessFileHeaderAsync(fh, reader, cancellationToken).ConfigureAwait(false); + return fh; + } + case HeaderCodeV.RAR5_END_ARCHIVE_HEADER: + case HeaderCodeV.RAR4_END_ARCHIVE_HEADER: + { + var eh = await EndArchiveHeader + .CreateAsync(header, reader, cancellationToken) + .ConfigureAwait(false); + return await CreateRarHeaderAsync(header, eh, reader, cancellationToken) + .ConfigureAwait(false); + } + case HeaderCodeV.RAR5_ARCHIVE_ENCRYPTION_HEADER: + { + var cryptoHeader = await ArchiveCryptHeader + .CreateAsync(header, reader, cancellationToken) + .ConfigureAwait(false); + IsEncrypted = true; + _cryptInfo = cryptoHeader.CryptInfo; + + return await CreateRarHeaderAsync(header, cryptoHeader, reader, cancellationToken) + .ConfigureAwait(false); + } + default: + { + throw new InvalidFormatException("Unknown Rar Header: " + header.HeaderCode); + } + } + } + + private static async ValueTask CreateRarHeaderAsync( + RarHeader header, + RarHeader result, + RarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + await result.ReadFinishAsync(reader, cancellationToken).ConfigureAwait(false); + + var n = result.RemainingHeaderBytes(reader); + if (n > 0) + { + await reader.ReadBytesAsync(n, cancellationToken).ConfigureAwait(false); + } + + result.VerifyHeaderCrc(reader.GetCrc32()); + return result; + } + + private static async ValueTask SkipProtectHeaderDataAsync( + ProtectHeader ph, + RarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + switch (ph.StreamingMode) + { + case StreamingMode.Seekable: + { + reader.BaseStream.Position += ph.DataSize; + } + break; + case StreamingMode.Streaming: + { + reader.BaseStream.Skip(ph.DataSize); + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } + private void SkipData(FileHeader fh, RarCrcBinaryReader reader) { switch (StreamingMode) @@ -210,7 +417,6 @@ public class RarHeaderFactory break; case StreamingMode.Streaming: { - //skip the data because it's useless? reader.BaseStream.Skip(fh.CompressedSize); } break; @@ -220,4 +426,108 @@ public class RarHeaderFactory } } } + + private static void ProcessFileHeader(FileHeader fh, RarCrcBinaryReader reader) + { + switch (fh.StreamingMode) + { + case StreamingMode.Seekable: + { + fh.DataStartPosition = reader.BaseStream.Position; + reader.BaseStream.Position += fh.CompressedSize; + } + break; + case StreamingMode.Streaming: + { + var ms = new ReadOnlySubStream(reader.BaseStream, fh.CompressedSize); + if (fh.R4Salt is null && fh.Rar5CryptoInfo is null) + { + fh.PackedStream = ms; + } + else + { + fh.PackedStream = new RarCryptoWrapper( + ms, + fh.R4Salt is null ? fh.Rar5CryptoInfo.NotNull().Salt : fh.R4Salt, + fh.R4Salt is null + ? new CryptKey5(Options.Password, fh.Rar5CryptoInfo.NotNull()) + : new CryptKey3(Options.Password) + ); + } + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } + + private static async ValueTask SkipDataAsync( + FileHeader fh, + RarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + switch (fh.StreamingMode) + { + case StreamingMode.Seekable: + { + fh.DataStartPosition = reader.BaseStream.Position; + reader.BaseStream.Position += fh.CompressedSize; + } + break; + case StreamingMode.Streaming: + { + await reader + .BaseStream.SkipAsync((int)fh.CompressedSize, cancellationToken) + .ConfigureAwait(false); + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } + + private static async ValueTask ProcessFileHeaderAsync( + FileHeader fh, + RarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + switch (fh.StreamingMode) + { + case StreamingMode.Seekable: + { + fh.DataStartPosition = reader.BaseStream.Position; + reader.BaseStream.Position += fh.CompressedSize; + } + break; + case StreamingMode.Streaming: + { + var ms = new ReadOnlySubStream(reader.BaseStream, fh.CompressedSize); + if (fh.R4Salt is null && fh.Rar5CryptoInfo is null) + { + fh.PackedStream = ms; + } + else + { + fh.PackedStream = new RarCryptoWrapper( + ms, + fh.R4Salt is null ? fh.Rar5CryptoInfo.NotNull().Salt : fh.R4Salt, + fh.R4Salt is null + ? new CryptKey5(Options.Password, fh.Rar5CryptoInfo.NotNull()) + : new CryptKey3(Options.Password) + ); + } + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } } diff --git a/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs b/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs index 7b8edff9..fa59f513 100644 --- a/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs +++ b/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs @@ -1,5 +1,7 @@ using System; using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Rar.Headers; using SharpCompress.IO; @@ -9,6 +11,58 @@ internal class Rar5CryptoInfo { public Rar5CryptoInfo() { } + public static async ValueTask CreateAsync( + AsyncMarkingBinaryReader reader, + bool readInitV, + CancellationToken cancellationToken = default + ) + { + var info = new Rar5CryptoInfo(); + + var cryptVersion = await reader.ReadRarVIntUInt32Async().ConfigureAwait(false); + if (cryptVersion > EncryptionConstV5.VERSION) + { + throw new CryptographicException($"Unsupported crypto version of {cryptVersion}"); + } + var encryptionFlags = await reader.ReadRarVIntUInt32Async().ConfigureAwait(false); + info.UsePswCheck = FlagUtility.HasFlag( + encryptionFlags, + EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK + ); + info.LG2Count = await reader.ReadRarVIntByteAsync(1).ConfigureAwait(false); + + if (info.LG2Count > EncryptionConstV5.CRYPT5_KDF_LG2_COUNT_MAX) + { + throw new CryptographicException($"Unsupported LG2 count of {info.LG2Count}."); + } + + info.Salt = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_SALT50, cancellationToken) + .ConfigureAwait(false); + + if (readInitV) + { + await info.ReadInitVAsync(reader, cancellationToken).ConfigureAwait(false); + } + + if (info.UsePswCheck) + { + info.PswCheck = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_PSWCHECK, cancellationToken) + .ConfigureAwait(false); + var _pswCheckCsm = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_PSWCHECK_CSUM, cancellationToken) + .ConfigureAwait(false); + + var sha = SHA256.Create(); + info.UsePswCheck = sha.ComputeHash(info.PswCheck) + .AsSpan() + .StartsWith(_pswCheckCsm.AsSpan()); + } + + return info; + } + public Rar5CryptoInfo(MarkingBinaryReader reader, bool readInitV) { var cryptVersion = reader.ReadRarVIntUInt32(); @@ -45,6 +99,14 @@ internal class Rar5CryptoInfo public void ReadInitV(MarkingBinaryReader reader) => InitV = reader.ReadBytes(EncryptionConstV5.SIZE_INITV); + public async ValueTask ReadInitVAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) => + InitV = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_INITV, cancellationToken) + .ConfigureAwait(false); + public bool UsePswCheck = false; public int LG2Count = 0; diff --git a/src/SharpCompress/IO/AsyncMarkingBinaryReader.cs b/src/SharpCompress/IO/AsyncMarkingBinaryReader.cs new file mode 100644 index 00000000..2e85c66d --- /dev/null +++ b/src/SharpCompress/IO/AsyncMarkingBinaryReader.cs @@ -0,0 +1,186 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.IO; + +internal class AsyncMarkingBinaryReader : IDisposable +{ + private readonly AsyncBinaryReader _asyncReader; + private readonly bool _leaveOpen; + private bool _disposed; + + public AsyncMarkingBinaryReader(Stream stream, CancellationToken cancellationToken = default) + { + _asyncReader = new AsyncBinaryReader(stream, leaveOpen: false); + } + + public Stream BaseStream => _asyncReader.BaseStream; + + public virtual long CurrentReadByteCount { get; protected set; } + + public virtual void Mark() => CurrentReadByteCount = 0; + + public virtual async ValueTask ReadBooleanAsync(CancellationToken ct = default) + { + var b = await ReadByteAsync(ct).ConfigureAwait(false); + return b != 0; + } + + public virtual async ValueTask ReadByteAsync(CancellationToken ct = default) + { + CurrentReadByteCount++; + return await _asyncReader.ReadByteAsync(ct).ConfigureAwait(false); + } + + public virtual async ValueTask ReadBytesAsync(int count, CancellationToken ct = default) + { + CurrentReadByteCount += count; + var buffer = new byte[count]; + await _asyncReader.ReadBytesAsync(buffer, 0, count, ct).ConfigureAwait(false); + return buffer; + } + + public virtual async ValueTask ReadInt16Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(2, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadInt16LittleEndian(bytes); + } + + public virtual async ValueTask ReadInt32Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(4, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + public virtual async ValueTask ReadInt64Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(8, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadInt64LittleEndian(bytes); + } + + public virtual async ValueTask ReadSByteAsync(CancellationToken ct = default) + { + var b = await ReadByteAsync(ct).ConfigureAwait(false); + return (sbyte)b; + } + + public virtual async ValueTask ReadUInt16Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(2, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt16LittleEndian(bytes); + } + + public virtual async ValueTask ReadUInt32Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(4, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + public virtual async ValueTask ReadUInt64Async(CancellationToken ct = default) + { + var bytes = await ReadBytesAsync(8, ct).ConfigureAwait(false); + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } + + public virtual async ValueTask ReadRarVIntAsync( + int maxBytes = 10, + CancellationToken ct = default + ) => await DoReadRarVIntAsync((maxBytes - 1) * 7, ct).ConfigureAwait(false); + + private async ValueTask DoReadRarVIntAsync(int maxShift, CancellationToken ct) + { + var shift = 0; + ulong result = 0; + do + { + var b0 = await ReadByteAsync(ct).ConfigureAwait(false); + var b1 = ((uint)b0) & 0x7f; + ulong n = b1; + var shifted = n << shift; + if (n != shifted >> shift) + { + break; + } + result |= shifted; + if (b0 == b1) + { + return result; + } + shift += 7; + } while (shift <= maxShift); + + throw new FormatException("malformed vint"); + } + + public virtual async ValueTask ReadRarVIntUInt32Async( + int maxBytes = 5, + CancellationToken ct = default + ) => await DoReadRarVIntUInt32Async((maxBytes - 1) * 7, ct).ConfigureAwait(false); + + private async ValueTask DoReadRarVIntUInt32Async(int maxShift, CancellationToken ct) + { + var shift = 0; + uint result = 0; + do + { + var b0 = await ReadByteAsync(ct).ConfigureAwait(false); + var b1 = ((uint)b0) & 0x7f; + var n = b1; + var shifted = n << shift; + if (n != shifted >> shift) + { + break; + } + result |= shifted; + if (b0 == b1) + { + return result; + } + shift += 7; + } while (shift <= maxShift); + + throw new FormatException("malformed vint"); + } + + public virtual async ValueTask ReadRarVIntUInt16Async( + int maxBytes = 3, + CancellationToken ct = default + ) => + checked( + (ushort)await DoReadRarVIntUInt32Async((maxBytes - 1) * 7, ct).ConfigureAwait(false) + ); + + public virtual async ValueTask ReadRarVIntByteAsync( + int maxBytes = 2, + CancellationToken ct = default + ) => + checked((byte)await DoReadRarVIntUInt32Async((maxBytes - 1) * 7, ct).ConfigureAwait(false)); + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _asyncReader.Dispose(); + } + +#if NET6_0_OR_GREATER + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + await _asyncReader.DisposeAsync().ConfigureAwait(false); + } +#endif +}