diff --git a/src/SharpCompress/Common/Zip/SOZip/SOZipDeflateStream.cs b/src/SharpCompress/Common/Zip/SOZip/SOZipDeflateStream.cs index 5b564db8..ba306a69 100644 --- a/src/SharpCompress/Common/Zip/SOZip/SOZipDeflateStream.cs +++ b/src/SharpCompress/Common/Zip/SOZip/SOZipDeflateStream.cs @@ -37,11 +37,7 @@ internal sealed class SOZipDeflateStream : Stream // Record the first offset (start of compressed data) _compressedOffsets.Add(0); - _deflateStream = new DeflateStream( - baseStream, - CompressionMode.Compress, - compressionLevel - ); + _deflateStream = new DeflateStream(baseStream, CompressionMode.Compress, compressionLevel); } /// diff --git a/src/SharpCompress/Common/Zip/SOZip/SOZipIndex.cs b/src/SharpCompress/Common/Zip/SOZip/SOZipIndex.cs index 22a119c3..ff2e5e57 100644 --- a/src/SharpCompress/Common/Zip/SOZip/SOZipIndex.cs +++ b/src/SharpCompress/Common/Zip/SOZip/SOZipIndex.cs @@ -150,9 +150,7 @@ public sealed class SOZipIndex Span buf8 = stackalloc byte[8]; if (stream.Read(buf8) != 8) { - throw new InvalidDataException( - "Invalid SOZip index: unable to read uncompressed size" - ); + throw new InvalidDataException("Invalid SOZip index: unable to read uncompressed size"); } index.UncompressedSize = BinaryPrimitives.ReadUInt64LittleEndian(buf8); diff --git a/src/SharpCompress/Common/Zip/ZipEntry.cs b/src/SharpCompress/Common/Zip/ZipEntry.cs index 19f9961b..81298784 100644 --- a/src/SharpCompress/Common/Zip/ZipEntry.cs +++ b/src/SharpCompress/Common/Zip/ZipEntry.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using SharpCompress.Common.Zip.Headers; +using SharpCompress.Common.Zip.SOZip; namespace SharpCompress.Common.Zip; @@ -11,7 +12,7 @@ public class ZipEntry : Entry internal ZipEntry(ZipFilePart? filePart) { - if (filePart == null) + if (filePart is null) { return; } @@ -88,4 +89,24 @@ public class ZipEntry : Entry public override int? Attrib => (int?)_filePart?.Header.ExternalFileAttributes; public string? Comment => _filePart?.Header.Comment; + + /// + /// Gets a value indicating whether this entry has SOZip (Seek-Optimized ZIP) support. + /// A SOZip entry has an associated index file that enables random access within + /// the compressed data. + /// + public bool IsSozip => _filePart?.Header.Extra.Any(e => e.Type == ExtraDataType.SOZip) ?? false; + + /// + /// Gets a value indicating whether this entry is a SOZip index file. + /// Index files are hidden files with a .sozip.idx extension that contain + /// offsets into the main compressed file. + /// + public bool IsSozipIndexFile => Key is not null && SOZipIndex.IsIndexFile(Key); + + /// + /// Gets the SOZip extra field data, if present. + /// + internal SOZipExtraField? SOZipExtra => + _filePart?.Header.Extra.OfType().FirstOrDefault(); } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index f867c8a9..e70dba06 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -26,12 +26,19 @@ public class ZipWriter : AbstractWriter private long streamPosition; private PpmdProperties? ppmdProps; private readonly bool isZip64; + private readonly bool enableSOZip; + private readonly int sozipChunkSize; + private readonly long sozipMinFileSize; public ZipWriter(Stream destination, ZipWriterOptions zipWriterOptions) : base(ArchiveType.Zip, zipWriterOptions) { zipComment = zipWriterOptions.ArchiveComment ?? string.Empty; isZip64 = zipWriterOptions.UseZip64; + enableSOZip = zipWriterOptions.EnableSOZip; + sozipChunkSize = zipWriterOptions.SOZipChunkSize; + sozipMinFileSize = zipWriterOptions.SOZipMinFileSize; + if (destination.CanSeek) { streamPosition = destination.Position; diff --git a/tests/SharpCompress.Test/Zip/SOZipTests.cs b/tests/SharpCompress.Test/Zip/SOZipTests.cs index a53529bb..33b71dc2 100644 --- a/tests/SharpCompress.Test/Zip/SOZipTests.cs +++ b/tests/SharpCompress.Test/Zip/SOZipTests.cs @@ -1,6 +1,12 @@ using System; using System.IO; +using System.Linq; +using System.Text; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; using SharpCompress.Common.Zip.SOZip; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; using Xunit; namespace SharpCompress.Test.Zip; @@ -39,9 +45,7 @@ public class SOZipTests : TestBase { var invalidData = new byte[] { 0x00, 0x00, 0x00, 0x00 }; - var exception = Assert.Throws( - () => SOZipIndex.Read(invalidData) - ); + var exception = Assert.Throws(() => SOZipIndex.Read(invalidData)); Assert.Contains("magic number mismatch", exception.Message); } @@ -132,4 +136,64 @@ public class SOZipTests : TestBase Assert.Null(SOZipIndex.GetMainFileName("file.txt")); Assert.Null(SOZipIndex.GetMainFileName("")); } + + [Fact] + public void ZipEntry_IsSozipIndexFile_Detection() + { + // Create a zip with a file that has a SOZip index file name pattern + using var memoryStream = new MemoryStream(); + + using ( + var writer = WriterFactory.Open( + memoryStream, + ArchiveType.Zip, + new ZipWriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true } + ) + ) + { + // Write a regular file + writer.Write("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("Hello World"))); + + // Write a file with SOZip index name pattern + var indexData = new SOZipIndex( + chunkSize: 32768, + uncompressedSize: 100, + compressedSize: 50, + compressedOffsets: new ulong[] { 0 } + ); + writer.Write(".test.txt.sozip.idx", new MemoryStream(indexData.ToByteArray())); + } + + memoryStream.Position = 0; + + using var archive = ZipArchive.Open(memoryStream); + var entries = archive.Entries.ToList(); + + Assert.Equal(2, entries.Count); + + var regularEntry = entries.First(e => e.Key == "test.txt"); + Assert.False(regularEntry.IsSozipIndexFile); + Assert.False(regularEntry.IsSozip); // No SOZip extra field + + var indexEntry = entries.First(e => e.Key == ".test.txt.sozip.idx"); + Assert.True(indexEntry.IsSozipIndexFile); + } + + [Fact] + public void ZipWriterOptions_SOZipDefaults() + { + var options = new ZipWriterOptions(CompressionType.Deflate); + + Assert.False(options.EnableSOZip); + Assert.Equal((int)SOZipIndex.DEFAULT_CHUNK_SIZE, options.SOZipChunkSize); + Assert.Equal(1048576L, options.SOZipMinFileSize); // 1MB + } + + [Fact] + public void ZipWriterEntryOptions_SOZipDefaults() + { + var options = new ZipWriterEntryOptions(); + + Assert.Null(options.EnableSOZip); + } }