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