1 // Written in the D programming language.
2 
3 /**
4 Read and write data in the
5 $(LINK2 https://en.wikipedia.org/wiki/Zip_%28file_format%29, zip archive)
6 format.
7 
8 Standards:
9 
10 The current implementation mostly conforms to
11 $(LINK2 https://www.iso.org/standard/60101.html, ISO/IEC 21320-1:2015),
12 which means,
13 $(UL
14 $(LI that files can only be stored uncompressed or using the deflate mechanism,)
15 $(LI that encryption features are not used,)
16 $(LI that digital signature features are not used,)
17 $(LI that patched data features are not used, and)
18 $(LI that archives may not span multiple volumes.)
19 )
20 
21 Additionally, archives are checked for malware attacks and rejected if detected.
22 This includes
23 $(UL
24 $(LI $(LINK2 https://news.ycombinator.com/item?id=20352439, zip bombs) which
25      generate gigantic amounts of unpacked data)
26 $(LI zip archives that contain overlapping records)
27 $(LI chameleon zip archives which generate different unpacked data, depending
28      on the implementation of the unpack algorithm)
29 )
30 
31 The current implementation makes use of the zlib compression library.
32 
33 Usage:
34 
35 There are two main ways of usage: Extracting files from a zip archive
36 and storing files into a zip archive. These can be mixed though (e.g.
37 read an archive, remove some files, add others and write the new
38 archive).
39 
40 Examples:
41 
42 Example for reading an existing zip archive:
43 ---
44 import std.stdio : writeln, writefln;
45 import std.file : read;
46 import std.zip;
47 
48 void main(string[] args)
49 {
50     // read a zip file into memory
51     auto zip = new ZipArchive(read(args[1]));
52 
53     // iterate over all zip members
54     writefln("%-10s  %-8s  Name", "Length", "CRC-32");
55     foreach (name, am; zip.directory)
56     {
57         // print some data about each member
58         writefln("%10s  %08x  %s", am.expandedSize, am.crc32, name);
59         assert(am.expandedData.length == 0);
60 
61         // decompress the archive member
62         zip.expand(am);
63         assert(am.expandedData.length == am.expandedSize);
64     }
65 }
66 ---
67 
68 Example for writing files into a zip archive:
69 ---
70 import std.file : write;
71 import std.string : representation;
72 import std.zip;
73 
74 void main()
75 {
76     // Create an ArchiveMembers for each file.
77     ArchiveMember file1 = new ArchiveMember();
78     file1.name = "test1.txt";
79     file1.expandedData("Test data.\n".dup.representation);
80     file1.compressionMethod = CompressionMethod.none; // don't compress
81 
82     ArchiveMember file2 = new ArchiveMember();
83     file2.name = "test2.txt";
84     file2.expandedData("More test data.\n".dup.representation);
85     file2.compressionMethod = CompressionMethod.deflate; // compress
86 
87     // Create an archive and add the member.
88     ZipArchive zip = new ZipArchive();
89 
90     // add ArchiveMembers
91     zip.addMember(file1);
92     zip.addMember(file2);
93 
94     // Build the archive
95     void[] compressed_data = zip.build();
96 
97     // Write to a file
98     write("test.zip", compressed_data);
99 }
100 ---
101 
102  * Copyright: Copyright The D Language Foundation 2000 - 2009.
103  * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
104  * Authors:   $(HTTP digitalmars.com, Walter Bright)
105  * Source:    $(PHOBOSSRC std/zip.d)
106  */
107 
108 /*          Copyright The D Language Foundation 2000 - 2009.
109  * Distributed under the Boost Software License, Version 1.0.
110  *    (See accompanying file LICENSE_1_0.txt or copy at
111  *          http://www.boost.org/LICENSE_1_0.txt)
112  */
113 module std.zip;
114 
115 import std.exception : enforce;
116 
117 // Non-Android/Apple ARM POSIX-only, because we can't rely on the unzip
118 // command being available on Android, Apple ARM or Windows
119 version (Android) {}
120 else version (iOS) {}
121 else version (TVOS) {}
122 else version (WatchOS) {}
123 else version (Posix)
124     version = HasUnzip;
125 
126 //debug=print;
127 
128 /// Thrown on error.
129 class ZipException : Exception
130 {
131     import std.exception : basicExceptionCtors;
132     ///
133     mixin basicExceptionCtors;
134 }
135 
136 /// Compression method used by `ArchiveMember`.
137 enum CompressionMethod : ushort
138 {
139     none = 0,   /// No compression, just archiving.
140     deflate = 8 /// Deflate algorithm. Use zlib library to compress.
141 }
142 
143 /// A single file or directory inside the archive.
144 final class ArchiveMember
145 {
146     import std.conv : to, octal;
147     import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime;
148 
149     /**
150      * The name of the archive member; it is used to index the
151      * archive directory for the member. Each member must have a
152      * unique name. Do not change without removing member from the
153      * directory first.
154      */
155     string name;
156 
157     /**
158      * The content of the extra data field for this member. See
159      * $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT,
160      *         original documentation)
161      * for a description of the general format of this data. May contain
162      * undocumented 3rd-party data.
163      */
164     ubyte[] extra;
165 
166     string comment; /// Comment associated with this member.
167 
168     private ubyte[] _compressedData;
169     private ubyte[] _expandedData;
170     private uint offset;
171     private uint _crc32;
172     private uint _compressedSize;
173     private uint _expandedSize;
174     private CompressionMethod _compressionMethod;
175     private ushort _madeVersion = 20;
176     private ushort _extractVersion = 20;
177     private uint _externalAttributes;
178     private DosFileTime _time;
179     // by default, no explicit order goes after explicit order
180     private uint _index = uint.max;
181 
182     /**
183      * Contains some information on how to extract this archive. See
184      * $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT,
185      *         original documentation)
186      * for details.
187      */
188     ushort flags;
189 
190     /**
191      * Internal attributes. Bit 1 is set, if the member is apparently in binary format
192      * and bit 2 is set, if each record is preceded by the length of the record.
193      */
194     ushort internalAttributes;
195 
196     /**
197      * The zip file format version needed to extract this member.
198      *
199      * Returns: Format version needed to extract this member.
200      */
201     @property @safe pure nothrow @nogc ushort extractVersion() const { return _extractVersion; }
202 
203     /**
204      * Cyclic redundancy check (CRC) value.
205      *
206      * Returns: CRC32 value.
207      */
208     @property @safe pure nothrow @nogc uint crc32() const { return _crc32; }
209 
210     /**
211      * Size of data of member in compressed form.
212      *
213      * Returns: Size of the compressed archive.
214      */
215     @property @safe pure nothrow @nogc uint compressedSize() const { return _compressedSize; }
216 
217     /**
218      * Size of data of member in uncompressed form.
219      *
220      * Returns: Size of uncompressed archive.
221      */
222     @property @safe pure nothrow @nogc uint expandedSize() const { return _expandedSize; }
223 
224     /**
225      * Data of member in compressed form.
226      *
227      * Returns: The file data in compressed form.
228      */
229     @property @safe pure nothrow @nogc ubyte[] compressedData() { return _compressedData; }
230 
231     /**
232      * Get or set data of member in uncompressed form. When an existing archive is
233      * read `ZipArchive.expand` needs to be called before this can be accessed.
234      *
235      * Params:
236      *     ed = Expanded Data.
237      *
238      * Returns: The file data.
239      */
240     @property @safe pure nothrow @nogc ubyte[] expandedData() { return _expandedData; }
241 
242     /// ditto
243     @property @safe void expandedData(ubyte[] ed)
244     {
245         _expandedData = ed;
246         _expandedSize  = to!uint(_expandedData.length);
247 
248         // Clean old compressed data, if any
249         _compressedData.length = 0;
250         _compressedSize = 0;
251     }
252 
253     /**
254      * Get or set the OS specific file attributes for this archive member.
255      *
256      * Params:
257      *     attr = Attributes as obtained by $(REF getAttributes, std,file) or
258      *            $(REF DirEntry.attributes, std,file).
259      *
260      * Returns: The file attributes or 0 if the file attributes were
261      * encoded for an incompatible OS (Windows vs. POSIX).
262      */
263     @property @safe void fileAttributes(uint attr)
264     {
265         version (Posix)
266         {
267             _externalAttributes = (attr & 0xFFFF) << 16;
268             _madeVersion &= 0x00FF;
269             _madeVersion |= 0x0300; // attributes are in UNIX format
270         }
271         else version (Windows)
272         {
273             _externalAttributes = attr;
274             _madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format
275         }
276         else
277         {
278             static assert(0, "Unimplemented platform");
279         }
280     }
281 
282     version (Posix) @safe unittest
283     {
284         auto am = new ArchiveMember();
285         am.fileAttributes = octal!100644;
286         assert(am._externalAttributes == octal!100644 << 16);
287         assert((am._madeVersion & 0xFF00) == 0x0300);
288     }
289 
290     /// ditto
291     @property @nogc nothrow uint fileAttributes() const
292     {
293         version (Posix)
294         {
295             if ((_madeVersion & 0xFF00) == 0x0300)
296                 return _externalAttributes >> 16;
297             return 0;
298         }
299         else version (Windows)
300         {
301             if ((_madeVersion & 0xFF00) == 0x0000)
302                 return _externalAttributes;
303             return 0;
304         }
305         else
306         {
307             static assert(0, "Unimplemented platform");
308         }
309     }
310 
311     /**
312      * Get or set the last modification time for this member.
313      *
314      * Params:
315      *     time = Time to set (will be saved as DosFileTime, which is less accurate).
316      *
317      * Returns:
318      *     The last modification time in DosFileFormat.
319      */
320     @property DosFileTime time() const @safe pure nothrow @nogc
321     {
322         return _time;
323     }
324 
325     /// ditto
326     @property void time(SysTime time)
327     {
328         _time = SysTimeToDosFileTime(time);
329     }
330 
331     /// ditto
332     @property void time(DosFileTime time) @safe pure nothrow @nogc
333     {
334         _time = time;
335     }
336 
337     /**
338      * Get or set compression method used for this member.
339      *
340      * Params:
341      *     cm = Compression method.
342      *
343      * Returns: Compression method.
344      *
345      * See_Also:
346      *     $(LREF CompressionMethod)
347      **/
348     @property @safe @nogc pure nothrow CompressionMethod compressionMethod() const { return _compressionMethod; }
349 
350     /// ditto
351     @property @safe pure void compressionMethod(CompressionMethod cm)
352     {
353         if (cm == _compressionMethod) return;
354 
355         enforce!ZipException(_compressedSize == 0, "Can't change compression method for a compressed element");
356 
357         _compressionMethod = cm;
358     }
359 
360     /**
361      * The index of this archive member within the archive. Set this to a
362      * different value for reordering the members of an archive.
363      *
364      * Params:
365      *     value = Index value to set.
366      *
367      * Returns: The index.
368      */
369     @property uint index(uint value) @safe pure nothrow @nogc { return _index = value; }
370     @property uint index() const @safe pure nothrow @nogc { return _index; } /// ditto
371 
372     debug(print)
373     {
374     void print()
375     {
376         printf("name = '%.*s'\n", cast(int) name.length, name.ptr);
377         printf("\tcomment = '%.*s'\n", cast(int) comment.length, comment.ptr);
378         printf("\tmadeVersion = x%04x\n", _madeVersion);
379         printf("\textractVersion = x%04x\n", extractVersion);
380         printf("\tflags = x%04x\n", flags);
381         printf("\tcompressionMethod = %d\n", compressionMethod);
382         printf("\ttime = %d\n", time);
383         printf("\tcrc32 = x%08x\n", crc32);
384         printf("\texpandedSize = %d\n", expandedSize);
385         printf("\tcompressedSize = %d\n", compressedSize);
386         printf("\tinternalAttributes = x%04x\n", internalAttributes);
387         printf("\texternalAttributes = x%08x\n", externalAttributes);
388         printf("\tindex = x%08x\n", index);
389     }
390     }
391 }
392 
393 @safe pure unittest
394 {
395     import std.exception : assertThrown, assertNotThrown;
396 
397     auto am = new ArchiveMember();
398 
399     assertNotThrown(am.compressionMethod(CompressionMethod.deflate));
400     assertNotThrown(am.compressionMethod(CompressionMethod.none));
401 
402     am._compressedData = [0x65]; // not strictly necessary, but for consistency
403     am._compressedSize = 1;
404 
405     assertThrown!ZipException(am.compressionMethod(CompressionMethod.deflate));
406 }
407 
408 /**
409  * Object representing the entire archive.
410  * ZipArchives are collections of ArchiveMembers.
411  */
412 final class ZipArchive
413 {
414     import std.algorithm.comparison : max;
415     import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
416     import std.conv : to;
417     import std.datetime.systime : DosFileTime;
418 
419 private:
420     // names are taken directly from the specification
421     // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
422     static immutable ubyte[] centralFileHeaderSignature = [ 0x50, 0x4b, 0x01, 0x02 ];
423     static immutable ubyte[] localFileHeaderSignature = [ 0x50, 0x4b, 0x03, 0x04 ];
424     static immutable ubyte[] endOfCentralDirSignature = [ 0x50, 0x4b, 0x05, 0x06 ];
425     static immutable ubyte[] archiveExtraDataSignature = [ 0x50, 0x4b, 0x06, 0x08 ];
426     static immutable ubyte[] digitalSignatureSignature = [ 0x50, 0x4b, 0x05, 0x05 ];
427     static immutable ubyte[] zip64EndOfCentralDirSignature = [ 0x50, 0x4b, 0x06, 0x06 ];
428     static immutable ubyte[] zip64EndOfCentralDirLocatorSignature = [ 0x50, 0x4b, 0x06, 0x07 ];
429 
430     enum centralFileHeaderLength = 46;
431     enum localFileHeaderLength = 30;
432     enum endOfCentralDirLength = 22;
433     enum archiveExtraDataLength = 8;
434     enum digitalSignatureLength = 6;
435     enum zip64EndOfCentralDirLength = 56;
436     enum zip64EndOfCentralDirLocatorLength = 20;
437     enum dataDescriptorLength = 12;
438 
439 public:
440     string comment; /// The archive comment. Must be less than 65536 bytes in length.
441 
442     private ubyte[] _data;
443 
444     private bool _isZip64;
445     static const ushort zip64ExtractVersion = 45;
446 
447     private Segment[] _segs;
448 
449     /**
450      * Array representing the entire contents of the archive.
451      *
452      * Returns: Data of the entire contents of the archive.
453      */
454     @property @safe @nogc pure nothrow ubyte[] data() { return _data; }
455 
456     /**
457      * Number of ArchiveMembers in the directory.
458      *
459      * Returns: The number of files in this archive.
460      */
461     @property @safe @nogc pure nothrow uint totalEntries() const { return cast(uint) _directory.length; }
462 
463     /**
464      * True when the archive is in Zip64 format. Set this to true to force building a Zip64 archive.
465      *
466      * Params:
467      *     value = True, when the archive is forced to be build in Zip64 format.
468      *
469      * Returns: True, when the archive is in Zip64 format.
470      */
471     @property @safe @nogc pure nothrow bool isZip64() const { return _isZip64; }
472 
473     /// ditto
474     @property @safe @nogc pure nothrow void isZip64(bool value) { _isZip64 = value; }
475 
476     /**
477      * Associative array indexed by the name of each member of the archive.
478      *
479      * All the members of the archive can be accessed with a foreach loop:
480      *
481      * Example:
482      * --------------------
483      * ZipArchive archive = new ZipArchive(data);
484      * foreach (ArchiveMember am; archive.directory)
485      * {
486      *     writefln("member name is '%s'", am.name);
487      * }
488      * --------------------
489      *
490      * Returns: Associative array with all archive members.
491      */
492     @property @safe @nogc pure nothrow ArchiveMember[string] directory() { return _directory; }
493 
494     private ArchiveMember[string] _directory;
495 
496     debug (print)
497     {
498     @safe void print()
499     {
500         printf("\tdiskNumber = %u\n", diskNumber);
501         printf("\tdiskStartDir = %u\n", diskStartDir);
502         printf("\tnumEntries = %u\n", numEntries);
503         printf("\ttotalEntries = %u\n", totalEntries);
504         printf("\tcomment = '%.*s'\n", cast(int) comment.length, comment.ptr);
505     }
506     }
507 
508     /* ============ Creating a new archive =================== */
509 
510     /**
511      * Constructor to use when creating a new archive.
512      */
513     this() @safe @nogc pure nothrow
514     {
515     }
516 
517     /**
518      * Add a member to the archive. The file is compressed on the fly.
519      *
520      * Params:
521      *     de = Member to be added.
522      *
523      * Throws: ZipException when an unsupported compression method is used or when
524      *         compression failed.
525      */
526     @safe void addMember(ArchiveMember de)
527     {
528         _directory[de.name] = de;
529         if (!de._compressedData.length)
530         {
531             switch (de.compressionMethod)
532             {
533                 case CompressionMethod.none:
534                     de._compressedData = de._expandedData;
535                     break;
536 
537                 case CompressionMethod.deflate:
538                     import std.zlib : compress;
539                     () @trusted
540                     {
541                         de._compressedData = cast(ubyte[]) compress(cast(void[]) de._expandedData);
542                     }();
543                         de._compressedData = de._compressedData[2 .. de._compressedData.length - 4];
544                     break;
545 
546                 default:
547                     throw new ZipException("unsupported compression method");
548             }
549 
550             de._compressedSize = to!uint(de._compressedData.length);
551             import std.zlib : crc32;
552             () @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }();
553         }
554         assert(de._compressedData.length == de._compressedSize, "Archive member compressed failed.");
555     }
556 
557     @safe unittest
558     {
559         import std.exception : assertThrown;
560 
561         ArchiveMember am = new ArchiveMember();
562         am.compressionMethod = cast(CompressionMethod) 3;
563 
564         ZipArchive zip = new ZipArchive();
565 
566         assertThrown!ZipException(zip.addMember(am));
567     }
568 
569     /**
570      * Delete member `de` from the archive. Uses the name of the member
571      * to detect which element to delete.
572      *
573      * Params:
574      *     de = Member to be deleted.
575      */
576     @safe void deleteMember(ArchiveMember de)
577     {
578         _directory.remove(de.name);
579     }
580 
581     // https://issues.dlang.org/show_bug.cgi?id=20398
582     @safe unittest
583     {
584         import std.string : representation;
585 
586         ArchiveMember file1 = new ArchiveMember();
587         file1.name = "test1.txt";
588         file1.expandedData("Test data.\n".dup.representation);
589 
590         ZipArchive zip = new ZipArchive();
591 
592         zip.addMember(file1);
593         assert(zip.totalEntries == 1);
594 
595         zip.deleteMember(file1);
596         assert(zip.totalEntries == 0);
597     }
598 
599     /**
600      * Construct the entire contents of the current members of the archive.
601      *
602      * Fills in the properties data[], totalEntries, and directory[].
603      * For each ArchiveMember, fills in properties crc32, compressedSize,
604      * compressedData[].
605      *
606      * Returns: Array representing the entire archive.
607      *
608      * Throws: ZipException when the archive could not be build.
609      */
610     void[] build() @safe pure
611     {
612         import std.array : array, uninitializedArray;
613         import std.algorithm.sorting : sort;
614         import std.string : representation;
615 
616         uint i;
617         uint directoryOffset;
618 
619         enforce!ZipException(comment.length <= 0xFFFF, "archive comment longer than 65535");
620 
621         // Compress each member; compute size
622         uint archiveSize = 0;
623         uint directorySize = 0;
624         auto directory = _directory.byValue.array.sort!((x, y) => x.index < y.index).release;
625         foreach (ArchiveMember de; directory)
626         {
627             enforce!ZipException(to!ulong(archiveSize) + localFileHeaderLength + de.name.length
628                                  + de.extra.length + de.compressedSize + directorySize
629                                  + centralFileHeaderLength + de.name.length + de.extra.length
630                                  + de.comment.length + endOfCentralDirLength + comment.length
631                                  + zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength <= uint.max,
632                                  "zip files bigger than 4 GB are unsupported");
633 
634             archiveSize += localFileHeaderLength + de.name.length +
635                                 de.extra.length +
636                                 de.compressedSize;
637             directorySize += centralFileHeaderLength + de.name.length +
638                                 de.extra.length +
639                                 de.comment.length;
640         }
641 
642         if (!isZip64 && _directory.length > ushort.max)
643             _isZip64 = true;
644         uint dataSize = archiveSize + directorySize + endOfCentralDirLength + cast(uint) comment.length;
645         if (isZip64)
646             dataSize += zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength;
647 
648         _data = uninitializedArray!(ubyte[])(dataSize);
649 
650         // Populate the data[]
651 
652         // Store each archive member
653         i = 0;
654         foreach (ArchiveMember de; directory)
655         {
656             de.offset = i;
657             _data[i .. i + 4] = localFileHeaderSignature;
658             putUshort(i + 4,  de.extractVersion);
659             putUshort(i + 6,  de.flags);
660             putUshort(i + 8,  de._compressionMethod);
661             putUint  (i + 10, cast(uint) de.time);
662             putUint  (i + 14, de.crc32);
663             putUint  (i + 18, de.compressedSize);
664             putUint  (i + 22, to!uint(de.expandedSize));
665             putUshort(i + 26, cast(ushort) de.name.length);
666             putUshort(i + 28, cast(ushort) de.extra.length);
667             i += localFileHeaderLength;
668 
669             _data[i .. i + de.name.length] = (de.name.representation)[];
670             i += de.name.length;
671             _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
672             i += de.extra.length;
673             _data[i .. i + de.compressedSize] = de.compressedData[];
674             i += de.compressedSize;
675         }
676 
677         // Write directory
678         directoryOffset = i;
679         foreach (ArchiveMember de; directory)
680         {
681             _data[i .. i + 4] = centralFileHeaderSignature;
682             putUshort(i + 4,  de._madeVersion);
683             putUshort(i + 6,  de.extractVersion);
684             putUshort(i + 8,  de.flags);
685             putUshort(i + 10, de._compressionMethod);
686             putUint  (i + 12, cast(uint) de.time);
687             putUint  (i + 16, de.crc32);
688             putUint  (i + 20, de.compressedSize);
689             putUint  (i + 24, de.expandedSize);
690             putUshort(i + 28, cast(ushort) de.name.length);
691             putUshort(i + 30, cast(ushort) de.extra.length);
692             putUshort(i + 32, cast(ushort) de.comment.length);
693             putUshort(i + 34, cast(ushort) 0);
694             putUshort(i + 36, de.internalAttributes);
695             putUint  (i + 38, de._externalAttributes);
696             putUint  (i + 42, de.offset);
697             i += centralFileHeaderLength;
698 
699             _data[i .. i + de.name.length] = (de.name.representation)[];
700             i += de.name.length;
701             _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
702             i += de.extra.length;
703             _data[i .. i + de.comment.length] = (de.comment.representation)[];
704             i += de.comment.length;
705         }
706 
707         if (isZip64)
708         {
709             // Write zip64 end of central directory record
710             uint eocd64Offset = i;
711             _data[i .. i + 4] = zip64EndOfCentralDirSignature;
712             putUlong (i + 4,  zip64EndOfCentralDirLength - 12);
713             putUshort(i + 12, zip64ExtractVersion);
714             putUshort(i + 14, zip64ExtractVersion);
715             putUint  (i + 16, cast(ushort) 0);
716             putUint  (i + 20, cast(ushort) 0);
717             putUlong (i + 24, directory.length);
718             putUlong (i + 32, directory.length);
719             putUlong (i + 40, directorySize);
720             putUlong (i + 48, directoryOffset);
721             i += zip64EndOfCentralDirLength;
722 
723             // Write zip64 end of central directory record locator
724             _data[i .. i + 4] = zip64EndOfCentralDirLocatorSignature;
725             putUint  (i + 4,  cast(ushort) 0);
726             putUlong (i + 8,  eocd64Offset);
727             putUint  (i + 16, 1);
728             i += zip64EndOfCentralDirLocatorLength;
729         }
730 
731         // Write end record
732         _data[i .. i + 4] = endOfCentralDirSignature;
733         putUshort(i + 4,  cast(ushort) 0);
734         putUshort(i + 6,  cast(ushort) 0);
735         putUshort(i + 8,  (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries));
736         putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries));
737         putUint  (i + 12, directorySize);
738         putUint  (i + 16, directoryOffset);
739         putUshort(i + 20, cast(ushort) comment.length);
740         i += endOfCentralDirLength;
741 
742         // Write archive comment
743         assert(i + comment.length == data.length, "Writing the archive comment failed.");
744         _data[i .. data.length] = (comment.representation)[];
745 
746         return cast(void[]) data;
747     }
748 
749     @safe pure unittest
750     {
751         import std.exception : assertNotThrown;
752 
753         ZipArchive zip = new ZipArchive();
754         zip.comment = "A";
755         assertNotThrown(zip.build());
756     }
757 
758     @safe pure unittest
759     {
760         import std.range : repeat, array;
761         import std.exception : assertThrown;
762 
763         ZipArchive zip = new ZipArchive();
764         zip.comment = 'A'.repeat(70_000).array;
765         assertThrown!ZipException(zip.build());
766     }
767 
768     /* ============ Reading an existing archive =================== */
769 
770     /**
771      * Constructor to use when reading an existing archive.
772      *
773      * Fills in the properties data[], totalEntries, comment[], and directory[].
774      * For each ArchiveMember, fills in
775      * properties madeVersion, extractVersion, flags, compressionMethod, time,
776      * crc32, compressedSize, expandedSize, compressedData[],
777      * internalAttributes, externalAttributes, name[], extra[], comment[].
778      * Use expand() to get the expanded data for each ArchiveMember.
779      *
780      * Params:
781      *     buffer = The entire contents of the archive.
782      *
783      * Throws: ZipException when the archive was invalid or when malware was detected.
784      */
785     this(void[] buffer)
786     {
787         this._data = cast(ubyte[]) buffer;
788 
789         enforce!ZipException(data.length <= uint.max - 2, "zip files bigger than 4 GB are unsupported");
790 
791         _segs = [Segment(0, cast(uint) data.length)];
792 
793         uint i = findEndOfCentralDirRecord();
794 
795         int endCommentLength = getUshort(i + 20);
796         comment = cast(string)(_data[i + endOfCentralDirLength .. i + endOfCentralDirLength + endCommentLength]);
797 
798         // end of central dir record
799         removeSegment(i, i + endOfCentralDirLength + endCommentLength);
800 
801         uint k = i - zip64EndOfCentralDirLocatorLength;
802         if (k < i && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature)
803         {
804             _isZip64 = true;
805             i = k;
806 
807             // zip64 end of central dir record locator
808             removeSegment(k, k + zip64EndOfCentralDirLocatorLength);
809         }
810 
811         uint directorySize;
812         uint directoryOffset;
813         uint directoryCount;
814 
815         if (isZip64)
816         {
817             // Read Zip64 record data
818             ulong eocdOffset = getUlong(i + 8);
819             enforce!ZipException(eocdOffset + zip64EndOfCentralDirLength <= _data.length,
820                                  "corrupted directory");
821 
822             i = to!uint(eocdOffset);
823             enforce!ZipException(_data[i .. i + 4] == zip64EndOfCentralDirSignature,
824                                  "invalid Zip EOCD64 signature");
825 
826             ulong eocd64Size = getUlong(i + 4);
827             enforce!ZipException(eocd64Size + i - 12 <= data.length,
828                                  "invalid Zip EOCD64 size");
829 
830             // zip64 end of central dir record
831             removeSegment(i, cast(uint) (i + 12 + eocd64Size));
832 
833             ulong numEntriesUlong = getUlong(i + 24);
834             ulong totalEntriesUlong = getUlong(i + 32);
835             ulong directorySizeUlong = getUlong(i + 40);
836             ulong directoryOffsetUlong = getUlong(i + 48);
837 
838             enforce!ZipException(numEntriesUlong <= uint.max,
839                                  "supposedly more than 4294967296 files in archive");
840 
841             enforce!ZipException(numEntriesUlong == totalEntriesUlong,
842                                  "multiple disk zips not supported");
843 
844             enforce!ZipException(directorySizeUlong <= i && directoryOffsetUlong <= i
845                                  && directorySizeUlong + directoryOffsetUlong <= i,
846                                  "corrupted directory");
847 
848             directoryCount = to!uint(totalEntriesUlong);
849             directorySize = to!uint(directorySizeUlong);
850             directoryOffset = to!uint(directoryOffsetUlong);
851         }
852         else
853         {
854             // Read end record data
855             directoryCount = getUshort(i + 10);
856             directorySize = getUint(i + 12);
857             directoryOffset = getUint(i + 16);
858         }
859 
860         i = directoryOffset;
861         for (int n = 0; n < directoryCount; n++)
862         {
863             /* The format of an entry is:
864              *  'PK' 1, 2
865              *  directory info
866              *  path
867              *  extra data
868              *  comment
869              */
870 
871             uint namelen;
872             uint extralen;
873             uint commentlen;
874 
875             enforce!ZipException(_data[i .. i + 4] == centralFileHeaderSignature,
876                                  "wrong central file header signature found");
877             ArchiveMember de = new ArchiveMember();
878             de._index = n;
879             de._madeVersion = getUshort(i + 4);
880             de._extractVersion = getUshort(i + 6);
881             de.flags = getUshort(i + 8);
882             de._compressionMethod = cast(CompressionMethod) getUshort(i + 10);
883             de.time = cast(DosFileTime) getUint(i + 12);
884             de._crc32 = getUint(i + 16);
885             de._compressedSize = getUint(i + 20);
886             de._expandedSize = getUint(i + 24);
887             namelen = getUshort(i + 28);
888             extralen = getUshort(i + 30);
889             commentlen = getUshort(i + 32);
890             de.internalAttributes = getUshort(i + 36);
891             de._externalAttributes = getUint(i + 38);
892             de.offset = getUint(i + 42);
893 
894             // central file header
895             removeSegment(i, i + centralFileHeaderLength + namelen + extralen + commentlen);
896 
897             i += centralFileHeaderLength;
898 
899             enforce!ZipException(i + namelen + extralen + commentlen <= directoryOffset + directorySize,
900                                  "invalid field lengths in file header found");
901 
902             de.name = cast(string)(_data[i .. i + namelen]);
903             i += namelen;
904             de.extra = _data[i .. i + extralen];
905             i += extralen;
906             de.comment = cast(string)(_data[i .. i + commentlen]);
907             i += commentlen;
908 
909             auto localFileHeaderNamelen = getUshort(de.offset + 26);
910             auto localFileHeaderExtralen = getUshort(de.offset + 28);
911 
912             // file data
913             removeSegment(de.offset, de.offset + localFileHeaderLength + localFileHeaderNamelen
914                                      + localFileHeaderExtralen + de._compressedSize);
915 
916             immutable uint dataOffset = de.offset + localFileHeaderLength
917                                         + localFileHeaderNamelen + localFileHeaderExtralen;
918             de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize];
919 
920             _directory[de.name] = de;
921         }
922 
923         enforce!ZipException(i == directoryOffset + directorySize, "invalid directory entry 3");
924     }
925 
926     @system unittest
927     {
928         import std.exception : assertThrown;
929 
930         // contains wrong directorySize (extra byte 0xff)
931         auto file =
932             "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
933             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
934             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
935             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
936             "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
937             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
938             "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
939             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
940             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~
941             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4b\x00\x00\x00\x43\x00\x00"~
942             "\x00\x00\x00";
943 
944         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
945     }
946 
947     @system unittest
948     {
949         import std.exception : assertThrown;
950 
951         // wrong eocdOffset
952         auto file =
953             "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
954             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
955             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
956             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
957             "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
958             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
959             "\x00\x00";
960 
961         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
962     }
963 
964     @system unittest
965     {
966         import std.exception : assertThrown;
967 
968         // wrong signature of zip64 end of central directory
969         auto file =
970             "\x50\x4b\x06\x07\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
971             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
972             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
973             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
974             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
975             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
976             "\x00\x00";
977 
978         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
979     }
980 
981     @system unittest
982     {
983         import std.exception : assertThrown;
984 
985         // wrong size of zip64 end of central directory
986         auto file =
987             "\x50\x4b\x06\x06\xff\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
988             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
989             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
990             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
991             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
992             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
993             "\x00\x00";
994 
995         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
996     }
997 
998     @system unittest
999     {
1000         import std.exception : assertThrown;
1001 
1002         // too many entries in zip64 end of central directory
1003         auto file =
1004             "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1005             "\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\x00\x00\x00"~
1006             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1007             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1008             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1009             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1010             "\x00\x00";
1011 
1012         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1013     }
1014 
1015     @system unittest
1016     {
1017         import std.exception : assertThrown;
1018 
1019         // zip64: numEntries and totalEntries differ
1020         auto file =
1021             "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1022             "\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~
1023             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1024             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1025             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1026             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1027             "\x00\x00";
1028 
1029         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1030     }
1031 
1032     @system unittest
1033     {
1034         import std.exception : assertThrown;
1035 
1036         // zip64: directorySize too large
1037         auto file =
1038             "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1039             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1040             "\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00"~
1041             "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1042             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1043             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1044             "\x00\x00";
1045 
1046         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1047 
1048         // zip64: directoryOffset too large
1049         file =
1050             "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1051             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1052             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1053             "\xff\xff\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1054             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1055             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1056             "\x00\x00";
1057 
1058         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1059 
1060         // zip64: directorySize + directoryOffset too large
1061         // we need to add a useless byte at the beginning to avoid that one of the other two checks allready fires
1062         file =
1063             "\x00\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1064             "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1065             "\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~
1066             "\x01\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1067             "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1068             "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1069             "\x00\x00";
1070 
1071         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1072     }
1073 
1074     @system unittest
1075     {
1076         import std.exception : assertThrown;
1077 
1078         // wrong central file header signature
1079         auto file =
1080             "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1081             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1082             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1083             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1084             "\x6c\x6c\x6f\x50\x4b\x01\x03\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1085             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1086             "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1087             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1088             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1089             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1090             "\x00\x00\x00";
1091 
1092         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1093     }
1094 
1095     @system unittest
1096     {
1097         import std.exception : assertThrown;
1098 
1099         // invalid field lengths in file header
1100         auto file =
1101             "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1102             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1103             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1104             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1105             "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1106             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1107             "\x00\x18\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1108             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1109             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~
1110             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1111             "\x00\x00\x00";
1112 
1113         assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1114     }
1115 
1116     private uint findEndOfCentralDirRecord()
1117     {
1118         // end of central dir record can be followed by a comment of up to 2^^16-1 bytes
1119         // therefore we have to scan 2^^16 positions
1120 
1121         uint endrecOffset = to!uint(data.length);
1122         foreach (i; 0 .. 2 ^^ 16)
1123         {
1124             if (endOfCentralDirLength + i > data.length) break;
1125             uint start = to!uint(data.length) - endOfCentralDirLength - i;
1126 
1127             if (data[start .. start + 4] != endOfCentralDirSignature) continue;
1128 
1129             auto numberOfThisDisc = getUshort(start + 4);
1130             if (numberOfThisDisc != 0) continue; // no support for multiple volumes yet
1131 
1132             auto numberOfStartOfCentralDirectory = getUshort(start + 6);
1133             if (numberOfStartOfCentralDirectory != 0) continue; // dito
1134 
1135             if (numberOfThisDisc < numberOfStartOfCentralDirectory) continue;
1136 
1137             uint k = start - zip64EndOfCentralDirLocatorLength;
1138             auto maybeZip64 = k < start && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature;
1139 
1140             auto totalNumberOfEntriesOnThisDisk = getUshort(start + 8);
1141             auto totalNumberOfEntriesInCentralDir = getUshort(start + 10);
1142 
1143             if (totalNumberOfEntriesOnThisDisk > totalNumberOfEntriesInCentralDir &&
1144                (!maybeZip64 || totalNumberOfEntriesOnThisDisk < 0xffff)) continue;
1145 
1146             auto sizeOfCentralDirectory = getUint(start + 12);
1147             if (sizeOfCentralDirectory > start &&
1148                (!maybeZip64 || sizeOfCentralDirectory < 0xffff)) continue;
1149 
1150             auto offsetOfCentralDirectory = getUint(start + 16);
1151             if (offsetOfCentralDirectory > start - sizeOfCentralDirectory &&
1152                (!maybeZip64 || offsetOfCentralDirectory < 0xffff)) continue;
1153 
1154             auto zipfileCommentLength = getUshort(start + 20);
1155             if (start + zipfileCommentLength + endOfCentralDirLength != data.length) continue;
1156 
1157             enforce!ZipException(endrecOffset == to!uint(data.length),
1158                                  "found more than one valid 'end of central dir record'");
1159 
1160             endrecOffset = start;
1161         }
1162 
1163         enforce!ZipException(endrecOffset != to!uint(data.length),
1164                              "found no valid 'end of central dir record'");
1165 
1166         return endrecOffset;
1167     }
1168 
1169     /**
1170      * Decompress the contents of a member.
1171      *
1172      * Fills in properties extractVersion, flags, compressionMethod, time,
1173      * crc32, compressedSize, expandedSize, expandedData[], name[], extra[].
1174      *
1175      * Params:
1176      *     de = Member to be decompressed.
1177      *
1178      * Returns: The expanded data.
1179      *
1180      * Throws: ZipException when the entry is invalid or the compression method is not supported.
1181      */
1182     ubyte[] expand(ArchiveMember de)
1183     {
1184         import std.string : representation;
1185 
1186         uint namelen;
1187         uint extralen;
1188 
1189         enforce!ZipException(_data[de.offset .. de.offset + 4] == localFileHeaderSignature,
1190                              "wrong local file header signature found");
1191 
1192         // These values should match what is in the main zip archive directory
1193         de._extractVersion = getUshort(de.offset + 4);
1194         de.flags = getUshort(de.offset + 6);
1195         de._compressionMethod = cast(CompressionMethod) getUshort(de.offset + 8);
1196         de.time = cast(DosFileTime) getUint(de.offset + 10);
1197         de._crc32 = getUint(de.offset + 14);
1198         de._compressedSize = max(getUint(de.offset + 18), de.compressedSize);
1199         de._expandedSize = max(getUint(de.offset + 22), de.expandedSize);
1200         namelen = getUshort(de.offset + 26);
1201         extralen = getUshort(de.offset + 28);
1202 
1203         debug(print)
1204         {
1205             printf("\t\texpandedSize = %d\n", de.expandedSize);
1206             printf("\t\tcompressedSize = %d\n", de.compressedSize);
1207             printf("\t\tnamelen = %d\n", namelen);
1208             printf("\t\textralen = %d\n", extralen);
1209         }
1210 
1211         enforce!ZipException((de.flags & 1) == 0, "encryption not supported");
1212 
1213         switch (de.compressionMethod)
1214         {
1215             case CompressionMethod.none:
1216                 de._expandedData = de.compressedData;
1217                 return de.expandedData;
1218 
1219             case CompressionMethod.deflate:
1220                 // -15 is a magic value used to decompress zip files.
1221                 // It has the effect of not requiring the 2 byte header
1222                 // and 4 byte trailer.
1223                 import std.zlib : uncompress;
1224                 de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15);
1225                 return de.expandedData;
1226 
1227             default:
1228                 throw new ZipException("unsupported compression method");
1229         }
1230     }
1231 
1232     @system unittest
1233     {
1234         import std.exception : assertThrown;
1235 
1236         // check for correct local file header signature
1237         auto file =
1238             "\x50\x4b\x04\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1239             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1240             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1241             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1242             "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1243             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1244             "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1245             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1246             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1247             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1248             "\x00\x00\x00";
1249 
1250         auto za = new ZipArchive(cast(void[]) file);
1251 
1252         assertThrown!ZipException(za.expand(za._directory["file"]));
1253     }
1254 
1255     @system unittest
1256     {
1257         import std.exception : assertThrown;
1258 
1259         // check for encryption flag
1260         auto file =
1261             "\x50\x4b\x03\x04\x0a\x00\x01\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1262             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1263             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1264             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1265             "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1266             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1267             "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1268             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1269             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1270             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1271             "\x00\x00\x00";
1272 
1273         auto za = new ZipArchive(cast(void[]) file);
1274 
1275         assertThrown!ZipException(za.expand(za._directory["file"]));
1276     }
1277 
1278     @system unittest
1279     {
1280         import std.exception : assertThrown;
1281 
1282         // check for invalid compression method
1283         auto file =
1284             "\x50\x4b\x03\x04\x0a\x00\x00\x00\x03\x00\x8f\x72\x4a\x4f\x86\xa6"~
1285             "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1286             "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1287             "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1288             "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1289             "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1290             "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1291             "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1292             "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1293             "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1294             "\x00\x00\x00";
1295 
1296         auto za = new ZipArchive(cast(void[]) file);
1297 
1298         assertThrown!ZipException(za.expand(za._directory["file"]));
1299     }
1300 
1301     /* ============ Utility =================== */
1302 
1303     @safe @nogc pure nothrow ushort getUshort(uint i)
1304     {
1305         ubyte[2] result = data[i .. i + 2];
1306         return littleEndianToNative!ushort(result);
1307     }
1308 
1309     @safe @nogc pure nothrow uint getUint(uint i)
1310     {
1311         ubyte[4] result = data[i .. i + 4];
1312         return littleEndianToNative!uint(result);
1313     }
1314 
1315     @safe @nogc pure nothrow ulong getUlong(uint i)
1316     {
1317         ubyte[8] result = data[i .. i + 8];
1318         return littleEndianToNative!ulong(result);
1319     }
1320 
1321     @safe @nogc pure nothrow void putUshort(uint i, ushort us)
1322     {
1323         data[i .. i + 2] = nativeToLittleEndian(us);
1324     }
1325 
1326     @safe @nogc pure nothrow void putUint(uint i, uint ui)
1327     {
1328         data[i .. i + 4] = nativeToLittleEndian(ui);
1329     }
1330 
1331     @safe @nogc pure nothrow void putUlong(uint i, ulong ul)
1332     {
1333         data[i .. i + 8] = nativeToLittleEndian(ul);
1334     }
1335 
1336     /* ============== for detecting overlaps =============== */
1337 
1338 private:
1339 
1340     // defines a segment of the zip file, including start, excluding end
1341     struct Segment
1342     {
1343         uint start;
1344         uint end;
1345     }
1346 
1347     // removes Segment start .. end from _segs
1348     // throws zipException if start .. end is not completely available in _segs;
1349     void removeSegment(uint start, uint end) pure @safe
1350     in (start < end, "segment invalid")
1351     {
1352         auto found = false;
1353         size_t pos;
1354         foreach (i,seg;_segs)
1355             if (seg.start <= start && seg.end >= end
1356                 && (!found || seg.start > _segs[pos].start))
1357             {
1358                 found = true;
1359                 pos = i;
1360             }
1361 
1362         enforce!ZipException(found, "overlapping data detected");
1363 
1364         if (start>_segs[pos].start)
1365             _segs ~= Segment(_segs[pos].start, start);
1366         if (end<_segs[pos].end)
1367             _segs ~= Segment(end, _segs[pos].end);
1368         _segs = _segs[0 .. pos] ~ _segs[pos + 1 .. $];
1369     }
1370 
1371     pure @safe unittest
1372     {
1373         with (new ZipArchive())
1374         {
1375             _segs = [Segment(0,100)];
1376             removeSegment(10,20);
1377             assert(_segs == [Segment(0,10),Segment(20,100)]);
1378 
1379             _segs = [Segment(0,100)];
1380             removeSegment(0,20);
1381             assert(_segs == [Segment(20,100)]);
1382 
1383             _segs = [Segment(0,100)];
1384             removeSegment(10,100);
1385             assert(_segs == [Segment(0,10)]);
1386 
1387             _segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
1388             removeSegment(220,230);
1389             assert(_segs == [Segment(0,100),Segment(400,500),Segment(200,220),Segment(230,300)]);
1390 
1391             _segs = [Segment(200,300), Segment(0,100), Segment(400,500)];
1392             removeSegment(20,30);
1393             assert(_segs == [Segment(200,300),Segment(400,500),Segment(0,20),Segment(30,100)]);
1394 
1395             import std.exception : assertThrown;
1396 
1397             _segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
1398             assertThrown(removeSegment(120,230));
1399 
1400             _segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
1401             removeSegment(0,100);
1402             assertThrown(removeSegment(0,100));
1403 
1404             _segs = [Segment(0,100)];
1405             removeSegment(0,100);
1406             assertThrown(removeSegment(0,100));
1407         }
1408     }
1409 }
1410 
1411 debug(print)
1412 {
1413     @safe void arrayPrint(ubyte[] array)
1414     {
1415         printf("array %p,%d\n", cast(void*) array, array.length);
1416         for (int i = 0; i < array.length; i++)
1417         {
1418             printf("%02x ", array[i]);
1419             if (((i + 1) & 15) == 0)
1420                 printf("\n");
1421         }
1422         printf("\n");
1423     }
1424 }
1425 
1426 @system unittest
1427 {
1428     // @system due to (at least) ZipArchive.build
1429     auto zip1 = new ZipArchive();
1430     auto zip2 = new ZipArchive();
1431     auto am1 = new ArchiveMember();
1432     am1.name = "foo";
1433     am1.expandedData = new ubyte[](1024);
1434     zip1.addMember(am1);
1435     auto data1 = zip1.build();
1436     zip2.addMember(zip1.directory["foo"]);
1437     zip2.build();
1438     auto am2 = zip2.directory["foo"];
1439     zip2.expand(am2);
1440     assert(am1.expandedData == am2.expandedData);
1441     auto zip3 = new ZipArchive(data1);
1442     zip3.build();
1443     assert(zip3.directory["foo"].compressedSize == am1.compressedSize);
1444 
1445     // Test if packing and unpacking produces the original data
1446     import std.conv, std.stdio;
1447     import std.random : uniform, MinstdRand0;
1448     MinstdRand0 gen;
1449     const uint itemCount = 20, minSize = 10, maxSize = 500;
1450     foreach (variant; 0 .. 2)
1451     {
1452         bool useZip64 = !!variant;
1453         zip1 = new ZipArchive();
1454         zip1.isZip64 = useZip64;
1455         ArchiveMember[itemCount] ams;
1456         foreach (i; 0 .. itemCount)
1457         {
1458             ams[i] = new ArchiveMember();
1459             ams[i].name = to!string(i);
1460             ams[i].expandedData = new ubyte[](uniform(minSize, maxSize));
1461             foreach (ref ubyte c; ams[i].expandedData)
1462                 c = cast(ubyte)(uniform(0, 256));
1463             ams[i].compressionMethod = CompressionMethod.deflate;
1464             zip1.addMember(ams[i]);
1465         }
1466         auto zippedData = zip1.build();
1467         zip2 = new ZipArchive(zippedData);
1468         assert(zip2.isZip64 == useZip64);
1469         foreach (am; ams)
1470         {
1471             am2 = zip2.directory[am.name];
1472             zip2.expand(am2);
1473             assert(am.crc32 == am2.crc32);
1474             assert(am.expandedData == am2.expandedData);
1475         }
1476     }
1477 }
1478 
1479 @system unittest
1480 {
1481     import std.conv : to;
1482     import std.random : Mt19937, randomShuffle;
1483     // Test if packing and unpacking preserves order.
1484     auto rand = Mt19937(15966);
1485     string[] names;
1486     int value = 0;
1487     // Generate a series of unique numbers as filenames.
1488     foreach (i; 0 .. 20)
1489     {
1490         value += 1 + rand.front & 0xFFFF;
1491         rand.popFront;
1492         names ~= value.to!string;
1493     }
1494     // Insert them in a random order.
1495     names.randomShuffle(rand);
1496     auto zip1 = new ZipArchive();
1497     foreach (i, name; names)
1498     {
1499         auto member = new ArchiveMember();
1500         member.name = name;
1501         member.expandedData = cast(ubyte[]) name;
1502         member.index = cast(int) i;
1503         zip1.addMember(member);
1504     }
1505     auto data = zip1.build();
1506 
1507     // Ensure that they appear in the same order.
1508     auto zip2 = new ZipArchive(data);
1509     foreach (i, name; names)
1510     {
1511         const member = zip2.directory[name];
1512         assert(member.index == i, "member " ~ name ~ " had index " ~
1513                 member.index.to!string ~ " but we expected index " ~ i.to!string ~
1514                 ". The input array was " ~ names.to!string);
1515     }
1516 }
1517 
1518 @system unittest
1519 {
1520     import std.zlib;
1521 
1522     ubyte[] src = cast(ubyte[])
1523 "the quick brown fox jumps over the lazy dog\r
1524 the quick brown fox jumps over the lazy dog\r
1525 ";
1526     auto dst = cast(ubyte[]) compress(cast(void[]) src);
1527     auto after = cast(ubyte[]) uncompress(cast(void[]) dst);
1528     assert(src == after);
1529 }
1530 
1531 @system unittest
1532 {
1533     // @system due to ZipArchive.build
1534     import std.datetime;
1535     ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9];
1536 
1537     auto ar = new ZipArchive;
1538     auto am = new ArchiveMember;  // 10
1539     am.name = "buf";
1540     am.expandedData = buf;
1541     am.compressionMethod = CompressionMethod.deflate;
1542     am.time = SysTimeToDosFileTime(Clock.currTime());
1543     ar.addMember(am);            // 15
1544 
1545     auto zip1 = ar.build();
1546     auto arAfter = new ZipArchive(zip1);
1547     assert(arAfter.directory.length == 1);
1548     auto amAfter = arAfter.directory["buf"];
1549     arAfter.expand(amAfter);
1550     assert(amAfter.name == am.name);
1551     assert(amAfter.expandedData == am.expandedData);
1552     assert(amAfter.time == am.time);
1553 }
1554 
1555 @system unittest
1556 {
1557     // invalid format of end of central directory entry
1558     import std.exception : assertThrown;
1559     assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06aaaaaaaaaaaaaaaaaaaa"));
1560 }
1561 
1562 @system unittest
1563 {
1564     // minimum (empty) archive should pass
1565     auto za = new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
1566                                           "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
1567     assert(za.directory.length == 0);
1568 
1569     // one byte too short or too long should not pass
1570     import std.exception : assertThrown;
1571     assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
1572                                                           "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"));
1573     assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
1574                                                           "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"));
1575 }
1576 
1577 @system unittest
1578 {
1579     // https://issues.dlang.org/show_bug.cgi?id=20239
1580     // chameleon file, containing two valid end of central directory entries
1581     auto file =
1582         "\x50\x4B\x03\x04\x0A\x00\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00"~
1583         "\x00\x00\x01\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75"~
1584         "\x61\x67\x65\x55\x54\x09\x00\x03\x82\xF2\x8A\x5D\x82\xF2\x8A\x5D\x75\x78\x0B\x00"~
1585         "\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x44\x50\x4B\x01\x02\x1E\x03\x0A\x00"~
1586         "\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00\x00\x00\x01\x00\x00\x00"~
1587         "\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\x00\x00\x00\x00\x62\x65"~
1588         "\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x82\xF2\x8A\x5D"~
1589         "\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B\x05\x06\x00"~
1590         "\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\x48\x00\x00\x00\xB7\x00\x50\x4B\x03"~
1591         "\x04\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~
1592         "\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65"~
1593         "\x55\x54\x09\x00\x03\x97\xF2\x8A\x5D\x8C\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB"~
1594         "\x03\x00\x00\x04\xEB\x03\x00\x00\x46\x4F\x52\x54\x52\x41\x4E\x50\x4B\x01\x02\x1E"~
1595         "\x03\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~
1596         "\x00\x00\x00\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\xB1\x00\x00"~
1597         "\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x97"~
1598         "\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B"~
1599         "\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\xFF\x00\x00\x00\x00\x00";
1600 
1601     import std.exception : assertThrown;
1602     assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1603 }
1604 
1605 @system unittest
1606 {
1607     // https://issues.dlang.org/show_bug.cgi?id=20287
1608     // check for correct compressed data
1609     auto file =
1610         "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1611         "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1612         "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1613         "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1614         "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1615         "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1616         "\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1617         "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1618         "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1619         "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1620         "\x00\x00\x00";
1621 
1622     auto za = new ZipArchive(cast(void[]) file);
1623     assert(za.directory["file"].compressedData == [104, 101, 108, 108, 111]);
1624 }
1625 
1626 // https://issues.dlang.org/show_bug.cgi?id=20027
1627 @system unittest
1628 {
1629     // central file header overlaps end of central directory
1630     auto file =
1631         // lfh
1632         "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1633         "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
1634         "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1635         "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1636         "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1637         "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1638         "\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1639         "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1640         "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1641         "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1642         "\x00\x00\x00";
1643 
1644     import std.exception : assertThrown;
1645     assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1646 
1647     // local file header and file data overlap second local file header and file data
1648     file =
1649         "\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
1650         "\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1e\x00\x66\x69"~
1651         "\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
1652         "\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
1653         "\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
1654         "\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
1655         "\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
1656         "\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
1657         "\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
1658         "\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
1659         "\x00\x00\x00";
1660 
1661     assertThrown!ZipException(new ZipArchive(cast(void[]) file));
1662 }
1663 
1664 @system unittest
1665 {
1666     // https://issues.dlang.org/show_bug.cgi?id=20295
1667     // zip64 with 0xff bytes in end of central dir record do not work
1668     // minimum (empty zip64) archive should pass
1669     auto file =
1670         "\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
1671         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1672         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
1673         "\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
1674         "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
1675         "\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
1676         "\x00\x00";
1677 
1678     auto za = new ZipArchive(cast(void[]) file);
1679     assert(za.directory.length == 0);
1680 }
1681 
1682 version (HasUnzip)
1683 @system unittest
1684 {
1685     import std.datetime, std.file, std.format, std.path, std.process, std.stdio;
1686 
1687     if (executeShell("unzip").status != 0)
1688     {
1689         writeln("Can't run unzip, skipping unzip test");
1690         return;
1691     }
1692 
1693     auto zr = new ZipArchive();
1694     auto am = new ArchiveMember();
1695     am.compressionMethod = CompressionMethod.deflate;
1696     am.name = "foo.bar";
1697     am.time = SysTimeToDosFileTime(Clock.currTime());
1698     am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine";
1699     zr.addMember(am);
1700     auto data2 = zr.build();
1701 
1702     mkdirRecurse(deleteme);
1703     scope(exit) rmdirRecurse(deleteme);
1704     string zipFile = buildPath(deleteme, "foo.zip");
1705     std.file.write(zipFile, cast(byte[]) data2);
1706 
1707     auto result = executeShell(format("unzip -l %s", zipFile));
1708     scope(failure) writeln(result.output);
1709     assert(result.status == 0);
1710 }