mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 18:16:24 +00:00
Add ZIP archive support with entry handling, decompression, and metadata features
This commit is contained in:
42
Aaru.Archives/Localization/Localization.Designer.cs
generated
42
Aaru.Archives/Localization/Localization.Designer.cs
generated
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
54
Aaru.Archives/Zip/Consts.cs
Normal file
54
Aaru.Archives/Zip/Consts.cs
Normal 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;
|
||||
}
|
||||
70
Aaru.Archives/Zip/Enums.cs
Normal file
70
Aaru.Archives/Zip/Enums.cs
Normal 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
275
Aaru.Archives/Zip/Files.cs
Normal 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
83
Aaru.Archives/Zip/Info.cs
Normal 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
719
Aaru.Archives/Zip/Open.cs
Normal 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
|
||||
}
|
||||
32
Aaru.Archives/Zip/Structs.cs
Normal file
32
Aaru.Archives/Zip/Structs.cs
Normal 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
|
||||
}
|
||||
44
Aaru.Archives/Zip/Xattrs.cs
Normal file
44
Aaru.Archives/Zip/Xattrs.cs
Normal 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
37
Aaru.Archives/Zip/Zip.cs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user