mirror of
https://github.com/dlang/phobos.git
synced 2025-04-29 06:30:28 +03:00

For whatever reason, the dmd test suite has tests for std.zip, though for the most part, they seem to just run it without actually checking the results, making them of questionable value. But this commit ports them to std.zip (to remove a Phobos dependency in the dmd test suite) and attempts to make it so that they actually check the results. So, the tests are not identical by any means, but they're similar. Perhaps the most significant test is the one that tests a file that's written with std.zip against the unzip utility, since that's testing against something that we didn't write.
952 lines
31 KiB
D
952 lines
31 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
|
|
*
|
|
* Examples:
|
|
* ---
|
|
// 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
|
|
|
|
// Explicitly undocumented. It will be removed in January 2015.
|
|
deprecated("Please use fileAttributes instead.")
|
|
@property ref inout(ushort) madeVersion() inout @safe pure nothrow
|
|
{ return _madeVersion; }
|
|
|
|
// Explicitly undocumented. It will be removed in January 2015.
|
|
deprecated("Please use fileAttributes instead.")
|
|
@property ref inout(uint) externalAttributes() inout @safe pure nothrow
|
|
{ return _externalAttributes; }
|
|
|
|
/// 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; }
|
|
|
|
// Explicitly undocumented. It will be removed in January 2015.
|
|
deprecated("Please use the enum CompressionMethod to set this property instead.")
|
|
@property void compressionMethod(ushort cm)
|
|
{
|
|
compressionMethod = cast(CompressionMethod)(cm);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|