The OpenD Programming Language

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