mirror of
https://github.com/dlang/phobos.git
synced 2025-04-27 13:40:20 +03:00
935 lines
30 KiB
D
935 lines
30 KiB
D
// Written in the D programming language.
|
|
|
|
/**
|
|
* Read/write data in the $(LINK2 http://www.info-_zip.org, zip archive) format.
|
|
* Makes use of the etc.c.zlib compression library.
|
|
*
|
|
* Bugs:
|
|
* $(UL
|
|
* $(LI Multi-disk zips not supported.)
|
|
* $(LI Only Zip version 20 formats are supported.)
|
|
* $(LI Only supports compression modes 0 (no compression) and 8 (deflate).)
|
|
* $(LI Does not support encryption.)
|
|
* $(LI $(BUGZILLA 592))
|
|
* $(LI $(BUGZILLA 2137))
|
|
* )
|
|
*
|
|
* Macros:
|
|
* WIKI = Phobos/StdZip
|
|
*
|
|
* Example:
|
|
* ---
|
|
// Read existing zip file.
|
|
import std.digest.crc, std.file, std.stdio, std.zip;
|
|
|
|
void main(string[] args)
|
|
{
|
|
// read a zip file into memory
|
|
auto zip = new ZipArchive(read(args[1]));
|
|
writeln("Archive: ", args[1]);
|
|
writefln("%-10s %-8s Name", "Length", "CRC-32");
|
|
// iterate over all zip members
|
|
foreach (name, am; zip.directory)
|
|
{
|
|
// print some data about each member
|
|
writefln("%10s %08x %s", am.expandedSize, am.crc32, name);
|
|
assert(am.expandedData.length == 0);
|
|
// decompress the archive member
|
|
zip.expand(am);
|
|
assert(am.expandedData.length == am.expandedSize);
|
|
}
|
|
}
|
|
|
|
// Create and write new zip file.
|
|
import std.file: write;
|
|
import std.string: representation;
|
|
|
|
void main()
|
|
{
|
|
char[] data = "Test data.\n".dup;
|
|
// Create an ArchiveMember for the test file.
|
|
ArchiveMember am = new ArchiveMember();
|
|
am.name = "test.txt";
|
|
am.expandedData(data.representation);
|
|
// Create an archive and add the member.
|
|
ZipArchive zip = new ZipArchive();
|
|
zip.addMember(am);
|
|
// Build the archive
|
|
void[] compressed_data = zip.build();
|
|
// Write to a file
|
|
write("test.zip", compressed_data);
|
|
}
|
|
* ---
|
|
*
|
|
* Copyright: Copyright Digital Mars 2000 - 2009.
|
|
* License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
|
|
* Authors: $(WEB digitalmars.com, Walter Bright)
|
|
* Source: $(PHOBOSSRC std/_zip.d)
|
|
*/
|
|
|
|
/* Copyright Digital Mars 2000 - 2009.
|
|
* Distributed under the Boost Software License, Version 1.0.
|
|
* (See accompanying file LICENSE_1_0.txt or copy at
|
|
* http://www.boost.org/LICENSE_1_0.txt)
|
|
*/
|
|
module std.zip;
|
|
|
|
//debug=print;
|
|
|
|
/** Thrown on error.
|
|
*/
|
|
class ZipException : Exception
|
|
{
|
|
this(string msg)
|
|
{
|
|
super("ZipException: " ~ msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compression method used by ArchiveMember
|
|
*/
|
|
enum CompressionMethod : ushort
|
|
{
|
|
none = 0, /// No compression, just archiving
|
|
deflate = 8 /// Deflate algorithm. Use zlib library to compress
|
|
}
|
|
|
|
/**
|
|
* A member of the ZipArchive.
|
|
*/
|
|
final class ArchiveMember
|
|
{
|
|
import std.conv : to, octal;
|
|
import std.datetime : DosFileTime, SysTime, SysTimeToDosFileTime;
|
|
|
|
/**
|
|
* Read/Write: Usually the file name of the archive member; it is used to
|
|
* index the archive directory for the member. Each member must have a unique
|
|
* name[]. Do not change without removing member from the directory first.
|
|
*/
|
|
string name;
|
|
|
|
ubyte[] extra; /// Read/Write: extra data for this member.
|
|
string comment; /// Read/Write: comment associated with this member.
|
|
|
|
private ubyte[] _compressedData;
|
|
private ubyte[] _expandedData;
|
|
private uint offset;
|
|
private uint _crc32;
|
|
private uint _compressedSize;
|
|
private uint _expandedSize;
|
|
private CompressionMethod _compressionMethod;
|
|
private ushort _madeVersion = 20;
|
|
private ushort _extractVersion = 20;
|
|
private ushort _diskNumber;
|
|
private uint _externalAttributes;
|
|
private DosFileTime _time;
|
|
|
|
ushort flags; /// Read/Write: normally set to 0
|
|
ushort internalAttributes; /// Read/Write
|
|
|
|
@property ushort extractVersion() { return _extractVersion; } /// Read Only
|
|
@property uint crc32() { return _crc32; } /// Read Only: cyclic redundancy check (CRC) value
|
|
|
|
/// Read Only: size of data of member in compressed form.
|
|
@property uint compressedSize() { return _compressedSize; }
|
|
|
|
/// Read Only: size of data of member in expanded form.
|
|
@property uint expandedSize() { return _expandedSize; }
|
|
@property ushort diskNumber() { return _diskNumber; } /// Read Only: should be 0.
|
|
|
|
/// Read Only: data of member in compressed form.
|
|
@property ubyte[] compressedData() { return _compressedData; }
|
|
|
|
/// Read data of member in uncompressed form.
|
|
@property ubyte[] expandedData() { return _expandedData; }
|
|
|
|
/// Write data of member in uncompressed form.
|
|
@property void expandedData(ubyte[] ed)
|
|
{
|
|
_expandedData = ed;
|
|
_expandedSize = to!uint(_expandedData.length);
|
|
|
|
// Clean old compressed data, if any
|
|
_compressedData.length = 0;
|
|
_compressedSize = 0;
|
|
}
|
|
|
|
/**
|
|
* Set the OS specific file attributes, as obtained by
|
|
* $(XREF file,getAttributes) or $(XREF file,DirEntry.attributes), for this archive member.
|
|
*/
|
|
@property void fileAttributes(uint attr)
|
|
{
|
|
version (Posix)
|
|
{
|
|
_externalAttributes = (attr & 0xFFFF) << 16;
|
|
_madeVersion &= 0x00FF;
|
|
_madeVersion |= 0x0300; // attributes are in UNIX format
|
|
}
|
|
else version (Windows)
|
|
{
|
|
_externalAttributes = attr;
|
|
_madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format
|
|
}
|
|
else
|
|
{
|
|
static assert(0, "Unimplemented platform");
|
|
}
|
|
}
|
|
|
|
version (Posix) unittest
|
|
{
|
|
auto am = new ArchiveMember();
|
|
am.fileAttributes = octal!100644;
|
|
assert(am._externalAttributes == octal!100644 << 16);
|
|
assert((am._madeVersion & 0xFF00) == 0x0300);
|
|
}
|
|
|
|
/**
|
|
* Get the OS specific file attributes for the archive member.
|
|
*
|
|
* Returns: The file attributes or 0 if the file attributes were
|
|
* encoded for an incompatible OS (Windows vs. Posix).
|
|
*
|
|
*/
|
|
@property uint fileAttributes() const
|
|
{
|
|
version (Posix)
|
|
{
|
|
if ((_madeVersion & 0xFF00) == 0x0300)
|
|
return _externalAttributes >> 16;
|
|
return 0;
|
|
}
|
|
else version (Windows)
|
|
{
|
|
if ((_madeVersion & 0xFF00) == 0x0000)
|
|
return _externalAttributes;
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
static assert(0, "Unimplemented platform");
|
|
}
|
|
}
|
|
|
|
/// Set the last modification time for this member.
|
|
@property void time(SysTime time)
|
|
{
|
|
_time = SysTimeToDosFileTime(time);
|
|
}
|
|
|
|
/// ditto
|
|
@property void time(DosFileTime time)
|
|
{
|
|
_time = time;
|
|
}
|
|
|
|
/// Get the last modification time for this member.
|
|
@property DosFileTime time() const
|
|
{
|
|
return _time;
|
|
}
|
|
|
|
/**
|
|
* Read compression method used for this member
|
|
* See_Also:
|
|
* CompressionMethod
|
|
**/
|
|
@property CompressionMethod compressionMethod() { return _compressionMethod; }
|
|
|
|
/**
|
|
* Write compression method used for this member
|
|
* See_Also:
|
|
* CompressionMethod
|
|
**/
|
|
@property void compressionMethod(CompressionMethod cm)
|
|
{
|
|
if (cm == _compressionMethod) return;
|
|
|
|
if (_compressedSize > 0)
|
|
throw new ZipException("Can't change compression method for a compressed element");
|
|
|
|
_compressionMethod = cm;
|
|
}
|
|
|
|
debug(print)
|
|
{
|
|
void print()
|
|
{
|
|
printf("name = '%.*s'\n", name.length, name.ptr);
|
|
printf("\tcomment = '%.*s'\n", comment.length, comment.ptr);
|
|
printf("\tmadeVersion = x%04x\n", _madeVersion);
|
|
printf("\textractVersion = x%04x\n", extractVersion);
|
|
printf("\tflags = x%04x\n", flags);
|
|
printf("\tcompressionMethod = %d\n", compressionMethod);
|
|
printf("\ttime = %d\n", time);
|
|
printf("\tcrc32 = x%08x\n", crc32);
|
|
printf("\texpandedSize = %d\n", expandedSize);
|
|
printf("\tcompressedSize = %d\n", compressedSize);
|
|
printf("\tinternalAttributes = x%04x\n", internalAttributes);
|
|
printf("\texternalAttributes = x%08x\n", externalAttributes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Object representing the entire archive.
|
|
* ZipArchives are collections of ArchiveMembers.
|
|
*/
|
|
final class ZipArchive
|
|
{
|
|
import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
|
|
import std.algorithm : max;
|
|
import std.conv : to;
|
|
import std.zlib : compress;
|
|
import std.datetime : DosFileTime;
|
|
|
|
string comment; /// Read/Write: the archive comment. Must be less than 65536 bytes in length.
|
|
|
|
private ubyte[] _data;
|
|
private uint endrecOffset;
|
|
|
|
private uint _diskNumber;
|
|
private uint _diskStartDir;
|
|
private uint _numEntries;
|
|
private uint _totalEntries;
|
|
private bool _isZip64;
|
|
static const ushort zip64ExtractVersion = 45;
|
|
static const int digiSignLength = 6;
|
|
static const int eocd64LocLength = 20;
|
|
static const int eocd64Length = 56;
|
|
|
|
/// Read Only: array representing the entire contents of the archive.
|
|
@property ubyte[] data() { return _data; }
|
|
|
|
/// Read Only: 0 since multi-disk zip archives are not supported.
|
|
@property uint diskNumber() { return _diskNumber; }
|
|
|
|
/// Read Only: 0 since multi-disk zip archives are not supported
|
|
@property uint diskStartDir() { return _diskStartDir; }
|
|
|
|
/// Read Only: number of ArchiveMembers in the directory.
|
|
@property uint numEntries() { return _numEntries; }
|
|
@property uint totalEntries() { return _totalEntries; } /// ditto
|
|
|
|
/// True when the archive is in Zip64 format.
|
|
@property bool isZip64() { return _isZip64; }
|
|
|
|
/// Set this to true to force building a Zip64 archive.
|
|
@property void isZip64(bool value) { _isZip64 = value; }
|
|
/**
|
|
* Read Only: array indexed by the name of each member of the archive.
|
|
* All the members of the archive can be accessed with a foreach loop:
|
|
* Example:
|
|
* --------------------
|
|
* ZipArchive archive = new ZipArchive(data);
|
|
* foreach (ArchiveMember am; archive.directory)
|
|
* {
|
|
* writefln("member name is '%s'", am.name);
|
|
* }
|
|
* --------------------
|
|
*/
|
|
@property ArchiveMember[string] directory() { return _directory; }
|
|
|
|
private ArchiveMember[string] _directory;
|
|
|
|
debug (print)
|
|
{
|
|
void print()
|
|
{
|
|
printf("\tdiskNumber = %u\n", diskNumber);
|
|
printf("\tdiskStartDir = %u\n", diskStartDir);
|
|
printf("\tnumEntries = %u\n", numEntries);
|
|
printf("\ttotalEntries = %u\n", totalEntries);
|
|
printf("\tcomment = '%.*s'\n", comment.length, comment.ptr);
|
|
}
|
|
}
|
|
|
|
/* ============ Creating a new archive =================== */
|
|
|
|
/** Constructor to use when creating a new archive.
|
|
*/
|
|
this()
|
|
{
|
|
}
|
|
|
|
/** Add de to the archive.
|
|
*/
|
|
void addMember(ArchiveMember de)
|
|
{
|
|
_directory[de.name] = de;
|
|
}
|
|
|
|
/** Delete de from the archive.
|
|
*/
|
|
void deleteMember(ArchiveMember de)
|
|
{
|
|
_directory.remove(de.name);
|
|
}
|
|
|
|
/**
|
|
* Construct an archive out of the current members of the archive.
|
|
*
|
|
* Fills in the properties data[], diskNumber, diskStartDir, numEntries,
|
|
* totalEntries, and directory[].
|
|
* For each ArchiveMember, fills in properties crc32, compressedSize,
|
|
* compressedData[].
|
|
*
|
|
* Returns: array representing the entire archive.
|
|
*/
|
|
void[] build()
|
|
{ uint i;
|
|
uint directoryOffset;
|
|
|
|
if (comment.length > 0xFFFF)
|
|
throw new ZipException("archive comment longer than 65535");
|
|
|
|
// Compress each member; compute size
|
|
uint archiveSize = 0;
|
|
uint directorySize = 0;
|
|
foreach (ArchiveMember de; _directory)
|
|
{
|
|
if (!de._compressedData.length)
|
|
{
|
|
switch (de.compressionMethod)
|
|
{
|
|
case CompressionMethod.none:
|
|
de._compressedData = de._expandedData;
|
|
break;
|
|
|
|
case CompressionMethod.deflate:
|
|
de._compressedData = cast(ubyte[])std.zlib.compress(cast(void[])de._expandedData);
|
|
de._compressedData = de._compressedData[2 .. de._compressedData.length - 4];
|
|
break;
|
|
|
|
default:
|
|
throw new ZipException("unsupported compression method");
|
|
}
|
|
|
|
de._compressedSize = to!uint(de._compressedData.length);
|
|
de._crc32 = std.zlib.crc32(0, cast(void[])de._expandedData);
|
|
}
|
|
assert(de._compressedData.length == de._compressedSize);
|
|
|
|
if (to!ulong(archiveSize) + 30 + de.name.length + de.extra.length + de.compressedSize
|
|
+ directorySize + 46 + de.name.length + de.extra.length + de.comment.length
|
|
+ 22 + comment.length + eocd64LocLength + eocd64Length > uint.max)
|
|
throw new ZipException("zip files bigger than 4 GB are unsupported");
|
|
|
|
archiveSize += 30 + de.name.length +
|
|
de.extra.length +
|
|
de.compressedSize;
|
|
directorySize += 46 + de.name.length +
|
|
de.extra.length +
|
|
de.comment.length;
|
|
}
|
|
|
|
if (!isZip64 && _directory.length > ushort.max)
|
|
_isZip64 = true;
|
|
uint dataSize = archiveSize + directorySize + 22 + cast(uint)comment.length;
|
|
if (isZip64)
|
|
dataSize += eocd64LocLength + eocd64Length;
|
|
|
|
_data = new ubyte[dataSize];
|
|
|
|
// Populate the data[]
|
|
|
|
// Store each archive member
|
|
i = 0;
|
|
foreach (ArchiveMember de; _directory)
|
|
{
|
|
de.offset = i;
|
|
_data[i .. i + 4] = cast(ubyte[])"PK\x03\x04";
|
|
putUshort(i + 4, de.extractVersion);
|
|
putUshort(i + 6, de.flags);
|
|
putUshort(i + 8, de._compressionMethod);
|
|
putUint (i + 10, cast(uint)de.time);
|
|
putUint (i + 14, de.crc32);
|
|
putUint (i + 18, de.compressedSize);
|
|
putUint (i + 22, to!uint(de.expandedSize));
|
|
putUshort(i + 26, cast(ushort)de.name.length);
|
|
putUshort(i + 28, cast(ushort)de.extra.length);
|
|
i += 30;
|
|
|
|
_data[i .. i + de.name.length] = (cast(ubyte[])de.name)[];
|
|
i += de.name.length;
|
|
_data[i .. i + de.extra.length] = (cast(ubyte[])de.extra)[];
|
|
i += de.extra.length;
|
|
_data[i .. i + de.compressedSize] = de.compressedData[];
|
|
i += de.compressedSize;
|
|
}
|
|
|
|
// Write directory
|
|
directoryOffset = i;
|
|
_numEntries = 0;
|
|
foreach (ArchiveMember de; _directory)
|
|
{
|
|
_data[i .. i + 4] = cast(ubyte[])"PK\x01\x02";
|
|
putUshort(i + 4, de._madeVersion);
|
|
putUshort(i + 6, de.extractVersion);
|
|
putUshort(i + 8, de.flags);
|
|
putUshort(i + 10, de._compressionMethod);
|
|
putUint (i + 12, cast(uint)de.time);
|
|
putUint (i + 16, de.crc32);
|
|
putUint (i + 20, de.compressedSize);
|
|
putUint (i + 24, de.expandedSize);
|
|
putUshort(i + 28, cast(ushort)de.name.length);
|
|
putUshort(i + 30, cast(ushort)de.extra.length);
|
|
putUshort(i + 32, cast(ushort)de.comment.length);
|
|
putUshort(i + 34, de.diskNumber);
|
|
putUshort(i + 36, de.internalAttributes);
|
|
putUint (i + 38, de._externalAttributes);
|
|
putUint (i + 42, de.offset);
|
|
i += 46;
|
|
|
|
_data[i .. i + de.name.length] = (cast(ubyte[])de.name)[];
|
|
i += de.name.length;
|
|
_data[i .. i + de.extra.length] = (cast(ubyte[])de.extra)[];
|
|
i += de.extra.length;
|
|
_data[i .. i + de.comment.length] = (cast(ubyte[])de.comment)[];
|
|
i += de.comment.length;
|
|
_numEntries++;
|
|
}
|
|
_totalEntries = numEntries;
|
|
|
|
if (isZip64)
|
|
{
|
|
// Write zip64 end of central directory record
|
|
uint eocd64Offset = i;
|
|
_data[i .. i + 4] = cast(ubyte[])"PK\x06\x06";
|
|
putUlong (i + 4, eocd64Length - 12);
|
|
putUshort(i + 12, zip64ExtractVersion);
|
|
putUshort(i + 14, zip64ExtractVersion);
|
|
putUint (i + 16, diskNumber);
|
|
putUint (i + 20, diskStartDir);
|
|
putUlong (i + 24, numEntries);
|
|
putUlong (i + 32, totalEntries);
|
|
putUlong (i + 40, directorySize);
|
|
putUlong (i + 48, directoryOffset);
|
|
i += eocd64Length;
|
|
|
|
// Write zip64 end of central directory record locator
|
|
_data[i .. i + 4] = cast(ubyte[])"PK\x06\x07";
|
|
putUint (i + 4, diskNumber);
|
|
putUlong (i + 8, eocd64Offset);
|
|
putUint (i + 16, 1);
|
|
i += eocd64LocLength;
|
|
}
|
|
|
|
// Write end record
|
|
endrecOffset = i;
|
|
_data[i .. i + 4] = cast(ubyte[])"PK\x05\x06";
|
|
putUshort(i + 4, cast(ushort)diskNumber);
|
|
putUshort(i + 6, cast(ushort)diskStartDir);
|
|
putUshort(i + 8, (numEntries > ushort.max ? ushort.max : cast(ushort)numEntries));
|
|
putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort)totalEntries));
|
|
putUint (i + 12, directorySize);
|
|
putUint (i + 16, directoryOffset);
|
|
putUshort(i + 20, cast(ushort)comment.length);
|
|
i += 22;
|
|
|
|
// Write archive comment
|
|
assert(i + comment.length == data.length);
|
|
_data[i .. data.length] = (cast(ubyte[])comment)[];
|
|
|
|
return cast(void[])data;
|
|
}
|
|
|
|
/* ============ Reading an existing archive =================== */
|
|
|
|
/**
|
|
* Constructor to use when reading an existing archive.
|
|
*
|
|
* Fills in the properties data[], diskNumber, diskStartDir, numEntries,
|
|
* totalEntries, comment[], and directory[].
|
|
* For each ArchiveMember, fills in
|
|
* properties madeVersion, extractVersion, flags, compressionMethod, time,
|
|
* crc32, compressedSize, expandedSize, compressedData[], diskNumber,
|
|
* internalAttributes, externalAttributes, name[], extra[], comment[].
|
|
* Use expand() to get the expanded data for each ArchiveMember.
|
|
*
|
|
* Params:
|
|
* buffer = the entire contents of the archive.
|
|
*/
|
|
|
|
this(void[] buffer)
|
|
{ uint iend;
|
|
uint i;
|
|
int endcommentlength;
|
|
uint directorySize;
|
|
uint directoryOffset;
|
|
|
|
this._data = cast(ubyte[]) buffer;
|
|
|
|
if (data.length > uint.max - 2)
|
|
throw new ZipException("zip files bigger than 4 GB are unsupported");
|
|
|
|
// Find 'end record index' by searching backwards for signature
|
|
iend = (data.length > 66000 ? to!uint(data.length - 66000) : 0);
|
|
for (i = to!uint(data.length) - 22; 1; i--)
|
|
{
|
|
if (i < iend || i >= data.length)
|
|
throw new ZipException("no end record");
|
|
|
|
if (_data[i .. i + 4] == cast(ubyte[])"PK\x05\x06")
|
|
{
|
|
endcommentlength = getUshort(i + 20);
|
|
if (i + 22 + endcommentlength > data.length
|
|
|| i + 22 + endcommentlength < i)
|
|
continue;
|
|
comment = cast(string)(_data[i + 22 .. i + 22 + endcommentlength]);
|
|
endrecOffset = i;
|
|
|
|
uint k = i - eocd64LocLength;
|
|
if (k < i && _data[k .. k + 4] == cast(ubyte[])"PK\x06\x07")
|
|
{
|
|
_isZip64 = true;
|
|
i = k;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isZip64)
|
|
{
|
|
// Read Zip64 record data
|
|
uint eocd64LocStart = i;
|
|
ulong eocdOffset = getUlong(i + 8);
|
|
if (eocdOffset + eocd64Length > _data.length)
|
|
throw new ZipException("corrupted directory");
|
|
|
|
i = to!uint(eocdOffset);
|
|
if (_data[i .. i + 4] != cast(ubyte[])"PK\x06\x06")
|
|
throw new ZipException("invalid Zip EOCD64 signature");
|
|
|
|
ulong eocd64Size = getUlong(i + 4);
|
|
if (eocd64Size + i - 12 > data.length)
|
|
throw new ZipException("invalid Zip EOCD64 size");
|
|
|
|
_diskNumber = getUint(i + 16);
|
|
_diskStartDir = getUint(i + 20);
|
|
|
|
ulong numEntriesUlong = getUlong(i + 24);
|
|
ulong totalEntriesUlong = getUlong(i + 32);
|
|
ulong directorySizeUlong = getUlong(i + 40);
|
|
ulong directoryOffsetUlong = getUlong(i + 48);
|
|
|
|
if (numEntriesUlong > uint.max)
|
|
throw new ZipException("supposedly more than 4294967296 files in archive");
|
|
|
|
if (numEntriesUlong != totalEntriesUlong)
|
|
throw new ZipException("multiple disk zips not supported");
|
|
|
|
if (directorySizeUlong > i || directoryOffsetUlong > i
|
|
|| directorySizeUlong + directoryOffsetUlong > i)
|
|
throw new ZipException("corrupted directory");
|
|
|
|
_numEntries = to!uint(numEntriesUlong);
|
|
_totalEntries = to!uint(totalEntriesUlong);
|
|
directorySize = to!uint(directorySizeUlong);
|
|
directoryOffset = to!uint(directoryOffsetUlong);
|
|
}
|
|
else
|
|
{
|
|
// Read end record data
|
|
_diskNumber = getUshort(i + 4);
|
|
_diskStartDir = getUshort(i + 6);
|
|
|
|
_numEntries = getUshort(i + 8);
|
|
_totalEntries = getUshort(i + 10);
|
|
|
|
if (numEntries != totalEntries)
|
|
throw new ZipException("multiple disk zips not supported");
|
|
|
|
directorySize = getUint(i + 12);
|
|
directoryOffset = getUint(i + 16);
|
|
|
|
if (directoryOffset + directorySize > i)
|
|
throw new ZipException("corrupted directory");
|
|
}
|
|
|
|
i = directoryOffset;
|
|
for (int n = 0; n < numEntries; n++)
|
|
{
|
|
/* The format of an entry is:
|
|
* 'PK' 1, 2
|
|
* directory info
|
|
* path
|
|
* extra data
|
|
* comment
|
|
*/
|
|
|
|
uint offset;
|
|
uint namelen;
|
|
uint extralen;
|
|
uint commentlen;
|
|
|
|
if (_data[i .. i + 4] != cast(ubyte[])"PK\x01\x02")
|
|
throw new ZipException("invalid directory entry 1");
|
|
ArchiveMember de = new ArchiveMember();
|
|
de._madeVersion = getUshort(i + 4);
|
|
de._extractVersion = getUshort(i + 6);
|
|
de.flags = getUshort(i + 8);
|
|
de._compressionMethod = cast(CompressionMethod)getUshort(i + 10);
|
|
de.time = cast(DosFileTime)getUint(i + 12);
|
|
de._crc32 = getUint(i + 16);
|
|
de._compressedSize = getUint(i + 20);
|
|
de._expandedSize = getUint(i + 24);
|
|
namelen = getUshort(i + 28);
|
|
extralen = getUshort(i + 30);
|
|
commentlen = getUshort(i + 32);
|
|
de._diskNumber = getUshort(i + 34);
|
|
de.internalAttributes = getUshort(i + 36);
|
|
de._externalAttributes = getUint(i + 38);
|
|
de.offset = getUint(i + 42);
|
|
i += 46;
|
|
|
|
if (i + namelen + extralen + commentlen > directoryOffset + directorySize)
|
|
throw new ZipException("invalid directory entry 2");
|
|
|
|
de.name = cast(string)(_data[i .. i + namelen]);
|
|
i += namelen;
|
|
de.extra = _data[i .. i + extralen];
|
|
i += extralen;
|
|
de.comment = cast(string)(_data[i .. i + commentlen]);
|
|
i += commentlen;
|
|
|
|
immutable uint dataOffset = de.offset + 30 + namelen + extralen;
|
|
if (dataOffset + de.compressedSize > endrecOffset)
|
|
throw new ZipException("Invalid directory entry offset or size.");
|
|
de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize];
|
|
|
|
_directory[de.name] = de;
|
|
|
|
}
|
|
if (i != directoryOffset + directorySize)
|
|
throw new ZipException("invalid directory entry 3");
|
|
}
|
|
|
|
/*****
|
|
* Decompress the contents of archive member de and return the expanded
|
|
* data.
|
|
*
|
|
* Fills in properties extractVersion, flags, compressionMethod, time,
|
|
* crc32, compressedSize, expandedSize, expandedData[], name[], extra[].
|
|
*/
|
|
ubyte[] expand(ArchiveMember de)
|
|
{ uint namelen;
|
|
uint extralen;
|
|
|
|
if (_data[de.offset .. de.offset + 4] != cast(ubyte[])"PK\x03\x04")
|
|
throw new ZipException("invalid directory entry 4");
|
|
|
|
// These values should match what is in the main zip archive directory
|
|
de._extractVersion = getUshort(de.offset + 4);
|
|
de.flags = getUshort(de.offset + 6);
|
|
de._compressionMethod = cast(CompressionMethod)getUshort(de.offset + 8);
|
|
de.time = cast(DosFileTime)getUint(de.offset + 10);
|
|
de._crc32 = getUint(de.offset + 14);
|
|
de._compressedSize = max(getUint(de.offset + 18), de.compressedSize);
|
|
de._expandedSize = max(getUint(de.offset + 22), de.expandedSize);
|
|
namelen = getUshort(de.offset + 26);
|
|
extralen = getUshort(de.offset + 28);
|
|
|
|
debug(print)
|
|
{
|
|
printf("\t\texpandedSize = %d\n", de.expandedSize);
|
|
printf("\t\tcompressedSize = %d\n", de.compressedSize);
|
|
printf("\t\tnamelen = %d\n", namelen);
|
|
printf("\t\textralen = %d\n", extralen);
|
|
}
|
|
|
|
if (de.flags & 1)
|
|
throw new ZipException("encryption not supported");
|
|
|
|
int i;
|
|
i = de.offset + 30 + namelen + extralen;
|
|
if (i + de.compressedSize > endrecOffset)
|
|
throw new ZipException("invalid directory entry 5");
|
|
|
|
de._compressedData = _data[i .. i + de.compressedSize];
|
|
debug(print) arrayPrint(de.compressedData);
|
|
|
|
switch (de.compressionMethod)
|
|
{
|
|
case CompressionMethod.none:
|
|
de._expandedData = de.compressedData;
|
|
return de.expandedData;
|
|
|
|
case CompressionMethod.deflate:
|
|
// -15 is a magic value used to decompress zip files.
|
|
// It has the effect of not requiring the 2 byte header
|
|
// and 4 byte trailer.
|
|
de._expandedData = cast(ubyte[])std.zlib.uncompress(cast(void[])de.compressedData, de.expandedSize, -15);
|
|
return de.expandedData;
|
|
|
|
default:
|
|
throw new ZipException("unsupported compression method");
|
|
}
|
|
}
|
|
|
|
/* ============ Utility =================== */
|
|
|
|
ushort getUshort(int i)
|
|
{
|
|
ubyte[2] result = data[i .. i + 2];
|
|
return littleEndianToNative!ushort(result);
|
|
}
|
|
|
|
uint getUint(int i)
|
|
{
|
|
ubyte[4] result = data[i .. i + 4];
|
|
return littleEndianToNative!uint(result);
|
|
}
|
|
|
|
ulong getUlong(int i)
|
|
{
|
|
ubyte[8] result = data[i .. i + 8];
|
|
return littleEndianToNative!ulong(result);
|
|
}
|
|
|
|
void putUshort(int i, ushort us)
|
|
{
|
|
data[i .. i + 2] = nativeToLittleEndian(us);
|
|
}
|
|
|
|
void putUint(int i, uint ui)
|
|
{
|
|
data[i .. i + 4] = nativeToLittleEndian(ui);
|
|
}
|
|
|
|
void putUlong(int i, ulong ul)
|
|
{
|
|
data[i .. i + 8] = nativeToLittleEndian(ul);
|
|
}
|
|
}
|
|
|
|
debug(print)
|
|
{
|
|
void arrayPrint(ubyte[] array)
|
|
{
|
|
printf("array %p,%d\n", cast(void*)array, array.length);
|
|
for (int i = 0; i < array.length; i++)
|
|
{
|
|
printf("%02x ", array[i]);
|
|
if (((i + 1) & 15) == 0)
|
|
printf("\n");
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
auto zip1 = new ZipArchive();
|
|
auto zip2 = new ZipArchive();
|
|
auto am1 = new ArchiveMember();
|
|
am1.name = "foo";
|
|
am1.expandedData = new ubyte[](1024);
|
|
zip1.addMember(am1);
|
|
auto data1 = zip1.build();
|
|
zip2.addMember(zip1.directory["foo"]);
|
|
zip2.build();
|
|
auto am2 = zip2.directory["foo"];
|
|
zip2.expand(am2);
|
|
assert(am1.expandedData == am2.expandedData);
|
|
auto zip3 = new ZipArchive(data1);
|
|
zip3.build();
|
|
assert(zip3.directory["foo"].compressedSize == am1.compressedSize);
|
|
|
|
// Test if packing and unpacking produces the original data
|
|
import std.random : uniform, MinstdRand0;
|
|
import std.stdio, std.conv;
|
|
MinstdRand0 gen;
|
|
const uint itemCount = 20, minSize = 10, maxSize = 500;
|
|
foreach (variant; 0..2)
|
|
{
|
|
bool useZip64 = !!variant;
|
|
zip1 = new ZipArchive();
|
|
zip1.isZip64 = useZip64;
|
|
ArchiveMember[itemCount] ams;
|
|
foreach (i; 0..itemCount)
|
|
{
|
|
ams[i] = new ArchiveMember();
|
|
ams[i].name = to!string(i);
|
|
ams[i].expandedData = new ubyte[](uniform(minSize, maxSize));
|
|
foreach (ref ubyte c; ams[i].expandedData)
|
|
c = cast(ubyte)(uniform(0, 256));
|
|
ams[i].compressionMethod = CompressionMethod.deflate;
|
|
zip1.addMember(ams[i]);
|
|
}
|
|
auto zippedData = zip1.build();
|
|
zip2 = new ZipArchive(zippedData);
|
|
assert(zip2.isZip64 == useZip64);
|
|
foreach (am; ams)
|
|
{
|
|
am2 = zip2.directory[am.name];
|
|
zip2.expand(am2);
|
|
assert(am.crc32 == am2.crc32);
|
|
assert(am.expandedData == am2.expandedData);
|
|
}
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
import std.zlib;
|
|
|
|
ubyte[] src = cast(ubyte[])
|
|
"the quick brown fox jumps over the lazy dog\r
|
|
the quick brown fox jumps over the lazy dog\r
|
|
";
|
|
auto dst = cast(ubyte[])compress(cast(void[])src);
|
|
auto after = cast(ubyte[])uncompress(cast(void[])dst);
|
|
assert(src == after);
|
|
}
|
|
|
|
unittest
|
|
{
|
|
import std.datetime;
|
|
ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9];
|
|
|
|
auto ar = new ZipArchive;
|
|
auto am = new ArchiveMember; // 10
|
|
am.name = "buf";
|
|
am.expandedData = buf;
|
|
am.compressionMethod = CompressionMethod.deflate;
|
|
am.time = SysTimeToDosFileTime(Clock.currTime());
|
|
ar.addMember(am); // 15
|
|
|
|
auto zip1 = ar.build();
|
|
auto arAfter = new ZipArchive(zip1);
|
|
assert(arAfter.directory.length == 1);
|
|
auto amAfter = arAfter.directory["buf"];
|
|
arAfter.expand(amAfter);
|
|
assert(amAfter.name == am.name);
|
|
assert(amAfter.expandedData == am.expandedData);
|
|
assert(amAfter.time == am.time);
|
|
}
|
|
|
|
// Posix-only, because we can't rely on the unzip command being available on Windows
|
|
version(Posix) unittest
|
|
{
|
|
import std.datetime, std.file, std.format, std.path, std.process, std.stdio;
|
|
|
|
auto zr = new ZipArchive();
|
|
auto am = new ArchiveMember();
|
|
am.compressionMethod = CompressionMethod.deflate;
|
|
am.name = "foo.bar";
|
|
am.time = SysTimeToDosFileTime(Clock.currTime());
|
|
am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine";
|
|
zr.addMember(am);
|
|
auto data2 = zr.build();
|
|
|
|
mkdirRecurse(deleteme);
|
|
scope(exit) rmdirRecurse(deleteme);
|
|
string zipFile = buildPath(deleteme, "foo.zip");
|
|
std.file.write(zipFile, cast(byte[])data2);
|
|
|
|
auto result = executeShell(format("unzip -l %s", zipFile));
|
|
scope(failure) writeln(result.output);
|
|
assert(result.status == 0);
|
|
}
|