diff --git a/Aaru.Archives/Localization/Localization.Designer.cs b/Aaru.Archives/Localization/Localization.Designer.cs
index ad1da29b2..ddc44da22 100644
--- a/Aaru.Archives/Localization/Localization.Designer.cs
+++ b/Aaru.Archives/Localization/Localization.Designer.cs
@@ -836,5 +836,47 @@ namespace Aaru.Archives {
return ResourceManager.GetString("RAR_has_recovery_record", resourceCulture);
}
}
+
+ internal static string Zip_Name {
+ get {
+ return ResourceManager.GetString("Zip_Name", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_archive {
+ get {
+ return ResourceManager.GetString("ZIP_archive", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_entries_0 {
+ get {
+ return ResourceManager.GetString("ZIP_entries_0", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_files_0 {
+ get {
+ return ResourceManager.GetString("ZIP_files_0", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_directories_0 {
+ get {
+ return ResourceManager.GetString("ZIP_directories_0", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_encrypted_entries_0 {
+ get {
+ return ResourceManager.GetString("ZIP_encrypted_entries_0", resourceCulture);
+ }
+ }
+
+ internal static string ZIP_comment_0 {
+ get {
+ return ResourceManager.GetString("ZIP_comment_0", resourceCulture);
+ }
+ }
}
}
diff --git a/Aaru.Archives/Localization/Localization.es.resx b/Aaru.Archives/Localization/Localization.es.resx
index acc8442d6..8766033be 100644
--- a/Aaru.Archives/Localization/Localization.es.resx
+++ b/Aaru.Archives/Localization/Localization.es.resx
@@ -411,4 +411,25 @@
[slateblue1]El archivo tiene [green]registro de recuperación[/][/]
+
+ ZIP
+
+
+ [bold][blue]Archivo ZIP[/][/]
+
+
+ [slateblue1]Entradas: [green]{0}[/][/]
+
+
+ [slateblue1]Archivos: [green]{0}[/][/]
+
+
+ [slateblue1]Directorios: [green]{0}[/][/]
+
+
+ [slateblue1]Entradas cifradas: [green]{0}[/][/]
+
+
+ [slateblue1]Comentario del archivo: [green]{0}[/][/]
+
\ No newline at end of file
diff --git a/Aaru.Archives/Localization/Localization.resx b/Aaru.Archives/Localization/Localization.resx
index b2ba25aeb..43138538b 100644
--- a/Aaru.Archives/Localization/Localization.resx
+++ b/Aaru.Archives/Localization/Localization.resx
@@ -419,4 +419,25 @@
[slateblue1]Archive has [green]recovery record[/][/]
+
+ ZIP
+
+
+ [bold][blue]ZIP archive[/][/]
+
+
+ [slateblue1]Entries: [green]{0}[/][/]
+
+
+ [slateblue1]Files: [green]{0}[/][/]
+
+
+ [slateblue1]Directories: [green]{0}[/][/]
+
+
+ [slateblue1]Encrypted entries: [green]{0}[/][/]
+
+
+ [slateblue1]Archive comment: [green]{0}[/][/]
+
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Consts.cs b/Aaru.Archives/Zip/Consts.cs
new file mode 100644
index 000000000..571e6323d
--- /dev/null
+++ b/Aaru.Archives/Zip/Consts.cs
@@ -0,0 +1,54 @@
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+ /// Local file header signature: PK\x03\x04
+ const uint LOCAL_HEADER_SIG = 0x04034B50;
+ /// Central directory file header signature: PK\x01\x02
+ const uint CENTRAL_DIR_SIG = 0x02014B50;
+ /// End of central directory record signature: PK\x05\x06
+ const uint EOCD_SIG = 0x06054B50;
+ /// ZIP64 end of central directory record signature: PK\x06\x06
+ const uint ZIP64_EOCD_SIG = 0x06064B50;
+ /// ZIP64 end of central directory locator signature: PK\x06\x07
+ const uint ZIP64_LOCATOR_SIG = 0x07064B50;
+ /// Data descriptor signature: PK\x07\x08
+ const uint DATA_DESCRIPTOR_SIG = 0x08074B50;
+
+ // Extra field header IDs
+ const ushort EXTRA_ZIP64 = 0x0001;
+ const ushort EXTRA_EXT_TIMESTAMP = 0x5455;
+ const ushort EXTRA_UNIX1 = 0x5855;
+ const ushort EXTRA_UNIX2 = 0x7855;
+ const ushort EXTRA_UNIX3 = 0x7875;
+ const ushort EXTRA_UNICODE_PATH = 0x7075;
+ const ushort EXTRA_WINZIP_AES = 0x9901;
+
+ // General purpose bit flags
+ const ushort FLAG_ENCRYPTED = 0x0001;
+ const ushort FLAG_IMPLODE_8K = 0x0002;
+ const ushort FLAG_IMPLODE_LITERALS = 0x0004;
+ const ushort FLAG_DATA_DESCRIPTOR = 0x0008;
+ const ushort FLAG_UTF8 = 0x0800;
+
+ /// Minimum ZIP file size: empty EOCD record (22 bytes)
+ const int MIN_FILE_SIZE = 22;
+
+ /// Size of a fixed central directory entry (without variable-length fields)
+ const int CENTRAL_DIR_ENTRY_SIZE = 46;
+
+ /// Size of a fixed local file header (without variable-length fields)
+ const int LOCAL_HEADER_SIZE = 30;
+
+ /// Size of a fixed End of Central Directory record (without comment)
+ const int EOCD_SIZE = 22;
+
+ /// Chunk size for backward EOCD search
+ const int EOCD_SEARCH_CHUNK = 0x10000;
+
+ /// Directory attribute in DOS external file attributes
+ const uint ATTR_DIRECTORY = 0x10;
+
+ /// CRC32 polynomial used in ZIP format
+ const uint CRC_POLY = 0xEDB88320;
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Enums.cs b/Aaru.Archives/Zip/Enums.cs
new file mode 100644
index 000000000..5fd747ff0
--- /dev/null
+++ b/Aaru.Archives/Zip/Enums.cs
@@ -0,0 +1,70 @@
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+#region CompressionMethod enum
+
+ enum CompressionMethod : ushort
+ {
+ /// No compression
+ Stored = 0,
+ /// LZW-based shrink
+ Shrink = 1,
+ /// Reduce with compression factor 1
+ Reduce1 = 2,
+ /// Reduce with compression factor 2
+ Reduce2 = 3,
+ /// Reduce with compression factor 3
+ Reduce3 = 4,
+ /// Reduce with compression factor 4
+ Reduce4 = 5,
+ /// Shannon-Fano + LZSS imploding
+ Implode = 6,
+ /// Deflate (RFC 1951)
+ Deflate = 8,
+ /// Deflate64 / Enhanced Deflate (64KB window)
+ Deflate64 = 9,
+ /// BZip2
+ BZip2 = 12,
+ /// LZMA
+ Lzma = 14,
+ /// WinZip JPEG compressed data
+ WinZipJpeg = 96,
+ /// WinZip WavPack compressed data
+ WavPack = 97,
+ /// PPMd version I, Rev 1
+ PPMd = 98,
+ /// WinZip AES encryption marker (actual method in extra field)
+ WinZipAes = 99
+ }
+
+#endregion
+
+#region HostOs enum
+
+ enum HostOs : byte
+ {
+ MsDos = 0,
+ Amiga = 1,
+ OpenVms = 2,
+ Unix = 3,
+ VmCms = 4,
+ AtariSt = 5,
+ Os2Hpfs = 6,
+ Macintosh = 7,
+ ZSystem = 8,
+ CpM = 9,
+ WindowsNtfs = 10,
+ Mvs = 11,
+ Vse = 12,
+ AcornRisc = 13,
+ Vfat = 14,
+ AltMvs = 15,
+ BeOs = 16,
+ Tandem = 17,
+ Os400 = 18,
+ OsX = 19
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Files.cs b/Aaru.Archives/Zip/Files.cs
new file mode 100644
index 000000000..df67e28d5
--- /dev/null
+++ b/Aaru.Archives/Zip/Files.cs
@@ -0,0 +1,275 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+using Aaru.CommonTypes.Structs;
+using Aaru.Compression;
+using Aaru.Compression.Zip;
+using Aaru.Filters;
+using Aaru.Helpers.IO;
+using BZip2 = Aaru.Compression.BZip2;
+using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+#region IArchive Members
+
+ ///
+ public ErrorNumber GetFilename(int entryNumber, out string fileName)
+ {
+ fileName = null;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ fileName = _entries[entryNumber].Filename ?? "";
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public ErrorNumber GetEntryNumber(string fileName, bool caseInsensitiveMatch, out int entryNumber)
+ {
+ entryNumber = -1;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ StringComparison comparison = caseInsensitiveMatch
+ ? StringComparison.CurrentCultureIgnoreCase
+ : StringComparison.CurrentCulture;
+
+ for(int i = 0, count = _entries.Count; i < count; i++)
+ {
+ GetFilename(i, out string name);
+
+ if(name is null) continue;
+
+ if(!name.Equals(fileName, comparison)) continue;
+
+ entryNumber = i;
+
+ return ErrorNumber.NoError;
+ }
+
+ return ErrorNumber.NoSuchFile;
+ }
+
+ ///
+ public ErrorNumber GetCompressedSize(int entryNumber, out long length)
+ {
+ length = -1;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ length = _entries[entryNumber].CompressedSize;
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public ErrorNumber GetUncompressedSize(int entryNumber, out long length)
+ {
+ length = -1;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ length = _entries[entryNumber].UncompressedSize;
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public ErrorNumber Stat(int entryNumber, out FileEntryInfo stat)
+ {
+ stat = null;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ Entry entry = _entries[entryNumber];
+
+ stat = new FileEntryInfo
+ {
+ Attributes = entry.IsDirectory ? FileAttributes.Directory : FileAttributes.File,
+ Blocks = entry.UncompressedSize / 512,
+ BlockSize = 512,
+ Length = entry.UncompressedSize,
+ LastWriteTimeUtc = entry.LastWriteTime != default(DateTime) ? entry.LastWriteTime.ToUniversalTime() : null
+ };
+
+ if(entry.LastAccessTime != default(DateTime)) stat.AccessTimeUtc = entry.LastAccessTime.ToUniversalTime();
+
+ if(entry.CreationTime != default(DateTime)) stat.CreationTimeUtc = entry.CreationTime.ToUniversalTime();
+
+ if(entry.UncompressedSize % 512 != 0) stat.Blocks++;
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public ErrorNumber GetEntry(int entryNumber, out IFilter filter)
+ {
+ filter = null;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ Entry entry = _entries[entryNumber];
+
+ if(entry.IsDirectory) return ErrorNumber.InvalidArgument;
+
+ if(entry.IsEncrypted) return ErrorNumber.NotSupported;
+
+ Stream stream;
+
+ if(entry.UncompressedSize == 0)
+ stream = new MemoryStream([]);
+ else
+ {
+ Stream compressedStream = new OffsetStream(new NonClosableStream(_stream),
+ entry.DataOffset,
+ entry.DataOffset + entry.CompressedSize - 1);
+
+ switch(entry.Method)
+ {
+ case CompressionMethod.Stored:
+ stream = compressedStream;
+
+ break;
+
+ case CompressionMethod.Shrink:
+ stream = new ShrinkStream(compressedStream, entry.UncompressedSize);
+
+ break;
+
+ case CompressionMethod.Reduce1:
+ case CompressionMethod.Reduce2:
+ case CompressionMethod.Reduce3:
+ case CompressionMethod.Reduce4:
+ stream = new ReduceStream(compressedStream, entry.UncompressedSize, (int)entry.Method - 1);
+
+ break;
+
+ case CompressionMethod.Implode:
+ stream = new ImplodeStream(compressedStream,
+ entry.UncompressedSize,
+ (entry.Flags & FLAG_IMPLODE_8K) != 0,
+ (entry.Flags & FLAG_IMPLODE_LITERALS) != 0);
+
+ break;
+
+ case CompressionMethod.Deflate:
+ {
+ // DeflateStream decompresses and we wrap in a MemoryStream for seekability
+ var decompressed = new byte[entry.UncompressedSize];
+
+ using(var deflate = new DeflateStream(compressedStream, CompressionMode.Decompress, true))
+ {
+ var totalRead = 0;
+
+ while(totalRead < decompressed.Length)
+ {
+ int bytesRead = deflate.Read(decompressed, totalRead, decompressed.Length - totalRead);
+
+ if(bytesRead == 0) break;
+
+ totalRead += bytesRead;
+ }
+ }
+
+ stream = new MemoryStream(decompressed);
+
+ break;
+ }
+
+ case CompressionMethod.Deflate64:
+ stream = new Deflate64Stream(compressedStream, entry.UncompressedSize);
+
+ break;
+
+ case CompressionMethod.BZip2:
+ {
+ var compressedData = new byte[entry.CompressedSize];
+ int compRead = compressedStream.Read(compressedData, 0, compressedData.Length);
+ var decompressedBuf = new byte[entry.UncompressedSize];
+
+ BZip2.DecodeBuffer(compressedData, decompressedBuf);
+
+ stream = new MemoryStream(decompressedBuf);
+
+ break;
+ }
+
+ case CompressionMethod.Lzma:
+ {
+ // ZIP LZMA header: 2 bytes version, 2 bytes properties size, N bytes properties
+ var lzmaHeader = new byte[4];
+ compressedStream.Read(lzmaHeader, 0, 4);
+ var propsSize = BitConverter.ToUInt16(lzmaHeader, 2);
+
+ var properties = new byte[propsSize];
+ compressedStream.Read(properties, 0, propsSize);
+
+ long remainingCompressed = entry.CompressedSize - 4 - propsSize;
+ var compressedData = new byte[remainingCompressed];
+ compressedStream.Read(compressedData, 0, compressedData.Length);
+
+ var decompressedBuf = new byte[entry.UncompressedSize];
+
+ LZMA.DecodeBuffer(compressedData, decompressedBuf, properties);
+
+ stream = new MemoryStream(decompressedBuf);
+
+ break;
+ }
+
+ case CompressionMethod.PPMd:
+ {
+ // PPMd info word: 2 bytes
+ var infoBytes = new byte[2];
+ compressedStream.Read(infoBytes, 0, 2);
+ var info = BitConverter.ToUInt16(infoBytes, 0);
+
+ int maxOrder = (info & 0x0F) + 1;
+ int subAllocSize = (info >> 4 & 0xFF) + 1 << 20;
+ bool restoration = (info >> 12 & 0x0F) != 0;
+
+ // Remaining data is the PPMd stream
+ long remaining = entry.CompressedSize - 2;
+
+ Stream ppmdInput = new OffsetStream(new NonClosableStream(_stream),
+ entry.DataOffset + 2,
+ entry.DataOffset + entry.CompressedSize - 1);
+
+ stream = new PpmdStream(ppmdInput, entry.UncompressedSize, maxOrder, subAllocSize, restoration);
+
+ break;
+ }
+
+ default:
+ return ErrorNumber.NotSupported;
+ }
+ }
+
+ filter = new ZZZNoFilter();
+ ErrorNumber errno = filter.Open(stream);
+
+ if(errno == ErrorNumber.NoError) return ErrorNumber.NoError;
+
+ stream.Close();
+
+ return errno;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Info.cs b/Aaru.Archives/Zip/Info.cs
new file mode 100644
index 000000000..02eb1eb9f
--- /dev/null
+++ b/Aaru.Archives/Zip/Info.cs
@@ -0,0 +1,83 @@
+using System.IO;
+using System.Text;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+#region IArchive Members
+
+ ///
+ public bool Identify(IFilter filter)
+ {
+ if(filter.DataForkLength < MIN_FILE_SIZE) return false;
+
+ Stream stream = filter.GetDataForkStream();
+
+ if(stream.Length < 4) return false;
+
+ stream.Position = 0;
+
+ var sig = new byte[8];
+ int read = stream.Read(sig, 0, sig.Length < stream.Length ? sig.Length : (int)stream.Length);
+
+ if(read < 4) return false;
+
+ // PK\x03\x04 at offset 0
+ if(sig[0] == 0x50 && sig[1] == 0x4B && sig[2] == 0x03 && sig[3] == 0x04) return true;
+
+ // PK\x05\x06 at offset 0 (empty archive)
+ if(sig[0] == 0x50 && sig[1] == 0x4B && sig[2] == 0x05 && sig[3] == 0x06) return true;
+
+ // PK\x03\x04 at offset 4 (some SFX variants prepend 4 bytes)
+ if(read >= 8 && sig[4] == 0x50 && sig[5] == 0x4B && sig[6] == 0x03 && sig[7] == 0x04) return true;
+
+ return false;
+ }
+
+ ///
+ public void GetInformation(IFilter filter, Encoding encoding, out string information)
+ {
+ information = "";
+
+ if(filter.DataForkLength < MIN_FILE_SIZE) return;
+
+ ErrorNumber errno = Open(filter, encoding);
+
+ if(errno != ErrorNumber.NoError) return;
+
+ var sb = new StringBuilder();
+ sb.AppendLine(Localization.ZIP_archive);
+ sb.AppendFormat(Localization.ZIP_entries_0, _entries.Count).AppendLine();
+
+ var directories = 0;
+ var files = 0;
+ var encrypted = 0;
+
+ foreach(Entry entry in _entries)
+ {
+ if(entry.IsDirectory)
+ directories++;
+ else
+ files++;
+
+ if(entry.IsEncrypted) encrypted++;
+ }
+
+ sb.AppendFormat(Localization.ZIP_files_0, files).AppendLine();
+
+ if(directories > 0) sb.AppendFormat(Localization.ZIP_directories_0, directories).AppendLine();
+
+ if(encrypted > 0) sb.AppendFormat(Localization.ZIP_encrypted_entries_0, encrypted).AppendLine();
+
+ if(_archiveComment is not null) sb.AppendFormat(Localization.ZIP_comment_0, _archiveComment).AppendLine();
+
+ information = sb.ToString();
+
+ Close();
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Open.cs b/Aaru.Archives/Zip/Open.cs
new file mode 100644
index 000000000..b98d2584b
--- /dev/null
+++ b/Aaru.Archives/Zip/Open.cs
@@ -0,0 +1,719 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Aaru.CommonTypes.Enums;
+using Aaru.CommonTypes.Interfaces;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+ List _entries;
+
+ void FindEndOfCentralDirectory(out long eocdOffset, out long zip64LocOffset)
+ {
+ eocdOffset = -1;
+ zip64LocOffset = -1;
+
+ long end = _stream.Length;
+
+ // Search backward in chunks for the EOCD signature
+ long chunkEnd = end;
+
+ while(chunkEnd > 0)
+ {
+ int numBytes = EOCD_SEARCH_CHUNK;
+
+ if(chunkEnd - numBytes < 0) numBytes = (int)chunkEnd;
+
+ long chunkStart = chunkEnd - numBytes;
+
+ // Overlap by 20 bytes to catch ZIP64 locator that might span chunk boundaries
+ int preservedBytes = chunkStart >= 20 ? 20 : 0;
+
+ var buf = new byte[numBytes];
+ _stream.Position = chunkStart;
+
+ int bytesRead = _stream.Read(buf, 0, numBytes);
+
+ if(bytesRead < 4) break;
+
+ // Search for PK\x05\x06 (EOCD signature)
+ for(int pos = bytesRead - 4; pos >= preservedBytes; pos--)
+ {
+ if(buf[pos] != 0x50 || buf[pos + 1] != 0x4B || buf[pos + 2] != 0x05 || buf[pos + 3] != 0x06) continue;
+
+ eocdOffset = chunkStart + pos;
+
+ // Check for ZIP64 end of central directory locator 20 bytes before EOCD
+ if(pos >= 20 &&
+ buf[pos - 20] == 0x50 &&
+ buf[pos - 19] == 0x4B &&
+ buf[pos - 18] == 0x06 &&
+ buf[pos - 17] == 0x07)
+ zip64LocOffset = chunkStart + pos - 20;
+
+ return;
+ }
+
+ chunkEnd -= numBytes - preservedBytes * 2;
+ }
+ }
+
+ void ParseCentralDirectory(long eocdOffset, long zip64LocOffset)
+ {
+ var reader = new BinaryReader(_stream, Encoding.UTF8, true);
+
+ // Read EOCD record
+ _stream.Position = eocdOffset + 4; // skip signature
+
+ ushort diskNumber = reader.ReadUInt16();
+ ushort centralDirDisk = reader.ReadUInt16();
+ ushort entriesOnDisk = reader.ReadUInt16();
+ long totalEntries = reader.ReadUInt16();
+ long centralDirSize = reader.ReadUInt32();
+ long centralDirOffset = reader.ReadUInt32();
+ ushort commentLength = reader.ReadUInt16();
+
+ if(commentLength > 0)
+ {
+ byte[] commentBytes = reader.ReadBytes(commentLength);
+ _archiveComment = Encoding.UTF8.GetString(commentBytes);
+ }
+
+ // If we have a ZIP64 locator, read the ZIP64 EOCD record
+ if(zip64LocOffset >= 0)
+ {
+ _stream.Position = zip64LocOffset + 4; // skip signature
+
+ uint zip64EocdDisk = reader.ReadUInt32();
+ long zip64EocdOffset = reader.ReadInt64();
+
+ _stream.Position = zip64EocdOffset;
+ uint zip64Sig = reader.ReadUInt32();
+
+ if(zip64Sig == ZIP64_EOCD_SIG)
+ {
+ reader.ReadInt64(); // size of ZIP64 EOCD record
+ reader.ReadUInt16(); // version made by
+ reader.ReadUInt16(); // version needed
+ reader.ReadUInt32(); // disk number
+ centralDirDisk = (ushort)reader.ReadUInt32();
+ reader.ReadInt64(); // entries on disk
+ totalEntries = reader.ReadInt64();
+ centralDirSize = reader.ReadInt64();
+ centralDirOffset = reader.ReadInt64();
+ }
+ }
+
+ // Seek to the start of the central directory
+ _stream.Position = centralDirOffset;
+
+ for(long i = 0; i < totalEntries; i++)
+ {
+ if(_stream.Position >= _stream.Length) break;
+
+ Entry entry = ReadCentralDirectoryRecord(reader);
+
+ if(entry.Filename is null) break;
+
+ long savedPos = _stream.Position;
+
+ // Read local header to get exact data offset
+ _stream.Position = entry.DataOffset; // locheaderoffset stored temporarily in DataOffset
+ uint localSig = reader.ReadUInt32();
+
+ if(localSig == LOCAL_HEADER_SIG || localSig == EOCD_SIG)
+ {
+ reader.ReadUInt16(); // version needed
+ reader.ReadUInt16(); // flags
+ reader.ReadUInt16(); // compression method
+ uint localDate = reader.ReadUInt32();
+ reader.ReadUInt32(); // crc32
+ reader.ReadUInt32(); // compressed size
+ reader.ReadUInt32(); // uncompressed size
+ ushort localNameLen = reader.ReadUInt16();
+ ushort localExtraLen = reader.ReadUInt16();
+
+ long dataOffset = _stream.Position + localNameLen + localExtraLen;
+
+ // Parse local extra fields for additional metadata (timestamps, unicode paths, etc.)
+ if(localNameLen > 0) reader.ReadBytes(localNameLen);
+
+ if(localExtraLen > 0)
+ {
+ long extraStart = _stream.Position;
+ ParseExtraFields(reader, localExtraLen, ref entry);
+ _stream.Position = extraStart + localExtraLen;
+ }
+
+ entry.DataOffset = dataOffset;
+ }
+
+ // Handle overflow for archives with >65535 entries without ZIP64
+ if(i == totalEntries - 1 && zip64LocOffset < 0)
+ {
+ long remaining = centralDirOffset + centralDirSize - savedPos;
+
+ if(remaining > 65536 * CENTRAL_DIR_ENTRY_SIZE) totalEntries += 65536;
+ }
+
+ _entries.Add(entry);
+
+ _stream.Position = savedPos;
+ }
+ }
+
+ Entry ReadCentralDirectoryRecord(BinaryReader reader)
+ {
+ Entry entry = new();
+
+ uint sig = reader.ReadUInt32();
+
+ if(sig != CENTRAL_DIR_SIG) return entry;
+
+ byte creatorVersion = reader.ReadByte();
+ byte system = reader.ReadByte();
+ ushort extractVersion = reader.ReadUInt16();
+ ushort flags = reader.ReadUInt16();
+ ushort method = reader.ReadUInt16();
+ uint dosDateTime = reader.ReadUInt32();
+ uint crc32 = reader.ReadUInt32();
+ long compSize = reader.ReadUInt32();
+ long uncompSize = reader.ReadUInt32();
+ ushort nameLength = reader.ReadUInt16();
+ ushort extraLength = reader.ReadUInt16();
+ ushort commentLength = reader.ReadUInt16();
+ ushort startDisk = reader.ReadUInt16();
+ ushort intFileAttrib = reader.ReadUInt16();
+ uint extFileAttrib = reader.ReadUInt32();
+ long locHeaderOff = reader.ReadUInt32();
+
+ // Read filename
+ byte[] nameBytes = nameLength > 0 ? reader.ReadBytes(nameLength) : null;
+
+ // Parse extra fields in central directory (primarily for ZIP64 overrides)
+ int extraRemaining = extraLength;
+
+ while(extraRemaining >= 4)
+ {
+ ushort extId = reader.ReadUInt16();
+ ushort extSize = reader.ReadUInt16();
+ extraRemaining -= 4;
+
+ if(extSize > extraRemaining) break;
+
+ extraRemaining -= extSize;
+ long nextExtra = _stream.Position + extSize;
+
+ if(extId == EXTRA_ZIP64)
+ {
+ if(uncompSize == 0xFFFFFFFF && _stream.Position + 8 <= nextExtra) uncompSize = reader.ReadInt64();
+
+ if(compSize == 0xFFFFFFFF && _stream.Position + 8 <= nextExtra) compSize = reader.ReadInt64();
+
+ if(locHeaderOff == 0xFFFFFFFF && _stream.Position + 8 <= nextExtra) locHeaderOff = reader.ReadInt64();
+
+ if(startDisk == 0xFFFF && _stream.Position + 4 <= nextExtra) startDisk = (ushort)reader.ReadUInt32();
+ }
+
+ _stream.Position = nextExtra;
+ }
+
+ if(extraRemaining > 0) _stream.Position += extraRemaining;
+
+ // Read comment
+ string comment = null;
+
+ if(commentLength > 0)
+ {
+ byte[] commentBytes = reader.ReadBytes(commentLength);
+
+ comment = (flags & FLAG_UTF8) != 0
+ ? Encoding.UTF8.GetString(commentBytes)
+ : _encoding.GetString(commentBytes);
+ }
+
+ // Decode filename
+ string filename = null;
+
+ if(nameBytes is not null)
+ {
+ filename = (flags & FLAG_UTF8) != 0 ? Encoding.UTF8.GetString(nameBytes) : _encoding.GetString(nameBytes);
+
+ // Normalize path separators
+ filename = filename.Replace('\\', '/');
+ }
+
+ // Detect directory
+ var isDirectory = false;
+
+ if(filename is not null && filename.EndsWith('/') && uncompSize == 0) isDirectory = true;
+
+ if(system == (byte)HostOs.MsDos && (extFileAttrib & ATTR_DIRECTORY) != 0 && compSize == 0 && uncompSize == 0)
+ isDirectory = true;
+
+ // Extract Unix permissions from external attributes for Unix-created archives
+ ushort unixPerms = 0;
+
+ if(system == (byte)HostOs.Unix)
+ {
+ var perm = (int)(extFileAttrib >> 16);
+
+ if(perm != 0) unixPerms = (ushort)perm;
+ }
+
+ entry.Method = (CompressionMethod)method;
+ entry.Filename = filename;
+ entry.CompressedSize = compSize;
+ entry.UncompressedSize = uncompSize;
+ entry.DataOffset = locHeaderOff; // temporary, resolved to actual data offset later
+ entry.LastWriteTime = DosToDateTime(dosDateTime);
+ entry.Crc32 = crc32;
+ entry.System = (HostOs)system;
+ entry.ExternalAttributes = extFileAttrib;
+ entry.UnixPermissions = unixPerms;
+ entry.Flags = flags;
+ entry.Comment = comment;
+ entry.IsDirectory = isDirectory;
+ entry.IsEncrypted = (flags & FLAG_ENCRYPTED) != 0;
+
+ return entry;
+ }
+
+ void ParseWithoutCentralDirectory()
+ {
+ var reader = new BinaryReader(_stream, Encoding.UTF8, true);
+
+ _stream.Position = 0;
+
+ while(_stream.Position < _stream.Length - 4)
+ {
+ uint sig;
+
+ try
+ {
+ sig = reader.ReadUInt32();
+ }
+ catch(EndOfStreamException)
+ {
+ break;
+ }
+
+ switch(sig)
+ {
+ case LOCAL_HEADER_SIG:
+ case EOCD_SIG: // kludge for strange archives
+ {
+ ushort extractVersion = reader.ReadUInt16();
+ ushort flags = reader.ReadUInt16();
+ ushort method = reader.ReadUInt16();
+ uint dosDateTime = reader.ReadUInt32();
+ uint crc32 = reader.ReadUInt32();
+ long compSize = reader.ReadUInt32();
+ long uncompSize = reader.ReadUInt32();
+ ushort nameLength = reader.ReadUInt16();
+ ushort extraLength = reader.ReadUInt16();
+
+ long dataOffset = _stream.Position + nameLength + extraLength;
+
+ byte[] nameBytes = nameLength > 0 ? reader.ReadBytes(nameLength) : null;
+
+ var entry = new Entry
+ {
+ Method = (CompressionMethod)method,
+ Flags = flags,
+ Crc32 = crc32,
+ CompressedSize = compSize,
+ UncompressedSize = uncompSize,
+ LastWriteTime = DosToDateTime(dosDateTime),
+ IsEncrypted = (flags & FLAG_ENCRYPTED) != 0
+ };
+
+ // Parse extra fields
+ if(extraLength > 0)
+ {
+ long extraStart = _stream.Position;
+
+ // ZIP64 fields may override sizes
+ ParseExtraFields(reader, extraLength, ref entry);
+
+ _stream.Position = extraStart + extraLength;
+ }
+
+ // If data descriptor flag is set, we need to find the sizes after the data
+ if((flags & FLAG_DATA_DESCRIPTOR) != 0)
+ {
+ // Skip to the data descriptor after compressed data
+ // For streaming archives, scan for the next signature
+ FindDataDescriptor(reader, ref entry);
+ }
+
+ // Decode filename
+ if(nameBytes is not null)
+ {
+ entry.Filename = (flags & FLAG_UTF8) != 0
+ ? Encoding.UTF8.GetString(nameBytes)
+ : _encoding.GetString(nameBytes);
+
+ entry.Filename = entry.Filename.Replace('\\', '/');
+
+ if(entry.Filename.EndsWith('/') && entry.UncompressedSize == 0) entry.IsDirectory = true;
+ }
+
+ entry.DataOffset = dataOffset;
+
+ long nextPos = (flags & FLAG_DATA_DESCRIPTOR) != 0
+ ? _stream.Position
+ : dataOffset + entry.CompressedSize;
+
+ _entries.Add(entry);
+
+ _stream.Position = nextPos;
+
+ break;
+ }
+
+ case CENTRAL_DIR_SIG:
+ // Hit central directory — stop scanning
+ return;
+
+ default:
+ // Try to find the next valid entry
+ if(!FindNextLocalHeader(reader)) return;
+
+ break;
+ }
+ }
+ }
+
+ void ParseExtraFields(BinaryReader reader, int length, ref Entry entry)
+ {
+ long end = _stream.Position + length;
+
+ while(_stream.Position + 4 <= end)
+ {
+ ushort extId = reader.ReadUInt16();
+ ushort extSize = reader.ReadUInt16();
+
+ if(_stream.Position + extSize > end) break;
+
+ long nextExtra = _stream.Position + extSize;
+
+ switch(extId)
+ {
+ case EXTRA_ZIP64:
+ if(entry.UncompressedSize == 0xFFFFFFFF && _stream.Position + 8 <= nextExtra)
+ entry.UncompressedSize = reader.ReadInt64();
+
+ if(entry.CompressedSize == 0xFFFFFFFF && _stream.Position + 8 <= nextExtra)
+ entry.CompressedSize = reader.ReadInt64();
+
+ break;
+
+ case EXTRA_EXT_TIMESTAMP when extSize >= 1:
+ {
+ byte tsFlags = reader.ReadByte();
+
+ if((tsFlags & 1) != 0 && _stream.Position + 4 <= nextExtra)
+ entry.LastWriteTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadInt32()).DateTime;
+
+ if((tsFlags & 2) != 0 && _stream.Position + 4 <= nextExtra)
+ entry.LastAccessTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadInt32()).DateTime;
+
+ if((tsFlags & 4) != 0 && _stream.Position + 4 <= nextExtra)
+ entry.CreationTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadInt32()).DateTime;
+
+ break;
+ }
+
+ case EXTRA_UNIX1 when extSize >= 8:
+ {
+ entry.LastAccessTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadInt32()).DateTime;
+ entry.LastWriteTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadInt32()).DateTime;
+
+ if(extSize >= 10) entry.Uid = reader.ReadUInt16();
+
+ if(extSize >= 12) entry.Gid = reader.ReadUInt16();
+
+ break;
+ }
+
+ case EXTRA_UNIX2 when extSize >= 4:
+ {
+ entry.Uid = reader.ReadUInt16();
+ entry.Gid = reader.ReadUInt16();
+
+ break;
+ }
+
+ case EXTRA_UNIX3 when extSize >= 4:
+ {
+ byte version = reader.ReadByte();
+
+ if(version == 1)
+ {
+ byte uidSize = reader.ReadByte();
+
+ entry.Uid = uidSize switch
+ {
+ 2 => reader.ReadUInt16(),
+ 4 => reader.ReadUInt32(),
+ _ => entry.Uid
+ };
+
+ if(uidSize != 2 && uidSize != 4) _stream.Position += uidSize;
+
+ byte gidSize = reader.ReadByte();
+
+ entry.Gid = gidSize switch
+ {
+ 2 => reader.ReadUInt16(),
+ 4 => reader.ReadUInt32(),
+ _ => entry.Gid
+ };
+
+ if(gidSize != 2 && gidSize != 4) _stream.Position += gidSize;
+ }
+
+ break;
+ }
+
+ case EXTRA_UNICODE_PATH when extSize >= 6:
+ {
+ byte unicodeVersion = reader.ReadByte();
+
+ if(unicodeVersion == 1)
+ {
+ uint nameCrc = reader.ReadUInt32();
+
+ int unicodeLen = extSize - 5;
+
+ if(unicodeLen > 0)
+ {
+ byte[] unicodeBytes = reader.ReadBytes(unicodeLen);
+
+ // Strip trailing null bytes
+ int trimLen = unicodeLen;
+
+ while(trimLen > 0 && unicodeBytes[trimLen - 1] == 0) trimLen--;
+
+ if(trimLen > 0)
+ {
+ string unicodeName = Encoding.UTF8.GetString(unicodeBytes, 0, trimLen);
+
+ // Normalize path separators
+ unicodeName = unicodeName.Replace('\\', '/');
+ entry.Filename = unicodeName;
+ }
+ }
+ }
+
+ break;
+ }
+
+ case EXTRA_WINZIP_AES when extSize >= 7:
+ {
+ ushort aesVersion = reader.ReadUInt16();
+ ushort aesVendor = reader.ReadUInt16();
+ byte aesKeySize = reader.ReadByte();
+ ushort aesMethod = reader.ReadUInt16();
+
+ // The actual compression method is stored in the AES extra field
+ if(entry.Method == CompressionMethod.WinZipAes) entry.Method = (CompressionMethod)aesMethod;
+
+ entry.IsEncrypted = true;
+
+ break;
+ }
+ }
+
+ _stream.Position = nextExtra;
+ }
+
+ _stream.Position = end;
+ }
+
+ void FindDataDescriptor(BinaryReader reader, ref Entry entry)
+ {
+ // When the data descriptor flag is set, the CRC and sizes follow the compressed data.
+ // We scan for the data descriptor signature or the next local header/central directory.
+ long startPos = _stream.Position + entry.CompressedSize;
+
+ if(startPos >= _stream.Length) return;
+
+ _stream.Position = startPos;
+
+ // Try to read a data descriptor at the expected position
+ if(_stream.Position + 16 <= _stream.Length)
+ {
+ uint possibleSig = reader.ReadUInt32();
+
+ if(possibleSig == DATA_DESCRIPTOR_SIG)
+ {
+ // Data descriptor with signature
+ entry.Crc32 = reader.ReadUInt32();
+ entry.CompressedSize = reader.ReadUInt32();
+ entry.UncompressedSize = reader.ReadUInt32();
+
+ // Check for ZIP64 data descriptor (8-byte sizes)
+ if(entry.CompressedSize == 0xFFFFFFFF || entry.UncompressedSize == 0xFFFFFFFF)
+ {
+ _stream.Position -= 8;
+ entry.CompressedSize = reader.ReadInt64();
+ entry.UncompressedSize = reader.ReadInt64();
+ }
+ }
+ else
+ {
+ // Data descriptor without signature (CRC directly)
+ entry.Crc32 = possibleSig;
+ entry.CompressedSize = reader.ReadUInt32();
+ entry.UncompressedSize = reader.ReadUInt32();
+ }
+ }
+ }
+
+ bool FindNextLocalHeader(BinaryReader reader)
+ {
+ // Scan forward for the next PK signature
+ while(_stream.Position < _stream.Length - 4)
+ {
+ byte b = reader.ReadByte();
+
+ if(b != 0x50) continue;
+
+ if(_stream.Position >= _stream.Length - 3) return false;
+
+ byte b2 = reader.ReadByte();
+
+ if(b2 != 0x4B)
+ {
+ _stream.Position--;
+
+ continue;
+ }
+
+ byte b3 = reader.ReadByte();
+ byte b4 = reader.ReadByte();
+
+ // Local header, central directory, or EOCD
+ if(b3 == 0x03 && b4 == 0x04 || b3 == 0x01 && b4 == 0x02 || b3 == 0x05 && b4 == 0x06)
+ {
+ _stream.Position -= 4;
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static DateTime DosToDateTime(uint dosDateTime)
+ {
+ var time = (int)(dosDateTime & 0xFFFF);
+ var date = (int)(dosDateTime >> 16);
+
+ int second = (time & 0x1F) * 2;
+ int minute = time >> 5 & 0x3F;
+ int hour = time >> 11 & 0x1F;
+
+ int day = date & 0x1F;
+ int month = date >> 5 & 0x0F;
+ int year = (date >> 9 & 0x7F) + 1980;
+
+ // Validate ranges
+ if(month < 1) month = 1;
+ if(month > 12) month = 12;
+ if(day < 1) day = 1;
+
+ if(day > DateTime.DaysInMonth(year, month)) day = DateTime.DaysInMonth(year, month);
+
+ if(hour > 23) hour = 23;
+ if(minute > 59) minute = 59;
+ if(second > 59) second = 59;
+
+ try
+ {
+ return new DateTime(year, month, day, hour, minute, second, DateTimeKind.Local);
+ }
+ catch(ArgumentOutOfRangeException)
+ {
+ return default(DateTime);
+ }
+ }
+
+ static uint ComputeCrc32(byte[] data)
+ {
+ var crc = 0xFFFFFFFF;
+
+ foreach(byte b in data)
+ {
+ crc ^= b;
+
+ for(var j = 0; j < 8; j++)
+ {
+ if((crc & 1) != 0)
+ crc = crc >> 1 ^ CRC_POLY;
+ else
+ crc >>= 1;
+ }
+ }
+
+ return crc ^ 0xFFFFFFFF;
+ }
+
+#region IArchive Members
+
+ ///
+ public ErrorNumber Open(IFilter filter, Encoding encoding)
+ {
+ if(filter is null || filter.DataForkLength < MIN_FILE_SIZE) return ErrorNumber.InvalidArgument;
+
+ _stream = filter.GetDataForkStream();
+ _encoding = encoding ?? Encoding.GetEncoding("ibm437");
+ _entries = [];
+
+ _features = ArchiveSupportedFeature.SupportsFilenames | ArchiveSupportedFeature.SupportsCompression;
+
+ long eocdOffset = -1;
+ long zip64LocOffset = -1;
+
+ FindEndOfCentralDirectory(out eocdOffset, out zip64LocOffset);
+
+ if(eocdOffset >= 0)
+ ParseCentralDirectory(eocdOffset, zip64LocOffset);
+ else
+ ParseWithoutCentralDirectory();
+
+ // Detect features from parsed entries
+ foreach(Entry entry in _entries)
+ {
+ if(entry.IsDirectory)
+ {
+ _features |= ArchiveSupportedFeature.SupportsSubdirectories |
+ ArchiveSupportedFeature.HasExplicitDirectories;
+ }
+
+ if(entry.LastWriteTime != default(DateTime)) _features |= ArchiveSupportedFeature.HasEntryTimestamp;
+
+ if(entry.Comment is not null) _features |= ArchiveSupportedFeature.SupportsXAttrs;
+
+ if(entry.IsEncrypted) _features |= ArchiveSupportedFeature.SupportsProtection;
+ }
+
+ Opened = true;
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public void Close()
+ {
+ _entries = null;
+ Opened = false;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Structs.cs b/Aaru.Archives/Zip/Structs.cs
new file mode 100644
index 000000000..0f1492fae
--- /dev/null
+++ b/Aaru.Archives/Zip/Structs.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+#region Nested type: Entry
+
+ struct Entry
+ {
+ public CompressionMethod Method;
+ public string Filename;
+ public long CompressedSize;
+ public long UncompressedSize;
+ public long DataOffset;
+ public DateTime LastWriteTime;
+ public DateTime LastAccessTime;
+ public DateTime CreationTime;
+ public uint Crc32;
+ public HostOs System;
+ public uint ExternalAttributes;
+ public ushort UnixPermissions;
+ public uint Uid;
+ public uint Gid;
+ public string Comment;
+ public ushort Flags;
+ public bool IsDirectory;
+ public bool IsEncrypted;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Xattrs.cs b/Aaru.Archives/Zip/Xattrs.cs
new file mode 100644
index 000000000..db359b528
--- /dev/null
+++ b/Aaru.Archives/Zip/Xattrs.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Text;
+using Aaru.CommonTypes.Enums;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip
+{
+#region IArchive Members
+
+ ///
+ public ErrorNumber ListXAttr(int entryNumber, out List xattrs)
+ {
+ xattrs = null;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ xattrs = [];
+
+ if(_entries[entryNumber].Comment is not null) xattrs.Add("comment");
+
+ return ErrorNumber.NoError;
+ }
+
+ ///
+ public ErrorNumber GetXattr(int entryNumber, string xattr, ref byte[] buffer)
+ {
+ buffer = null;
+
+ if(!Opened) return ErrorNumber.NotOpened;
+
+ if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
+
+ if(xattr != "comment" || _entries[entryNumber].Comment is null) return ErrorNumber.NoSuchExtendedAttribute;
+
+ buffer = Encoding.UTF8.GetBytes(_entries[entryNumber].Comment);
+
+ return ErrorNumber.NoError;
+ }
+
+#endregion
+}
\ No newline at end of file
diff --git a/Aaru.Archives/Zip/Zip.cs b/Aaru.Archives/Zip/Zip.cs
new file mode 100644
index 000000000..28ff6365c
--- /dev/null
+++ b/Aaru.Archives/Zip/Zip.cs
@@ -0,0 +1,37 @@
+using System;
+using System.IO;
+using System.Text;
+using Aaru.CommonTypes.Interfaces;
+
+namespace Aaru.Archives;
+
+public sealed partial class Zip : IArchive
+{
+ const string MODULE_NAME = "ZIP Archive Plugin";
+ string _archiveComment;
+ Encoding _encoding;
+ ArchiveSupportedFeature _features;
+ Stream _stream;
+
+#region IArchive Members
+
+ ///
+ public string Name => Localization.Zip_Name;
+ ///
+ public Guid Id => new("A3E2F8C1-6D4B-4E7A-B9C5-3F1D8A2E6B47");
+ ///
+ public string Author => Authors.NataliaPortillo;
+ ///
+ public bool Opened { get; private set; }
+ ///
+ public ArchiveSupportedFeature ArchiveFeatures => !Opened
+ ? ArchiveSupportedFeature.SupportsCompression |
+ ArchiveSupportedFeature.SupportsFilenames |
+ ArchiveSupportedFeature.SupportsSubdirectories |
+ ArchiveSupportedFeature.HasEntryTimestamp
+ : _features;
+ ///
+ public int NumberOfEntries => Opened ? _entries.Count : -1;
+
+#endregion
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index aef6a3e0f..0c05bce59 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,7 @@ Supported archives
* RAR (.RAR)
* Symbian Installation File (.SIS)
* Xbox 360 Secure Transacted File System (STFS)
+* ZIP (.ZIP)
* ZOO (.ZOO)
Supported partitioning schemes