diff options
Diffstat (limited to 'libphobos/src/std/zip.d')
-rw-r--r-- | libphobos/src/std/zip.d | 990 |
1 files changed, 990 insertions, 0 deletions
diff --git a/libphobos/src/std/zip.d b/libphobos/src/std/zip.d new file mode 100644 index 0000000..db47dde --- /dev/null +++ b/libphobos/src/std/zip.d @@ -0,0 +1,990 @@ +// 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", 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); + 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", 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; + + 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); +} |