using System; using System.Buffers.Binary; using System.IO; using System.Linq; 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; using SharpCompress.Providers; namespace SharpCompress.Common.Zip; internal abstract partial class ZipFilePart : FilePart { private readonly CompressionProviderRegistry _compressionProviders; internal ZipFilePart( ZipFileEntry header, Stream stream, CompressionProviderRegistry compressionProviders ) : base(header.ArchiveEncoding) { Header = header; header.Part = this; BaseStream = stream; _compressionProviders = compressionProviders; } internal Stream BaseStream { get; } internal ZipFileEntry Header { get; set; } internal override string? FilePartName => Header.Name; internal override Stream GetCompressedStream() { if (!Header.HasData) { return Stream.Null; } var decompressionStream = CreateDecompressionStream( GetCryptoStream(CreateBaseStream()), Header.CompressionMethod ); if (LeaveStreamOpen) { return SharpCompressStream.CreateNonDisposing(decompressionStream); } return decompressionStream; } internal override Stream GetRawStream() { if (!Header.HasData) { return Stream.Null; } return CreateBaseStream(); } protected abstract Stream CreateBaseStream(); protected bool LeaveStreamOpen => FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor) || Header.IsZip64; /// /// Gets the compression provider registry, falling back to default if not set. /// protected CompressionProviderRegistry GetProviders() => _compressionProviders; /// /// Converts ZipCompressionMethod to CompressionType. /// protected static CompressionType ToCompressionType(ZipCompressionMethod method) => method switch { ZipCompressionMethod.None => CompressionType.None, ZipCompressionMethod.Deflate => CompressionType.Deflate, ZipCompressionMethod.Deflate64 => CompressionType.Deflate64, ZipCompressionMethod.BZip2 => CompressionType.BZip2, ZipCompressionMethod.LZMA => CompressionType.LZMA, ZipCompressionMethod.PPMd => CompressionType.PPMd, ZipCompressionMethod.ZStandard => CompressionType.ZStandard, ZipCompressionMethod.Xz => CompressionType.Xz, ZipCompressionMethod.Shrink => CompressionType.Shrink, ZipCompressionMethod.Reduce1 => CompressionType.Reduce1, ZipCompressionMethod.Reduce2 => CompressionType.Reduce2, ZipCompressionMethod.Reduce3 => CompressionType.Reduce3, ZipCompressionMethod.Reduce4 => CompressionType.Reduce4, ZipCompressionMethod.Explode => CompressionType.Explode, _ => throw new NotSupportedException($"Unsupported compression method: {method}"), }; protected Stream CreateDecompressionStream(Stream stream, ZipCompressionMethod method) { // Handle special cases first switch (method) { case ZipCompressionMethod.None: { if (Header.CompressedSize is 0) { return new DataDescriptorStream(stream); } return stream; } case ZipCompressionMethod.WinzipAes: { return CreateWinzipAesDecompressionStream(stream); } } // Get the compression type and providers var compressionType = ToCompressionType(method); var providers = GetProviders(); // Build context with header information var context = new CompressionContext { InputSize = Header.CompressedSize, OutputSize = Header.UncompressedSize, CanSeek = stream.CanSeek, }; // Handle methods that need special context switch (method) { case ZipCompressionMethod.LZMA: { if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted)) { throw new NotSupportedException("LZMA with pkware encryption."); } using var reader = new BinaryReader( stream, System.Text.Encoding.Default, leaveOpen: true ); reader.ReadUInt16(); // LZMA version var propsLength = reader.ReadUInt16(); var props = reader.ReadBytes(propsLength); // When the uncompressed size is known to be zero, skip remaining compressed // bytes (required for streaming reads) and return an empty stream. // Bit1 (EOS marker flag) means the output size is not stored in the header // (the LZMA stream itself contains an end-of-stream marker instead), so we // only short-circuit when the size is explicitly known to be zero. if ( !FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) && Header.UncompressedSize == 0 ) { stream.Skip(); return Stream.Null; } context = context with { Properties = props, InputSize = Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, OutputSize = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) ? -1 : Header.UncompressedSize, }; return providers.CreateDecompressStream(compressionType, stream, context); } case ZipCompressionMethod.PPMd: { Span props = stackalloc byte[2]; stream.ReadFully(props); context = context with { Properties = props.ToArray() }; return providers.CreateDecompressStream(compressionType, stream, context); } case ZipCompressionMethod.Explode: { context = context with { FormatOptions = Header.Flags }; return providers.CreateDecompressStream(compressionType, stream, context); } } // For simple methods, use the basic decompress return providers.CreateDecompressStream(compressionType, stream, context); } private Stream CreateWinzipAesDecompressionStream(Stream stream) { 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 CreateDecompressionStream( stream, (ZipCompressionMethod)BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)) ); } protected Stream GetCryptoStream(Stream plainStream) { 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.CreateNonDisposing(plainStream); //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, Header.ComposeEncryptionData(plainStream), CryptoMode.Decrypt ); } case ZipCompressionMethod.WinzipAes: { if (Header.WinzipAesEncryptionData != null) { return new WinzipAesCryptoStream( plainStream, Header.WinzipAesEncryptionData, Header.CompressedSize - 10 ); } return plainStream; } default: { throw new ArchiveOperationException("Header.CompressionMethod is invalid"); } } } return plainStream; } }