// Written in the D programming language. /** Read and write data in the $(LINK2 https://en.wikipedia.org/wiki/Zip_%28file_format%29, zip archive) format. Standards: The current implementation mostly conforms to $(LINK2 https://www.iso.org/standard/60101.html, ISO/IEC 21320-1:2015), which means, $(UL $(LI that files can only be stored uncompressed or using the deflate mechanism,) $(LI that encryption features are not used,) $(LI that digital signature features are not used,) $(LI that patched data features are not used, and) $(LI that archives may not span multiple volumes.) ) Additionally, archives are checked for malware attacks and rejected if detected. This includes $(UL $(LI $(LINK2 https://news.ycombinator.com/item?id=20352439, zip bombs) which generate gigantic amounts of unpacked data) $(LI zip archives that contain overlapping records) $(LI chameleon zip archives which generate different unpacked data, depending on the implementation of the unpack algorithm) ) The current implementation makes use of the zlib compression library. Usage: There are two main ways of usage: Extracting files from a zip archive and storing files into a zip archive. These can be mixed though (e.g. read an archive, remove some files, add others and write the new archive). Examples: Example for reading an existing zip archive: --- import std.stdio : writeln, writefln; import std.file : read; import std.zip; void main(string[] args) { // read a zip file into memory auto zip = new ZipArchive(read(args[1])); // iterate over all zip members writefln("%-10s %-8s Name", "Length", "CRC-32"); 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); } } --- Example for writing files into a zip archive: --- import std.file : write; import std.string : representation; import std.zip; void main() { // Create an ArchiveMembers for each file. ArchiveMember file1 = new ArchiveMember(); file1.name = "test1.txt"; file1.expandedData("Test data.\n".dup.representation); file1.compressionMethod = CompressionMethod.none; // don't compress ArchiveMember file2 = new ArchiveMember(); file2.name = "test2.txt"; file2.expandedData("More test data.\n".dup.representation); file2.compressionMethod = CompressionMethod.deflate; // compress // Create an archive and add the member. ZipArchive zip = new ZipArchive(); // add ArchiveMembers zip.addMember(file1); zip.addMember(file2); // Build the archive void[] compressed_data = zip.build(); // Write to a file write("test.zip", compressed_data); } --- * Copyright: Copyright The D Language Foundation 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 The D Language Foundation 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; import std.exception : enforce; // Non-Android/Apple ARM POSIX-only, because we can't rely on the unzip // command being available on Android, Apple ARM or Windows version (Android) {} else version (iOS) {} else version (TVOS) {} else version (WatchOS) {} else version (Posix) version = HasUnzip; //debug=print; /// Thrown on error. class ZipException : Exception { import std.exception : basicExceptionCtors; /// mixin basicExceptionCtors; } /// Compression method used by `ArchiveMember`. enum CompressionMethod : ushort { none = 0, /// No compression, just archiving. deflate = 8 /// Deflate algorithm. Use zlib library to compress. } /// A single file or directory inside the archive. final class ArchiveMember { import std.conv : to, octal; import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime; /** * The 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; /** * The content of the extra data field for this member. See * $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT, * original documentation) * for a description of the general format of this data. May contain * undocumented 3rd-party data. */ ubyte[] extra; string comment; /// 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 uint _externalAttributes; private DosFileTime _time; // by default, no explicit order goes after explicit order private uint _index = uint.max; /** * Contains some information on how to extract this archive. See * $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT, * original documentation) * for details. */ ushort flags; /** * Internal attributes. Bit 1 is set, if the member is apparently in binary format * and bit 2 is set, if each record is preceded by the length of the record. */ ushort internalAttributes; /** * The zip file format version needed to extract this member. * * Returns: Format version needed to extract this member. */ @property @safe pure nothrow @nogc ushort extractVersion() const { return _extractVersion; } /** * Cyclic redundancy check (CRC) value. * * Returns: CRC32 value. */ @property @safe pure nothrow @nogc uint crc32() const { return _crc32; } /** * Size of data of member in compressed form. * * Returns: Size of the compressed archive. */ @property @safe pure nothrow @nogc uint compressedSize() const { return _compressedSize; } /** * Size of data of member in uncompressed form. * * Returns: Size of uncompressed archive. */ @property @safe pure nothrow @nogc uint expandedSize() const { return _expandedSize; } /** * Data of member in compressed form. * * Returns: The file data in compressed form. */ @property @safe pure nothrow @nogc ubyte[] compressedData() { return _compressedData; } /** * Get or set data of member in uncompressed form. When an existing archive is * read `ZipArchive.expand` needs to be called before this can be accessed. * * Params: * ed = Expanded Data. * * Returns: The file data. */ @property @safe pure nothrow @nogc ubyte[] expandedData() { return _expandedData; } /// ditto @property @safe void expandedData(ubyte[] ed) { _expandedData = ed; _expandedSize = to!uint(_expandedData.length); // Clean old compressed data, if any _compressedData.length = 0; _compressedSize = 0; } /** * Get or set the OS specific file attributes for this archive member. * * Params: * attr = Attributes as obtained by $(REF getAttributes, std,file) or * $(REF DirEntry.attributes, std,file). * * Returns: The file attributes or 0 if the file attributes were * encoded for an incompatible OS (Windows vs. POSIX). */ @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); } /// ditto @property @nogc nothrow 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"); } } /** * Get or set the last modification time for this member. * * Params: * time = Time to set (will be saved as DosFileTime, which is less accurate). * * Returns: * The last modification time in DosFileFormat. */ @property DosFileTime time() const @safe pure nothrow @nogc { return _time; } /// ditto @property void time(SysTime time) { _time = SysTimeToDosFileTime(time); } /// ditto @property void time(DosFileTime time) @safe pure nothrow @nogc { _time = time; } /** * Get or set compression method used for this member. * * Params: * cm = Compression method. * * Returns: Compression method. * * See_Also: * $(LREF CompressionMethod) **/ @property @safe @nogc pure nothrow CompressionMethod compressionMethod() const { return _compressionMethod; } /// ditto @property @safe pure void compressionMethod(CompressionMethod cm) { if (cm == _compressionMethod) return; enforce!ZipException(_compressedSize == 0, "Can't change compression method for a compressed element"); _compressionMethod = cm; } /** * The index of this archive member within the archive. Set this to a * different value for reordering the members of an archive. * * Params: * value = Index value to set. * * Returns: The index. */ @property uint index(uint value) @safe pure nothrow @nogc { return _index = value; } @property uint index() const @safe pure nothrow @nogc { return _index; } /// ditto 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); } } } @safe pure unittest { import std.exception : assertThrown, assertNotThrown; auto am = new ArchiveMember(); assertNotThrown(am.compressionMethod(CompressionMethod.deflate)); assertNotThrown(am.compressionMethod(CompressionMethod.none)); am._compressedData = [0x65]; // not strictly necessary, but for consistency am._compressedSize = 1; assertThrown!ZipException(am.compressionMethod(CompressionMethod.deflate)); } /** * 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; private: // names are taken directly from the specification // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT static immutable ubyte[] centralFileHeaderSignature = [ 0x50, 0x4b, 0x01, 0x02 ]; static immutable ubyte[] localFileHeaderSignature = [ 0x50, 0x4b, 0x03, 0x04 ]; static immutable ubyte[] endOfCentralDirSignature = [ 0x50, 0x4b, 0x05, 0x06 ]; static immutable ubyte[] archiveExtraDataSignature = [ 0x50, 0x4b, 0x06, 0x08 ]; static immutable ubyte[] digitalSignatureSignature = [ 0x50, 0x4b, 0x05, 0x05 ]; static immutable ubyte[] zip64EndOfCentralDirSignature = [ 0x50, 0x4b, 0x06, 0x06 ]; static immutable ubyte[] zip64EndOfCentralDirLocatorSignature = [ 0x50, 0x4b, 0x06, 0x07 ]; enum centralFileHeaderLength = 46; enum localFileHeaderLength = 30; enum endOfCentralDirLength = 22; enum archiveExtraDataLength = 8; enum digitalSignatureLength = 6; enum zip64EndOfCentralDirLength = 56; enum zip64EndOfCentralDirLocatorLength = 20; enum dataDescriptorLength = 12; public: string comment; /// The archive comment. Must be less than 65536 bytes in length. private ubyte[] _data; private bool _isZip64; static const ushort zip64ExtractVersion = 45; private Segment[] _segs; /** * Array representing the entire contents of the archive. * * Returns: Data of the entire contents of the archive. */ @property @safe @nogc pure nothrow ubyte[] data() { return _data; } /** * Number of ArchiveMembers in the directory. * * Returns: The number of files in this archive. */ @property @safe @nogc pure nothrow uint totalEntries() const { return cast(uint) _directory.length; } /** * True when the archive is in Zip64 format. Set this to true to force building a Zip64 archive. * * Params: * value = True, when the archive is forced to be build in Zip64 format. * * Returns: True, when the archive is in Zip64 format. */ @property @safe @nogc pure nothrow bool isZip64() const { return _isZip64; } /// ditto @property @safe @nogc pure nothrow void isZip64(bool value) { _isZip64 = value; } /** * Associative 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); * } * -------------------- * * Returns: Associative array with all archive members. */ @property @safe @nogc pure nothrow 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 @nogc pure nothrow { } /** * Add a member to the archive. The file is compressed on the fly. * * Params: * de = Member to be added. * * Throws: ZipException when an unsupported compression method is used or when * compression failed. */ @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, "Archive member compressed failed."); } @safe unittest { import std.exception : assertThrown; ArchiveMember am = new ArchiveMember(); am.compressionMethod = cast(CompressionMethod) 3; ZipArchive zip = new ZipArchive(); assertThrown!ZipException(zip.addMember(am)); } /** * Delete member `de` from the archive. Uses the name of the member * to detect which element to delete. * * Params: * de = Member to be deleted. */ @safe void deleteMember(ArchiveMember de) { _directory.remove(de.name); } // https://issues.dlang.org/show_bug.cgi?id=20398 @safe unittest { import std.string : representation; ArchiveMember file1 = new ArchiveMember(); file1.name = "test1.txt"; file1.expandedData("Test data.\n".dup.representation); ZipArchive zip = new ZipArchive(); zip.addMember(file1); assert(zip.totalEntries == 1); zip.deleteMember(file1); assert(zip.totalEntries == 0); } /** * Construct the entire contents of the current members of the archive. * * Fills in the properties data[], totalEntries, and directory[]. * For each ArchiveMember, fills in properties crc32, compressedSize, * compressedData[]. * * Returns: Array representing the entire archive. * * Throws: ZipException when the archive could not be build. */ void[] build() @safe pure { import std.array : array, uninitializedArray; import std.algorithm.sorting : sort; import std.string : representation; uint i; uint directoryOffset; enforce!ZipException(comment.length <= 0xFFFF, "archive comment longer than 65535"); // Compress each member; compute size uint archiveSize = 0; uint directorySize = 0; auto directory = _directory.byValue.array.sort!((x, y) => x.index < y.index).release; foreach (ArchiveMember de; directory) { enforce!ZipException(to!ulong(archiveSize) + localFileHeaderLength + de.name.length + de.extra.length + de.compressedSize + directorySize + centralFileHeaderLength + de.name.length + de.extra.length + de.comment.length + endOfCentralDirLength + comment.length + zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength <= uint.max, "zip files bigger than 4 GB are unsupported"); archiveSize += localFileHeaderLength + de.name.length + de.extra.length + de.compressedSize; directorySize += centralFileHeaderLength + de.name.length + de.extra.length + de.comment.length; } if (!isZip64 && _directory.length > ushort.max) _isZip64 = true; uint dataSize = archiveSize + directorySize + endOfCentralDirLength + cast(uint) comment.length; if (isZip64) dataSize += zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength; _data = uninitializedArray!(ubyte[])(dataSize); // Populate the data[] // Store each archive member i = 0; foreach (ArchiveMember de; directory) { de.offset = i; _data[i .. i + 4] = localFileHeaderSignature; 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 += localFileHeaderLength; _data[i .. i + de.name.length] = (de.name.representation)[]; 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; foreach (ArchiveMember de; directory) { _data[i .. i + 4] = centralFileHeaderSignature; 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, cast(ushort) 0); putUshort(i + 36, de.internalAttributes); putUint (i + 38, de._externalAttributes); putUint (i + 42, de.offset); i += centralFileHeaderLength; _data[i .. i + de.name.length] = (de.name.representation)[]; i += de.name.length; _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[]; i += de.extra.length; _data[i .. i + de.comment.length] = (de.comment.representation)[]; i += de.comment.length; } if (isZip64) { // Write zip64 end of central directory record uint eocd64Offset = i; _data[i .. i + 4] = zip64EndOfCentralDirSignature; putUlong (i + 4, zip64EndOfCentralDirLength - 12); putUshort(i + 12, zip64ExtractVersion); putUshort(i + 14, zip64ExtractVersion); putUint (i + 16, cast(ushort) 0); putUint (i + 20, cast(ushort) 0); putUlong (i + 24, directory.length); putUlong (i + 32, directory.length); putUlong (i + 40, directorySize); putUlong (i + 48, directoryOffset); i += zip64EndOfCentralDirLength; // Write zip64 end of central directory record locator _data[i .. i + 4] = zip64EndOfCentralDirLocatorSignature; putUint (i + 4, cast(ushort) 0); putUlong (i + 8, eocd64Offset); putUint (i + 16, 1); i += zip64EndOfCentralDirLocatorLength; } // Write end record _data[i .. i + 4] = endOfCentralDirSignature; putUshort(i + 4, cast(ushort) 0); putUshort(i + 6, cast(ushort) 0); putUshort(i + 8, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries)); 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 += endOfCentralDirLength; // Write archive comment assert(i + comment.length == data.length, "Writing the archive comment failed."); _data[i .. data.length] = (comment.representation)[]; return cast(void[]) data; } @safe pure unittest { import std.exception : assertNotThrown; ZipArchive zip = new ZipArchive(); zip.comment = "A"; assertNotThrown(zip.build()); } @safe pure unittest { import std.range : repeat, array; import std.exception : assertThrown; ZipArchive zip = new ZipArchive(); zip.comment = 'A'.repeat(70_000).array; assertThrown!ZipException(zip.build()); } /* ============ Reading an existing archive =================== */ /** * Constructor to use when reading an existing archive. * * Fills in the properties data[], totalEntries, comment[], and directory[]. * For each ArchiveMember, fills in * properties madeVersion, extractVersion, flags, compressionMethod, time, * crc32, compressedSize, expandedSize, compressedData[], * internalAttributes, externalAttributes, name[], extra[], comment[]. * Use expand() to get the expanded data for each ArchiveMember. * * Params: * buffer = The entire contents of the archive. * * Throws: ZipException when the archive was invalid or when malware was detected. */ this(void[] buffer) { this._data = cast(ubyte[]) buffer; enforce!ZipException(data.length <= uint.max - 2, "zip files bigger than 4 GB are unsupported"); _segs = [Segment(0, cast(uint) data.length)]; uint i = findEndOfCentralDirRecord(); int endCommentLength = getUshort(i + 20); comment = cast(string)(_data[i + endOfCentralDirLength .. i + endOfCentralDirLength + endCommentLength]); // end of central dir record removeSegment(i, i + endOfCentralDirLength + endCommentLength); uint k = i - zip64EndOfCentralDirLocatorLength; if (k < i && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature) { _isZip64 = true; i = k; // zip64 end of central dir record locator removeSegment(k, k + zip64EndOfCentralDirLocatorLength); } uint directorySize; uint directoryOffset; uint directoryCount; if (isZip64) { // Read Zip64 record data ulong eocdOffset = getUlong(i + 8); enforce!ZipException(eocdOffset + zip64EndOfCentralDirLength <= _data.length, "corrupted directory"); i = to!uint(eocdOffset); enforce!ZipException(_data[i .. i + 4] == zip64EndOfCentralDirSignature, "invalid Zip EOCD64 signature"); ulong eocd64Size = getUlong(i + 4); enforce!ZipException(eocd64Size + i - 12 <= data.length, "invalid Zip EOCD64 size"); // zip64 end of central dir record removeSegment(i, cast(uint) (i + 12 + eocd64Size)); ulong numEntriesUlong = getUlong(i + 24); ulong totalEntriesUlong = getUlong(i + 32); ulong directorySizeUlong = getUlong(i + 40); ulong directoryOffsetUlong = getUlong(i + 48); enforce!ZipException(numEntriesUlong <= uint.max, "supposedly more than 4294967296 files in archive"); enforce!ZipException(numEntriesUlong == totalEntriesUlong, "multiple disk zips not supported"); enforce!ZipException(directorySizeUlong <= i && directoryOffsetUlong <= i && directorySizeUlong + directoryOffsetUlong <= i, "corrupted directory"); directoryCount = to!uint(totalEntriesUlong); directorySize = to!uint(directorySizeUlong); directoryOffset = to!uint(directoryOffsetUlong); } else { // Read end record data directoryCount = getUshort(i + 10); directorySize = getUint(i + 12); directoryOffset = getUint(i + 16); } i = directoryOffset; for (int n = 0; n < directoryCount; n++) { /* The format of an entry is: * 'PK' 1, 2 * directory info * path * extra data * comment */ uint namelen; uint extralen; uint commentlen; enforce!ZipException(_data[i .. i + 4] == centralFileHeaderSignature, "wrong central file header signature found"); 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.internalAttributes = getUshort(i + 36); de._externalAttributes = getUint(i + 38); de.offset = getUint(i + 42); // central file header removeSegment(i, i + centralFileHeaderLength + namelen + extralen + commentlen); i += centralFileHeaderLength; enforce!ZipException(i + namelen + extralen + commentlen <= directoryOffset + directorySize, "invalid field lengths in file header found"); 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; auto localFileHeaderNamelen = getUshort(de.offset + 26); auto localFileHeaderExtralen = getUshort(de.offset + 28); // file data removeSegment(de.offset, de.offset + localFileHeaderLength + localFileHeaderNamelen + localFileHeaderExtralen + de._compressedSize); immutable uint dataOffset = de.offset + localFileHeaderLength + localFileHeaderNamelen + localFileHeaderExtralen; de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize]; _directory[de.name] = de; } enforce!ZipException(i == directoryOffset + directorySize, "invalid directory entry 3"); } @system unittest { import std.exception : assertThrown; // contains wrong directorySize (extra byte 0xff) auto file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4b\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // wrong eocdOffset auto file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // wrong signature of zip64 end of central directory auto file = "\x50\x4b\x06\x07\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // wrong size of zip64 end of central directory auto file = "\x50\x4b\x06\x06\xff\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // too many entries in zip64 end of central directory auto file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // zip64: numEntries and totalEntries differ auto file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // zip64: directorySize too large auto file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); // zip64: directoryOffset too large file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\xff\xff\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); // zip64: directorySize + directoryOffset too large // we need to add a useless byte at the beginning to avoid that one of the other two checks allready fires file = "\x00\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~ "\x01\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // wrong central file header signature auto file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x03\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { import std.exception : assertThrown; // invalid field lengths in file header auto file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } private uint findEndOfCentralDirRecord() { // end of central dir record can be followed by a comment of up to 2^^16-1 bytes // therefore we have to scan 2^^16 positions uint endrecOffset = to!uint(data.length); foreach (i; 0 .. 2 ^^ 16) { if (endOfCentralDirLength + i > data.length) break; uint start = to!uint(data.length) - endOfCentralDirLength - i; if (data[start .. start + 4] != endOfCentralDirSignature) continue; auto numberOfThisDisc = getUshort(start + 4); if (numberOfThisDisc != 0) continue; // no support for multiple volumes yet auto numberOfStartOfCentralDirectory = getUshort(start + 6); if (numberOfStartOfCentralDirectory != 0) continue; // dito if (numberOfThisDisc < numberOfStartOfCentralDirectory) continue; uint k = start - zip64EndOfCentralDirLocatorLength; auto maybeZip64 = k < start && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature; auto totalNumberOfEntriesOnThisDisk = getUshort(start + 8); auto totalNumberOfEntriesInCentralDir = getUshort(start + 10); if (totalNumberOfEntriesOnThisDisk > totalNumberOfEntriesInCentralDir && (!maybeZip64 || totalNumberOfEntriesOnThisDisk < 0xffff)) continue; auto sizeOfCentralDirectory = getUint(start + 12); if (sizeOfCentralDirectory > start && (!maybeZip64 || sizeOfCentralDirectory < 0xffff)) continue; auto offsetOfCentralDirectory = getUint(start + 16); if (offsetOfCentralDirectory > start - sizeOfCentralDirectory && (!maybeZip64 || offsetOfCentralDirectory < 0xffff)) continue; auto zipfileCommentLength = getUshort(start + 20); if (start + zipfileCommentLength + endOfCentralDirLength != data.length) continue; enforce!ZipException(endrecOffset == to!uint(data.length), "found more than one valid 'end of central dir record'"); endrecOffset = start; } enforce!ZipException(endrecOffset != to!uint(data.length), "found no valid 'end of central dir record'"); return endrecOffset; } /** * Decompress the contents of a member. * * Fills in properties extractVersion, flags, compressionMethod, time, * crc32, compressedSize, expandedSize, expandedData[], name[], extra[]. * * Params: * de = Member to be decompressed. * * Returns: The expanded data. * * Throws: ZipException when the entry is invalid or the compression method is not supported. */ ubyte[] expand(ArchiveMember de) { import std.string : representation; uint namelen; uint extralen; enforce!ZipException(_data[de.offset .. de.offset + 4] == localFileHeaderSignature, "wrong local file header signature found"); // 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); } enforce!ZipException((de.flags & 1) == 0, "encryption not supported"); 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"); } } @system unittest { import std.exception : assertThrown; // check for correct local file header signature auto file = "\x50\x4b\x04\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; auto za = new ZipArchive(cast(void[]) file); assertThrown!ZipException(za.expand(za._directory["file"])); } @system unittest { import std.exception : assertThrown; // check for encryption flag auto file = "\x50\x4b\x03\x04\x0a\x00\x01\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; auto za = new ZipArchive(cast(void[]) file); assertThrown!ZipException(za.expand(za._directory["file"])); } @system unittest { import std.exception : assertThrown; // check for invalid compression method auto file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x03\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; auto za = new ZipArchive(cast(void[]) file); assertThrown!ZipException(za.expand(za._directory["file"])); } /* ============ Utility =================== */ @safe @nogc pure nothrow ushort getUshort(uint i) { ubyte[2] result = data[i .. i + 2]; return littleEndianToNative!ushort(result); } @safe @nogc pure nothrow uint getUint(uint i) { ubyte[4] result = data[i .. i + 4]; return littleEndianToNative!uint(result); } @safe @nogc pure nothrow ulong getUlong(uint i) { ubyte[8] result = data[i .. i + 8]; return littleEndianToNative!ulong(result); } @safe @nogc pure nothrow void putUshort(uint i, ushort us) { data[i .. i + 2] = nativeToLittleEndian(us); } @safe @nogc pure nothrow void putUint(uint i, uint ui) { data[i .. i + 4] = nativeToLittleEndian(ui); } @safe @nogc pure nothrow void putUlong(uint i, ulong ul) { data[i .. i + 8] = nativeToLittleEndian(ul); } /* ============== for detecting overlaps =============== */ private: // defines a segment of the zip file, including start, excluding end struct Segment { uint start; uint end; } // removes Segment start .. end from _segs // throws zipException if start .. end is not completely available in _segs; void removeSegment(uint start, uint end) pure @safe in (start < end, "segment invalid") { auto found = false; size_t pos; foreach (i,seg;_segs) if (seg.start <= start && seg.end >= end && (!found || seg.start > _segs[pos].start)) { found = true; pos = i; } enforce!ZipException(found, "overlapping data detected"); if (start>_segs[pos].start) _segs ~= Segment(_segs[pos].start, start); if (end<_segs[pos].end) _segs ~= Segment(end, _segs[pos].end); _segs = _segs[0 .. pos] ~ _segs[pos + 1 .. $]; } pure @safe unittest { with (new ZipArchive()) { _segs = [Segment(0,100)]; removeSegment(10,20); assert(_segs == [Segment(0,10),Segment(20,100)]); _segs = [Segment(0,100)]; removeSegment(0,20); assert(_segs == [Segment(20,100)]); _segs = [Segment(0,100)]; removeSegment(10,100); assert(_segs == [Segment(0,10)]); _segs = [Segment(0,100), Segment(200,300), Segment(400,500)]; removeSegment(220,230); assert(_segs == [Segment(0,100),Segment(400,500),Segment(200,220),Segment(230,300)]); _segs = [Segment(200,300), Segment(0,100), Segment(400,500)]; removeSegment(20,30); assert(_segs == [Segment(200,300),Segment(400,500),Segment(0,20),Segment(30,100)]); import std.exception : assertThrown; _segs = [Segment(0,100), Segment(200,300), Segment(400,500)]; assertThrown(removeSegment(120,230)); _segs = [Segment(0,100), Segment(200,300), Segment(400,500)]; removeSegment(0,100); assertThrown(removeSegment(0,100)); _segs = [Segment(0,100)]; removeSegment(0,100); assertThrown(removeSegment(0,100)); } } } 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); } @system unittest { // invalid format of end of central directory entry import std.exception : assertThrown; assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06aaaaaaaaaaaaaaaaaaaa")); } @system unittest { // minimum (empty) archive should pass auto za = new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); assert(za.directory.length == 0); // one byte too short or too long should not pass import std.exception : assertThrown; assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")); assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")); } @system unittest { // https://issues.dlang.org/show_bug.cgi?id=20239 // chameleon file, containing two valid end of central directory entries auto file = "\x50\x4B\x03\x04\x0A\x00\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00"~ "\x00\x00\x01\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75"~ "\x61\x67\x65\x55\x54\x09\x00\x03\x82\xF2\x8A\x5D\x82\xF2\x8A\x5D\x75\x78\x0B\x00"~ "\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x44\x50\x4B\x01\x02\x1E\x03\x0A\x00"~ "\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00\x00\x00\x01\x00\x00\x00"~ "\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\x00\x00\x00\x00\x62\x65"~ "\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x82\xF2\x8A\x5D"~ "\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B\x05\x06\x00"~ "\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\x48\x00\x00\x00\xB7\x00\x50\x4B\x03"~ "\x04\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~ "\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65"~ "\x55\x54\x09\x00\x03\x97\xF2\x8A\x5D\x8C\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB"~ "\x03\x00\x00\x04\xEB\x03\x00\x00\x46\x4F\x52\x54\x52\x41\x4E\x50\x4B\x01\x02\x1E"~ "\x03\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~ "\x00\x00\x00\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\xB1\x00\x00"~ "\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x97"~ "\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B"~ "\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\xFF\x00\x00\x00\x00\x00"; import std.exception : assertThrown; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { // https://issues.dlang.org/show_bug.cgi?id=20287 // check for correct compressed data auto file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; auto za = new ZipArchive(cast(void[]) file); assert(za.directory["file"].compressedData == [104, 101, 108, 108, 111]); } // https://issues.dlang.org/show_bug.cgi?id=20027 @system unittest { // central file header overlaps end of central directory auto file = // lfh "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; import std.exception : assertThrown; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); // local file header and file data overlap second local file header and file data file = "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~ "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1e\x00\x66\x69"~ "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~ "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~ "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~ "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~ "\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~ "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~ "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~ "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~ "\x00\x00\x00"; assertThrown!ZipException(new ZipArchive(cast(void[]) file)); } @system unittest { // https://issues.dlang.org/show_bug.cgi?id=20295 // zip64 with 0xff bytes in end of central dir record do not work // minimum (empty zip64) archive should pass auto file = "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~ "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~ "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~ "\x00\x00"; auto za = new ZipArchive(cast(void[]) file); assert(za.directory.length == 0); } version (HasUnzip) @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); }