diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 57db85c2..ae1a58e5 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -199,7 +199,7 @@ public class ZipArchive : AbstractWritableArchive if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe { var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding()); - var x = z.ReadSeekableHeader(stream).FirstOrDefault(); + var x = z.ReadSeekableHeader(stream, useSync: true).FirstOrDefault(); return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry; } else @@ -254,7 +254,9 @@ public class ZipArchive : AbstractWritableArchive protected override IEnumerable LoadEntries(IEnumerable volumes) { var vols = volumes.ToArray(); - foreach (var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream)) + foreach ( + var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream, useSync: true) + ) { if (h != null) { diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs index 71502c0d..7d35f3ea 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -7,7 +8,7 @@ internal class DirectoryEndHeader : ZipHeader public DirectoryEndHeader() : base(ZipHeaderType.DirectoryEnd) { } - internal override void Read(AsyncBinaryReader reader) + internal override void Read(BinaryReader reader) { VolumeNumber = reader.ReadUInt16(); FirstVolumeWithDirectory = reader.ReadUInt16(); @@ -19,6 +20,18 @@ 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 = await reader.ReadBytesAsync(CommentLength); + } + public ushort VolumeNumber { get; private set; } public ushort FirstVolumeWithDirectory { get; private set; } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs index 8cf4f4ad..b446423a 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -31,7 +32,37 @@ internal class DirectoryEntryHeader : ZipFileEntry var extra = reader.ReadBytes(extraLength); var comment = reader.ReadBytes(commentLength); - // According to .ZIP File Format Specification + 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 = await reader.ReadBytesAsync(nameLength); + var extra = await reader.ReadBytesAsync(extraLength); + var comment = await reader.ReadBytesAsync(commentLength); + + ProcessReadData(name, extra, comment); + } + + private void ProcessReadData(byte[] name, byte[] extra, byte[] comment) + { // // For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // diff --git a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs index 5a587a7b..9c648baf 100644 --- a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -8,4 +9,6 @@ internal class IgnoreHeader : ZipHeader : base(type) { } internal override void Read(BinaryReader reader) { } + + internal override ValueTask Read(AsyncBinaryReader reader) => default; } diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs index c1ce5a6d..1df74e57 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs @@ -4,8 +4,27 @@ using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; -internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) +internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) + : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding) { + internal override void Read(BinaryReader reader) + { + Version = reader.ReadUInt16(); + Flags = (HeaderFlags)reader.ReadUInt16(); + CompressionMethod = (ZipCompressionMethod)reader.ReadUInt16(); + OriginalLastModifiedTime = LastModifiedTime = reader.ReadUInt16(); + OriginalLastModifiedDate = LastModifiedDate = reader.ReadUInt16(); + Crc = reader.ReadUInt32(); + CompressedSize = reader.ReadUInt32(); + UncompressedSize = reader.ReadUInt32(); + var nameLength = reader.ReadUInt16(); + var extraLength = reader.ReadUInt16(); + var name = reader.ReadBytes(nameLength); + var extra = reader.ReadBytes(extraLength); + + ProcessReadData(name, extra); + } + internal override async ValueTask Read(AsyncBinaryReader reader) { Version = await reader.ReadUInt16Async(); @@ -21,7 +40,11 @@ internal class LocalEntryHeader(ArchiveEncoding archiveEncoding) : ZipFileEntry( var name = await reader.ReadBytesAsync(nameLength); var extra = await reader.ReadBytesAsync(extraLength); - // According to .ZIP File Format Specification + ProcessReadData(name, extra); + } + + private void ProcessReadData(byte[] name, byte[] extra) + { // // For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT // diff --git a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs index 4151a6cb..29aaabaa 100644 --- a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -9,4 +10,7 @@ internal class SplitHeader : ZipHeader : base(ZipHeaderType.Split) { } internal override void Read(BinaryReader reader) => throw new NotImplementedException(); + + internal override ValueTask Read(AsyncBinaryReader reader) => + throw new NotImplementedException(); } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs index a74b4d1f..b15b6f16 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -26,6 +27,25 @@ 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(); + DataSector = await reader.ReadBytesAsync( + (int)( + SizeOfDirectoryEndRecord + - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS + ) + ); + } + 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.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs index 6b44b219..8326be99 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs @@ -5,7 +5,14 @@ namespace SharpCompress.Common.Zip.Headers; internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator) { - internal override async ValueTask Read(AsyncBinaryReader reader) + internal override void Read(BinaryReader reader) + { + FirstVolumeWithDirectory = reader.ReadUInt32(); + RelativeOffsetOfTheEndOfDirectoryRecord = (long)reader.ReadUInt64(); + TotalNumberOfVolumes = reader.ReadUInt32(); + } + + internal override async ValueTask Read(AsyncBinaryReader reader) { FirstVolumeWithDirectory = await reader.ReadUInt32Async(); RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs index 5d7f1c32..0f361c9c 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs @@ -5,7 +5,8 @@ using System.IO; namespace SharpCompress.Common.Zip.Headers; -internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) : ZipHeader(type) +internal abstract class ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding) + : ZipHeader(type) { internal bool IsDirectory { diff --git a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs index ad1714fa..9ce1caa3 100644 --- a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs +++ b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Threading.Tasks; namespace SharpCompress.Common.Zip.Headers; @@ -6,6 +7,7 @@ internal abstract class ZipHeader(ZipHeaderType type) { internal ZipHeaderType ZipHeaderType { get; } = type; + internal abstract void Read(BinaryReader reader); internal abstract ValueTask Read(AsyncBinaryReader reader); internal bool HasData { get; set; } = true; diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs index be32f8a8..edb182da 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs @@ -27,7 +27,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); - entry.Read(reader); + await entry.Read(reader); if (entry.IsZip64) { @@ -86,6 +86,73 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory } } + internal IEnumerable ReadSeekableHeader(Stream stream, bool useSync) + { + var reader = new BinaryReader(stream); + + SeekBackToHeader(stream, reader); + + var eocd_location = stream.Position; + var entry = new DirectoryEndHeader(); + entry.Read(reader); + + if (entry.IsZip64) + { + _zip64 = true; + + // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD + stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); + var zip64_locator = reader.ReadUInt32(); + if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) + { + throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); + } + + var zip64Locator = new Zip64DirectoryEndLocatorHeader(); + zip64Locator.Read(reader); + + stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); + var zip64Signature = reader.ReadUInt32(); + if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) + { + throw new ArchiveException("Failed to locate the Zip64 Header"); + } + + var zip64Entry = new Zip64DirectoryEndHeader(); + zip64Entry.Read(reader); + stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + else + { + stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); + } + + var position = stream.Position; + while (true) + { + stream.Position = position; + var signature = reader.ReadUInt32(); + var nextHeader = ReadHeader(signature, reader, _zip64); + position = stream.Position; + + if (nextHeader is null) + { + yield break; + } + + if (nextHeader is DirectoryEntryHeader entryHeader) + { + //entry could be zero bytes so we need to know that. + entryHeader.HasData = entryHeader.CompressedSize != 0; + yield return entryHeader; + } + else if (nextHeader is DirectoryEndHeader endHeader) + { + yield return endHeader; + } + } + } + private static bool IsMatch(byte[] haystack, int position, byte[] needle) { for (var i = 0; i < needle.Length; i++) @@ -138,6 +205,45 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory throw new ArchiveException("Failed to locate the Zip Header"); } + private static void SeekBackToHeader(Stream stream, BinaryReader 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; + // We search for marker in reverse to find the first occurance + byte[] needle = { 0x06, 0x05, 0x4b, 0x50 }; + + stream.Seek(-len, SeekOrigin.End); + + var seek = reader.ReadBytes(len); + + // Search in reverse + Array.Reverse(seek); + + // 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(seek, pos_from_end, needle)) + { + stream.Seek(-pos_from_end, SeekOrigin.End); + return; + } + } + + throw new ArchiveException("Failed to locate the Zip Header"); + } + internal LocalEntryHeader GetLocalHeader( Stream stream, DirectoryEntryHeader directoryEntryHeader diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs index f461050f..e085a0de 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs @@ -35,7 +35,11 @@ internal class ZipHeaderFactory _archiveEncoding = archiveEncoding; } - protected async ValueTask ReadHeader(uint headerBytes, AsyncBinaryReader reader, bool zip64 = false) + protected async ValueTask ReadHeader( + uint headerBytes, + AsyncBinaryReader reader, + bool zip64 = false + ) { switch (headerBytes) { @@ -49,6 +53,78 @@ internal class ZipHeaderFactory 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.ReadBytesAsync(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) + { + case ENTRY_HEADER_BYTES: + { + var entryHeader = new LocalEntryHeader(_archiveEncoding); + entryHeader.Read(reader); + LoadHeader(entryHeader, reader.BaseStream); + + _lastEntryHeader = entryHeader; + return entryHeader; + } + case DIRECTORY_START_HEADER_BYTES: { var entry = new DirectoryEntryHeader(_archiveEncoding); entry.Read(reader);