Add ZIP archive support with entry handling, decompression, and metadata features

This commit is contained in:
2026-04-15 17:44:07 +01:00
parent f8dbd840b1
commit 9cf0ad9fe8
12 changed files with 1399 additions and 0 deletions

View File

@@ -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);
}
}
}
}

View File

@@ -411,4 +411,25 @@
<data name="RAR_has_recovery_record" xml:space="preserve">
<value>[slateblue1]El archivo tiene [green]registro de recuperación[/][/]</value>
</data>
<data name="Zip_Name" xml:space="preserve">
<value>ZIP</value>
</data>
<data name="ZIP_archive" xml:space="preserve">
<value>[bold][blue]Archivo ZIP[/][/]</value>
</data>
<data name="ZIP_entries_0" xml:space="preserve">
<value>[slateblue1]Entradas: [green]{0}[/][/]</value>
</data>
<data name="ZIP_files_0" xml:space="preserve">
<value>[slateblue1]Archivos: [green]{0}[/][/]</value>
</data>
<data name="ZIP_directories_0" xml:space="preserve">
<value>[slateblue1]Directorios: [green]{0}[/][/]</value>
</data>
<data name="ZIP_encrypted_entries_0" xml:space="preserve">
<value>[slateblue1]Entradas cifradas: [green]{0}[/][/]</value>
</data>
<data name="ZIP_comment_0" xml:space="preserve">
<value>[slateblue1]Comentario del archivo: [green]{0}[/][/]</value>
</data>
</root>

View File

@@ -419,4 +419,25 @@
<data name="RAR_has_recovery_record" xml:space="preserve">
<value>[slateblue1]Archive has [green]recovery record[/][/]</value>
</data>
<data name="Zip_Name" xml:space="preserve">
<value>ZIP</value>
</data>
<data name="ZIP_archive" xml:space="preserve">
<value>[bold][blue]ZIP archive[/][/]</value>
</data>
<data name="ZIP_entries_0" xml:space="preserve">
<value>[slateblue1]Entries: [green]{0}[/][/]</value>
</data>
<data name="ZIP_files_0" xml:space="preserve">
<value>[slateblue1]Files: [green]{0}[/][/]</value>
</data>
<data name="ZIP_directories_0" xml:space="preserve">
<value>[slateblue1]Directories: [green]{0}[/][/]</value>
</data>
<data name="ZIP_encrypted_entries_0" xml:space="preserve">
<value>[slateblue1]Encrypted entries: [green]{0}[/][/]</value>
</data>
<data name="ZIP_comment_0" xml:space="preserve">
<value>[slateblue1]Archive comment: [green]{0}[/][/]</value>
</data>
</root>

View File

@@ -0,0 +1,54 @@
namespace Aaru.Archives;
public sealed partial class Zip
{
/// <summary>Local file header signature: PK\x03\x04</summary>
const uint LOCAL_HEADER_SIG = 0x04034B50;
/// <summary>Central directory file header signature: PK\x01\x02</summary>
const uint CENTRAL_DIR_SIG = 0x02014B50;
/// <summary>End of central directory record signature: PK\x05\x06</summary>
const uint EOCD_SIG = 0x06054B50;
/// <summary>ZIP64 end of central directory record signature: PK\x06\x06</summary>
const uint ZIP64_EOCD_SIG = 0x06064B50;
/// <summary>ZIP64 end of central directory locator signature: PK\x06\x07</summary>
const uint ZIP64_LOCATOR_SIG = 0x07064B50;
/// <summary>Data descriptor signature: PK\x07\x08</summary>
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;
/// <summary>Minimum ZIP file size: empty EOCD record (22 bytes)</summary>
const int MIN_FILE_SIZE = 22;
/// <summary>Size of a fixed central directory entry (without variable-length fields)</summary>
const int CENTRAL_DIR_ENTRY_SIZE = 46;
/// <summary>Size of a fixed local file header (without variable-length fields)</summary>
const int LOCAL_HEADER_SIZE = 30;
/// <summary>Size of a fixed End of Central Directory record (without comment)</summary>
const int EOCD_SIZE = 22;
/// <summary>Chunk size for backward EOCD search</summary>
const int EOCD_SEARCH_CHUNK = 0x10000;
/// <summary>Directory attribute in DOS external file attributes</summary>
const uint ATTR_DIRECTORY = 0x10;
/// <summary>CRC32 polynomial used in ZIP format</summary>
const uint CRC_POLY = 0xEDB88320;
}

View File

@@ -0,0 +1,70 @@
namespace Aaru.Archives;
public sealed partial class Zip
{
#region CompressionMethod enum
enum CompressionMethod : ushort
{
/// <summary>No compression</summary>
Stored = 0,
/// <summary>LZW-based shrink</summary>
Shrink = 1,
/// <summary>Reduce with compression factor 1</summary>
Reduce1 = 2,
/// <summary>Reduce with compression factor 2</summary>
Reduce2 = 3,
/// <summary>Reduce with compression factor 3</summary>
Reduce3 = 4,
/// <summary>Reduce with compression factor 4</summary>
Reduce4 = 5,
/// <summary>Shannon-Fano + LZSS imploding</summary>
Implode = 6,
/// <summary>Deflate (RFC 1951)</summary>
Deflate = 8,
/// <summary>Deflate64 / Enhanced Deflate (64KB window)</summary>
Deflate64 = 9,
/// <summary>BZip2</summary>
BZip2 = 12,
/// <summary>LZMA</summary>
Lzma = 14,
/// <summary>WinZip JPEG compressed data</summary>
WinZipJpeg = 96,
/// <summary>WinZip WavPack compressed data</summary>
WavPack = 97,
/// <summary>PPMd version I, Rev 1</summary>
PPMd = 98,
/// <summary>WinZip AES encryption marker (actual method in extra field)</summary>
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
}

275
Aaru.Archives/Zip/Files.cs Normal file
View File

@@ -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
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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
}

83
Aaru.Archives/Zip/Info.cs Normal file
View File

@@ -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
/// <inheritdoc />
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;
}
/// <inheritdoc />
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
}

719
Aaru.Archives/Zip/Open.cs Normal file
View File

@@ -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<Entry> _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
/// <inheritdoc />
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;
}
/// <inheritdoc />
public void Close()
{
_entries = null;
Opened = false;
}
#endregion
}

View File

@@ -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
}

View File

@@ -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
/// <inheritdoc />
public ErrorNumber ListXAttr(int entryNumber, out List<string> 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;
}
/// <inheritdoc />
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
}

37
Aaru.Archives/Zip/Zip.cs Normal file
View File

@@ -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
/// <inheritdoc />
public string Name => Localization.Zip_Name;
/// <inheritdoc />
public Guid Id => new("A3E2F8C1-6D4B-4E7A-B9C5-3F1D8A2E6B47");
/// <inheritdoc />
public string Author => Authors.NataliaPortillo;
/// <inheritdoc />
public bool Opened { get; private set; }
/// <inheritdoc />
public ArchiveSupportedFeature ArchiveFeatures => !Opened
? ArchiveSupportedFeature.SupportsCompression |
ArchiveSupportedFeature.SupportsFilenames |
ArchiveSupportedFeature.SupportsSubdirectories |
ArchiveSupportedFeature.HasEntryTimestamp
: _features;
/// <inheritdoc />
public int NumberOfEntries => Opened ? _entries.Count : -1;
#endregion
}

View File

@@ -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