diff --git a/.gitignore b/.gitignore index ef448bbf..d6a5a1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tools .DS_Store *.snupkg +/tests/TestArchives/6d23a38c-f064-4ef1-ad89-b942396f53b9/Scratch diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index e81a9924..7814170b 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -50,6 +50,12 @@ namespace SharpCompress.Archives { } + internal AbstractWritableArchive(ArchiveType type, IEnumerable streams, ReaderOptions readerFactoryOptions) + : base(type, streams, readerFactoryOptions) + { + } + + public override ICollection Entries { get diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 77af50d5..c280f7f4 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -137,6 +137,16 @@ namespace SharpCompress.Archives switch (type.Value) { case ArchiveType.Zip: + using (Stream prt2z = files[1].OpenRead()) + { + try + { + prt2z.Position += 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception + if (ZipArchive.IsZipFile(prt2z)) //if part2 is a zip then it's multi not split zip, z01,z02... zip is moved to the end when opened + return ZipArchive.Open(fileInfos.Select(a => a.OpenRead()), options); + } + catch { } + } return ZipArchive.Open(new SplitStream(files), options); case ArchiveType.SevenZip: return SevenZipArchive.Open(new SplitStream(files), options); @@ -216,6 +226,12 @@ namespace SharpCompress.Archives type = ArchiveType.Tar; stream.Seek(0, SeekOrigin.Begin); } + if (type == null) //test multipartzip as it could find zips in other non compressed archive types? + { + if (ZipArchive.IsZipMulti(stream)) //test the zip (last) file of a multipart zip + type = ArchiveType.Zip; + stream.Seek(0, SeekOrigin.Begin); + } return type != null; } diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 9f9dee0c..38c5ad4e 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -21,6 +21,7 @@ namespace SharpCompress.Archives.Rar this.parts = parts.ToList(); this.archive = archive; this.readerOptions = readerOptions; + this.IsSolid = this.FileHeader.IsSolid; } public override CompressionType CompressionType => CompressionType.Rar; diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index e4adc151..62d31eac 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System; using System.Collections.Generic; @@ -101,11 +101,23 @@ namespace SharpCompress.Archives.SevenZip { var stream = volumes.Single().Stream; LoadFactory(stream); + var entries = new SevenZipArchiveEntry[database._files.Count]; for (int i = 0; i < database._files.Count; i++) { var file = database._files[i]; - yield return new SevenZipArchiveEntry(this, new SevenZipFilePart(stream, database, i, file, ReaderOptions.ArchiveEncoding)); + entries[i] = new SevenZipArchiveEntry(this, new SevenZipFilePart(stream, database, i, file, ReaderOptions.ArchiveEncoding)); } + foreach (var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)) + { + var isSolid = false; + foreach (var entry in group) + { + entry.IsSolid = isSolid; + isSolid = true; //mark others in this group as solid - same as rar behaviour. + } + } + + return entries; } private void LoadFactory(Stream stream) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 58d94ee0..4a5abb74 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +6,7 @@ using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors.Deflate; +using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.Zip; using SharpCompress.Writers; @@ -47,6 +48,29 @@ namespace SharpCompress.Archives.Zip return new ZipArchive(fileInfo, readerOptions ?? new ReaderOptions()); } + /// + /// Takes multiple seekable Streams for a multi-part archive + /// + /// + /// + public static ZipArchive Open(IEnumerable streams, ReaderOptions? options = null) + { + streams.CheckNotNull(nameof(streams)); + return new ZipArchive(streams, options ?? new ReaderOptions()); + } + + /// + /// Takes multiple seekable Streams for a multi-part archive + /// + /// + /// + internal ZipArchive(IEnumerable streams, ReaderOptions options) + : base(ArchiveType.Zip, streams, options) + { + headerFactory = new SeekableZipHeaderFactory(options.Password, options.ArchiveEncoding); + } + + /// /// Takes a seekable Stream as a source /// @@ -97,6 +121,35 @@ namespace SharpCompress.Archives.Zip } } + public static bool IsZipMulti(Stream stream, string? password = null) + { + StreamingZipHeaderFactory headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding()); + try + { + ZipHeader? header = headerFactory.ReadStreamHeader(stream).FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split); + if (header is null) + { + if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe + { + SeekableZipHeaderFactory z = new SeekableZipHeaderFactory(password, new ArchiveEncoding()); + var x = z.ReadSeekableHeader(stream).FirstOrDefault(); + return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry; + } + else + return false; + } + return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType); + } + catch (CryptographicException) + { + return true; + } + catch + { + return false; + } + } + /// /// Constructor with a FileInfo object to an existing file. /// @@ -131,33 +184,41 @@ namespace SharpCompress.Archives.Zip protected override IEnumerable LoadVolumes(IEnumerable streams) { - return new ZipVolume(streams.First(), ReaderOptions).AsEnumerable(); + var st = streams.ToList(); //swap the zip to the end + var tmp = st[0]; + st.RemoveAt(0); + st.Add(tmp); + + foreach (var s in st) + yield return new ZipVolume(s, base.ReaderOptions); } protected override IEnumerable LoadEntries(IEnumerable volumes) { - var volume = volumes.Single(); - Stream stream = volume.Stream; - foreach (ZipHeader h in headerFactory.ReadSeekableHeader(stream)) + var vols = volumes.ToArray(); + foreach (ZipHeader h in headerFactory.ReadSeekableHeader(vols.Last().Stream)) { if (h != null) { switch (h.ZipHeaderType) { case ZipHeaderType.DirectoryEntry: - { - yield return new ZipArchiveEntry(this, - new SeekableZipFilePart(headerFactory, - (DirectoryEntryHeader)h, - stream)); - } - break; + { + DirectoryEntryHeader deh = (DirectoryEntryHeader)h; + Stream s; + if (deh.RelativeOffsetOfEntryHeader + deh.CompressedSize > vols[deh.DiskNumberStart].Stream.Length) + s = new SplitStream(vols.Skip(deh.DiskNumberStart).Select(a => a.Stream)); + else + s = vols[deh.DiskNumberStart].Stream; + yield return new ZipArchiveEntry(this, new SeekableZipFilePart(headerFactory, deh, s)); + } + break; case ZipHeaderType.DirectoryEnd: - { - byte[] bytes = ((DirectoryEndHeader)h).Comment ?? Array.Empty(); - volume.Comment = ReaderOptions.ArchiveEncoding.Decode(bytes); - yield break; - } + { + byte[] bytes = ((DirectoryEndHeader)h).Comment ?? Array.Empty(); + volumes.Last().Comment = ReaderOptions.ArchiveEncoding.Decode(bytes); + yield break; + } } } } diff --git a/src/SharpCompress/Common/Entry.cs b/src/SharpCompress/Common/Entry.cs index 7a1047d1..26a959bd 100644 --- a/src/SharpCompress/Common/Entry.cs +++ b/src/SharpCompress/Common/Entry.cs @@ -75,7 +75,7 @@ namespace SharpCompress.Common internal abstract IEnumerable Parts { get; } - internal bool IsSolid { get; set; } + public bool IsSolid { get; set; } internal virtual void Close() { diff --git a/src/SharpCompress/Common/IEntry.cs b/src/SharpCompress/Common/IEntry.cs index 14018227..8328ff29 100644 --- a/src/SharpCompress/Common/IEntry.cs +++ b/src/SharpCompress/Common/IEntry.cs @@ -14,6 +14,7 @@ namespace SharpCompress.Common bool IsDirectory { get; } bool IsEncrypted { get; } bool IsSplitAfter { get; } + bool IsSolid { get; } DateTime? LastAccessedTime { get; } DateTime? LastModifiedTime { get; } long Size { get; } diff --git a/src/SharpCompress/IO/SplitStream.cs b/src/SharpCompress/IO/SplitStream.cs index 1a4cbce0..087f8c92 100644 --- a/src/SharpCompress/IO/SplitStream.cs +++ b/src/SharpCompress/IO/SplitStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -14,31 +14,67 @@ namespace SharpCompress.IO private bool _isSplit; private long _prevSize; private long _pos; - private FileInfo[] _files; + private long _streamCount; + private FileInfo[]? _files; + private Stream[]? _streams; private Stream _stream; - public SplitStream(IEnumerable files) + public SplitStream(IEnumerable files) : this(null, files) { - _files = files.ToArray(); - _isSplit = _files.Length > 1; - _size = _files.Sum(a => a.Length); - _idx = 0; - _prevSize = 0; - _stream = openStream(0); } - private Stream openStream(int idx) + public SplitStream(IEnumerable streams) : this(streams, null) { - if (_stream != null) - _stream.Dispose(); + } + + private SplitStream(IEnumerable? streams, IEnumerable? files) + { + if (streams != null) + { + _streams = streams.ToArray(); + _isSplit = _streams.Length > 1; + _size = _streams.Sum(a => a.Length); + _streamCount = _streams.Length; + } + else if (files != null) + { + _files = files.ToArray(); + _isSplit = _files.Length > 1; + _size = _files.Sum(a => a.Length); + _streamCount = _files.Length; + } + _idx = 0; + _prevSize = 0; + _stream = OpenStream(0); + } + + private Stream OpenStream(int idx) + { + if (_streams != null) + _stream = _streams[idx]; + else if (_files != null) + { + if (_stream != null) + _stream.Dispose(); + + _stream = File.OpenRead(_files[idx].FullName); + } - _stream = File.OpenRead(_files[idx].FullName); _idx = idx; _pos = 0; return _stream; } + private long StreamLen(int idx) + { + if (_streams != null) + return _streams[idx].Length; + else if (_files != null) + return _files[idx].Length; + return 0; + } + public override bool CanRead => true; public override bool CanSeek => true; @@ -76,7 +112,7 @@ namespace SharpCompress.IO count -= r; offset += r; - if (_isSplit && _pos == _files[_idx].Length) + if (_isSplit && _pos == StreamLen(_idx)) Seek(0, SeekOrigin.Current); //will load next file } @@ -96,15 +132,15 @@ namespace SharpCompress.IO if (_isSplit) { _prevSize = 0; - for (int i = 0; i < _files.Length; i++) + for (int i = 0; i < _streamCount; i++) { - if (_prevSize + _files[i].Length > pos) + if (_prevSize + StreamLen(i) > pos) { if (_idx != i) - _stream = openStream(i); + _stream = OpenStream(i); break; } - _prevSize += _files[i].Length; + _prevSize += StreamLen(i); } } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 3076684f..1702da14 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -127,6 +127,35 @@ namespace SharpCompress.Test } } + protected void ArchiveStreamMultiRead(ReaderOptions readerOptions = null, params string[] testArchives) + { + ArchiveStreamMultiRead(readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x))); + } + + protected void ArchiveStreamMultiRead(ReaderOptions readerOptions, IEnumerable testArchives) + { + using (var archive = ArchiveFactory.Open(testArchives.Select(a => new FileInfo(a)), readerOptions)) + { + try + { + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + entry.WriteToDirectory(SCRATCH_FILES_PATH, + new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + catch (IndexOutOfRangeException) + { + throw; + } + } + VerifyFiles(); + } + protected void ArchiveOpenStreamRead(ReaderOptions readerOptions = null, params string[] testArchives) { ArchiveOpenStreamRead(readerOptions, testArchives.Select(x => Path.Combine(TEST_ARCHIVES_PATH, x))); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index 8f9a8848..93992861 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using SharpCompress.Common; using SharpCompress.Readers; using Xunit; @@ -7,6 +7,16 @@ namespace SharpCompress.Test.SevenZip { public class SevenZipArchiveTests : ArchiveTests { + [Fact] + public void SevenZipArchive_Solid_StreamRead() + { + ArchiveStreamRead("7Zip.solid.7z"); + } + [Fact] + public void SevenZipArchive_NonSolid_StreamRead() + { + ArchiveStreamRead("7Zip.nonsolid.7z"); + } [Fact] public void SevenZipArchive_LZMA_StreamRead() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 244d1599..b6199012 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text; @@ -93,6 +93,33 @@ namespace SharpCompress.Test.Zip ArchiveFileRead("Zip.bzip2.zip"); } [Fact] + public void WinZip26_ArchiveFileRead() + { + ArchiveFileRead("WinZip26.zip"); + } + [Fact] + public void WinZip26_Multi_ArchiveFileRead() + { + ArchiveStreamMultiRead(null, "WinZip26.nocomp.multi.zip", + "WinZip26.nocomp.multi.z01"); //min split size is 64k so no compression used + } + [Fact] + public void WinZip26_X_BZip2_ArchiveFileRead() + { + ArchiveFileRead("WinZip26_BZip2.zipx"); + } + [Fact] + public void WinZip26_X_Lzma_ArchiveFileRead() + { + ArchiveFileRead("WinZip26_LZMA.zipx"); + } + [Fact] + public void WinZip26_X_Multi_ArchiveFileRead() + { + ArchiveStreamMultiRead(null, "WinZip26.nocomp.multi.zipx", + "WinZip26.nocomp.multi.zx01"); //min split size is 64k so no compression used + } + [Fact] public void Zip_Deflate_Streamed2_ArchiveFileRead() { ArchiveFileRead("Zip.deflate.dd-.zip"); @@ -118,6 +145,12 @@ namespace SharpCompress.Test.Zip "Zip.deflate.split.006"); } [Fact] + public void Zip_InfoZip_Multi_ArchiveFileRead() + { + ArchiveStreamMultiRead(null, "Infozip.nocomp.multi.zip", + "Infozip.nocomp.multi.z01"); //min split size is 64k so no compression used + } + [Fact] public void Zip_Deflate64_ArchiveFileRead() { ArchiveFileRead("Zip.deflate64.zip"); diff --git a/tests/TestArchives/Archives/7Zip.nonsolid.7z b/tests/TestArchives/Archives/7Zip.nonsolid.7z new file mode 100644 index 00000000..907e1e2c Binary files /dev/null and b/tests/TestArchives/Archives/7Zip.nonsolid.7z differ diff --git a/tests/TestArchives/Archives/7Zip.solid.7z b/tests/TestArchives/Archives/7Zip.solid.7z new file mode 100644 index 00000000..5ac695e5 Binary files /dev/null and b/tests/TestArchives/Archives/7Zip.solid.7z differ diff --git a/tests/TestArchives/Archives/DotNetZip-obhg3mhx.tmp b/tests/TestArchives/Archives/DotNetZip-obhg3mhx.tmp new file mode 100644 index 00000000..e69de29b diff --git a/tests/TestArchives/Archives/Infozip.nocomp.multi.z01 b/tests/TestArchives/Archives/Infozip.nocomp.multi.z01 new file mode 100644 index 00000000..2f01189a Binary files /dev/null and b/tests/TestArchives/Archives/Infozip.nocomp.multi.z01 differ diff --git a/tests/TestArchives/Archives/Infozip.nocomp.multi.zip b/tests/TestArchives/Archives/Infozip.nocomp.multi.zip new file mode 100644 index 00000000..de469944 Binary files /dev/null and b/tests/TestArchives/Archives/Infozip.nocomp.multi.zip differ diff --git a/tests/TestArchives/Archives/WinZip26.nocomp.multi.z01 b/tests/TestArchives/Archives/WinZip26.nocomp.multi.z01 new file mode 100644 index 00000000..0e75014e Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26.nocomp.multi.z01 differ diff --git a/tests/TestArchives/Archives/WinZip26.nocomp.multi.zip b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zip new file mode 100644 index 00000000..130cfff0 Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zip differ diff --git a/tests/TestArchives/Archives/WinZip26.nocomp.multi.zipx b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zipx new file mode 100644 index 00000000..41c941c6 Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zipx differ diff --git a/tests/TestArchives/Archives/WinZip26.nocomp.multi.zx01 b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zx01 new file mode 100644 index 00000000..0e75014e Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26.nocomp.multi.zx01 differ diff --git a/tests/TestArchives/Archives/WinZip26.zip b/tests/TestArchives/Archives/WinZip26.zip new file mode 100644 index 00000000..a0b5d6a9 Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26.zip differ diff --git a/tests/TestArchives/Archives/WinZip26_BZip2.zipx b/tests/TestArchives/Archives/WinZip26_BZip2.zipx new file mode 100644 index 00000000..00fb8b0c Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26_BZip2.zipx differ diff --git a/tests/TestArchives/Archives/WinZip26_LZMA.zipx b/tests/TestArchives/Archives/WinZip26_LZMA.zipx new file mode 100644 index 00000000..81d0068a Binary files /dev/null and b/tests/TestArchives/Archives/WinZip26_LZMA.zipx differ