From a8f47237d7c5edbe7d5cbcdb2dac757f612b9ca2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 22 Jan 2026 13:38:20 +0000 Subject: [PATCH] divide async and sync into new files --- .../Archives/AbstractArchive.Async.cs | 93 ++ src/SharpCompress/Archives/AbstractArchive.cs | 89 +- .../Archives/AbstractWritableArchive.Async.cs | 123 +++ .../Archives/AbstractWritableArchive.cs | 112 +-- .../Archives/GZip/GZipArchive.Async.cs | 74 ++ .../Archives/GZip/GZipArchive.cs | 59 +- .../Archives/Rar/RarArchive.Async.cs | 53 ++ src/SharpCompress/Archives/Rar/RarArchive.cs | 37 +- .../Archives/Rar/RarArchiveEntry.Async.cs | 43 + .../Archives/Rar/RarArchiveEntry.cs | 32 +- .../SevenZip/SevenZipArchive.Async.cs | 37 + .../Archives/SevenZip/SevenZipArchive.cs | 25 +- .../Archives/Tar/TarArchive.Async.cs | 88 ++ src/SharpCompress/Archives/Tar/TarArchive.cs | 73 +- .../Archives/Zip/ZipArchive.Async.cs | 76 ++ src/SharpCompress/Archives/Zip/ZipArchive.cs | 62 +- .../Archives/Zip/ZipArchiveEntry.Async.cs | 22 + .../Archives/Zip/ZipArchiveEntry.cs | 14 +- .../Common/Ace/Headers/AceFileHeader.Async.cs | 111 +++ .../Common/Ace/Headers/AceFileHeader.cs | 103 +- .../Common/Ace/Headers/AceHeader.Async.cs | 69 ++ .../Common/Ace/Headers/AceHeader.cs | 62 +- .../Common/Ace/Headers/AceMainHeader.Async.cs | 83 ++ .../Common/Ace/Headers/AceMainHeader.cs | 74 +- .../Common/Arj/Headers/ArjHeader.Async.cs | 132 +++ .../Common/Arj/Headers/ArjHeader.cs | 123 +-- .../Arj/Headers/ArjLocalHeader.Async.cs | 24 + .../Common/Arj/Headers/ArjLocalHeader.cs | 18 +- .../Common/Arj/Headers/ArjMainHeader.Async.cs | 18 + .../Common/Arj/Headers/ArjMainHeader.cs | 12 +- src/SharpCompress/Common/EntryStream.Async.cs | 84 ++ src/SharpCompress/Common/EntryStream.cs | 76 +- .../Common/ExtractionMethods.Async.cs | 116 +++ src/SharpCompress/Common/ExtractionMethods.cs | 109 +-- .../Common/GZip/GZipFilePart.Async.cs | 133 +++ src/SharpCompress/Common/GZip/GZipFilePart.cs | 124 +-- .../Rar/Headers/ArchiveCryptHeader.Async.cs | 32 + .../Common/Rar/Headers/ArchiveCryptHeader.cs | 27 +- .../Common/Rar/Headers/ArchiveHeader.Async.cs | 53 ++ .../Common/Rar/Headers/ArchiveHeader.cs | 50 +- .../Rar/Headers/EndArchiveHeader.Async.cs | 47 + .../Common/Rar/Headers/EndArchiveHeader.cs | 44 +- .../Common/Rar/Headers/FileHeader.Async.cs | 441 +++++++++ .../Common/Rar/Headers/FileHeader.cs | 465 +-------- .../Common/Rar/Headers/MarkHeader.Async.cs | 132 +++ .../Common/Rar/Headers/MarkHeader.cs | 125 +-- .../Common/Rar/Headers/ProtectHeader.Async.cs | 40 + .../Common/Rar/Headers/ProtectHeader.cs | 37 +- .../Common/Rar/Headers/RarHeader.Async.cs | 115 +++ .../Common/Rar/Headers/RarHeader.cs | 107 +-- .../Rar/Headers/RarHeaderFactory.Async.cs | 256 +++++ .../Common/Rar/Headers/RarHeaderFactory.cs | 246 +---- .../Common/SevenZip/ArchiveReader.Async.cs | 158 ++++ .../Common/SevenZip/ArchiveReader.cs | 146 +-- .../Common/Tar/Headers/TarHeader.Async.cs | 154 +++ .../Common/Tar/Headers/TarHeader.cs | 147 +-- src/SharpCompress/Common/Volume.Async.cs | 17 + src/SharpCompress/Common/Volume.cs | 12 +- .../Zip/Headers/DirectoryEndHeader.Async.cs | 20 + .../Common/Zip/Headers/DirectoryEndHeader.cs | 16 +- .../Zip/Headers/DirectoryEntryHeader.Async.cs | 37 + .../Zip/Headers/DirectoryEntryHeader.cs | 31 +- .../Zip/Headers/LocalEntryHeader.Async.cs | 28 + .../Common/Zip/Headers/LocalEntryHeader.cs | 27 +- .../Headers/Zip64DirectoryEndHeader.Async.cs | 25 + .../Zip/Headers/Zip64DirectoryEndHeader.cs | 21 +- .../Zip64DirectoryEndLocatorHeader.Async.cs | 14 + .../Headers/Zip64DirectoryEndLocatorHeader.cs | 11 +- .../Common/Zip/Headers/ZipFileEntry.Async.cs | 27 + .../Common/Zip/Headers/ZipFileEntry.cs | 24 +- .../Common/Zip/SeekableZipFilePart.Async.cs | 24 + .../Common/Zip/SeekableZipFilePart.cs | 19 +- .../Zip/SeekableZipHeaderFactory.Async.cs | 83 ++ .../Common/Zip/SeekableZipHeaderFactory.cs | 74 +- .../Common/Zip/StreamingZipFilePart.Async.cs | 31 + .../Common/Zip/StreamingZipFilePart.cs | 26 +- .../Zip/StreamingZipHeaderFactory.Async.cs | 341 +++++++ .../Common/Zip/StreamingZipHeaderFactory.cs | 332 +------ .../Common/Zip/ZipFilePart.Async.cs | 264 ++++++ src/SharpCompress/Common/Zip/ZipFilePart.cs | 244 +---- .../Common/Zip/ZipHeaderFactory.Async.cs | 166 ++++ .../Common/Zip/ZipHeaderFactory.cs | 156 +--- .../Compressors/ADC/ADCStream.Async.cs | 108 +++ .../Compressors/ADC/ADCStream.cs | 72 +- .../Compressors/Arj/BitReader.Async.cs | 52 ++ .../Compressors/Arj/BitReader.cs | 51 +- .../Compressors/Arj/LHDecoderStream.Async.cs | 183 ++++ .../Compressors/Arj/LHDecoderStream.cs | 177 +--- .../Compressors/BZip2/BZip2Stream.Async.cs | 36 + .../Compressors/BZip2/BZip2Stream.cs | 27 +- .../BZip2/CBZip2InputStream.Async.cs | 879 ++++++++++++++++++ .../Compressors/BZip2/CBZip2InputStream.cs | 598 +----------- .../Deflate/DeflateStream.Async.cs | 94 ++ .../Compressors/Deflate/DeflateStream.cs | 89 +- .../Compressors/Deflate/GZipStream.Async.cs | 135 +++ .../Compressors/Deflate/GZipStream.cs | 130 +-- .../Compressors/Deflate/ZlibStream.Async.cs | 95 ++ .../Compressors/Deflate/ZlibStream.cs | 90 +- .../Deflate64/Deflate64Stream.Async.cs | 113 +++ .../Compressors/Deflate64/Deflate64Stream.cs | 102 +- .../LZMA/AesDecoderStream.Async.cs | 75 ++ .../Compressors/LZMA/AesDecoderStream.cs | 67 +- .../Compressors/LZMA/LZ/LzOutWindow.Async.cs | 143 +++ .../Compressors/LZMA/LZ/LzOutWindow.cs | 134 +-- .../Compressors/LZMA/LZipStream.Async.cs | 46 + .../Compressors/LZMA/LZipStream.cs | 26 +- .../Compressors/LZMA/LzmaDecoder.Async.cs | 186 ++++ .../Compressors/LZMA/LzmaDecoder.cs | 175 +--- .../Compressors/LZMA/LzmaStream.Async.cs | 332 +++++++ .../Compressors/LZMA/LzmaStream.cs | 322 +------ .../LZMA/Utilites/CrcBuilderStream.Async.cs | 28 + .../LZMA/Utilites/CrcBuilderStream.cs | 20 +- .../Compressors/Lzw/LzwStream.Async.cs | 361 +++++++ .../Compressors/Lzw/LzwStream.cs | 351 +------ .../MultiVolumeReadOnlyAsyncStream.Async.cs | 158 ++++ .../Rar/MultiVolumeReadOnlyAsyncStream.cs | 147 +-- .../Rar/MultiVolumeReadOnlyStream.Async.cs | 138 +++ .../Rar/MultiVolumeReadOnlyStream.cs | 127 +-- .../Rar/RarBLAKE2spStream.Async.cs | 67 ++ .../Compressors/Rar/RarBLAKE2spStream.cs | 57 +- .../Compressors/Rar/RarCrcStream.Async.cs | 64 ++ .../Compressors/Rar/RarCrcStream.cs | 54 +- .../Compressors/Rar/RarStream.Async.cs | 116 +++ .../Compressors/Rar/RarStream.cs | 91 +- .../Compressors/Xz/BinaryUtils.Async.cs | 32 + .../Compressors/Xz/BinaryUtils.cs | 24 +- .../Compressors/Xz/MultiByteIntegers.Async.cs | 48 + .../Compressors/Xz/MultiByteIntegers.cs | 40 +- .../Compressors/Xz/XZBlock.Async.cs | 134 +++ src/SharpCompress/Compressors/Xz/XZBlock.cs | 121 +-- .../Compressors/Xz/XZFooter.Async.cs | 47 + src/SharpCompress/Compressors/Xz/XZFooter.cs | 37 +- .../Compressors/Xz/XZHeader.Async.cs | 48 + src/SharpCompress/Compressors/Xz/XZHeader.cs | 38 +- .../Compressors/Xz/XZIndex.Async.cs | 80 ++ src/SharpCompress/Compressors/Xz/XZIndex.cs | 68 +- .../Compressors/Xz/XZIndexRecord.Async.cs | 21 + .../Compressors/Xz/XZIndexRecord.cs | 14 +- .../Compressors/Xz/XZStream.Async.cs | 135 +++ src/SharpCompress/Compressors/Xz/XZStream.cs | 107 +-- .../ZStandard/CompressionStream.Async.cs | 114 +++ .../ZStandard/CompressionStream.cs | 107 +-- .../ZStandard/DecompressionStream.Async.cs | 91 ++ .../ZStandard/DecompressionStream.cs | 82 +- .../ZStandard/ZStandardStream.Async.cs | 31 + .../Compressors/ZStandard/ZStandardStream.cs | 23 +- .../IO/BufferedSubStream.Async.cs | 82 ++ src/SharpCompress/IO/BufferedSubStream.cs | 74 +- .../IO/ProgressReportingStream.Async.cs | 56 ++ .../IO/ProgressReportingStream.cs | 51 +- .../IO/ReadOnlySubStream.Async.cs | 50 + src/SharpCompress/IO/ReadOnlySubStream.cs | 45 +- .../IO/SharpCompressStream.Async.cs | 179 ++++ src/SharpCompress/IO/SharpCompressStream.cs | 171 +--- src/SharpCompress/IO/SourceStream.Async.cs | 110 +++ src/SharpCompress/IO/SourceStream.cs | 98 +- .../Readers/AbstractReader.Async.cs | 207 +++++ src/SharpCompress/Readers/AbstractReader.cs | 195 +--- .../Readers/GZip/GZipReader.Async.cs | 15 + src/SharpCompress/Readers/GZip/GZipReader.cs | 5 +- .../Readers/Rar/MultiVolumeRarReader.Async.cs | 84 ++ .../Readers/Rar/MultiVolumeRarReader.cs | 71 +- .../Readers/Rar/RarReader.Async.cs | 62 ++ src/SharpCompress/Readers/Rar/RarReader.cs | 40 +- .../Readers/Tar/TarReader.Async.cs | 31 + src/SharpCompress/Readers/Tar/TarReader.cs | 8 +- .../Readers/Zip/ZipReader.Async.cs | 106 +++ src/SharpCompress/Readers/Zip/ZipReader.cs | 92 +- .../Writers/AbstractWriter.Async.cs | 35 + src/SharpCompress/Writers/AbstractWriter.cs | 27 +- .../Writers/Tar/TarWriter.Async.cs | 67 ++ src/SharpCompress/Writers/Tar/TarWriter.cs | 46 - .../Writers/Zip/ZipWriter.Async.cs | 27 + src/SharpCompress/Writers/Zip/ZipWriter.cs | 11 +- 174 files changed, 9313 insertions(+), 7965 deletions(-) create mode 100644 src/SharpCompress/Archives/AbstractArchive.Async.cs create mode 100644 src/SharpCompress/Archives/AbstractWritableArchive.Async.cs create mode 100644 src/SharpCompress/Archives/GZip/GZipArchive.Async.cs create mode 100644 src/SharpCompress/Archives/Rar/RarArchive.Async.cs create mode 100644 src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs create mode 100644 src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs create mode 100644 src/SharpCompress/Archives/Tar/TarArchive.Async.cs create mode 100644 src/SharpCompress/Archives/Zip/ZipArchive.Async.cs create mode 100644 src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs create mode 100644 src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs create mode 100644 src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs create mode 100644 src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs create mode 100644 src/SharpCompress/Common/EntryStream.Async.cs create mode 100644 src/SharpCompress/Common/ExtractionMethods.Async.cs create mode 100644 src/SharpCompress/Common/GZip/GZipFilePart.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/ArchiveHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/ProtectHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/RarHeader.Async.cs create mode 100644 src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs create mode 100644 src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs create mode 100644 src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs create mode 100644 src/SharpCompress/Common/Volume.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs create mode 100644 src/SharpCompress/Common/Zip/Headers/ZipFileEntry.Async.cs create mode 100644 src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs create mode 100644 src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs create mode 100644 src/SharpCompress/Common/Zip/StreamingZipFilePart.Async.cs create mode 100644 src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs create mode 100644 src/SharpCompress/Common/Zip/ZipFilePart.Async.cs create mode 100644 src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs create mode 100644 src/SharpCompress/Compressors/ADC/ADCStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Arj/BitReader.Async.cs create mode 100644 src/SharpCompress/Compressors/Arj/LHDecoderStream.Async.cs create mode 100644 src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs create mode 100644 src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Deflate64/Deflate64Stream.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/AesDecoderStream.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs create mode 100644 src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Rar/RarStream.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/BinaryUtils.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/MultiByteIntegers.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZBlock.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZFooter.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZHeader.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZIndex.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZIndexRecord.Async.cs create mode 100644 src/SharpCompress/Compressors/Xz/XZStream.Async.cs create mode 100644 src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs create mode 100644 src/SharpCompress/Compressors/ZStandard/DecompressionStream.Async.cs create mode 100644 src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs create mode 100644 src/SharpCompress/IO/BufferedSubStream.Async.cs create mode 100644 src/SharpCompress/IO/ProgressReportingStream.Async.cs create mode 100644 src/SharpCompress/IO/ReadOnlySubStream.Async.cs create mode 100644 src/SharpCompress/IO/SharpCompressStream.Async.cs create mode 100644 src/SharpCompress/IO/SourceStream.Async.cs create mode 100644 src/SharpCompress/Readers/AbstractReader.Async.cs create mode 100644 src/SharpCompress/Readers/GZip/GZipReader.Async.cs create mode 100644 src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs create mode 100644 src/SharpCompress/Readers/Rar/RarReader.Async.cs create mode 100644 src/SharpCompress/Readers/Tar/TarReader.Async.cs create mode 100644 src/SharpCompress/Readers/Zip/ZipReader.Async.cs create mode 100644 src/SharpCompress/Writers/AbstractWriter.Async.cs create mode 100644 src/SharpCompress/Writers/Tar/TarWriter.Async.cs create mode 100644 src/SharpCompress/Writers/Zip/ZipWriter.Async.cs diff --git a/src/SharpCompress/Archives/AbstractArchive.Async.cs b/src/SharpCompress/Archives/AbstractArchive.Async.cs new file mode 100644 index 00000000..15691374 --- /dev/null +++ b/src/SharpCompress/Archives/AbstractArchive.Async.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace SharpCompress.Archives; + +public abstract partial class AbstractArchive + where TEntry : IArchiveEntry + where TVolume : IVolume +{ + #region Async Support + + // Async properties + public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync; + + public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync; + + public virtual async ValueTask DisposeAsync() + { + if (!_disposed) + { + await foreach (var v in _lazyVolumesAsync) + { + v.Dispose(); + } + foreach (var v in _lazyEntriesAsync.GetLoaded().Cast()) + { + v.Close(); + } + _sourceStream?.Dispose(); + + _disposed = true; + } + } + + private async ValueTask EnsureEntriesLoadedAsync() + { + await _lazyEntriesAsync.EnsureFullyLoaded(); + await _lazyVolumesAsync.EnsureFullyLoaded(); + } + + private async IAsyncEnumerable EntriesAsyncCast() + { + await foreach (var entry in EntriesAsync) + { + yield return entry; + } + } + + IAsyncEnumerable IAsyncArchive.EntriesAsync => EntriesAsyncCast(); + + IAsyncEnumerable IAsyncArchive.VolumesAsync => VolumesAsyncCast(); + + private async IAsyncEnumerable VolumesAsyncCast() + { + await foreach (var volume in _lazyVolumesAsync) + { + yield return volume; + } + } + + public async ValueTask ExtractAllEntriesAsync() + { + if (!await IsSolidAsync() && Type != ArchiveType.SevenZip) + { + throw new SharpCompressException( + "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." + ); + } + await EnsureEntriesLoadedAsync(); + return await CreateReaderForSolidExtractionAsync(); + } + + public virtual ValueTask IsSolidAsync() => new(false); + + public async ValueTask IsCompleteAsync() + { + await EnsureEntriesLoadedAsync(); + return await EntriesAsync.AllAsync(x => x.IsComplete); + } + + public async ValueTask TotalSizeAsync() => + await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.CompressedSize); + + public async ValueTask TotalUncompressedSizeAsync() => + await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size); + + public ValueTask IsEncryptedAsync() => new(IsEncrypted); + + #endregion +} diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 765c55d6..8d38527b 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -7,7 +7,7 @@ using SharpCompress.Readers; namespace SharpCompress.Archives; -public abstract class AbstractArchive : IArchive, IAsyncArchive +public abstract partial class AbstractArchive : IArchive, IAsyncArchive where TEntry : IArchiveEntry where TVolume : IVolume { @@ -16,6 +16,10 @@ public abstract class AbstractArchive : IArchive, IAsyncArchive private bool _disposed; private readonly SourceStream? _sourceStream; + // Async fields - kept in original file per refactoring rules + private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync; + private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync; + protected ReaderOptions ReaderOptions { get; } internal AbstractArchive(ArchiveType type, SourceStream sourceStream) @@ -156,87 +160,4 @@ public abstract class AbstractArchive : IArchive, IAsyncArchive return Entries.All(x => x.IsComplete); } } - - #region Async Support - - private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync; - private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync; - - public virtual async ValueTask DisposeAsync() - { - if (!_disposed) - { - await foreach (var v in _lazyVolumesAsync) - { - v.Dispose(); - } - foreach (var v in _lazyEntriesAsync.GetLoaded().Cast()) - { - v.Close(); - } - _sourceStream?.Dispose(); - - _disposed = true; - } - } - - private async ValueTask EnsureEntriesLoadedAsync() - { - await _lazyEntriesAsync.EnsureFullyLoaded(); - await _lazyVolumesAsync.EnsureFullyLoaded(); - } - - public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync; - - private async IAsyncEnumerable EntriesAsyncCast() - { - await foreach (var entry in EntriesAsync) - { - yield return entry; - } - } - - IAsyncEnumerable IAsyncArchive.EntriesAsync => EntriesAsyncCast(); - - IAsyncEnumerable IAsyncArchive.VolumesAsync => VolumesAsyncCast(); - - private async IAsyncEnumerable VolumesAsyncCast() - { - await foreach (var volume in _lazyVolumesAsync) - { - yield return volume; - } - } - - public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync; - - public async ValueTask ExtractAllEntriesAsync() - { - if (!await IsSolidAsync() && Type != ArchiveType.SevenZip) - { - throw new SharpCompressException( - "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." - ); - } - await EnsureEntriesLoadedAsync(); - return await CreateReaderForSolidExtractionAsync(); - } - - public virtual ValueTask IsSolidAsync() => new(false); - - public async ValueTask IsCompleteAsync() - { - await EnsureEntriesLoadedAsync(); - return await EntriesAsync.AllAsync(x => x.IsComplete); - } - - public async ValueTask TotalSizeAsync() => - await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.CompressedSize); - - public async ValueTask TotalUncompressedSizeAsync() => - await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size); - - public ValueTask IsEncryptedAsync() => new(IsEncrypted); - - #endregion } diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs new file mode 100644 index 00000000..80f3f209 --- /dev/null +++ b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Writers; + +namespace SharpCompress.Archives; + +public abstract partial class AbstractWritableArchive + where TEntry : IArchiveEntry + where TVolume : IVolume +{ + // Async property moved from main file + private IAsyncEnumerable OldEntriesAsync => + base.EntriesAsync.Where(x => !removedEntries.Contains(x)); + + private async ValueTask RebuildModifiedCollectionAsync() + { + if (pauseRebuilding) + { + return; + } + hasModifications = true; + newEntries.RemoveAll(v => removedEntries.Contains(v)); + modifiedEntries.Clear(); + await foreach (var entry in OldEntriesAsync) + { + modifiedEntries.Add(entry); + } + modifiedEntries.AddRange(newEntries); + } + + public async ValueTask RemoveEntryAsync(TEntry entry) + { + if (!removedEntries.Contains(entry)) + { + removedEntries.Add(entry); + await RebuildModifiedCollectionAsync(); + } + } + + private async ValueTask DoesKeyMatchExistingAsync( + string key, + CancellationToken cancellationToken + ) + { + await foreach ( + var entry in EntriesAsync.WithCancellation(cancellationToken).ConfigureAwait(false) + ) + { + var path = entry.Key; + if (path is null) + { + continue; + } + var p = path.Replace('/', '\\'); + if (p.Length > 0 && p[0] == '\\') + { + p = p.Substring(1); + } + return string.Equals(p, key, StringComparison.OrdinalIgnoreCase); + } + return false; + } + + public async ValueTask AddEntryAsync( + string key, + Stream source, + bool closeStream, + long size = 0, + DateTime? modified = null, + CancellationToken cancellationToken = default + ) + { + if (key.Length > 0 && key[0] is '/' or '\\') + { + key = key.Substring(1); + } + if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) + { + throw new ArchiveException("Cannot add entry with duplicate key: " + key); + } + var entry = CreateEntry(key, source, size, modified, closeStream); + newEntries.Add(entry); + await RebuildModifiedCollectionAsync(); + return entry; + } + + public async ValueTask AddDirectoryEntryAsync( + string key, + DateTime? modified = null, + CancellationToken cancellationToken = default + ) + { + if (key.Length > 0 && key[0] is '/' or '\\') + { + key = key.Substring(1); + } + if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) + { + throw new ArchiveException("Cannot add entry with duplicate key: " + key); + } + var entry = CreateDirectoryEntry(key, modified); + newEntries.Add(entry); + await RebuildModifiedCollectionAsync(); + return entry; + } + + public async ValueTask SaveToAsync( + Stream stream, + WriterOptions options, + CancellationToken cancellationToken = default + ) + { + //reset streams of new entries + newEntries.Cast().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin)); + await SaveToAsync(stream, options, OldEntriesAsync, newEntries, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index 231ef680..8a1090ee 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -10,7 +10,7 @@ using SharpCompress.Writers; namespace SharpCompress.Archives; -public abstract class AbstractWritableArchive +public abstract partial class AbstractWritableArchive : AbstractArchive, IWritableArchive, IWritableAsyncArchive @@ -73,25 +73,7 @@ public abstract class AbstractWritableArchive modifiedEntries.AddRange(OldEntries.Concat(newEntries)); } - private async ValueTask RebuildModifiedCollectionAsync() - { - if (pauseRebuilding) - { - return; - } - hasModifications = true; - newEntries.RemoveAll(v => removedEntries.Contains(v)); - modifiedEntries.Clear(); - await foreach (var entry in OldEntriesAsync) - { - modifiedEntries.Add(entry); - } - modifiedEntries.AddRange(newEntries); - } - private IEnumerable OldEntries => base.Entries.Where(x => !removedEntries.Contains(x)); - private IAsyncEnumerable OldEntriesAsync => - base.EntriesAsync.Where(x => !removedEntries.Contains(x)); public void RemoveEntry(TEntry entry) { @@ -102,20 +84,8 @@ public abstract class AbstractWritableArchive } } - public async ValueTask RemoveEntryAsync(TEntry entry) - { - if (!removedEntries.Contains(entry)) - { - removedEntries.Add(entry); - await RebuildModifiedCollectionAsync(); - } - } - void IWritableArchive.RemoveEntry(IArchiveEntry entry) => RemoveEntry((TEntry)entry); - ValueTask IWritableAsyncArchive.RemoveEntryAsync(IArchiveEntry entry) => - RemoveEntryAsync((TEntry)entry); - public TEntry AddEntry(string key, Stream source, long size = 0, DateTime? modified = null) => AddEntry(key, source, false, size, modified); @@ -170,29 +140,8 @@ public abstract class AbstractWritableArchive return false; } - private async ValueTask DoesKeyMatchExistingAsync( - string key, - CancellationToken cancellationToken - ) - { - await foreach ( - var entry in EntriesAsync.WithCancellation(cancellationToken).ConfigureAwait(false) - ) - { - var path = entry.Key; - if (path is null) - { - continue; - } - var p = path.Replace('/', '\\'); - if (p.Length > 0 && p[0] == '\\') - { - p = p.Substring(1); - } - return string.Equals(p, key, StringComparison.OrdinalIgnoreCase); - } - return false; - } + ValueTask IWritableAsyncArchive.RemoveEntryAsync(IArchiveEntry entry) => + RemoveEntryAsync((TEntry)entry); async ValueTask IWritableAsyncArchive.AddEntryAsync( string key, @@ -209,29 +158,6 @@ public abstract class AbstractWritableArchive CancellationToken cancellationToken ) => await AddDirectoryEntryAsync(key, modified, cancellationToken); - public async ValueTask AddEntryAsync( - string key, - Stream source, - bool closeStream, - long size = 0, - DateTime? modified = null, - CancellationToken cancellationToken = default - ) - { - if (key.Length > 0 && key[0] is '/' or '\\') - { - key = key.Substring(1); - } - if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) - { - throw new ArchiveException("Cannot add entry with duplicate key: " + key); - } - var entry = CreateEntry(key, source, size, modified, closeStream); - newEntries.Add(entry); - await RebuildModifiedCollectionAsync(); - return entry; - } - public TEntry AddDirectoryEntry(string key, DateTime? modified = null) { if (key.Length > 0 && key[0] is '/' or '\\') @@ -248,26 +174,6 @@ public abstract class AbstractWritableArchive return entry; } - public async ValueTask AddDirectoryEntryAsync( - string key, - DateTime? modified = null, - CancellationToken cancellationToken = default - ) - { - if (key.Length > 0 && key[0] is '/' or '\\') - { - key = key.Substring(1); - } - if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) - { - throw new ArchiveException("Cannot add entry with duplicate key: " + key); - } - var entry = CreateDirectoryEntry(key, modified); - newEntries.Add(entry); - await RebuildModifiedCollectionAsync(); - return entry; - } - public void SaveTo(Stream stream, WriterOptions options) { //reset streams of new entries @@ -275,18 +181,6 @@ public abstract class AbstractWritableArchive SaveTo(stream, options, OldEntries, newEntries); } - public async ValueTask SaveToAsync( - Stream stream, - WriterOptions options, - CancellationToken cancellationToken = default - ) - { - //reset streams of new entries - newEntries.Cast().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin)); - await SaveToAsync(stream, options, OldEntriesAsync, newEntries, cancellationToken) - .ConfigureAwait(false); - } - protected TEntry CreateEntry( string key, Stream source, diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs new file mode 100644 index 00000000..9a288207 --- /dev/null +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.GZip; +using SharpCompress.Writers; +using SharpCompress.Writers.GZip; + +namespace SharpCompress.Archives.GZip; + +public partial class GZipArchive +{ + public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) => + SaveToAsync(new FileInfo(filePath), cancellationToken); + + public async ValueTask SaveToAsync( + FileInfo fileInfo, + CancellationToken cancellationToken = default + ) + { + using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken) + .ConfigureAwait(false); + } + + protected override async ValueTask SaveToAsync( + Stream stream, + WriterOptions options, + IAsyncEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + if (Entries.Count > 1) + { + throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); + } + using var writer = new GZipWriter(stream, new GZipWriterOptions(options)); + await foreach ( + var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) + ) + { + if (!entry.IsDirectory) + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + cancellationToken + ) + .ConfigureAwait(false); + } + } + foreach (var entry in newEntries.Where(x => !x.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .ConfigureAwait(false); + } + } + + protected override ValueTask CreateReaderForSolidExtractionAsync() + { + var stream = Volumes.Single().Stream; + stream.Position = 0; + return new((IAsyncReader)GZipReader.OpenReader(stream)); + } +} diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index 7ff3c3c6..b8093e8d 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -36,19 +36,6 @@ public partial class GZipArchive : AbstractWritableArchive - SaveToAsync(new FileInfo(filePath), cancellationToken); - - public async ValueTask SaveToAsync( - FileInfo fileInfo, - CancellationToken cancellationToken = default - ) - { - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); - await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken) - .ConfigureAwait(false); - } - protected override GZipArchiveEntry CreateEntryInternal( string filePath, Stream source, @@ -92,44 +79,6 @@ public partial class GZipArchive : AbstractWritableArchive oldEntries, - IEnumerable newEntries, - CancellationToken cancellationToken = default - ) - { - if (Entries.Count > 1) - { - throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); - } - using var writer = new GZipWriter(stream, new GZipWriterOptions(options)); - await foreach ( - var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) - ) - { - if (!entry.IsDirectory) - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync( - entry.Key.NotNull("Entry Key is null"), - entryStream, - cancellationToken - ) - .ConfigureAwait(false); - } - } - foreach (var entry in newEntries.Where(x => !x.IsDirectory)) - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) - .ConfigureAwait(false); - } - } - protected override IEnumerable LoadEntries(IEnumerable volumes) { var stream = volumes.Single().Stream; @@ -139,6 +88,7 @@ public partial class GZipArchive : AbstractWritableArchive LoadEntriesAsync( IAsyncEnumerable volumes ) @@ -156,11 +106,4 @@ public partial class GZipArchive : AbstractWritableArchive CreateReaderForSolidExtractionAsync() - { - var stream = Volumes.Single().Stream; - stream.Position = 0; - return new((IAsyncReader)GZipReader.OpenReader(stream)); - } } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Async.cs b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs new file mode 100644 index 00000000..563d6bcb --- /dev/null +++ b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Rar; + +namespace SharpCompress.Archives.Rar; + +public partial class RarArchive +{ + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { + if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1) + { + unpackV1.Dispose(); + } + + _disposed = true; + await base.DisposeAsync(); + } + } + + protected override async ValueTask CreateReaderForSolidExtractionAsync() + { + if (await this.IsMultipartVolumeAsync()) + { + var streams = await VolumesAsync + .Select(volume => + { + volume.Stream.Position = 0; + return volume.Stream; + }) + .ToListAsync(); + return (RarReader)RarReader.OpenReader(streams, ReaderOptions); + } + + var stream = (await VolumesAsync.FirstAsync()).Stream; + stream.Position = 0; + return (RarReader)RarReader.OpenReader(stream, ReaderOptions); + } + + public override async ValueTask IsSolidAsync() => + await (await VolumesAsync.CastAsync().FirstAsync()).IsSolidArchiveAsync(); +} diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs index 8be82166..deb49aea 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.cs @@ -51,23 +51,10 @@ public partial class RarArchive } } - public override async ValueTask DisposeAsync() - { - if (!_disposed) - { - if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1) - { - unpackV1.Dispose(); - } - - _disposed = true; - await base.DisposeAsync(); - } - } - protected override IEnumerable LoadEntries(IEnumerable volumes) => RarArchiveEntryFactory.GetEntries(this, volumes, ReaderOptions); + // Simple async property - kept in original file protected override IAsyncEnumerable LoadEntriesAsync( IAsyncEnumerable volumes ) => RarArchiveEntryFactory.GetEntriesAsync(this, volumes, ReaderOptions); @@ -93,25 +80,6 @@ public partial class RarArchive return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable(); } - protected override async ValueTask CreateReaderForSolidExtractionAsync() - { - if (await this.IsMultipartVolumeAsync()) - { - var streams = await VolumesAsync - .Select(volume => - { - volume.Stream.Position = 0; - return volume.Stream; - }) - .ToListAsync(); - return (RarReader)RarReader.OpenReader(streams, ReaderOptions); - } - - var stream = (await VolumesAsync.FirstAsync()).Stream; - stream.Position = 0; - return (RarReader)RarReader.OpenReader(stream, ReaderOptions); - } - protected override IReader CreateReaderForSolidExtraction() { if (this.IsMultipartVolume()) @@ -131,9 +99,6 @@ public partial class RarArchive public override bool IsSolid => Volumes.First().IsSolidArchive; - public override async ValueTask IsSolidAsync() => - await (await VolumesAsync.CastAsync().FirstAsync()).IsSolidArchiveAsync(); - public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted; public virtual int MinVersion => Volumes.First().MinVersion; diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs new file mode 100644 index 00000000..dbc2cac0 --- /dev/null +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.Compressors.Rar; +using SharpCompress.Readers; + +namespace SharpCompress.Archives.Rar; + +public partial class RarArchiveEntry +{ + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + RarStream stream; + if (IsRarV3) + { + stream = new RarStream( + archive.UnpackV1.Value, + FileHeader, + await MultiVolumeReadOnlyAsyncStream.Create( + Parts.ToAsyncEnumerable().CastAsync() + ) + ); + } + else + { + stream = new RarStream( + archive.UnpackV2017.Value, + FileHeader, + await MultiVolumeReadOnlyAsyncStream.Create( + Parts.ToAsyncEnumerable().CastAsync() + ) + ); + } + + await stream.InitializeAsync(cancellationToken); + return stream; + } +} diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 65033298..3d1a74d2 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -12,7 +12,7 @@ using SharpCompress.Readers; namespace SharpCompress.Archives.Rar; -public class RarArchiveEntry : RarEntry, IArchiveEntry +public partial class RarArchiveEntry : RarEntry, IArchiveEntry { private readonly ICollection parts; private readonly RarArchive archive; @@ -92,36 +92,6 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry return stream; } - public async ValueTask OpenEntryStreamAsync( - CancellationToken cancellationToken = default - ) - { - RarStream stream; - if (IsRarV3) - { - stream = new RarStream( - archive.UnpackV1.Value, - FileHeader, - await MultiVolumeReadOnlyAsyncStream.Create( - Parts.ToAsyncEnumerable().CastAsync() - ) - ); - } - else - { - stream = new RarStream( - archive.UnpackV2017.Value, - FileHeader, - await MultiVolumeReadOnlyAsyncStream.Create( - Parts.ToAsyncEnumerable().CastAsync() - ) - ); - } - - await stream.InitializeAsync(cancellationToken); - return stream; - } - public bool IsComplete { get diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs new file mode 100644 index 00000000..d336cee9 --- /dev/null +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.SevenZip; +using SharpCompress.IO; +using SharpCompress.Readers; + +namespace SharpCompress.Archives.SevenZip; + +public partial class SevenZipArchive +{ + private async ValueTask LoadFactoryAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + if (_database is null) + { + stream.Position = 0; + var reader = new ArchiveReader(); + await reader.OpenAsync( + stream, + lookForHeader: ReaderOptions.LookForHeader, + cancellationToken + ); + _database = await reader.ReadDatabaseAsync( + new PasswordProvider(ReaderOptions.Password), + cancellationToken + ); + } + } + + protected override ValueTask CreateReaderForSolidExtractionAsync() => + new(new SevenZipReader(ReaderOptions, this)); +} diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index ad92f806..72cbbdb8 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -73,6 +73,7 @@ public partial class SevenZipArchive : AbstractArchive LoadEntriesAsync( IAsyncEnumerable volumes ) @@ -119,33 +120,9 @@ public partial class SevenZipArchive : AbstractArchive new SevenZipReader(ReaderOptions, this); - protected override ValueTask CreateReaderForSolidExtractionAsync() => - new(new SevenZipReader(ReaderOptions, this)); - public override bool IsSolid => Entries .Where(x => !x.IsDirectory) diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs new file mode 100644 index 00000000..6c2dcb9d --- /dev/null +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Tar; +using SharpCompress.Writers; +using SharpCompress.Writers.Tar; + +namespace SharpCompress.Archives.Tar; + +public partial class TarArchive +{ + protected override async ValueTask SaveToAsync( + Stream stream, + WriterOptions options, + IAsyncEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + using var writer = new TarWriter(stream, new TarWriterOptions(options)); + await foreach ( + var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) + ) + { + if (entry.IsDirectory) + { + await writer + .WriteDirectoryAsync( + entry.Key.NotNull("Entry Key is null"), + entry.LastModifiedTime, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + entry.LastModifiedTime, + entry.Size, + cancellationToken + ) + .ConfigureAwait(false); + } + } + foreach (var entry in newEntries) + { + if (entry.IsDirectory) + { + await writer + .WriteDirectoryAsync( + entry.Key.NotNull("Entry Key is null"), + entry.LastModifiedTime, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + entry.LastModifiedTime, + entry.Size, + cancellationToken + ) + .ConfigureAwait(false); + } + } + } + + protected override ValueTask CreateReaderForSolidExtractionAsync() + { + var stream = Volumes.Single().Stream; + stream.Position = 0; + return new((IAsyncReader)TarReader.OpenReader(stream)); + } +} diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 48e98dbe..e64d0a01 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -91,6 +91,7 @@ public partial class TarArchive : AbstractWritableArchive LoadEntriesAsync( IAsyncEnumerable volumes ) @@ -214,82 +215,10 @@ public partial class TarArchive : AbstractWritableArchive oldEntries, - IEnumerable newEntries, - CancellationToken cancellationToken = default - ) - { - using var writer = new TarWriter(stream, new TarWriterOptions(options)); - await foreach ( - var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) - ) - { - if (entry.IsDirectory) - { - await writer - .WriteDirectoryAsync( - entry.Key.NotNull("Entry Key is null"), - entry.LastModifiedTime, - cancellationToken - ) - .ConfigureAwait(false); - } - else - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync( - entry.Key.NotNull("Entry Key is null"), - entryStream, - entry.LastModifiedTime, - entry.Size, - cancellationToken - ) - .ConfigureAwait(false); - } - } - foreach (var entry in newEntries) - { - if (entry.IsDirectory) - { - await writer - .WriteDirectoryAsync( - entry.Key.NotNull("Entry Key is null"), - entry.LastModifiedTime, - cancellationToken - ) - .ConfigureAwait(false); - } - else - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync( - entry.Key.NotNull("Entry Key is null"), - entryStream, - entry.LastModifiedTime, - entry.Size, - cancellationToken - ) - .ConfigureAwait(false); - } - } - } - protected override IReader CreateReaderForSolidExtraction() { var stream = Volumes.Single().Stream; stream.Position = 0; return TarReader.OpenReader(stream); } - - protected override ValueTask CreateReaderForSolidExtractionAsync() - { - var stream = Volumes.Single().Stream; - stream.Position = 0; - return new((IAsyncReader)TarReader.OpenReader(stream)); - } } diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs new file mode 100644 index 00000000..0afaae03 --- /dev/null +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; + +namespace SharpCompress.Archives.Zip; + +public partial class ZipArchive +{ + protected override async ValueTask SaveToAsync( + Stream stream, + WriterOptions options, + IAsyncEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + using var writer = new ZipWriter(stream, new ZipWriterOptions(options)); + await foreach ( + var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) + ) + { + if (entry.IsDirectory) + { + await writer + .WriteDirectoryAsync( + entry.Key.NotNull("Entry Key is null"), + entry.LastModifiedTime, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + cancellationToken + ) + .ConfigureAwait(false); + } + } + foreach (var entry in newEntries) + { + if (entry.IsDirectory) + { + await writer + .WriteDirectoryAsync( + entry.Key.NotNull("Entry Key is null"), + entry.LastModifiedTime, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync( + entry.Key.NotNull("Entry Key is null"), + entryStream, + cancellationToken + ) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 4da9e6a1..76646bb1 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -108,6 +108,7 @@ public partial class ZipArchive : AbstractWritableArchive LoadEntriesAsync( IAsyncEnumerable volumes ) @@ -192,67 +193,6 @@ public partial class ZipArchive : AbstractWritableArchive oldEntries, - IEnumerable newEntries, - CancellationToken cancellationToken = default - ) - { - using var writer = new ZipWriter(stream, new ZipWriterOptions(options)); - await foreach ( - var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) - ) - { - if (entry.IsDirectory) - { - await writer - .WriteDirectoryAsync( - entry.Key.NotNull("Entry Key is null"), - entry.LastModifiedTime, - cancellationToken - ) - .ConfigureAwait(false); - } - else - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync( - entry.Key.NotNull("Entry Key is null"), - entryStream, - cancellationToken - ) - .ConfigureAwait(false); - } - } - foreach (var entry in newEntries) - { - if (entry.IsDirectory) - { - await writer - .WriteDirectoryAsync( - entry.Key.NotNull("Entry Key is null"), - entry.LastModifiedTime, - cancellationToken - ) - .ConfigureAwait(false); - } - else - { - using var entryStream = entry.OpenEntryStream(); - await writer - .WriteAsync( - entry.Key.NotNull("Entry Key is null"), - entryStream, - cancellationToken - ) - .ConfigureAwait(false); - } - } - } - protected override ZipArchiveEntry CreateEntryInternal( string filePath, Stream source, diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs new file mode 100644 index 00000000..2d76dcd7 --- /dev/null +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Zip; + +namespace SharpCompress.Archives.Zip; + +public partial class ZipArchiveEntry +{ + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + var part = Parts.Single(); + if (part is SeekableZipFilePart seekablePart) + { + return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull(); + } + return OpenEntryStream(); + } +} diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs index f59da4f6..bf3e67ac 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs @@ -6,25 +6,13 @@ using SharpCompress.Common.Zip; namespace SharpCompress.Archives.Zip; -public class ZipArchiveEntry : ZipEntry, IArchiveEntry +public partial class ZipArchiveEntry : ZipEntry, IArchiveEntry { internal ZipArchiveEntry(ZipArchive archive, SeekableZipFilePart? part) : base(part) => Archive = archive; public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public async ValueTask OpenEntryStreamAsync( - CancellationToken cancellationToken = default - ) - { - var part = Parts.Single(); - if (part is SeekableZipFilePart seekablePart) - { - return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull(); - } - return OpenEntryStream(); - } - #region IArchiveEntry Members public IArchive Archive { get; } diff --git a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs new file mode 100644 index 00000000..07601fdb --- /dev/null +++ b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Arc; + +namespace SharpCompress.Common.Ace.Headers; + +public sealed partial class AceFileHeader +{ + /// + /// Asynchronously reads the next file entry header from the stream. + /// Returns null if no more entries or end of archive. + /// Supports both ACE 1.0 and ACE 2.0 formats. + /// + public override async ValueTask ReadAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var headerData = await ReadHeaderAsync(stream, cancellationToken); + if (headerData.Length == 0) + { + return null; + } + int offset = 0; + + // Header type (1 byte) + HeaderType = headerData[offset++]; + + // Skip recovery record headers (ACE 2.0 feature) + if (HeaderType == (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.RECOVERY32) + { + // Skip to next header + return null; + } + + if (HeaderType != (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.FILE) + { + // Unknown header type - skip + return null; + } + + // Header flags (2 bytes) + HeaderFlags = BitConverter.ToUInt16(headerData, offset); + offset += 2; + + // Packed size (4 bytes) + PackedSize = BitConverter.ToUInt32(headerData, offset); + offset += 4; + + // Original size (4 bytes) + OriginalSize = BitConverter.ToUInt32(headerData, offset); + offset += 4; + + // File date/time in DOS format (4 bytes) + var dosDateTime = BitConverter.ToUInt32(headerData, offset); + DateTime = ConvertDosDateTime(dosDateTime); + offset += 4; + + // File attributes (4 bytes) + Attributes = (int)BitConverter.ToUInt32(headerData, offset); + offset += 4; + + // CRC32 (4 bytes) + Crc32 = BitConverter.ToUInt32(headerData, offset); + offset += 4; + + // Compression type (1 byte) + byte compressionType = headerData[offset++]; + CompressionType = GetCompressionType(compressionType); + + // Compression quality/parameter (1 byte) + byte compressionQuality = headerData[offset++]; + CompressionQuality = GetCompressionQuality(compressionQuality); + + // Parameters (2 bytes) + Parameters = BitConverter.ToUInt16(headerData, offset); + offset += 2; + + // Reserved (2 bytes) - skip + offset += 2; + + // Filename length (2 bytes) + var filenameLength = BitConverter.ToUInt16(headerData, offset); + offset += 2; + + // Filename + if (offset + filenameLength <= headerData.Length) + { + Filename = ArchiveEncoding.Decode(headerData, offset, filenameLength); + offset += filenameLength; + } + + // Handle comment if present + if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0) + { + // Comment length (2 bytes) + if (offset + 2 <= headerData.Length) + { + ushort commentLength = BitConverter.ToUInt16(headerData, offset); + offset += 2 + commentLength; // Skip comment + } + } + + // Store the data start position + DataStartPosition = stream.Position; + + return this; + } +} diff --git a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs index 0f067572..16aa1e3b 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs @@ -12,7 +12,7 @@ namespace SharpCompress.Common.Ace.Headers /// /// ACE file entry header /// - public sealed class AceFileHeader : AceHeader + public sealed partial class AceFileHeader : AceHeader { public long DataStartPosition { get; private set; } public long PackedSize { get; set; } @@ -149,106 +149,7 @@ namespace SharpCompress.Common.Ace.Headers return this; } - /// - /// Asynchronously reads the next file entry header from the stream. - /// Returns null if no more entries or end of archive. - /// Supports both ACE 1.0 and ACE 2.0 formats. - /// - public override async ValueTask ReadAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var headerData = await ReadHeaderAsync(stream, cancellationToken); - if (headerData.Length == 0) - { - return null; - } - int offset = 0; - - // Header type (1 byte) - HeaderType = headerData[offset++]; - - // Skip recovery record headers (ACE 2.0 feature) - if (HeaderType == (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.RECOVERY32) - { - // Skip to next header - return null; - } - - if (HeaderType != (byte)SharpCompress.Common.Ace.Headers.AceHeaderType.FILE) - { - // Unknown header type - skip - return null; - } - - // Header flags (2 bytes) - HeaderFlags = BitConverter.ToUInt16(headerData, offset); - offset += 2; - - // Packed size (4 bytes) - PackedSize = BitConverter.ToUInt32(headerData, offset); - offset += 4; - - // Original size (4 bytes) - OriginalSize = BitConverter.ToUInt32(headerData, offset); - offset += 4; - - // File date/time in DOS format (4 bytes) - var dosDateTime = BitConverter.ToUInt32(headerData, offset); - DateTime = ConvertDosDateTime(dosDateTime); - offset += 4; - - // File attributes (4 bytes) - Attributes = (int)BitConverter.ToUInt32(headerData, offset); - offset += 4; - - // CRC32 (4 bytes) - Crc32 = BitConverter.ToUInt32(headerData, offset); - offset += 4; - - // Compression type (1 byte) - byte compressionType = headerData[offset++]; - CompressionType = GetCompressionType(compressionType); - - // Compression quality/parameter (1 byte) - byte compressionQuality = headerData[offset++]; - CompressionQuality = GetCompressionQuality(compressionQuality); - - // Parameters (2 bytes) - Parameters = BitConverter.ToUInt16(headerData, offset); - offset += 2; - - // Reserved (2 bytes) - skip - offset += 2; - - // Filename length (2 bytes) - var filenameLength = BitConverter.ToUInt16(headerData, offset); - offset += 2; - - // Filename - if (offset + filenameLength <= headerData.Length) - { - Filename = ArchiveEncoding.Decode(headerData, offset, filenameLength); - offset += filenameLength; - } - - // Handle comment if present - if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0) - { - // Comment length (2 bytes) - if (offset + 2 <= headerData.Length) - { - ushort commentLength = BitConverter.ToUInt16(headerData, offset); - offset += 2 + commentLength; // Skip comment - } - } - - // Store the data start position - DataStartPosition = stream.Position; - - return this; - } + // ReadAsync moved to AceFileHeader.Async.cs public CompressionType GetCompressionType(byte value) => value switch diff --git a/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs new file mode 100644 index 00000000..957eb663 --- /dev/null +++ b/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Ace.Headers; + +public abstract partial class AceHeader +{ + public abstract ValueTask ReadAsync( + Stream reader, + CancellationToken cancellationToken = default + ); + + public async ValueTask ReadHeaderAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + // Read header CRC (2 bytes) and header size (2 bytes) + var headerBytes = new byte[4]; + if (await stream.ReadAsync(headerBytes, 0, 4, cancellationToken) != 4) + { + return Array.Empty(); + } + + HeaderCrc = BitConverter.ToUInt16(headerBytes, 0); // CRC for validation + HeaderSize = BitConverter.ToUInt16(headerBytes, 2); + if (HeaderSize == 0) + { + return Array.Empty(); + } + + // Read the header data + var body = new byte[HeaderSize]; + if (await stream.ReadAsync(body, 0, HeaderSize, cancellationToken) != HeaderSize) + { + return Array.Empty(); + } + + // Verify crc + var checksum = AceCrc.AceCrc16(body); + if (checksum != HeaderCrc) + { + throw new InvalidDataException("Header checksum is invalid"); + } + return body; + } + + /// + /// Asynchronously checks if the stream is an ACE archive + /// + /// The stream to read from + /// Cancellation token + /// True if the stream is an ACE archive, false otherwise + public static async ValueTask IsArchiveAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var bytes = new byte[14]; + if (await stream.ReadAsync(bytes, 0, 14, cancellationToken) != 14) + { + return false; + } + + return CheckMagicBytes(bytes, 7); + } +} diff --git a/src/SharpCompress/Common/Ace/Headers/AceHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceHeader.cs index e6e59634..b5f1f18c 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceHeader.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceHeader.cs @@ -19,7 +19,7 @@ namespace SharpCompress.Common.Ace.Headers RECOVERY64B = 4, } - public abstract class AceHeader + public abstract partial class AceHeader { // ACE signature: bytes at offset 7 should be "**ACE**" private static readonly byte[] AceSignature = @@ -60,10 +60,7 @@ namespace SharpCompress.Common.Ace.Headers public abstract AceHeader? Read(Stream reader); - public abstract ValueTask ReadAsync( - Stream reader, - CancellationToken cancellationToken = default - ); + // Async methods moved to AceHeader.Async.cs public byte[] ReadHeader(Stream stream) { @@ -97,41 +94,6 @@ namespace SharpCompress.Common.Ace.Headers return body; } - public async ValueTask ReadHeaderAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - // Read header CRC (2 bytes) and header size (2 bytes) - var headerBytes = new byte[4]; - if (await stream.ReadAsync(headerBytes, 0, 4, cancellationToken) != 4) - { - return Array.Empty(); - } - - HeaderCrc = BitConverter.ToUInt16(headerBytes, 0); // CRC for validation - HeaderSize = BitConverter.ToUInt16(headerBytes, 2); - if (HeaderSize == 0) - { - return Array.Empty(); - } - - // Read the header data - var body = new byte[HeaderSize]; - if (await stream.ReadAsync(body, 0, HeaderSize, cancellationToken) != HeaderSize) - { - return Array.Empty(); - } - - // Verify crc - var checksum = AceCrc.AceCrc16(body); - if (checksum != HeaderCrc) - { - throw new InvalidDataException("Header checksum is invalid"); - } - return body; - } - public static bool IsArchive(Stream stream) { // ACE files have a specific signature @@ -147,26 +109,6 @@ namespace SharpCompress.Common.Ace.Headers return CheckMagicBytes(bytes, 7); } - /// - /// Asynchronously checks if the stream is an ACE archive - /// - /// The stream to read from - /// Cancellation token - /// True if the stream is an ACE archive, false otherwise - public static async ValueTask IsArchiveAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var bytes = new byte[14]; - if (await stream.ReadAsync(bytes, 0, 14, cancellationToken) != 14) - { - return false; - } - - return CheckMagicBytes(bytes, 7); - } - protected static bool CheckMagicBytes(byte[] headerBytes, int offset) { // Check for "**ACE**" at specified offset diff --git a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs new file mode 100644 index 00000000..10b3f022 --- /dev/null +++ b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs @@ -0,0 +1,83 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Ace.Headers; + +public sealed partial class AceMainHeader +{ + /// + /// Asynchronously reads the main archive header from the stream. + /// Returns header if this is a valid ACE archive. + /// Supports both ACE 1.0 and ACE 2.0 formats. + /// + public override async ValueTask ReadAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var headerData = await ReadHeaderAsync(stream, cancellationToken); + if (headerData.Length == 0) + { + return null; + } + int offset = 0; + + // Header type should be 0 for main header + if (headerData[offset++] != HeaderType) + { + return null; + } + + // Header flags (2 bytes) + HeaderFlags = BitConverter.ToUInt16(headerData, offset); + offset += 2; + + // Skip signature "**ACE**" (7 bytes) + if (!CheckMagicBytes(headerData, offset)) + { + throw new InvalidDataException("Invalid ACE archive signature."); + } + offset += 7; + + // ACE version (1 byte) - 10 for ACE 1.0, 20 for ACE 2.0 + AceVersion = headerData[offset++]; + ExtractVersion = headerData[offset++]; + + // Host OS (1 byte) + if (offset < headerData.Length) + { + var hostOsByte = headerData[offset++]; + HostOS = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown; + } + // Volume number (1 byte) + VolumeNumber = headerData[offset++]; + + // Creation date/time (4 bytes) + var dosDateTime = BitConverter.ToUInt32(headerData, offset); + DateTime = ConvertDosDateTime(dosDateTime); + offset += 4; + + // Reserved fields (8 bytes) + if (offset + 8 <= headerData.Length) + { + offset += 8; + } + + // Skip additional fields based on flags + // Handle comment if present + if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0) + { + if (offset + 2 <= headerData.Length) + { + ushort commentLength = BitConverter.ToUInt16(headerData, offset); + offset += 2 + commentLength; + } + } + + return this; + } +} diff --git a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs index 61a06ab0..7b189cd6 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs @@ -13,7 +13,7 @@ namespace SharpCompress.Common.Ace.Headers /// /// ACE main archive header /// - public sealed class AceMainHeader : AceHeader + public sealed partial class AceMainHeader : AceHeader { public byte ExtractVersion { get; set; } public byte CreatorVersion { get; set; } @@ -96,76 +96,6 @@ namespace SharpCompress.Common.Ace.Headers return this; } - /// - /// Asynchronously reads the main archive header from the stream. - /// Returns header if this is a valid ACE archive. - /// Supports both ACE 1.0 and ACE 2.0 formats. - /// - public override async ValueTask ReadAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var headerData = await ReadHeaderAsync(stream, cancellationToken); - if (headerData.Length == 0) - { - return null; - } - int offset = 0; - - // Header type should be 0 for main header - if (headerData[offset++] != HeaderType) - { - return null; - } - - // Header flags (2 bytes) - HeaderFlags = BitConverter.ToUInt16(headerData, offset); - offset += 2; - - // Skip signature "**ACE**" (7 bytes) - if (!CheckMagicBytes(headerData, offset)) - { - throw new InvalidDataException("Invalid ACE archive signature."); - } - offset += 7; - - // ACE version (1 byte) - 10 for ACE 1.0, 20 for ACE 2.0 - AceVersion = headerData[offset++]; - ExtractVersion = headerData[offset++]; - - // Host OS (1 byte) - if (offset < headerData.Length) - { - var hostOsByte = headerData[offset++]; - HostOS = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown; - } - // Volume number (1 byte) - VolumeNumber = headerData[offset++]; - - // Creation date/time (4 bytes) - var dosDateTime = BitConverter.ToUInt32(headerData, offset); - DateTime = ConvertDosDateTime(dosDateTime); - offset += 4; - - // Reserved fields (8 bytes) - if (offset + 8 <= headerData.Length) - { - offset += 8; - } - - // Skip additional fields based on flags - // Handle comment if present - if ((HeaderFlags & SharpCompress.Common.Ace.Headers.HeaderFlags.COMMENT) != 0) - { - if (offset + 2 <= headerData.Length) - { - ushort commentLength = BitConverter.ToUInt16(headerData, offset); - offset += 2 + commentLength; - } - } - - return this; - } + // ReadAsync moved to AceMainHeader.Async.cs } } diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs new file mode 100644 index 00000000..8f455444 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Arj.Headers; + +public abstract partial class ArjHeader +{ + public abstract ValueTask ReadAsync( + Stream reader, + CancellationToken cancellationToken = default + ); + + public async ValueTask ReadHeaderAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + // check for magic bytes + var magic = new byte[2]; + if (await stream.ReadAsync(magic, 0, 2, cancellationToken) != 2) + { + return Array.Empty(); + } + + if (!CheckMagicBytes(magic)) + { + throw new InvalidDataException("Not an ARJ file (wrong magic bytes)"); + } + + // read header_size + byte[] headerBytes = new byte[2]; + await stream.ReadAsync(headerBytes, 0, 2, cancellationToken); + var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8); + if (headerSize < 1) + { + return Array.Empty(); + } + + var body = new byte[headerSize]; + var read = await stream.ReadAsync(body, 0, headerSize, cancellationToken); + if (read < headerSize) + { + return Array.Empty(); + } + + byte[] crc = new byte[4]; + read = await stream.ReadAsync(crc, 0, 4, cancellationToken); + var checksum = Crc32Stream.Compute(body); + // Compute the hash value + if (checksum != BitConverter.ToUInt32(crc, 0)) + { + throw new InvalidDataException("Header checksum is invalid"); + } + return body; + } + + protected async ValueTask> ReadExtendedHeadersAsync( + Stream reader, + CancellationToken cancellationToken = default + ) + { + List extendedHeader = new List(); + byte[] buffer = new byte[2]; + + while (true) + { + int bytesRead = await reader.ReadAsync(buffer, 0, 2, cancellationToken); + if (bytesRead < 2) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header size." + ); + } + + var extHeaderSize = (ushort)(buffer[0] | (buffer[1] << 8)); + if (extHeaderSize == 0) + { + return extendedHeader; + } + + byte[] header = new byte[extHeaderSize]; + bytesRead = await reader.ReadAsync(header, 0, extHeaderSize, cancellationToken); + if (bytesRead < extHeaderSize) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header data." + ); + } + + byte[] crcextended = new byte[4]; + bytesRead = await reader.ReadAsync(crcextended, 0, 4, cancellationToken); + if (bytesRead < 4) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header CRC." + ); + } + + var checksum = Crc32Stream.Compute(header); + if (checksum != BitConverter.ToUInt32(crcextended, 0)) + { + throw new InvalidDataException("Extended header checksum is invalid"); + } + + extendedHeader.Add(header); + } + } + + /// + /// Asynchronously checks if the stream is an ARJ archive + /// + /// The stream to read from + /// Cancellation token + /// True if the stream is an ARJ archive, false otherwise + public static async ValueTask IsArchiveAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var bytes = new byte[2]; + if (await stream.ReadAsync(bytes, 0, 2, cancellationToken) != 2) + { + return false; + } + + return CheckMagicBytes(bytes); + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs index ffb020dd..dd7ca402 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs @@ -16,7 +16,7 @@ namespace SharpCompress.Common.Arj.Headers LocalHeader, } - public abstract class ArjHeader + public abstract partial class ArjHeader { private const int FIRST_HDR_SIZE = 34; private const ushort ARJ_MAGIC = 0xEA60; @@ -32,10 +32,7 @@ namespace SharpCompress.Common.Arj.Headers public abstract ArjHeader? Read(Stream reader); - public abstract ValueTask ReadAsync( - Stream reader, - CancellationToken cancellationToken = default - ); + // Async methods moved to ArjHeader.Async.cs public byte[] ReadHeader(Stream stream) { @@ -78,101 +75,7 @@ namespace SharpCompress.Common.Arj.Headers return body; } - public async ValueTask ReadHeaderAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - // check for magic bytes - var magic = new byte[2]; - if (await stream.ReadAsync(magic, 0, 2, cancellationToken) != 2) - { - return Array.Empty(); - } - - if (!CheckMagicBytes(magic)) - { - throw new InvalidDataException("Not an ARJ file (wrong magic bytes)"); - } - - // read header_size - byte[] headerBytes = new byte[2]; - await stream.ReadAsync(headerBytes, 0, 2, cancellationToken); - var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8); - if (headerSize < 1) - { - return Array.Empty(); - } - - var body = new byte[headerSize]; - var read = await stream.ReadAsync(body, 0, headerSize, cancellationToken); - if (read < headerSize) - { - return Array.Empty(); - } - - byte[] crc = new byte[4]; - read = await stream.ReadAsync(crc, 0, 4, cancellationToken); - var checksum = Crc32Stream.Compute(body); - // Compute the hash value - if (checksum != BitConverter.ToUInt32(crc, 0)) - { - throw new InvalidDataException("Header checksum is invalid"); - } - return body; - } - - protected async ValueTask> ReadExtendedHeadersAsync( - Stream reader, - CancellationToken cancellationToken = default - ) - { - List extendedHeader = new List(); - byte[] buffer = new byte[2]; - - while (true) - { - int bytesRead = await reader.ReadAsync(buffer, 0, 2, cancellationToken); - if (bytesRead < 2) - { - throw new EndOfStreamException( - "Unexpected end of stream while reading extended header size." - ); - } - - var extHeaderSize = (ushort)(buffer[0] | (buffer[1] << 8)); - if (extHeaderSize == 0) - { - return extendedHeader; - } - - byte[] header = new byte[extHeaderSize]; - bytesRead = await reader.ReadAsync(header, 0, extHeaderSize, cancellationToken); - if (bytesRead < extHeaderSize) - { - throw new EndOfStreamException( - "Unexpected end of stream while reading extended header data." - ); - } - - byte[] crc = new byte[4]; - bytesRead = await reader.ReadAsync(crc, 0, 4, cancellationToken); - if (bytesRead < 4) - { - throw new EndOfStreamException( - "Unexpected end of stream while reading extended header CRC." - ); - } - - var checksum = Crc32Stream.Compute(header); - if (checksum != BitConverter.ToUInt32(crc, 0)) - { - throw new InvalidDataException("Extended header checksum is invalid"); - } - - extendedHeader.Add(header); - } - } + // ReadHeaderAsync moved to ArjHeader.Async.cs protected List ReadExtendedHeaders(Stream reader) { @@ -251,26 +154,6 @@ namespace SharpCompress.Common.Arj.Headers return CheckMagicBytes(bytes); } - /// - /// Asynchronously checks if the stream is an ARJ archive - /// - /// The stream to read from - /// Cancellation token - /// True if the stream is an ARJ archive, false otherwise - public static async ValueTask IsArchiveAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var bytes = new byte[2]; - if (await stream.ReadAsync(bytes, 0, 2, cancellationToken) != 2) - { - return false; - } - - return CheckMagicBytes(bytes); - } - protected static bool CheckMagicBytes(byte[] headerBytes) { var magicValue = (ushort)(headerBytes[0] | headerBytes[1] << 8); diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs new file mode 100644 index 00000000..eda70ae1 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers; + +public partial class ArjLocalHeader +{ + public override async ValueTask ReadAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var body = await ReadHeaderAsync(stream, cancellationToken); + if (body.Length > 0) + { + await ReadExtendedHeadersAsync(stream, cancellationToken); + var header = LoadFrom(body); + header.DataStartPosition = stream.Position; + return header; + } + return null; + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs index fa23653b..a16596df 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Arj.Headers { - public class ArjLocalHeader : ArjHeader + public partial class ArjLocalHeader : ArjHeader { public ArchiveEncoding ArchiveEncoding { get; } public long DataStartPosition { get; protected set; } @@ -56,21 +56,7 @@ namespace SharpCompress.Common.Arj.Headers return null; } - public override async ValueTask ReadAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var body = await ReadHeaderAsync(stream, cancellationToken); - if (body.Length > 0) - { - await ReadExtendedHeadersAsync(stream, cancellationToken); - var header = LoadFrom(body); - header.DataStartPosition = stream.Position; - return header; - } - return null; - } + // ReadAsync moved to ArjLocalHeader.Async.cs public ArjLocalHeader LoadFrom(byte[] headerBytes) { diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs new file mode 100644 index 00000000..cc0592f8 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers; + +public partial class ArjMainHeader +{ + public override async ValueTask ReadAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var body = await ReadHeaderAsync(stream, cancellationToken); + await ReadExtendedHeadersAsync(stream, cancellationToken); + return LoadFrom(body); + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs index eb2302b7..6fb13896 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs @@ -8,7 +8,7 @@ using SharpCompress.Crypto; namespace SharpCompress.Common.Arj.Headers { - public class ArjMainHeader : ArjHeader + public partial class ArjMainHeader : ArjHeader { private const int FIRST_HDR_SIZE = 34; private const ushort ARJ_MAGIC = 0xEA60; @@ -47,15 +47,7 @@ namespace SharpCompress.Common.Arj.Headers return LoadFrom(body); } - public override async ValueTask ReadAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var body = await ReadHeaderAsync(stream, cancellationToken); - await ReadExtendedHeadersAsync(stream, cancellationToken); - return LoadFrom(body); - } + // ReadAsync moved to ArjMainHeader.Async.cs public ArjMainHeader LoadFrom(byte[] headerBytes) { diff --git a/src/SharpCompress/Common/EntryStream.Async.cs b/src/SharpCompress/Common/EntryStream.Async.cs new file mode 100644 index 00000000..21708a89 --- /dev/null +++ b/src/SharpCompress/Common/EntryStream.Async.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Common; + +public partial class EntryStream +{ + /// + /// Asynchronously skip the rest of the entry stream. + /// + public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default) + { + await this.SkipAsync(cancellationToken).ConfigureAwait(false); + _completed = true; + } + +#if !LEGACY_DOTNET + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + _isDisposed = true; + if (!(_completed || _reader.Cancelled)) + { + await SkipEntryAsync().ConfigureAwait(false); + } + + //Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised + if (_stream is IStreamStack ss) + { + if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream) + { + await deflateStream.FlushAsync().ConfigureAwait(false); + } + else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream) + { + await lzmaStream.FlushAsync().ConfigureAwait(false); + } + } +#if DEBUG_STREAMS + this.DebugDispose(typeof(EntryStream)); +#endif + await base.DisposeAsync().ConfigureAwait(false); + await _stream.DisposeAsync().ConfigureAwait(false); + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var read = await _stream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (read <= 0) + { + _completed = true; + } + return read; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (read <= 0) + { + _completed = true; + } + return read; + } +#endif +} diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs index e4de4ca9..1398d08b 100644 --- a/src/SharpCompress/Common/EntryStream.cs +++ b/src/SharpCompress/Common/EntryStream.cs @@ -8,7 +8,7 @@ using SharpCompress.Readers; namespace SharpCompress.Common; -public class EntryStream : Stream, IStreamStack +public partial class EntryStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -53,15 +53,6 @@ public class EntryStream : Stream, IStreamStack _completed = true; } - /// - /// Asynchronously skip the rest of the entry stream. - /// - public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default) - { - await this.SkipAsync(cancellationToken).ConfigureAwait(false); - _completed = true; - } - protected override void Dispose(bool disposing) { if (_isDisposed) @@ -93,39 +84,6 @@ public class EntryStream : Stream, IStreamStack _stream.Dispose(); } -#if !LEGACY_DOTNET - public override async ValueTask DisposeAsync() - { - if (_isDisposed) - { - return; - } - _isDisposed = true; - if (!(_completed || _reader.Cancelled)) - { - await SkipEntryAsync().ConfigureAwait(false); - } - - //Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised - if (_stream is IStreamStack ss) - { - if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream) - { - await deflateStream.FlushAsync().ConfigureAwait(false); - } - else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream) - { - await lzmaStream.FlushAsync().ConfigureAwait(false); - } - } -#if DEBUG_STREAMS - this.DebugDispose(typeof(EntryStream)); -#endif - await base.DisposeAsync().ConfigureAwait(false); - await _stream.DisposeAsync().ConfigureAwait(false); - } -#endif - public override bool CanRead => true; public override bool CanSeek => false; @@ -154,38 +112,6 @@ public class EntryStream : Stream, IStreamStack return read; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - var read = await _stream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (read <= 0) - { - _completed = true; - } - return read; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - if (read <= 0) - { - _completed = true; - } - return read; - } -#endif - public override int ReadByte() { var value = _stream.ReadByte(); diff --git a/src/SharpCompress/Common/ExtractionMethods.Async.cs b/src/SharpCompress/Common/ExtractionMethods.Async.cs new file mode 100644 index 00000000..b5653877 --- /dev/null +++ b/src/SharpCompress/Common/ExtractionMethods.Async.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common; + +internal static partial class ExtractionMethods +{ + public static async ValueTask WriteEntryToDirectoryAsync( + IEntry entry, + string destinationDirectory, + ExtractionOptions? options, + Func writeAsync, + CancellationToken cancellationToken = default + ) + { + string destinationFileName; + var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory); + + //check for trailing slash. + if ( + fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1] + != Path.DirectorySeparatorChar + ) + { + fullDestinationDirectoryPath += Path.DirectorySeparatorChar; + } + + if (!Directory.Exists(fullDestinationDirectoryPath)) + { + throw new ExtractionException( + $"Directory does not exist to extract to: {fullDestinationDirectoryPath}" + ); + } + + options ??= new ExtractionOptions() { Overwrite = true }; + + var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); + file = Utility.ReplaceInvalidFileNameChars(file); + if (options.ExtractFullPath) + { + var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) + .NotNull("Directory is null"); + var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder)); + + if (!Directory.Exists(destdir)) + { + if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison)) + { + throw new ExtractionException( + "Entry is trying to create a directory outside of the destination directory." + ); + } + + Directory.CreateDirectory(destdir); + } + destinationFileName = Path.Combine(destdir, file); + } + else + { + destinationFileName = Path.Combine(fullDestinationDirectoryPath, file); + } + + if (!entry.IsDirectory) + { + destinationFileName = Path.GetFullPath(destinationFileName); + + if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison)) + { + throw new ExtractionException( + "Entry is trying to write a file outside of the destination directory." + ); + } + await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false); + } + else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) + { + Directory.CreateDirectory(destinationFileName); + } + } + + public static async ValueTask WriteEntryToFileAsync( + IEntry entry, + string destinationFileName, + ExtractionOptions? options, + Func openAndWriteAsync, + CancellationToken cancellationToken = default + ) + { + if (entry.LinkTarget != null) + { + if (options?.WriteSymbolicLink is null) + { + throw new ExtractionException( + "Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null" + ); + } + options.WriteSymbolicLink(destinationFileName, entry.LinkTarget); + } + else + { + var fm = FileMode.Create; + options ??= new ExtractionOptions() { Overwrite = true }; + + if (!options.Overwrite) + { + fm = FileMode.CreateNew; + } + + await openAndWriteAsync(destinationFileName, fm, cancellationToken) + .ConfigureAwait(false); + entry.PreserveExtractionOptions(destinationFileName, options); + } + } +} diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 787771de..db6719f7 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common; -internal static class ExtractionMethods +internal static partial class ExtractionMethods { /// /// Gets the appropriate StringComparison for path checks based on the file system. @@ -123,111 +123,4 @@ internal static class ExtractionMethods entry.PreserveExtractionOptions(destinationFileName, options); } } - - public static async ValueTask WriteEntryToDirectoryAsync( - IEntry entry, - string destinationDirectory, - ExtractionOptions? options, - Func writeAsync, - CancellationToken cancellationToken = default - ) - { - string destinationFileName; - var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory); - - //check for trailing slash. - if ( - fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1] - != Path.DirectorySeparatorChar - ) - { - fullDestinationDirectoryPath += Path.DirectorySeparatorChar; - } - - if (!Directory.Exists(fullDestinationDirectoryPath)) - { - throw new ExtractionException( - $"Directory does not exist to extract to: {fullDestinationDirectoryPath}" - ); - } - - options ??= new ExtractionOptions() { Overwrite = true }; - - var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); - file = Utility.ReplaceInvalidFileNameChars(file); - if (options.ExtractFullPath) - { - var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) - .NotNull("Directory is null"); - var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder)); - - if (!Directory.Exists(destdir)) - { - if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison)) - { - throw new ExtractionException( - "Entry is trying to create a directory outside of the destination directory." - ); - } - - Directory.CreateDirectory(destdir); - } - destinationFileName = Path.Combine(destdir, file); - } - else - { - destinationFileName = Path.Combine(fullDestinationDirectoryPath, file); - } - - if (!entry.IsDirectory) - { - destinationFileName = Path.GetFullPath(destinationFileName); - - if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison)) - { - throw new ExtractionException( - "Entry is trying to write a file outside of the destination directory." - ); - } - await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false); - } - else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) - { - Directory.CreateDirectory(destinationFileName); - } - } - - public static async ValueTask WriteEntryToFileAsync( - IEntry entry, - string destinationFileName, - ExtractionOptions? options, - Func openAndWriteAsync, - CancellationToken cancellationToken = default - ) - { - if (entry.LinkTarget != null) - { - if (options?.WriteSymbolicLink is null) - { - throw new ExtractionException( - "Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null" - ); - } - options.WriteSymbolicLink(destinationFileName, entry.LinkTarget); - } - else - { - var fm = FileMode.Create; - options ??= new ExtractionOptions() { Overwrite = true }; - - if (!options.Overwrite) - { - fm = FileMode.CreateNew; - } - - await openAndWriteAsync(destinationFileName, fm, cancellationToken) - .ConfigureAwait(false); - entry.PreserveExtractionOptions(destinationFileName, options); - } - } } diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs new file mode 100644 index 00000000..34c3c697 --- /dev/null +++ b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs @@ -0,0 +1,133 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Tar.Headers; +using SharpCompress.Compressors.Deflate; + +namespace SharpCompress.Common.GZip; + +internal sealed partial class GZipFilePart +{ + internal static async ValueTask CreateAsync( + Stream stream, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken = default + ) + { + var part = new GZipFilePart(stream, archiveEncoding); + + await part.ReadAndValidateGzipHeaderAsync(cancellationToken); + if (stream.CanSeek) + { + var position = stream.Position; + stream.Position = stream.Length - 8; + await part.ReadTrailerAsync(cancellationToken); + 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 async ValueTask ReadTrailerAsync(CancellationToken cancellationToken = default) + { + // Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32 + var trailer = new byte[8]; + _ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken); + + Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer); + UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4)); + } + + private async ValueTask ReadAndValidateGzipHeaderAsync( + CancellationToken cancellationToken = default + ) + { + // read the header on the first read + var header = new byte[10]; + var n = await _stream.ReadAsync(header, 0, 10, cancellationToken); + + // workitem 8501: handle edge case (decompress empty stream) + if (n == 0) + { + return; + } + + if (n != 10) + { + throw new ZlibException("Not a valid GZIP stream."); + } + + if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8) + { + throw new ZlibException("Bad GZIP header."); + } + + var timet = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan().Slice(4)); + DateModified = TarHeader.EPOCH.AddSeconds(timet); + if ((header[3] & 0x04) == 0x04) + { + // read and discard extra field + var lengthField = new byte[2]; + _ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken); + + var extraLength = (short)(lengthField[0] + (lengthField[1] * 256)); + var extra = new byte[extraLength]; + + if (!await _stream.ReadFullyAsync(extra, cancellationToken)) + { + throw new ZlibException("Unexpected end-of-file reading GZIP header."); + } + } + if ((header[3] & 0x08) == 0x08) + { + _name = await ReadZeroTerminatedStringAsync(_stream, cancellationToken); + } + if ((header[3] & 0x10) == 0x010) + { + await ReadZeroTerminatedStringAsync(_stream, cancellationToken); + } + if ((header[3] & 0x02) == 0x02) + { + var buf = new byte[1]; + _ = await _stream.ReadAsync(buf, 0, 1, cancellationToken); // CRC16, ignore + } + } + + private async ValueTask ReadZeroTerminatedStringAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var buf1 = new byte[1]; + var list = new List(); + var done = false; + do + { + // workitem 7740 + var n = await stream.ReadAsync(buf1, 0, 1, cancellationToken); + if (n != 1) + { + throw new ZlibException("Unexpected EOF reading GZIP header."); + } + if (buf1[0] == 0) + { + done = true; + } + else + { + list.Add(buf1[0]); + } + } while (!done); + var buffer = list.ToArray(); + return ArchiveEncoding.Decode(buffer); + } +} diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.cs b/src/SharpCompress/Common/GZip/GZipFilePart.cs index 7809bf70..1134fa89 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.cs @@ -2,15 +2,13 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Tar.Headers; using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; namespace SharpCompress.Common.GZip; -internal sealed class GZipFilePart : FilePart +internal sealed partial class GZipFilePart : FilePart { private string? _name; private readonly Stream _stream; @@ -37,32 +35,6 @@ internal sealed class GZipFilePart : FilePart return part; } - internal static async ValueTask CreateAsync( - Stream stream, - IArchiveEncoding archiveEncoding, - CancellationToken cancellationToken = default - ) - { - var part = new GZipFilePart(stream, archiveEncoding); - - await part.ReadAndValidateGzipHeaderAsync(cancellationToken); - if (stream.CanSeek) - { - var position = stream.Position; - stream.Position = stream.Length - 8; - await part.ReadTrailerAsync(cancellationToken); - 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) : base(archiveEncoding) => _stream = stream; @@ -94,16 +66,6 @@ internal sealed class GZipFilePart : FilePart UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.Slice(4)); } - private async ValueTask ReadTrailerAsync(CancellationToken cancellationToken = default) - { - // Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32 - var trailer = new byte[8]; - _ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken); - - Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer); - UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4)); - } - private void ReadAndValidateGzipHeader() { // read the header on the first read @@ -156,61 +118,6 @@ internal sealed class GZipFilePart : FilePart } } - private async ValueTask ReadAndValidateGzipHeaderAsync( - CancellationToken cancellationToken = default - ) - { - // read the header on the first read - var header = new byte[10]; - var n = await _stream.ReadAsync(header, 0, 10, cancellationToken); - - // workitem 8501: handle edge case (decompress empty stream) - if (n == 0) - { - return; - } - - if (n != 10) - { - throw new ZlibException("Not a valid GZIP stream."); - } - - if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8) - { - throw new ZlibException("Bad GZIP header."); - } - - var timet = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan().Slice(4)); - DateModified = TarHeader.EPOCH.AddSeconds(timet); - if ((header[3] & 0x04) == 0x04) - { - // read and discard extra field - var lengthField = new byte[2]; - _ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken); - - var extraLength = (short)(lengthField[0] + (lengthField[1] * 256)); - var extra = new byte[extraLength]; - - if (!await _stream.ReadFullyAsync(extra, cancellationToken)) - { - throw new ZlibException("Unexpected end-of-file reading GZIP header."); - } - } - if ((header[3] & 0x08) == 0x08) - { - _name = await ReadZeroTerminatedStringAsync(_stream, cancellationToken); - } - if ((header[3] & 0x10) == 0x010) - { - await ReadZeroTerminatedStringAsync(_stream, cancellationToken); - } - if ((header[3] & 0x02) == 0x02) - { - var buf = new byte[1]; - _ = await _stream.ReadAsync(buf, 0, 1, cancellationToken); // CRC16, ignore - } - } - private string ReadZeroTerminatedString(Stream stream) { Span buf1 = stackalloc byte[1]; @@ -236,33 +143,4 @@ internal sealed class GZipFilePart : FilePart var buffer = list.ToArray(); return ArchiveEncoding.Decode(buffer); } - - private async ValueTask ReadZeroTerminatedStringAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var buf1 = new byte[1]; - var list = new List(); - var done = false; - do - { - // workitem 7740 - var n = await stream.ReadAsync(buf1, 0, 1, cancellationToken); - if (n != 1) - { - throw new ZlibException("Unexpected EOF reading GZIP header."); - } - if (buf1[0] == 0) - { - done = true; - } - else - { - list.Add(buf1[0]); - } - } while (!done); - var buffer = list.ToArray(); - return ArchiveEncoding.Decode(buffer); - } } diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs new file mode 100644 index 00000000..ef3d4b02 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs @@ -0,0 +1,32 @@ +#nullable disable + +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar.Headers; + +internal sealed partial class ArchiveCryptHeader +{ + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) => + await CreateChildAsync( + header, + reader, + HeaderType.Crypt, + cancellationToken + ) + .ConfigureAwait(false); + + protected sealed override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + CryptInfo = await Rar5CryptoInfo.CreateAsync(reader, false); + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs index c3bf4953..1f5ab837 100644 --- a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.cs @@ -1,40 +1,17 @@ #nullable disable -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; -internal class ArchiveCryptHeader : RarHeader +internal sealed partial class ArchiveCryptHeader : RarHeader { public static ArchiveCryptHeader Create(RarHeader header, RarCrcBinaryReader reader) => CreateChild(header, reader, HeaderType.Crypt); - public static async ValueTask CreateAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - CancellationToken cancellationToken = default - ) => - await CreateChildAsync( - header, - reader, - HeaderType.Crypt, - cancellationToken - ) - .ConfigureAwait(false); - public Rar5CryptoInfo CryptInfo = default!; - protected override void ReadFinish(MarkingBinaryReader reader) => + protected sealed override void ReadFinish(MarkingBinaryReader reader) => CryptInfo = Rar5CryptoInfo.Create(reader, false); - - protected override async ValueTask ReadFinishAsync( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - CryptInfo = await Rar5CryptoInfo.CreateAsync(reader, false); - } } diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.Async.cs new file mode 100644 index 00000000..9be7d4e9 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.Async.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar.Headers; + +internal sealed partial class ArchiveHeader +{ + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) => + await CreateChildAsync(header, reader, HeaderType.Archive, cancellationToken) + .ConfigureAwait(false); + + protected sealed override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + if (IsRar5) + { + Flags = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + if (HasFlag(ArchiveFlagsV5.HAS_VOLUME_NUMBER)) + { + VolumeNumber = (int) + await reader + .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) + .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); + } + } + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs index e2eecf73..d3b5ff25 100644 --- a/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveHeader.cs @@ -1,24 +1,14 @@ -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; -internal sealed class ArchiveHeader : RarHeader +internal sealed partial class ArchiveHeader : RarHeader { public static ArchiveHeader Create(RarHeader header, RarCrcBinaryReader reader) => CreateChild(header, reader, HeaderType.Archive); - public static async ValueTask CreateAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - CancellationToken cancellationToken = default - ) => - await CreateChildAsync(header, reader, HeaderType.Archive, cancellationToken) - .ConfigureAwait(false); - - protected override void ReadFinish(MarkingBinaryReader reader) + protected sealed override void ReadFinish(MarkingBinaryReader reader) { if (IsRar5) { @@ -44,42 +34,6 @@ internal sealed class ArchiveHeader : RarHeader } } - protected override async ValueTask ReadFinishAsync( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - if (IsRar5) - { - Flags = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - if (HasFlag(ArchiveFlagsV5.HAS_VOLUME_NUMBER)) - { - VolumeNumber = (int) - await reader - .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) - .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.Async.cs b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.Async.cs new file mode 100644 index 00000000..8311c796 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.Async.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar.Headers; + +internal sealed partial class EndArchiveHeader +{ + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) => + await CreateChildAsync( + header, + reader, + HeaderType.EndArchive, + cancellationToken + ) + .ConfigureAwait(false); + + protected sealed override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + if (IsRar5) + { + Flags = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .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); + } + } + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs index 5eee7cdc..def87962 100644 --- a/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/EndArchiveHeader.cs @@ -1,29 +1,14 @@ -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; -internal class EndArchiveHeader : RarHeader +internal sealed partial class EndArchiveHeader : RarHeader { public static EndArchiveHeader Create(RarHeader header, RarCrcBinaryReader reader) => CreateChild(header, reader, HeaderType.EndArchive); - public static async ValueTask CreateAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - CancellationToken cancellationToken = default - ) => - await CreateChildAsync( - header, - reader, - HeaderType.EndArchive, - cancellationToken - ) - .ConfigureAwait(false); - - protected override void ReadFinish(MarkingBinaryReader reader) + protected sealed override void ReadFinish(MarkingBinaryReader reader) { if (IsRar5) { @@ -43,31 +28,6 @@ internal class EndArchiveHeader : RarHeader } } - protected override async ValueTask ReadFinishAsync( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - if (IsRar5) - { - Flags = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .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.Async.cs b/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs new file mode 100644 index 00000000..845da53f --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs @@ -0,0 +1,441 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; +#if !Rar2017_64bit +using size_t = System.UInt32; +#else +using nint = System.Int64; +using nuint = System.UInt64; +using size_t = System.UInt64; +#endif + +namespace SharpCompress.Common.Rar.Headers; + +internal partial class FileHeader +{ + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + HeaderType headerType, + CancellationToken cancellationToken = default + ) => + await CreateChildAsync(header, reader, headerType, cancellationToken) + .ConfigureAwait(false); + + protected override async ValueTask ReadFinishAsync( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken + ) + { + if (IsRar5) + { + await ReadFromReaderV5Async(reader, cancellationToken).ConfigureAwait(false); + } + else + { + await ReadFromReaderV4Async(reader, cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask ReadFromReaderV5Async( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken + ) + { + Flags = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + var lvalue = checked( + (long) + await reader + .ReadRarVIntAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false) + ); + + UncompressedSize = HasFlag(FileFlagsV5.UNPACKED_SIZE_UNKNOWN) ? long.MaxValue : lvalue; + + FileAttributes = await reader + .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (HasFlag(FileFlagsV5.HAS_MOD_TIME)) + { + FileLastModifiedTime = Utility.UnixTimeToDateTime( + await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) + ); + } + + if (HasFlag(FileFlagsV5.HAS_CRC32)) + { + FileCrc = await reader.ReadBytesAsync(4, cancellationToken).ConfigureAwait(false); + } + + var compressionInfo = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .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(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + var nameSize = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + var b = await reader.ReadBytesAsync(nameSize, cancellationToken).ConfigureAwait(false); + FileName = ConvertPathV5(Encoding.UTF8.GetString(b, 0, b.Length)); + + if (ExtraSize != (uint)RemainingHeaderBytesAsync(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 (reader.CurrentReadByteCount < HeaderSize) + { + var size = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + var n = HeaderSize - reader.CurrentReadByteCount; + var type = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + switch (type) + { + case FHEXTRA_CRYPT: + { + Rar5CryptoInfo = await Rar5CryptoInfo.CreateAsync(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 ( + await reader + .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) + .ConfigureAwait(false) == FHEXTRA_HASH_BLAKE2 + ) + { + _hash = await reader + .ReadBytesAsync(BLAKE2_DIGEST_SIZE, cancellationToken) + .ConfigureAwait(false); + } + } + break; + case FHEXTRA_HTIME: + { + var flags = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + var isWindowsTime = (flags & 1) == 0; + if ((flags & 0x2) == 0x2) + { + FileLastModifiedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ) + .ConfigureAwait(false); + } + if ((flags & 0x4) == 0x4) + { + FileCreatedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ) + .ConfigureAwait(false); + } + if ((flags & 0x8) == 0x8) + { + FileLastAccessedTime = await ReadExtendedTimeV5Async( + reader, + isWindowsTime, + cancellationToken + ) + .ConfigureAwait(false); + } + } + break; + case FHEXTRA_REDIR: + { + RedirType = await reader + .ReadRarVIntByteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + RedirFlags = await reader + .ReadRarVIntByteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + var nn = await reader + .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) + .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 = (int)(n - (HeaderSize - reader.CurrentReadByteCount)); + var drain = size - did; + if (drain > 0) + { + await reader.ReadBytesAsync(drain, cancellationToken).ConfigureAwait(false); + } + } + + if (AdditionalDataSize != 0) + { + CompressedSize = AdditionalDataSize; + } + } + + private async ValueTask ReadFromReaderV4Async( + AsyncMarkingBinaryReader reader, + CancellationToken cancellationToken + ) + { + 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); + + FileLastModifiedTime = Utility.DosDateToDateTime( + await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) + ); + + CompressionAlgorithm = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + CompressionMethod = (byte)( + (await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false)) - 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); + } + } + 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.Take(4).ToArray())) + { + 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); + } + if (HasFlag(FileFlagsV4.EXT_TIME)) + { + if (reader.CurrentReadByteCount >= 2) + { + var extendedFlags = await reader + .ReadUInt16Async(cancellationToken) + .ConfigureAwait(false); + if (FileLastModifiedTime is not null) + { + FileLastModifiedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + FileLastModifiedTime, + reader, + 0, + cancellationToken + ) + .ConfigureAwait(false); + } + + FileCreatedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 1, + cancellationToken + ) + .ConfigureAwait(false); + FileLastAccessedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 2, + cancellationToken + ) + .ConfigureAwait(false); + FileArchivedTime = await ProcessExtendedTimeV4Async( + extendedFlags, + null, + reader, + 3, + cancellationToken + ) + .ConfigureAwait(false); + } + } + } + + private static async ValueTask ReadExtendedTimeV5Async( + AsyncMarkingBinaryReader reader, + bool isWindowsTime, + CancellationToken cancellationToken + ) + { + if (isWindowsTime) + { + return DateTime.FromFileTime( + await reader.ReadInt64Async(cancellationToken).ConfigureAwait(false) + ); + } + else + { + return Utility.UnixTimeToDateTime( + await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) + ); + } + } + + private static async ValueTask ProcessExtendedTimeV4Async( + ushort extendedFlags, + DateTime? time, + AsyncMarkingBinaryReader reader, + int i, + CancellationToken cancellationToken + ) + { + 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; + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/FileHeader.cs b/src/SharpCompress/Common/Rar/Headers/FileHeader.cs index 09985fe2..f0b7a53c 100644 --- a/src/SharpCompress/Common/Rar/Headers/FileHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/FileHeader.cs @@ -16,7 +16,7 @@ using size_t = System.UInt64; namespace SharpCompress.Common.Rar.Headers; -internal class FileHeader : RarHeader +internal partial class FileHeader : RarHeader { private byte[]? _hash; @@ -26,15 +26,6 @@ internal class FileHeader : RarHeader HeaderType headerType ) => CreateChild(header, reader, headerType); - public static async ValueTask CreateAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - HeaderType headerType, - CancellationToken cancellationToken = default - ) => - await CreateChildAsync(header, reader, headerType, cancellationToken) - .ConfigureAwait(false); - protected override void ReadFinish(MarkingBinaryReader reader) { if (IsRar5) @@ -47,21 +38,6 @@ internal class FileHeader : RarHeader } } - protected override async ValueTask ReadFinishAsync( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - if (IsRar5) - { - await ReadFromReaderV5Async(reader, cancellationToken).ConfigureAwait(false); - } - else - { - await ReadFromReaderV4Async(reader, cancellationToken).ConfigureAwait(false); - } - } - private void ReadFromReaderV5(MarkingBinaryReader reader) { Flags = reader.ReadRarVIntUInt16(); @@ -106,23 +82,6 @@ internal class FileHeader : RarHeader var nameSize = reader.ReadRarVIntUInt16(); - // Variable length field containing Name length bytes in UTF-8 format without trailing zero. - // For file header this is a name of archived file. Forward slash character is used as the path separator both for Unix and Windows names. - // Backslashes are treated as a part of name for Unix names and as invalid character for Windows file names. Type of name is defined by Host OS field. - // - // TODO: not sure if anything needs to be done to handle the following: - // If Unix file name contains any high ASCII characters which cannot be correctly converted to Unicode and UTF-8 - // we map such characters to to 0xE080 - 0xE0FF private use Unicode area and insert 0xFFFE Unicode non-character - // to resulting string to indicate that it contains mapped characters, which need to be converted back when extracting. - // Concrete position of 0xFFFE is not defined, we need to search the entire string for it. Such mapped names are not - // portable and can be correctly unpacked only on the same system where they were created. - // - // For service header this field contains a name of service header. Now the following names are used: - // CMT Archive comment - // QO Archive quick open data - // ACL NTFS file permissions - // STM NTFS alternate data stream - // RR Recovery record var b = reader.ReadBytes(nameSize); FileName = ConvertPathV5(Encoding.UTF8.GetString(b, 0, b.Length)); @@ -160,14 +119,11 @@ internal class FileHeader : RarHeader case FHEXTRA_HASH: { const uint FHEXTRA_HASH_BLAKE2 = 0x0; - // const uint HASH_BLAKE2 = 0x03; const int BLAKE2_DIGEST_SIZE = 0x20; if ((uint)reader.ReadRarVInt() == FHEXTRA_HASH_BLAKE2) { - // var hash = HASH_BLAKE2; _hash = reader.ReadBytes(BLAKE2_DIGEST_SIZE); } - // enum HASH_TYPE {HASH_NONE,HASH_RAR14,HASH_CRC32,HASH_BLAKE2}; } break; case FHEXTRA_HTIME: // file time @@ -188,12 +144,6 @@ internal class FileHeader : RarHeader } } break; - //TODO - // case FHEXTRA_VERSION: // file version - // { - // - // } - // break; case FHEXTRA_REDIR: // file system redirection { RedirType = reader.ReadRarVIntByte(); @@ -203,21 +153,7 @@ internal class FileHeader : RarHeader RedirTargetName = ConvertPathV5(Encoding.UTF8.GetString(bb, 0, bb.Length)); } break; - //TODO - // case FHEXTRA_UOWNER: // unix owner - // { - // - // } - // break; - // case FHEXTRA_SUBDATA: // service data - // { - // - // } - // break; - default: - // skip unknown record types to allow new record types to be added in the future - //Console.WriteLine($"unhandled rar header field type {type}"); break; } // drain any trailing bytes of extra record @@ -366,8 +302,6 @@ internal class FileHeader : RarHeader } if (HasFlag(FileFlagsV4.EXT_TIME)) { - // verify that the end of the header hasn't been reached before reading the Extended Time. - // some tools incorrectly omit Extended Time despite specifying FileFlags.EXTTIME, which most parsers tolerate. if (RemainingHeaderBytes(reader) >= 2) { var extendedFlags = reader.ReadUInt16(); @@ -445,403 +379,6 @@ internal class FileHeader : RarHeader return path; } - private async ValueTask ReadFromReaderV5Async( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - Flags = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - var lvalue = checked( - (long) - await reader - .ReadRarVIntAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false) - ); - - UncompressedSize = HasFlag(FileFlagsV5.UNPACKED_SIZE_UNKNOWN) ? long.MaxValue : lvalue; - - FileAttributes = await reader - .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - if (HasFlag(FileFlagsV5.HAS_MOD_TIME)) - { - FileLastModifiedTime = Utility.UnixTimeToDateTime( - await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) - ); - } - - if (HasFlag(FileFlagsV5.HAS_CRC32)) - { - FileCrc = await reader.ReadBytesAsync(4, cancellationToken).ConfigureAwait(false); - } - - var compressionInfo = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .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(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - var nameSize = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - var b = await reader.ReadBytesAsync(nameSize, cancellationToken).ConfigureAwait(false); - FileName = ConvertPathV5(Encoding.UTF8.GetString(b, 0, b.Length)); - - if (ExtraSize != (uint)RemainingHeaderBytesAsync(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 (reader.CurrentReadByteCount < HeaderSize) - { - var size = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - var n = HeaderSize - reader.CurrentReadByteCount; - var type = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - switch (type) - { - case FHEXTRA_CRYPT: - { - Rar5CryptoInfo = await Rar5CryptoInfo.CreateAsync(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 ( - await reader - .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) - .ConfigureAwait(false) == FHEXTRA_HASH_BLAKE2 - ) - { - _hash = await reader - .ReadBytesAsync(BLAKE2_DIGEST_SIZE, cancellationToken) - .ConfigureAwait(false); - } - } - break; - case FHEXTRA_HTIME: - { - var flags = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - var isWindowsTime = (flags & 1) == 0; - if ((flags & 0x2) == 0x2) - { - FileLastModifiedTime = await ReadExtendedTimeV5Async( - reader, - isWindowsTime, - cancellationToken - ) - .ConfigureAwait(false); - } - if ((flags & 0x4) == 0x4) - { - FileCreatedTime = await ReadExtendedTimeV5Async( - reader, - isWindowsTime, - cancellationToken - ) - .ConfigureAwait(false); - } - if ((flags & 0x8) == 0x8) - { - FileLastAccessedTime = await ReadExtendedTimeV5Async( - reader, - isWindowsTime, - cancellationToken - ) - .ConfigureAwait(false); - } - } - break; - case FHEXTRA_REDIR: - { - RedirType = await reader - .ReadRarVIntByteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - RedirFlags = await reader - .ReadRarVIntByteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - var nn = await reader - .ReadRarVIntUInt16Async(cancellationToken: cancellationToken) - .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 = (int)(n - (HeaderSize - reader.CurrentReadByteCount)); - var drain = size - did; - if (drain > 0) - { - await reader.ReadBytesAsync(drain, cancellationToken).ConfigureAwait(false); - } - } - - if (AdditionalDataSize != 0) - { - CompressedSize = AdditionalDataSize; - } - } - - private async ValueTask ReadFromReaderV4Async( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - 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); - - FileLastModifiedTime = Utility.DosDateToDateTime( - await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) - ); - - CompressionAlgorithm = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); - CompressionMethod = (byte)( - (await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false)) - 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); - } - } - 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.Take(4).ToArray())) - { - 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); - } - if (HasFlag(FileFlagsV4.EXT_TIME)) - { - if (reader.CurrentReadByteCount >= 2) - { - var extendedFlags = await reader - .ReadUInt16Async(cancellationToken) - .ConfigureAwait(false); - if (FileLastModifiedTime is not null) - { - FileLastModifiedTime = await ProcessExtendedTimeV4Async( - extendedFlags, - FileLastModifiedTime, - reader, - 0, - cancellationToken - ) - .ConfigureAwait(false); - } - - FileCreatedTime = await ProcessExtendedTimeV4Async( - extendedFlags, - null, - reader, - 1, - cancellationToken - ) - .ConfigureAwait(false); - FileLastAccessedTime = await ProcessExtendedTimeV4Async( - extendedFlags, - null, - reader, - 2, - cancellationToken - ) - .ConfigureAwait(false); - FileArchivedTime = await ProcessExtendedTimeV4Async( - extendedFlags, - null, - reader, - 3, - cancellationToken - ) - .ConfigureAwait(false); - } - } - } - - private static async ValueTask ReadExtendedTimeV5Async( - AsyncMarkingBinaryReader reader, - bool isWindowsTime, - CancellationToken cancellationToken - ) - { - if (isWindowsTime) - { - return DateTime.FromFileTime( - await reader.ReadInt64Async(cancellationToken).ConfigureAwait(false) - ); - } - else - { - return Utility.UnixTimeToDateTime( - await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false) - ); - } - } - - private static async ValueTask ProcessExtendedTimeV4Async( - ushort extendedFlags, - DateTime? time, - AsyncMarkingBinaryReader reader, - int i, - CancellationToken cancellationToken - ) - { - 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; - } - public override string ToString() => FileName ?? "FileHeader"; private ushort Flags { get; set; } diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs new file mode 100644 index 00000000..19112f78 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Rar.Headers; + +internal partial class MarkHeader +{ + private static async ValueTask GetByteAsync( + Stream stream, + CancellationToken cancellationToken + ) + { + var buffer = new byte[1]; + var bytesRead = await stream + .ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead == 1) + { + return buffer[0]; + } + throw new EndOfStreamException(); + } + + public static async ValueTask ReadAsync( + Stream stream, + bool leaveStreamOpen, + bool lookForHeader, + CancellationToken cancellationToken = default + ) + { + var maxScanIndex = lookForHeader ? MAX_SFX_SIZE : 0; + try + { + var start = -1; + var b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + while (start <= maxScanIndex) + { + if (b == 0x52) + { + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b == 0x61) + { + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x72) + { + continue; + } + + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x21) + { + continue; + } + + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x1a) + { + continue; + } + + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x07) + { + continue; + } + + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b == 1) + { + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0) + { + continue; + } + + return new MarkHeader(true); // Rar5 + } + else if (b == 0) + { + return new MarkHeader(false); // Rar4 + } + } + else if (b == 0x45) + { + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x7e) + { + continue; + } + + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + if (b != 0x5e) + { + continue; + } + + throw new InvalidFormatException( + "Rar format version pre-4 is unsupported." + ); + } + } + else + { + b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); + start++; + } + } + } + catch (Exception e) + { + if (!leaveStreamOpen) + { + stream.Dispose(); + } + throw new InvalidFormatException("Error trying to read rar signature.", e); + } + + throw new InvalidFormatException("Rar signature not found"); + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.cs index 74c4bee1..33cf5ae2 100644 --- a/src/SharpCompress/Common/Rar/Headers/MarkHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/MarkHeader.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Rar.Headers; -internal class MarkHeader : IRarHeader +internal partial class MarkHeader : IRarHeader { private const int MAX_SFX_SIZE = 0x80000 - 16; //archive.cpp line 136 @@ -27,22 +27,6 @@ internal class MarkHeader : IRarHeader throw new EndOfStreamException(); } - private static async ValueTask GetByteAsync( - Stream stream, - CancellationToken cancellationToken - ) - { - var buffer = new byte[1]; - var bytesRead = await stream - .ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead == 1) - { - return buffer[0]; - } - throw new EndOfStreamException(); - } - public static MarkHeader Read(Stream stream, bool leaveStreamOpen, bool lookForHeader) { var maxScanIndex = lookForHeader ? MAX_SFX_SIZE : 0; @@ -147,111 +131,4 @@ internal class MarkHeader : IRarHeader throw new InvalidFormatException("Rar signature not found"); } - - public static async ValueTask ReadAsync( - Stream stream, - bool leaveStreamOpen, - bool lookForHeader, - CancellationToken cancellationToken = default - ) - { - var maxScanIndex = lookForHeader ? MAX_SFX_SIZE : 0; - try - { - var start = -1; - var b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - while (start <= maxScanIndex) - { - if (b == 0x52) - { - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b == 0x61) - { - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x72) - { - continue; - } - - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x21) - { - continue; - } - - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x1a) - { - continue; - } - - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x07) - { - continue; - } - - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b == 1) - { - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0) - { - continue; - } - - return new MarkHeader(true); // Rar5 - } - else if (b == 0) - { - return new MarkHeader(false); // Rar4 - } - } - else if (b == 0x45) - { - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x7e) - { - continue; - } - - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - if (b != 0x5e) - { - continue; - } - - throw new InvalidFormatException( - "Rar format version pre-4 is unsupported." - ); - } - } - else - { - b = await GetByteAsync(stream, cancellationToken).ConfigureAwait(false); - start++; - } - } - } - catch (Exception e) - { - if (!leaveStreamOpen) - { - stream.Dispose(); - } - throw new InvalidFormatException("Error trying to read rar signature.", e); - } - - throw new InvalidFormatException("Rar signature not found"); - } } diff --git a/src/SharpCompress/Common/Rar/Headers/ProtectHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.Async.cs new file mode 100644 index 00000000..0fe33a38 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.Async.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar.Headers; + +internal sealed partial class ProtectHeader +{ + public static async ValueTask CreateAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + CancellationToken cancellationToken = default + ) + { + var c = await CreateChildAsync( + header, + reader, + HeaderType.Protect, + cancellationToken + ) + .ConfigureAwait(false); + if (c.IsRar5) + { + throw new InvalidFormatException("unexpected rar5 record"); + } + return c; + } + + protected sealed 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); + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs index c0f0d71b..71b9c420 100644 --- a/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/ProtectHeader.cs @@ -1,11 +1,9 @@ -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar; using SharpCompress.IO; namespace SharpCompress.Common.Rar.Headers; -internal sealed class ProtectHeader : RarHeader +internal sealed partial class ProtectHeader : RarHeader { public static ProtectHeader Create(RarHeader header, RarCrcBinaryReader reader) { @@ -17,27 +15,7 @@ internal sealed class ProtectHeader : RarHeader return c; } - public static async ValueTask CreateAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - CancellationToken cancellationToken = default - ) - { - var c = await CreateChildAsync( - header, - reader, - HeaderType.Protect, - cancellationToken - ) - .ConfigureAwait(false); - if (c.IsRar5) - { - throw new InvalidFormatException("unexpected rar5 record"); - } - return c; - } - - protected override void ReadFinish(MarkingBinaryReader reader) + protected sealed override void ReadFinish(MarkingBinaryReader reader) { Version = reader.ReadByte(); RecSectors = reader.ReadUInt16(); @@ -45,17 +23,6 @@ internal sealed class ProtectHeader : RarHeader Mark = reader.ReadBytes(8); } - protected override async ValueTask ReadFinishAsync( - AsyncMarkingBinaryReader reader, - CancellationToken cancellationToken - ) - { - 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.Async.cs b/src/SharpCompress/Common/Rar/Headers/RarHeader.Async.cs new file mode 100644 index 00000000..e4692a97 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/RarHeader.Async.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Common.Rar.Headers; + +internal partial class RarHeader +{ + internal static async ValueTask TryReadBaseAsync( + AsyncRarCrcBinaryReader reader, + bool isRar5, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken = default + ) + { + try + { + var header = new RarHeader(); + await header + .InitializeAsync(reader, isRar5, archiveEncoding, cancellationToken) + .ConfigureAwait(false); + return header; + } + catch (InvalidFormatException) + { + return null; + } + } + + private async ValueTask InitializeAsync( + AsyncRarCrcBinaryReader reader, + bool isRar5, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken + ) + { + _headerType = HeaderType.Null; + _isRar5 = isRar5; + ArchiveEncoding = archiveEncoding; + if (IsRar5) + { + HeaderCrc = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); + reader.ResetCrc(); + HeaderSize = (int) + await reader.ReadRarVIntUInt32Async(3, cancellationToken).ConfigureAwait(false); + reader.Mark(); + HeaderCode = await reader + .ReadRarVIntByteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + HeaderFlags = await reader + .ReadRarVIntUInt16Async(2, cancellationToken) + .ConfigureAwait(false); + + if (HasHeaderFlag(HeaderFlagsV5.HAS_EXTRA)) + { + ExtraSize = await reader + .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + if (HasHeaderFlag(HeaderFlagsV5.HAS_DATA)) + { + AdditionalDataSize = (long) + await reader + .ReadRarVIntAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } + else + { + reader.Mark(); + HeaderCrc = await reader.ReadUInt16Async(cancellationToken).ConfigureAwait(false); + reader.ResetCrc(); + HeaderCode = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + HeaderFlags = await reader.ReadUInt16Async(cancellationToken).ConfigureAwait(false); + HeaderSize = await reader.ReadInt16Async(cancellationToken).ConfigureAwait(false); + if (HasHeaderFlag(HeaderFlagsV4.HAS_DATA)) + { + AdditionalDataSize = await reader + .ReadUInt32Async(cancellationToken) + .ConfigureAwait(false); + } + } + } + + internal static async ValueTask CreateChildAsync( + RarHeader header, + AsyncRarCrcBinaryReader reader, + HeaderType headerType, + CancellationToken cancellationToken = default + ) + where T : RarHeader, new() + { + var child = new T() { ArchiveEncoding = header.ArchiveEncoding }; + child._headerType = headerType; + child._isRar5 = header.IsRar5; + child.HeaderCrc = header.HeaderCrc; + child.HeaderCode = header.HeaderCode; + child.HeaderFlags = header.HeaderFlags; + child.HeaderSize = header.HeaderSize; + child.ExtraSize = header.ExtraSize; + child.AdditionalDataSize = header.AdditionalDataSize; + await child.ReadFinishAsync(reader, cancellationToken).ConfigureAwait(false); + + var n = child.RemainingHeaderBytesAsync(reader); + if (n > 0) + { + await reader.ReadBytesAsync(n, cancellationToken).ConfigureAwait(false); + } + + child.VerifyHeaderCrc(reader.GetCrc32()); + return child; + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs index 66dda152..23e08060 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Common.Rar.Headers; // http://www.forensicswiki.org/w/images/5/5b/RARFileStructure.txt // https://www.rarlab.com/technote.htm -internal class RarHeader : IRarHeader +internal partial class RarHeader : IRarHeader { private HeaderType _headerType; private bool _isRar5; @@ -36,27 +36,6 @@ internal class RarHeader : IRarHeader } } - internal static async ValueTask TryReadBaseAsync( - AsyncRarCrcBinaryReader reader, - bool isRar5, - IArchiveEncoding archiveEncoding, - CancellationToken cancellationToken = default - ) - { - try - { - var header = new RarHeader(); - await header - .InitializeAsync(reader, isRar5, archiveEncoding, cancellationToken) - .ConfigureAwait(false); - return header; - } - catch (InvalidFormatException) - { - return null; - } - } - private void Initialize( RarCrcBinaryReader reader, bool isRar5, @@ -99,61 +78,6 @@ internal class RarHeader : IRarHeader } } - private async ValueTask InitializeAsync( - AsyncRarCrcBinaryReader reader, - bool isRar5, - IArchiveEncoding archiveEncoding, - CancellationToken cancellationToken - ) - { - _headerType = HeaderType.Null; - _isRar5 = isRar5; - ArchiveEncoding = archiveEncoding; - if (IsRar5) - { - HeaderCrc = await reader.ReadUInt32Async(cancellationToken).ConfigureAwait(false); - reader.ResetCrc(); - HeaderSize = (int) - await reader.ReadRarVIntUInt32Async(3, cancellationToken).ConfigureAwait(false); - reader.Mark(); - HeaderCode = await reader - .ReadRarVIntByteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - HeaderFlags = await reader - .ReadRarVIntUInt16Async(2, cancellationToken) - .ConfigureAwait(false); - - if (HasHeaderFlag(HeaderFlagsV5.HAS_EXTRA)) - { - ExtraSize = await reader - .ReadRarVIntUInt32Async(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - if (HasHeaderFlag(HeaderFlagsV5.HAS_DATA)) - { - AdditionalDataSize = (long) - await reader - .ReadRarVIntAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - } - else - { - reader.Mark(); - HeaderCrc = await reader.ReadUInt16Async(cancellationToken).ConfigureAwait(false); - reader.ResetCrc(); - HeaderCode = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); - HeaderFlags = await reader.ReadUInt16Async(cancellationToken).ConfigureAwait(false); - HeaderSize = await reader.ReadInt16Async(cancellationToken).ConfigureAwait(false); - if (HasHeaderFlag(HeaderFlagsV4.HAS_DATA)) - { - AdditionalDataSize = await reader - .ReadUInt32Async(cancellationToken) - .ConfigureAwait(false); - } - } - } - internal static T CreateChild( RarHeader header, RarCrcBinaryReader reader, @@ -182,35 +106,6 @@ internal class RarHeader : IRarHeader return child; } - internal static async ValueTask CreateChildAsync( - RarHeader header, - AsyncRarCrcBinaryReader reader, - HeaderType headerType, - CancellationToken cancellationToken = default - ) - where T : RarHeader, new() - { - var child = new T() { ArchiveEncoding = header.ArchiveEncoding }; - child._headerType = headerType; - child._isRar5 = header.IsRar5; - child.HeaderCrc = header.HeaderCrc; - child.HeaderCode = header.HeaderCode; - child.HeaderFlags = header.HeaderFlags; - child.HeaderSize = header.HeaderSize; - child.ExtraSize = header.ExtraSize; - child.AdditionalDataSize = header.AdditionalDataSize; - await child.ReadFinishAsync(reader, cancellationToken).ConfigureAwait(false); - - var n = child.RemainingHeaderBytesAsync(reader); - if (n > 0) - { - await reader.ReadBytesAsync(n, cancellationToken).ConfigureAwait(false); - } - - child.VerifyHeaderCrc(reader.GetCrc32()); - return child; - } - protected int RemainingHeaderBytes(MarkingBinaryReader reader) => checked(HeaderSize - (int)reader.CurrentReadByteCount); diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs new file mode 100644 index 00000000..38b88b25 --- /dev/null +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar; +using SharpCompress.IO; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Rar.Headers; + +public partial class RarHeaderFactory +{ + public async IAsyncEnumerable ReadHeadersAsync(Stream stream) + { + var markHeader = await MarkHeader + .ReadAsync( + stream, + Options.LeaveStreamOpen, + Options.LookForHeader, + CancellationToken.None + ) + .ConfigureAwait(false); + _isRar5 = markHeader.IsRar5; + yield return markHeader; + + RarHeader? header; + while ( + ( + header = await TryReadNextHeaderAsync(stream, CancellationToken.None) + .ConfigureAwait(false) + ) != null + ) + { + 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 async ValueTask TryReadNextHeaderAsync( + Stream stream, + CancellationToken cancellationToken + ) + { + 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) + { + await _cryptInfo.ReadInitVAsync(new AsyncMarkingBinaryReader(stream)); + var _headerKey = new CryptKey5(Options.Password!, _cryptInfo); + + reader = await AsyncRarCryptoBinaryReader.Create( + stream, + _headerKey, + _cryptInfo.Salt + ); + } + else + { + var key = new CryptKey3(Options.Password); + reader = await AsyncRarCryptoBinaryReader.Create(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 + IsEncrypted = true; + } + return ah; + } + + case HeaderCodeV.RAR4_PROTECT_HEADER: + { + var ph = await ProtectHeader + .CreateAsync(header, reader, cancellationToken) + .ConfigureAwait(false); + // skip the recovery record data, we do not use it. + switch (StreamingMode) + { + case StreamingMode.Seekable: + { + reader.BaseStream.Position += ph.DataSize; + } + break; + case StreamingMode.Streaming: + { + await reader + .BaseStream.SkipAsync(ph.DataSize, cancellationToken) + .ConfigureAwait(false); + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + return ph; + } + + 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); + switch (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"); + } + } + return fh; + } + case HeaderCodeV.RAR5_END_ARCHIVE_HEADER: + case HeaderCodeV.RAR4_END_ARCHIVE_HEADER: + { + return await EndArchiveHeader + .CreateAsync(header, 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 cryptoHeader; + } + default: + { + throw new InvalidFormatException("Unknown Rar Header: " + header.HeaderCode); + } + } + } + + private async ValueTask SkipDataAsync( + FileHeader fh, + AsyncRarCrcBinaryReader reader, + CancellationToken cancellationToken + ) + { + switch (StreamingMode) + { + case StreamingMode.Seekable: + { + fh.DataStartPosition = reader.BaseStream.Position; + reader.BaseStream.Position += fh.CompressedSize; + } + break; + case StreamingMode.Streaming: + { + //skip the data because it's useless? + await reader + .BaseStream.SkipAsync(fh.CompressedSize, cancellationToken) + .ConfigureAwait(false); + } + break; + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } +} diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs index c3be7166..e9694c2a 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs @@ -2,15 +2,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar; using SharpCompress.IO; using SharpCompress.Readers; namespace SharpCompress.Common.Rar.Headers; -public class RarHeaderFactory +public partial class RarHeaderFactory { private bool _isRar5; @@ -45,37 +43,6 @@ public class RarHeaderFactory } } - public async IAsyncEnumerable ReadHeadersAsync(Stream stream) - { - var markHeader = await MarkHeader - .ReadAsync( - stream, - Options.LeaveStreamOpen, - Options.LookForHeader, - CancellationToken.None - ) - .ConfigureAwait(false); - _isRar5 = markHeader.IsRar5; - yield return markHeader; - - RarHeader? header; - while ( - ( - header = await TryReadNextHeaderAsync(stream, CancellationToken.None) - .ConfigureAwait(false) - ) != null - ) - { - 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; @@ -234,188 +201,6 @@ public class RarHeaderFactory } } - private async ValueTask TryReadNextHeaderAsync( - Stream stream, - CancellationToken cancellationToken - ) - { - 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) - { - await _cryptInfo.ReadInitVAsync(new AsyncMarkingBinaryReader(stream)); - var _headerKey = new CryptKey5(Options.Password!, _cryptInfo); - - reader = await AsyncRarCryptoBinaryReader.Create( - stream, - _headerKey, - _cryptInfo.Salt - ); - } - else - { - var key = new CryptKey3(Options.Password); - reader = await AsyncRarCryptoBinaryReader.Create(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 - IsEncrypted = true; - } - return ah; - } - - case HeaderCodeV.RAR4_PROTECT_HEADER: - { - var ph = await ProtectHeader - .CreateAsync(header, reader, cancellationToken) - .ConfigureAwait(false); - // skip the recovery record data, we do not use it. - switch (StreamingMode) - { - case StreamingMode.Seekable: - { - reader.BaseStream.Position += ph.DataSize; - } - break; - case StreamingMode.Streaming: - { - await reader - .BaseStream.SkipAsync(ph.DataSize, cancellationToken) - .ConfigureAwait(false); - } - break; - default: - { - throw new InvalidFormatException("Invalid StreamingMode"); - } - } - return ph; - } - - 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); - switch (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"); - } - } - return fh; - } - case HeaderCodeV.RAR5_END_ARCHIVE_HEADER: - case HeaderCodeV.RAR4_END_ARCHIVE_HEADER: - { - return await EndArchiveHeader - .CreateAsync(header, 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 cryptoHeader; - } - default: - { - throw new InvalidFormatException("Unknown Rar Header: " + header.HeaderCode); - } - } - } - private void SkipData(FileHeader fh, RarCrcBinaryReader reader) { switch (StreamingMode) @@ -438,33 +223,4 @@ public class RarHeaderFactory } } } - - private async ValueTask SkipDataAsync( - FileHeader fh, - AsyncRarCrcBinaryReader reader, - CancellationToken cancellationToken - ) - { - switch (StreamingMode) - { - case StreamingMode.Seekable: - { - fh.DataStartPosition = reader.BaseStream.Position; - reader.BaseStream.Position += fh.CompressedSize; - } - break; - case StreamingMode.Streaming: - { - //skip the data because it's useless? - await reader - .BaseStream.SkipAsync(fh.CompressedSize, cancellationToken) - .ConfigureAwait(false); - } - break; - default: - { - throw new InvalidFormatException("Invalid StreamingMode"); - } - } - } } diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs new file mode 100644 index 00000000..426a7a5c --- /dev/null +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs @@ -0,0 +1,158 @@ +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.LZMA.Utilites; +using SharpCompress.IO; + +namespace SharpCompress.Common.SevenZip; + +internal sealed partial class ArchiveReader +{ + public async ValueTask OpenAsync( + Stream stream, + bool lookForHeader, + CancellationToken cancellationToken = default + ) + { + Close(); + + _streamOrigin = stream.Position; + _streamEnding = stream.Length; + + var canScan = lookForHeader ? 0x80000 - 20 : 0; + while (true) + { + // TODO: Check Signature! + _header = new byte[0x20]; + await stream.ReadExactAsync(_header, 0, 0x20, cancellationToken); + + if ( + !lookForHeader + || _header + .AsSpan(0, length: 6) + .SequenceEqual([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) + ) + { + break; + } + + if (canScan == 0) + { + throw new InvalidFormatException("Unable to find 7z signature"); + } + + canScan--; + stream.Position = ++_streamOrigin; + } + + _stream = stream; + } + + public async ValueTask ReadDatabaseAsync( + IPasswordProvider pass, + CancellationToken cancellationToken = default + ) + { + var db = new ArchiveDatabase(pass); + db.Clear(); + + db._majorVersion = _header[6]; + db._minorVersion = _header[7]; + + if (db._majorVersion != 0) + { + throw new InvalidOperationException(); + } + + var crcFromArchive = DataReader.Get32(_header, 8); + var nextHeaderOffset = (long)DataReader.Get64(_header, 0xC); + var nextHeaderSize = (long)DataReader.Get64(_header, 0x14); + var nextHeaderCrc = DataReader.Get32(_header, 0x1C); + + var crc = Crc.INIT_CRC; + crc = Crc.Update(crc, nextHeaderOffset); + crc = Crc.Update(crc, nextHeaderSize); + crc = Crc.Update(crc, nextHeaderCrc); + crc = Crc.Finish(crc); + + if (crc != crcFromArchive) + { + throw new InvalidOperationException(); + } + + db._startPositionAfterHeader = _streamOrigin + 0x20; + + // empty header is ok + if (nextHeaderSize == 0) + { + db.Fill(); + return db; + } + + if (nextHeaderOffset < 0 || nextHeaderSize < 0 || nextHeaderSize > int.MaxValue) + { + throw new InvalidOperationException(); + } + + if (nextHeaderOffset > _streamEnding - db._startPositionAfterHeader) + { + throw new InvalidOperationException("nextHeaderOffset is invalid"); + } + + _stream.Seek(nextHeaderOffset, SeekOrigin.Current); + + var header = new byte[nextHeaderSize]; + await _stream.ReadExactAsync(header, 0, header.Length, cancellationToken); + + if (Crc.Finish(Crc.Update(Crc.INIT_CRC, header, 0, header.Length)) != nextHeaderCrc) + { + throw new InvalidOperationException(); + } + + using (var streamSwitch = new CStreamSwitch()) + { + streamSwitch.Set(this, header); + + var type = ReadId(); + if (type != BlockType.Header) + { + if (type != BlockType.EncodedHeader) + { + throw new InvalidOperationException(); + } + + var dataVector = ReadAndDecodePackedStreams( + db._startPositionAfterHeader, + db.PasswordProvider + ); + + // compressed header without content is odd but ok + if (dataVector.Count == 0) + { + db.Fill(); + return db; + } + + if (dataVector.Count != 1) + { + throw new InvalidOperationException(); + } + + streamSwitch.Set(this, dataVector[0]); + + if (ReadId() != BlockType.Header) + { + throw new InvalidOperationException(); + } + } + + ReadHeader(db, db.PasswordProvider); + } + db.Fill(); + return db; + } +} diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs index f5f82ab8..bb19b4b1 100644 --- a/src/SharpCompress/Common/SevenZip/ArchiveReader.cs +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.cs @@ -13,7 +13,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.SevenZip; -internal class ArchiveReader +internal partial class ArchiveReader { internal Stream _stream; internal Stack _readerStack = new(); @@ -1272,45 +1272,7 @@ internal class ArchiveReader _stream = stream; } - public async ValueTask OpenAsync( - Stream stream, - bool lookForHeader, - CancellationToken cancellationToken = default - ) - { - Close(); - - _streamOrigin = stream.Position; - _streamEnding = stream.Length; - - var canScan = lookForHeader ? 0x80000 - 20 : 0; - while (true) - { - // TODO: Check Signature! - _header = new byte[0x20]; - await stream.ReadExactAsync(_header, 0, 0x20, cancellationToken); - - if ( - !lookForHeader - || _header - .AsSpan(0, length: 6) - .SequenceEqual([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]) - ) - { - break; - } - - if (canScan == 0) - { - throw new InvalidFormatException("Unable to find 7z signature"); - } - - canScan--; - stream.Position = ++_streamOrigin; - } - - _stream = stream; - } + // OpenAsync moved to ArchiveReader.Async.cs public void Close() { @@ -1425,109 +1387,7 @@ internal class ArchiveReader return db; } - public async ValueTask ReadDatabaseAsync( - IPasswordProvider pass, - CancellationToken cancellationToken = default - ) - { - var db = new ArchiveDatabase(pass); - db.Clear(); - - db._majorVersion = _header[6]; - db._minorVersion = _header[7]; - - if (db._majorVersion != 0) - { - throw new InvalidOperationException(); - } - - var crcFromArchive = DataReader.Get32(_header, 8); - var nextHeaderOffset = (long)DataReader.Get64(_header, 0xC); - var nextHeaderSize = (long)DataReader.Get64(_header, 0x14); - var nextHeaderCrc = DataReader.Get32(_header, 0x1C); - - var crc = Crc.INIT_CRC; - crc = Crc.Update(crc, nextHeaderOffset); - crc = Crc.Update(crc, nextHeaderSize); - crc = Crc.Update(crc, nextHeaderCrc); - crc = Crc.Finish(crc); - - if (crc != crcFromArchive) - { - throw new InvalidOperationException(); - } - - db._startPositionAfterHeader = _streamOrigin + 0x20; - - // empty header is ok - if (nextHeaderSize == 0) - { - db.Fill(); - return db; - } - - if (nextHeaderOffset < 0 || nextHeaderSize < 0 || nextHeaderSize > int.MaxValue) - { - throw new InvalidOperationException(); - } - - if (nextHeaderOffset > _streamEnding - db._startPositionAfterHeader) - { - throw new InvalidOperationException("nextHeaderOffset is invalid"); - } - - _stream.Seek(nextHeaderOffset, SeekOrigin.Current); - - var header = new byte[nextHeaderSize]; - await _stream.ReadExactAsync(header, 0, header.Length, cancellationToken); - - if (Crc.Finish(Crc.Update(Crc.INIT_CRC, header, 0, header.Length)) != nextHeaderCrc) - { - throw new InvalidOperationException(); - } - - using (var streamSwitch = new CStreamSwitch()) - { - streamSwitch.Set(this, header); - - var type = ReadId(); - if (type != BlockType.Header) - { - if (type != BlockType.EncodedHeader) - { - throw new InvalidOperationException(); - } - - var dataVector = ReadAndDecodePackedStreams( - db._startPositionAfterHeader, - db.PasswordProvider - ); - - // compressed header without content is odd but ok - if (dataVector.Count == 0) - { - db.Fill(); - return db; - } - - if (dataVector.Count != 1) - { - throw new InvalidOperationException(); - } - - streamSwitch.Set(this, dataVector[0]); - - if (ReadId() != BlockType.Header) - { - throw new InvalidOperationException(); - } - } - - ReadHeader(db, db.PasswordProvider); - } - db.Fill(); - return db; - } + // ReadDatabaseAsync moved to ArchiveReader.Async.cs internal class CExtractFolderInfo { diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs new file mode 100644 index 00000000..2282a75b --- /dev/null +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs @@ -0,0 +1,154 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Tar.Headers; + +internal sealed partial class TarHeader +{ + internal async ValueTask ReadAsync(AsyncBinaryReader reader) + { + string? longName = null; + string? longLinkName = null; + var hasLongValue = true; + byte[] buffer; + EntryType entryType; + + do + { + buffer = await ReadBlockAsync(reader); + + if (buffer.Length == 0) + { + return false; + } + + entryType = ReadEntryType(buffer); + + // LongName and LongLink headers can follow each other and need + // to apply to the header that follows them. + if (entryType == EntryType.LongName) + { + longName = await ReadLongNameAsync(reader, buffer); + continue; + } + else if (entryType == EntryType.LongLink) + { + longLinkName = await ReadLongNameAsync(reader, buffer); + continue; + } + + hasLongValue = false; + } while (hasLongValue); + + // Check header checksum + if (!checkChecksum(buffer)) + { + return false; + } + + Name = longName ?? ArchiveEncoding.Decode(buffer, 0, 100).TrimNulls(); + EntryType = entryType; + Size = ReadSize(buffer); + + // for symlinks, additionally read the linkname + if (entryType == EntryType.SymLink || entryType == EntryType.HardLink) + { + LinkName = longLinkName ?? ArchiveEncoding.Decode(buffer, 157, 100).TrimNulls(); + } + + Mode = ReadAsciiInt64Base8(buffer, 100, 7); + + if (entryType == EntryType.Directory) + { + Mode |= 0b1_000_000_000; + } + + UserId = ReadAsciiInt64Base8oldGnu(buffer, 108, 7); + GroupId = ReadAsciiInt64Base8oldGnu(buffer, 116, 7); + + var unixTimeStamp = ReadAsciiInt64Base8(buffer, 136, 11); + + LastModifiedTime = EPOCH.AddSeconds(unixTimeStamp).ToLocalTime(); + Magic = ArchiveEncoding.Decode(buffer, 257, 6).TrimNulls(); + + if (!string.IsNullOrEmpty(Magic) && "ustar".Equals(Magic)) + { + var namePrefix = ArchiveEncoding.Decode(buffer, 345, 157).TrimNulls(); + + if (!string.IsNullOrEmpty(namePrefix)) + { + Name = namePrefix + "/" + Name; + } + } + + if (entryType != EntryType.LongName && Name.Length == 0) + { + return false; + } + + return true; + } + + private static async ValueTask ReadBlockAsync(AsyncBinaryReader reader) + { + var buffer = ArrayPool.Shared.Rent(BLOCK_SIZE); + try + { + await reader.ReadBytesAsync(buffer, 0, BLOCK_SIZE); + + if (buffer.Length != 0 && buffer.Length < BLOCK_SIZE) + { + throw new InvalidFormatException("Buffer is invalid size"); + } + + return buffer; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private async ValueTask ReadLongNameAsync(AsyncBinaryReader reader, byte[] buffer) + { + var size = ReadSize(buffer); + + // Validate size to prevent memory exhaustion from malformed headers + if (size < 0 || size > MAX_LONG_NAME_SIZE) + { + throw new InvalidFormatException( + $"Long name size {size} is invalid or exceeds maximum allowed size of {MAX_LONG_NAME_SIZE} bytes" + ); + } + + var nameLength = (int)size; + var nameBytes = ArrayPool.Shared.Rent(nameLength); + try + { + await reader.ReadBytesAsync(nameBytes, 0, nameLength); + var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE); + + // Read the rest of the block and discard the data + if (remainingBytesToRead < BLOCK_SIZE) + { + var remainingBytes = ArrayPool.Shared.Rent(remainingBytesToRead); + try + { + await reader.ReadBytesAsync(remainingBytes, 0, remainingBytesToRead); + } + finally + { + ArrayPool.Shared.Return(remainingBytes); + } + } + + return ArchiveEncoding.Decode(nameBytes, 0, nameLength).TrimNulls(); + } + finally + { + ArrayPool.Shared.Return(nameBytes); + } + } +} diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs index 4c006abc..c91cf0d1 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Tar.Headers; -internal sealed class TarHeader +internal sealed partial class TarHeader { internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -497,90 +497,6 @@ internal sealed class TarHeader return true; } - internal async ValueTask ReadAsync(AsyncBinaryReader reader) - { - string? longName = null; - string? longLinkName = null; - var hasLongValue = true; - byte[] buffer; - EntryType entryType; - - do - { - buffer = await ReadBlockAsync(reader); - - if (buffer.Length == 0) - { - return false; - } - - entryType = ReadEntryType(buffer); - - // LongName and LongLink headers can follow each other and need - // to apply to the header that follows them. - if (entryType == EntryType.LongName) - { - longName = await ReadLongNameAsync(reader, buffer); - continue; - } - else if (entryType == EntryType.LongLink) - { - longLinkName = await ReadLongNameAsync(reader, buffer); - continue; - } - - hasLongValue = false; - } while (hasLongValue); - - // Check header checksum - if (!checkChecksum(buffer)) - { - return false; - } - - Name = longName ?? ArchiveEncoding.Decode(buffer, 0, 100).TrimNulls(); - EntryType = entryType; - Size = ReadSize(buffer); - - // for symlinks, additionally read the linkname - if (entryType == EntryType.SymLink || entryType == EntryType.HardLink) - { - LinkName = longLinkName ?? ArchiveEncoding.Decode(buffer, 157, 100).TrimNulls(); - } - - Mode = ReadAsciiInt64Base8(buffer, 100, 7); - - if (entryType == EntryType.Directory) - { - Mode |= 0b1_000_000_000; - } - - UserId = ReadAsciiInt64Base8oldGnu(buffer, 108, 7); - GroupId = ReadAsciiInt64Base8oldGnu(buffer, 116, 7); - - var unixTimeStamp = ReadAsciiInt64Base8(buffer, 136, 11); - - LastModifiedTime = EPOCH.AddSeconds(unixTimeStamp).ToLocalTime(); - Magic = ArchiveEncoding.Decode(buffer, 257, 6).TrimNulls(); - - if (!string.IsNullOrEmpty(Magic) && "ustar".Equals(Magic)) - { - var namePrefix = ArchiveEncoding.Decode(buffer, 345, 157).TrimNulls(); - - if (!string.IsNullOrEmpty(namePrefix)) - { - Name = namePrefix + "/" + Name; - } - } - - if (entryType != EntryType.LongName && Name.Length == 0) - { - return false; - } - - return true; - } - internal static int RecalculateChecksum(byte[] buf) { // Set default value for checksum. That is 8 spaces. @@ -616,65 +532,4 @@ internal sealed class TarHeader public long? DataStartPosition { get; set; } public string? Magic { get; set; } - - private static async ValueTask ReadBlockAsync(AsyncBinaryReader reader) - { - var buffer = ArrayPool.Shared.Rent(BLOCK_SIZE); - try - { - await reader.ReadBytesAsync(buffer, 0, BLOCK_SIZE); - - if (buffer.Length != 0 && buffer.Length < BLOCK_SIZE) - { - throw new InvalidFormatException("Buffer is invalid size"); - } - - return buffer; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - private async ValueTask ReadLongNameAsync(AsyncBinaryReader reader, byte[] buffer) - { - var size = ReadSize(buffer); - - // Validate size to prevent memory exhaustion from malformed headers - if (size < 0 || size > MAX_LONG_NAME_SIZE) - { - throw new InvalidFormatException( - $"Long name size {size} is invalid or exceeds maximum allowed size of {MAX_LONG_NAME_SIZE} bytes" - ); - } - - var nameLength = (int)size; - var nameBytes = ArrayPool.Shared.Rent(nameLength); - try - { - await reader.ReadBytesAsync(nameBytes, 0, nameLength); - var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE); - - // Read the rest of the block and discard the data - if (remainingBytesToRead < BLOCK_SIZE) - { - var remainingBytes = ArrayPool.Shared.Rent(remainingBytesToRead); - try - { - await reader.ReadBytesAsync(remainingBytes, 0, remainingBytesToRead); - } - finally - { - ArrayPool.Shared.Return(remainingBytes); - } - } - - return ArchiveEncoding.Decode(nameBytes, 0, nameLength).TrimNulls(); - } - finally - { - ArrayPool.Shared.Return(nameBytes); - } - } } diff --git a/src/SharpCompress/Common/Volume.Async.cs b/src/SharpCompress/Common/Volume.Async.cs new file mode 100644 index 00000000..c42b1e77 --- /dev/null +++ b/src/SharpCompress/Common/Volume.Async.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace SharpCompress.Common; + +public abstract partial class Volume +{ + public virtual async ValueTask DisposeAsync() + { +#if NETFRAMEWORK || NETSTANDARD2_0 + await Task.Run(() => _actualStream.Dispose()).ConfigureAwait(false); +#else + await _actualStream.DisposeAsync().ConfigureAwait(false); +#endif + GC.SuppressFinalize(this); + } +} diff --git a/src/SharpCompress/Common/Volume.cs b/src/SharpCompress/Common/Volume.cs index 24f370f4..6c54eeca 100644 --- a/src/SharpCompress/Common/Volume.cs +++ b/src/SharpCompress/Common/Volume.cs @@ -6,7 +6,7 @@ using SharpCompress.Readers; namespace SharpCompress.Common; -public abstract class Volume : IVolume, IAsyncDisposable +public abstract partial class Volume : IVolume, IAsyncDisposable { private readonly Stream _baseStream; private readonly Stream _actualStream; @@ -59,14 +59,4 @@ public abstract class Volume : IVolume, IAsyncDisposable Dispose(true); GC.SuppressFinalize(this); } - - public virtual async ValueTask DisposeAsync() - { -#if NETFRAMEWORK || NETSTANDARD2_0 - await Task.Run(() => _actualStream.Dispose()).ConfigureAwait(false); -#else - await _actualStream.DisposeAsync().ConfigureAwait(false); -#endif - GC.SuppressFinalize(this); - } } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs new file mode 100644 index 00000000..811beeff --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Zip.Headers; + +internal partial class DirectoryEndHeader +{ + internal override async ValueTask Read(AsyncBinaryReader reader) + { + VolumeNumber = await reader.ReadUInt16Async(); + FirstVolumeWithDirectory = await reader.ReadUInt16Async(); + TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async(); + TotalNumberOfEntries = await reader.ReadUInt16Async(); + DirectorySize = await reader.ReadUInt32Async(); + DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async(); + CommentLength = await reader.ReadUInt16Async(); + Comment = new byte[CommentLength]; + await reader.ReadBytesAsync(Comment, 0, CommentLength); + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs index 068358cf..71da83af 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs @@ -1,9 +1,8 @@ using System.IO; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class DirectoryEndHeader : ZipHeader +internal partial class DirectoryEndHeader : ZipHeader { public DirectoryEndHeader() : base(ZipHeaderType.DirectoryEnd) { } @@ -20,19 +19,6 @@ internal class DirectoryEndHeader : ZipHeader Comment = reader.ReadBytes(CommentLength); } - internal override async ValueTask Read(AsyncBinaryReader reader) - { - VolumeNumber = await reader.ReadUInt16Async(); - FirstVolumeWithDirectory = await reader.ReadUInt16Async(); - TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async(); - TotalNumberOfEntries = await reader.ReadUInt16Async(); - DirectorySize = await reader.ReadUInt32Async(); - DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async(); - CommentLength = await reader.ReadUInt16Async(); - Comment = new byte[CommentLength]; - await reader.ReadBytesAsync(Comment, 0, CommentLength); - } - public ushort VolumeNumber { get; private set; } public ushort FirstVolumeWithDirectory { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs new file mode 100644 index 00000000..e723f1e5 --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Common.Zip.Headers; + +internal partial class DirectoryEntryHeader +{ + internal override async ValueTask Read(AsyncBinaryReader reader) + { + Version = await reader.ReadUInt16Async(); + VersionNeededToExtract = 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 commentLength = await reader.ReadUInt16Async(); + DiskNumberStart = await reader.ReadUInt16Async(); + InternalFileAttributes = await reader.ReadUInt16Async(); + ExternalFileAttributes = await reader.ReadUInt32Async(); + RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async(); + var name = new byte[nameLength]; + var extra = new byte[extraLength]; + var comment = new byte[commentLength]; + await reader.ReadBytesAsync(name, 0, nameLength); + await reader.ReadBytesAsync(extra, 0, extraLength); + await reader.ReadBytesAsync(comment, 0, commentLength); + + ProcessReadData(name, extra, comment); + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs index b19aeea5..e4e0f331 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs @@ -1,10 +1,9 @@ using System.IO; using System.Linq; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class DirectoryEntryHeader : ZipFileEntry +internal partial class DirectoryEntryHeader : ZipFileEntry { public DirectoryEntryHeader(IArchiveEncoding archiveEncoding) : base(ZipHeaderType.DirectoryEntry, archiveEncoding) { } @@ -35,34 +34,6 @@ internal class DirectoryEntryHeader : ZipFileEntry ProcessReadData(name, extra, comment); } - internal override async ValueTask Read(AsyncBinaryReader reader) - { - Version = await reader.ReadUInt16Async(); - VersionNeededToExtract = 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 commentLength = await reader.ReadUInt16Async(); - DiskNumberStart = await reader.ReadUInt16Async(); - InternalFileAttributes = await reader.ReadUInt16Async(); - ExternalFileAttributes = await reader.ReadUInt32Async(); - RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async(); - var name = new byte[nameLength]; - var extra = new byte[extraLength]; - var comment = new byte[commentLength]; - await reader.ReadBytesAsync(name, 0, nameLength); - await reader.ReadBytesAsync(extra, 0, extraLength); - await reader.ReadBytesAsync(comment, 0, commentLength); - - ProcessReadData(name, extra, comment); - } - private void ProcessReadData(byte[] name, byte[] extra, byte[] comment) { // diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs new file mode 100644 index 00000000..3a7426a1 --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Zip.Headers; + +internal partial class LocalEntryHeader +{ + internal override async ValueTask Read(AsyncBinaryReader reader) + { + 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 = new byte[nameLength]; + var extra = new byte[extraLength]; + await reader.ReadBytesAsync(name, 0, nameLength); + await reader.ReadBytesAsync(extra, 0, extraLength); + + ProcessReadData(name, extra); + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index d676dde0..d9490137 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -1,12 +1,13 @@ using System.IO; using System.Linq; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader(IArchiveEncoding archiveEncoding) - : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) +internal partial class LocalEntryHeader : ZipFileEntry { + public LocalEntryHeader(IArchiveEncoding archiveEncoding) + : base(ZipHeaderType.LocalEntry, archiveEncoding) { } + internal override void Read(BinaryReader reader) { Version = reader.ReadUInt16(); @@ -25,26 +26,6 @@ internal class LocalEntryHeader(IArchiveEncoding archiveEncoding) ProcessReadData(name, extra); } - internal override async ValueTask Read(AsyncBinaryReader reader) - { - 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 = new byte[nameLength]; - var extra = new byte[extraLength]; - await reader.ReadBytesAsync(name, 0, nameLength); - await reader.ReadBytesAsync(extra, 0, extraLength); - - ProcessReadData(name, extra); - } - private void ProcessReadData(byte[] name, byte[] extra) { // diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs new file mode 100644 index 00000000..386db415 --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs @@ -0,0 +1,25 @@ +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Zip.Headers; + +internal partial class Zip64DirectoryEndHeader +{ + internal override async ValueTask Read(AsyncBinaryReader reader) + { + SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async(); + VersionMadeBy = await reader.ReadUInt16Async(); + VersionNeededToExtract = await reader.ReadUInt16Async(); + VolumeNumber = await reader.ReadUInt32Async(); + FirstVolumeWithDirectory = await reader.ReadUInt32Async(); + TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async(); + TotalNumberOfEntries = (long)await reader.ReadUInt64Async(); + DirectorySize = (long)await reader.ReadUInt64Async(); + DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async(); + var size = (int)( + SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS + ); + DataSector = new byte[size]; + await reader.ReadBytesAsync(DataSector, 0, size); + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs index cdd412b2..3933b2e0 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs @@ -1,9 +1,8 @@ using System.IO; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class Zip64DirectoryEndHeader : ZipHeader +internal partial class Zip64DirectoryEndHeader : ZipHeader { public Zip64DirectoryEndHeader() : base(ZipHeaderType.Zip64DirectoryEnd) { } @@ -27,24 +26,6 @@ internal class Zip64DirectoryEndHeader : ZipHeader ); } - internal override async ValueTask Read(AsyncBinaryReader reader) - { - SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async(); - VersionMadeBy = await reader.ReadUInt16Async(); - VersionNeededToExtract = await reader.ReadUInt16Async(); - VolumeNumber = await reader.ReadUInt32Async(); - FirstVolumeWithDirectory = await reader.ReadUInt32Async(); - TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async(); - TotalNumberOfEntries = (long)await reader.ReadUInt64Async(); - DirectorySize = (long)await reader.ReadUInt64Async(); - DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async(); - var size = (int)( - SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS - ); - DataSector = new byte[size]; - await reader.ReadBytesAsync(DataSector, 0, size); - } - private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44; public long SizeOfDirectoryEndRecord { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs new file mode 100644 index 00000000..804f05da --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs @@ -0,0 +1,14 @@ +using System.IO; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Zip.Headers; + +internal partial class Zip64DirectoryEndLocatorHeader +{ + internal override async ValueTask Read(AsyncBinaryReader reader) + { + FirstVolumeWithDirectory = await reader.ReadUInt32Async(); + RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); + TotalNumberOfVolumes = await reader.ReadUInt32Async(); + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs index 8326be99..3477c804 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs @@ -1,9 +1,9 @@ using System.IO; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) +internal partial class Zip64DirectoryEndLocatorHeader() + : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) { internal override void Read(BinaryReader reader) { @@ -12,13 +12,6 @@ internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64D TotalNumberOfVolumes = reader.ReadUInt32(); } - internal override async ValueTask Read(AsyncBinaryReader reader) - { - FirstVolumeWithDirectory = await reader.ReadUInt32Async(); - RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); - TotalNumberOfVolumes = await reader.ReadUInt32Async(); - } - public uint FirstVolumeWithDirectory { get; private set; } public long RelativeOffsetOfTheEndOfDirectoryRecord { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.Async.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.Async.cs new file mode 100644 index 00000000..5cc805b7 --- /dev/null +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.Async.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Zip.Headers; + +internal abstract partial class ZipFileEntry +{ + internal async ValueTask ComposeEncryptionDataAsync( + Stream archiveStream, + CancellationToken cancellationToken = default + ) + { + if (archiveStream is null) + { + throw new ArgumentNullException(nameof(archiveStream)); + } + + var buffer = new byte[12]; + await archiveStream.ReadFullyAsync(buffer, 0, 12, cancellationToken).ConfigureAwait(false); + + var encryptionData = PkwareTraditionalEncryptionData.ForRead(Password!, this, buffer); + + return encryptionData; + } +} diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index edcb2976..c5bf85e6 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -2,12 +2,10 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding) +internal abstract partial class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding) : ZipHeader(type) { internal bool IsDirectory @@ -59,24 +57,6 @@ internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiv return encryptionData; } - internal async ValueTask ComposeEncryptionDataAsync( - Stream archiveStream, - CancellationToken cancellationToken = default - ) - { - if (archiveStream is null) - { - throw new ArgumentNullException(nameof(archiveStream)); - } - - var buffer = new byte[12]; - await archiveStream.ReadFullyAsync(buffer, 0, 12, cancellationToken).ConfigureAwait(false); - - var encryptionData = PkwareTraditionalEncryptionData.ForRead(Password!, this, buffer); - - return encryptionData; - } - internal WinzipAesEncryptionData? WinzipAesEncryptionData { get; set; } /// @@ -137,7 +117,7 @@ internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiv if (i + 4 + length > extra.Length) { // incomplete or corrupt field - break; // allow processing optional other blocks + break; // allow processing other blocks } var data = new byte[length]; diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs new file mode 100644 index 00000000..92a8f7b3 --- /dev/null +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Zip.Headers; + +namespace SharpCompress.Common.Zip; + +internal partial class SeekableZipFilePart +{ + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!_isLocalHeaderLoaded) + { + await LoadLocalHeaderAsync(cancellationToken); + _isLocalHeaderLoaded = true; + } + return await base.GetCompressedStreamAsync(cancellationToken); + } + + private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) => + Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header); +} diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs index 7dbf93ba..9e2e06b0 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs @@ -1,11 +1,9 @@ using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; namespace SharpCompress.Common.Zip; -internal class SeekableZipFilePart : ZipFilePart +internal partial class SeekableZipFilePart : ZipFilePart { private bool _isLocalHeaderLoaded; private readonly SeekableZipHeaderFactory _headerFactory; @@ -27,24 +25,9 @@ internal class SeekableZipFilePart : ZipFilePart return base.GetCompressedStream(); } - internal override async ValueTask GetCompressedStreamAsync( - CancellationToken cancellationToken = default - ) - { - if (!_isLocalHeaderLoaded) - { - await LoadLocalHeaderAsync(cancellationToken); - _isLocalHeaderLoaded = true; - } - return await base.GetCompressedStreamAsync(cancellationToken); - } - private void LoadLocalHeader() => Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header); - private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) => - Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header); - protected override Stream CreateBaseStream() { BaseStream.Position = Header.DataStartPosition.NotNull(); diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs new file mode 100644 index 00000000..7b95485b --- /dev/null +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs @@ -0,0 +1,83 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Common.Zip; + +internal sealed partial class SeekableZipHeaderFactory +{ + private static async ValueTask SeekBackToHeaderAsync(Stream stream, AsyncBinaryReader reader) + { + // Minimum EOCD length + if (stream.Length < MINIMUM_EOCD_LENGTH) + { + throw new ArchiveException( + "Could not find Zip file Directory at the end of the file. File may be corrupted." + ); + } + + var len = + stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD + ? (int)stream.Length + : MAX_SEARCH_LENGTH_FOR_EOCD; + + stream.Seek(-len, SeekOrigin.End); + var seek = ArrayPool.Shared.Rent(len); + + try + { + await reader.ReadBytesAsync(seek, 0, len, default); + var memory = new Memory(seek, 0, len); + var span = memory.Span; + span.Reverse(); + + // don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files + var max_search_area = len; // - MINIMUM_EOCD_LENGTH; + + for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end) + { + if (IsMatch(span, pos_from_end, needle)) + { + stream.Seek(-pos_from_end, SeekOrigin.End); + return; + } + } + + throw new ArchiveException("Failed to locate the Zip Header"); + } + finally + { + ArrayPool.Shared.Return(seek); + } + } + + internal async ValueTask GetLocalHeaderAsync( + Stream stream, + DirectoryEntryHeader directoryEntryHeader + ) + { + stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin); + var reader = new AsyncBinaryReader(stream); + var signature = await reader.ReadUInt32Async(); + if (await ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader) + { + throw new InvalidOperationException(); + } + + // populate fields only known from the DirectoryEntryHeader + localEntryHeader.HasData = directoryEntryHeader.HasData; + localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes; + localEntryHeader.Comment = directoryEntryHeader.Comment; + + if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor)) + { + localEntryHeader.Crc = directoryEntryHeader.Crc; + localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize; + localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize; + } + return localEntryHeader; + } +} diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index 971ffa3c..77f5883e 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory +internal sealed partial class SeekableZipHeaderFactory : ZipHeaderFactory { private const int MINIMUM_EOCD_LENGTH = 22; private const int ZIP64_EOCD_LENGTH = 20; @@ -169,51 +169,6 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory return true; } - private static async ValueTask SeekBackToHeaderAsync(Stream stream, AsyncBinaryReader reader) - { - // Minimum EOCD length - if (stream.Length < MINIMUM_EOCD_LENGTH) - { - throw new ArchiveException( - "Could not find Zip file Directory at the end of the file. File may be corrupted." - ); - } - - var len = - stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD - ? (int)stream.Length - : MAX_SEARCH_LENGTH_FOR_EOCD; - - stream.Seek(-len, SeekOrigin.End); - var seek = ArrayPool.Shared.Rent(len); - - try - { - await reader.ReadBytesAsync(seek, 0, len, default); - var memory = new Memory(seek, 0, len); - var span = memory.Span; - span.Reverse(); - - // don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files - var max_search_area = len; // - MINIMUM_EOCD_LENGTH; - - for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end) - { - if (IsMatch(span, pos_from_end, needle)) - { - stream.Seek(-pos_from_end, SeekOrigin.End); - return; - } - } - - throw new ArchiveException("Failed to locate the Zip Header"); - } - finally - { - ArrayPool.Shared.Return(seek); - } - } - private static void SeekBackToHeader(Stream stream, BinaryReader reader) { // Minimum EOCD length @@ -276,31 +231,4 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory } return localEntryHeader; } - - internal async ValueTask GetLocalHeaderAsync( - Stream stream, - DirectoryEntryHeader directoryEntryHeader - ) - { - stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin); - var reader = new AsyncBinaryReader(stream); - var signature = await reader.ReadUInt32Async(); - if (await ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader) - { - throw new InvalidOperationException(); - } - - // populate fields only known from the DirectoryEntryHeader - localEntryHeader.HasData = directoryEntryHeader.HasData; - localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes; - localEntryHeader.Comment = directoryEntryHeader.Comment; - - if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor)) - { - localEntryHeader.Crc = directoryEntryHeader.Crc; - localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize; - localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize; - } - return localEntryHeader; - } } diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.Async.cs new file mode 100644 index 00000000..55910668 --- /dev/null +++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.Async.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Common.Zip; + +internal sealed partial class StreamingZipFilePart +{ + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!Header.HasData) + { + return Stream.Null; + } + _decompressionStream = await CreateDecompressionStreamAsync( + await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) + .ConfigureAwait(false), + Header.CompressionMethod, + cancellationToken + ) + .ConfigureAwait(false); + if (LeaveStreamOpen) + { + return SharpCompressStream.Create(_decompressionStream, leaveOpen: true); + } + return _decompressionStream; + } +} diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs index 312ea126..c95fad9f 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs @@ -1,13 +1,11 @@ using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors.Deflate; using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal sealed class StreamingZipFilePart : ZipFilePart +internal sealed partial class StreamingZipFilePart : ZipFilePart { private Stream? _decompressionStream; @@ -33,28 +31,6 @@ internal sealed class StreamingZipFilePart : ZipFilePart return _decompressionStream; } - internal override async ValueTask GetCompressedStreamAsync( - CancellationToken cancellationToken = default - ) - { - if (!Header.HasData) - { - return Stream.Null; - } - _decompressionStream = await CreateDecompressionStreamAsync( - await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) - .ConfigureAwait(false), - Header.CompressionMethod, - cancellationToken - ) - .ConfigureAwait(false); - if (LeaveStreamOpen) - { - return SharpCompressStream.Create(_decompressionStream, leaveOpen: true); - } - return _decompressionStream; - } - internal BinaryReader FixStreamedFileLocation(ref SharpCompressStream rewindableStream) { if (Header.IsDirectory) diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs new file mode 100644 index 00000000..df0378d9 --- /dev/null +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Common.Zip; + +internal sealed partial class StreamingZipHeaderFactory +{ + /// + /// Reads ZIP headers asynchronously for streams that do not support synchronous reads. + /// + internal IAsyncEnumerable ReadStreamHeaderAsync(Stream stream) => + new StreamHeaderAsyncEnumerable(this, stream); + + /// + /// Invokes the shared async header parsing logic on the base factory. + /// + private ValueTask ReadHeaderAsyncInternal( + uint headerBytes, + AsyncBinaryReader reader + ) => ReadHeader(headerBytes, reader); + + /// + /// Exposes the last parsed local entry header to the async enumerator so it can handle streaming data descriptors. + /// + private LocalEntryHeader? LastEntryHeader + { + get => _lastEntryHeader; + set => _lastEntryHeader = value; + } + + /// + /// Produces an async enumerator for streaming ZIP headers. + /// + private sealed class StreamHeaderAsyncEnumerable : IAsyncEnumerable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly Stream _stream; + + public StreamHeaderAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) + { + _headerFactory = headerFactory; + _stream = stream; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) => new StreamHeaderAsyncEnumerator(_headerFactory, _stream, cancellationToken); + } + + /// + /// Async implementation of reading stream headers using to avoid sync reads. + /// + private sealed class StreamHeaderAsyncEnumerator : IAsyncEnumerator, IDisposable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly SharpCompressStream _rewindableStream; + private readonly AsyncBinaryReader _reader; + private readonly CancellationToken _cancellationToken; + private bool _completed; + + public StreamHeaderAsyncEnumerator( + StreamingZipHeaderFactory headerFactory, + Stream stream, + CancellationToken cancellationToken + ) + { + _headerFactory = headerFactory; + _rewindableStream = EnsureSharpCompressStream(stream); + _reader = new AsyncBinaryReader(_rewindableStream, leaveOpen: true); + _cancellationToken = cancellationToken; + } + + private ZipHeader? _current; + + public ZipHeader Current => + _current ?? throw new InvalidOperationException("No current header is available."); + + /// + /// Advances to the next ZIP header in the stream, honoring streaming data descriptors where applicable. + /// + public async ValueTask MoveNextAsync() + { + if (_completed) + { + return false; + } + + while (true) + { + _cancellationToken.ThrowIfCancellationRequested(); + + uint headerBytes; + var lastEntryHeader = _headerFactory.LastEntryHeader; + if ( + lastEntryHeader != null + && FlagUtility.HasFlag(lastEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor) + ) + { + if (lastEntryHeader.Part is null) + { + continue; + } + + var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; + + var crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + if (crc == POST_DATA_DESCRIPTOR) + { + crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + lastEntryHeader.Crc = crc; + + //attempt 32bit read + ulong compressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + ulong uncompressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + //check for zip64 sentinel or unexpected header + bool isSentinel = + compressedSize == 0xFFFFFFFF || uncompressedSize == 0xFFFFFFFF; + bool isHeader = headerBytes == 0x04034b50 || headerBytes == 0x02014b50; + + if (!isHeader && !isSentinel) + { + //reshuffle into 64-bit values + compressedSize = (uncompressedSize << 32) | compressedSize; + uncompressedSize = + ((ulong)headerBytes << 32) + | await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + else if (isSentinel) + { + //standards-compliant zip64 descriptor + compressedSize = await _reader + .ReadUInt64Async(_cancellationToken) + .ConfigureAwait(false); + uncompressedSize = await _reader + .ReadUInt64Async(_cancellationToken) + .ConfigureAwait(false); + } + + lastEntryHeader.CompressedSize = (long)compressedSize; + lastEntryHeader.UncompressedSize = (long)uncompressedSize; + + if (pos.HasValue) + { + lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; + } + } + else if (lastEntryHeader != null && lastEntryHeader.IsZip64) + { + if (lastEntryHeader.Part is null) + { + continue; + } + + var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; + + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate + _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime + + var crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + if (crc == POST_DATA_DESCRIPTOR) + { + crc = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + lastEntryHeader.Crc = crc; + + // The DataDescriptor can be either 64bit or 32bit + var compressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + var uncompressedSize = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + + // Check if we have header or 64bit DataDescriptor + var testHeader = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50); + + var test64Bit = ((long)uncompressedSize << 32) | compressedSize; + if (test64Bit == lastEntryHeader.CompressedSize && testHeader) + { + lastEntryHeader.UncompressedSize = + ( + (long) + await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false) << 32 + ) | headerBytes; + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + else + { + lastEntryHeader.UncompressedSize = uncompressedSize; + } + + if (pos.HasValue) + { + lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; + + // 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04) + _rewindableStream.Position = pos.Value + 4; + } + } + else + { + headerBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + } + + _headerFactory.LastEntryHeader = null; + var header = await _headerFactory + .ReadHeaderAsyncInternal(headerBytes, _reader) + .ConfigureAwait(false); + if (header is null) + { + _completed = true; + return false; + } + + //entry could be zero bytes so we need to know that. + if (header.ZipHeaderType == ZipHeaderType.LocalEntry) + { + var localHeader = (LocalEntryHeader)header; + var directoryHeader = _headerFactory._entries?.FirstOrDefault(entry => + entry.Key == localHeader.Name + && localHeader.CompressedSize == 0 + && localHeader.UncompressedSize == 0 + && localHeader.Crc == 0 + && localHeader.IsDirectory == false + ); + + if (directoryHeader != null) + { + localHeader.UncompressedSize = directoryHeader.Size; + localHeader.CompressedSize = directoryHeader.CompressedSize; + localHeader.Crc = (uint)directoryHeader.Crc; + } + + // If we have CompressedSize, there is data to be read + if (localHeader.CompressedSize > 0) + { + header.HasData = true; + } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor ) + else if (localHeader.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) + { + var nextHeaderBytes = await _reader + .ReadUInt32Async(_cancellationToken) + .ConfigureAwait(false); + ((IStreamStack)_rewindableStream).Rewind(sizeof(uint)); + + // Check if next data is PostDataDescriptor, streamed file with 0 length + header.HasData = !IsHeader(nextHeaderBytes); + } + else // We are not streaming and compressed size is 0, we have no data + { + header.HasData = false; + } + } + + _current = header; + return true; + } + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + /// + /// Disposes the underlying reader (without closing the archive stream). + /// + public void Dispose() + { + _reader.Dispose(); + } + + /// + /// Ensures the stream is a so header parsing can use rewind/buffer helpers. + /// + private static SharpCompressStream EnsureSharpCompressStream(Stream stream) + { + if (stream is SharpCompressStream sharpCompressStream) + { + return sharpCompressStream; + } + + // Ensure the stream is already a SharpCompressStream so the buffer/size is set. + // The original code wrapped this with RewindableStream; use SharpCompressStream so we can get the buffer size. + if (stream is SourceStream src) + { + return new SharpCompressStream( + stream, + src.ReaderOptions.LeaveStreamOpen, + bufferSize: src.ReaderOptions.BufferSize + ); + } + + throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream)); + } + } +} diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index 479a5c2a..c6ddf70e 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -10,7 +10,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal class StreamingZipHeaderFactory : ZipHeaderFactory +internal sealed partial class StreamingZipHeaderFactory : ZipHeaderFactory { private IEnumerable? _entries; @@ -204,330 +204,24 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory } } - /// - /// Reads ZIP headers asynchronously for streams that do not support synchronous reads. - /// - internal IAsyncEnumerable ReadStreamHeaderAsync(Stream stream) => - new StreamHeaderAsyncEnumerable(this, stream); - - /// - /// Invokes the shared async header parsing logic on the base factory. - /// - private ValueTask ReadHeaderAsyncInternal( - uint headerBytes, - AsyncBinaryReader reader - ) => ReadHeader(headerBytes, reader); - - /// - /// Exposes the last parsed local entry header to the async enumerator so it can handle streaming data descriptors. - /// - private LocalEntryHeader? LastEntryHeader + private static SharpCompressStream EnsureSharpCompressStream(Stream stream) { - get => _lastEntryHeader; - set => _lastEntryHeader = value; - } - - /// - /// Produces an async enumerator for streaming ZIP headers. - /// - private sealed class StreamHeaderAsyncEnumerable : IAsyncEnumerable - { - private readonly StreamingZipHeaderFactory _headerFactory; - private readonly Stream _stream; - - public StreamHeaderAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) + if (stream is SharpCompressStream sharpCompressStream) { - _headerFactory = headerFactory; - _stream = stream; + return sharpCompressStream; } - public IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) => new StreamHeaderAsyncEnumerator(_headerFactory, _stream, cancellationToken); - } - - /// - /// Async implementation of using to avoid sync reads. - /// - private sealed class StreamHeaderAsyncEnumerator : IAsyncEnumerator, IDisposable - { - private readonly StreamingZipHeaderFactory _headerFactory; - private readonly SharpCompressStream _rewindableStream; - private readonly AsyncBinaryReader _reader; - private readonly CancellationToken _cancellationToken; - private bool _completed; - - public StreamHeaderAsyncEnumerator( - StreamingZipHeaderFactory headerFactory, - Stream stream, - CancellationToken cancellationToken - ) + // Ensure the stream is already a SharpCompressStream so the buffer/size is set. + // The original code wrapped this with RewindableStream; use SharpCompressStream so we can get the buffer size. + if (stream is SourceStream src) { - _headerFactory = headerFactory; - _rewindableStream = EnsureSharpCompressStream(stream); - _reader = new AsyncBinaryReader(_rewindableStream, leaveOpen: true); - _cancellationToken = cancellationToken; + return new SharpCompressStream( + stream, + src.ReaderOptions.LeaveStreamOpen, + bufferSize: src.ReaderOptions.BufferSize + ); } - private ZipHeader? _current; - - public ZipHeader Current => - _current ?? throw new InvalidOperationException("No current header is available."); - - /// - /// Advances to the next ZIP header in the stream, honoring streaming data descriptors where applicable. - /// - public async ValueTask MoveNextAsync() - { - if (_completed) - { - return false; - } - - while (true) - { - _cancellationToken.ThrowIfCancellationRequested(); - - uint headerBytes; - var lastEntryHeader = _headerFactory.LastEntryHeader; - if ( - lastEntryHeader != null - && FlagUtility.HasFlag(lastEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor) - ) - { - if (lastEntryHeader.Part is null) - { - continue; - } - - var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; - - var crc = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - if (crc == POST_DATA_DESCRIPTOR) - { - crc = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - } - lastEntryHeader.Crc = crc; - - //attempt 32bit read - ulong compressedSize = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - ulong uncompressedSize = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - headerBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - - //check for zip64 sentinel or unexpected header - bool isSentinel = - compressedSize == 0xFFFFFFFF || uncompressedSize == 0xFFFFFFFF; - bool isHeader = headerBytes == 0x04034b50 || headerBytes == 0x02014b50; - - if (!isHeader && !isSentinel) - { - //reshuffle into 64-bit values - compressedSize = (uncompressedSize << 32) | compressedSize; - uncompressedSize = - ((ulong)headerBytes << 32) - | await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - headerBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - } - else if (isSentinel) - { - //standards-compliant zip64 descriptor - compressedSize = await _reader - .ReadUInt64Async(_cancellationToken) - .ConfigureAwait(false); - uncompressedSize = await _reader - .ReadUInt64Async(_cancellationToken) - .ConfigureAwait(false); - } - - lastEntryHeader.CompressedSize = (long)compressedSize; - lastEntryHeader.UncompressedSize = (long)uncompressedSize; - - if (pos.HasValue) - { - lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; - } - } - else if (lastEntryHeader != null && lastEntryHeader.IsZip64) - { - if (lastEntryHeader.Part is null) - { - continue; - } - - var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null; - - headerBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - - _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version - _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags - _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod - _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate - _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime - - var crc = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - - if (crc == POST_DATA_DESCRIPTOR) - { - crc = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - } - lastEntryHeader.Crc = crc; - - // The DataDescriptor can be either 64bit or 32bit - var compressedSize = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - var uncompressedSize = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - - // Check if we have header or 64bit DataDescriptor - var testHeader = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50); - - var test64Bit = ((long)uncompressedSize << 32) | compressedSize; - if (test64Bit == lastEntryHeader.CompressedSize && testHeader) - { - lastEntryHeader.UncompressedSize = - ( - (long) - await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false) << 32 - ) | headerBytes; - headerBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - } - else - { - lastEntryHeader.UncompressedSize = uncompressedSize; - } - - if (pos.HasValue) - { - lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize; - - // 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04) - _rewindableStream.Position = pos.Value + 4; - } - } - else - { - headerBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - } - - _headerFactory.LastEntryHeader = null; - var header = await _headerFactory - .ReadHeaderAsyncInternal(headerBytes, _reader) - .ConfigureAwait(false); - if (header is null) - { - _completed = true; - return false; - } - - //entry could be zero bytes so we need to know that. - if (header.ZipHeaderType == ZipHeaderType.LocalEntry) - { - var localHeader = (LocalEntryHeader)header; - var directoryHeader = _headerFactory._entries?.FirstOrDefault(entry => - entry.Key == localHeader.Name - && localHeader.CompressedSize == 0 - && localHeader.UncompressedSize == 0 - && localHeader.Crc == 0 - && localHeader.IsDirectory == false - ); - - if (directoryHeader != null) - { - localHeader.UncompressedSize = directoryHeader.Size; - localHeader.CompressedSize = directoryHeader.CompressedSize; - localHeader.Crc = (uint)directoryHeader.Crc; - } - - // If we have CompressedSize, there is data to be read - if (localHeader.CompressedSize > 0) - { - header.HasData = true; - } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor ) - else if (localHeader.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) - { - var nextHeaderBytes = await _reader - .ReadUInt32Async(_cancellationToken) - .ConfigureAwait(false); - ((IStreamStack)_rewindableStream).Rewind(sizeof(uint)); - - // Check if next data is PostDataDescriptor, streamed file with 0 length - header.HasData = !IsHeader(nextHeaderBytes); - } - else // We are not streaming and compressed size is 0, we have no data - { - header.HasData = false; - } - } - - _current = header; - return true; - } - } - - public ValueTask DisposeAsync() - { - Dispose(); - return default; - } - - /// - /// Disposes the underlying reader (without closing the archive stream). - /// - public void Dispose() - { - _reader.Dispose(); - } - - /// - /// Ensures the stream is a so header parsing can use rewind/buffer helpers. - /// - private static SharpCompressStream EnsureSharpCompressStream(Stream stream) - { - if (stream is SharpCompressStream sharpCompressStream) - { - return sharpCompressStream; - } - - // Ensure the stream is already a SharpCompressStream so the buffer/size is set. - // The original code wrapped this with RewindableStream; use SharpCompressStream so we can get the buffer size. - if (stream is SourceStream src) - { - return new SharpCompressStream( - stream, - src.ReaderOptions.LeaveStreamOpen, - bufferSize: src.ReaderOptions.BufferSize - ); - } - - throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream)); - } + throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream)); } } diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs new file mode 100644 index 00000000..a1eac925 --- /dev/null +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -0,0 +1,264 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Compressors.Deflate64; +using SharpCompress.Compressors.Explode; +using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.PPMd; +using SharpCompress.Compressors.Reduce; +using SharpCompress.Compressors.Shrink; +using SharpCompress.Compressors.Xz; +using SharpCompress.Compressors.ZStandard; +using SharpCompress.IO; + +namespace SharpCompress.Common.Zip; + +internal abstract partial class ZipFilePart +{ + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (!Header.HasData) + { + return Stream.Null; + } + var decompressionStream = await CreateDecompressionStreamAsync( + await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) + .ConfigureAwait(false), + Header.CompressionMethod, + cancellationToken + ) + .ConfigureAwait(false); + if (LeaveStreamOpen) + { + return SharpCompressStream.Create(decompressionStream, leaveOpen: true); + } + return decompressionStream; + } + + protected async ValueTask GetCryptoStreamAsync( + Stream plainStream, + CancellationToken cancellationToken = default + ) + { + var isFileEncrypted = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted); + + if (Header.CompressedSize == 0 && isFileEncrypted) + { + throw new NotSupportedException("Cannot encrypt file with unknown size at start."); + } + + if ( + ( + Header.CompressedSize == 0 + && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) + ) || Header.IsZip64 + ) + { + plainStream = SharpCompressStream.Create(plainStream, leaveOpen: true); //make sure AES doesn't close + } + else + { + plainStream = new ReadOnlySubStream(plainStream, Header.CompressedSize); //make sure AES doesn't close + } + + if (isFileEncrypted) + { + switch (Header.CompressionMethod) + { + case ZipCompressionMethod.None: + case ZipCompressionMethod.Shrink: + case ZipCompressionMethod.Reduce1: + case ZipCompressionMethod.Reduce2: + case ZipCompressionMethod.Reduce3: + case ZipCompressionMethod.Reduce4: + case ZipCompressionMethod.Deflate: + case ZipCompressionMethod.Deflate64: + case ZipCompressionMethod.BZip2: + case ZipCompressionMethod.LZMA: + case ZipCompressionMethod.PPMd: + { + return new PkwareTraditionalCryptoStream( + plainStream, + await Header + .ComposeEncryptionDataAsync(plainStream, cancellationToken) + .ConfigureAwait(false), + CryptoMode.Decrypt + ); + } + + case ZipCompressionMethod.WinzipAes: + { + if (Header.WinzipAesEncryptionData != null) + { + return new WinzipAesCryptoStream( + plainStream, + Header.WinzipAesEncryptionData, + Header.CompressedSize - 10 + ); + } + return plainStream; + } + + default: + { + throw new InvalidOperationException("Header.CompressionMethod is invalid"); + } + } + } + return plainStream; + } + + protected async ValueTask CreateDecompressionStreamAsync( + Stream stream, + ZipCompressionMethod method, + CancellationToken cancellationToken = default + ) + { + switch (method) + { + case ZipCompressionMethod.None: + { + if (Header.CompressedSize is 0) + { + return new DataDescriptorStream(stream); + } + + return stream; + } + case ZipCompressionMethod.Shrink: + { + return new ShrinkStream( + stream, + CompressionMode.Decompress, + Header.CompressedSize, + Header.UncompressedSize + ); + } + case ZipCompressionMethod.Reduce1: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 1); + } + case ZipCompressionMethod.Reduce2: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 2); + } + case ZipCompressionMethod.Reduce3: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 3); + } + case ZipCompressionMethod.Reduce4: + { + return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 4); + } + case ZipCompressionMethod.Explode: + { + return new ExplodeStream( + stream, + Header.CompressedSize, + Header.UncompressedSize, + Header.Flags + ); + } + + case ZipCompressionMethod.Deflate: + { + return new DeflateStream(stream, CompressionMode.Decompress); + } + case ZipCompressionMethod.Deflate64: + { + return new Deflate64Stream(stream, CompressionMode.Decompress); + } + case ZipCompressionMethod.BZip2: + { + return BZip2Stream.Create(stream, CompressionMode.Decompress, false); + } + case ZipCompressionMethod.LZMA: + { + if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) + { + throw new NotSupportedException("LZMA with pkware encryption."); + } + var buffer = new byte[4]; + await stream.ReadFullyAsync(buffer, 0, 4, cancellationToken).ConfigureAwait(false); + var version = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0, 2)); + var propsSize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2, 2)); + var props = new byte[propsSize]; + await stream + .ReadFullyAsync(props, 0, propsSize, cancellationToken) + .ConfigureAwait(false); + return new LzmaStream( + props, + stream, + Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, + FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + ? -1 + : Header.UncompressedSize + ); + } + case ZipCompressionMethod.Xz: + { + return new XZStream(stream); + } + case ZipCompressionMethod.ZStandard: + { + return new DecompressionStream(stream); + } + case ZipCompressionMethod.PPMd: + { + var props = new byte[2]; + await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false); + return new PpmdStream(new PpmdProperties(props), stream, false); + } + case ZipCompressionMethod.WinzipAes: + { + var data = Header.Extra.SingleOrDefault(x => x.Type == ExtraDataType.WinZipAes); + if (data is null) + { + throw new InvalidFormatException("No Winzip AES extra data found."); + } + + if (data.Length != 7) + { + throw new InvalidFormatException("Winzip data length is not 7."); + } + + var compressedMethod = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes); + + if (compressedMethod != 0x01 && compressedMethod != 0x02) + { + throw new InvalidFormatException( + "Unexpected vendor version number for WinZip AES metadata" + ); + } + + var vendorId = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(2)); + if (vendorId != 0x4541) + { + throw new InvalidFormatException( + "Unexpected vendor ID for WinZip AES metadata" + ); + } + + return await CreateDecompressionStreamAsync( + stream, + (ZipCompressionMethod) + BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), + cancellationToken + ); + } + default: + { + throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); + } + } + } +} diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs index 69ba27bc..689b1c8b 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs @@ -2,8 +2,6 @@ using System; using System.Buffers.Binary; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; using SharpCompress.Compressors.BZip2; @@ -20,7 +18,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal abstract class ZipFilePart : FilePart +internal abstract partial class ZipFilePart : FilePart { internal ZipFilePart(ZipFileEntry header, Stream stream) : base(header.ArchiveEncoding) @@ -266,244 +264,4 @@ internal abstract class ZipFilePart : FilePart } return plainStream; } - - internal override async ValueTask GetCompressedStreamAsync( - CancellationToken cancellationToken = default - ) - { - if (!Header.HasData) - { - return Stream.Null; - } - var decompressionStream = await CreateDecompressionStreamAsync( - await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken) - .ConfigureAwait(false), - Header.CompressionMethod, - cancellationToken - ) - .ConfigureAwait(false); - if (LeaveStreamOpen) - { - return SharpCompressStream.Create(decompressionStream, leaveOpen: true); - } - return decompressionStream; - } - - protected async ValueTask GetCryptoStreamAsync( - Stream plainStream, - CancellationToken cancellationToken = default - ) - { - var isFileEncrypted = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted); - - if (Header.CompressedSize == 0 && isFileEncrypted) - { - throw new NotSupportedException("Cannot encrypt file with unknown size at start."); - } - - if ( - ( - Header.CompressedSize == 0 - && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) - ) || Header.IsZip64 - ) - { - plainStream = SharpCompressStream.Create(plainStream, leaveOpen: true); //make sure AES doesn't close - } - else - { - plainStream = new ReadOnlySubStream(plainStream, Header.CompressedSize); //make sure AES doesn't close - } - - if (isFileEncrypted) - { - switch (Header.CompressionMethod) - { - case ZipCompressionMethod.None: - case ZipCompressionMethod.Shrink: - case ZipCompressionMethod.Reduce1: - case ZipCompressionMethod.Reduce2: - case ZipCompressionMethod.Reduce3: - case ZipCompressionMethod.Reduce4: - case ZipCompressionMethod.Deflate: - case ZipCompressionMethod.Deflate64: - case ZipCompressionMethod.BZip2: - case ZipCompressionMethod.LZMA: - case ZipCompressionMethod.PPMd: - { - return new PkwareTraditionalCryptoStream( - plainStream, - await Header - .ComposeEncryptionDataAsync(plainStream, cancellationToken) - .ConfigureAwait(false), - CryptoMode.Decrypt - ); - } - - case ZipCompressionMethod.WinzipAes: - { - if (Header.WinzipAesEncryptionData != null) - { - return new WinzipAesCryptoStream( - plainStream, - Header.WinzipAesEncryptionData, - Header.CompressedSize - 10 - ); - } - return plainStream; - } - - default: - { - throw new InvalidOperationException("Header.CompressionMethod is invalid"); - } - } - } - return plainStream; - } - - protected async ValueTask CreateDecompressionStreamAsync( - Stream stream, - ZipCompressionMethod method, - CancellationToken cancellationToken = default - ) - { - switch (method) - { - case ZipCompressionMethod.None: - { - if (Header.CompressedSize is 0) - { - return new DataDescriptorStream(stream); - } - - return stream; - } - case ZipCompressionMethod.Shrink: - { - return new ShrinkStream( - stream, - CompressionMode.Decompress, - Header.CompressedSize, - Header.UncompressedSize - ); - } - case ZipCompressionMethod.Reduce1: - { - return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 1); - } - case ZipCompressionMethod.Reduce2: - { - return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 2); - } - case ZipCompressionMethod.Reduce3: - { - return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 3); - } - case ZipCompressionMethod.Reduce4: - { - return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 4); - } - case ZipCompressionMethod.Explode: - { - return new ExplodeStream( - stream, - Header.CompressedSize, - Header.UncompressedSize, - Header.Flags - ); - } - - case ZipCompressionMethod.Deflate: - { - return new DeflateStream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.Deflate64: - { - return new Deflate64Stream(stream, CompressionMode.Decompress); - } - case ZipCompressionMethod.BZip2: - { - return BZip2Stream.Create(stream, CompressionMode.Decompress, false); - } - case ZipCompressionMethod.LZMA: - { - if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) - { - throw new NotSupportedException("LZMA with pkware encryption."); - } - var buffer = new byte[4]; - await stream.ReadFullyAsync(buffer, 0, 4, cancellationToken).ConfigureAwait(false); - var version = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0, 2)); - var propsSize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2, 2)); - var props = new byte[propsSize]; - await stream - .ReadFullyAsync(props, 0, propsSize, cancellationToken) - .ConfigureAwait(false); - return new LzmaStream( - props, - stream, - Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, - FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) - ? -1 - : Header.UncompressedSize - ); - } - case ZipCompressionMethod.Xz: - { - return new XZStream(stream); - } - case ZipCompressionMethod.ZStandard: - { - return new DecompressionStream(stream); - } - case ZipCompressionMethod.PPMd: - { - var props = new byte[2]; - await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false); - return new PpmdStream(new PpmdProperties(props), stream, false); - } - case ZipCompressionMethod.WinzipAes: - { - var data = Header.Extra.SingleOrDefault(x => x.Type == ExtraDataType.WinZipAes); - if (data is null) - { - throw new InvalidFormatException("No Winzip AES extra data found."); - } - - if (data.Length != 7) - { - throw new InvalidFormatException("Winzip data length is not 7."); - } - - var compressedMethod = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes); - - if (compressedMethod != 0x01 && compressedMethod != 0x02) - { - throw new InvalidFormatException( - "Unexpected vendor version number for WinZip AES metadata" - ); - } - - var vendorId = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(2)); - if (vendorId != 0x4541) - { - throw new InvalidFormatException( - "Unexpected vendor ID for WinZip AES metadata" - ); - } - - return await CreateDecompressionStreamAsync( - stream, - (ZipCompressionMethod) - BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), - cancellationToken - ); - } - default: - { - throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod); - } - } - } } diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs new file mode 100644 index 00000000..8d585505 --- /dev/null +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs @@ -0,0 +1,166 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Common.Zip; + +internal partial class ZipHeaderFactory +{ + protected async ValueTask ReadHeader( + uint headerBytes, + AsyncBinaryReader reader, + bool zip64 = false + ) + { + switch (headerBytes) + { + case ENTRY_HEADER_BYTES: + { + var entryHeader = new LocalEntryHeader(_archiveEncoding); + await entryHeader.Read(reader); + await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false); + + _lastEntryHeader = entryHeader; + return entryHeader; + } + case DIRECTORY_START_HEADER_BYTES: + { + var entry = new DirectoryEntryHeader(_archiveEncoding); + await entry.Read(reader); + return entry; + } + case POST_DATA_DESCRIPTOR: + { + if ( + _lastEntryHeader != null + && FlagUtility.HasFlag( + _lastEntryHeader.NotNull().Flags, + HeaderFlags.UsePostDataDescriptor + ) + ) + { + _lastEntryHeader.Crc = await reader.ReadUInt32Async(); + _lastEntryHeader.CompressedSize = zip64 + ? (long)await reader.ReadUInt64Async() + : await reader.ReadUInt32Async(); + _lastEntryHeader.UncompressedSize = zip64 + ? (long)await reader.ReadUInt64Async() + : await reader.ReadUInt32Async(); + } + else + { + await reader.SkipAsync(zip64 ? 20 : 12); + } + return null; + } + case DIGITAL_SIGNATURE: + return null; + case DIRECTORY_END_HEADER_BYTES: + { + var entry = new DirectoryEndHeader(); + await entry.Read(reader); + return entry; + } + case SPLIT_ARCHIVE_HEADER_BYTES: + { + return new SplitHeader(); + } + case ZIP64_END_OF_CENTRAL_DIRECTORY: + { + var entry = new Zip64DirectoryEndHeader(); + await entry.Read(reader); + return entry; + } + case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR: + { + var entry = new Zip64DirectoryEndLocatorHeader(); + await entry.Read(reader); + return entry; + } + default: + return null; + } + } + + /// + /// Loads encryption metadata and stream positioning for a header using async reads where needed. + /// + private async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream) + { + if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted)) + { + if ( + !entryHeader.IsDirectory + && entryHeader.CompressedSize == 0 + && FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor) + ) + { + throw new NotSupportedException( + "SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner." + ); + } + + if (_password is null) + { + throw new CryptographicException("No password supplied for encrypted zip."); + } + + entryHeader.Password = _password; + + if (entryHeader.CompressionMethod == ZipCompressionMethod.WinzipAes) + { + var data = entryHeader.Extra.SingleOrDefault(x => + x.Type == ExtraDataType.WinZipAes + ); + if (data != null) + { + var keySize = (WinzipAesKeySize)data.DataBytes[4]; + + var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2]; + var passwordVerifyValue = new byte[2]; + await stream.ReadExactAsync(salt, 0, salt.Length).ConfigureAwait(false); + await stream.ReadExactAsync(passwordVerifyValue, 0, 2).ConfigureAwait(false); + + entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData( + keySize, + salt, + passwordVerifyValue, + _password + ); + + entryHeader.CompressedSize -= (uint)(salt.Length + 2); + } + } + } + + if (entryHeader.IsDirectory) + { + return; + } + + switch (_mode) + { + case StreamingMode.Seekable: + { + entryHeader.DataStartPosition = stream.Position; + stream.Position += entryHeader.CompressedSize; + break; + } + + case StreamingMode.Streaming: + { + entryHeader.PackedStream = stream; + break; + } + + default: + { + throw new InvalidFormatException("Invalid StreamingMode"); + } + } + } +} diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index 44e40734..8d0ffdb4 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal class ZipHeaderFactory +internal partial class ZipHeaderFactory { internal const uint ENTRY_HEADER_BYTES = 0x04034b50; internal const uint POST_DATA_DESCRIPTOR = 0x08074b50; @@ -36,82 +36,6 @@ internal class ZipHeaderFactory _archiveEncoding = archiveEncoding; } - protected async ValueTask ReadHeader( - uint headerBytes, - AsyncBinaryReader reader, - bool zip64 = false - ) - { - switch (headerBytes) - { - case ENTRY_HEADER_BYTES: - { - var entryHeader = new LocalEntryHeader(_archiveEncoding); - await entryHeader.Read(reader); - await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false); - - _lastEntryHeader = entryHeader; - return entryHeader; - } - case DIRECTORY_START_HEADER_BYTES: - { - var entry = new DirectoryEntryHeader(_archiveEncoding); - await entry.Read(reader); - return entry; - } - case POST_DATA_DESCRIPTOR: - { - if ( - _lastEntryHeader != null - && FlagUtility.HasFlag( - _lastEntryHeader.NotNull().Flags, - HeaderFlags.UsePostDataDescriptor - ) - ) - { - _lastEntryHeader.Crc = await reader.ReadUInt32Async(); - _lastEntryHeader.CompressedSize = zip64 - ? (long)await reader.ReadUInt64Async() - : await reader.ReadUInt32Async(); - _lastEntryHeader.UncompressedSize = zip64 - ? (long)await reader.ReadUInt64Async() - : await reader.ReadUInt32Async(); - } - else - { - await reader.SkipAsync(zip64 ? 20 : 12); - } - return null; - } - case DIGITAL_SIGNATURE: - return null; - case DIRECTORY_END_HEADER_BYTES: - { - var entry = new DirectoryEndHeader(); - await entry.Read(reader); - return entry; - } - case SPLIT_ARCHIVE_HEADER_BYTES: - { - return new SplitHeader(); - } - case ZIP64_END_OF_CENTRAL_DIRECTORY: - { - var entry = new Zip64DirectoryEndHeader(); - await entry.Read(reader); - return entry; - } - case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR: - { - var entry = new Zip64DirectoryEndLocatorHeader(); - await entry.Read(reader); - return entry; - } - default: - return null; - } - } - protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false) { switch (headerBytes) @@ -283,82 +207,4 @@ internal class ZipHeaderFactory //} } - - /// - /// Loads encryption metadata and stream positioning for a header using async reads where needed. - /// - private async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream) - { - if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted)) - { - if ( - !entryHeader.IsDirectory - && entryHeader.CompressedSize == 0 - && FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor) - ) - { - throw new NotSupportedException( - "SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner." - ); - } - - if (_password is null) - { - throw new CryptographicException("No password supplied for encrypted zip."); - } - - entryHeader.Password = _password; - - if (entryHeader.CompressionMethod == ZipCompressionMethod.WinzipAes) - { - var data = entryHeader.Extra.SingleOrDefault(x => - x.Type == ExtraDataType.WinZipAes - ); - if (data != null) - { - var keySize = (WinzipAesKeySize)data.DataBytes[4]; - - var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2]; - var passwordVerifyValue = new byte[2]; - await stream.ReadExactAsync(salt, 0, salt.Length).ConfigureAwait(false); - await stream.ReadExactAsync(passwordVerifyValue, 0, 2).ConfigureAwait(false); - - entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData( - keySize, - salt, - passwordVerifyValue, - _password - ); - - entryHeader.CompressedSize -= (uint)(salt.Length + 2); - } - } - } - - if (entryHeader.IsDirectory) - { - return; - } - - switch (_mode) - { - case StreamingMode.Seekable: - { - entryHeader.DataStartPosition = stream.Position; - stream.Position += entryHeader.CompressedSize; - break; - } - - case StreamingMode.Streaming: - { - entryHeader.PackedStream = stream; - break; - } - - default: - { - throw new InvalidFormatException("Invalid StreamingMode"); - } - } - } } diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs b/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs new file mode 100644 index 00000000..cf12a8c7 --- /dev/null +++ b/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs @@ -0,0 +1,108 @@ +// +// ADC.cs +// +// Author: +// Natalia Portillo +// +// Copyright (c) 2016 © Claunia.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.ADC; + +public sealed partial class ADCStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + if (count == 0) + { + return 0; + } + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (offset < buffer.GetLowerBound(0)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if ((offset + count) > buffer.GetLength(0)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_outBuffer is null) + { + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + } + + var inPosition = offset; + var toCopy = count; + var copied = 0; + + while (_outPosition + toCopy >= _outBuffer.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var piece = _outBuffer.Length - _outPosition; + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece); + inPosition += piece; + copied += piece; + _position += piece; + toCopy -= piece; + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0) + { + return copied; + } + } + + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy); + _outPosition += toCopy; + _position += toCopy; + copied += toCopy; + return copied; + } +} diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.cs b/src/SharpCompress/Compressors/ADC/ADCStream.cs index 935207c9..c35514a6 100644 --- a/src/SharpCompress/Compressors/ADC/ADCStream.cs +++ b/src/SharpCompress/Compressors/ADC/ADCStream.cs @@ -37,7 +37,7 @@ namespace SharpCompress.Compressors.ADC; /// /// Provides a forward readable only stream that decompresses ADC data /// -public sealed class ADCStream : Stream, IStreamStack +public sealed partial class ADCStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -189,76 +189,6 @@ public sealed class ADCStream : Stream, IStreamStack return copied; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - if (count == 0) - { - return 0; - } - if (buffer is null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - if (offset < buffer.GetLowerBound(0)) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - if ((offset + count) > buffer.GetLength(0)) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (_outBuffer is null) - { - var result = await ADCBase.DecompressAsync( - _stream, - cancellationToken: cancellationToken - ); - _outBuffer = result.Output; - _outPosition = 0; - } - - var inPosition = offset; - var toCopy = count; - var copied = 0; - - while (_outPosition + toCopy >= _outBuffer.Length) - { - cancellationToken.ThrowIfCancellationRequested(); - var piece = _outBuffer.Length - _outPosition; - Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece); - inPosition += piece; - copied += piece; - _position += piece; - toCopy -= piece; - var result = await ADCBase.DecompressAsync( - _stream, - cancellationToken: cancellationToken - ); - _outBuffer = result.Output; - _outPosition = 0; - if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0) - { - return copied; - } - } - - Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy); - _outPosition += toCopy; - _position += toCopy; - copied += toCopy; - return copied; - } - 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/Compressors/Arj/BitReader.Async.cs b/src/SharpCompress/Compressors/Arj/BitReader.Async.cs new file mode 100644 index 00000000..612c9143 --- /dev/null +++ b/src/SharpCompress/Compressors/Arj/BitReader.Async.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.Arj; + +public partial class BitReader +{ + /// + /// Asynchronously reads a single bit from the stream. Returns 0 or 1. + /// + public async ValueTask ReadBitAsync(CancellationToken cancellationToken) + { + if (_bitCount == 0) + { + var buffer = new byte[1]; + int bytesRead = await _input + .ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead < 1) + { + throw new EndOfStreamException("No more data available in BitReader."); + } + + _bitBuffer = buffer[0]; + _bitCount = 8; + } + + int bit = (_bitBuffer >> (_bitCount - 1)) & 1; + _bitCount--; + return bit; + } + + /// + /// Asynchronously reads n bits (up to 32) from the stream. + /// + public async ValueTask ReadBitsAsync(int count, CancellationToken cancellationToken) + { + if (count < 0 || count > 32) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count must be between 0 and 32."); + } + + int result = 0; + for (int i = 0; i < count; i++) + { + result = (result << 1) | await ReadBitAsync(cancellationToken).ConfigureAwait(false); + } + return result; + } +} diff --git a/src/SharpCompress/Compressors/Arj/BitReader.cs b/src/SharpCompress/Compressors/Arj/BitReader.cs index 87c1a7eb..feb8cb94 100644 --- a/src/SharpCompress/Compressors/Arj/BitReader.cs +++ b/src/SharpCompress/Compressors/Arj/BitReader.cs @@ -1,12 +1,10 @@ using System; using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace SharpCompress.Compressors.Arj { [CLSCompliant(true)] - public class BitReader + public partial class BitReader { private readonly Stream _input; private int _bitBuffer; // currently buffered bits @@ -62,53 +60,6 @@ namespace SharpCompress.Compressors.Arj return result; } - /// - /// Asynchronously reads a single bit from the stream. Returns 0 or 1. - /// - public async ValueTask ReadBitAsync(CancellationToken cancellationToken) - { - if (_bitCount == 0) - { - var buffer = new byte[1]; - int bytesRead = await _input - .ReadAsync(buffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - if (bytesRead < 1) - { - throw new EndOfStreamException("No more data available in BitReader."); - } - - _bitBuffer = buffer[0]; - _bitCount = 8; - } - - int bit = (_bitBuffer >> (_bitCount - 1)) & 1; - _bitCount--; - return bit; - } - - /// - /// Asynchronously reads n bits (up to 32) from the stream. - /// - public async ValueTask ReadBitsAsync(int count, CancellationToken cancellationToken) - { - if (count < 0 || count > 32) - { - throw new ArgumentOutOfRangeException( - nameof(count), - "Count must be between 0 and 32." - ); - } - - int result = 0; - for (int i = 0; i < count; i++) - { - result = - (result << 1) | await ReadBitAsync(cancellationToken).ConfigureAwait(false); - } - return result; - } - /// /// Resets any buffered bits. /// diff --git a/src/SharpCompress/Compressors/Arj/LHDecoderStream.Async.cs b/src/SharpCompress/Compressors/Arj/LHDecoderStream.Async.cs new file mode 100644 index 00000000..568e387e --- /dev/null +++ b/src/SharpCompress/Compressors/Arj/LHDecoderStream.Async.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.Arj; + +public sealed partial class LHDecoderStream +{ + /// + /// Asynchronously decodes a single element (literal or back-reference) and appends it to _buffer. + /// Returns true if data was added, or false if all input has already been decoded. + /// + private async ValueTask DecodeNextAsync(CancellationToken cancellationToken) + { + if (_buffer.Count >= _originalSize) + { + _finishedDecoding = true; + return false; + } + + int len = await DecodeValAsync(0, 7, cancellationToken).ConfigureAwait(false); + if (len == 0) + { + byte nextChar = (byte) + await _bitReader.ReadBitsAsync(8, cancellationToken).ConfigureAwait(false); + _buffer.Add(nextChar); + } + else + { + int repCount = len + THRESHOLD - 1; + int backPtr = await DecodeValAsync(9, 13, cancellationToken).ConfigureAwait(false); + + if (backPtr >= _buffer.Count) + throw new InvalidDataException("Invalid back_ptr in LH stream"); + + int srcIndex = _buffer.Count - 1 - backPtr; + for (int j = 0; j < repCount && _buffer.Count < _originalSize; j++) + { + byte b = _buffer[srcIndex]; + _buffer.Add(b); + srcIndex++; + // srcIndex may grow; it's allowed (source region can overlap destination) + } + } + + if (_buffer.Count >= _originalSize) + { + _finishedDecoding = true; + } + + return true; + } + + private async ValueTask DecodeValAsync( + int from, + int to, + CancellationToken cancellationToken + ) + { + int add = 0; + int bit = from; + + while ( + bit < to + && await _bitReader.ReadBitsAsync(1, cancellationToken).ConfigureAwait(false) == 1 + ) + { + add |= 1 << bit; + bit++; + } + + int res = + bit > 0 + ? await _bitReader.ReadBitsAsync(bit, cancellationToken).ConfigureAwait(false) + : 0; + return res + add; + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + throw new ObjectDisposedException(nameof(LHDecoderStream)); + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || count < 0 || offset + count > buffer.Length) + throw new ArgumentOutOfRangeException("offset/count"); + + if (_readPosition >= _originalSize) + return 0; // EOF + + int totalRead = 0; + + while (totalRead < count && _readPosition < _originalSize) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_readPosition >= _buffer.Count) + { + bool had = await DecodeNextAsync(cancellationToken).ConfigureAwait(false); + if (!had) + { + break; + } + } + + int available = _buffer.Count - (int)_readPosition; + if (available <= 0) + { + if (!_finishedDecoding) + { + continue; + } + break; + } + + int toCopy = Math.Min(available, count - totalRead); + _buffer.CopyTo((int)_readPosition, buffer, offset + totalRead, toCopy); + + _readPosition += toCopy; + totalRead += toCopy; + } + + return totalRead; + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + throw new ObjectDisposedException(nameof(LHDecoderStream)); + + if (_readPosition >= _originalSize) + return 0; // EOF + + int totalRead = 0; + + while (totalRead < buffer.Length && _readPosition < _originalSize) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_readPosition >= _buffer.Count) + { + bool had = await DecodeNextAsync(cancellationToken).ConfigureAwait(false); + if (!had) + { + break; + } + } + + int available = _buffer.Count - (int)_readPosition; + if (available <= 0) + { + if (!_finishedDecoding) + { + continue; + } + break; + } + + int toCopy = Math.Min(available, buffer.Length - totalRead); + for (int i = 0; i < toCopy; i++) + { + buffer.Span[totalRead + i] = _buffer[(int)_readPosition + i]; + } + + _readPosition += toCopy; + totalRead += toCopy; + } + + return totalRead; + } +#endif +} diff --git a/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs b/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs index 27d1af3c..f2c4fca1 100644 --- a/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs +++ b/src/SharpCompress/Compressors/Arj/LHDecoderStream.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.Arj { [CLSCompliant(true)] - public sealed class LHDecoderStream : Stream, IStreamStack + public sealed partial class LHDecoderStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -116,51 +114,6 @@ namespace SharpCompress.Compressors.Arj return true; } - /// - /// Asynchronously decodes a single element (literal or back-reference) and appends it to _buffer. - /// Returns true if data was added, or false if all input has already been decoded. - /// - private async ValueTask DecodeNextAsync(CancellationToken cancellationToken) - { - if (_buffer.Count >= _originalSize) - { - _finishedDecoding = true; - return false; - } - - int len = await DecodeValAsync(0, 7, cancellationToken).ConfigureAwait(false); - if (len == 0) - { - byte nextChar = (byte) - await _bitReader.ReadBitsAsync(8, cancellationToken).ConfigureAwait(false); - _buffer.Add(nextChar); - } - else - { - int repCount = len + THRESHOLD - 1; - int backPtr = await DecodeValAsync(9, 13, cancellationToken).ConfigureAwait(false); - - if (backPtr >= _buffer.Count) - throw new InvalidDataException("Invalid back_ptr in LH stream"); - - int srcIndex = _buffer.Count - 1 - backPtr; - for (int j = 0; j < repCount && _buffer.Count < _originalSize; j++) - { - byte b = _buffer[srcIndex]; - _buffer.Add(b); - srcIndex++; - // srcIndex may grow; it's allowed (source region can overlap destination) - } - } - - if (_buffer.Count >= _originalSize) - { - _finishedDecoding = true; - } - - return true; - } - private int DecodeVal(int from, int to) { int add = 0; @@ -176,31 +129,6 @@ namespace SharpCompress.Compressors.Arj return res + add; } - private async ValueTask DecodeValAsync( - int from, - int to, - CancellationToken cancellationToken - ) - { - int add = 0; - int bit = from; - - while ( - bit < to - && await _bitReader.ReadBitsAsync(1, cancellationToken).ConfigureAwait(false) == 1 - ) - { - add |= 1 << bit; - bit++; - } - - int res = - bit > 0 - ? await _bitReader.ReadBitsAsync(bit, cancellationToken).ConfigureAwait(false) - : 0; - return res + add; - } - /// /// Reads decompressed bytes into buffer[offset..offset+count]. /// The method decodes additional data on demand when needed. @@ -250,109 +178,6 @@ namespace SharpCompress.Compressors.Arj return totalRead; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - throw new ObjectDisposedException(nameof(LHDecoderStream)); - if (buffer is null) - throw new ArgumentNullException(nameof(buffer)); - if (offset < 0 || count < 0 || offset + count > buffer.Length) - throw new ArgumentOutOfRangeException("offset/count"); - - if (_readPosition >= _originalSize) - return 0; // EOF - - int totalRead = 0; - - while (totalRead < count && _readPosition < _originalSize) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_readPosition >= _buffer.Count) - { - bool had = await DecodeNextAsync(cancellationToken).ConfigureAwait(false); - if (!had) - { - break; - } - } - - int available = _buffer.Count - (int)_readPosition; - if (available <= 0) - { - if (!_finishedDecoding) - { - continue; - } - break; - } - - int toCopy = Math.Min(available, count - totalRead); - _buffer.CopyTo((int)_readPosition, buffer, offset + totalRead, toCopy); - - _readPosition += toCopy; - totalRead += toCopy; - } - - return totalRead; - } - -#if !NETFRAMEWORK && !NETSTANDARD2_0 - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - throw new ObjectDisposedException(nameof(LHDecoderStream)); - - if (_readPosition >= _originalSize) - return 0; // EOF - - int totalRead = 0; - - while (totalRead < buffer.Length && _readPosition < _originalSize) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_readPosition >= _buffer.Count) - { - bool had = await DecodeNextAsync(cancellationToken).ConfigureAwait(false); - if (!had) - { - break; - } - } - - int available = _buffer.Count - (int)_readPosition; - if (available <= 0) - { - if (!_finishedDecoding) - { - continue; - } - break; - } - - int toCopy = Math.Min(available, buffer.Length - totalRead); - for (int i = 0; i < toCopy; i++) - { - buffer.Span[totalRead + i] = _buffer[(int)_readPosition + i]; - } - - _readPosition += toCopy; - totalRead += toCopy; - } - - return totalRead; - } -#endif - public override void Flush() => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs new file mode 100644 index 00000000..ce125bd5 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.BZip2; + +public sealed partial class BZip2Stream +{ +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); +} diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index e7627c2a..5373f71f 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -6,7 +6,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.BZip2; -public sealed class BZip2Stream : Stream, IStreamStack +public sealed partial class BZip2Stream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -152,36 +152,11 @@ public sealed class BZip2Stream : Stream, IStreamStack public override void SetLength(long value) => stream.SetLength(value); #if !LEGACY_DOTNET - public override int Read(Span buffer) => stream.Read(buffer); public override void Write(ReadOnlySpan buffer) => stream.Write(buffer); - - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); #endif - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs new file mode 100644 index 00000000..74d7ea28 --- /dev/null +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs @@ -0,0 +1,879 @@ +#nullable disable + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.BZip2; + +internal partial class CBZip2InputStream +{ + public async ValueTask ReadByteAsync(CancellationToken cancellationToken) + { + if (streamEnd) + { + return -1; + } + var retChar = currentChar; + switch (currentState) + { + case START_BLOCK_STATE: + break; + case RAND_PART_A_STATE: + break; + case RAND_PART_B_STATE: + await SetupRandPartBAsync(cancellationToken).ConfigureAwait(false); + break; + case RAND_PART_C_STATE: + await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false); + break; + case NO_RAND_PART_A_STATE: + break; + case NO_RAND_PART_B_STATE: + await SetupNoRandPartBAsync(cancellationToken).ConfigureAwait(false); + break; + case NO_RAND_PART_C_STATE: + await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false); + break; + default: + break; + } + return retChar; + } + + private async ValueTask InitializeAsync( + bool isFirstStream, + CancellationToken cancellationToken + ) + { + var singleByte = new byte[1]; + var read0 = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + var magic0 = read0 == 0 ? -1 : singleByte[0]; + var read1 = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + var magic1 = read1 == 0 ? -1 : singleByte[0]; + var read2 = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + var magic2 = read2 == 0 ? -1 : singleByte[0]; + if (magic0 == -1 && !isFirstStream) + { + return false; + } + if (magic0 != 'B' || magic1 != 'Z' || magic2 != 'h') + { + throw new IOException("Not a BZIP2 marked stream"); + } + var read3 = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + var magic3 = read3 == 0 ? -1 : singleByte[0]; + if (magic3 < '1' || magic3 > '9') + { + BsFinishedWithStream(); + streamEnd = true; + return false; + } + + SetDecompressStructureSizes(magic3 - '0'); + bsLive = 0; + computedCombinedCRC = 0; + return true; + } + + private async ValueTask InitBlockAsync(CancellationToken cancellationToken) + { + char magic1, + magic2, + magic3, + magic4; + char magic5, + magic6; + + while (true) + { + magic1 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + magic2 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + magic3 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + magic4 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + magic5 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + magic6 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); + if ( + magic1 != 0x17 + || magic2 != 0x72 + || magic3 != 0x45 + || magic4 != 0x38 + || magic5 != 0x50 + || magic6 != 0x90 + ) + { + break; + } + + if (await CompleteAsync(cancellationToken).ConfigureAwait(false)) + { + return; + } + } + + if ( + magic1 != 0x31 + || magic2 != 0x41 + || magic3 != 0x59 + || magic4 != 0x26 + || magic5 != 0x53 + || magic6 != 0x59 + ) + { + BadBlockHeader(); + streamEnd = true; + return; + } + + storedBlockCRC = await BsGetInt32Async(cancellationToken).ConfigureAwait(false); + + if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) + { + blockRandomised = true; + } + else + { + blockRandomised = false; + } + + // currBlockNo++; + await GetAndMoveToFrontDecodeAsync(cancellationToken).ConfigureAwait(false); + + mCrc.InitialiseCRC(); + currentState = START_BLOCK_STATE; + } + + private async ValueTask CompleteAsync(CancellationToken cancellationToken) + { + storedCombinedCRC = await BsGetInt32Async(cancellationToken).ConfigureAwait(false); + if (storedCombinedCRC != computedCombinedCRC) + { + CrcError(); + } + + var complete = + !decompressConcatenated + || !(await InitializeAsync(false, cancellationToken).ConfigureAwait(false)); + if (complete) + { + BsFinishedWithStream(); + streamEnd = true; + } + + // Look for the next .bz2 stream if decompressing + // concatenated files. + return complete; + } + + private async ValueTask BsGetintAsync(CancellationToken cancellationToken) + { + var u = 0; + u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); + u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); + u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); + u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); + return u; + } + + private async ValueTask RecvDecodingTablesAsync(CancellationToken cancellationToken) + { + var len = InitCharArray(BZip2Constants.N_GROUPS, BZip2Constants.MAX_ALPHA_SIZE); + int i, + j, + t, + nGroups, + nSelectors, + alphaSize; + int minLen, + maxLen; + var inUse16 = new bool[16]; + + /* Receive the mapping table */ + for (i = 0; i < 16; i++) + { + if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) + { + inUse16[i] = true; + } + else + { + inUse16[i] = false; + } + } + + for (i = 0; i < 256; i++) + { + inUse[i] = false; + } + + for (i = 0; i < 16; i++) + { + if (inUse16[i]) + { + for (j = 0; j < 16; j++) + { + if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) + { + inUse[(i * 16) + j] = true; + } + } + } + } + + MakeMaps(); + alphaSize = nInUse + 2; + + /* Now the selectors */ + nGroups = await BsRAsync(3, cancellationToken).ConfigureAwait(false); + nSelectors = await BsRAsync(15, cancellationToken).ConfigureAwait(false); + for (i = 0; i < nSelectors; i++) + { + j = 0; + while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) + { + j++; + } + if (i < BZip2Constants.MAX_SELECTORS) + { + selectorMtf[i] = (char)j; + } + } + + nSelectors = Math.Min(nSelectors, BZip2Constants.MAX_SELECTORS); + + /* Undo the MTF values for the selectors. */ + { + var pos = new char[BZip2Constants.N_GROUPS]; + char tmp, + v; + for (v = '\0'; v < nGroups; v++) + { + pos[v] = v; + } + + for (i = 0; i < nSelectors; i++) + { + v = selectorMtf[i]; + tmp = pos[v]; + while (v > 0) + { + pos[v] = pos[v - 1]; + v--; + } + pos[0] = tmp; + selector[i] = tmp; + } + } + + /* Now the coding tables */ + for (t = 0; t < nGroups; t++) + { + var curr = await BsRAsync(5, cancellationToken).ConfigureAwait(false); + for (i = 0; i < alphaSize; i++) + { + while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) + { + if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 0) + { + curr++; + } + else + { + curr--; + } + } + len[t][i] = (char)curr; + } + } + + /* Create the Huffman decoding tables */ + for (t = 0; t < nGroups; t++) + { + minLen = 32; + maxLen = 0; + for (i = 0; i < alphaSize; i++) + { + if (len[t][i] > maxLen) + { + maxLen = len[t][i]; + } + if (len[t][i] < minLen) + { + minLen = len[t][i]; + } + } + HbCreateDecodeTables(limit[t], basev[t], perm[t], len[t], minLen, maxLen, alphaSize); + minLens[t] = minLen; + } + } + + private async ValueTask GetAndMoveToFrontDecodeAsync(CancellationToken cancellationToken) + { + var yy = new char[256]; + int i, + j, + nextSym, + limitLast; + int EOB, + groupNo, + groupPos; + var singleByte = new byte[1]; + + limitLast = BZip2Constants.baseBlockSize * blockSize100k; + origPtr = await BsGetIntVSAsync(24, cancellationToken).ConfigureAwait(false); + + await RecvDecodingTablesAsync(cancellationToken).ConfigureAwait(false); + EOB = nInUse + 1; + groupNo = -1; + groupPos = 0; + + /* + Setting up the unzftab entries here is not strictly + necessary, but it does save having to do it later + in a separate pass, and so saves a block's worth of + cache misses. + */ + for (i = 0; i <= 255; i++) + { + unzftab[i] = 0; + } + + for (i = 0; i <= 255; i++) + { + yy[i] = (char)i; + } + + last = -1; + + { + int zt, + zn, + zvec, + zj; + if (groupPos == 0) + { + groupNo++; + groupPos = BZip2Constants.G_SIZE; + } + groupPos--; + zt = selector[groupNo]; + zn = minLens[zt]; + zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false); + while (zvec > limit[zt][zn]) + { + zn++; + { + { + while (bsLive < 1) + { + int zzi; + int thech = '\0'; + try + { + var readCount = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + thech = readCount == 0 ? '\uffff' : singleByte[0]; + } + catch (IOException) + { + CompressedStreamEOF(); + } + if (thech == '\uffff') + { + CompressedStreamEOF(); + } + zzi = thech; + bsBuff = (bsBuff << 8) | (zzi & 0xff); + bsLive += 8; + } + } + zj = (bsBuff >> (bsLive - 1)) & 1; + bsLive--; + } + zvec = (zvec << 1) | zj; + } + nextSym = perm[zt][zvec - basev[zt][zn]]; + } + + while (true) + { + if (nextSym == EOB) + { + break; + } + + if (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB) + { + char ch; + var s = -1; + var N = 1; + do + { + if (nextSym == BZip2Constants.RUNA) + { + s += (0 + 1) * N; + } + else if (nextSym == BZip2Constants.RUNB) + { + s += (1 + 1) * N; + } + N *= 2; + { + int zt, + zn, + zvec, + zj; + if (groupPos == 0) + { + groupNo++; + groupPos = BZip2Constants.G_SIZE; + } + groupPos--; + zt = selector[groupNo]; + zn = minLens[zt]; + zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false); + while (zvec > limit[zt][zn]) + { + zn++; + { + { + while (bsLive < 1) + { + int zzi; + int thech = '\0'; + try + { + var readCount = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + thech = readCount == 0 ? '\uffff' : singleByte[0]; + } + catch (IOException) + { + CompressedStreamEOF(); + } + if (thech == '\uffff') + { + CompressedStreamEOF(); + } + zzi = thech; + bsBuff = (bsBuff << 8) | (zzi & 0xff); + bsLive += 8; + } + } + zj = (bsBuff >> (bsLive - 1)) & 1; + bsLive--; + } + zvec = (zvec << 1) | zj; + } + nextSym = perm[zt][zvec - basev[zt][zn]]; + } + } while (nextSym == BZip2Constants.RUNA || nextSym == BZip2Constants.RUNB); + + s++; + ch = seqToUnseq[yy[0]]; + unzftab[ch] += s; + + while (s > 0) + { + last++; + ll8[last] = ch; + s--; + } + + if (last >= limitLast) + { + BlockOverrun(); + } + } + else + { + char tmp; + last++; + if (last >= limitLast) + { + BlockOverrun(); + } + + tmp = yy[nextSym - 1]; + unzftab[seqToUnseq[tmp]]++; + ll8[last] = seqToUnseq[tmp]; + + /* + This loop is hammered during decompression, + hence the unrolling. + + for (j = nextSym-1; j > 0; j--) yy[j] = yy[j-1]; + */ + + j = nextSym - 1; + for (; j > 3; j -= 4) + { + yy[j] = yy[j - 1]; + yy[j - 1] = yy[j - 2]; + yy[j - 2] = yy[j - 3]; + yy[j - 3] = yy[j - 4]; + } + for (; j > 0; j--) + { + yy[j] = yy[j - 1]; + } + + yy[0] = tmp; + { + int zt, + zn, + zvec, + zj; + if (groupPos == 0) + { + groupNo++; + groupPos = BZip2Constants.G_SIZE; + } + groupPos--; + zt = selector[groupNo]; + zn = minLens[zt]; + zvec = await BsRAsync(zn, cancellationToken).ConfigureAwait(false); + while (zvec > limit[zt][zn]) + { + zn++; + { + { + while (bsLive < 1) + { + int zzi; + int thech = '\0'; + try + { + var readCount = await bsStream + .ReadAsync(singleByte, 0, 1, cancellationToken) + .ConfigureAwait(false); + thech = readCount == 0 ? '\uffff' : singleByte[0]; + } + catch (IOException) + { + CompressedStreamEOF(); + } + if (thech == '\uffff') + { + CompressedStreamEOF(); + } + zzi = thech; + bsBuff = (bsBuff << 8) | (zzi & 0xff); + bsLive += 8; + } + } + zj = (bsBuff >> (bsLive - 1)) & 1; + bsLive--; + } + zvec = (zvec << 1) | zj; + } + nextSym = perm[zt][zvec - basev[zt][zn]]; + } + } + } + } + + private async ValueTask SetupBlockAsync(CancellationToken cancellationToken) + { + Span cftab = stackalloc int[257]; + char ch; + + cftab[0] = 0; + for (i = 1; i <= 256; i++) + { + cftab[i] = unzftab[i - 1]; + } + for (i = 1; i <= 256; i++) + { + cftab[i] += cftab[i - 1]; + } + + for (i = 0; i <= last; i++) + { + ch = ll8[i]; + tt[cftab[ch]] = i; + cftab[ch]++; + } + + tPos = tt[origPtr]; + + count = 0; + i2 = 0; + ch2 = 256; /* not a char and not EOF */ + + if (blockRandomised) + { + rNToGo = 0; + rTPos = 0; + await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + else + { + SetupNoRandPartA(); + } + } + + private async ValueTask SetupRandPartAAsync(CancellationToken cancellationToken) + { + if (i2 <= last) + { + chPrev = ch2; + ch2 = ll8[tPos]; + tPos = tt[tPos]; + if (rNToGo == 0) + { + rNToGo = BZip2Constants.rNums[rTPos]; + rTPos++; + if (rTPos == 512) + { + rTPos = 0; + } + } + rNToGo--; + ch2 ^= (rNToGo == 1) ? (char)1 : (char)0; + i2++; + + currentChar = ch2; + currentState = RAND_PART_B_STATE; + mCrc.UpdateCRC(ch2); + } + else + { + EndBlock(); + await InitBlockAsync(cancellationToken).ConfigureAwait(false); + await SetupBlockAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask SetupNoRandPartAAsync(CancellationToken cancellationToken) + { + if (i2 <= last) + { + chPrev = ch2; + ch2 = ll8[tPos]; + tPos = tt[tPos]; + i2++; + + currentChar = ch2; + currentState = NO_RAND_PART_B_STATE; + mCrc.UpdateCRC(ch2); + } + else + { + EndBlock(); + await InitBlockAsync(cancellationToken).ConfigureAwait(false); + await SetupBlockAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask SetupRandPartBAsync(CancellationToken cancellationToken) + { + if (ch2 != chPrev) + { + currentState = RAND_PART_A_STATE; + count = 1; + await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + else + { + count++; + if (count >= 4) + { + z = ll8[tPos]; + tPos = tt[tPos]; + if (rNToGo == 0) + { + rNToGo = BZip2Constants.rNums[rTPos]; + rTPos++; + if (rTPos == 512) + { + rTPos = 0; + } + } + rNToGo--; + z ^= (char)((rNToGo == 1) ? 1 : 0); + j2 = 0; + currentState = RAND_PART_C_STATE; + SetupRandPartC(); + } + else + { + currentState = RAND_PART_A_STATE; + await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + } + } + + private async ValueTask SetupRandPartCAsync(CancellationToken cancellationToken) + { + if (j2 < z) + { + currentChar = ch2; + mCrc.UpdateCRC(ch2); + j2++; + } + else + { + currentState = RAND_PART_A_STATE; + i2++; + count = 0; + await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask SetupNoRandPartBAsync(CancellationToken cancellationToken) + { + if (ch2 != chPrev) + { + currentState = NO_RAND_PART_A_STATE; + count = 1; + await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + else + { + count++; + if (count >= 4) + { + z = ll8[tPos]; + tPos = tt[tPos]; + currentState = NO_RAND_PART_C_STATE; + j2 = 0; + await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false); + } + else + { + currentState = NO_RAND_PART_A_STATE; + await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + } + } + + private async ValueTask SetupNoRandPartCAsync(CancellationToken cancellationToken) + { + if (j2 < z) + { + currentChar = ch2; + mCrc.UpdateCRC(ch2); + j2++; + } + else + { + currentState = NO_RAND_PART_A_STATE; + i2++; + count = 0; + await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var c = -1; + int k; + for (k = 0; k < count; ++k) + { + cancellationToken.ThrowIfCancellationRequested(); + c = await ReadByteAsync(cancellationToken).ConfigureAwait(false); + if (c == -1) + { + break; + } + buffer[k + offset] = (byte)c; + } + return k; + } + + private async ValueTask BsSetStreamAsync(Stream f, CancellationToken cancellationToken) + { + bsStream = f; + bsLive = 0; + bsBuff = 0; + await Task.CompletedTask; + } + + private async ValueTask BsRAsync(int n, CancellationToken cancellationToken) + { + int v; + while (bsLive < n) + { + int zzi; + int thech = '\0'; + var b = ArrayPool.Shared.Rent(1); + try + { + await bsStream.ReadExactAsync(b, 0, 1, cancellationToken); + thech = (char)b[0]; + } + catch (IOException) + { + CompressedStreamEOF(); + } + finally + { + ArrayPool.Shared.Return(b); + } + if (thech == '\uffff') + { + CompressedStreamEOF(); + } + zzi = thech; + bsBuff = (bsBuff << 8) | (zzi & 0xff); + bsLive += 8; + } + + v = (bsBuff >> (bsLive - n)) & ((1 << n) - 1); + bsLive -= n; + return v; + } + + private async ValueTask BsGetUCharAsync(CancellationToken cancellationToken) => + (char)await BsRAsync(8, cancellationToken); + + private async ValueTask BsGetIntVSAsync( + int numBits, + CancellationToken cancellationToken + ) => await BsRAsync(numBits, cancellationToken); + + private async ValueTask BsGetInt32Async(CancellationToken cancellationToken) => + await BsGetintAsync(cancellationToken); + + public static async ValueTask CreateAsync( + Stream zStream, + bool decompressConcatenated, + CancellationToken cancellationToken = default + ) + { + var cbZip2InputStream = new CBZip2InputStream(); + cbZip2InputStream.decompressConcatenated = decompressConcatenated; + cbZip2InputStream.ll8 = null; + cbZip2InputStream.tt = null; + await cbZip2InputStream.BsSetStreamAsync(zStream, cancellationToken); + + if (zStream.CanSeek) + { + zStream.Seek(0, SeekOrigin.Begin); + } + + await cbZip2InputStream.InitializeAsync(true, cancellationToken); + await cbZip2InputStream.InitBlockAsync(cancellationToken); + cbZip2InputStream.SetupBlock(); + return cbZip2InputStream; + } +} diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index eedf7a39..38bc1075 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -41,7 +41,7 @@ namespace SharpCompress.Compressors.BZip2; * start of the BZIP2 stream to make it compatible with other PGP programs. */ -internal class CBZip2InputStream : Stream, IStreamStack +internal partial class CBZip2InputStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -286,39 +286,6 @@ internal class CBZip2InputStream : Stream, IStreamStack return retChar; } - public async ValueTask ReadByteAsync(CancellationToken cancellationToken) - { - if (streamEnd) - { - return -1; - } - var retChar = currentChar; - switch (currentState) - { - case START_BLOCK_STATE: - break; - case RAND_PART_A_STATE: - break; - case RAND_PART_B_STATE: - await SetupRandPartBAsync(cancellationToken).ConfigureAwait(false); - break; - case RAND_PART_C_STATE: - await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false); - break; - case NO_RAND_PART_A_STATE: - break; - case NO_RAND_PART_B_STATE: - await SetupNoRandPartBAsync(cancellationToken).ConfigureAwait(false); - break; - case NO_RAND_PART_C_STATE: - await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false); - break; - default: - break; - } - return retChar; - } - private bool Initialize(bool isFirstStream) { var magic0 = bsStream.ReadByte(); @@ -346,49 +313,6 @@ internal class CBZip2InputStream : Stream, IStreamStack return true; } - private async ValueTask InitializeAsync( - bool isFirstStream, - CancellationToken cancellationToken - ) - { - var singleByte = new byte[1]; - var read0 = await bsStream - .ReadAsync(singleByte, 0, 1, cancellationToken) - .ConfigureAwait(false); - var magic0 = read0 == 0 ? -1 : singleByte[0]; - var read1 = await bsStream - .ReadAsync(singleByte, 0, 1, cancellationToken) - .ConfigureAwait(false); - var magic1 = read1 == 0 ? -1 : singleByte[0]; - var read2 = await bsStream - .ReadAsync(singleByte, 0, 1, cancellationToken) - .ConfigureAwait(false); - var magic2 = read2 == 0 ? -1 : singleByte[0]; - if (magic0 == -1 && !isFirstStream) - { - return false; - } - if (magic0 != 'B' || magic1 != 'Z' || magic2 != 'h') - { - throw new IOException("Not a BZIP2 marked stream"); - } - var read3 = await bsStream - .ReadAsync(singleByte, 0, 1, cancellationToken) - .ConfigureAwait(false); - var magic3 = read3 == 0 ? -1 : singleByte[0]; - if (magic3 < '1' || magic3 > '9') - { - BsFinishedWithStream(); - streamEnd = true; - return false; - } - - SetDecompressStructureSizes(magic3 - '0'); - bsLive = 0; - computedCombinedCRC = 0; - return true; - } - private void InitBlock() { char magic1, @@ -456,73 +380,6 @@ internal class CBZip2InputStream : Stream, IStreamStack currentState = START_BLOCK_STATE; } - private async ValueTask InitBlockAsync(CancellationToken cancellationToken) - { - char magic1, - magic2, - magic3, - magic4; - char magic5, - magic6; - - while (true) - { - magic1 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - magic2 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - magic3 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - magic4 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - magic5 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - magic6 = await BsGetUCharAsync(cancellationToken).ConfigureAwait(false); - if ( - magic1 != 0x17 - || magic2 != 0x72 - || magic3 != 0x45 - || magic4 != 0x38 - || magic5 != 0x50 - || magic6 != 0x90 - ) - { - break; - } - - if (await CompleteAsync(cancellationToken).ConfigureAwait(false)) - { - return; - } - } - - if ( - magic1 != 0x31 - || magic2 != 0x41 - || magic3 != 0x59 - || magic4 != 0x26 - || magic5 != 0x53 - || magic6 != 0x59 - ) - { - BadBlockHeader(); - streamEnd = true; - return; - } - - storedBlockCRC = await BsGetInt32Async(cancellationToken).ConfigureAwait(false); - - if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) - { - blockRandomised = true; - } - else - { - blockRandomised = false; - } - - // currBlockNo++; - await GetAndMoveToFrontDecodeAsync(cancellationToken).ConfigureAwait(false); - - mCrc.InitialiseCRC(); - currentState = START_BLOCK_STATE; - } - private void EndBlock() { computedBlockCRC = mCrc.GetFinalCRC(); @@ -556,28 +413,6 @@ internal class CBZip2InputStream : Stream, IStreamStack return complete; } - private async ValueTask CompleteAsync(CancellationToken cancellationToken) - { - storedCombinedCRC = await BsGetInt32Async(cancellationToken).ConfigureAwait(false); - if (storedCombinedCRC != computedCombinedCRC) - { - CrcError(); - } - - var complete = - !decompressConcatenated - || !(await InitializeAsync(false, cancellationToken).ConfigureAwait(false)); - if (complete) - { - BsFinishedWithStream(); - streamEnd = true; - } - - // Look for the next .bz2 stream if decompressing - // concatenated files. - return complete; - } - private static void BlockOverrun() => Cadvise(); private static void BadBlockHeader() => Cadvise(); @@ -641,26 +476,10 @@ internal class CBZip2InputStream : Stream, IStreamStack return u; } - private async ValueTask BsGetintAsync(CancellationToken cancellationToken) - { - var u = 0; - u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); - u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); - u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); - u = (u << 8) | (await BsRAsync(8, cancellationToken).ConfigureAwait(false)); - return u; - } - private int BsGetIntVS(int numBits) => BsR(numBits); - private ValueTask BsGetIntVSAsync(int numBits, CancellationToken cancellationToken) => - BsRAsync(numBits, cancellationToken); - private int BsGetInt32() => BsGetint(); - private ValueTask BsGetInt32Async(CancellationToken cancellationToken) => - BsGetintAsync(cancellationToken); - private void HbCreateDecodeTables( int[] limit, int[] basev, @@ -853,138 +672,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask RecvDecodingTablesAsync(CancellationToken cancellationToken) - { - var len = InitCharArray(BZip2Constants.N_GROUPS, BZip2Constants.MAX_ALPHA_SIZE); - int i, - j, - t, - nGroups, - nSelectors, - alphaSize; - int minLen, - maxLen; - var inUse16 = new bool[16]; - - /* Receive the mapping table */ - for (i = 0; i < 16; i++) - { - if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) - { - inUse16[i] = true; - } - else - { - inUse16[i] = false; - } - } - - for (i = 0; i < 256; i++) - { - inUse[i] = false; - } - - for (i = 0; i < 16; i++) - { - if (inUse16[i]) - { - for (j = 0; j < 16; j++) - { - if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) - { - inUse[(i * 16) + j] = true; - } - } - } - } - - MakeMaps(); - alphaSize = nInUse + 2; - - /* Now the selectors */ - nGroups = await BsRAsync(3, cancellationToken).ConfigureAwait(false); - nSelectors = await BsRAsync(15, cancellationToken).ConfigureAwait(false); - for (i = 0; i < nSelectors; i++) - { - j = 0; - while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) - { - j++; - } - if (i < BZip2Constants.MAX_SELECTORS) - { - selectorMtf[i] = (char)j; - } - } - - nSelectors = Math.Min(nSelectors, BZip2Constants.MAX_SELECTORS); - - /* Undo the MTF values for the selectors. */ - { - var pos = new char[BZip2Constants.N_GROUPS]; - char tmp, - v; - for (v = '\0'; v < nGroups; v++) - { - pos[v] = v; - } - - for (i = 0; i < nSelectors; i++) - { - v = selectorMtf[i]; - tmp = pos[v]; - while (v > 0) - { - pos[v] = pos[v - 1]; - v--; - } - pos[0] = tmp; - selector[i] = tmp; - } - } - - /* Now the coding tables */ - for (t = 0; t < nGroups; t++) - { - var curr = await BsRAsync(5, cancellationToken).ConfigureAwait(false); - for (i = 0; i < alphaSize; i++) - { - while (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 1) - { - if (await BsRAsync(1, cancellationToken).ConfigureAwait(false) == 0) - { - curr++; - } - else - { - curr--; - } - } - len[t][i] = (char)curr; - } - } - - /* Create the Huffman decoding tables */ - for (t = 0; t < nGroups; t++) - { - minLen = 32; - maxLen = 0; - for (i = 0; i < alphaSize; i++) - { - if (len[t][i] > maxLen) - { - maxLen = len[t][i]; - } - if (len[t][i] < minLen) - { - minLen = len[t][i]; - } - } - HbCreateDecodeTables(limit[t], basev[t], perm[t], len[t], minLen, maxLen, alphaSize); - minLens[t] = minLen; - } - } - private void GetAndMoveToFrontDecode() { var yy = new char[256]; @@ -1503,46 +1190,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupBlockAsync(CancellationToken cancellationToken) - { - Span cftab = stackalloc int[257]; - char ch; - - cftab[0] = 0; - for (i = 1; i <= 256; i++) - { - cftab[i] = unzftab[i - 1]; - } - for (i = 1; i <= 256; i++) - { - cftab[i] += cftab[i - 1]; - } - - for (i = 0; i <= last; i++) - { - ch = ll8[i]; - tt[cftab[ch]] = i; - cftab[ch]++; - } - - tPos = tt[origPtr]; - - count = 0; - i2 = 0; - ch2 = 256; /* not a char and not EOF */ - - if (blockRandomised) - { - rNToGo = 0; - rTPos = 0; - await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - else - { - SetupNoRandPartA(); - } - } - private void SetupBlock() { Span cftab = stackalloc int[257]; @@ -1583,38 +1230,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupRandPartAAsync(CancellationToken cancellationToken) - { - if (i2 <= last) - { - chPrev = ch2; - ch2 = ll8[tPos]; - tPos = tt[tPos]; - if (rNToGo == 0) - { - rNToGo = BZip2Constants.rNums[rTPos]; - rTPos++; - if (rTPos == 512) - { - rTPos = 0; - } - } - rNToGo--; - ch2 ^= (rNToGo == 1) ? (char)1 : (char)0; - i2++; - - currentChar = ch2; - currentState = RAND_PART_B_STATE; - mCrc.UpdateCRC(ch2); - } - else - { - EndBlock(); - await InitBlockAsync(cancellationToken).ConfigureAwait(false); - await SetupBlockAsync(cancellationToken).ConfigureAwait(false); - } - } - private void SetupRandPartA() { if (i2 <= last) @@ -1668,27 +1283,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupNoRandPartAAsync(CancellationToken cancellationToken) - { - if (i2 <= last) - { - chPrev = ch2; - ch2 = ll8[tPos]; - tPos = tt[tPos]; - i2++; - - currentChar = ch2; - currentState = NO_RAND_PART_B_STATE; - mCrc.UpdateCRC(ch2); - } - else - { - EndBlock(); - await InitBlockAsync(cancellationToken).ConfigureAwait(false); - await SetupBlockAsync(cancellationToken).ConfigureAwait(false); - } - } - private void SetupRandPartB() { if (ch2 != chPrev) @@ -1727,44 +1321,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupRandPartBAsync(CancellationToken cancellationToken) - { - if (ch2 != chPrev) - { - currentState = RAND_PART_A_STATE; - count = 1; - await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - else - { - count++; - if (count >= 4) - { - z = ll8[tPos]; - tPos = tt[tPos]; - if (rNToGo == 0) - { - rNToGo = BZip2Constants.rNums[rTPos]; - rTPos++; - if (rTPos == 512) - { - rTPos = 0; - } - } - rNToGo--; - z ^= (char)((rNToGo == 1) ? 1 : 0); - j2 = 0; - currentState = RAND_PART_C_STATE; - SetupRandPartC(); - } - else - { - currentState = RAND_PART_A_STATE; - await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - } - } - private void SetupRandPartC() { if (j2 < z) @@ -1782,23 +1338,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupRandPartCAsync(CancellationToken cancellationToken) - { - if (j2 < z) - { - currentChar = ch2; - mCrc.UpdateCRC(ch2); - j2++; - } - else - { - currentState = RAND_PART_A_STATE; - i2++; - count = 0; - await SetupRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - } - private void SetupNoRandPartB() { if (ch2 != chPrev) @@ -1826,33 +1365,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupNoRandPartBAsync(CancellationToken cancellationToken) - { - if (ch2 != chPrev) - { - currentState = NO_RAND_PART_A_STATE; - count = 1; - await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - else - { - count++; - if (count >= 4) - { - z = ll8[tPos]; - tPos = tt[tPos]; - currentState = NO_RAND_PART_C_STATE; - j2 = 0; - await SetupNoRandPartCAsync(cancellationToken).ConfigureAwait(false); - } - else - { - currentState = NO_RAND_PART_A_STATE; - await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - } - } - private void SetupNoRandPartC() { if (j2 < z) @@ -1870,23 +1382,6 @@ internal class CBZip2InputStream : Stream, IStreamStack } } - private async ValueTask SetupNoRandPartCAsync(CancellationToken cancellationToken) - { - if (j2 < z) - { - currentChar = ch2; - mCrc.UpdateCRC(ch2); - j2++; - } - else - { - currentState = NO_RAND_PART_A_STATE; - i2++; - count = 0; - await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); - } - } - private void SetDecompressStructureSizes(int newSize100k) { if (!(0 <= newSize100k && newSize100k <= 9 && 0 <= blockSize100k && blockSize100k <= 9)) @@ -1924,28 +1419,6 @@ internal class CBZip2InputStream : Stream, IStreamStack return k; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var c = -1; - int k; - for (k = 0; k < count; ++k) - { - cancellationToken.ThrowIfCancellationRequested(); - c = await ReadByteAsync(cancellationToken).ConfigureAwait(false); - if (c == -1) - { - break; - } - buffer[k + offset] = (byte)c; - } - return k; - } - public override long Seek(long offset, SeekOrigin origin) => 0; public override void SetLength(long value) { } @@ -1967,73 +1440,4 @@ internal class CBZip2InputStream : Stream, IStreamStack get => 0; set { } } - - private async ValueTask BsSetStreamAsync(Stream f, CancellationToken cancellationToken) - { - bsStream = f; - bsLive = 0; - bsBuff = 0; - await Task.CompletedTask; - } - - private async ValueTask BsRAsync(int n, CancellationToken cancellationToken) - { - int v; - while (bsLive < n) - { - int zzi; - int thech = '\0'; - var b = ArrayPool.Shared.Rent(1); - try - { - await bsStream.ReadExactAsync(b, 0, 1, cancellationToken); - thech = (char)b[0]; - } - catch (IOException) - { - CompressedStreamEOF(); - } - finally - { - ArrayPool.Shared.Return(b); - } - if (thech == '\uffff') - { - CompressedStreamEOF(); - } - zzi = thech; - bsBuff = (bsBuff << 8) | (zzi & 0xff); - bsLive += 8; - } - - v = (bsBuff >> (bsLive - n)) & ((1 << n) - 1); - bsLive -= n; - return v; - } - - private async ValueTask BsGetUCharAsync(CancellationToken cancellationToken) => - (char)await BsRAsync(8, cancellationToken); - - public static async ValueTask CreateAsync( - Stream zStream, - bool decompressConcatenated, - CancellationToken cancellationToken = default - ) - { - var cbZip2InputStream = new CBZip2InputStream(); - cbZip2InputStream.decompressConcatenated = decompressConcatenated; - cbZip2InputStream.ll8 = null; - cbZip2InputStream.tt = null; - await cbZip2InputStream.BsSetStreamAsync(zStream, cancellationToken); - - if (zStream.CanSeek) - { - zStream.Seek(0, SeekOrigin.Begin); - } - - await cbZip2InputStream.InitializeAsync(true, cancellationToken); - await cbZip2InputStream.InitBlockAsync(cancellationToken); - cbZip2InputStream.SetupBlock(); - return cbZip2InputStream; - } } diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs new file mode 100644 index 00000000..7a67f2a3 --- /dev/null +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Deflate; + +public partial class DeflateStream +{ +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { + if (!_leaveOpen) + { + await _baseStream.DisposeAsync().ConfigureAwait(false); + } + _disposed = true; + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException("DeflateStream"); + } + await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("DeflateStream"); + } + return await _baseStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("DeflateStream"); + } + return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("DeflateStream"); + } + await _baseStream + .WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("DeflateStream"); + } + await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif +} diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs index 3fe15db6..3d54174e 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.cs @@ -33,7 +33,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Deflate; -public class DeflateStream : Stream, IStreamStack +public partial class DeflateStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -290,24 +290,6 @@ public class DeflateStream : Stream, IStreamStack } } -#if !NETFRAMEWORK && !NETSTANDARD2_0 - public override async ValueTask DisposeAsync() - { - if (!_disposed) - { -#if DEBUG_STREAMS - this.DebugDispose(typeof(DeflateStream)); -#endif - if (!_leaveOpen) - { - await _baseStream.DisposeAsync().ConfigureAwait(false); - } - _disposed = true; - } - await base.DisposeAsync().ConfigureAwait(false); - } -#endif - /// /// Flush the stream. /// @@ -320,15 +302,6 @@ public class DeflateStream : Stream, IStreamStack _baseStream.Flush(); } - public override async Task FlushAsync(CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException("DeflateStream"); - } - await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - /// /// Read data from the stream. /// @@ -365,36 +338,6 @@ public class DeflateStream : Stream, IStreamStack return _baseStream.Read(buffer, offset, count); } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("DeflateStream"); - } - return await _baseStream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("DeflateStream"); - } - return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - } -#endif - public override int ReadByte() { if (_disposed) @@ -456,36 +399,6 @@ public class DeflateStream : Stream, IStreamStack _baseStream.Write(buffer, offset, count); } - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("DeflateStream"); - } - await _baseStream - .WriteAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("DeflateStream"); - } - await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - } -#endif - public override void WriteByte(byte value) { if (_disposed) diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs new file mode 100644 index 00000000..270421bf --- /dev/null +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Deflate; + +public partial class GZipStream +{ + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException("GZipStream"); + } + await BaseStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("GZipStream"); + } + var n = await BaseStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + if (!_firstReadDone) + { + _firstReadDone = true; + FileName = BaseStream._GzipFileName; + Comment = BaseStream._GzipComment; + LastModified = BaseStream._GzipMtime; + } + return n; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("GZipStream"); + } + var n = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + + if (!_firstReadDone) + { + _firstReadDone = true; + FileName = BaseStream._GzipFileName; + Comment = BaseStream._GzipComment; + LastModified = BaseStream._GzipMtime; + } + return n; + } +#endif + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("GZipStream"); + } + if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) + { + if (BaseStream._wantCompress) + { + // first write in compression, therefore, emit the GZIP header + _headerByteCount = EmitHeader(); + } + else + { + throw new InvalidOperationException(); + } + } + + await BaseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("GZipStream"); + } + if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) + { + if (BaseStream._wantCompress) + { + // first write in compression, therefore, emit the GZIP header + _headerByteCount = EmitHeader(); + } + else + { + throw new InvalidOperationException(); + } + } + + await BaseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + public override async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + _disposed = true; + if (BaseStream != null) + { + await BaseStream.DisposeAsync().ConfigureAwait(false); + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.cs index aceacb28..cccd63f4 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.cs @@ -36,7 +36,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Deflate; -public class GZipStream : Stream, IStreamStack +public partial class GZipStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -259,15 +259,6 @@ public class GZipStream : Stream, IStreamStack BaseStream.Flush(); } - public override async Task FlushAsync(CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - await BaseStream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - /// /// Read and decompress data from the source stream. /// @@ -320,54 +311,6 @@ public class GZipStream : Stream, IStreamStack return n; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - var n = await BaseStream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - - if (!_firstReadDone) - { - _firstReadDone = true; - FileName = BaseStream._GzipFileName; - Comment = BaseStream._GzipComment; - LastModified = BaseStream._GzipMtime; - } - return n; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - var n = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - if (!_firstReadDone) - { - _firstReadDone = true; - FileName = BaseStream._GzipFileName; - Comment = BaseStream._GzipComment; - LastModified = BaseStream._GzipMtime; - } - return n; - } -#endif - /// /// Calling this method always throws a . /// @@ -427,77 +370,6 @@ public class GZipStream : Stream, IStreamStack BaseStream.Write(buffer, offset, count); } - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) - { - if (BaseStream._wantCompress) - { - // first write in compression, therefore, emit the GZIP header - _headerByteCount = EmitHeader(); - } - else - { - throw new InvalidOperationException(); - } - } - - await BaseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("GZipStream"); - } - if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined) - { - if (BaseStream._wantCompress) - { - // first write in compression, therefore, emit the GZIP header - _headerByteCount = EmitHeader(); - } - else - { - throw new InvalidOperationException(); - } - } - - await BaseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - } - - public override async ValueTask DisposeAsync() - { - if (_disposed) - { - return; - } - _disposed = true; - if (BaseStream != null) - { - await BaseStream.DisposeAsync().ConfigureAwait(false); - } -#if DEBUG_STREAMS - this.DebugDispose(typeof(GZipStream)); -#endif - await base.DisposeAsync().ConfigureAwait(false); - } -#endif - #endregion Stream methods public string? Comment diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs new file mode 100644 index 00000000..0dee2197 --- /dev/null +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Deflate; + +public partial class ZlibStream +{ + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException("ZlibStream"); + } + await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + _disposed = true; + if (_baseStream != null) + { + await _baseStream.DisposeAsync().ConfigureAwait(false); + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("ZlibStream"); + } + return await _baseStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("ZlibStream"); + } + return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_disposed) + { + throw new ObjectDisposedException("ZlibStream"); + } + await _baseStream + .WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + if (_disposed) + { + throw new ObjectDisposedException("ZlibStream"); + } + await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif +} diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs index d94ca210..971fb71c 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs @@ -34,7 +34,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Deflate; -public class ZlibStream : Stream, IStreamStack +public partial class ZlibStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -268,34 +268,6 @@ public class ZlibStream : Stream, IStreamStack _baseStream.Flush(); } - public override async Task FlushAsync(CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException("ZlibStream"); - } - await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask DisposeAsync() - { - if (_disposed) - { - return; - } - _disposed = true; - if (_baseStream != null) - { - await _baseStream.DisposeAsync().ConfigureAwait(false); - } -#if DEBUG_STREAMS - this.DebugDispose(typeof(ZlibStream)); -#endif - await base.DisposeAsync().ConfigureAwait(false); - } -#endif - /// /// Read data from the stream. /// @@ -331,36 +303,6 @@ public class ZlibStream : Stream, IStreamStack return _baseStream.Read(buffer, offset, count); } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("ZlibStream"); - } - return await _baseStream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("ZlibStream"); - } - return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - } -#endif - public override int ReadByte() { if (_disposed) @@ -415,36 +357,6 @@ public class ZlibStream : Stream, IStreamStack _baseStream.Write(buffer, offset, count); } - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_disposed) - { - throw new ObjectDisposedException("ZlibStream"); - } - await _baseStream - .WriteAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) - { - if (_disposed) - { - throw new ObjectDisposedException("ZlibStream"); - } - await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - } -#endif - public override void WriteByte(byte value) { if (_disposed) diff --git a/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.Async.cs b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.Async.cs new file mode 100644 index 00000000..8182cc57 --- /dev/null +++ b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.Async.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Deflate64; + +public sealed partial class Deflate64Stream +{ + public override async Task ReadAsync( + byte[] array, + int offset, + int count, + CancellationToken cancellationToken + ) + { + ValidateParameters(array, offset, count); + EnsureNotDisposed(); + + int bytesRead; + var currentOffset = offset; + var remainingCount = count; + + while (true) + { + bytesRead = _inflater.Inflate(array, currentOffset, remainingCount); + currentOffset += bytesRead; + remainingCount -= bytesRead; + + if (remainingCount == 0) + { + break; + } + + if (_inflater.Finished()) + { + // if we finished decompressing, we can't have anything left in the outputwindow. + Debug.Assert( + _inflater.AvailableOutput == 0, + "We should have copied all stuff out!" + ); + break; + } + + var bytes = await _stream + .ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + if (bytes <= 0) + { + break; + } + else if (bytes > _buffer.Length) + { + // The stream is either malicious or poorly implemented and returned a number of + // bytes larger than the buffer supplied to it. + throw new InvalidFormatException("Deflate64: invalid data"); + } + + _inflater.SetInput(_buffer, 0, bytes); + } + + return count - remainingCount; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + EnsureNotDisposed(); + + // InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays + // For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available + if ( + System.Runtime.InteropServices.MemoryMarshal.TryGetArray( + buffer, + out var arraySegment + ) + ) + { + // Fast path: the Memory is backed by an array + return await ReadAsync( + arraySegment.Array!, + arraySegment.Offset, + arraySegment.Count, + cancellationToken + ) + .ConfigureAwait(false); + } + else + { + // Slow path: rent a temporary array + var tempBuffer = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); + try + { + var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer); + return bytesRead; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(tempBuffer); + } + } + } +#endif +} diff --git a/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs index 083f0380..e028e58c 100644 --- a/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs +++ b/src/SharpCompress/Compressors/Deflate64/Deflate64Stream.cs @@ -14,7 +14,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Deflate64; -public sealed class Deflate64Stream : Stream, IStreamStack +public sealed partial class Deflate64Stream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -145,106 +145,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack return count - remainingCount; } - public override async Task ReadAsync( - byte[] array, - int offset, - int count, - CancellationToken cancellationToken - ) - { - ValidateParameters(array, offset, count); - EnsureNotDisposed(); - - int bytesRead; - var currentOffset = offset; - var remainingCount = count; - - while (true) - { - bytesRead = _inflater.Inflate(array, currentOffset, remainingCount); - currentOffset += bytesRead; - remainingCount -= bytesRead; - - if (remainingCount == 0) - { - break; - } - - if (_inflater.Finished()) - { - // if we finished decompressing, we can't have anything left in the outputwindow. - Debug.Assert( - _inflater.AvailableOutput == 0, - "We should have copied all stuff out!" - ); - break; - } - - var bytes = await _stream - .ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) - .ConfigureAwait(false); - if (bytes <= 0) - { - break; - } - else if (bytes > _buffer.Length) - { - // The stream is either malicious or poorly implemented and returned a number of - // bytes larger than the buffer supplied to it. - throw new InvalidFormatException("Deflate64: invalid data"); - } - - _inflater.SetInput(_buffer, 0, bytes); - } - - return count - remainingCount; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - EnsureNotDisposed(); - - // InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays - // For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available - if ( - System.Runtime.InteropServices.MemoryMarshal.TryGetArray( - buffer, - out var arraySegment - ) - ) - { - // Fast path: the Memory is backed by an array - return await ReadAsync( - arraySegment.Array!, - arraySegment.Offset, - arraySegment.Count, - cancellationToken - ) - .ConfigureAwait(false); - } - else - { - // Slow path: rent a temporary array - var tempBuffer = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); - try - { - var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken) - .ConfigureAwait(false); - tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer); - return bytesRead; - } - finally - { - System.Buffers.ArrayPool.Shared.Return(tempBuffer); - } - } - } -#endif - private void ValidateParameters(byte[] array, int offset, int count) { if (array is null) diff --git a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.Async.cs b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.Async.cs new file mode 100644 index 00000000..326eec1a --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.Async.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.LZMA; + +internal sealed partial class AesDecoderStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + if (count == 0 || mWritten == mLimit) + { + return 0; + } + + if (mUnderflow > 0) + { + return HandleUnderflow(buffer, offset, count); + } + + // Need at least 16 bytes to proceed. + if (mEnding - mOffset < 16) + { + Buffer.BlockCopy(mBuffer, mOffset, mBuffer, 0, mEnding - mOffset); + mEnding -= mOffset; + mOffset = 0; + + do + { + cancellationToken.ThrowIfCancellationRequested(); + var read = await mStream + .ReadAsync(mBuffer, mEnding, mBuffer.Length - mEnding, cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + // We are not done decoding and have less than 16 bytes. + throw new EndOfStreamException(); + } + + mEnding += read; + } while (mEnding - mOffset < 16); + } + + // We shouldn't return more data than we are limited to. + if (count > mLimit - mWritten) + { + count = (int)(mLimit - mWritten); + } + + // We cannot transform less than 16 bytes into the target buffer, + // but we also cannot return zero, so we need to handle this. + if (count < 16) + { + return HandleUnderflow(buffer, offset, count); + } + + if (count > mEnding - mOffset) + { + count = mEnding - mOffset; + } + + // Otherwise we transform directly into the target buffer. + var processed = mDecoder.TransformBlock(mBuffer, mOffset, count & ~15, buffer, offset); + mOffset += processed; + mWritten += processed; + return processed; + } +} diff --git a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs index 199f277b..685af602 100644 --- a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs @@ -9,7 +9,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA; -internal sealed class AesDecoderStream : DecoderStream2, IStreamStack +internal sealed partial class AesDecoderStream : DecoderStream2, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -285,70 +285,5 @@ internal sealed class AesDecoderStream : DecoderStream2, IStreamStack return count; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - if (count == 0 || mWritten == mLimit) - { - return 0; - } - - if (mUnderflow > 0) - { - return HandleUnderflow(buffer, offset, count); - } - - // Need at least 16 bytes to proceed. - if (mEnding - mOffset < 16) - { - Buffer.BlockCopy(mBuffer, mOffset, mBuffer, 0, mEnding - mOffset); - mEnding -= mOffset; - mOffset = 0; - - do - { - cancellationToken.ThrowIfCancellationRequested(); - var read = await mStream - .ReadAsync(mBuffer, mEnding, mBuffer.Length - mEnding, cancellationToken) - .ConfigureAwait(false); - if (read == 0) - { - // We are not done decoding and have less than 16 bytes. - throw new EndOfStreamException(); - } - - mEnding += read; - } while (mEnding - mOffset < 16); - } - - // We shouldn't return more data than we are limited to. - if (count > mLimit - mWritten) - { - count = (int)(mLimit - mWritten); - } - - // We cannot transform less than 16 bytes into the target buffer, - // but we also cannot return zero, so we need to handle this. - if (count < 16) - { - return HandleUnderflow(buffer, offset, count); - } - - if (count > mEnding - mOffset) - { - count = mEnding - mOffset; - } - - // Otherwise we transform directly into the target buffer. - var processed = mDecoder.TransformBlock(mBuffer, mOffset, count & ~15, buffer, offset); - mOffset += processed; - mWritten += processed; - return processed; - } - #endregion } diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs new file mode 100644 index 00000000..99cbd5fb --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs @@ -0,0 +1,143 @@ +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.LZMA.LZ; + +internal partial class OutWindow : IDisposable +{ + public async ValueTask ReleaseStreamAsync(CancellationToken cancellationToken = default) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + _stream = null; + } + + private async ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_stream is null) + { + return; + } + var size = _pos - _streamPos; + if (size == 0) + { + return; + } + await _stream + .WriteAsync(_buffer, _streamPos, size, cancellationToken) + .ConfigureAwait(false); + if (_pos >= _windowSize) + { + _pos = 0; + } + _streamPos = _pos; + } + + public async ValueTask CopyPendingAsync(CancellationToken cancellationToken = default) + { + if (_pendingLen < 1) + { + return; + } + var rem = _pendingLen; + var pos = (_pendingDist < _pos ? _pos : _pos + _windowSize) - _pendingDist - 1; + while (rem > 0 && HasSpace) + { + if (pos >= _windowSize) + { + pos = 0; + } + await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); + rem--; + } + _pendingLen = rem; + } + + public async ValueTask CopyBlockAsync( + int distance, + int len, + CancellationToken cancellationToken = default + ) + { + var rem = len; + var pos = (distance < _pos ? _pos : _pos + _windowSize) - distance - 1; + var targetSize = HasSpace ? (int)Math.Min(rem, _limit - _total) : 0; + var sizeUntilWindowEnd = Math.Min(_windowSize - _pos, _windowSize - pos); + var sizeUntilOverlap = Math.Abs(pos - _pos); + var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize); + if (fastSize >= 2) + { + _buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize)); + _pos += fastSize; + pos += fastSize; + _total += fastSize; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + rem -= fastSize; + } + while (rem > 0 && HasSpace) + { + if (pos >= _windowSize) + { + pos = 0; + } + await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); + rem--; + } + _pendingLen = rem; + _pendingDist = distance; + } + + public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default) + { + _buffer[_pos++] = b; + _total++; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + + public async ValueTask CopyStreamAsync( + Stream stream, + int len, + CancellationToken cancellationToken = default + ) + { + var size = len; + while (size > 0 && _pos < _windowSize && _total < _limit) + { + cancellationToken.ThrowIfCancellationRequested(); + + var curSize = _windowSize - _pos; + if (curSize > _limit - _total) + { + curSize = (int)(_limit - _total); + } + if (curSize > size) + { + curSize = size; + } + var numReadBytes = await stream + .ReadAsync(_buffer, _pos, curSize, cancellationToken) + .ConfigureAwait(false); + if (numReadBytes == 0) + { + throw new DataErrorException(); + } + size -= numReadBytes; + _pos += numReadBytes; + _total += numReadBytes; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + return len - size; + } +} diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index 0866f718..df5a74b7 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace SharpCompress.Compressors.LZMA.LZ; -internal class OutWindow : IDisposable +internal partial class OutWindow : IDisposable { private byte[] _buffer; private int _windowSize; @@ -87,12 +87,6 @@ internal class OutWindow : IDisposable _stream = null; } - public async ValueTask ReleaseStreamAsync(CancellationToken cancellationToken = default) - { - await FlushAsync(cancellationToken).ConfigureAwait(false); - _stream = null; - } - private void Flush() { if (_stream is null) @@ -112,27 +106,6 @@ internal class OutWindow : IDisposable _streamPos = _pos; } - private async ValueTask FlushAsync(CancellationToken cancellationToken = default) - { - if (_stream is null) - { - return; - } - var size = _pos - _streamPos; - if (size == 0) - { - return; - } - await _stream - .WriteAsync(_buffer, _streamPos, size, cancellationToken) - .ConfigureAwait(false); - if (_pos >= _windowSize) - { - _pos = 0; - } - _streamPos = _pos; - } - public void CopyPending() { if (_pendingLen < 1) @@ -153,26 +126,6 @@ internal class OutWindow : IDisposable _pendingLen = rem; } - public async ValueTask CopyPendingAsync(CancellationToken cancellationToken = default) - { - if (_pendingLen < 1) - { - return; - } - var rem = _pendingLen; - var pos = (_pendingDist < _pos ? _pos : _pos + _windowSize) - _pendingDist - 1; - while (rem > 0 && HasSpace) - { - if (pos >= _windowSize) - { - pos = 0; - } - await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); - rem--; - } - _pendingLen = rem; - } - public void CopyBlock(int distance, int len) { var rem = len; @@ -206,43 +159,6 @@ internal class OutWindow : IDisposable _pendingDist = distance; } - public async ValueTask CopyBlockAsync( - int distance, - int len, - CancellationToken cancellationToken = default - ) - { - var rem = len; - var pos = (distance < _pos ? _pos : _pos + _windowSize) - distance - 1; - var targetSize = HasSpace ? (int)Math.Min(rem, _limit - _total) : 0; - var sizeUntilWindowEnd = Math.Min(_windowSize - _pos, _windowSize - pos); - var sizeUntilOverlap = Math.Abs(pos - _pos); - var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize); - if (fastSize >= 2) - { - _buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize)); - _pos += fastSize; - pos += fastSize; - _total += fastSize; - if (_pos >= _windowSize) - { - await FlushAsync(cancellationToken).ConfigureAwait(false); - } - rem -= fastSize; - } - while (rem > 0 && HasSpace) - { - if (pos >= _windowSize) - { - pos = 0; - } - await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); - rem--; - } - _pendingLen = rem; - _pendingDist = distance; - } - public void PutByte(byte b) { _buffer[_pos++] = b; @@ -253,16 +169,6 @@ internal class OutWindow : IDisposable } } - public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default) - { - _buffer[_pos++] = b; - _total++; - if (_pos >= _windowSize) - { - await FlushAsync(cancellationToken).ConfigureAwait(false); - } - } - public byte GetByte(int distance) { var pos = _pos - distance - 1; @@ -303,44 +209,6 @@ internal class OutWindow : IDisposable return len - size; } - public async ValueTask CopyStreamAsync( - Stream stream, - int len, - CancellationToken cancellationToken = default - ) - { - var size = len; - while (size > 0 && _pos < _windowSize && _total < _limit) - { - cancellationToken.ThrowIfCancellationRequested(); - - var curSize = _windowSize - _pos; - if (curSize > _limit - _total) - { - curSize = (int)(_limit - _total); - } - if (curSize > size) - { - curSize = size; - } - var numReadBytes = await stream - .ReadAsync(_buffer, _pos, curSize, cancellationToken) - .ConfigureAwait(false); - if (numReadBytes == 0) - { - throw new DataErrorException(); - } - size -= numReadBytes; - _pos += numReadBytes; - _total += numReadBytes; - if (_pos >= _windowSize) - { - await FlushAsync(cancellationToken).ConfigureAwait(false); - } - } - return len - size; - } - public void SetLimit(long size) => _limit = _total + size; public bool HasSpace => _pos < _windowSize && _total < _limit; diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs new file mode 100644 index 00000000..3e674239 --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Crypto; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.LZMA; + +public sealed partial class LZipStream +{ +#if !LEGACY_DOTNET + /// + /// Asynchronously reads bytes from the current stream into a buffer. + /// + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => _stream.ReadAsync(buffer, cancellationToken); +#endif + + /// + /// Asynchronously reads bytes from the current stream into a buffer. + /// + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); + + /// + /// Asynchronously writes bytes from a buffer to the current stream. + /// + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + await _stream.WriteAsync(buffer, offset, count, cancellationToken); + _writeCount += count; + } +} diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index a7e2d572..7ff97a78 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -17,7 +17,7 @@ namespace SharpCompress.Compressors.LZMA; /// /// Stream supporting the LZIP format, as documented at http://www.nongnu.org/lzip/manual/lzip_manual.html /// -public sealed class LZipStream : Stream, IStreamStack +public sealed partial class LZipStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -167,11 +167,6 @@ public sealed class LZipStream : Stream, IStreamStack #if !LEGACY_DOTNET - public override ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) => _stream.ReadAsync(buffer, cancellationToken); - public override int Read(Span buffer) => _stream.Read(buffer); public override void Write(ReadOnlySpan buffer) @@ -194,24 +189,7 @@ public sealed class LZipStream : Stream, IStreamStack ++_writeCount; } - public override Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); - - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - cancellationToken.ThrowIfCancellationRequested(); - await _stream.WriteAsync(buffer, offset, count, cancellationToken); - _writeCount += count; - } + // Async methods moved to LZipStream.Async.cs #endregion diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs new file mode 100644 index 00000000..5cb96cd5 --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs @@ -0,0 +1,186 @@ +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.LZMA.LZ; +using SharpCompress.Compressors.LZMA.RangeCoder; + +namespace SharpCompress.Compressors.LZMA; + +public partial class Decoder : ICoder, ISetDecoderProperties +{ + public async Task CodeAsync( + Stream inStream, + Stream outStream, + long inSize, + long outSize, + ICodeProgress progress, + CancellationToken cancellationToken = default + ) + { + if (_outWindow is null) + { + CreateDictionary(); + } + _outWindow.Init(outStream); + if (outSize > 0) + { + _outWindow.SetLimit(outSize); + } + else + { + _outWindow.SetLimit(long.MaxValue - _outWindow.Total); + } + + var rangeDecoder = new RangeCoder.Decoder(); + rangeDecoder.Init(inStream); + + await CodeAsync(_dictionarySize, _outWindow, rangeDecoder, cancellationToken) + .ConfigureAwait(false); + + await _outWindow.ReleaseStreamAsync(cancellationToken).ConfigureAwait(false); + rangeDecoder.ReleaseStream(); + + _outWindow.Dispose(); + _outWindow = null; + } + + internal async ValueTask CodeAsync( + int dictionarySize, + OutWindow outWindow, + RangeCoder.Decoder rangeDecoder, + CancellationToken cancellationToken = default + ) + { + var dictionarySizeCheck = Math.Max(dictionarySize, 1); + + await outWindow.CopyPendingAsync(cancellationToken).ConfigureAwait(false); + + while (outWindow.HasSpace) + { + cancellationToken.ThrowIfCancellationRequested(); + + var posState = (uint)outWindow.Total & _posStateMask; + if ( + _isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState] + .Decode(rangeDecoder) == 0 + ) + { + byte b; + var prevByte = outWindow.GetByte(0); + if (!_state.IsCharState()) + { + b = _literalDecoder.DecodeWithMatchByte( + rangeDecoder, + (uint)outWindow.Total, + prevByte, + outWindow.GetByte((int)_rep0) + ); + } + else + { + b = _literalDecoder.DecodeNormal(rangeDecoder, (uint)outWindow.Total, prevByte); + } + await outWindow.PutByteAsync(b, cancellationToken).ConfigureAwait(false); + _state.UpdateChar(); + } + else + { + uint len; + if (_isRepDecoders[_state._index].Decode(rangeDecoder) == 1) + { + if (_isRepG0Decoders[_state._index].Decode(rangeDecoder) == 0) + { + if ( + _isRep0LongDecoders[ + (_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState + ] + .Decode(rangeDecoder) == 0 + ) + { + _state.UpdateShortRep(); + await outWindow + .PutByteAsync(outWindow.GetByte((int)_rep0), cancellationToken) + .ConfigureAwait(false); + continue; + } + } + else + { + uint distance; + if (_isRepG1Decoders[_state._index].Decode(rangeDecoder) == 0) + { + distance = _rep1; + } + else + { + if (_isRepG2Decoders[_state._index].Decode(rangeDecoder) == 0) + { + distance = _rep2; + } + else + { + distance = _rep3; + _rep3 = _rep2; + } + _rep2 = _rep1; + } + _rep1 = _rep0; + _rep0 = distance; + } + len = _repLenDecoder.Decode(rangeDecoder, posState) + Base.K_MATCH_MIN_LEN; + _state.UpdateRep(); + } + else + { + _rep3 = _rep2; + _rep2 = _rep1; + _rep1 = _rep0; + len = Base.K_MATCH_MIN_LEN + _lenDecoder.Decode(rangeDecoder, posState); + _state.UpdateMatch(); + var posSlot = _posSlotDecoder[Base.GetLenToPosState(len)].Decode(rangeDecoder); + if (posSlot >= Base.K_START_POS_MODEL_INDEX) + { + var numDirectBits = (int)((posSlot >> 1) - 1); + _rep0 = ((2 | (posSlot & 1)) << numDirectBits); + if (posSlot < Base.K_END_POS_MODEL_INDEX) + { + _rep0 += BitTreeDecoder.ReverseDecode( + _posDecoders, + _rep0 - posSlot - 1, + rangeDecoder, + numDirectBits + ); + } + else + { + _rep0 += ( + rangeDecoder.DecodeDirectBits(numDirectBits - Base.K_NUM_ALIGN_BITS) + << Base.K_NUM_ALIGN_BITS + ); + _rep0 += _posAlignDecoder.ReverseDecode(rangeDecoder); + } + } + else + { + _rep0 = posSlot; + } + } + if (_rep0 >= outWindow.Total || _rep0 >= dictionarySizeCheck) + { + if (_rep0 == 0xFFFFFFFF) + { + return true; + } + throw new DataErrorException(); + } + await outWindow + .CopyBlockAsync((int)_rep0, (int)len, cancellationToken) + .ConfigureAwait(false); + } + } + return false; + } +} diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index 0fb39626..4a7659a5 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs @@ -9,7 +9,7 @@ using SharpCompress.Compressors.LZMA.RangeCoder; namespace SharpCompress.Compressors.LZMA; -public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream +public partial class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream { private class LenDecoder { @@ -314,42 +314,6 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream _outWindow = null; } - public async System.Threading.Tasks.Task CodeAsync( - Stream inStream, - Stream outStream, - long inSize, - long outSize, - ICodeProgress progress, - System.Threading.CancellationToken cancellationToken = default - ) - { - if (_outWindow is null) - { - CreateDictionary(); - } - _outWindow.Init(outStream); - if (outSize > 0) - { - _outWindow.SetLimit(outSize); - } - else - { - _outWindow.SetLimit(long.MaxValue - _outWindow.Total); - } - - var rangeDecoder = new RangeCoder.Decoder(); - rangeDecoder.Init(inStream); - - await CodeAsync(_dictionarySize, _outWindow, rangeDecoder, cancellationToken) - .ConfigureAwait(false); - - await _outWindow.ReleaseStreamAsync(cancellationToken).ConfigureAwait(false); - rangeDecoder.ReleaseStream(); - - _outWindow.Dispose(); - _outWindow = null; - } - internal bool Code(int dictionarySize, OutWindow outWindow, RangeCoder.Decoder rangeDecoder) { var dictionarySizeCheck = Math.Max(dictionarySize, 1); @@ -476,143 +440,6 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream return false; } - internal async ValueTask CodeAsync( - int dictionarySize, - OutWindow outWindow, - RangeCoder.Decoder rangeDecoder, - System.Threading.CancellationToken cancellationToken = default - ) - { - var dictionarySizeCheck = Math.Max(dictionarySize, 1); - - await outWindow.CopyPendingAsync(cancellationToken).ConfigureAwait(false); - - while (outWindow.HasSpace) - { - cancellationToken.ThrowIfCancellationRequested(); - - var posState = (uint)outWindow.Total & _posStateMask; - if ( - _isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState] - .Decode(rangeDecoder) == 0 - ) - { - byte b; - var prevByte = outWindow.GetByte(0); - if (!_state.IsCharState()) - { - b = _literalDecoder.DecodeWithMatchByte( - rangeDecoder, - (uint)outWindow.Total, - prevByte, - outWindow.GetByte((int)_rep0) - ); - } - else - { - b = _literalDecoder.DecodeNormal(rangeDecoder, (uint)outWindow.Total, prevByte); - } - await outWindow.PutByteAsync(b, cancellationToken).ConfigureAwait(false); - _state.UpdateChar(); - } - else - { - uint len; - if (_isRepDecoders[_state._index].Decode(rangeDecoder) == 1) - { - if (_isRepG0Decoders[_state._index].Decode(rangeDecoder) == 0) - { - if ( - _isRep0LongDecoders[ - (_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState - ] - .Decode(rangeDecoder) == 0 - ) - { - _state.UpdateShortRep(); - await outWindow - .PutByteAsync(outWindow.GetByte((int)_rep0), cancellationToken) - .ConfigureAwait(false); - continue; - } - } - else - { - uint distance; - if (_isRepG1Decoders[_state._index].Decode(rangeDecoder) == 0) - { - distance = _rep1; - } - else - { - if (_isRepG2Decoders[_state._index].Decode(rangeDecoder) == 0) - { - distance = _rep2; - } - else - { - distance = _rep3; - _rep3 = _rep2; - } - _rep2 = _rep1; - } - _rep1 = _rep0; - _rep0 = distance; - } - len = _repLenDecoder.Decode(rangeDecoder, posState) + Base.K_MATCH_MIN_LEN; - _state.UpdateRep(); - } - else - { - _rep3 = _rep2; - _rep2 = _rep1; - _rep1 = _rep0; - len = Base.K_MATCH_MIN_LEN + _lenDecoder.Decode(rangeDecoder, posState); - _state.UpdateMatch(); - var posSlot = _posSlotDecoder[Base.GetLenToPosState(len)].Decode(rangeDecoder); - if (posSlot >= Base.K_START_POS_MODEL_INDEX) - { - var numDirectBits = (int)((posSlot >> 1) - 1); - _rep0 = ((2 | (posSlot & 1)) << numDirectBits); - if (posSlot < Base.K_END_POS_MODEL_INDEX) - { - _rep0 += BitTreeDecoder.ReverseDecode( - _posDecoders, - _rep0 - posSlot - 1, - rangeDecoder, - numDirectBits - ); - } - else - { - _rep0 += ( - rangeDecoder.DecodeDirectBits(numDirectBits - Base.K_NUM_ALIGN_BITS) - << Base.K_NUM_ALIGN_BITS - ); - _rep0 += _posAlignDecoder.ReverseDecode(rangeDecoder); - } - } - else - { - _rep0 = posSlot; - } - } - if (_rep0 >= outWindow.Total || _rep0 >= dictionarySizeCheck) - { - if (_rep0 == 0xFFFFFFFF) - { - return true; - } - throw new DataErrorException(); - } - await outWindow - .CopyBlockAsync((int)_rep0, (int)len, cancellationToken) - .ConfigureAwait(false); - } - } - return false; - } - public void SetDecoderProperties(byte[] properties) { if (properties.Length < 1) diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs new file mode 100644 index 00000000..0630de0e --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -0,0 +1,332 @@ +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.LZMA; + +public partial class LzmaStream +{ + private async ValueTask DecodeChunkHeaderAsync(CancellationToken cancellationToken = default) + { + var controlBuffer = new byte[1]; + await _inputStream + .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + var control = controlBuffer[0]; + _inputPosition++; + + if (control == 0x00) + { + _endReached = true; + return; + } + + if (control >= 0xE0 || control == 0x01) + { + _needProps = true; + _needDictReset = false; + _outWindow.Reset(); + } + else if (_needDictReset) + { + throw new DataErrorException(); + } + + if (control >= 0x80) + { + _uncompressedChunk = false; + + _availableBytes = (control & 0x1F) << 16; + var buffer = new byte[2]; + await _inputStream + .ReadExactAsync(buffer, 0, 2, cancellationToken) + .ConfigureAwait(false); + _availableBytes += (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + + await _inputStream + .ReadExactAsync(buffer, 0, 2, cancellationToken) + .ConfigureAwait(false); + _rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + + if (control >= 0xC0) + { + _needProps = false; + await _inputStream + .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + Properties[0] = controlBuffer[0]; + _inputPosition++; + + _decoder = new Decoder(); + _decoder.SetDecoderProperties(Properties); + } + else if (_needProps) + { + throw new DataErrorException(); + } + else if (control >= 0xA0) + { + _decoder = new Decoder(); + _decoder.SetDecoderProperties(Properties); + } + + _rangeDecoder.Init(_inputStream); + } + else if (control > 0x02) + { + throw new DataErrorException(); + } + else + { + _uncompressedChunk = true; + var buffer = new byte[2]; + await _inputStream + .ReadExactAsync(buffer, 0, 2, cancellationToken) + .ConfigureAwait(false); + _availableBytes = (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + } + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_endReached) + { + return 0; + } + + var total = 0; + while (total < count) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_availableBytes == 0) + { + if (_isLzma2) + { + await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _endReached = true; + } + if (_endReached) + { + break; + } + } + + var toProcess = count - total; + if (toProcess > _availableBytes) + { + toProcess = (int)_availableBytes; + } + + _outWindow.SetLimit(toProcess); + if (_uncompressedChunk) + { + _inputPosition += await _outWindow + .CopyStreamAsync(_inputStream, toProcess, cancellationToken) + .ConfigureAwait(false); + } + else if ( + await _decoder + .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) + .ConfigureAwait(false) + && _outputSize < 0 + ) + { + _availableBytes = _outWindow.AvailableBytes; + } + + var read = _outWindow.Read(buffer, offset, toProcess); + total += read; + offset += read; + _position += read; + _availableBytes -= read; + + if (_availableBytes == 0 && !_uncompressedChunk) + { + if ( + !_rangeDecoder.IsFinished + || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) + ) + { + _outWindow.SetLimit(toProcess + 1); + if ( + !await _decoder + .CodeAsync( + _dictionarySize, + _outWindow, + _rangeDecoder, + cancellationToken + ) + .ConfigureAwait(false) + ) + { + _rangeDecoder.ReleaseStream(); + throw new DataErrorException(); + } + } + + _rangeDecoder.ReleaseStream(); + + _inputPosition += _rangeDecoder._total; + if (_outWindow.HasPending) + { + throw new DataErrorException(); + } + } + } + + if (_endReached) + { + if (_inputSize >= 0 && _inputPosition != _inputSize) + { + throw new DataErrorException(); + } + if (_outputSize >= 0 && _position != _outputSize) + { + throw new DataErrorException(); + } + } + + return total; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_endReached) + { + return 0; + } + + var total = 0; + var offset = 0; + var count = buffer.Length; + while (total < count) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_availableBytes == 0) + { + if (_isLzma2) + { + await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _endReached = true; + } + if (_endReached) + { + break; + } + } + + var toProcess = count - total; + if (toProcess > _availableBytes) + { + toProcess = (int)_availableBytes; + } + + _outWindow.SetLimit(toProcess); + if (_uncompressedChunk) + { + _inputPosition += await _outWindow + .CopyStreamAsync(_inputStream, toProcess, cancellationToken) + .ConfigureAwait(false); + } + else if ( + await _decoder + .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) + .ConfigureAwait(false) + && _outputSize < 0 + ) + { + _availableBytes = _outWindow.AvailableBytes; + } + + var read = _outWindow.Read(buffer, offset, toProcess); + total += read; + offset += read; + _position += read; + _availableBytes -= read; + + if (_availableBytes == 0 && !_uncompressedChunk) + { + if ( + !_rangeDecoder.IsFinished + || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) + ) + { + _outWindow.SetLimit(toProcess + 1); + if ( + !await _decoder + .CodeAsync( + _dictionarySize, + _outWindow, + _rangeDecoder, + cancellationToken + ) + .ConfigureAwait(false) + ) + { + _rangeDecoder.ReleaseStream(); + throw new DataErrorException(); + } + } + + _rangeDecoder.ReleaseStream(); + + _inputPosition += _rangeDecoder._total; + if (_outWindow.HasPending) + { + throw new DataErrorException(); + } + } + } + + if (_endReached) + { + if (_inputSize >= 0 && _inputPosition != _inputSize) + { + throw new DataErrorException(); + } + if (_outputSize >= 0 && _position != _outputSize) + { + throw new DataErrorException(); + } + } + + return total; + } +#endif + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + return Task.CompletedTask; + } +} diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 2ddfdd8c..0351936f 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -10,7 +10,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA; -public class LzmaStream : Stream, IStreamStack +public partial class LzmaStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -445,90 +445,6 @@ public class LzmaStream : Stream, IStreamStack } } - private async ValueTask DecodeChunkHeaderAsync(CancellationToken cancellationToken = default) - { - var controlBuffer = new byte[1]; - await _inputStream - .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - var control = controlBuffer[0]; - _inputPosition++; - - if (control == 0x00) - { - _endReached = true; - return; - } - - if (control >= 0xE0 || control == 0x01) - { - _needProps = true; - _needDictReset = false; - _outWindow.Reset(); - } - else if (_needDictReset) - { - throw new DataErrorException(); - } - - if (control >= 0x80) - { - _uncompressedChunk = false; - - _availableBytes = (control & 0x1F) << 16; - var buffer = new byte[2]; - await _inputStream - .ReadExactAsync(buffer, 0, 2, cancellationToken) - .ConfigureAwait(false); - _availableBytes += (buffer[0] << 8) + buffer[1] + 1; - _inputPosition += 2; - - await _inputStream - .ReadExactAsync(buffer, 0, 2, cancellationToken) - .ConfigureAwait(false); - _rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1; - _inputPosition += 2; - - if (control >= 0xC0) - { - _needProps = false; - await _inputStream - .ReadExactAsync(controlBuffer, 0, 1, cancellationToken) - .ConfigureAwait(false); - Properties[0] = controlBuffer[0]; - _inputPosition++; - - _decoder = new Decoder(); - _decoder.SetDecoderProperties(Properties); - } - else if (_needProps) - { - throw new DataErrorException(); - } - else if (control >= 0xA0) - { - _decoder = new Decoder(); - _decoder.SetDecoderProperties(Properties); - } - - _rangeDecoder.Init(_inputStream); - } - else if (control > 0x02) - { - throw new DataErrorException(); - } - else - { - _uncompressedChunk = true; - var buffer = new byte[2]; - await _inputStream - .ReadExactAsync(buffer, 0, 2, cancellationToken) - .ConfigureAwait(false); - _availableBytes = (buffer[0] << 8) + buffer[1] + 1; - _inputPosition += 2; - } - } - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); @@ -541,241 +457,5 @@ public class LzmaStream : Stream, IStreamStack } } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (_endReached) - { - return 0; - } - - var total = 0; - while (total < count) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_availableBytes == 0) - { - if (_isLzma2) - { - await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); - } - else - { - _endReached = true; - } - if (_endReached) - { - break; - } - } - - var toProcess = count - total; - if (toProcess > _availableBytes) - { - toProcess = (int)_availableBytes; - } - - _outWindow.SetLimit(toProcess); - if (_uncompressedChunk) - { - _inputPosition += await _outWindow - .CopyStreamAsync(_inputStream, toProcess, cancellationToken) - .ConfigureAwait(false); - } - else if ( - await _decoder - .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) - .ConfigureAwait(false) - && _outputSize < 0 - ) - { - _availableBytes = _outWindow.AvailableBytes; - } - - var read = _outWindow.Read(buffer, offset, toProcess); - total += read; - offset += read; - _position += read; - _availableBytes -= read; - - if (_availableBytes == 0 && !_uncompressedChunk) - { - if ( - !_rangeDecoder.IsFinished - || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) - ) - { - _outWindow.SetLimit(toProcess + 1); - if ( - !await _decoder - .CodeAsync( - _dictionarySize, - _outWindow, - _rangeDecoder, - cancellationToken - ) - .ConfigureAwait(false) - ) - { - _rangeDecoder.ReleaseStream(); - throw new DataErrorException(); - } - } - - _rangeDecoder.ReleaseStream(); - - _inputPosition += _rangeDecoder._total; - if (_outWindow.HasPending) - { - throw new DataErrorException(); - } - } - } - - if (_endReached) - { - if (_inputSize >= 0 && _inputPosition != _inputSize) - { - throw new DataErrorException(); - } - if (_outputSize >= 0 && _position != _outputSize) - { - throw new DataErrorException(); - } - } - - return total; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (_endReached) - { - return 0; - } - - var total = 0; - var offset = 0; - var count = buffer.Length; - while (total < count) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_availableBytes == 0) - { - if (_isLzma2) - { - await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); - } - else - { - _endReached = true; - } - if (_endReached) - { - break; - } - } - - var toProcess = count - total; - if (toProcess > _availableBytes) - { - toProcess = (int)_availableBytes; - } - - _outWindow.SetLimit(toProcess); - if (_uncompressedChunk) - { - _inputPosition += await _outWindow - .CopyStreamAsync(_inputStream, toProcess, cancellationToken) - .ConfigureAwait(false); - } - else if ( - await _decoder - .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) - .ConfigureAwait(false) - && _outputSize < 0 - ) - { - _availableBytes = _outWindow.AvailableBytes; - } - - var read = _outWindow.Read(buffer, offset, toProcess); - total += read; - offset += read; - _position += read; - _availableBytes -= read; - - if (_availableBytes == 0 && !_uncompressedChunk) - { - if ( - !_rangeDecoder.IsFinished - || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) - ) - { - _outWindow.SetLimit(toProcess + 1); - if ( - !await _decoder - .CodeAsync( - _dictionarySize, - _outWindow, - _rangeDecoder, - cancellationToken - ) - .ConfigureAwait(false) - ) - { - _rangeDecoder.ReleaseStream(); - throw new DataErrorException(); - } - } - - _rangeDecoder.ReleaseStream(); - - _inputPosition += _rangeDecoder._total; - if (_outWindow.HasPending) - { - throw new DataErrorException(); - } - } - } - - if (_endReached) - { - if (_inputSize >= 0 && _inputPosition != _inputSize) - { - throw new DataErrorException(); - } - if (_outputSize >= 0 && _position != _outputSize) - { - throw new DataErrorException(); - } - } - - return total; - } -#endif - - public override Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - cancellationToken.ThrowIfCancellationRequested(); - Write(buffer, offset, count); - return Task.CompletedTask; - } - public byte[] Properties { get; } = new byte[5]; } diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs new file mode 100644 index 00000000..8cf2d974 --- /dev/null +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.LZMA.Utilites; + +internal partial class CrcBuilderStream : Stream, IStreamStack +{ + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_mFinished) + { + throw new InvalidOperationException("CRC calculation has been finished."); + } + + Processed += count; + _mCrc = Crc.Update(_mCrc, buffer, offset, count); + await _mTarget.WriteAsync(buffer, offset, count, cancellationToken); + } +} diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs index 015ff744..14c2b773 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs @@ -6,7 +6,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA.Utilites; -internal class CrcBuilderStream : Stream, IStreamStack +internal partial class CrcBuilderStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -103,22 +103,4 @@ internal class CrcBuilderStream : Stream, IStreamStack _mCrc = Crc.Update(_mCrc, buffer, offset, count); _mTarget.Write(buffer, offset, count); } - - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - if (_mFinished) - { - throw new InvalidOperationException("CRC calculation has been finished."); - } - - Processed += count; - _mCrc = Crc.Update(_mCrc, buffer, offset, count); - await _mTarget.WriteAsync(buffer, offset, count, cancellationToken); - } } diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs new file mode 100644 index 00000000..95966f20 --- /dev/null +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs @@ -0,0 +1,361 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Lzw +{ + public partial class LzwStream + { + /// + /// Asynchronously checks if the stream is an LZW stream + /// + /// The stream to read from + /// Cancellation token + /// True if the stream is an LZW stream, false otherwise + public static async ValueTask IsLzwStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + try + { + byte[] hdr = new byte[LzwConstants.HDR_SIZE]; + + int result = await stream.ReadAsync(hdr, 0, hdr.Length, cancellationToken); + + // Check the magic marker + if (result < 0) + throw new IncompleteArchiveException("Failed to read LZW header"); + + if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) + { + throw new IncompleteArchiveException( + String.Format( + "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", + hdr[0], + hdr[1] + ) + ); + } + } + catch (Exception) + { + return false; + } + return true; + } + + /// + /// Reads decompressed data asynchronously into the provided buffer byte array + /// + /// The array to read and decompress data into + /// The offset indicating where the data should be placed + /// The number of bytes to decompress + /// Cancellation token + /// The number of bytes read. Zero signals the end of stream + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (!headerParsed) + await ParseHeaderAsync(cancellationToken).ConfigureAwait(false); + + if (eof) + return 0; + + int start = offset; + + int[] lTabPrefix = tabPrefix; + byte[] lTabSuffix = tabSuffix; + byte[] lStack = stack; + int lNBits = nBits; + int lMaxCode = maxCode; + int lMaxMaxCode = maxMaxCode; + int lBitMask = bitMask; + int lOldCode = oldCode; + byte lFinChar = finChar; + int lStackP = stackP; + int lFreeEnt = freeEnt; + byte[] lData = data; + int lBitPos = bitPos; + + int sSize = lStack.Length - lStackP; + if (sSize > 0) + { + int num = (sSize >= count) ? count : sSize; + Array.Copy(lStack, lStackP, buffer, offset, num); + offset += num; + count -= num; + lStackP += num; + } + + if (count == 0) + { + stackP = lStackP; + return offset - start; + } + + MainLoop: + do + { + if (end < EXTRA) + { + await FillAsync(cancellationToken).ConfigureAwait(false); + } + + int bitIn = (got > 0) ? (end - end % lNBits) << 3 : (end << 3) - (lNBits - 1); + + while (lBitPos < bitIn) + { + if (count == 0) + { + nBits = lNBits; + maxCode = lMaxCode; + maxMaxCode = lMaxMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; + + return offset - start; + } + + if (lFreeEnt > lMaxCode) + { + int nBytes = lNBits << 3; + lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; + + lNBits++; + lMaxCode = (lNBits == maxBits) ? lMaxMaxCode : (1 << lNBits) - 1; + + lBitMask = (1 << lNBits) - 1; + lBitPos = ResetBuf(lBitPos); + goto MainLoop; + } + + int pos = lBitPos >> 3; + int code = + ( + ( + (lData[pos] & 0xFF) + | ((lData[pos + 1] & 0xFF) << 8) + | ((lData[pos + 2] & 0xFF) << 16) + ) >> (lBitPos & 0x7) + ) & lBitMask; + + lBitPos += lNBits; + + if (lOldCode == -1) + { + if (code >= 256) + throw new IncompleteArchiveException( + "corrupt input: " + code + " > 255" + ); + + lFinChar = (byte)(lOldCode = code); + buffer[offset++] = lFinChar; + count--; + continue; + } + + if (code == TBL_CLEAR && blockMode) + { + Array.Copy(zeros, 0, lTabPrefix, 0, zeros.Length); + lFreeEnt = TBL_FIRST - 1; + + int nBytes = lNBits << 3; + lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; + lNBits = LzwConstants.INIT_BITS; + lMaxCode = (1 << lNBits) - 1; + lBitMask = lMaxCode; + + lBitPos = ResetBuf(lBitPos); + goto MainLoop; + } + + int inCode = code; + lStackP = lStack.Length; + + if (code >= lFreeEnt) + { + if (code > lFreeEnt) + { + throw new IncompleteArchiveException( + "corrupt input: code=" + code + ", freeEnt=" + lFreeEnt + ); + } + + lStack[--lStackP] = lFinChar; + code = lOldCode; + } + + while (code >= 256) + { + lStack[--lStackP] = lTabSuffix[code]; + code = lTabPrefix[code]; + } + + lFinChar = lTabSuffix[code]; + buffer[offset++] = lFinChar; + count--; + + sSize = lStack.Length - lStackP; + int num = (sSize >= count) ? count : sSize; + Array.Copy(lStack, lStackP, buffer, offset, num); + offset += num; + count -= num; + lStackP += num; + + if (lFreeEnt < lMaxMaxCode) + { + lTabPrefix[lFreeEnt] = lOldCode; + lTabSuffix[lFreeEnt] = lFinChar; + lFreeEnt++; + } + + lOldCode = inCode; + + if (count == 0) + { + nBits = lNBits; + maxCode = lMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; + + return offset - start; + } + } + + lBitPos = ResetBuf(lBitPos); + } while (got > 0); + + nBits = lNBits; + maxCode = lMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; + + eof = true; + return offset - start; + } + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + /// + /// Reads decompressed data asynchronously into the provided buffer + /// + /// The memory to read and decompress data into + /// Cancellation token + /// The number of bytes read. Zero signals the end of stream + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (buffer.IsEmpty) + { + return 0; + } + + byte[] array = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); + try + { + int read = await ReadAsync(array, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + array.AsSpan(0, read).CopyTo(buffer.Span); + return read; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(array); + } + } +#endif + + private async ValueTask FillAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + got = await baseInputStream + .ReadAsync(data, end, data.Length - 1 - end, cancellationToken) + .ConfigureAwait(false); + if (got > 0) + { + end += got; + } + } + + private async ValueTask ParseHeaderAsync(CancellationToken cancellationToken) + { + headerParsed = true; + + byte[] hdr = new byte[LzwConstants.HDR_SIZE]; + + int result = await baseInputStream + .ReadAsync(hdr, 0, hdr.Length, cancellationToken) + .ConfigureAwait(false); + + if (result < 0) + throw new IncompleteArchiveException("Failed to read LZW header"); + + if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) + { + throw new IncompleteArchiveException( + String.Format( + "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", + hdr[0], + hdr[1] + ) + ); + } + + blockMode = (hdr[2] & LzwConstants.BLOCK_MODE_MASK) > 0; + maxBits = hdr[2] & LzwConstants.BIT_MASK; + + if (maxBits > LzwConstants.MAX_BITS) + { + throw new ArchiveException( + "Stream compressed with " + + maxBits + + " bits, but decompression can only handle " + + LzwConstants.MAX_BITS + + " bits." + ); + } + + if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) + { + throw new ArchiveException("Unsupported bits set in the header."); + } + + maxMaxCode = 1 << maxBits; + nBits = LzwConstants.INIT_BITS; + maxCode = (1 << nBits) - 1; + bitMask = maxCode; + oldCode = -1; + finChar = 0; + freeEnt = blockMode ? TBL_FIRST : 256; + + tabPrefix = new int[1 << maxBits]; + tabSuffix = new byte[1 << maxBits]; + stack = new byte[1 << maxBits]; + stackP = stack.Length; + + for (int idx = 255; idx >= 0; idx--) + tabSuffix[idx] = (byte)idx; + } + } +} diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.cs index ec29263d..2e9ce905 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwStream.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.cs @@ -45,7 +45,7 @@ namespace SharpCompress.Compressors.Lzw /// } /// /// - public class LzwStream : Stream, IStreamStack + public partial class LzwStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -97,45 +97,6 @@ namespace SharpCompress.Compressors.Lzw return true; } - /// - /// Asynchronously checks if the stream is an LZW stream - /// - /// The stream to read from - /// Cancellation token - /// True if the stream is an LZW stream, false otherwise - public static async ValueTask IsLzwStreamAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - try - { - byte[] hdr = new byte[LzwConstants.HDR_SIZE]; - - int result = await stream.ReadAsync(hdr, 0, hdr.Length, cancellationToken); - - // Check the magic marker - if (result < 0) - throw new IncompleteArchiveException("Failed to read LZW header"); - - if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) - { - throw new IncompleteArchiveException( - String.Format( - "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", - hdr[0], - hdr[1] - ) - ); - } - } - catch (Exception) - { - return false; - } - return true; - } - /// /// Gets or sets a flag indicating ownership of underlying stream. /// When the flag is true will close the underlying stream also. @@ -410,244 +371,6 @@ namespace SharpCompress.Compressors.Lzw return offset - start; } - /// - /// Reads decompressed data asynchronously into the provided buffer byte array - /// - /// The array to read and decompress data into - /// The offset indicating where the data should be placed - /// The number of bytes to decompress - /// Cancellation token - /// The number of bytes read. Zero signals the end of stream - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (!headerParsed) - await ParseHeaderAsync(cancellationToken).ConfigureAwait(false); - - if (eof) - return 0; - - int start = offset; - - int[] lTabPrefix = tabPrefix; - byte[] lTabSuffix = tabSuffix; - byte[] lStack = stack; - int lNBits = nBits; - int lMaxCode = maxCode; - int lMaxMaxCode = maxMaxCode; - int lBitMask = bitMask; - int lOldCode = oldCode; - byte lFinChar = finChar; - int lStackP = stackP; - int lFreeEnt = freeEnt; - byte[] lData = data; - int lBitPos = bitPos; - - int sSize = lStack.Length - lStackP; - if (sSize > 0) - { - int num = (sSize >= count) ? count : sSize; - Array.Copy(lStack, lStackP, buffer, offset, num); - offset += num; - count -= num; - lStackP += num; - } - - if (count == 0) - { - stackP = lStackP; - return offset - start; - } - - MainLoop: - do - { - if (end < EXTRA) - { - await FillAsync(cancellationToken).ConfigureAwait(false); - } - - int bitIn = (got > 0) ? (end - end % lNBits) << 3 : (end << 3) - (lNBits - 1); - - while (lBitPos < bitIn) - { - if (count == 0) - { - nBits = lNBits; - maxCode = lMaxCode; - maxMaxCode = lMaxMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - return offset - start; - } - - if (lFreeEnt > lMaxCode) - { - int nBytes = lNBits << 3; - lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; - - lNBits++; - lMaxCode = (lNBits == maxBits) ? lMaxMaxCode : (1 << lNBits) - 1; - - lBitMask = (1 << lNBits) - 1; - lBitPos = ResetBuf(lBitPos); - goto MainLoop; - } - - int pos = lBitPos >> 3; - int code = - ( - ( - (lData[pos] & 0xFF) - | ((lData[pos + 1] & 0xFF) << 8) - | ((lData[pos + 2] & 0xFF) << 16) - ) >> (lBitPos & 0x7) - ) & lBitMask; - - lBitPos += lNBits; - - if (lOldCode == -1) - { - if (code >= 256) - throw new IncompleteArchiveException( - "corrupt input: " + code + " > 255" - ); - - lFinChar = (byte)(lOldCode = code); - buffer[offset++] = lFinChar; - count--; - continue; - } - - if (code == TBL_CLEAR && blockMode) - { - Array.Copy(zeros, 0, lTabPrefix, 0, zeros.Length); - lFreeEnt = TBL_FIRST - 1; - - int nBytes = lNBits << 3; - lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; - lNBits = LzwConstants.INIT_BITS; - lMaxCode = (1 << lNBits) - 1; - lBitMask = lMaxCode; - - lBitPos = ResetBuf(lBitPos); - goto MainLoop; - } - - int inCode = code; - lStackP = lStack.Length; - - if (code >= lFreeEnt) - { - if (code > lFreeEnt) - { - throw new IncompleteArchiveException( - "corrupt input: code=" + code + ", freeEnt=" + lFreeEnt - ); - } - - lStack[--lStackP] = lFinChar; - code = lOldCode; - } - - while (code >= 256) - { - lStack[--lStackP] = lTabSuffix[code]; - code = lTabPrefix[code]; - } - - lFinChar = lTabSuffix[code]; - buffer[offset++] = lFinChar; - count--; - - sSize = lStack.Length - lStackP; - int num = (sSize >= count) ? count : sSize; - Array.Copy(lStack, lStackP, buffer, offset, num); - offset += num; - count -= num; - lStackP += num; - - if (lFreeEnt < lMaxMaxCode) - { - lTabPrefix[lFreeEnt] = lOldCode; - lTabSuffix[lFreeEnt] = lFinChar; - lFreeEnt++; - } - - lOldCode = inCode; - - if (count == 0) - { - nBits = lNBits; - maxCode = lMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - return offset - start; - } - } - - lBitPos = ResetBuf(lBitPos); - } while (got > 0); - - nBits = lNBits; - maxCode = lMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - eof = true; - return offset - start; - } - -#if !NETFRAMEWORK && !NETSTANDARD2_0 - /// - /// Reads decompressed data asynchronously into the provided buffer - /// - /// The memory to read and decompress data into - /// Cancellation token - /// The number of bytes read. Zero signals the end of stream - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (buffer.IsEmpty) - { - return 0; - } - - byte[] array = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); - try - { - int read = await ReadAsync(array, 0, buffer.Length, cancellationToken) - .ConfigureAwait(false); - array.AsSpan(0, read).CopyTo(buffer.Span); - return read; - } - finally - { - System.Buffers.ArrayPool.Shared.Return(array); - } - } -#endif - /// /// Moves the unread data in the buffer to the beginning and resets /// the pointers. @@ -671,18 +394,6 @@ namespace SharpCompress.Compressors.Lzw } } - private async ValueTask FillAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - got = await baseInputStream - .ReadAsync(data, end, data.Length - 1 - end, cancellationToken) - .ConfigureAwait(false); - if (got > 0) - { - end += got; - } - } - private void ParseHeader() { headerParsed = true; @@ -744,66 +455,6 @@ namespace SharpCompress.Compressors.Lzw tabSuffix[idx] = (byte)idx; } - private async ValueTask ParseHeaderAsync(CancellationToken cancellationToken) - { - headerParsed = true; - - byte[] hdr = new byte[LzwConstants.HDR_SIZE]; - - int result = await baseInputStream - .ReadAsync(hdr, 0, hdr.Length, cancellationToken) - .ConfigureAwait(false); - - if (result < 0) - throw new IncompleteArchiveException("Failed to read LZW header"); - - if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) - { - throw new IncompleteArchiveException( - String.Format( - "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", - hdr[0], - hdr[1] - ) - ); - } - - blockMode = (hdr[2] & LzwConstants.BLOCK_MODE_MASK) > 0; - maxBits = hdr[2] & LzwConstants.BIT_MASK; - - if (maxBits > LzwConstants.MAX_BITS) - { - throw new ArchiveException( - "Stream compressed with " - + maxBits - + " bits, but decompression can only handle " - + LzwConstants.MAX_BITS - + " bits." - ); - } - - if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) - { - throw new ArchiveException("Unsupported bits set in the header."); - } - - maxMaxCode = 1 << maxBits; - nBits = LzwConstants.INIT_BITS; - maxCode = (1 << nBits) - 1; - bitMask = maxCode; - oldCode = -1; - finChar = 0; - freeEnt = blockMode ? TBL_FIRST : 256; - - tabPrefix = new int[1 << maxBits]; - tabSuffix = new byte[1 << maxBits]; - stack = new byte[1 << maxBits]; - stackP = stack.Length; - - for (int idx = 255; idx >= 0; idx--) - tabSuffix[idx] = (byte)idx; - } - #region Stream Overrides /// diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs new file mode 100644 index 00000000..ed005372 --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal sealed partial class MultiVolumeReadOnlyAsyncStream + : MultiVolumeReadOnlyStreamBase, + IStreamStack +{ +#if NET8_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + if (filePartEnumerator != null) + { + await filePartEnumerator.DisposeAsync(); + } + currentStream = null; + } +#else + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + filePartEnumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); + + currentStream = null; + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var totalRead = 0; + var currentOffset = offset; + var currentCount = count; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer, currentOffset, readSize, cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!await filePartEnumerator.MoveNextAsync()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + + InitializeNextFilePart(); + } + else + { + break; + } + } + + return totalRead; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var totalRead = 0; + var currentOffset = 0; + var currentCount = buffer.Length; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!await filePartEnumerator.MoveNextAsync()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + InitializeNextFilePart(); + } + else + { + break; + } + } + return totalRead; + } +#endif +} diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs index ba06646f..467c8dd3 100644 --- a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs @@ -8,7 +8,9 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal sealed class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOnlyStreamBase, IStreamStack +internal sealed partial class MultiVolumeReadOnlyAsyncStream + : MultiVolumeReadOnlyStreamBase, + IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -52,26 +54,6 @@ internal sealed class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOnlyStream return stream; } -#if NET8_0_OR_GREATER - public override async ValueTask DisposeAsync() - { - await base.DisposeAsync(); - if (filePartEnumerator != null) - { - await filePartEnumerator.DisposeAsync(); - } - currentStream = null; - } -#else - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - filePartEnumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); - - currentStream = null; - } -#endif - private void InitializeNextFilePart() { maxPosition = filePartEnumerator.Current.FileHeader.CompressedSize; @@ -86,129 +68,6 @@ internal sealed class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOnlyStream "Synchronous read is not supported in MultiVolumeReadOnlyAsyncStream." ); - public override async System.Threading.Tasks.Task ReadAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - var totalRead = 0; - var currentOffset = offset; - var currentCount = count; - while (currentCount > 0) - { - var readSize = currentCount; - if (currentCount > maxPosition - currentPosition) - { - readSize = (int)(maxPosition - currentPosition); - } - - var read = await currentStream - .NotNull() - .ReadAsync(buffer, currentOffset, readSize, cancellationToken) - .ConfigureAwait(false); - if (read < 0) - { - throw new EndOfStreamException(); - } - - currentPosition += read; - currentOffset += read; - currentCount -= read; - totalRead += read; - if ( - ((maxPosition - currentPosition) == 0) - && filePartEnumerator.Current.FileHeader.IsSplitAfter - ) - { - if (filePartEnumerator.Current.FileHeader.R4Salt != null) - { - throw new InvalidFormatException( - "Sharpcompress currently does not support multi-volume decryption." - ); - } - - var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!await filePartEnumerator.MoveNextAsync()) - { - throw new InvalidFormatException( - "Multi-part rar file is incomplete. Entry expects a new volume: " - + fileName - ); - } - - InitializeNextFilePart(); - } - else - { - break; - } - } - - return totalRead; - } - -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - public override async System.Threading.Tasks.ValueTask ReadAsync( - Memory buffer, - System.Threading.CancellationToken cancellationToken = default - ) - { - var totalRead = 0; - var currentOffset = 0; - var currentCount = buffer.Length; - while (currentCount > 0) - { - var readSize = currentCount; - if (currentCount > maxPosition - currentPosition) - { - readSize = (int)(maxPosition - currentPosition); - } - - var read = await currentStream - .NotNull() - .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) - .ConfigureAwait(false); - if (read < 0) - { - throw new EndOfStreamException(); - } - - currentPosition += read; - currentOffset += read; - currentCount -= read; - totalRead += read; - if ( - ((maxPosition - currentPosition) == 0) - && filePartEnumerator.Current.FileHeader.IsSplitAfter - ) - { - if (filePartEnumerator.Current.FileHeader.R4Salt != null) - { - throw new InvalidFormatException( - "Sharpcompress currently does not support multi-volume decryption." - ); - } - var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!await filePartEnumerator.MoveNextAsync()) - { - throw new InvalidFormatException( - "Multi-part rar file is incomplete. Entry expects a new volume: " - + fileName - ); - } - InitializeNextFilePart(); - } - else - { - break; - } - } - return totalRead; - } -#endif - public override bool CanRead => true; public override bool CanSeek => false; diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.Async.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.Async.cs new file mode 100644 index 00000000..122ccda5 --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.Async.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal sealed partial class MultiVolumeReadOnlyStream + : MultiVolumeReadOnlyStreamBase, + IStreamStack +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var totalRead = 0; + var currentOffset = offset; + var currentCount = count; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer, currentOffset, readSize, cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!filePartEnumerator.MoveNext()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + + InitializeNextFilePart(); + } + else + { + break; + } + } + + return totalRead; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var totalRead = 0; + var currentOffset = 0; + var currentCount = buffer.Length; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!filePartEnumerator.MoveNext()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + InitializeNextFilePart(); + } + else + { + break; + } + } + return totalRead; + } +#endif +} diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs index ebf9202c..9503cf03 100644 --- a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs @@ -7,7 +7,9 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal sealed class MultiVolumeReadOnlyStream : MultiVolumeReadOnlyStreamBase, IStreamStack +internal sealed partial class MultiVolumeReadOnlyStream + : MultiVolumeReadOnlyStreamBase, + IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -125,129 +127,6 @@ internal sealed class MultiVolumeReadOnlyStream : MultiVolumeReadOnlyStreamBase, return totalRead; } - public override async System.Threading.Tasks.Task ReadAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - var totalRead = 0; - var currentOffset = offset; - var currentCount = count; - while (currentCount > 0) - { - var readSize = currentCount; - if (currentCount > maxPosition - currentPosition) - { - readSize = (int)(maxPosition - currentPosition); - } - - var read = await currentStream - .NotNull() - .ReadAsync(buffer, currentOffset, readSize, cancellationToken) - .ConfigureAwait(false); - if (read < 0) - { - throw new EndOfStreamException(); - } - - currentPosition += read; - currentOffset += read; - currentCount -= read; - totalRead += read; - if ( - ((maxPosition - currentPosition) == 0) - && filePartEnumerator.Current.FileHeader.IsSplitAfter - ) - { - if (filePartEnumerator.Current.FileHeader.R4Salt != null) - { - throw new InvalidFormatException( - "Sharpcompress currently does not support multi-volume decryption." - ); - } - - var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!filePartEnumerator.MoveNext()) - { - throw new InvalidFormatException( - "Multi-part rar file is incomplete. Entry expects a new volume: " - + fileName - ); - } - - InitializeNextFilePart(); - } - else - { - break; - } - } - - return totalRead; - } - -#if !LEGACY_DOTNET - public override async System.Threading.Tasks.ValueTask ReadAsync( - Memory buffer, - System.Threading.CancellationToken cancellationToken = default - ) - { - var totalRead = 0; - var currentOffset = 0; - var currentCount = buffer.Length; - while (currentCount > 0) - { - var readSize = currentCount; - if (currentCount > maxPosition - currentPosition) - { - readSize = (int)(maxPosition - currentPosition); - } - - var read = await currentStream - .NotNull() - .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) - .ConfigureAwait(false); - if (read < 0) - { - throw new EndOfStreamException(); - } - - currentPosition += read; - currentOffset += read; - currentCount -= read; - totalRead += read; - if ( - ((maxPosition - currentPosition) == 0) - && filePartEnumerator.Current.FileHeader.IsSplitAfter - ) - { - if (filePartEnumerator.Current.FileHeader.R4Salt != null) - { - throw new InvalidFormatException( - "Sharpcompress currently does not support multi-volume decryption." - ); - } - var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!filePartEnumerator.MoveNext()) - { - throw new InvalidFormatException( - "Multi-part rar file is incomplete. Entry expects a new volume: " - + fileName - ); - } - InitializeNextFilePart(); - } - else - { - break; - } - } - return totalRead; - } -#endif - public override bool CanRead => true; public override bool CanSeek => false; diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs new file mode 100644 index 00000000..2a38239d --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal partial class RarBLAKE2spStream : RarStream, IStreamStack +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var result = await base.ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (result != 0) + { + Update(_blake2sp, new ReadOnlySpan(buffer, offset, result), result); + } + else + { + _hash = Final(_blake2sp); + if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0) + { + // NOTE: we use the last FileHeader in a multipart volume to check CRC + throw new InvalidFormatException("file crc mismatch"); + } + } + + return result; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (result != 0) + { + Update(_blake2sp, buffer.Span.Slice(0, result), result); + } + else + { + _hash = Final(_blake2sp); + if ( + !disableCRCCheck + && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) + && buffer.Length != 0 + ) + { + // NOTE: we use the last FileHeader in a multipart volume to check CRC + throw new InvalidFormatException("file crc mismatch"); + } + } + + return result; + } +#endif +} diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs index 906a29eb..fb7fc98d 100644 --- a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs @@ -9,7 +9,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal class RarBLAKE2spStream : RarStream, IStreamStack +internal partial class RarBLAKE2spStream : RarStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -358,59 +358,4 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack return result; } - - public override async System.Threading.Tasks.Task ReadAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - var result = await base.ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (result != 0) - { - Update(_blake2sp, new ReadOnlySpan(buffer, offset, result), result); - } - else - { - _hash = Final(_blake2sp); - if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0) - { - // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); - } - } - - return result; - } - -#if !LEGACY_DOTNET - public override async System.Threading.Tasks.ValueTask ReadAsync( - Memory buffer, - System.Threading.CancellationToken cancellationToken = default - ) - { - var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - if (result != 0) - { - Update(_blake2sp, buffer.Span.Slice(0, result), result); - } - else - { - _hash = Final(_blake2sp); - if ( - !disableCRCCheck - && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) - && buffer.Length != 0 - ) - { - // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); - } - } - - return result; - } -#endif } diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs new file mode 100644 index 00000000..d7e672c8 --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal partial class RarCrcStream : RarStream, IStreamStack +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var result = await base.ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (result != 0) + { + currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result); + } + else if ( + !disableCRC + && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) + && count != 0 + ) + { + // NOTE: we use the last FileHeader in a multipart volume to check CRC + throw new InvalidFormatException("file crc mismatch"); + } + + return result; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + if (result != 0) + { + currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result); + } + else if ( + !disableCRC + && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) + && buffer.Length != 0 + ) + { + // NOTE: we use the last FileHeader in a multipart volume to check CRC + throw new InvalidFormatException("file crc mismatch"); + } + + return result; + } +#endif +} diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs index 535c29fb..960783a8 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal class RarCrcStream : RarStream, IStreamStack +internal partial class RarCrcStream : RarStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -102,56 +102,4 @@ internal class RarCrcStream : RarStream, IStreamStack return result; } - - public override async System.Threading.Tasks.Task ReadAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - var result = await base.ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (result != 0) - { - currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result); - } - else if ( - !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) - && count != 0 - ) - { - // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); - } - - return result; - } - -#if !LEGACY_DOTNET - public override async System.Threading.Tasks.ValueTask ReadAsync( - Memory buffer, - System.Threading.CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - if (result != 0) - { - currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result); - } - else if ( - !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) - && buffer.Length != 0 - ) - { - // NOTE: we use the last FileHeader in a multipart volume to check CRC - throw new InvalidFormatException("file crc mismatch"); - } - - return result; - } -#endif } diff --git a/src/SharpCompress/Compressors/Rar/RarStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarStream.Async.cs new file mode 100644 index 00000000..4ea0651d --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/RarStream.Async.cs @@ -0,0 +1,116 @@ +#nullable disable + +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common.Rar.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal partial class RarStream +{ + /// + /// Asynchronously initializes the RAR stream for reading. + /// + public async ValueTask InitializeAsync(CancellationToken cancellationToken = default) + { + fetch = true; + await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken); + fetch = false; + _position = 0; + } + + /// + /// Asynchronously reads bytes from the current stream into a buffer. + /// + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => await ReadImplAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + /// + /// Internal async implementation of ReadAsync. + /// + private async Task ReadImplAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + outTotal = 0; + if (tmpCount > 0) + { + var toCopy = tmpCount < count ? tmpCount : count; + Buffer.BlockCopy(tmpBuffer, tmpOffset, buffer, offset, toCopy); + tmpOffset += toCopy; + tmpCount -= toCopy; + offset += toCopy; + count -= toCopy; + outTotal += toCopy; + } + if (count > 0 && unpack.DestSize > 0) + { + outBuffer = buffer; + outOffset = offset; + outCount = count; + fetch = true; + await unpack.DoUnpackAsync(cancellationToken).ConfigureAwait(false); + fetch = false; + } + _position += outTotal; + if (count > 0 && outTotal == 0 && _position < Length) + { + // sanity check, eg if we try to decompress a redir entry + throw new InvalidOperationException( + $"unpacked file size does not match header: expected {Length} found {_position}" + ); + } + return outTotal; + } + +#if !LEGACY_DOTNET + /// + /// Asynchronously reads bytes from the current stream into a memory buffer. + /// + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var array = ArrayPool.Shared.Rent(buffer.Length); + try + { + var bytesRead = await ReadImplAsync(array, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + new ReadOnlySpan(array, 0, bytesRead).CopyTo(buffer.Span); + return bytesRead; + } + finally + { + ArrayPool.Shared.Return(array); + } + } +#endif + + /// + /// Asynchronously writes bytes to the current stream. + /// + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + return Task.CompletedTask; + } +} diff --git a/src/SharpCompress/Compressors/Rar/RarStream.cs b/src/SharpCompress/Compressors/Rar/RarStream.cs index 34700edf..30fcc5ea 100644 --- a/src/SharpCompress/Compressors/Rar/RarStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarStream.cs @@ -3,14 +3,12 @@ using System; using System.Buffers; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common.Rar.Headers; using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal class RarStream : Stream, IStreamStack +internal partial class RarStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -68,14 +66,6 @@ internal class RarStream : Stream, IStreamStack _position = 0; } - public async ValueTask InitializeAsync(CancellationToken cancellationToken = default) - { - fetch = true; - await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken); - fetch = false; - _position = 0; - } - protected override void Dispose(bool disposing) { if (!isDisposed) @@ -144,73 +134,6 @@ internal class RarStream : Stream, IStreamStack return outTotal; } - public override async System.Threading.Tasks.Task ReadAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) => await ReadImplAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - private async System.Threading.Tasks.Task ReadImplAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - outTotal = 0; - if (tmpCount > 0) - { - var toCopy = tmpCount < count ? tmpCount : count; - Buffer.BlockCopy(tmpBuffer, tmpOffset, buffer, offset, toCopy); - tmpOffset += toCopy; - tmpCount -= toCopy; - offset += toCopy; - count -= toCopy; - outTotal += toCopy; - } - if (count > 0 && unpack.DestSize > 0) - { - outBuffer = buffer; - outOffset = offset; - outCount = count; - fetch = true; - await unpack.DoUnpackAsync(cancellationToken).ConfigureAwait(false); - fetch = false; - } - _position += outTotal; - if (count > 0 && outTotal == 0 && _position < Length) - { - // sanity check, eg if we try to decompress a redir entry - throw new InvalidOperationException( - $"unpacked file size does not match header: expected {Length} found {_position}" - ); - } - return outTotal; - } - -#if !LEGACY_DOTNET - public override async System.Threading.Tasks.ValueTask ReadAsync( - Memory buffer, - System.Threading.CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - var array = System.Buffers.ArrayPool.Shared.Rent(buffer.Length); - try - { - var bytesRead = await ReadImplAsync(array, 0, buffer.Length, cancellationToken) - .ConfigureAwait(false); - new ReadOnlySpan(array, 0, bytesRead).CopyTo(buffer.Span); - return bytesRead; - } - finally - { - System.Buffers.ArrayPool.Shared.Return(array); - } - } -#endif - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); @@ -245,18 +168,6 @@ internal class RarStream : Stream, IStreamStack } } - public override System.Threading.Tasks.Task WriteAsync( - byte[] buffer, - int offset, - int count, - System.Threading.CancellationToken cancellationToken - ) - { - cancellationToken.ThrowIfCancellationRequested(); - Write(buffer, offset, count); - return System.Threading.Tasks.Task.CompletedTask; - } - private void EnsureBufferCapacity(int count) { if (this.tmpBuffer.Length < this.tmpCount + count) diff --git a/src/SharpCompress/Compressors/Xz/BinaryUtils.Async.cs b/src/SharpCompress/Compressors/Xz/BinaryUtils.Async.cs new file mode 100644 index 00000000..25e1182d --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/BinaryUtils.Async.cs @@ -0,0 +1,32 @@ +using System; +using System.Buffers.Binary; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.Xz; + +public static partial class BinaryUtils +{ + public static async ValueTask ReadLittleEndianInt32Async( + this Stream stream, + CancellationToken cancellationToken = default + ) + { + var bytes = new byte[4]; + var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false); + if (!read) + { + throw new EndOfStreamException(); + } + return BinaryPrimitives.ReadInt32LittleEndian(bytes); + } + + internal static async ValueTask ReadLittleEndianUInt32Async( + this Stream stream, + CancellationToken cancellationToken = default + ) => + unchecked( + (uint)await ReadLittleEndianInt32Async(stream, cancellationToken).ConfigureAwait(false) + ); +} diff --git a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs index bb9cb412..f9f7af31 100644 --- a/src/SharpCompress/Compressors/Xz/BinaryUtils.cs +++ b/src/SharpCompress/Compressors/Xz/BinaryUtils.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace SharpCompress.Compressors.Xz; [CLSCompliant(false)] -public static class BinaryUtils +public static partial class BinaryUtils { public static int ReadLittleEndianInt32(this BinaryReader reader) { @@ -32,28 +32,6 @@ public static class BinaryUtils internal static uint ReadLittleEndianUInt32(this Stream stream) => unchecked((uint)ReadLittleEndianInt32(stream)); - public static async ValueTask ReadLittleEndianInt32Async( - this Stream stream, - CancellationToken cancellationToken = default - ) - { - var bytes = new byte[4]; - var read = await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false); - if (!read) - { - throw new EndOfStreamException(); - } - return BinaryPrimitives.ReadInt32LittleEndian(bytes); - } - - internal static async ValueTask ReadLittleEndianUInt32Async( - this Stream stream, - CancellationToken cancellationToken = default - ) => - unchecked( - (uint)await ReadLittleEndianInt32Async(stream, cancellationToken).ConfigureAwait(false) - ); - internal static byte[] ToBigEndianBytes(this uint uint32) { var result = BitConverter.GetBytes(uint32); diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.Async.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.Async.cs new file mode 100644 index 00000000..05b95ac4 --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.Async.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Compressors.Xz; + +internal static partial class MultiByteIntegers +{ + public static async ValueTask ReadXZIntegerAsync( + this BinaryReader reader, + CancellationToken cancellationToken = default, + int MaxBytes = 9 + ) + { + if (MaxBytes <= 0) + { + throw new ArgumentOutOfRangeException(nameof(MaxBytes)); + } + + if (MaxBytes > 9) + { + MaxBytes = 9; + } + + var LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + var Output = (ulong)LastByte & 0x7F; + + var i = 0; + while ((LastByte & 0x80) != 0) + { + if (++i >= MaxBytes) + { + throw new InvalidFormatException(); + } + + LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + if (LastByte == 0) + { + throw new InvalidFormatException(); + } + + Output |= ((ulong)(LastByte & 0x7F)) << (i * 7); + } + return Output; + } +} diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs index dbdb51f3..52dbd5b8 100644 --- a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs +++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs @@ -6,7 +6,7 @@ using SharpCompress.Common; namespace SharpCompress.Compressors.Xz; -internal static class MultiByteIntegers +internal static partial class MultiByteIntegers { public static ulong ReadXZInteger(this BinaryReader reader, int MaxBytes = 9) { @@ -41,42 +41,4 @@ internal static class MultiByteIntegers } return Output; } - - public static async ValueTask ReadXZIntegerAsync( - this BinaryReader reader, - CancellationToken cancellationToken = default, - int MaxBytes = 9 - ) - { - if (MaxBytes <= 0) - { - throw new ArgumentOutOfRangeException(nameof(MaxBytes)); - } - - if (MaxBytes > 9) - { - MaxBytes = 9; - } - - var LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); - var Output = (ulong)LastByte & 0x7F; - - var i = 0; - while ((LastByte & 0x80) != 0) - { - if (++i >= MaxBytes) - { - throw new InvalidFormatException(); - } - - LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); - if (LastByte == 0) - { - throw new InvalidFormatException(); - } - - Output |= ((ulong)(LastByte & 0x7F)) << (i * 7); - } - return Output; - } } diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs b/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs new file mode 100644 index 00000000..78d2de3d --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZBlock.Async.cs @@ -0,0 +1,134 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Compressors.Xz.Filters; + +namespace SharpCompress.Compressors.Xz; + +public sealed partial class XZBlock +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (!HeaderIsLoaded) + { + await LoadHeaderAsync(cancellationToken).ConfigureAwait(false); + } + + if (!_streamConnected) + { + ConnectStream(); + } + + if (!_endOfStream) + { + bytesRead = await _decomStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + + if (bytesRead != count) + { + _endOfStream = true; + } + + if (_endOfStream && !_paddingSkipped) + { + await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); + } + + if (_endOfStream && !_crcChecked) + { + await CheckCrcAsync(cancellationToken).ConfigureAwait(false); + } + + return bytesRead; + } + + private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) + { + var bytes = (BaseStream.Position - _startPosition) % 4; + if (bytes > 0) + { + var paddingBytes = new byte[4 - bytes]; + await BaseStream + .ReadAsync(paddingBytes, 0, paddingBytes.Length, cancellationToken) + .ConfigureAwait(false); + if (paddingBytes.Any(b => b != 0)) + { + throw new InvalidFormatException("Padding bytes were non-null"); + } + } + _paddingSkipped = true; + } + + private async ValueTask CheckCrcAsync(CancellationToken cancellationToken = default) + { + var crc = new byte[_checkSize]; + await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false); + // Actually do a check (and read in the bytes + // into the function throughout the stream read). + _crcChecked = true; + } + + private async ValueTask LoadHeaderAsync(CancellationToken cancellationToken = default) + { + await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false); + var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false); + + using (var cache = new MemoryStream(headerCache)) + using (var cachedReader = new BinaryReader(cache)) + { + cachedReader.BaseStream.Position = 1; // skip the header size byte + ReadBlockFlags(cachedReader); + ReadFilters(cachedReader); + } + HeaderIsLoaded = true; + } + + private async ValueTask ReadHeaderSizeAsync(CancellationToken cancellationToken = default) + { + var buffer = new byte[1]; + await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); + _blockHeaderSizeByte = buffer[0]; + if (_blockHeaderSizeByte == 0) + { + throw new XZIndexMarkerReachedException(); + } + } + + private async ValueTask CacheHeaderAsync(CancellationToken cancellationToken = default) + { + var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; + blockHeaderWithoutCrc[0] = _blockHeaderSizeByte; + var read = await BaseStream + .ReadAsync(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5, cancellationToken) + .ConfigureAwait(false); + if (read != BlockHeaderSize - 5) + { + throw new EndOfStreamException("Reached end of stream unexpectedly"); + } + + var crc = await BaseStream + .ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var calcCrc = Crc32.Compute(blockHeaderWithoutCrc); + if (crc != calcCrc) + { + throw new InvalidFormatException("Block header corrupt"); + } + + return blockHeaderWithoutCrc; + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs index 7c18e3b4..39c3f0b5 100644 --- a/src/SharpCompress/Compressors/Xz/XZBlock.cs +++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs @@ -12,7 +12,7 @@ using SharpCompress.Compressors.Xz.Filters; namespace SharpCompress.Compressors.Xz; [CLSCompliant(false)] -public sealed class XZBlock : XZReadOnlyStream +public sealed partial class XZBlock : XZReadOnlyStream { public int BlockHeaderSize => (_blockHeaderSizeByte + 1) * 4; public ulong? CompressedSize { get; private set; } @@ -74,49 +74,6 @@ public sealed class XZBlock : XZReadOnlyStream return bytesRead; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var bytesRead = 0; - if (!HeaderIsLoaded) - { - await LoadHeaderAsync(cancellationToken).ConfigureAwait(false); - } - - if (!_streamConnected) - { - ConnectStream(); - } - - if (!_endOfStream) - { - bytesRead = await _decomStream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - } - - if (bytesRead != count) - { - _endOfStream = true; - } - - if (_endOfStream && !_paddingSkipped) - { - await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); - } - - if (_endOfStream && !_crcChecked) - { - await CheckCrcAsync(cancellationToken).ConfigureAwait(false); - } - - return bytesRead; - } - private void SkipPadding() { var bytes = (BaseStream.Position - _startPosition) % 4; @@ -132,23 +89,6 @@ public sealed class XZBlock : XZReadOnlyStream _paddingSkipped = true; } - private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) - { - var bytes = (BaseStream.Position - _startPosition) % 4; - if (bytes > 0) - { - var paddingBytes = new byte[4 - bytes]; - await BaseStream - .ReadAsync(paddingBytes, 0, paddingBytes.Length, cancellationToken) - .ConfigureAwait(false); - if (paddingBytes.Any(b => b != 0)) - { - throw new InvalidFormatException("Padding bytes were non-null"); - } - } - _paddingSkipped = true; - } - private void CheckCrc() { var crc = new byte[_checkSize]; @@ -158,15 +98,6 @@ public sealed class XZBlock : XZReadOnlyStream _crcChecked = true; } - private async ValueTask CheckCrcAsync(CancellationToken cancellationToken = default) - { - var crc = new byte[_checkSize]; - await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false); - // Actually do a check (and read in the bytes - // into the function throughout the stream read). - _crcChecked = true; - } - private void ConnectStream() { _decomStream = BaseStream; @@ -194,21 +125,6 @@ public sealed class XZBlock : XZReadOnlyStream HeaderIsLoaded = true; } - private async ValueTask LoadHeaderAsync(CancellationToken cancellationToken = default) - { - await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false); - var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false); - - using (var cache = new MemoryStream(headerCache)) - using (var cachedReader = new BinaryReader(cache)) - { - cachedReader.BaseStream.Position = 1; // skip the header size byte - ReadBlockFlags(cachedReader); - ReadFilters(cachedReader); - } - HeaderIsLoaded = true; - } - private void ReadHeaderSize() { _blockHeaderSizeByte = (byte)BaseStream.ReadByte(); @@ -218,17 +134,6 @@ public sealed class XZBlock : XZReadOnlyStream } } - private async ValueTask ReadHeaderSizeAsync(CancellationToken cancellationToken = default) - { - var buffer = new byte[1]; - await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); - _blockHeaderSizeByte = buffer[0]; - if (_blockHeaderSizeByte == 0) - { - throw new XZIndexMarkerReachedException(); - } - } - private byte[] CacheHeader() { var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; @@ -249,30 +154,6 @@ public sealed class XZBlock : XZReadOnlyStream return blockHeaderWithoutCrc; } - private async ValueTask CacheHeaderAsync(CancellationToken cancellationToken = default) - { - var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4]; - blockHeaderWithoutCrc[0] = _blockHeaderSizeByte; - var read = await BaseStream - .ReadAsync(blockHeaderWithoutCrc, 1, BlockHeaderSize - 5, cancellationToken) - .ConfigureAwait(false); - if (read != BlockHeaderSize - 5) - { - throw new EndOfStreamException("Reached end of stream unexpectedly"); - } - - var crc = await BaseStream - .ReadLittleEndianUInt32Async(cancellationToken) - .ConfigureAwait(false); - var calcCrc = Crc32.Compute(blockHeaderWithoutCrc); - if (crc != calcCrc) - { - throw new InvalidFormatException("Block header corrupt"); - } - - return blockHeaderWithoutCrc; - } - private void ReadBlockFlags(BinaryReader reader) { var blockFlags = reader.ReadByte(); diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs new file mode 100644 index 00000000..b136335d --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Xz; + +public partial class XZFooter +{ + public static async ValueTask FromStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var footer = new XZFooter(new BinaryReader(stream, Encoding.UTF8, true)); + await footer.ProcessAsync(cancellationToken).ConfigureAwait(false); + return footer; + } + + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) + { + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var footerBytes = await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false); + var myCrc = Crc32.Compute(footerBytes); + if (crc != myCrc) + { + throw new InvalidFormatException("Footer corrupt"); + } + + using (var stream = new MemoryStream(footerBytes)) + using (var reader = new BinaryReader(stream)) + { + BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; + StreamFlags = reader.ReadBytes(2); + } + var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); + if (!magBy.AsSpan().SequenceEqual(_magicBytes)) + { + throw new InvalidFormatException("Magic footer missing"); + } + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.cs b/src/SharpCompress/Compressors/Xz/XZFooter.cs index 372e0d63..d6b80d5e 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Xz; -public class XZFooter +public partial class XZFooter { private readonly BinaryReader _reader; private static ReadOnlySpan _magicBytes => "YZ"u8; @@ -29,16 +29,6 @@ public class XZFooter return footer; } - public static async ValueTask FromStreamAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var footer = new XZFooter(new BinaryReader(stream, Encoding.UTF8, true)); - await footer.ProcessAsync(cancellationToken).ConfigureAwait(false); - return footer; - } - public void Process() { var crc = _reader.ReadLittleEndianUInt32(); @@ -61,29 +51,4 @@ public class XZFooter throw new InvalidFormatException("Magic footer missing"); } } - - public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) - { - var crc = await _reader - .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) - .ConfigureAwait(false); - var footerBytes = await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false); - var myCrc = Crc32.Compute(footerBytes); - if (crc != myCrc) - { - throw new InvalidFormatException("Footer corrupt"); - } - - using (var stream = new MemoryStream(footerBytes)) - using (var reader = new BinaryReader(stream)) - { - BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; - StreamFlags = reader.ReadBytes(2); - } - var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); - if (!magBy.AsSpan().SequenceEqual(_magicBytes)) - { - throw new InvalidFormatException("Magic footer missing"); - } - } } diff --git a/src/SharpCompress/Compressors/Xz/XZHeader.Async.cs b/src/SharpCompress/Compressors/Xz/XZHeader.Async.cs new file mode 100644 index 00000000..aafe103b --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZHeader.Async.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Xz; + +public partial class XZHeader +{ + public static async ValueTask FromStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + var header = new XZHeader(new BinaryReader(stream, Encoding.UTF8, true)); + await header.ProcessAsync(cancellationToken).ConfigureAwait(false); + return header; + } + + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) + { + CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false)); + await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false); + } + + private async ValueTask ProcessStreamFlagsAsync(CancellationToken cancellationToken = default) + { + var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + var calcCrc = Crc32.Compute(streamFlags); + if (crc != calcCrc) + { + throw new InvalidFormatException("Stream header corrupt"); + } + + BlockCheckType = (CheckType)(streamFlags[1] & 0x0F); + var futureUse = (byte)(streamFlags[1] & 0xF0); + if (futureUse != 0 || streamFlags[0] != 0) + { + throw new InvalidFormatException("Unknown XZ Stream Version"); + } + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZHeader.cs b/src/SharpCompress/Compressors/Xz/XZHeader.cs index e855368f..0dda1583 100644 --- a/src/SharpCompress/Compressors/Xz/XZHeader.cs +++ b/src/SharpCompress/Compressors/Xz/XZHeader.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Xz; -public class XZHeader +public partial class XZHeader { private readonly BinaryReader _reader; private readonly byte[] MagicHeader = { 0xFD, 0x37, 0x7A, 0x58, 0x5a, 0x00 }; @@ -25,28 +25,12 @@ public class XZHeader return header; } - public static async ValueTask FromStreamAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - var header = new XZHeader(new BinaryReader(stream, Encoding.UTF8, true)); - await header.ProcessAsync(cancellationToken).ConfigureAwait(false); - return header; - } - public void Process() { CheckMagicBytes(_reader.ReadBytes(6)); ProcessStreamFlags(); } - public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) - { - CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false)); - await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false); - } - private void ProcessStreamFlags() { var streamFlags = _reader.ReadBytes(2); @@ -65,26 +49,6 @@ public class XZHeader } } - private async ValueTask ProcessStreamFlagsAsync(CancellationToken cancellationToken = default) - { - var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); - var crc = await _reader - .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) - .ConfigureAwait(false); - var calcCrc = Crc32.Compute(streamFlags); - if (crc != calcCrc) - { - throw new InvalidFormatException("Stream header corrupt"); - } - - BlockCheckType = (CheckType)(streamFlags[1] & 0x0F); - var futureUse = (byte)(streamFlags[1] & 0xF0); - if (futureUse != 0 || streamFlags[0] != 0) - { - throw new InvalidFormatException("Unknown XZ Stream Version"); - } - } - private void CheckMagicBytes(byte[] header) { if (!header.SequenceEqual(MagicHeader)) diff --git a/src/SharpCompress/Compressors/Xz/XZIndex.Async.cs b/src/SharpCompress/Compressors/Xz/XZIndex.Async.cs new file mode 100644 index 00000000..b2feaabe --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZIndex.Async.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Xz; + +public partial class XZIndex +{ + public static async ValueTask FromStreamAsync( + Stream stream, + bool indexMarkerAlreadyVerified, + CancellationToken cancellationToken = default + ) + { + var index = new XZIndex( + new BinaryReader(stream, Encoding.UTF8, true), + indexMarkerAlreadyVerified + ); + await index.ProcessAsync(cancellationToken).ConfigureAwait(false); + return index; + } + + public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) + { + if (!_indexMarkerAlreadyVerified) + { + await VerifyIndexMarkerAsync(cancellationToken).ConfigureAwait(false); + } + + NumberOfRecords = await _reader.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); + for (ulong i = 0; i < NumberOfRecords; i++) + { + Records.Add( + await XZIndexRecord + .FromBinaryReaderAsync(_reader, cancellationToken) + .ConfigureAwait(false) + ); + } + await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); + await VerifyCrc32Async(cancellationToken).ConfigureAwait(false); + } + + private async ValueTask VerifyIndexMarkerAsync(CancellationToken cancellationToken = default) + { + var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); + if (marker != 0) + { + throw new InvalidFormatException("Not an index block"); + } + } + + private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) + { + var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; + if (bytes > 0) + { + var paddingBytes = await _reader + .ReadBytesAsync(4 - bytes, cancellationToken) + .ConfigureAwait(false); + if (paddingBytes.Any(b => b != 0)) + { + throw new InvalidFormatException("Padding bytes were non-null"); + } + } + } + + private async ValueTask VerifyCrc32Async(CancellationToken cancellationToken = default) + { + var crc = await _reader + .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) + .ConfigureAwait(false); + // TODO verify this matches + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZIndex.cs b/src/SharpCompress/Compressors/Xz/XZIndex.cs index 3bc8ea42..3ca6b06e 100644 --- a/src/SharpCompress/Compressors/Xz/XZIndex.cs +++ b/src/SharpCompress/Compressors/Xz/XZIndex.cs @@ -11,7 +11,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Xz; [CLSCompliant(false)] -public class XZIndex +public partial class XZIndex { private readonly BinaryReader _reader; public long StreamStartPosition { get; private set; } @@ -41,20 +41,6 @@ public class XZIndex return index; } - public static async ValueTask FromStreamAsync( - Stream stream, - bool indexMarkerAlreadyVerified, - CancellationToken cancellationToken = default - ) - { - var index = new XZIndex( - new BinaryReader(stream, Encoding.UTF8, true), - indexMarkerAlreadyVerified - ); - await index.ProcessAsync(cancellationToken).ConfigureAwait(false); - return index; - } - public void Process() { if (!_indexMarkerAlreadyVerified) @@ -71,26 +57,6 @@ public class XZIndex VerifyCrc32(); } - public async ValueTask ProcessAsync(CancellationToken cancellationToken = default) - { - if (!_indexMarkerAlreadyVerified) - { - await VerifyIndexMarkerAsync(cancellationToken).ConfigureAwait(false); - } - - NumberOfRecords = await _reader.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); - for (ulong i = 0; i < NumberOfRecords; i++) - { - Records.Add( - await XZIndexRecord - .FromBinaryReaderAsync(_reader, cancellationToken) - .ConfigureAwait(false) - ); - } - await SkipPaddingAsync(cancellationToken).ConfigureAwait(false); - await VerifyCrc32Async(cancellationToken).ConfigureAwait(false); - } - private void VerifyIndexMarker() { var marker = _reader.ReadByte(); @@ -100,15 +66,6 @@ public class XZIndex } } - private async ValueTask VerifyIndexMarkerAsync(CancellationToken cancellationToken = default) - { - var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false); - if (marker != 0) - { - throw new InvalidFormatException("Not an index block"); - } - } - private void SkipPadding() { var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; @@ -122,32 +79,9 @@ public class XZIndex } } - private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default) - { - var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4; - if (bytes > 0) - { - var paddingBytes = await _reader - .ReadBytesAsync(4 - bytes, cancellationToken) - .ConfigureAwait(false); - if (paddingBytes.Any(b => b != 0)) - { - throw new InvalidFormatException("Padding bytes were non-null"); - } - } - } - private void VerifyCrc32() { var crc = _reader.ReadLittleEndianUInt32(); // TODO verify this matches } - - private async ValueTask VerifyCrc32Async(CancellationToken cancellationToken = default) - { - var crc = await _reader - .BaseStream.ReadLittleEndianUInt32Async(cancellationToken) - .ConfigureAwait(false); - // TODO verify this matches - } } diff --git a/src/SharpCompress/Compressors/Xz/XZIndexRecord.Async.cs b/src/SharpCompress/Compressors/Xz/XZIndexRecord.Async.cs new file mode 100644 index 00000000..9d6d6f6c --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZIndexRecord.Async.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.Xz; + +public partial class XZIndexRecord +{ + public static async ValueTask FromBinaryReaderAsync( + BinaryReader br, + CancellationToken cancellationToken = default + ) + { + var record = new XZIndexRecord(); + record.UnpaddedSize = await br.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); + record.UncompressedSize = await br.ReadXZIntegerAsync(cancellationToken) + .ConfigureAwait(false); + return record; + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs b/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs index 355af2a8..d447c7b3 100644 --- a/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs +++ b/src/SharpCompress/Compressors/Xz/XZIndexRecord.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace SharpCompress.Compressors.Xz; [CLSCompliant(false)] -public class XZIndexRecord +public partial class XZIndexRecord { public ulong UnpaddedSize { get; private set; } public ulong UncompressedSize { get; private set; } @@ -20,16 +20,4 @@ public class XZIndexRecord record.UncompressedSize = br.ReadXZInteger(); return record; } - - public static async ValueTask FromBinaryReaderAsync( - BinaryReader br, - CancellationToken cancellationToken = default - ) - { - var record = new XZIndexRecord(); - record.UnpaddedSize = await br.ReadXZIntegerAsync(cancellationToken).ConfigureAwait(false); - record.UncompressedSize = await br.ReadXZIntegerAsync(cancellationToken) - .ConfigureAwait(false); - return record; - } } diff --git a/src/SharpCompress/Compressors/Xz/XZStream.Async.cs b/src/SharpCompress/Compressors/Xz/XZStream.Async.cs new file mode 100644 index 00000000..25c6bcca --- /dev/null +++ b/src/SharpCompress/Compressors/Xz/XZStream.Async.cs @@ -0,0 +1,135 @@ +#nullable disable + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Xz; + +public sealed partial class XZStream +{ + public static async ValueTask IsXZStreamAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + return null + != await XZHeader.FromStreamAsync(stream, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + return false; + } + } + + /// + /// Asynchronously reads bytes from the current stream into a buffer. + /// + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (_endOfStream) + { + return bytesRead; + } + + if (!HeaderIsRead) + { + await ReadHeaderAsync(cancellationToken).ConfigureAwait(false); + } + + bytesRead = await ReadBlocksAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (bytesRead < count) + { + _endOfStream = true; + await ReadIndexAsync(cancellationToken).ConfigureAwait(false); + await ReadFooterAsync(cancellationToken).ConfigureAwait(false); + } + return bytesRead; + } + + /// + /// Asynchronously reads and validates the XZ header. + /// + private async ValueTask ReadHeaderAsync(CancellationToken cancellationToken = default) + { + Header = await XZHeader + .FromStreamAsync(BaseStream, cancellationToken) + .ConfigureAwait(false); + AssertBlockCheckTypeIsSupported(); + HeaderIsRead = true; + } + + /// + /// Asynchronously reads the XZ index. + /// + private async ValueTask ReadIndexAsync(CancellationToken cancellationToken = default) => + Index = await XZIndex + .FromStreamAsync(BaseStream, true, cancellationToken) + .ConfigureAwait(false); + + /// + /// Asynchronously reads the XZ footer. + /// + private async ValueTask ReadFooterAsync(CancellationToken cancellationToken = default) => + Footer = await XZFooter + .FromStreamAsync(BaseStream, cancellationToken) + .ConfigureAwait(false); + + /// + /// Asynchronously reads blocks of data from the stream. + /// + private async ValueTask ReadBlocksAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var bytesRead = 0; + if (_currentBlock is null) + { + NextBlock(); + } + + for (; ; ) + { + try + { + if (bytesRead >= count) + { + break; + } + + var remaining = count - bytesRead; + var newOffset = offset + bytesRead; + var justRead = await _currentBlock + .ReadAsync(buffer, newOffset, remaining, cancellationToken) + .ConfigureAwait(false); + if (justRead < remaining) + { + NextBlock(); + } + + bytesRead += justRead; + } + catch (XZIndexMarkerReachedException) + { + break; + } + } + return bytesRead; + } +} diff --git a/src/SharpCompress/Compressors/Xz/XZStream.cs b/src/SharpCompress/Compressors/Xz/XZStream.cs index 374cbd1e..d68334a1 100644 --- a/src/SharpCompress/Compressors/Xz/XZStream.cs +++ b/src/SharpCompress/Compressors/Xz/XZStream.cs @@ -10,7 +10,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Xz; [CLSCompliant(false)] -public sealed class XZStream : XZReadOnlyStream, IStreamStack +public sealed partial class XZStream : XZReadOnlyStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -60,22 +60,6 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack } } - public static async ValueTask IsXZStreamAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - try - { - return null != await XZHeader.FromStreamAsync(stream, cancellationToken); - } - catch (Exception) - { - return false; - } - } - private void AssertBlockCheckTypeIsSupported() { switch (Header.BlockCheckType) @@ -122,35 +106,6 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack return bytesRead; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var bytesRead = 0; - if (_endOfStream) - { - return bytesRead; - } - - if (!HeaderIsRead) - { - await ReadHeaderAsync(cancellationToken).ConfigureAwait(false); - } - - bytesRead = await ReadBlocksAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (bytesRead < count) - { - _endOfStream = true; - await ReadIndexAsync(cancellationToken).ConfigureAwait(false); - await ReadFooterAsync(cancellationToken).ConfigureAwait(false); - } - return bytesRead; - } - private void ReadHeader() { Header = XZHeader.FromStream(BaseStream); @@ -158,30 +113,12 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack HeaderIsRead = true; } - private async ValueTask ReadHeaderAsync(CancellationToken cancellationToken = default) - { - Header = await XZHeader - .FromStreamAsync(BaseStream, cancellationToken) - .ConfigureAwait(false); - AssertBlockCheckTypeIsSupported(); - HeaderIsRead = true; - } - private void ReadIndex() => Index = XZIndex.FromStream(BaseStream, true); - private async ValueTask ReadIndexAsync(CancellationToken cancellationToken = default) => - Index = await XZIndex - .FromStreamAsync(BaseStream, true, cancellationToken) - .ConfigureAwait(false); - // TODO verify Index private void ReadFooter() => Footer = XZFooter.FromStream(BaseStream); // TODO verify footer - private async ValueTask ReadFooterAsync(CancellationToken cancellationToken = default) => - Footer = await XZFooter - .FromStreamAsync(BaseStream, cancellationToken) - .ConfigureAwait(false); private int ReadBlocks(byte[] buffer, int offset, int count) { @@ -218,48 +155,6 @@ public sealed class XZStream : XZReadOnlyStream, IStreamStack return bytesRead; } - private async ValueTask ReadBlocksAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken = default - ) - { - var bytesRead = 0; - if (_currentBlock is null) - { - NextBlock(); - } - - for (; ; ) - { - try - { - if (bytesRead >= count) - { - break; - } - - var remaining = count - bytesRead; - var newOffset = offset + bytesRead; - var justRead = await _currentBlock - .ReadAsync(buffer, newOffset, remaining, cancellationToken) - .ConfigureAwait(false); - if (justRead < remaining) - { - NextBlock(); - } - - bytesRead += justRead; - } - catch (XZIndexMarkerReachedException) - { - break; - } - } - return bytesRead; - } - private void NextBlock() => _currentBlock = new XZBlock(BaseStream, Header.BlockCheckType, Header.BlockCheckSize); } diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs new file mode 100644 index 00000000..3e5423f5 --- /dev/null +++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.ZStandard.Unsafe; + +namespace SharpCompress.Compressors.ZStandard; + +public partial class CompressionStream : Stream +{ +#if !LEGACY_DOTNET + public override async ValueTask DisposeAsync() +#else + public async ValueTask DisposeAsync() +#endif + { + if (compressor == null) + return; + + try + { + await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_end).ConfigureAwait(false); + } + finally + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + } + + public override async Task FlushAsync(CancellationToken cancellationToken) => + await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_flush, cancellationToken) + .ConfigureAwait(false); + + private async ValueTask FlushInternalAsync( + ZSTD_EndDirective directive, + CancellationToken cancellationToken = default + ) => await WriteInternalAsync(null, directive, cancellationToken).ConfigureAwait(false); + +#if !LEGACY_DOTNET + private async ValueTask WriteInternalAsync( + ReadOnlyMemory? buffer, + ZSTD_EndDirective directive, + CancellationToken cancellationToken = default + ) +#else + private async ValueTask WriteInternalAsync( + ReadOnlyMemory? buffer, + ZSTD_EndDirective directive, + CancellationToken cancellationToken = default + ) +#endif + { + EnsureNotDisposed(); + + var input = new ZSTD_inBuffer_s + { + pos = 0, + size = buffer.HasValue ? (nuint)buffer.Value.Length : 0, + }; + nuint remaining; + do + { + output.pos = 0; + remaining = CompressStream( + ref input, + buffer.HasValue ? buffer.Value.Span : null, + directive + ); + + var written = (int)output.pos; + if (written > 0) + await innerStream + .WriteAsync(outputBuffer, 0, written, cancellationToken) + .ConfigureAwait(false); + } while ( + directive == ZSTD_EndDirective.ZSTD_e_continue ? input.pos < input.size : remaining > 0 + ); + } + +#if !LEGACY_DOTNET + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => + await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken) + .ConfigureAwait(false); +#else + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => + await WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken) + .ConfigureAwait(false); + + public async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => + await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken) + .ConfigureAwait(false); +#endif +} diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs index df4b4d68..6624e952 100644 --- a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs @@ -7,7 +7,7 @@ using SharpCompress.Compressors.ZStandard.Unsafe; namespace SharpCompress.Compressors.ZStandard; -public class CompressionStream : Stream +public partial class CompressionStream : Stream { private readonly Stream innerStream; private readonly byte[] outputBuffer; @@ -74,26 +74,6 @@ public class CompressionStream : Stream ~CompressionStream() => Dispose(false); -#if !LEGACY_DOTNET - public override async ValueTask DisposeAsync() -#else - public async ValueTask DisposeAsync() -#endif - { - if (compressor == null) - return; - - try - { - await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_end).ConfigureAwait(false); - } - finally - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - } - protected override void Dispose(bool disposing) { if (compressor == null) @@ -131,17 +111,8 @@ public class CompressionStream : Stream public override void Flush() => FlushInternal(ZSTD_EndDirective.ZSTD_e_flush); - public override async Task FlushAsync(CancellationToken cancellationToken) => - await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_flush, cancellationToken) - .ConfigureAwait(false); - private void FlushInternal(ZSTD_EndDirective directive) => WriteInternal(null, directive); - private async ValueTask FlushInternalAsync( - ZSTD_EndDirective directive, - CancellationToken cancellationToken = default - ) => await WriteInternalAsync(null, directive, cancellationToken).ConfigureAwait(false); - public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan(buffer, offset, count)); @@ -176,82 +147,6 @@ public class CompressionStream : Stream ); } -#if !LEGACY_DOTNET - private async ValueTask WriteInternalAsync( - ReadOnlyMemory? buffer, - ZSTD_EndDirective directive, - CancellationToken cancellationToken = default - ) -#else - private async ValueTask WriteInternalAsync( - ReadOnlyMemory? buffer, - ZSTD_EndDirective directive, - CancellationToken cancellationToken = default - ) -#endif - - { - EnsureNotDisposed(); - - var input = new ZSTD_inBuffer_s - { - pos = 0, - size = buffer.HasValue ? (nuint)buffer.Value.Length : 0, - }; - nuint remaining; - do - { - output.pos = 0; - remaining = CompressStream( - ref input, - buffer.HasValue ? buffer.Value.Span : null, - directive - ); - - var written = (int)output.pos; - if (written > 0) - await innerStream - .WriteAsync(outputBuffer, 0, written, cancellationToken) - .ConfigureAwait(false); - } while ( - directive == ZSTD_EndDirective.ZSTD_e_continue ? input.pos < input.size : remaining > 0 - ); - } - -#if !LEGACY_DOTNET - - public override Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); - - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) => - await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken) - .ConfigureAwait(false); -#else - - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => - await WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken) - .ConfigureAwait(false); - - public async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) => - await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellationToken) - .ConfigureAwait(false); -#endif - internal unsafe nuint CompressStream( ref ZSTD_inBuffer_s input, ReadOnlySpan inputBuffer, diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.Async.cs new file mode 100644 index 00000000..5cbdada2 --- /dev/null +++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.Async.cs @@ -0,0 +1,91 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.ZStandard.Unsafe; + +namespace SharpCompress.Compressors.ZStandard; + +public partial class DecompressionStream +{ +#if !LEGACY_DOTNET + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) +#else + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); + + public async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) +#endif + { + EnsureNotDisposed(); + + // Guard against infinite loop (output.pos would never become non-zero) + if (buffer.Length == 0) + { + return 0; + } + + var output = new ZSTD_outBuffer_s { pos = 0, size = (nuint)buffer.Length }; + while (true) + { + // If there is still input available, or there might be data buffered in the decompressor context, flush that out + while (input.pos < input.size || !contextDrained) + { + nuint oldInputPos = input.pos; + nuint result = DecompressStream(ref output, buffer.Span); + if (output.pos > 0 || oldInputPos != input.pos) + { + // Keep result from last decompress call that made some progress, so we known if we're at end of frame + lastDecompressResult = result; + } + // If decompression filled the output buffer, there might still be data buffered in the decompressor context + contextDrained = output.pos < output.size; + // If we have data to return, return it immediately, so we won't stall on Read + if (output.pos > 0) + { + return (int)output.pos; + } + } + + // Otherwise, read some more input + int bytesRead; + if ( + ( + bytesRead = await innerStream + .ReadAsync(inputBuffer, 0, inputBufferSize, cancellationToken) + .ConfigureAwait(false) + ) == 0 + ) + { + if (checkEndOfStream && lastDecompressResult != 0) + { + throw new EndOfStreamException("Premature end of stream"); + } + + return 0; + } + + input.size = (nuint)bytesRead; + input.pos = 0; + } + } +} diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs index c1aed766..152d9e55 100644 --- a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs @@ -7,7 +7,7 @@ using SharpCompress.Compressors.ZStandard.Unsafe; namespace SharpCompress.Compressors.ZStandard; -public class DecompressionStream : Stream +public partial class DecompressionStream : Stream { private readonly Stream innerStream; private readonly byte[] inputBuffer; @@ -158,86 +158,6 @@ public class DecompressionStream : Stream } } -#if !LEGACY_DOTNET - public override Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) -#else - - public override Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); - - public async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) -#endif - { - EnsureNotDisposed(); - - // Guard against infinite loop (output.pos would never become non-zero) - if (buffer.Length == 0) - { - return 0; - } - - var output = new ZSTD_outBuffer_s { pos = 0, size = (nuint)buffer.Length }; - while (true) - { - // If there is still input available, or there might be data buffered in the decompressor context, flush that out - while (input.pos < input.size || !contextDrained) - { - nuint oldInputPos = input.pos; - nuint result = DecompressStream(ref output, buffer.Span); - if (output.pos > 0 || oldInputPos != input.pos) - { - // Keep result from last decompress call that made some progress, so we known if we're at end of frame - lastDecompressResult = result; - } - // If decompression filled the output buffer, there might still be data buffered in the decompressor context - contextDrained = output.pos < output.size; - // If we have data to return, return it immediately, so we won't stall on Read - if (output.pos > 0) - { - return (int)output.pos; - } - } - - // Otherwise, read some more input - int bytesRead; - if ( - ( - bytesRead = await innerStream - .ReadAsync(inputBuffer, 0, inputBufferSize, cancellationToken) - .ConfigureAwait(false) - ) == 0 - ) - { - if (checkEndOfStream && lastDecompressResult != 0) - { - throw new EndOfStreamException("Premature end of stream"); - } - - return 0; - } - - input.size = (nuint)bytesRead; - input.pos = 0; - } - } - private unsafe nuint DecompressStream(ref ZSTD_outBuffer_s output, Span outputBuffer) { fixed (byte* inputBufferPtr = inputBuffer) diff --git a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs new file mode 100644 index 00000000..abef7d42 --- /dev/null +++ b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.ZStandard; + +internal partial class ZStandardStream +{ + internal static async ValueTask IsZStandardAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var buffer = new byte[4]; + var bytesRead = await stream.ReadAsync(buffer, 0, 4, cancellationToken); + if (bytesRead < 4) + { + return false; + } + + var magic = BitConverter.ToUInt32(buffer, 0); + if (ZstandardConstants.MAGIC != magic) + { + return false; + } + return true; + } +} diff --git a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs index c9c23f97..944cdcc9 100644 --- a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.cs @@ -9,7 +9,7 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.ZStandard; -internal class ZStandardStream : DecompressionStream, IStreamStack +internal partial class ZStandardStream : DecompressionStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -44,27 +44,6 @@ internal class ZStandardStream : DecompressionStream, IStreamStack return true; } - internal static async ValueTask IsZStandardAsync( - Stream stream, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - var buffer = new byte[4]; - var bytesRead = await stream.ReadAsync(buffer, 0, 4, cancellationToken); - if (bytesRead < 4) - { - return false; - } - - var magic = BitConverter.ToUInt32(buffer, 0); - if (ZstandardConstants.MAGIC != magic) - { - return false; - } - return true; - } - public ZStandardStream(Stream baseInputStream) : base(baseInputStream) { diff --git a/src/SharpCompress/IO/BufferedSubStream.Async.cs b/src/SharpCompress/IO/BufferedSubStream.Async.cs new file mode 100644 index 00000000..6b0570ab --- /dev/null +++ b/src/SharpCompress/IO/BufferedSubStream.Async.cs @@ -0,0 +1,82 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +internal partial class BufferedSubStream +{ + private async ValueTask RefillCacheAsync(CancellationToken cancellationToken) + { + var count = (int)Math.Min(BytesLeftToRead, _cache.Length); + _cacheOffset = 0; + if (count == 0) + { + _cacheLength = 0; + return; + } + Stream.Position = origin; + _cacheLength = await Stream + .ReadAsync(_cache, 0, count, cancellationToken) + .ConfigureAwait(false); + origin += _cacheLength; + BytesLeftToRead -= _cacheLength; + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (count > Length) + { + count = (int)Length; + } + + if (count > 0) + { + if (_cacheOffset == _cacheLength) + { + await RefillCacheAsync(cancellationToken).ConfigureAwait(false); + } + + count = Math.Min(count, _cacheLength - _cacheOffset); + Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count); + _cacheOffset += count; + } + + return count; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var count = buffer.Length; + if (count > Length) + { + count = (int)Length; + } + + if (count > 0) + { + if (_cacheOffset == _cacheLength) + { + await RefillCacheAsync(cancellationToken).ConfigureAwait(false); + } + + count = Math.Min(count, _cacheLength - _cacheOffset); + _cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span); + _cacheOffset += count; + } + + return count; + } +#endif +} diff --git a/src/SharpCompress/IO/BufferedSubStream.cs b/src/SharpCompress/IO/BufferedSubStream.cs index 7a51bb0f..104eb125 100755 --- a/src/SharpCompress/IO/BufferedSubStream.cs +++ b/src/SharpCompress/IO/BufferedSubStream.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace SharpCompress.IO; -internal class BufferedSubStream : SharpCompressStream, IStreamStack +internal partial class BufferedSubStream : SharpCompressStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -74,23 +74,6 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack BytesLeftToRead -= _cacheLength; } - private async ValueTask RefillCacheAsync(CancellationToken cancellationToken) - { - var count = (int)Math.Min(BytesLeftToRead, _cache.Length); - _cacheOffset = 0; - if (count == 0) - { - _cacheLength = 0; - return; - } - Stream.Position = origin; - _cacheLength = await Stream - .ReadAsync(_cache, 0, count, cancellationToken) - .ConfigureAwait(false); - origin += _cacheLength; - BytesLeftToRead -= _cacheLength; - } - public override int Read(byte[] buffer, int offset, int count) { if (count > Length) @@ -127,61 +110,6 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack return _cache[_cacheOffset++]; } - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (count > Length) - { - count = (int)Length; - } - - if (count > 0) - { - if (_cacheOffset == _cacheLength) - { - await RefillCacheAsync(cancellationToken).ConfigureAwait(false); - } - - count = Math.Min(count, _cacheLength - _cacheOffset); - Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count); - _cacheOffset += count; - } - - return count; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - var count = buffer.Length; - if (count > Length) - { - count = (int)Length; - } - - if (count > 0) - { - if (_cacheOffset == _cacheLength) - { - await RefillCacheAsync(cancellationToken).ConfigureAwait(false); - } - - count = Math.Min(count, _cacheLength - _cacheOffset); - _cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span); - _cacheOffset += count; - } - - return count; - } -#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/IO/ProgressReportingStream.Async.cs b/src/SharpCompress/IO/ProgressReportingStream.Async.cs new file mode 100644 index 00000000..5fed0c61 --- /dev/null +++ b/src/SharpCompress/IO/ProgressReportingStream.Async.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +internal sealed partial class ProgressReportingStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + var bytesRead = await _baseStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (bytesRead > 0) + { + _bytesTransferred += bytesRead; + ReportProgress(); + } + return bytesRead; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var bytesRead = await _baseStream + .ReadAsync(buffer, cancellationToken) + .ConfigureAwait(false); + if (bytesRead > 0) + { + _bytesTransferred += bytesRead; + ReportProgress(); + } + return bytesRead; + } +#endif + +#if !LEGACY_DOTNET + public override async ValueTask DisposeAsync() + { + if (!_leaveOpen) + { + await _baseStream.DisposeAsync().ConfigureAwait(false); + } + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/src/SharpCompress/IO/ProgressReportingStream.cs b/src/SharpCompress/IO/ProgressReportingStream.cs index efadba41..ec2f4fad 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.cs @@ -1,7 +1,5 @@ using System; using System.IO; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.IO; @@ -10,7 +8,7 @@ namespace SharpCompress.IO; /// A stream wrapper that reports progress as data is read from the source. /// Used to track compression or extraction progress by wrapping the source stream. /// -internal sealed class ProgressReportingStream : Stream +internal sealed partial class ProgressReportingStream : Stream { private readonly Stream _baseStream; private readonly IProgress _progress; @@ -77,42 +75,6 @@ internal sealed class ProgressReportingStream : Stream } #endif - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - var bytesRead = await _baseStream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (bytesRead > 0) - { - _bytesTransferred += bytesRead; - ReportProgress(); - } - return bytesRead; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - var bytesRead = await _baseStream - .ReadAsync(buffer, cancellationToken) - .ConfigureAwait(false); - if (bytesRead > 0) - { - _bytesTransferred += bytesRead; - ReportProgress(); - } - return bytesRead; - } -#endif - public override int ReadByte() { var value = _baseStream.ReadByte(); @@ -146,15 +108,4 @@ internal sealed class ProgressReportingStream : Stream } base.Dispose(disposing); } - -#if !LEGACY_DOTNET - public override async ValueTask DisposeAsync() - { - if (!_leaveOpen) - { - await _baseStream.DisposeAsync().ConfigureAwait(false); - } - await base.DisposeAsync().ConfigureAwait(false); - } -#endif } diff --git a/src/SharpCompress/IO/ReadOnlySubStream.Async.cs b/src/SharpCompress/IO/ReadOnlySubStream.Async.cs new file mode 100644 index 00000000..42ef8c5e --- /dev/null +++ b/src/SharpCompress/IO/ReadOnlySubStream.Async.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +internal partial class ReadOnlySubStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (BytesLeftToRead < count) + { + count = (int)BytesLeftToRead; + } + var read = await Stream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + if (read > 0) + { + BytesLeftToRead -= read; + _position += read; + } + return read; + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length; + var read = await Stream + .ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken) + .ConfigureAwait(false); + if (read > 0) + { + BytesLeftToRead -= read; + _position += read; + } + return read; + } +#endif +} diff --git a/src/SharpCompress/IO/ReadOnlySubStream.cs b/src/SharpCompress/IO/ReadOnlySubStream.cs index c9d7d830..e98cabd0 100644 --- a/src/SharpCompress/IO/ReadOnlySubStream.cs +++ b/src/SharpCompress/IO/ReadOnlySubStream.cs @@ -1,12 +1,10 @@ using System; using System.Diagnostics; using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace SharpCompress.IO; -internal class ReadOnlySubStream : SharpCompressStream, IStreamStack +internal partial class ReadOnlySubStream : SharpCompressStream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -95,47 +93,6 @@ internal class ReadOnlySubStream : SharpCompressStream, IStreamStack } #endif - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (BytesLeftToRead < count) - { - count = (int)BytesLeftToRead; - } - var read = await Stream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - if (read > 0) - { - BytesLeftToRead -= read; - _position += read; - } - return read; - } - -#if !LEGACY_DOTNET - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length; - var read = await Stream - .ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken) - .ConfigureAwait(false); - if (read > 0) - { - BytesLeftToRead -= read; - _position += 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/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs new file mode 100644 index 00000000..8e2adc8f --- /dev/null +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -0,0 +1,179 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +public partial class SharpCompressStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (count == 0) + return 0; + + if (_bufferingEnabled) + { + ValidateBufferState(); + + // Fill buffer if needed + if (_bufferedLength == 0) + { + _bufferedLength = await Stream + .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) + .ConfigureAwait(false); + _bufferPosition = 0; + } + int available = _bufferedLength - _bufferPosition; + int toRead = Math.Min(count, available); + if (toRead > 0) + { + Array.Copy(_buffer!, _bufferPosition, buffer, offset, toRead); + _bufferPosition += toRead; + _internalPosition += toRead; + return toRead; + } + // If buffer exhausted, refill + int r = await Stream + .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) + .ConfigureAwait(false); + if (r == 0) + return 0; + _bufferedLength = r; + _bufferPosition = 0; + if (_bufferedLength == 0) + { + return 0; + } + toRead = Math.Min(count, _bufferedLength); + Array.Copy(_buffer!, 0, buffer, offset, toRead); + _bufferPosition = toRead; + _internalPosition += toRead; + return toRead; + } + else + { + int read = await Stream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + _internalPosition += read; + return read; + } + } + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + await Stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + _internalPosition += count; + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await Stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (buffer.Length == 0) + return 0; + + if (_bufferingEnabled) + { + ValidateBufferState(); + + // Fill buffer if needed + if (_bufferedLength == 0) + { + _bufferedLength = await Stream + .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + .ConfigureAwait(false); + _bufferPosition = 0; + } + int available = _bufferedLength - _bufferPosition; + int toRead = Math.Min(buffer.Length, available); + if (toRead > 0) + { + _buffer.AsSpan(_bufferPosition, toRead).CopyTo(buffer.Span); + _bufferPosition += toRead; + _internalPosition += toRead; + return toRead; + } + // If buffer exhausted, refill + int r = await Stream + .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) + .ConfigureAwait(false); + if (r == 0) + return 0; + _bufferedLength = r; + _bufferPosition = 0; + if (_bufferedLength == 0) + { + return 0; + } + toRead = Math.Min(buffer.Length, _bufferedLength); + _buffer.AsSpan(0, toRead).CopyTo(buffer.Span); + _bufferPosition = toRead; + _internalPosition += toRead; + return toRead; + } + else + { + int read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _internalPosition += read; + return read; + } + } + + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + await Stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + _internalPosition += buffer.Length; + } + + public override async ValueTask DisposeAsync() + { +#if DEBUG_STREAMS + this.DebugDispose(typeof(SharpCompressStream)); +#endif + if (_isDisposed || LeaveOpen) + { + return; + } + if (ThrowOnDispose) + { + throw new InvalidOperationException( + $"Attempt to dispose of a {nameof(SharpCompressStream)} when {nameof(ThrowOnDispose)} is {ThrowOnDispose}" + ); + } + _isDisposed = true; + await base.DisposeAsync(); + + await Stream.DisposeAsync(); + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null; + } + } + +#endif +} diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index ba3fb2a2..f5283b5e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace SharpCompress.IO; -public class SharpCompressStream : Stream, IStreamStack +public partial class SharpCompressStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -304,173 +304,4 @@ public class SharpCompressStream : Stream, IStreamStack Stream.Write(buffer, offset, count); _internalPosition += count; } - - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (count == 0) - return 0; - - if (_bufferingEnabled) - { - ValidateBufferState(); - - // Fill buffer if needed - if (_bufferedLength == 0) - { - _bufferedLength = await Stream - .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) - .ConfigureAwait(false); - _bufferPosition = 0; - } - int available = _bufferedLength - _bufferPosition; - int toRead = Math.Min(count, available); - if (toRead > 0) - { - Array.Copy(_buffer!, _bufferPosition, buffer, offset, toRead); - _bufferPosition += toRead; - _internalPosition += toRead; - return toRead; - } - // If buffer exhausted, refill - int r = await Stream - .ReadAsync(_buffer!, 0, _bufferSize, cancellationToken) - .ConfigureAwait(false); - if (r == 0) - return 0; - _bufferedLength = r; - _bufferPosition = 0; - if (_bufferedLength == 0) - { - return 0; - } - toRead = Math.Min(count, _bufferedLength); - Array.Copy(_buffer!, 0, buffer, offset, toRead); - _bufferPosition = toRead; - _internalPosition += toRead; - return toRead; - } - else - { - int read = await Stream - .ReadAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - _internalPosition += read; - return read; - } - } - - public override async Task WriteAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - await Stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - _internalPosition += count; - } - - public override async Task FlushAsync(CancellationToken cancellationToken) - { - await Stream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - -#if !LEGACY_DOTNET - - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (buffer.Length == 0) - return 0; - - if (_bufferingEnabled) - { - ValidateBufferState(); - - // Fill buffer if needed - if (_bufferedLength == 0) - { - _bufferedLength = await Stream - .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) - .ConfigureAwait(false); - _bufferPosition = 0; - } - int available = _bufferedLength - _bufferPosition; - int toRead = Math.Min(buffer.Length, available); - if (toRead > 0) - { - _buffer.AsSpan(_bufferPosition, toRead).CopyTo(buffer.Span); - _bufferPosition += toRead; - _internalPosition += toRead; - return toRead; - } - // If buffer exhausted, refill - int r = await Stream - .ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken) - .ConfigureAwait(false); - if (r == 0) - return 0; - _bufferedLength = r; - _bufferPosition = 0; - if (_bufferedLength == 0) - { - return 0; - } - toRead = Math.Min(buffer.Length, _bufferedLength); - _buffer.AsSpan(0, toRead).CopyTo(buffer.Span); - _bufferPosition = toRead; - _internalPosition += toRead; - return toRead; - } - else - { - int read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - _internalPosition += read; - return read; - } - } - - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default - ) - { - await Stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - _internalPosition += buffer.Length; - } - - public override async ValueTask DisposeAsync() - { -#if DEBUG_STREAMS - this.DebugDispose(typeof(SharpCompressStream)); -#endif - if (_isDisposed || LeaveOpen) - { - return; - } - if (ThrowOnDispose) - { - throw new InvalidOperationException( - $"Attempt to dispose of a {nameof(SharpCompressStream)} when {nameof(ThrowOnDispose)} is {ThrowOnDispose}" - ); - } - _isDisposed = true; - await base.DisposeAsync(); - - await Stream.DisposeAsync(); - if (_buffer != null) - { - ArrayPool.Shared.Return(_buffer); - _buffer = null; - } - } - -#endif } diff --git a/src/SharpCompress/IO/SourceStream.Async.cs b/src/SharpCompress/IO/SourceStream.Async.cs new file mode 100644 index 00000000..73576fc2 --- /dev/null +++ b/src/SharpCompress/IO/SourceStream.Async.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +public partial class SourceStream +{ + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (count <= 0) + { + return 0; + } + + var total = count; + var r = -1; + + while (count != 0 && r != 0) + { + r = await Current + .ReadAsync( + buffer, + offset, + (int)Math.Min(count, Current.Length - Current.Position), + cancellationToken + ) + .ConfigureAwait(false); + count -= r; + offset += r; + + if (!IsVolumes && count != 0 && Current.Position == Current.Length) + { + var length = Current.Length; + + // Load next file if present + if (!SetStream(_stream + 1)) + { + break; + } + + // Current stream switched + // Add length of previous stream + _prevSize += length; + Current.Seek(0, SeekOrigin.Begin); + r = -1; //BugFix: reset to allow loop if count is still not 0 - was breaking split zipx (lzma xz etc) + } + } + + return total - count; + } + +#if !LEGACY_DOTNET + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (buffer.Length <= 0) + { + return 0; + } + + var total = buffer.Length; + var count = buffer.Length; + var offset = 0; + var r = -1; + + while (count != 0 && r != 0) + { + r = await Current + .ReadAsync( + buffer.Slice(offset, (int)Math.Min(count, Current.Length - Current.Position)), + cancellationToken + ) + .ConfigureAwait(false); + count -= r; + offset += r; + + if (!IsVolumes && count != 0 && Current.Position == Current.Length) + { + var length = Current.Length; + + // Load next file if present + if (!SetStream(_stream + 1)) + { + break; + } + + // Current stream switched + // Add length of previous stream + _prevSize += length; + Current.Seek(0, SeekOrigin.Begin); + r = -1; + } + } + + return total - count; + } +#endif +} diff --git a/src/SharpCompress/IO/SourceStream.cs b/src/SharpCompress/IO/SourceStream.cs index e9cc74a6..53551186 100644 --- a/src/SharpCompress/IO/SourceStream.cs +++ b/src/SharpCompress/IO/SourceStream.cs @@ -8,7 +8,7 @@ using SharpCompress.Readers; namespace SharpCompress.IO; -public class SourceStream : Stream, IStreamStack +public partial class SourceStream : Stream, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } @@ -240,103 +240,7 @@ public class SourceStream : Stream, IStreamStack public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); - public override async Task ReadAsync( - byte[] buffer, - int offset, - int count, - CancellationToken cancellationToken - ) - { - if (count <= 0) - { - return 0; - } - - var total = count; - var r = -1; - - while (count != 0 && r != 0) - { - r = await Current - .ReadAsync( - buffer, - offset, - (int)Math.Min(count, Current.Length - Current.Position), - cancellationToken - ) - .ConfigureAwait(false); - count -= r; - offset += r; - - if (!IsVolumes && count != 0 && Current.Position == Current.Length) - { - var length = Current.Length; - - // Load next file if present - if (!SetStream(_stream + 1)) - { - break; - } - - // Current stream switched - // Add length of previous stream - _prevSize += length; - Current.Seek(0, SeekOrigin.Begin); - r = -1; //BugFix: reset to allow loop if count is still not 0 - was breaking split zipx (lzma xz etc) - } - } - - return total - count; - } - #if !LEGACY_DOTNET - - public override async ValueTask ReadAsync( - Memory buffer, - CancellationToken cancellationToken = default - ) - { - if (buffer.Length <= 0) - { - return 0; - } - - var total = buffer.Length; - var count = buffer.Length; - var offset = 0; - var r = -1; - - while (count != 0 && r != 0) - { - r = await Current - .ReadAsync( - buffer.Slice(offset, (int)Math.Min(count, Current.Length - Current.Position)), - cancellationToken - ) - .ConfigureAwait(false); - count -= r; - offset += r; - - if (!IsVolumes && count != 0 && Current.Position == Current.Length) - { - var length = Current.Length; - - // Load next file if present - if (!SetStream(_stream + 1)) - { - break; - } - - // Current stream switched - // Add length of previous stream - _prevSize += length; - Current.Seek(0, SeekOrigin.Begin); - r = -1; - } - } - - return total - count; - } #endif public override void Close() diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs new file mode 100644 index 00000000..9dacd88e --- /dev/null +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.IO; + +namespace SharpCompress.Readers; + +public abstract partial class AbstractReader + where TEntry : Entry + where TVolume : Volume +{ + public virtual async ValueTask DisposeAsync() + { + if (_entriesForCurrentReadStreamAsync is not null) + { + await _entriesForCurrentReadStreamAsync.DisposeAsync(); + } + + // If Volume implements IAsyncDisposable, use async disposal + if (Volume is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + Volume?.Dispose(); + } + } + + public async ValueTask MoveToNextEntryAsync(CancellationToken cancellationToken = default) + { + if (_completed) + { + return false; + } + if (Cancelled) + { + throw new ReaderCancelledException("Reader has been cancelled."); + } + if (_entriesForCurrentReadStreamAsync is null) + { + return await LoadStreamForReadingAsync(RequestInitialStream()); + } + if (!_wroteCurrentEntry) + { + await SkipEntryAsync(cancellationToken).ConfigureAwait(false); + } + _wroteCurrentEntry = false; + if (await NextEntryForCurrentStreamAsync(cancellationToken)) + { + return true; + } + _completed = true; + return false; + } + + protected async ValueTask LoadStreamForReadingAsync(Stream stream) + { + if (_entriesForCurrentReadStreamAsync is not null) + { + await _entriesForCurrentReadStreamAsync.DisposeAsync(); + } + if (stream is null || !stream.CanRead) + { + throw new MultipartStreamRequiredException( + "File is split into multiple archives: '" + + Entry.Key + + "'. A new readable stream is required. Use Cancel if it was intended." + ); + } + _entriesForCurrentReadStreamAsync = GetEntriesAsync(stream).GetAsyncEnumerator(); + return await _entriesForCurrentReadStreamAsync.MoveNextAsync(); + } + + private async ValueTask SkipEntryAsync(CancellationToken cancellationToken) + { + if (!Entry.IsDirectory) + { + await SkipAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask SkipAsync(CancellationToken cancellationToken) + { + var part = Entry.Parts.First(); + + if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0) + { + //not solid and has a known compressed size then we can skip raw bytes. + var rawStream = part.GetRawStream(); + + if (rawStream != null) + { + var bytesToAdvance = Entry.CompressedSize; + await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false); + part.Skipped = true; + return; + } + } + //don't know the size so we have to try to decompress to skip +#if LEGACY_DOTNET + using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); +#else + await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); +#endif + } + + public async ValueTask WriteEntryToAsync( + Stream writableStream, + CancellationToken cancellationToken = default + ) + { + if (_wroteCurrentEntry) + { + throw new ArgumentException( + "WriteEntryToAsync or OpenEntryStreamAsync can only be called once." + ); + } + + if (writableStream is null) + { + throw new ArgumentNullException(nameof(writableStream)); + } + if (!writableStream.CanWrite) + { + throw new ArgumentException( + "A writable Stream was required. Use Cancel if that was intended." + ); + } + + await WriteAsync(writableStream, cancellationToken).ConfigureAwait(false); + _wroteCurrentEntry = true; + } + + internal async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) + { +#if LEGACY_DOTNET + using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + var sourceStream = WrapWithProgress(s, Entry); + await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); +#else + await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); + var sourceStream = WrapWithProgress(s, Entry); + await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); +#endif + } + + public async ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (_wroteCurrentEntry) + { + throw new ArgumentException( + "WriteEntryToAsync or OpenEntryStreamAsync can only be called once." + ); + } + var stream = await GetEntryStreamAsync(cancellationToken).ConfigureAwait(false); + _wroteCurrentEntry = true; + return stream; + } + + protected virtual async ValueTask GetEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + var stream = await Entry + .Parts.First() + .GetCompressedStreamAsync(cancellationToken) + .ConfigureAwait(false); + return CreateEntryStream(stream); + } + + internal virtual ValueTask NextEntryForCurrentStreamAsync() => + _entriesForCurrentReadStreamAsync.NotNull().MoveNextAsync(); + + /// + /// Moves the current async enumerator to the next entry. + /// + internal virtual ValueTask NextEntryForCurrentStreamAsync( + CancellationToken cancellationToken + ) + { + if (_entriesForCurrentReadStreamAsync is not null) + { + return _entriesForCurrentReadStreamAsync.MoveNextAsync(); + } + + return new ValueTask(NextEntryForCurrentStream()); + } + + // Async iterator method + protected virtual async IAsyncEnumerable GetEntriesAsync(Stream stream) + { + await Task.CompletedTask; + foreach (var entry in GetEntries(stream)) + { + yield return entry; + } + } +} diff --git a/src/SharpCompress/Readers/AbstractReader.cs b/src/SharpCompress/Readers/AbstractReader.cs index ed495d42..18f214a7 100644 --- a/src/SharpCompress/Readers/AbstractReader.cs +++ b/src/SharpCompress/Readers/AbstractReader.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -12,7 +10,7 @@ namespace SharpCompress.Readers; /// /// A generic push reader that reads unseekable comrpessed streams. /// -public abstract class AbstractReader : IReader, IAsyncReader +public abstract partial class AbstractReader : IReader, IAsyncReader where TEntry : Entry where TVolume : Volume { @@ -61,24 +59,6 @@ public abstract class AbstractReader : IReader, IAsyncReader Volume?.Dispose(); } - public virtual async ValueTask DisposeAsync() - { - if (_entriesForCurrentReadStreamAsync is not null) - { - await _entriesForCurrentReadStreamAsync.DisposeAsync(); - } - - // If Volume implements IAsyncDisposable, use async disposal - if (Volume is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else - { - Volume?.Dispose(); - } - } - #endregion public bool Cancelled { get; private set; } @@ -129,33 +109,6 @@ public abstract class AbstractReader : IReader, IAsyncReader return false; } - public async ValueTask MoveToNextEntryAsync(CancellationToken cancellationToken = default) - { - if (_completed) - { - return false; - } - if (Cancelled) - { - throw new ReaderCancelledException("Reader has been cancelled."); - } - if (_entriesForCurrentReadStreamAsync is null) - { - return await LoadStreamForReadingAsync(RequestInitialStream()); - } - if (!_wroteCurrentEntry) - { - await SkipEntryAsync(cancellationToken).ConfigureAwait(false); - } - _wroteCurrentEntry = false; - if (await NextEntryForCurrentStreamAsync(cancellationToken)) - { - return true; - } - _completed = true; - return false; - } - protected bool LoadStreamForReading(Stream stream) { if (_entriesForCurrentReadStreamAsync is not null) @@ -177,59 +130,14 @@ public abstract class AbstractReader : IReader, IAsyncReader return _entriesForCurrentReadStream.MoveNext(); } - protected async ValueTask LoadStreamForReadingAsync(Stream stream) - { - if (_entriesForCurrentReadStreamAsync is not null) - { - await _entriesForCurrentReadStreamAsync.DisposeAsync(); - } - if (stream is null || !stream.CanRead) - { - throw new MultipartStreamRequiredException( - "File is split into multiple archives: '" - + Entry.Key - + "'. A new readable stream is required. Use Cancel if it was intended." - ); - } - _entriesForCurrentReadStreamAsync = GetEntriesAsync(stream).GetAsyncEnumerator(); - return await _entriesForCurrentReadStreamAsync.MoveNextAsync(); - } - protected virtual Stream RequestInitialStream() => Volume.NotNull("Volume isn't loaded.").Stream; internal virtual bool NextEntryForCurrentStream() => _entriesForCurrentReadStream.NotNull().MoveNext(); - internal virtual ValueTask NextEntryForCurrentStreamAsync() => - _entriesForCurrentReadStreamAsync.NotNull().MoveNextAsync(); - - /// - /// Moves the current async enumerator to the next entry. - /// - internal virtual ValueTask NextEntryForCurrentStreamAsync( - CancellationToken cancellationToken - ) - { - if (_entriesForCurrentReadStreamAsync is not null) - { - return _entriesForCurrentReadStreamAsync.MoveNextAsync(); - } - - return new ValueTask(NextEntryForCurrentStream()); - } - protected abstract IEnumerable GetEntries(Stream stream); - protected virtual async IAsyncEnumerable GetEntriesAsync(Stream stream) - { - await Task.CompletedTask; - foreach (var entry in GetEntries(stream)) - { - yield return entry; - } - } - #region Entry Skip/Write private void SkipEntry() @@ -240,14 +148,6 @@ public abstract class AbstractReader : IReader, IAsyncReader } } - private async ValueTask SkipEntryAsync(CancellationToken cancellationToken) - { - if (!Entry.IsDirectory) - { - await SkipAsync(cancellationToken).ConfigureAwait(false); - } - } - private void Skip() { var part = Entry.Parts.First(); @@ -270,33 +170,6 @@ public abstract class AbstractReader : IReader, IAsyncReader s.SkipEntry(); } - private async ValueTask SkipAsync(CancellationToken cancellationToken) - { - var part = Entry.Parts.First(); - - if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0) - { - //not solid and has a known compressed size then we can skip raw bytes. - var rawStream = part.GetRawStream(); - - if (rawStream != null) - { - var bytesToAdvance = Entry.CompressedSize; - await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false); - part.Skipped = true; - return; - } - } - //don't know the size so we have to try to decompress to skip -#if LEGACY_DOTNET - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#else - await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); -#endif - } - public void WriteEntryTo(Stream writableStream) { if (_wroteCurrentEntry) @@ -319,33 +192,6 @@ public abstract class AbstractReader : IReader, IAsyncReader _wroteCurrentEntry = true; } - public async ValueTask WriteEntryToAsync( - Stream writableStream, - CancellationToken cancellationToken = default - ) - { - if (_wroteCurrentEntry) - { - throw new ArgumentException( - "WriteEntryToAsync or OpenEntryStream can only be called once." - ); - } - - if (writableStream is null) - { - throw new ArgumentNullException(nameof(writableStream)); - } - if (!writableStream.CanWrite) - { - throw new ArgumentException( - "A writable Stream was required. Use Cancel if that was intended." - ); - } - - await WriteAsync(writableStream, cancellationToken).ConfigureAwait(false); - _wroteCurrentEntry = true; - } - internal void Write(Stream writeStream) { using Stream s = OpenEntryStream(); @@ -353,19 +199,6 @@ public abstract class AbstractReader : IReader, IAsyncReader sourceStream.CopyTo(writeStream, 81920); } - internal async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) - { -#if LEGACY_DOTNET - using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - var sourceStream = WrapWithProgress(s, Entry); - await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); -#else - await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); - var sourceStream = WrapWithProgress(s, Entry); - await sourceStream.CopyToAsync(writeStream, 81920, cancellationToken).ConfigureAwait(false); -#endif - } - private Stream WrapWithProgress(Stream source, Entry entry) { var progress = Options.Progress; @@ -411,21 +244,6 @@ public abstract class AbstractReader : IReader, IAsyncReader return stream; } - public async ValueTask OpenEntryStreamAsync( - CancellationToken cancellationToken = default - ) - { - if (_wroteCurrentEntry) - { - throw new ArgumentException( - "WriteEntryToAsync or OpenEntryStreamAsync can only be called once." - ); - } - var stream = await GetEntryStreamAsync(cancellationToken).ConfigureAwait(false); - _wroteCurrentEntry = true; - return stream; - } - /// /// Retains a reference to the entry stream, so we can check whether it completed later. /// @@ -435,17 +253,6 @@ public abstract class AbstractReader : IReader, IAsyncReader protected virtual EntryStream GetEntryStream() => CreateEntryStream(Entry.Parts.First().GetCompressedStream()); - protected virtual async ValueTask GetEntryStreamAsync( - CancellationToken cancellationToken = default - ) - { - var stream = await Entry - .Parts.First() - .GetCompressedStreamAsync(cancellationToken) - .ConfigureAwait(false); - return CreateEntryStream(stream); - } - #endregion IEntry IReader.Entry => Entry; diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Async.cs b/src/SharpCompress/Readers/GZip/GZipReader.Async.cs new file mode 100644 index 00000000..720cb792 --- /dev/null +++ b/src/SharpCompress/Readers/GZip/GZipReader.Async.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.IO; +using SharpCompress.Common; +using SharpCompress.Common.GZip; + +namespace SharpCompress.Readers.GZip; + +public partial class GZipReader +{ + /// + /// Returns entries asynchronously for streams that only support async reads. + /// + 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 48ff47cc..eb09ab6c 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.cs @@ -15,8 +15,5 @@ public partial class GZipReader : AbstractReader protected override IEnumerable GetEntries(Stream stream) => GZipEntry.GetEntries(stream, Options); - protected override IAsyncEnumerable GetEntriesAsync(Stream stream) - { - return GZipEntry.GetEntriesAsync(stream, Options); - } + // GetEntriesAsync moved to GZipReader.Async.cs } diff --git a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs new file mode 100644 index 00000000..7107adba --- /dev/null +++ b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs @@ -0,0 +1,84 @@ +#nullable disable + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; + +namespace SharpCompress.Readers.Rar; + +internal partial class MultiVolumeRarReader : RarReader +{ + protected override IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() + { + var enumerator = new MultiVolumeStreamAsyncEnumerator(this, streams, tempStream); + tempStream = null; + return enumerator; + } + + private class MultiVolumeStreamAsyncEnumerator + : IAsyncEnumerable, + IAsyncEnumerator + { + private readonly MultiVolumeRarReader reader; + private readonly IEnumerator nextReadableStreams; + private Stream tempStream; + private bool isFirst = true; + + internal MultiVolumeStreamAsyncEnumerator( + MultiVolumeRarReader r, + IEnumerator nextReadableStreams, + Stream tempStream + ) + { + reader = r; + this.nextReadableStreams = nextReadableStreams; + this.tempStream = tempStream; + } + + public FilePart Current { get; private set; } + + public async ValueTask MoveNextAsync() + { + if (isFirst) + { + Current = reader.Entry.Parts.First(); + isFirst = false; //first stream already to go + return true; + } + + if (!reader.Entry.IsSplitAfter) + { + return false; + } + if (tempStream != null) + { + await reader.LoadStreamForReadingAsync(tempStream); + tempStream = null; + } + else if (!nextReadableStreams.MoveNext()) + { + throw new MultiVolumeExtractionException( + "No stream provided when requested by MultiVolumeRarReader" + ); + } + else + { + await reader.LoadStreamForReadingAsync(nextReadableStreams.Current); + } + + Current = reader.Entry.Parts.First(); + return true; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = new() + ) => this; + + public ValueTask DisposeAsync() => new(); + } +} diff --git a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs index 56d7996f..dd811bc5 100644 --- a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs +++ b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs @@ -11,7 +11,7 @@ using SharpCompress.Common.Rar; namespace SharpCompress.Readers.Rar; -internal class MultiVolumeRarReader : RarReader +internal partial class MultiVolumeRarReader : RarReader { private readonly IEnumerator streams; private Stream tempStream; @@ -49,12 +49,7 @@ internal class MultiVolumeRarReader : RarReader return enumerator; } - protected override IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() - { - var enumerator = new MultiVolumeStreamAsyncEnumerator(this, streams, tempStream); - tempStream = null; - return enumerator; - } + // Async method and MultiVolumeStreamAsyncEnumerator moved to MultiVolumeRarReader.Async.cs private class MultiVolumeStreamEnumerator : IEnumerable, IEnumerator { @@ -119,66 +114,4 @@ internal class MultiVolumeRarReader : RarReader public void Reset() { } } - - private class MultiVolumeStreamAsyncEnumerator - : IAsyncEnumerable, - IAsyncEnumerator - { - private readonly MultiVolumeRarReader reader; - private readonly IEnumerator nextReadableStreams; - private Stream tempStream; - private bool isFirst = true; - - internal MultiVolumeStreamAsyncEnumerator( - MultiVolumeRarReader r, - IEnumerator nextReadableStreams, - Stream tempStream - ) - { - reader = r; - this.nextReadableStreams = nextReadableStreams; - this.tempStream = tempStream; - } - - public FilePart Current { get; private set; } - - public async ValueTask MoveNextAsync() - { - if (isFirst) - { - Current = reader.Entry.Parts.First(); - isFirst = false; //first stream already to go - return true; - } - - if (!reader.Entry.IsSplitAfter) - { - return false; - } - if (tempStream != null) - { - await reader.LoadStreamForReadingAsync(tempStream); - tempStream = null; - } - else if (!nextReadableStreams.MoveNext()) - { - throw new MultiVolumeExtractionException( - "No stream provided when requested by MultiVolumeRarReader" - ); - } - else - { - await reader.LoadStreamForReadingAsync(nextReadableStreams.Current); - } - - Current = reader.Entry.Parts.First(); - return true; - } - - public IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = new() - ) => this; - - public ValueTask DisposeAsync() => new(); - } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.Async.cs b/src/SharpCompress/Readers/Rar/RarReader.Async.cs new file mode 100644 index 00000000..0e1943d2 --- /dev/null +++ b/src/SharpCompress/Readers/Rar/RarReader.Async.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.Compressors.Rar; + +namespace SharpCompress.Readers.Rar; + +public abstract partial class RarReader +{ + /// + /// Returns file parts asynchronously for the current entry. + /// Used for async stream operations in solid RAR archives. + /// + protected virtual IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() => + Entry.Parts.ToAsyncEnumerable(); + + /// + /// Asynchronously creates an entry stream for the current entry. + /// Supports both RAR v3 and v5 archives with proper CRC verification. + /// + protected override async ValueTask GetEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (Entry.IsRedir) + { + throw new InvalidOperationException("no stream for redirect entry"); + } + + var stream = await MultiVolumeReadOnlyAsyncStream.Create( + CreateFilePartEnumerableForCurrentEntryAsync().CastAsync() + ); + if (Entry.IsRarV3) + { + return CreateEntryStream( + await RarCrcStream + .CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken) + .ConfigureAwait(false) + ); + } + + if (Entry.FileHeader.FileCrc?.Length > 5) + { + return CreateEntryStream( + await RarBLAKE2spStream + .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) + .ConfigureAwait(false) + ); + } + + return CreateEntryStream( + await RarCrcStream + .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) + .ConfigureAwait(false) + ); + } +} diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index dc2909b9..949263d8 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -100,9 +100,6 @@ public abstract partial class RarReader : AbstractReader CreateFilePartEnumerableForCurrentEntry() => Entry.Parts; - protected virtual IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() => - Entry.Parts.ToAsyncEnumerable(); - protected override EntryStream GetEntryStream() { if (Entry.IsRedir) @@ -128,40 +125,5 @@ public abstract partial class RarReader : AbstractReader GetEntryStreamAsync( - System.Threading.CancellationToken cancellationToken = default - ) - { - if (Entry.IsRedir) - { - throw new InvalidOperationException("no stream for redirect entry"); - } - - var stream = await MultiVolumeReadOnlyAsyncStream.Create( - CreateFilePartEnumerableForCurrentEntryAsync().CastAsync() - ); - if (Entry.IsRarV3) - { - return CreateEntryStream( - await RarCrcStream - .CreateAsync(UnpackV1.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) - ); - } - - if (Entry.FileHeader.FileCrc?.Length > 5) - { - return CreateEntryStream( - await RarBLAKE2spStream - .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) - ); - } - - return CreateEntryStream( - await RarCrcStream - .CreateAsync(UnpackV2017.Value, Entry.FileHeader, stream, cancellationToken) - .ConfigureAwait(false) - ); - } + // GetEntryStreamAsync moved to RarReader.Async.cs } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Async.cs b/src/SharpCompress/Readers/Tar/TarReader.Async.cs new file mode 100644 index 00000000..60026308 --- /dev/null +++ b/src/SharpCompress/Readers/Tar/TarReader.Async.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.IO; +using SharpCompress.Archives.GZip; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using SharpCompress.Common.Tar; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Compressors.LZMA; +using SharpCompress.Compressors.Lzw; +using SharpCompress.Compressors.Xz; +using SharpCompress.Compressors.ZStandard; +using SharpCompress.IO; + +namespace SharpCompress.Readers.Tar; + +public partial class TarReader +{ + /// + /// Returns entries asynchronously for streams that only support async reads. + /// Uses async decompression for compressed tar archives (gzip, bzip2, zstandard, etc.). + /// + protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => + TarEntry.GetEntriesAsync( + StreamingMode.Streaming, + stream, + compressionType, + Options.ArchiveEncoding + ); +} diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index f3a344da..be9bf45d 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -124,11 +124,5 @@ public partial class TarReader : AbstractReader Options.ArchiveEncoding ); - protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => - TarEntry.GetEntriesAsync( - StreamingMode.Streaming, - stream, - compressionType, - Options.ArchiveEncoding - ); + // GetEntriesAsync moved to TarReader.Async.cs } diff --git a/src/SharpCompress/Readers/Zip/ZipReader.Async.cs b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs new file mode 100644 index 00000000..b8c71eaa --- /dev/null +++ b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; + +namespace SharpCompress.Readers.Zip; + +public partial class ZipReader +{ + /// + /// Adapts an async header sequence into an async entry sequence. + /// + private sealed class ZipEntryAsyncEnumerable : IAsyncEnumerable + { + private readonly StreamingZipHeaderFactory _headerFactory; + private readonly Stream _stream; + + public ZipEntryAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) + { + _headerFactory = headerFactory; + _stream = stream; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = default + ) => new ZipEntryAsyncEnumerator(_headerFactory, _stream, cancellationToken); + } + + /// + /// Yields entries from streaming ZIP headers without requiring synchronous stream reads. + /// + private sealed class ZipEntryAsyncEnumerator : IAsyncEnumerator, IDisposable + { + private readonly Stream _stream; + private readonly IAsyncEnumerator _headerEnumerator; + private ZipEntry? _current; + + public ZipEntryAsyncEnumerator( + StreamingZipHeaderFactory headerFactory, + Stream stream, + CancellationToken cancellationToken + ) + { + _stream = stream; + _headerEnumerator = headerFactory + .ReadStreamHeaderAsync(stream) + .GetAsyncEnumerator(cancellationToken); + } + + public ZipEntry Current => + _current ?? throw new InvalidOperationException("No current entry is available."); + + /// + /// Advances to the next non-directory entry-relevant header and materializes a , + /// using async I/O for improved performance on non-seekable streams. + /// + public async ValueTask MoveNextAsync() + { + while (await _headerEnumerator.MoveNextAsync().ConfigureAwait(false)) + { + var header = _headerEnumerator.Current; + switch (header.ZipHeaderType) + { + case ZipHeaderType.LocalEntry: + _current = new ZipEntry( + new StreamingZipFilePart((LocalEntryHeader)header, _stream) + ); + return true; + case ZipHeaderType.DirectoryEntry: + // DirectoryEntry headers are intentionally skipped in streaming mode. + break; + case ZipHeaderType.DirectoryEnd: + _current = null; + return false; + } + } + + _current = null; + return false; + } + + /// + /// Disposes the underlying header enumerator asynchronously. + /// + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + + /// + /// Synchronously disposes the underlying header enumerator. + /// + public void Dispose() + { + if (_headerEnumerator is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 6c3df181..83d0aa8c 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -101,95 +101,5 @@ public partial class ZipReader : AbstractReader protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => new ZipEntryAsyncEnumerable(_headerFactory, stream); - /// - /// Adapts an async header sequence into an async entry sequence. - /// - private sealed class ZipEntryAsyncEnumerable : IAsyncEnumerable - { - private readonly StreamingZipHeaderFactory _headerFactory; - private readonly Stream _stream; - - public ZipEntryAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream) - { - _headerFactory = headerFactory; - _stream = stream; - } - - public IAsyncEnumerator GetAsyncEnumerator( - CancellationToken cancellationToken = default - ) => new ZipEntryAsyncEnumerator(_headerFactory, _stream, cancellationToken); - } - - /// - /// Yields entries from streaming ZIP headers without requiring synchronous stream reads. - /// - private sealed class ZipEntryAsyncEnumerator : IAsyncEnumerator, IDisposable - { - private readonly Stream _stream; - private readonly IAsyncEnumerator _headerEnumerator; - private ZipEntry? _current; - - public ZipEntryAsyncEnumerator( - StreamingZipHeaderFactory headerFactory, - Stream stream, - CancellationToken cancellationToken - ) - { - _stream = stream; - _headerEnumerator = headerFactory - .ReadStreamHeaderAsync(stream) - .GetAsyncEnumerator(cancellationToken); - } - - public ZipEntry Current => - _current ?? throw new InvalidOperationException("No current entry is available."); - - /// - /// Advances to the next non-directory entry-relevant header and materializes a . - /// - public async ValueTask MoveNextAsync() - { - while (await _headerEnumerator.MoveNextAsync().ConfigureAwait(false)) - { - var header = _headerEnumerator.Current; - switch (header.ZipHeaderType) - { - case ZipHeaderType.LocalEntry: - _current = new ZipEntry( - new StreamingZipFilePart((LocalEntryHeader)header, _stream) - ); - return true; - case ZipHeaderType.DirectoryEntry: - // DirectoryEntry headers are intentionally skipped in streaming mode. - break; - case ZipHeaderType.DirectoryEnd: - _current = null; - return false; - } - } - - _current = null; - return false; - } - - /// - /// Disposes the underlying header enumerator. - /// - public ValueTask DisposeAsync() - { - Dispose(); - return default; - } - - /// - /// Disposes the underlying header enumerator. - /// - public void Dispose() - { - if (_headerEnumerator is IDisposable disposable) - { - disposable.Dispose(); - } - } - } + // Async nested classes moved to ZipReader.Async.cs } diff --git a/src/SharpCompress/Writers/AbstractWriter.Async.cs b/src/SharpCompress/Writers/AbstractWriter.Async.cs new file mode 100644 index 00000000..d7aedd7b --- /dev/null +++ b/src/SharpCompress/Writers/AbstractWriter.Async.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; + +namespace SharpCompress.Writers; + +public abstract partial class AbstractWriter +{ + public virtual async ValueTask WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) + { + // Default implementation calls synchronous version + // Derived classes should override for true async behavior + Write(filename, source, modificationTime); + await Task.CompletedTask.ConfigureAwait(false); + } + + public virtual async ValueTask WriteDirectoryAsync( + string directoryName, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) + { + // Default implementation calls synchronous version + // Derived classes should override for true async behavior + WriteDirectory(directoryName, modificationTime); + await Task.CompletedTask.ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Writers/AbstractWriter.cs b/src/SharpCompress/Writers/AbstractWriter.cs index 63511afa..d9967ceb 100644 --- a/src/SharpCompress/Writers/AbstractWriter.cs +++ b/src/SharpCompress/Writers/AbstractWriter.cs @@ -8,7 +8,7 @@ using SharpCompress.IO; namespace SharpCompress.Writers; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptions) +public abstract partial class AbstractWriter(ArchiveType type, WriterOptions writerOptions) : IWriter, IAsyncWriter { @@ -50,33 +50,8 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio public abstract void Write(string filename, Stream source, DateTime? modificationTime); - public virtual async ValueTask WriteAsync( - string filename, - Stream source, - DateTime? modificationTime, - CancellationToken cancellationToken = default - ) - { - // Default implementation calls synchronous version - // Derived classes should override for true async behavior - Write(filename, source, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); - } - public abstract void WriteDirectory(string directoryName, DateTime? modificationTime); - public virtual async ValueTask WriteDirectoryAsync( - string directoryName, - DateTime? modificationTime, - CancellationToken cancellationToken = default - ) - { - // Default implementation calls synchronous version - // Derived classes should override for true async behavior - WriteDirectory(directoryName, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); - } - protected virtual void Dispose(bool isDisposing) { if (isDisposing) diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs new file mode 100644 index 00000000..b53c1cd1 --- /dev/null +++ b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Tar.Headers; + +namespace SharpCompress.Writers.Tar; + +public partial class TarWriter +{ + /// + /// Asynchronously writes a directory entry to the TAR archive. + /// Uses synchronous implementation for directory entries as they are lightweight. + /// + public override async ValueTask WriteDirectoryAsync( + string directoryName, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) + { + // Synchronous implementation is sufficient for header-only write + WriteDirectory(directoryName, modificationTime); + await Task.CompletedTask.ConfigureAwait(false); + } + + /// + /// Asynchronously writes a file entry to the TAR archive. + /// + public override async ValueTask WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) => await WriteAsync(filename, source, modificationTime, null, cancellationToken); + + /// + /// Asynchronously writes a file entry with optional size specification. + /// + public async ValueTask WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + long? size, + CancellationToken cancellationToken = default + ) + { + if (!source.CanSeek && size is null) + { + throw new ArgumentException("Seekable stream is required if no size is given."); + } + + var realSize = size ?? source.Length; + + var header = new TarHeader(WriterOptions.ArchiveEncoding); + + header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH; + header.Name = NormalizeFilename(filename); + header.Size = realSize; + header.Write(OutputStream); + var progressStream = WrapWithProgress(source, filename); + var written = await progressStream + .TransferToAsync(OutputStream, realSize, cancellationToken) + .ConfigureAwait(false); + PadTo512(written); + } +} diff --git a/src/SharpCompress/Writers/Tar/TarWriter.cs b/src/SharpCompress/Writers/Tar/TarWriter.cs index 7d2f2ab4..932b09d5 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.cs @@ -103,17 +103,6 @@ public partial class TarWriter : AbstractWriter header.Write(OutputStream); } - public override async ValueTask WriteDirectoryAsync( - string directoryName, - DateTime? modificationTime, - CancellationToken cancellationToken = default - ) - { - // Synchronous implementation is sufficient for header-only write - WriteDirectory(directoryName, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); - } - public void Write(string filename, Stream source, DateTime? modificationTime, long? size) { if (!source.CanSeek && size is null) @@ -134,41 +123,6 @@ public partial class TarWriter : AbstractWriter PadTo512(size.Value); } - public override async ValueTask WriteAsync( - string filename, - Stream source, - DateTime? modificationTime, - CancellationToken cancellationToken = default - ) => await WriteAsync(filename, source, modificationTime, null, cancellationToken); - - public async ValueTask WriteAsync( - string filename, - Stream source, - DateTime? modificationTime, - long? size, - CancellationToken cancellationToken = default - ) - { - if (!source.CanSeek && size is null) - { - throw new ArgumentException("Seekable stream is required if no size is given."); - } - - var realSize = size ?? source.Length; - - var header = new TarHeader(WriterOptions.ArchiveEncoding); - - header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH; - header.Name = NormalizeFilename(filename); - header.Size = realSize; - header.Write(OutputStream); - var progressStream = WrapWithProgress(source, filename); - var written = await progressStream - .TransferToAsync(OutputStream, realSize, cancellationToken) - .ConfigureAwait(false); - PadTo512(written); - } - private void PadTo512(long size) { var zeros = unchecked((int)(((size + 511L) & ~511L) - size)); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs new file mode 100644 index 00000000..192d7e03 --- /dev/null +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; + +namespace SharpCompress.Writers.Zip; + +public partial class ZipWriter +{ + /// + /// Asynchronously writes a directory entry to the ZIP archive. + /// Uses synchronous implementation for directory entries as they are lightweight. + /// + public override async ValueTask WriteDirectoryAsync( + string directoryName, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) + { + // Synchronous implementation is sufficient for directory entries + WriteDirectory(directoryName, modificationTime); + await Task.CompletedTask.ConfigureAwait(false); + } +} diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index 0378f375..8ac90f23 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -162,16 +162,7 @@ public partial class ZipWriter : AbstractWriter WriteDirectoryEntry(normalizedName, options); } - public override async ValueTask WriteDirectoryAsync( - string directoryName, - DateTime? modificationTime, - CancellationToken cancellationToken = default - ) - { - // Synchronous implementation is sufficient for directory entries - WriteDirectory(directoryName, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); - } + // WriteDirectoryAsync moved to ZipWriter.Async.cs private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options) {