blob: 9e55d199a9b5ce4d131bd414c90a993124edf361 [file] [log] [blame]
// 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))
* )
*
* 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: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
* Authors: $(HTTP 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) @safe
{
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.systime : 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;
// by default, no explicit order goes after explicit order
private uint _index = uint.max;
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 @safe 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
* $(REF getAttributes, std,file) or $(REF DirEntry.attributes, std,file), for this archive member.
*/
@property @safe 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) @safe 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 @safe 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;
}
/**
* The index of this archive member within the archive.
*/
@property uint index() const pure nothrow @nogc { return _index; }
@property uint index(uint value) pure nothrow @nogc { return _index = value; }
debug(print)
{
void print()
{
printf("name = '%.*s'\n", cast(int) name.length, name.ptr);
printf("\tcomment = '%.*s'\n", cast(int) 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);
printf("\tindex = x%08x\n", index);
}
}
}
/**
* Object representing the entire archive.
* ZipArchives are collections of ArchiveMembers.
*/
final class ZipArchive
{
import std.algorithm.comparison : max;
import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
import std.conv : to;
import std.datetime.systime : 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 @safe ubyte[] data() { return _data; }
/// Read Only: 0 since multi-disk zip archives are not supported.
@property @safe uint diskNumber() { return _diskNumber; }
/// Read Only: 0 since multi-disk zip archives are not supported
@property @safe uint diskStartDir() { return _diskStartDir; }
/// Read Only: number of ArchiveMembers in the directory.
@property @safe uint numEntries() { return _numEntries; }
@property @safe uint totalEntries() { return _totalEntries; } /// ditto
/// True when the archive is in Zip64 format.
@property @safe bool isZip64() { return _isZip64; }
/// Set this to true to force building a Zip64 archive.
@property @safe 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 @safe ArchiveMember[string] directory() { return _directory; }
private ArchiveMember[string] _directory;
debug (print)
{
@safe 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", cast(int) comment.length, comment.ptr);
}
}
/* ============ Creating a new archive =================== */
/** Constructor to use when creating a new archive.
*/
this() @safe
{
}
/** Add de to the archive. The file is compressed on the fly.
*/
@safe void addMember(ArchiveMember de)
{
_directory[de.name] = de;
if (!de._compressedData.length)
{
switch (de.compressionMethod)
{
case CompressionMethod.none:
de._compressedData = de._expandedData;
break;
case CompressionMethod.deflate:
import std.zlib : compress;
() @trusted
{
de._compressedData = cast(ubyte[]) 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);
import std.zlib : crc32;
() @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }();
}
assert(de._compressedData.length == de._compressedSize);
}
/** Delete de from the archive.
*/
@safe 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()
{
import std.algorithm.sorting : sort;
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;
auto directory = _directory.values().sort!((x, y) => x.index < y.index).release;
foreach (ArchiveMember de; directory)
{
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 > 66_000 ? to!uint(data.length - 66_000) : 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
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 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._index = n;
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.
import std.zlib : uncompress;
de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15);
return de.expandedData;
default:
throw new ZipException("unsupported compression method");
}
}
/* ============ Utility =================== */
@safe ushort getUshort(int i)
{
ubyte[2] result = data[i .. i + 2];
return littleEndianToNative!ushort(result);
}
@safe uint getUint(int i)
{
ubyte[4] result = data[i .. i + 4];
return littleEndianToNative!uint(result);
}
@safe ulong getUlong(int i)
{
ubyte[8] result = data[i .. i + 8];
return littleEndianToNative!ulong(result);
}
@safe void putUshort(int i, ushort us)
{
data[i .. i + 2] = nativeToLittleEndian(us);
}
@safe void putUint(int i, uint ui)
{
data[i .. i + 4] = nativeToLittleEndian(ui);
}
@safe void putUlong(int i, ulong ul)
{
data[i .. i + 8] = nativeToLittleEndian(ul);
}
}
debug(print)
{
@safe 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");
}
}
@system unittest
{
// @system due to (at least) ZipArchive.build
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.conv, std.stdio;
import std.random : uniform, MinstdRand0;
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);
}
}
}
@system unittest
{
import std.conv : to;
import std.random : Mt19937, randomShuffle;
// Test if packing and unpacking preserves order.
auto rand = Mt19937(15966);
string[] names;
int value = 0;
// Generate a series of unique numbers as filenames.
foreach (i; 0 .. 20)
{
value += 1 + rand.front & 0xFFFF;
rand.popFront;
names ~= value.to!string;
}
// Insert them in a random order.
names.randomShuffle(rand);
auto zip1 = new ZipArchive();
foreach (i, name; names)
{
auto member = new ArchiveMember();
member.name = name;
member.expandedData = cast(ubyte[]) name;
member.index = cast(int) i;
zip1.addMember(member);
}
auto data = zip1.build();
// Ensure that they appear in the same order.
auto zip2 = new ZipArchive(data);
foreach (i, name; names)
{
const member = zip2.directory[name];
assert(member.index == i, "member " ~ name ~ " had index " ~
member.index.to!string ~ " but we expected index " ~ i.to!string ~
". The input array was " ~ names.to!string);
}
}
@system 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);
}
@system unittest
{
// @system due to ZipArchive.build
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);
}
// Non-Android Posix-only, because we can't rely on the unzip command being
// available on Android or Windows
version (Android) {} else
version (Posix) @system unittest
{
import std.datetime, std.file, std.format, std.path, std.process, std.stdio;
if (executeShell("unzip").status != 0)
{
writeln("Can't run unzip, skipping unzip test");
return;
}
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);
}