| // Written in the D programming language. | |
| /** | |
| * Read/write data in the $(LINK2 http://www.info-zip.org, _zip archive) format. | |
| * Makes use of the etc.c.zlib compression library. | |
| * | |
| * Bugs: | |
| * $(UL | |
| * $(LI Multi-disk zips not supported.) | |
| * $(LI Only Zip version 20 formats are supported.) | |
| * $(LI Only supports compression modes 0 (no compression) and 8 (deflate).) | |
| * $(LI Does not support encryption.) | |
| * $(LI $(BUGZILLA 592)) | |
| * $(LI $(BUGZILLA 2137)) | |
| * ) | |
| * | |
| * Example: | |
| * --- | |
| // Read existing zip file. | |
| import std.digest.crc, std.file, std.stdio, std.zip; | |
| void main(string[] args) | |
| { | |
| // read a zip file into memory | |
| auto zip = new ZipArchive(read(args[1])); | |
| writeln("Archive: ", args[1]); | |
| writefln("%-10s %-8s Name", "Length", "CRC-32"); | |
| // iterate over all zip members | |
| foreach (name, am; zip.directory) | |
| { | |
| // print some data about each member | |
| writefln("%10s %08x %s", am.expandedSize, am.crc32, name); | |
| assert(am.expandedData.length == 0); | |
| // decompress the archive member | |
| zip.expand(am); | |
| assert(am.expandedData.length == am.expandedSize); | |
| } | |
| } | |
| // Create and write new zip file. | |
| import std.file : write; | |
| import std.string : representation; | |
| void main() | |
| { | |
| char[] data = "Test data.\n".dup; | |
| // Create an ArchiveMember for the test file. | |
| ArchiveMember am = new ArchiveMember(); | |
| am.name = "test.txt"; | |
| am.expandedData(data.representation); | |
| // Create an archive and add the member. | |
| ZipArchive zip = new ZipArchive(); | |
| zip.addMember(am); | |
| // Build the archive | |
| void[] compressed_data = zip.build(); | |
| // Write to a file | |
| write("test.zip", compressed_data); | |
| } | |
| * --- | |
| * | |
| * Copyright: Copyright Digital Mars 2000 - 2009. | |
| * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). | |
| * Authors: $(HTTP digitalmars.com, Walter Bright) | |
| * Source: $(PHOBOSSRC std/_zip.d) | |
| */ | |
| /* Copyright Digital Mars 2000 - 2009. | |
| * Distributed under the Boost Software License, Version 1.0. | |
| * (See accompanying file LICENSE_1_0.txt or copy at | |
| * http://www.boost.org/LICENSE_1_0.txt) | |
| */ | |
| module std.zip; | |
| //debug=print; | |
| /** Thrown on error. | |
| */ | |
| class ZipException : Exception | |
| { | |
| this(string msg) @safe | |
| { | |
| super("ZipException: " ~ msg); | |
| } | |
| } | |
| /** | |
| * Compression method used by ArchiveMember | |
| */ | |
| enum CompressionMethod : ushort | |
| { | |
| none = 0, /// No compression, just archiving | |
| deflate = 8 /// Deflate algorithm. Use zlib library to compress | |
| } | |
| /** | |
| * A member of the ZipArchive. | |
| */ | |
| final class ArchiveMember | |
| { | |
| import std.conv : to, octal; | |
| import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime; | |
| /** | |
| * Read/Write: Usually the file name of the archive member; it is used to | |
| * index the archive directory for the member. Each member must have a unique | |
| * name[]. Do not change without removing member from the directory first. | |
| */ | |
| string name; | |
| ubyte[] extra; /// Read/Write: extra data for this member. | |
| string comment; /// Read/Write: comment associated with this member. | |
| private ubyte[] _compressedData; | |
| private ubyte[] _expandedData; | |
| private uint offset; | |
| private uint _crc32; | |
| private uint _compressedSize; | |
| private uint _expandedSize; | |
| private CompressionMethod _compressionMethod; | |
| private ushort _madeVersion = 20; | |
| private ushort _extractVersion = 20; | |
| private ushort _diskNumber; | |
| private uint _externalAttributes; | |
| private DosFileTime _time; | |
| // by default, no explicit order goes after explicit order | |
| private uint _index = uint.max; | |
| ushort flags; /// Read/Write: normally set to 0 | |
| ushort internalAttributes; /// Read/Write | |
| @property ushort extractVersion() { return _extractVersion; } /// Read Only | |
| @property uint crc32() { return _crc32; } /// Read Only: cyclic redundancy check (CRC) value | |
| /// Read Only: size of data of member in compressed form. | |
| @property uint compressedSize() { return _compressedSize; } | |
| /// Read Only: size of data of member in expanded form. | |
| @property uint expandedSize() { return _expandedSize; } | |
| @property ushort diskNumber() { return _diskNumber; } /// Read Only: should be 0. | |
| /// Read Only: data of member in compressed form. | |
| @property ubyte[] compressedData() { return _compressedData; } | |
| /// Read data of member in uncompressed form. | |
| @property ubyte[] expandedData() { return _expandedData; } | |
| /// Write data of member in uncompressed form. | |
| @property @safe void expandedData(ubyte[] ed) | |
| { | |
| _expandedData = ed; | |
| _expandedSize = to!uint(_expandedData.length); | |
| // Clean old compressed data, if any | |
| _compressedData.length = 0; | |
| _compressedSize = 0; | |
| } | |
| /** | |
| * Set the OS specific file attributes, as obtained by | |
| * $(REF getAttributes, std,file) or $(REF DirEntry.attributes, std,file), for this archive member. | |
| */ | |
| @property @safe void fileAttributes(uint attr) | |
| { | |
| version (Posix) | |
| { | |
| _externalAttributes = (attr & 0xFFFF) << 16; | |
| _madeVersion &= 0x00FF; | |
| _madeVersion |= 0x0300; // attributes are in UNIX format | |
| } | |
| else version (Windows) | |
| { | |
| _externalAttributes = attr; | |
| _madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format | |
| } | |
| else | |
| { | |
| static assert(0, "Unimplemented platform"); | |
| } | |
| } | |
| version (Posix) @safe unittest | |
| { | |
| auto am = new ArchiveMember(); | |
| am.fileAttributes = octal!100644; | |
| assert(am._externalAttributes == octal!100644 << 16); | |
| assert((am._madeVersion & 0xFF00) == 0x0300); | |
| } | |
| /** | |
| * Get the OS specific file attributes for the archive member. | |
| * | |
| * Returns: The file attributes or 0 if the file attributes were | |
| * encoded for an incompatible OS (Windows vs. Posix). | |
| * | |
| */ | |
| @property uint fileAttributes() const | |
| { | |
| version (Posix) | |
| { | |
| if ((_madeVersion & 0xFF00) == 0x0300) | |
| return _externalAttributes >> 16; | |
| return 0; | |
| } | |
| else version (Windows) | |
| { | |
| if ((_madeVersion & 0xFF00) == 0x0000) | |
| return _externalAttributes; | |
| return 0; | |
| } | |
| else | |
| { | |
| static assert(0, "Unimplemented platform"); | |
| } | |
| } | |
| /// Set the last modification time for this member. | |
| @property void time(SysTime time) | |
| { | |
| _time = SysTimeToDosFileTime(time); | |
| } | |
| /// ditto | |
| @property void time(DosFileTime time) | |
| { | |
| _time = time; | |
| } | |
| /// Get the last modification time for this member. | |
| @property DosFileTime time() const | |
| { | |
| return _time; | |
| } | |
| /** | |
| * Read compression method used for this member | |
| * See_Also: | |
| * CompressionMethod | |
| **/ | |
| @property @safe CompressionMethod compressionMethod() { return _compressionMethod; } | |
| /** | |
| * Write compression method used for this member | |
| * See_Also: | |
| * CompressionMethod | |
| **/ | |
| @property void compressionMethod(CompressionMethod cm) | |
| { | |
| if (cm == _compressionMethod) return; | |
| if (_compressedSize > 0) | |
| throw new ZipException("Can't change compression method for a compressed element"); | |
| _compressionMethod = cm; | |
| } | |
| /** | |
| * The index of this archive member within the archive. | |
| */ | |
| @property uint index() const pure nothrow @nogc { return _index; } | |
| @property uint index(uint value) pure nothrow @nogc { return _index = value; } | |
| debug(print) | |
| { | |
| void print() | |
| { | |
| printf("name = '%.*s'\n", name.length, name.ptr); | |
| printf("\tcomment = '%.*s'\n", comment.length, comment.ptr); | |
| printf("\tmadeVersion = x%04x\n", _madeVersion); | |
| printf("\textractVersion = x%04x\n", extractVersion); | |
| printf("\tflags = x%04x\n", flags); | |
| printf("\tcompressionMethod = %d\n", compressionMethod); | |
| printf("\ttime = %d\n", time); | |
| printf("\tcrc32 = x%08x\n", crc32); | |
| printf("\texpandedSize = %d\n", expandedSize); | |
| printf("\tcompressedSize = %d\n", compressedSize); | |
| printf("\tinternalAttributes = x%04x\n", internalAttributes); | |
| printf("\texternalAttributes = x%08x\n", externalAttributes); | |
| printf("\tindex = x%08x\n", index); | |
| } | |
| } | |
| } | |
| /** | |
| * Object representing the entire archive. | |
| * ZipArchives are collections of ArchiveMembers. | |
| */ | |
| final class ZipArchive | |
| { | |
| import std.algorithm.comparison : max; | |
| import std.bitmanip : littleEndianToNative, nativeToLittleEndian; | |
| import std.conv : to; | |
| import std.datetime.systime : DosFileTime; | |
| string comment; /// Read/Write: the archive comment. Must be less than 65536 bytes in length. | |
| private ubyte[] _data; | |
| private uint endrecOffset; | |
| private uint _diskNumber; | |
| private uint _diskStartDir; | |
| private uint _numEntries; | |
| private uint _totalEntries; | |
| private bool _isZip64; | |
| static const ushort zip64ExtractVersion = 45; | |
| static const int digiSignLength = 6; | |
| static const int eocd64LocLength = 20; | |
| static const int eocd64Length = 56; | |
| /// Read Only: array representing the entire contents of the archive. | |
| @property @safe ubyte[] data() { return _data; } | |
| /// Read Only: 0 since multi-disk zip archives are not supported. | |
| @property @safe uint diskNumber() { return _diskNumber; } | |
| /// Read Only: 0 since multi-disk zip archives are not supported | |
| @property @safe uint diskStartDir() { return _diskStartDir; } | |
| /// Read Only: number of ArchiveMembers in the directory. | |
| @property @safe uint numEntries() { return _numEntries; } | |
| @property @safe uint totalEntries() { return _totalEntries; } /// ditto | |
| /// True when the archive is in Zip64 format. | |
| @property @safe bool isZip64() { return _isZip64; } | |
| /// Set this to true to force building a Zip64 archive. | |
| @property @safe void isZip64(bool value) { _isZip64 = value; } | |
| /** | |
| * Read Only: array indexed by the name of each member of the archive. | |
| * All the members of the archive can be accessed with a foreach loop: | |
| * Example: | |
| * -------------------- | |
| * ZipArchive archive = new ZipArchive(data); | |
| * foreach (ArchiveMember am; archive.directory) | |
| * { | |
| * writefln("member name is '%s'", am.name); | |
| * } | |
| * -------------------- | |
| */ | |
| @property @safe ArchiveMember[string] directory() { return _directory; } | |
| private ArchiveMember[string] _directory; | |
| debug (print) | |
| { | |
| @safe void print() | |
| { | |
| printf("\tdiskNumber = %u\n", diskNumber); | |
| printf("\tdiskStartDir = %u\n", diskStartDir); | |
| printf("\tnumEntries = %u\n", numEntries); | |
| printf("\ttotalEntries = %u\n", totalEntries); | |
| printf("\tcomment = '%.*s'\n", comment.length, comment.ptr); | |
| } | |
| } | |
| /* ============ Creating a new archive =================== */ | |
| /** Constructor to use when creating a new archive. | |
| */ | |
| this() @safe | |
| { | |
| } | |
| /** Add de to the archive. The file is compressed on the fly. | |
| */ | |
| @safe void addMember(ArchiveMember de) | |
| { | |
| _directory[de.name] = de; | |
| if (!de._compressedData.length) | |
| { | |
| switch (de.compressionMethod) | |
| { | |
| case CompressionMethod.none: | |
| de._compressedData = de._expandedData; | |
| break; | |
| case CompressionMethod.deflate: | |
| import std.zlib : compress; | |
| () @trusted | |
| { | |
| de._compressedData = cast(ubyte[]) compress(cast(void[]) de._expandedData); | |
| }(); | |
| de._compressedData = de._compressedData[2 .. de._compressedData.length - 4]; | |
| break; | |
| default: | |
| throw new ZipException("unsupported compression method"); | |
| } | |
| de._compressedSize = to!uint(de._compressedData.length); | |
| import std.zlib : crc32; | |
| () @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }(); | |
| } | |
| assert(de._compressedData.length == de._compressedSize); | |
| } | |
| /** Delete de from the archive. | |
| */ | |
| @safe void deleteMember(ArchiveMember de) | |
| { | |
| _directory.remove(de.name); | |
| } | |
| /** | |
| * Construct an archive out of the current members of the archive. | |
| * | |
| * Fills in the properties data[], diskNumber, diskStartDir, numEntries, | |
| * totalEntries, and directory[]. | |
| * For each ArchiveMember, fills in properties crc32, compressedSize, | |
| * compressedData[]. | |
| * | |
| * Returns: array representing the entire archive. | |
| */ | |
| void[] build() | |
| { | |
| import std.algorithm.sorting : sort; | |
| uint i; | |
| uint directoryOffset; | |
| if (comment.length > 0xFFFF) | |
| throw new ZipException("archive comment longer than 65535"); | |
| // Compress each member; compute size | |
| uint archiveSize = 0; | |
| uint directorySize = 0; | |
| auto directory = _directory.values().sort!((x, y) => x.index < y.index).release; | |
| foreach (ArchiveMember de; directory) | |
| { | |
| if (to!ulong(archiveSize) + 30 + de.name.length + de.extra.length + de.compressedSize | |
| + directorySize + 46 + de.name.length + de.extra.length + de.comment.length | |
| + 22 + comment.length + eocd64LocLength + eocd64Length > uint.max) | |
| throw new ZipException("zip files bigger than 4 GB are unsupported"); | |
| archiveSize += 30 + de.name.length + | |
| de.extra.length + | |
| de.compressedSize; | |
| directorySize += 46 + de.name.length + | |
| de.extra.length + | |
| de.comment.length; | |
| } | |
| if (!isZip64 && _directory.length > ushort.max) | |
| _isZip64 = true; | |
| uint dataSize = archiveSize + directorySize + 22 + cast(uint) comment.length; | |
| if (isZip64) | |
| dataSize += eocd64LocLength + eocd64Length; | |
| _data = new ubyte[dataSize]; | |
| // Populate the data[] | |
| // Store each archive member | |
| i = 0; | |
| foreach (ArchiveMember de; directory) | |
| { | |
| de.offset = i; | |
| _data[i .. i + 4] = cast(ubyte[])"PK\x03\x04"; | |
| putUshort(i + 4, de.extractVersion); | |
| putUshort(i + 6, de.flags); | |
| putUshort(i + 8, de._compressionMethod); | |
| putUint (i + 10, cast(uint) de.time); | |
| putUint (i + 14, de.crc32); | |
| putUint (i + 18, de.compressedSize); | |
| putUint (i + 22, to!uint(de.expandedSize)); | |
| putUshort(i + 26, cast(ushort) de.name.length); | |
| putUshort(i + 28, cast(ushort) de.extra.length); | |
| i += 30; | |
| _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[]; | |
| i += de.name.length; | |
| _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[]; | |
| i += de.extra.length; | |
| _data[i .. i + de.compressedSize] = de.compressedData[]; | |
| i += de.compressedSize; | |
| } | |
| // Write directory | |
| directoryOffset = i; | |
| _numEntries = 0; | |
| foreach (ArchiveMember de; directory) | |
| { | |
| _data[i .. i + 4] = cast(ubyte[])"PK\x01\x02"; | |
| putUshort(i + 4, de._madeVersion); | |
| putUshort(i + 6, de.extractVersion); | |
| putUshort(i + 8, de.flags); | |
| putUshort(i + 10, de._compressionMethod); | |
| putUint (i + 12, cast(uint) de.time); | |
| putUint (i + 16, de.crc32); | |
| putUint (i + 20, de.compressedSize); | |
| putUint (i + 24, de.expandedSize); | |
| putUshort(i + 28, cast(ushort) de.name.length); | |
| putUshort(i + 30, cast(ushort) de.extra.length); | |
| putUshort(i + 32, cast(ushort) de.comment.length); | |
| putUshort(i + 34, de.diskNumber); | |
| putUshort(i + 36, de.internalAttributes); | |
| putUint (i + 38, de._externalAttributes); | |
| putUint (i + 42, de.offset); | |
| i += 46; | |
| _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[]; | |
| i += de.name.length; | |
| _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[]; | |
| i += de.extra.length; | |
| _data[i .. i + de.comment.length] = (cast(ubyte[]) de.comment)[]; | |
| i += de.comment.length; | |
| _numEntries++; | |
| } | |
| _totalEntries = numEntries; | |
| if (isZip64) | |
| { | |
| // Write zip64 end of central directory record | |
| uint eocd64Offset = i; | |
| _data[i .. i + 4] = cast(ubyte[])"PK\x06\x06"; | |
| putUlong (i + 4, eocd64Length - 12); | |
| putUshort(i + 12, zip64ExtractVersion); | |
| putUshort(i + 14, zip64ExtractVersion); | |
| putUint (i + 16, diskNumber); | |
| putUint (i + 20, diskStartDir); | |
| putUlong (i + 24, numEntries); | |
| putUlong (i + 32, totalEntries); | |
| putUlong (i + 40, directorySize); | |
| putUlong (i + 48, directoryOffset); | |
| i += eocd64Length; | |
| // Write zip64 end of central directory record locator | |
| _data[i .. i + 4] = cast(ubyte[])"PK\x06\x07"; | |
| putUint (i + 4, diskNumber); | |
| putUlong (i + 8, eocd64Offset); | |
| putUint (i + 16, 1); | |
| i += eocd64LocLength; | |
| } | |
| // Write end record | |
| endrecOffset = i; | |
| _data[i .. i + 4] = cast(ubyte[])"PK\x05\x06"; | |
| putUshort(i + 4, cast(ushort) diskNumber); | |
| putUshort(i + 6, cast(ushort) diskStartDir); | |
| putUshort(i + 8, (numEntries > ushort.max ? ushort.max : cast(ushort) numEntries)); | |
| putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries)); | |
| putUint (i + 12, directorySize); | |
| putUint (i + 16, directoryOffset); | |
| putUshort(i + 20, cast(ushort) comment.length); | |
| i += 22; | |
| // Write archive comment | |
| assert(i + comment.length == data.length); | |
| _data[i .. data.length] = (cast(ubyte[]) comment)[]; | |
| return cast(void[]) data; | |
| } | |
| /* ============ Reading an existing archive =================== */ | |
| /** | |
| * Constructor to use when reading an existing archive. | |
| * | |
| * Fills in the properties data[], diskNumber, diskStartDir, numEntries, | |
| * totalEntries, comment[], and directory[]. | |
| * For each ArchiveMember, fills in | |
| * properties madeVersion, extractVersion, flags, compressionMethod, time, | |
| * crc32, compressedSize, expandedSize, compressedData[], diskNumber, | |
| * internalAttributes, externalAttributes, name[], extra[], comment[]. | |
| * Use expand() to get the expanded data for each ArchiveMember. | |
| * | |
| * Params: | |
| * buffer = the entire contents of the archive. | |
| */ | |
| this(void[] buffer) | |
| { uint iend; | |
| uint i; | |
| int endcommentlength; | |
| uint directorySize; | |
| uint directoryOffset; | |
| this._data = cast(ubyte[]) buffer; | |
| if (data.length > uint.max - 2) | |
| throw new ZipException("zip files bigger than 4 GB are unsupported"); | |
| // Find 'end record index' by searching backwards for signature | |
| iend = (data.length > 66_000 ? to!uint(data.length - 66_000) : 0); | |
| for (i = to!uint(data.length) - 22; 1; i--) | |
| { | |
| if (i < iend || i >= data.length) | |
| throw new ZipException("no end record"); | |
| if (_data[i .. i + 4] == cast(ubyte[])"PK\x05\x06") | |
| { | |
| endcommentlength = getUshort(i + 20); | |
| if (i + 22 + endcommentlength > data.length | |
| || i + 22 + endcommentlength < i) | |
| continue; | |
| comment = cast(string)(_data[i + 22 .. i + 22 + endcommentlength]); | |
| endrecOffset = i; | |
| uint k = i - eocd64LocLength; | |
| if (k < i && _data[k .. k + 4] == cast(ubyte[])"PK\x06\x07") | |
| { | |
| _isZip64 = true; | |
| i = k; | |
| } | |
| break; | |
| } | |
| } | |
| if (isZip64) | |
| { | |
| // Read Zip64 record data | |
| ulong eocdOffset = getUlong(i + 8); | |
| if (eocdOffset + eocd64Length > _data.length) | |
| throw new ZipException("corrupted directory"); | |
| i = to!uint(eocdOffset); | |
| if (_data[i .. i + 4] != cast(ubyte[])"PK\x06\x06") | |
| throw new ZipException("invalid Zip EOCD64 signature"); | |
| ulong eocd64Size = getUlong(i + 4); | |
| if (eocd64Size + i - 12 > data.length) | |
| throw new ZipException("invalid Zip EOCD64 size"); | |
| _diskNumber = getUint(i + 16); | |
| _diskStartDir = getUint(i + 20); | |
| ulong numEntriesUlong = getUlong(i + 24); | |
| ulong totalEntriesUlong = getUlong(i + 32); | |
| ulong directorySizeUlong = getUlong(i + 40); | |
| ulong directoryOffsetUlong = getUlong(i + 48); | |
| if (numEntriesUlong > uint.max) | |
| throw new ZipException("supposedly more than 4294967296 files in archive"); | |
| if (numEntriesUlong != totalEntriesUlong) | |
| throw new ZipException("multiple disk zips not supported"); | |
| if (directorySizeUlong > i || directoryOffsetUlong > i | |
| || directorySizeUlong + directoryOffsetUlong > i) | |
| throw new ZipException("corrupted directory"); | |
| _numEntries = to!uint(numEntriesUlong); | |
| _totalEntries = to!uint(totalEntriesUlong); | |
| directorySize = to!uint(directorySizeUlong); | |
| directoryOffset = to!uint(directoryOffsetUlong); | |
| } | |
| else | |
| { | |
| // Read end record data | |
| _diskNumber = getUshort(i + 4); | |
| _diskStartDir = getUshort(i + 6); | |
| _numEntries = getUshort(i + 8); | |
| _totalEntries = getUshort(i + 10); | |
| if (numEntries != totalEntries) | |
| throw new ZipException("multiple disk zips not supported"); | |
| directorySize = getUint(i + 12); | |
| directoryOffset = getUint(i + 16); | |
| if (directoryOffset + directorySize > i) | |
| throw new ZipException("corrupted directory"); | |
| } | |
| i = directoryOffset; | |
| for (int n = 0; n < numEntries; n++) | |
| { | |
| /* The format of an entry is: | |
| * 'PK' 1, 2 | |
| * directory info | |
| * path | |
| * extra data | |
| * comment | |
| */ | |
| uint namelen; | |
| uint extralen; | |
| uint commentlen; | |
| if (_data[i .. i + 4] != cast(ubyte[])"PK\x01\x02") | |
| throw new ZipException("invalid directory entry 1"); | |
| ArchiveMember de = new ArchiveMember(); | |
| de._index = n; | |
| de._madeVersion = getUshort(i + 4); | |
| de._extractVersion = getUshort(i + 6); | |
| de.flags = getUshort(i + 8); | |
| de._compressionMethod = cast(CompressionMethod) getUshort(i + 10); | |
| de.time = cast(DosFileTime) getUint(i + 12); | |
| de._crc32 = getUint(i + 16); | |
| de._compressedSize = getUint(i + 20); | |
| de._expandedSize = getUint(i + 24); | |
| namelen = getUshort(i + 28); | |
| extralen = getUshort(i + 30); | |
| commentlen = getUshort(i + 32); | |
| de._diskNumber = getUshort(i + 34); | |
| de.internalAttributes = getUshort(i + 36); | |
| de._externalAttributes = getUint(i + 38); | |
| de.offset = getUint(i + 42); | |
| i += 46; | |
| if (i + namelen + extralen + commentlen > directoryOffset + directorySize) | |
| throw new ZipException("invalid directory entry 2"); | |
| de.name = cast(string)(_data[i .. i + namelen]); | |
| i += namelen; | |
| de.extra = _data[i .. i + extralen]; | |
| i += extralen; | |
| de.comment = cast(string)(_data[i .. i + commentlen]); | |
| i += commentlen; | |
| immutable uint dataOffset = de.offset + 30 + namelen + extralen; | |
| if (dataOffset + de.compressedSize > endrecOffset) | |
| throw new ZipException("Invalid directory entry offset or size."); | |
| de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize]; | |
| _directory[de.name] = de; | |
| } | |
| if (i != directoryOffset + directorySize) | |
| throw new ZipException("invalid directory entry 3"); | |
| } | |
| /***** | |
| * Decompress the contents of archive member de and return the expanded | |
| * data. | |
| * | |
| * Fills in properties extractVersion, flags, compressionMethod, time, | |
| * crc32, compressedSize, expandedSize, expandedData[], name[], extra[]. | |
| */ | |
| ubyte[] expand(ArchiveMember de) | |
| { uint namelen; | |
| uint extralen; | |
| if (_data[de.offset .. de.offset + 4] != cast(ubyte[])"PK\x03\x04") | |
| throw new ZipException("invalid directory entry 4"); | |
| // These values should match what is in the main zip archive directory | |
| de._extractVersion = getUshort(de.offset + 4); | |
| de.flags = getUshort(de.offset + 6); | |
| de._compressionMethod = cast(CompressionMethod) getUshort(de.offset + 8); | |
| de.time = cast(DosFileTime) getUint(de.offset + 10); | |
| de._crc32 = getUint(de.offset + 14); | |
| de._compressedSize = max(getUint(de.offset + 18), de.compressedSize); | |
| de._expandedSize = max(getUint(de.offset + 22), de.expandedSize); | |
| namelen = getUshort(de.offset + 26); | |
| extralen = getUshort(de.offset + 28); | |
| debug(print) | |
| { | |
| printf("\t\texpandedSize = %d\n", de.expandedSize); | |
| printf("\t\tcompressedSize = %d\n", de.compressedSize); | |
| printf("\t\tnamelen = %d\n", namelen); | |
| printf("\t\textralen = %d\n", extralen); | |
| } | |
| if (de.flags & 1) | |
| throw new ZipException("encryption not supported"); | |
| int i; | |
| i = de.offset + 30 + namelen + extralen; | |
| if (i + de.compressedSize > endrecOffset) | |
| throw new ZipException("invalid directory entry 5"); | |
| de._compressedData = _data[i .. i + de.compressedSize]; | |
| debug(print) arrayPrint(de.compressedData); | |
| switch (de.compressionMethod) | |
| { | |
| case CompressionMethod.none: | |
| de._expandedData = de.compressedData; | |
| return de.expandedData; | |
| case CompressionMethod.deflate: | |
| // -15 is a magic value used to decompress zip files. | |
| // It has the effect of not requiring the 2 byte header | |
| // and 4 byte trailer. | |
| import std.zlib : uncompress; | |
| de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15); | |
| return de.expandedData; | |
| default: | |
| throw new ZipException("unsupported compression method"); | |
| } | |
| } | |
| /* ============ Utility =================== */ | |
| @safe ushort getUshort(int i) | |
| { | |
| ubyte[2] result = data[i .. i + 2]; | |
| return littleEndianToNative!ushort(result); | |
| } | |
| @safe uint getUint(int i) | |
| { | |
| ubyte[4] result = data[i .. i + 4]; | |
| return littleEndianToNative!uint(result); | |
| } | |
| @safe ulong getUlong(int i) | |
| { | |
| ubyte[8] result = data[i .. i + 8]; | |
| return littleEndianToNative!ulong(result); | |
| } | |
| @safe void putUshort(int i, ushort us) | |
| { | |
| data[i .. i + 2] = nativeToLittleEndian(us); | |
| } | |
| @safe void putUint(int i, uint ui) | |
| { | |
| data[i .. i + 4] = nativeToLittleEndian(ui); | |
| } | |
| @safe void putUlong(int i, ulong ul) | |
| { | |
| data[i .. i + 8] = nativeToLittleEndian(ul); | |
| } | |
| } | |
| debug(print) | |
| { | |
| @safe void arrayPrint(ubyte[] array) | |
| { | |
| printf("array %p,%d\n", cast(void*) array, array.length); | |
| for (int i = 0; i < array.length; i++) | |
| { | |
| printf("%02x ", array[i]); | |
| if (((i + 1) & 15) == 0) | |
| printf("\n"); | |
| } | |
| printf("\n"); | |
| } | |
| } | |
| @system unittest | |
| { | |
| // @system due to (at least) ZipArchive.build | |
| auto zip1 = new ZipArchive(); | |
| auto zip2 = new ZipArchive(); | |
| auto am1 = new ArchiveMember(); | |
| am1.name = "foo"; | |
| am1.expandedData = new ubyte[](1024); | |
| zip1.addMember(am1); | |
| auto data1 = zip1.build(); | |
| zip2.addMember(zip1.directory["foo"]); | |
| zip2.build(); | |
| auto am2 = zip2.directory["foo"]; | |
| zip2.expand(am2); | |
| assert(am1.expandedData == am2.expandedData); | |
| auto zip3 = new ZipArchive(data1); | |
| zip3.build(); | |
| assert(zip3.directory["foo"].compressedSize == am1.compressedSize); | |
| // Test if packing and unpacking produces the original data | |
| import std.conv, std.stdio; | |
| import std.random : uniform, MinstdRand0; | |
| MinstdRand0 gen; | |
| const uint itemCount = 20, minSize = 10, maxSize = 500; | |
| foreach (variant; 0 .. 2) | |
| { | |
| bool useZip64 = !!variant; | |
| zip1 = new ZipArchive(); | |
| zip1.isZip64 = useZip64; | |
| ArchiveMember[itemCount] ams; | |
| foreach (i; 0 .. itemCount) | |
| { | |
| ams[i] = new ArchiveMember(); | |
| ams[i].name = to!string(i); | |
| ams[i].expandedData = new ubyte[](uniform(minSize, maxSize)); | |
| foreach (ref ubyte c; ams[i].expandedData) | |
| c = cast(ubyte)(uniform(0, 256)); | |
| ams[i].compressionMethod = CompressionMethod.deflate; | |
| zip1.addMember(ams[i]); | |
| } | |
| auto zippedData = zip1.build(); | |
| zip2 = new ZipArchive(zippedData); | |
| assert(zip2.isZip64 == useZip64); | |
| foreach (am; ams) | |
| { | |
| am2 = zip2.directory[am.name]; | |
| zip2.expand(am2); | |
| assert(am.crc32 == am2.crc32); | |
| assert(am.expandedData == am2.expandedData); | |
| } | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.conv : to; | |
| import std.random : Mt19937, randomShuffle; | |
| // Test if packing and unpacking preserves order. | |
| auto rand = Mt19937(15966); | |
| string[] names; | |
| int value = 0; | |
| // Generate a series of unique numbers as filenames. | |
| foreach (i; 0 .. 20) | |
| { | |
| value += 1 + rand.front & 0xFFFF; | |
| rand.popFront; | |
| names ~= value.to!string; | |
| } | |
| // Insert them in a random order. | |
| names.randomShuffle(rand); | |
| auto zip1 = new ZipArchive(); | |
| foreach (i, name; names) | |
| { | |
| auto member = new ArchiveMember(); | |
| member.name = name; | |
| member.expandedData = cast(ubyte[]) name; | |
| member.index = cast(int) i; | |
| zip1.addMember(member); | |
| } | |
| auto data = zip1.build(); | |
| // Ensure that they appear in the same order. | |
| auto zip2 = new ZipArchive(data); | |
| foreach (i, name; names) | |
| { | |
| const member = zip2.directory[name]; | |
| assert(member.index == i, "member " ~ name ~ " had index " ~ | |
| member.index.to!string ~ " but we expected index " ~ i.to!string ~ | |
| ". The input array was " ~ names.to!string); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.zlib; | |
| ubyte[] src = cast(ubyte[]) | |
| "the quick brown fox jumps over the lazy dog\r | |
| the quick brown fox jumps over the lazy dog\r | |
| "; | |
| auto dst = cast(ubyte[]) compress(cast(void[]) src); | |
| auto after = cast(ubyte[]) uncompress(cast(void[]) dst); | |
| assert(src == after); | |
| } | |
| @system unittest | |
| { | |
| // @system due to ZipArchive.build | |
| import std.datetime; | |
| ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9]; | |
| auto ar = new ZipArchive; | |
| auto am = new ArchiveMember; // 10 | |
| am.name = "buf"; | |
| am.expandedData = buf; | |
| am.compressionMethod = CompressionMethod.deflate; | |
| am.time = SysTimeToDosFileTime(Clock.currTime()); | |
| ar.addMember(am); // 15 | |
| auto zip1 = ar.build(); | |
| auto arAfter = new ZipArchive(zip1); | |
| assert(arAfter.directory.length == 1); | |
| auto amAfter = arAfter.directory["buf"]; | |
| arAfter.expand(amAfter); | |
| assert(amAfter.name == am.name); | |
| assert(amAfter.expandedData == am.expandedData); | |
| assert(amAfter.time == am.time); | |
| } | |
| // Non-Android Posix-only, because we can't rely on the unzip command being | |
| // available on Android or Windows | |
| version (Android) {} else | |
| version (Posix) @system unittest | |
| { | |
| import std.datetime, std.file, std.format, std.path, std.process, std.stdio; | |
| auto zr = new ZipArchive(); | |
| auto am = new ArchiveMember(); | |
| am.compressionMethod = CompressionMethod.deflate; | |
| am.name = "foo.bar"; | |
| am.time = SysTimeToDosFileTime(Clock.currTime()); | |
| am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine"; | |
| zr.addMember(am); | |
| auto data2 = zr.build(); | |
| mkdirRecurse(deleteme); | |
| scope(exit) rmdirRecurse(deleteme); | |
| string zipFile = buildPath(deleteme, "foo.zip"); | |
| std.file.write(zipFile, cast(byte[]) data2); | |
| auto result = executeShell(format("unzip -l %s", zipFile)); | |
| scope(failure) writeln(result.output); | |
| assert(result.status == 0); | |
| } |