aboutsummaryrefslogtreecommitdiff
path: root/libphobos/src/std/zip.d
diff options
context:
space:
mode:
Diffstat (limited to 'libphobos/src/std/zip.d')
-rw-r--r--libphobos/src/std/zip.d1303
1 files changed, 1025 insertions, 278 deletions
diff --git a/libphobos/src/std/zip.d b/libphobos/src/std/zip.d
index 9e55d19..4d7422b 100644
--- a/libphobos/src/std/zip.d
+++ b/libphobos/src/std/zip.d
@@ -1,114 +1,169 @@
// 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;
+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]));
- writeln("Archive: ", args[1]);
- writefln("%-10s %-8s Name", "Length", "CRC-32");
+
// 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);
}
}
+---
-// Create and write new zip file.
+Example for writing files into a zip archive:
+---
import std.file : write;
import std.string : representation;
+import std.zip;
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 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();
- zip.addMember(am);
+
+ // 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 Digital Mars 2000 - 2009.
+---
+
+ * 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)
+ * Source: $(PHOBOSSRC std/zip.d)
*/
-/* Copyright Digital Mars 2000 - 2009.
+/* 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.
- */
+/// Thrown on error.
class ZipException : Exception
{
- this(string msg) @safe
- {
- super("ZipException: " ~ msg);
- }
+ import std.exception : basicExceptionCtors;
+ ///
+ mixin basicExceptionCtors;
}
-/**
- * Compression method used by ArchiveMember
- */
+/// Compression method used by `ArchiveMember`.
enum CompressionMethod : ushort
{
- none = 0, /// No compression, just archiving
- deflate = 8 /// Deflate algorithm. Use zlib library to compress
+ none = 0, /// No compression, just archiving.
+ deflate = 8 /// Deflate algorithm. Use zlib library to compress.
}
-/**
- * A member of the ZipArchive.
- */
+/// A single file or directory inside the archive.
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.
+ * 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;
- ubyte[] extra; /// Read/Write: extra data for this member.
- string comment; /// Read/Write: comment associated with this member.
+ /**
+ * 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;
@@ -119,32 +174,80 @@ final class ArchiveMember
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
+ /**
+ * 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; }
- @property ushort extractVersion() { return _extractVersion; } /// Read Only
- @property uint crc32() { return _crc32; } /// Read Only: cyclic redundancy check (CRC) value
+ /**
+ * 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; }
- /// Read Only: size of data of member in compressed form.
- @property uint compressedSize() { 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; }
- /// 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.
+ /**
+ * Should be 0.
+ *
+ * Returns: The number of the disk where this member can be found.
+ */
+ deprecated("Multidisk not supported; will be removed in 2.099.0")
+ @property @safe pure nothrow @nogc ushort diskNumber() const { return 0; }
- /// Read Only: data of member in compressed form.
- @property ubyte[] compressedData() { return _compressedData; }
+ /**
+ * Data of member in compressed form.
+ *
+ * Returns: The file data in compressed form.
+ */
+ @property @safe pure nothrow @nogc ubyte[] compressedData() { return _compressedData; }
- /// Read data of member in uncompressed form.
- @property ubyte[] expandedData() { return _expandedData; }
+ /**
+ * 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; }
- /// Write data of member in uncompressed form.
+ /// ditto
@property @safe void expandedData(ubyte[] ed)
{
_expandedData = ed;
@@ -156,8 +259,14 @@ final class ArchiveMember
}
/**
- * Set the OS specific file attributes, as obtained by
- * $(REF getAttributes, std,file) or $(REF DirEntry.attributes, std,file), for this archive member.
+ * 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)
{
@@ -186,14 +295,8 @@ final class ArchiveMember
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
+ /// ditto
+ @property @nogc nothrow uint fileAttributes() const
{
version (Posix)
{
@@ -213,51 +316,66 @@ final class ArchiveMember
}
}
- /// Set the last modification time for this member.
- @property void time(SysTime time)
+ /**
+ * 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
{
- _time = SysTimeToDosFileTime(time);
+ return _time;
}
/// ditto
- @property void time(DosFileTime time)
+ @property void time(SysTime time)
{
- _time = time;
+ _time = SysTimeToDosFileTime(time);
}
- /// Get the last modification time for this member.
- @property DosFileTime time() const
+ /// ditto
+ @property void time(DosFileTime time) @safe pure nothrow @nogc
{
- return _time;
+ _time = time;
}
/**
- * Read compression method used for this member
+ * Get or set compression method used for this member.
+ *
+ * Params:
+ * cm = Compression method.
+ *
+ * Returns: Compression method.
+ *
* See_Also:
- * CompressionMethod
+ * $(LREF CompressionMethod)
**/
- @property @safe CompressionMethod compressionMethod() { return _compressionMethod; }
+ @property @safe @nogc pure nothrow CompressionMethod compressionMethod() const { return _compressionMethod; }
- /**
- * Write compression method used for this member
- * See_Also:
- * CompressionMethod
- **/
- @property void compressionMethod(CompressionMethod cm)
+ /// ditto
+ @property @safe pure void compressionMethod(CompressionMethod cm)
{
if (cm == _compressionMethod) return;
- if (_compressedSize > 0)
- throw new ZipException("Can't change compression method for a compressed element");
+ enforce!ZipException(_compressedSize == 0, "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; }
+ * 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)
{
@@ -280,6 +398,21 @@ final class ArchiveMember
}
}
+@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.
@@ -291,42 +424,93 @@ final class ZipArchive
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:
+ // 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 uint endrecOffset;
- private uint _diskNumber;
- private uint _diskStartDir;
- private uint _numEntries;
- private uint _totalEntries;
private bool _isZip64;
static const ushort zip64ExtractVersion = 45;
+
+ deprecated("Use digitalSignatureLength instead; will be removed in 2.098.0")
static const int digiSignLength = 6;
+ deprecated("Use zip64EndOfCentralDirLocatorLength instead; will be removed in 2.098.0")
static const int eocd64LocLength = 20;
+ deprecated("Use zip64EndOfCentralDirLength instead; will be removed in 2.098.0")
static const int eocd64Length = 56;
- /// Read Only: array representing the entire contents of the archive.
- @property @safe ubyte[] data() { return _data; }
+ private Segment[] _segs;
- /// Read Only: 0 since multi-disk zip archives are not supported.
- @property @safe uint diskNumber() { return _diskNumber; }
+ /**
+ * 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; }
- /// Read Only: 0 since multi-disk zip archives are not supported
- @property @safe uint diskStartDir() { return _diskStartDir; }
+ /**
+ * 0 since multi-disk zip archives are not supported.
+ *
+ * Returns: Number of this disk.
+ */
+ deprecated("Multidisk not supported; will be removed in 2.099.0")
+ @property @safe @nogc pure nothrow uint diskNumber() const { return 0; }
- /// Read Only: number of ArchiveMembers in the directory.
- @property @safe uint numEntries() { return _numEntries; }
- @property @safe uint totalEntries() { return _totalEntries; } /// ditto
+ /**
+ * 0 since multi-disk zip archives are not supported.
+ *
+ * Returns: Number of the disk, where the central directory starts.
+ */
+ deprecated("Multidisk not supported; will be removed in 2.099.0")
+ @property @safe @nogc pure nothrow uint diskStartDir() const { return 0; }
- /// True when the archive is in Zip64 format.
- @property @safe bool isZip64() { return _isZip64; }
+ /**
+ * Number of ArchiveMembers in the directory.
+ *
+ * Returns: The number of files in this archive.
+ */
+ deprecated("Use totalEntries instead; will be removed in 2.099.0")
+ @property @safe @nogc pure nothrow uint numEntries() const { return cast(uint) _directory.length; }
+ @property @safe @nogc pure nothrow uint totalEntries() const { return cast(uint) _directory.length; } /// ditto
- /// 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:
+ * 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);
@@ -335,8 +519,10 @@ final class ZipArchive
* writefln("member name is '%s'", am.name);
* }
* --------------------
+ *
+ * Returns: Associative array with all archive members.
*/
- @property @safe ArchiveMember[string] directory() { return _directory; }
+ @property @safe @nogc pure nothrow ArchiveMember[string] directory() { return _directory; }
private ArchiveMember[string] _directory;
@@ -354,13 +540,21 @@ final class ZipArchive
/* ============ Creating a new archive =================== */
- /** Constructor to use when creating a new archive.
+ /**
+ * Constructor to use when creating a new archive.
*/
- this() @safe
+ this() @safe @nogc pure nothrow
{
}
- /** Add de to the archive. The file is compressed on the fly.
+ /**
+ * 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)
{
@@ -390,61 +584,101 @@ final class ZipArchive
import std.zlib : crc32;
() @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }();
}
- assert(de._compressedData.length == de._compressedSize);
+ 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 de from the archive.
+ /**
+ * 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 an archive out of the current members of the archive.
+ * Construct the entire contents of the current members of the archive.
*
- * Fills in the properties data[], diskNumber, diskStartDir, numEntries,
- * totalEntries, and directory[].
+ * Fills in the properties data[], totalEntries, and directory[].
* For each ArchiveMember, fills in properties crc32, compressedSize,
* compressedData[].
*
- * Returns: array representing the entire archive.
+ * Returns: Array representing the entire archive.
+ *
+ * Throws: ZipException when the archive could not be build.
*/
- void[] build()
+ void[] build() @safe pure
{
+ import std.array : array, uninitializedArray;
import std.algorithm.sorting : sort;
+ import std.string : representation;
+
uint i;
uint directoryOffset;
- if (comment.length > 0xFFFF)
- throw new ZipException("archive comment longer than 65535");
+ enforce!ZipException(comment.length <= 0xFFFF, "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;
+ auto directory = _directory.byValue.array.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 +
+ 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 += 46 + de.name.length +
+ directorySize += centralFileHeaderLength + 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;
+ uint dataSize = archiveSize + directorySize + endOfCentralDirLength + cast(uint) comment.length;
if (isZip64)
- dataSize += eocd64LocLength + eocd64Length;
+ dataSize += zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength;
- _data = new ubyte[dataSize];
+ _data = uninitializedArray!(ubyte[])(dataSize);
// Populate the data[]
@@ -453,7 +687,7 @@ final class ZipArchive
foreach (ArchiveMember de; directory)
{
de.offset = i;
- _data[i .. i + 4] = cast(ubyte[])"PK\x03\x04";
+ _data[i .. i + 4] = localFileHeaderSignature;
putUshort(i + 4, de.extractVersion);
putUshort(i + 6, de.flags);
putUshort(i + 8, de._compressionMethod);
@@ -463,9 +697,9 @@ final class ZipArchive
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;
+ i += localFileHeaderLength;
- _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[];
+ _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;
@@ -475,10 +709,9 @@ final class ZipArchive
// Write directory
directoryOffset = i;
- _numEntries = 0;
foreach (ArchiveMember de; directory)
{
- _data[i .. i + 4] = cast(ubyte[])"PK\x01\x02";
+ _data[i .. i + 4] = centralFileHeaderSignature;
putUshort(i + 4, de._madeVersion);
putUshort(i + 6, de.extractVersion);
putUshort(i + 8, de.flags);
@@ -490,180 +723,175 @@ final class ZipArchive
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 + 34, cast(ushort) 0);
putUshort(i + 36, de.internalAttributes);
putUint (i + 38, de._externalAttributes);
putUint (i + 42, de.offset);
- i += 46;
+ i += centralFileHeaderLength;
- _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[];
+ _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] = (cast(ubyte[]) de.comment)[];
+ _data[i .. i + de.comment.length] = (de.comment.representation)[];
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);
+ _data[i .. i + 4] = zip64EndOfCentralDirSignature;
+ putUlong (i + 4, zip64EndOfCentralDirLength - 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);
+ 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 += eocd64Length;
+ i += zip64EndOfCentralDirLength;
// Write zip64 end of central directory record locator
- _data[i .. i + 4] = cast(ubyte[])"PK\x06\x07";
- putUint (i + 4, diskNumber);
+ _data[i .. i + 4] = zip64EndOfCentralDirLocatorSignature;
+ putUint (i + 4, cast(ushort) 0);
putUlong (i + 8, eocd64Offset);
putUint (i + 16, 1);
- i += eocd64LocLength;
+ i += zip64EndOfCentralDirLocatorLength;
}
// 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));
+ _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 += 22;
+ i += endOfCentralDirLength;
// Write archive comment
- assert(i + comment.length == data.length);
- _data[i .. data.length] = (cast(ubyte[]) 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[], diskNumber, diskStartDir, numEntries,
- * totalEntries, comment[], and directory[].
+ * Fills in the properties data[], totalEntries, comment[], and directory[].
* For each ArchiveMember, fills in
* properties madeVersion, extractVersion, flags, compressionMethod, time,
- * crc32, compressedSize, expandedSize, compressedData[], diskNumber,
+ * 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.
+ * buffer = The entire contents of the archive.
+ *
+ * Throws: ZipException when the archive was invalid or when malware was detected.
*/
-
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");
+ 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();
- // 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--)
+ 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)
{
- if (i < iend || i >= data.length)
- throw new ZipException("no end record");
+ _isZip64 = true;
+ i = k;
- 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;
- }
+ // 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);
- if (eocdOffset + eocd64Length > _data.length)
- throw new ZipException("corrupted directory");
+ enforce!ZipException(eocdOffset + zip64EndOfCentralDirLength <= _data.length,
+ "corrupted directory");
i = to!uint(eocdOffset);
- if (_data[i .. i + 4] != cast(ubyte[])"PK\x06\x06")
- throw new ZipException("invalid Zip EOCD64 signature");
+ enforce!ZipException(_data[i .. i + 4] == zip64EndOfCentralDirSignature,
+ "invalid Zip EOCD64 signature");
ulong eocd64Size = getUlong(i + 4);
- if (eocd64Size + i - 12 > data.length)
- throw new ZipException("invalid Zip EOCD64 size");
+ enforce!ZipException(eocd64Size + i - 12 <= data.length,
+ "invalid Zip EOCD64 size");
- _diskNumber = getUint(i + 16);
- _diskStartDir = getUint(i + 20);
+ // 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);
- if (numEntriesUlong > uint.max)
- throw new ZipException("supposedly more than 4294967296 files in archive");
+ enforce!ZipException(numEntriesUlong <= uint.max,
+ "supposedly more than 4294967296 files in archive");
- if (numEntriesUlong != totalEntriesUlong)
- throw new ZipException("multiple disk zips not supported");
+ enforce!ZipException(numEntriesUlong == totalEntriesUlong,
+ "multiple disk zips not supported");
- if (directorySizeUlong > i || directoryOffsetUlong > i
- || directorySizeUlong + directoryOffsetUlong > i)
- throw new ZipException("corrupted directory");
+ enforce!ZipException(directorySizeUlong <= i && directoryOffsetUlong <= i
+ && directorySizeUlong + directoryOffsetUlong <= i,
+ "corrupted directory");
- _numEntries = to!uint(numEntriesUlong);
- _totalEntries = to!uint(totalEntriesUlong);
+ directoryCount = 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");
+ // Read end record data
+ directoryCount = getUshort(i + 10);
+ directorySize = getUint(i + 12);
+ directoryOffset = getUint(i + 16);
}
i = directoryOffset;
- for (int n = 0; n < numEntries; n++)
+ for (int n = 0; n < directoryCount; n++)
{
/* The format of an entry is:
* 'PK' 1, 2
@@ -677,8 +905,8 @@ final class ZipArchive
uint extralen;
uint commentlen;
- if (_data[i .. i + 4] != cast(ubyte[])"PK\x01\x02")
- throw new ZipException("invalid directory entry 1");
+ 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);
@@ -692,14 +920,17 @@ final class ZipArchive
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");
+ // 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;
@@ -708,31 +939,288 @@ final class ZipArchive
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.");
+ 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;
}
- if (i != directoryOffset + directorySize)
- throw new ZipException("invalid directory entry 3");
+
+ enforce!ZipException(endrecOffset != to!uint(data.length),
+ "found no valid 'end of central dir record'");
+
+ return endrecOffset;
}
- /*****
- * Decompress the contents of archive member de and return the expanded
- * data.
+ /**
+ * 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)
- { uint namelen;
+ {
+ import std.string : representation;
+
+ uint namelen;
uint extralen;
- if (_data[de.offset .. de.offset + 4] != cast(ubyte[])"PK\x03\x04")
- throw new ZipException("invalid directory entry 4");
+ 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);
@@ -753,16 +1241,7 @@ final class ZipArchive
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);
+ enforce!ZipException((de.flags & 1) == 0, "encryption not supported");
switch (de.compressionMethod)
{
@@ -783,40 +1262,183 @@ final class ZipArchive
}
}
+ @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 ushort getUshort(int i)
+ @safe @nogc pure nothrow ushort getUshort(uint i)
{
ubyte[2] result = data[i .. i + 2];
return littleEndianToNative!ushort(result);
}
- @safe uint getUint(int i)
+ @safe @nogc pure nothrow uint getUint(uint i)
{
ubyte[4] result = data[i .. i + 4];
return littleEndianToNative!uint(result);
}
- @safe ulong getUlong(int i)
+ @safe @nogc pure nothrow ulong getUlong(uint i)
{
ubyte[8] result = data[i .. i + 8];
return littleEndianToNative!ulong(result);
}
- @safe void putUshort(int i, ushort us)
+ @safe @nogc pure nothrow void putUshort(uint i, ushort us)
{
data[i .. i + 2] = nativeToLittleEndian(us);
}
- @safe void putUint(int i, uint ui)
+ @safe @nogc pure nothrow void putUint(uint i, uint ui)
{
data[i .. i + 4] = nativeToLittleEndian(ui);
}
- @safe void putUlong(int i, ulong ul)
+ @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)
@@ -963,10 +1585,135 @@ the quick brown fox jumps over the lazy dog\r
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
+@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;