| // 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); |
| } |