1 // Written in the D programming language.
2 
3 /**
4  * Read/write data in the $(LINK2 http://www.info-zip.org, _zip archive) format.
5  * Makes use of the etc.c.zlib compression library.
6  *
7  * Bugs:
8  *      $(UL
9  *      $(LI Multi-disk zips not supported.)
10  *      $(LI Only Zip version 20 formats are supported.)
11  *      $(LI Only supports compression modes 0 (no compression) and 8 (deflate).)
12  *      $(LI Does not support encryption.)
13  *      $(LI $(BUGZILLA 592))
14  *      $(LI $(BUGZILLA 2137))
15  *      )
16  *
17  * Example:
18  * ---
19 // Read existing zip file.
20 import std.digest.crc, std.file, std.stdio, std.zip;
21 
22 void main(string[] args)
23 {
24     // read a zip file into memory
25     auto zip = new ZipArchive(read(args[1]));
26     writeln("Archive: ", args[1]);
27     writefln("%-10s  %-8s  Name", "Length", "CRC-32");
28     // iterate over all zip members
29     foreach (name, am; zip.directory)
30     {
31         // print some data about each member
32         writefln("%10s  %08x  %s", am.expandedSize, am.crc32, name);
33         assert(am.expandedData.length == 0);
34         // decompress the archive member
35         zip.expand(am);
36         assert(am.expandedData.length == am.expandedSize);
37     }
38 }
39 
40 // Create and write new zip file.
41 import std.file : write;
42 import std.string : representation;
43 
44 void main()
45 {
46     char[] data = "Test data.\n".dup;
47     // Create an ArchiveMember for the test file.
48     ArchiveMember am = new ArchiveMember();
49     am.name = "test.txt";
50     am.expandedData(data.representation);
51     // Create an archive and add the member.
52     ZipArchive zip = new ZipArchive();
53     zip.addMember(am);
54     // Build the archive
55     void[] compressed_data = zip.build();
56     // Write to a file
57     write("test.zip", compressed_data);
58 }
59  * ---
60  *
61  * Copyright: Copyright Digital Mars 2000 - 2009.
62  * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
63  * Authors:   $(HTTP digitalmars.com, Walter Bright)
64  * Source:    $(PHOBOSSRC std/_zip.d)
65  */
66 
67 /*          Copyright Digital Mars 2000 - 2009.
68  * Distributed under the Boost Software License, Version 1.0.
69  *    (See accompanying file LICENSE_1_0.txt or copy at
70  *          http://www.boost.org/LICENSE_1_0.txt)
71  */
72 module std.zip;
73 
74 //debug=print;
75 
76 /** Thrown on error.
77  */
78 class ZipException : Exception
79 {
this(string msg)80     this(string msg) @safe
81     {
82         super("ZipException: " ~ msg);
83     }
84 }
85 
86 /**
87  * Compression method used by ArchiveMember
88  */
89 enum CompressionMethod : ushort
90 {
91     none = 0,   /// No compression, just archiving
92     deflate = 8 /// Deflate algorithm. Use zlib library to compress
93 }
94 
95 /**
96  * A member of the ZipArchive.
97  */
98 final class ArchiveMember
99 {
100     import std.conv : to, octal;
101     import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime;
102 
103     /**
104      * Read/Write: Usually the file name of the archive member; it is used to
105      * index the archive directory for the member. Each member must have a unique
106      * name[]. Do not change without removing member from the directory first.
107      */
108     string name;
109 
110     ubyte[] extra;              /// Read/Write: extra data for this member.
111     string comment;             /// Read/Write: comment associated with this member.
112 
113     private ubyte[] _compressedData;
114     private ubyte[] _expandedData;
115     private uint offset;
116     private uint _crc32;
117     private uint _compressedSize;
118     private uint _expandedSize;
119     private CompressionMethod _compressionMethod;
120     private ushort _madeVersion = 20;
121     private ushort _extractVersion = 20;
122     private ushort _diskNumber;
123     private uint _externalAttributes;
124     private DosFileTime _time;
125     // by default, no explicit order goes after explicit order
126     private uint _index = uint.max;
127 
128     ushort flags;                  /// Read/Write: normally set to 0
129     ushort internalAttributes;     /// Read/Write
130 
extractVersion()131     @property ushort extractVersion()     { return _extractVersion; }    /// Read Only
crc32()132     @property uint crc32()         { return _crc32; }    /// Read Only: cyclic redundancy check (CRC) value
133 
134     /// Read Only: size of data of member in compressed form.
compressedSize()135     @property uint compressedSize()     { return _compressedSize; }
136 
137     /// Read Only: size of data of member in expanded form.
expandedSize()138     @property uint expandedSize()     { return _expandedSize; }
diskNumber()139     @property ushort diskNumber()     { return _diskNumber; }        /// Read Only: should be 0.
140 
141     /// Read Only: data of member in compressed form.
compressedData()142     @property ubyte[] compressedData()     { return _compressedData; }
143 
144     /// Read data of member in uncompressed form.
expandedData()145     @property ubyte[] expandedData()     { return _expandedData; }
146 
147     /// Write data of member in uncompressed form.
expandedData(ubyte[]ed)148     @property @safe void expandedData(ubyte[] ed)
149     {
150         _expandedData = ed;
151         _expandedSize  = to!uint(_expandedData.length);
152 
153         // Clean old compressed data, if any
154         _compressedData.length = 0;
155         _compressedSize = 0;
156     }
157 
158     /**
159      * Set the OS specific file attributes, as obtained by
160      * $(REF getAttributes, std,file) or $(REF DirEntry.attributes, std,file), for this archive member.
161      */
fileAttributes(uint attr)162     @property @safe void fileAttributes(uint attr)
163     {
164         version (Posix)
165         {
166             _externalAttributes = (attr & 0xFFFF) << 16;
167             _madeVersion &= 0x00FF;
168             _madeVersion |= 0x0300; // attributes are in UNIX format
169         }
170         else version (Windows)
171         {
172             _externalAttributes = attr;
173             _madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format
174         }
175         else
176         {
177             static assert(0, "Unimplemented platform");
178         }
179     }
180 
version(Posix)181     version (Posix) @safe unittest
182     {
183         auto am = new ArchiveMember();
184         am.fileAttributes = octal!100644;
185         assert(am._externalAttributes == octal!100644 << 16);
186         assert((am._madeVersion & 0xFF00) == 0x0300);
187     }
188 
189     /**
190      * Get the OS specific file attributes for the archive member.
191      *
192      * Returns: The file attributes or 0 if the file attributes were
193      * encoded for an incompatible OS (Windows vs. Posix).
194      *
195      */
fileAttributes()196     @property uint fileAttributes() const
197     {
198         version (Posix)
199         {
200             if ((_madeVersion & 0xFF00) == 0x0300)
201                 return _externalAttributes >> 16;
202             return 0;
203         }
204         else version (Windows)
205         {
206             if ((_madeVersion & 0xFF00) == 0x0000)
207                 return _externalAttributes;
208             return 0;
209         }
210         else
211         {
212             static assert(0, "Unimplemented platform");
213         }
214     }
215 
216     /// Set the last modification time for this member.
time(SysTime time)217     @property void time(SysTime time)
218     {
219         _time = SysTimeToDosFileTime(time);
220     }
221 
222     /// ditto
time(DosFileTime time)223     @property void time(DosFileTime time)
224     {
225         _time = time;
226     }
227 
228     /// Get the last modification time for this member.
time()229     @property DosFileTime time() const
230     {
231         return _time;
232     }
233 
234     /**
235      * Read compression method used for this member
236      * See_Also:
237      *     CompressionMethod
238      **/
compressionMethod()239     @property @safe CompressionMethod compressionMethod() { return _compressionMethod; }
240 
241     /**
242      * Write compression method used for this member
243      * See_Also:
244      *     CompressionMethod
245      **/
compressionMethod(CompressionMethod cm)246     @property void compressionMethod(CompressionMethod cm)
247     {
248         if (cm == _compressionMethod) return;
249 
250         if (_compressedSize > 0)
251             throw new ZipException("Can't change compression method for a compressed element");
252 
253         _compressionMethod = cm;
254     }
255 
256     /**
257       * The index of this archive member within the archive.
258       */
index()259     @property uint index() const pure nothrow @nogc { return _index; }
index(uint value)260     @property uint index(uint value) pure nothrow @nogc { return _index = value; }
261 
debug(print)262     debug(print)
263     {
264     void print()
265     {
266         printf("name = '%.*s'\n", name.length, name.ptr);
267         printf("\tcomment = '%.*s'\n", comment.length, comment.ptr);
268         printf("\tmadeVersion = x%04x\n", _madeVersion);
269         printf("\textractVersion = x%04x\n", extractVersion);
270         printf("\tflags = x%04x\n", flags);
271         printf("\tcompressionMethod = %d\n", compressionMethod);
272         printf("\ttime = %d\n", time);
273         printf("\tcrc32 = x%08x\n", crc32);
274         printf("\texpandedSize = %d\n", expandedSize);
275         printf("\tcompressedSize = %d\n", compressedSize);
276         printf("\tinternalAttributes = x%04x\n", internalAttributes);
277         printf("\texternalAttributes = x%08x\n", externalAttributes);
278         printf("\tindex = x%08x\n", index);
279     }
280     }
281 }
282 
283 /**
284  * Object representing the entire archive.
285  * ZipArchives are collections of ArchiveMembers.
286  */
287 final class ZipArchive
288 {
289     import std.algorithm.comparison : max;
290     import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
291     import std.conv : to;
292     import std.datetime.systime : DosFileTime;
293 
294     string comment;     /// Read/Write: the archive comment. Must be less than 65536 bytes in length.
295 
296     private ubyte[] _data;
297     private uint endrecOffset;
298 
299     private uint _diskNumber;
300     private uint _diskStartDir;
301     private uint _numEntries;
302     private uint _totalEntries;
303     private bool _isZip64;
304     static const ushort zip64ExtractVersion = 45;
305     static const int digiSignLength = 6;
306     static const int eocd64LocLength = 20;
307     static const int eocd64Length = 56;
308 
309     /// Read Only: array representing the entire contents of the archive.
data()310     @property @safe ubyte[] data() { return _data; }
311 
312     /// Read Only: 0 since multi-disk zip archives are not supported.
diskNumber()313     @property @safe uint diskNumber()    { return _diskNumber; }
314 
315     /// Read Only: 0 since multi-disk zip archives are not supported
diskStartDir()316     @property @safe uint diskStartDir()  { return _diskStartDir; }
317 
318     /// Read Only: number of ArchiveMembers in the directory.
numEntries()319     @property @safe uint numEntries()    { return _numEntries; }
totalEntries()320     @property @safe uint totalEntries()  { return _totalEntries; }    /// ditto
321 
322     /// True when the archive is in Zip64 format.
isZip64()323     @property @safe bool isZip64()  { return _isZip64; }
324 
325     /// Set this to true to force building a Zip64 archive.
isZip64(bool value)326     @property @safe void isZip64(bool value) { _isZip64 = value; }
327     /**
328      * Read Only: array indexed by the name of each member of the archive.
329      *  All the members of the archive can be accessed with a foreach loop:
330      * Example:
331      * --------------------
332      * ZipArchive archive = new ZipArchive(data);
333      * foreach (ArchiveMember am; archive.directory)
334      * {
335      *     writefln("member name is '%s'", am.name);
336      * }
337      * --------------------
338      */
directory()339     @property @safe ArchiveMember[string] directory() { return _directory; }
340 
341     private ArchiveMember[string] _directory;
342 
debug(print)343     debug (print)
344     {
345     @safe void print()
346     {
347         printf("\tdiskNumber = %u\n", diskNumber);
348         printf("\tdiskStartDir = %u\n", diskStartDir);
349         printf("\tnumEntries = %u\n", numEntries);
350         printf("\ttotalEntries = %u\n", totalEntries);
351         printf("\tcomment = '%.*s'\n", comment.length, comment.ptr);
352     }
353     }
354 
355     /* ============ Creating a new archive =================== */
356 
357     /** Constructor to use when creating a new archive.
358      */
this()359     this() @safe
360     {
361     }
362 
363     /** Add de to the archive. The file is compressed on the fly.
364      */
addMember(ArchiveMember de)365     @safe void addMember(ArchiveMember de)
366     {
367         _directory[de.name] = de;
368         if (!de._compressedData.length)
369         {
370             switch (de.compressionMethod)
371             {
372                 case CompressionMethod.none:
373                     de._compressedData = de._expandedData;
374                     break;
375 
376                 case CompressionMethod.deflate:
377                     import std.zlib : compress;
378                     () @trusted
379                     {
380                         de._compressedData = cast(ubyte[]) compress(cast(void[]) de._expandedData);
381                     }();
382                         de._compressedData = de._compressedData[2 .. de._compressedData.length - 4];
383                     break;
384 
385                 default:
386                     throw new ZipException("unsupported compression method");
387             }
388 
389             de._compressedSize = to!uint(de._compressedData.length);
390             import std.zlib : crc32;
391             () @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }();
392         }
393         assert(de._compressedData.length == de._compressedSize);
394     }
395 
396     /** Delete de from the archive.
397      */
deleteMember(ArchiveMember de)398     @safe void deleteMember(ArchiveMember de)
399     {
400         _directory.remove(de.name);
401     }
402 
403     /**
404      * Construct an archive out of the current members of the archive.
405      *
406      * Fills in the properties data[], diskNumber, diskStartDir, numEntries,
407      * totalEntries, and directory[].
408      * For each ArchiveMember, fills in properties crc32, compressedSize,
409      * compressedData[].
410      *
411      * Returns: array representing the entire archive.
412      */
build()413     void[] build()
414     {
415         import std.algorithm.sorting : sort;
416         uint i;
417         uint directoryOffset;
418 
419         if (comment.length > 0xFFFF)
420             throw new ZipException("archive comment longer than 65535");
421 
422         // Compress each member; compute size
423         uint archiveSize = 0;
424         uint directorySize = 0;
425         auto directory = _directory.values().sort!((x, y) => x.index < y.index).release;
426         foreach (ArchiveMember de; directory)
427         {
428             if (to!ulong(archiveSize) + 30 + de.name.length + de.extra.length + de.compressedSize
429                     + directorySize + 46 + de.name.length + de.extra.length + de.comment.length
430                     + 22 + comment.length + eocd64LocLength + eocd64Length > uint.max)
431                 throw new ZipException("zip files bigger than 4 GB are unsupported");
432 
433             archiveSize += 30 + de.name.length +
434                                 de.extra.length +
435                                 de.compressedSize;
436             directorySize += 46 + de.name.length +
437                                 de.extra.length +
438                                 de.comment.length;
439         }
440 
441         if (!isZip64 && _directory.length > ushort.max)
442             _isZip64 = true;
443         uint dataSize = archiveSize + directorySize + 22 + cast(uint) comment.length;
444         if (isZip64)
445             dataSize += eocd64LocLength + eocd64Length;
446 
447         _data = new ubyte[dataSize];
448 
449         // Populate the data[]
450 
451         // Store each archive member
452         i = 0;
453         foreach (ArchiveMember de; directory)
454         {
455             de.offset = i;
456             _data[i .. i + 4] = cast(ubyte[])"PK\x03\x04";
457             putUshort(i + 4,  de.extractVersion);
458             putUshort(i + 6,  de.flags);
459             putUshort(i + 8,  de._compressionMethod);
460             putUint  (i + 10, cast(uint) de.time);
461             putUint  (i + 14, de.crc32);
462             putUint  (i + 18, de.compressedSize);
463             putUint  (i + 22, to!uint(de.expandedSize));
464             putUshort(i + 26, cast(ushort) de.name.length);
465             putUshort(i + 28, cast(ushort) de.extra.length);
466             i += 30;
467 
468             _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[];
469             i += de.name.length;
470             _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
471             i += de.extra.length;
472             _data[i .. i + de.compressedSize] = de.compressedData[];
473             i += de.compressedSize;
474         }
475 
476         // Write directory
477         directoryOffset = i;
478         _numEntries = 0;
479         foreach (ArchiveMember de; directory)
480         {
481             _data[i .. i + 4] = cast(ubyte[])"PK\x01\x02";
482             putUshort(i + 4,  de._madeVersion);
483             putUshort(i + 6,  de.extractVersion);
484             putUshort(i + 8,  de.flags);
485             putUshort(i + 10, de._compressionMethod);
486             putUint  (i + 12, cast(uint) de.time);
487             putUint  (i + 16, de.crc32);
488             putUint  (i + 20, de.compressedSize);
489             putUint  (i + 24, de.expandedSize);
490             putUshort(i + 28, cast(ushort) de.name.length);
491             putUshort(i + 30, cast(ushort) de.extra.length);
492             putUshort(i + 32, cast(ushort) de.comment.length);
493             putUshort(i + 34, de.diskNumber);
494             putUshort(i + 36, de.internalAttributes);
495             putUint  (i + 38, de._externalAttributes);
496             putUint  (i + 42, de.offset);
497             i += 46;
498 
499             _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[];
500             i += de.name.length;
501             _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
502             i += de.extra.length;
503             _data[i .. i + de.comment.length] = (cast(ubyte[]) de.comment)[];
504             i += de.comment.length;
505             _numEntries++;
506         }
507         _totalEntries = numEntries;
508 
509         if (isZip64)
510         {
511             // Write zip64 end of central directory record
512             uint eocd64Offset = i;
513             _data[i .. i + 4] = cast(ubyte[])"PK\x06\x06";
514             putUlong (i + 4,  eocd64Length - 12);
515             putUshort(i + 12, zip64ExtractVersion);
516             putUshort(i + 14, zip64ExtractVersion);
517             putUint  (i + 16, diskNumber);
518             putUint  (i + 20, diskStartDir);
519             putUlong (i + 24, numEntries);
520             putUlong (i + 32, totalEntries);
521             putUlong (i + 40, directorySize);
522             putUlong (i + 48, directoryOffset);
523             i += eocd64Length;
524 
525             // Write zip64 end of central directory record locator
526             _data[i .. i + 4] = cast(ubyte[])"PK\x06\x07";
527             putUint  (i + 4,  diskNumber);
528             putUlong (i + 8,  eocd64Offset);
529             putUint  (i + 16, 1);
530             i += eocd64LocLength;
531         }
532 
533         // Write end record
534         endrecOffset = i;
535         _data[i .. i + 4] = cast(ubyte[])"PK\x05\x06";
536         putUshort(i + 4,  cast(ushort) diskNumber);
537         putUshort(i + 6,  cast(ushort) diskStartDir);
538         putUshort(i + 8,  (numEntries > ushort.max ? ushort.max : cast(ushort) numEntries));
539         putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries));
540         putUint  (i + 12, directorySize);
541         putUint  (i + 16, directoryOffset);
542         putUshort(i + 20, cast(ushort) comment.length);
543         i += 22;
544 
545         // Write archive comment
546         assert(i + comment.length == data.length);
547         _data[i .. data.length] = (cast(ubyte[]) comment)[];
548 
549         return cast(void[]) data;
550     }
551 
552     /* ============ Reading an existing archive =================== */
553 
554     /**
555      * Constructor to use when reading an existing archive.
556      *
557      * Fills in the properties data[], diskNumber, diskStartDir, numEntries,
558      * totalEntries, comment[], and directory[].
559      * For each ArchiveMember, fills in
560      * properties madeVersion, extractVersion, flags, compressionMethod, time,
561      * crc32, compressedSize, expandedSize, compressedData[], diskNumber,
562      * internalAttributes, externalAttributes, name[], extra[], comment[].
563      * Use expand() to get the expanded data for each ArchiveMember.
564      *
565      * Params:
566      *  buffer = the entire contents of the archive.
567      */
568 
this(void[]buffer)569     this(void[] buffer)
570     {   uint iend;
571         uint i;
572         int endcommentlength;
573         uint directorySize;
574         uint directoryOffset;
575 
576         this._data = cast(ubyte[]) buffer;
577 
578         if (data.length > uint.max - 2)
579             throw new ZipException("zip files bigger than 4 GB are unsupported");
580 
581         // Find 'end record index' by searching backwards for signature
582         iend = (data.length > 66_000 ? to!uint(data.length - 66_000) : 0);
583         for (i = to!uint(data.length) - 22; 1; i--)
584         {
585             if (i < iend || i >= data.length)
586                 throw new ZipException("no end record");
587 
588             if (_data[i .. i + 4] == cast(ubyte[])"PK\x05\x06")
589             {
590                 endcommentlength = getUshort(i + 20);
591                 if (i + 22 + endcommentlength > data.length
592                         || i + 22 + endcommentlength < i)
593                     continue;
594                 comment = cast(string)(_data[i + 22 .. i + 22 + endcommentlength]);
595                 endrecOffset = i;
596 
597                 uint k = i - eocd64LocLength;
598                 if (k < i && _data[k .. k + 4] == cast(ubyte[])"PK\x06\x07")
599                 {
600                     _isZip64 = true;
601                     i = k;
602                 }
603 
604                 break;
605             }
606         }
607 
608         if (isZip64)
609         {
610             // Read Zip64 record data
611             ulong eocdOffset = getUlong(i + 8);
612             if (eocdOffset + eocd64Length > _data.length)
613                 throw new ZipException("corrupted directory");
614 
615             i = to!uint(eocdOffset);
616             if (_data[i .. i + 4] != cast(ubyte[])"PK\x06\x06")
617                 throw new ZipException("invalid Zip EOCD64 signature");
618 
619             ulong eocd64Size = getUlong(i + 4);
620             if (eocd64Size + i - 12 > data.length)
621                 throw new ZipException("invalid Zip EOCD64 size");
622 
623             _diskNumber = getUint(i + 16);
624             _diskStartDir = getUint(i + 20);
625 
626             ulong numEntriesUlong = getUlong(i + 24);
627             ulong totalEntriesUlong = getUlong(i + 32);
628             ulong directorySizeUlong = getUlong(i + 40);
629             ulong directoryOffsetUlong = getUlong(i + 48);
630 
631             if (numEntriesUlong > uint.max)
632                 throw new ZipException("supposedly more than 4294967296 files in archive");
633 
634             if (numEntriesUlong != totalEntriesUlong)
635                 throw new ZipException("multiple disk zips not supported");
636 
637             if (directorySizeUlong > i || directoryOffsetUlong > i
638                     || directorySizeUlong + directoryOffsetUlong > i)
639                 throw new ZipException("corrupted directory");
640 
641             _numEntries = to!uint(numEntriesUlong);
642             _totalEntries = to!uint(totalEntriesUlong);
643             directorySize = to!uint(directorySizeUlong);
644             directoryOffset = to!uint(directoryOffsetUlong);
645         }
646         else
647         {
648         // Read end record data
649         _diskNumber = getUshort(i + 4);
650         _diskStartDir = getUshort(i + 6);
651 
652         _numEntries = getUshort(i + 8);
653         _totalEntries = getUshort(i + 10);
654 
655         if (numEntries != totalEntries)
656             throw new ZipException("multiple disk zips not supported");
657 
658         directorySize = getUint(i + 12);
659         directoryOffset = getUint(i + 16);
660 
661         if (directoryOffset + directorySize > i)
662             throw new ZipException("corrupted directory");
663         }
664 
665         i = directoryOffset;
666         for (int n = 0; n < numEntries; n++)
667         {
668             /* The format of an entry is:
669              *  'PK' 1, 2
670              *  directory info
671              *  path
672              *  extra data
673              *  comment
674              */
675 
676             uint namelen;
677             uint extralen;
678             uint commentlen;
679 
680             if (_data[i .. i + 4] != cast(ubyte[])"PK\x01\x02")
681                 throw new ZipException("invalid directory entry 1");
682             ArchiveMember de = new ArchiveMember();
683             de._index = n;
684             de._madeVersion = getUshort(i + 4);
685             de._extractVersion = getUshort(i + 6);
686             de.flags = getUshort(i + 8);
687             de._compressionMethod = cast(CompressionMethod) getUshort(i + 10);
688             de.time = cast(DosFileTime) getUint(i + 12);
689             de._crc32 = getUint(i + 16);
690             de._compressedSize = getUint(i + 20);
691             de._expandedSize = getUint(i + 24);
692             namelen = getUshort(i + 28);
693             extralen = getUshort(i + 30);
694             commentlen = getUshort(i + 32);
695             de._diskNumber = getUshort(i + 34);
696             de.internalAttributes = getUshort(i + 36);
697             de._externalAttributes = getUint(i + 38);
698             de.offset = getUint(i + 42);
699             i += 46;
700 
701             if (i + namelen + extralen + commentlen > directoryOffset + directorySize)
702                 throw new ZipException("invalid directory entry 2");
703 
704             de.name = cast(string)(_data[i .. i + namelen]);
705             i += namelen;
706             de.extra = _data[i .. i + extralen];
707             i += extralen;
708             de.comment = cast(string)(_data[i .. i + commentlen]);
709             i += commentlen;
710 
711             immutable uint dataOffset = de.offset + 30 + namelen + extralen;
712             if (dataOffset + de.compressedSize > endrecOffset)
713                 throw new ZipException("Invalid directory entry offset or size.");
714             de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize];
715 
716             _directory[de.name] = de;
717 
718         }
719         if (i != directoryOffset + directorySize)
720             throw new ZipException("invalid directory entry 3");
721     }
722 
723     /*****
724      * Decompress the contents of archive member de and return the expanded
725      * data.
726      *
727      * Fills in properties extractVersion, flags, compressionMethod, time,
728      * crc32, compressedSize, expandedSize, expandedData[], name[], extra[].
729      */
expand(ArchiveMember de)730     ubyte[] expand(ArchiveMember de)
731     {   uint namelen;
732         uint extralen;
733 
734         if (_data[de.offset .. de.offset + 4] != cast(ubyte[])"PK\x03\x04")
735             throw new ZipException("invalid directory entry 4");
736 
737         // These values should match what is in the main zip archive directory
738         de._extractVersion = getUshort(de.offset + 4);
739         de.flags = getUshort(de.offset + 6);
740         de._compressionMethod = cast(CompressionMethod) getUshort(de.offset + 8);
741         de.time = cast(DosFileTime) getUint(de.offset + 10);
742         de._crc32 = getUint(de.offset + 14);
743         de._compressedSize = max(getUint(de.offset + 18), de.compressedSize);
744         de._expandedSize = max(getUint(de.offset + 22), de.expandedSize);
745         namelen = getUshort(de.offset + 26);
746         extralen = getUshort(de.offset + 28);
747 
748         debug(print)
749         {
750             printf("\t\texpandedSize = %d\n", de.expandedSize);
751             printf("\t\tcompressedSize = %d\n", de.compressedSize);
752             printf("\t\tnamelen = %d\n", namelen);
753             printf("\t\textralen = %d\n", extralen);
754         }
755 
756         if (de.flags & 1)
757             throw new ZipException("encryption not supported");
758 
759         int i;
760         i = de.offset + 30 + namelen + extralen;
761         if (i + de.compressedSize > endrecOffset)
762             throw new ZipException("invalid directory entry 5");
763 
764         de._compressedData = _data[i .. i + de.compressedSize];
765         debug(print) arrayPrint(de.compressedData);
766 
767         switch (de.compressionMethod)
768         {
769             case CompressionMethod.none:
770                 de._expandedData = de.compressedData;
771                 return de.expandedData;
772 
773             case CompressionMethod.deflate:
774                 // -15 is a magic value used to decompress zip files.
775                 // It has the effect of not requiring the 2 byte header
776                 // and 4 byte trailer.
777                 import std.zlib : uncompress;
778                 de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15);
779                 return de.expandedData;
780 
781             default:
782                 throw new ZipException("unsupported compression method");
783         }
784     }
785 
786     /* ============ Utility =================== */
787 
getUshort(int i)788     @safe ushort getUshort(int i)
789     {
790         ubyte[2] result = data[i .. i + 2];
791         return littleEndianToNative!ushort(result);
792     }
793 
getUint(int i)794     @safe uint getUint(int i)
795     {
796         ubyte[4] result = data[i .. i + 4];
797         return littleEndianToNative!uint(result);
798     }
799 
getUlong(int i)800     @safe ulong getUlong(int i)
801     {
802         ubyte[8] result = data[i .. i + 8];
803         return littleEndianToNative!ulong(result);
804     }
805 
putUshort(int i,ushort us)806     @safe void putUshort(int i, ushort us)
807     {
808         data[i .. i + 2] = nativeToLittleEndian(us);
809     }
810 
putUint(int i,uint ui)811     @safe void putUint(int i, uint ui)
812     {
813         data[i .. i + 4] = nativeToLittleEndian(ui);
814     }
815 
putUlong(int i,ulong ul)816     @safe void putUlong(int i, ulong ul)
817     {
818         data[i .. i + 8] = nativeToLittleEndian(ul);
819     }
820 }
821 
debug(print)822 debug(print)
823 {
824     @safe void arrayPrint(ubyte[] array)
825     {
826         printf("array %p,%d\n", cast(void*) array, array.length);
827         for (int i = 0; i < array.length; i++)
828         {
829             printf("%02x ", array[i]);
830             if (((i + 1) & 15) == 0)
831                 printf("\n");
832         }
833         printf("\n");
834     }
835 }
836 
837 @system unittest
838 {
839     // @system due to (at least) ZipArchive.build
840     auto zip1 = new ZipArchive();
841     auto zip2 = new ZipArchive();
842     auto am1 = new ArchiveMember();
843     am1.name = "foo";
844     am1.expandedData = new ubyte[](1024);
845     zip1.addMember(am1);
846     auto data1 = zip1.build();
847     zip2.addMember(zip1.directory["foo"]);
848     zip2.build();
849     auto am2 = zip2.directory["foo"];
850     zip2.expand(am2);
851     assert(am1.expandedData == am2.expandedData);
852     auto zip3 = new ZipArchive(data1);
853     zip3.build();
854     assert(zip3.directory["foo"].compressedSize == am1.compressedSize);
855 
856     // Test if packing and unpacking produces the original data
857     import std.conv, std.stdio;
858     import std.random : uniform, MinstdRand0;
859     MinstdRand0 gen;
860     const uint itemCount = 20, minSize = 10, maxSize = 500;
861     foreach (variant; 0 .. 2)
862     {
863         bool useZip64 = !!variant;
864         zip1 = new ZipArchive();
865         zip1.isZip64 = useZip64;
866         ArchiveMember[itemCount] ams;
867         foreach (i; 0 .. itemCount)
868         {
869             ams[i] = new ArchiveMember();
870             ams[i].name = to!string(i);
871             ams[i].expandedData = new ubyte[](uniform(minSize, maxSize));
872             foreach (ref ubyte c; ams[i].expandedData)
873                 c = cast(ubyte)(uniform(0, 256));
874             ams[i].compressionMethod = CompressionMethod.deflate;
875             zip1.addMember(ams[i]);
876         }
877         auto zippedData = zip1.build();
878         zip2 = new ZipArchive(zippedData);
879         assert(zip2.isZip64 == useZip64);
foreach(am;ams)880         foreach (am; ams)
881         {
882             am2 = zip2.directory[am.name];
883             zip2.expand(am2);
884             assert(am.crc32 == am2.crc32);
885             assert(am.expandedData == am2.expandedData);
886         }
887     }
888 }
889 
890 @system unittest
891 {
892     import std.conv : to;
893     import std.random : Mt19937, randomShuffle;
894     // Test if packing and unpacking preserves order.
895     auto rand = Mt19937(15966);
896     string[] names;
897     int value = 0;
898     // Generate a series of unique numbers as filenames.
899     foreach (i; 0 .. 20)
900     {
901         value += 1 + rand.front & 0xFFFF;
902         rand.popFront;
903         names ~= value.to!string;
904     }
905     // Insert them in a random order.
906     names.randomShuffle(rand);
907     auto zip1 = new ZipArchive();
foreach(i,name;names)908     foreach (i, name; names)
909     {
910         auto member = new ArchiveMember();
911         member.name = name;
912         member.expandedData = cast(ubyte[]) name;
913         member.index = cast(int) i;
914         zip1.addMember(member);
915     }
916     auto data = zip1.build();
917 
918     // Ensure that they appear in the same order.
919     auto zip2 = new ZipArchive(data);
foreach(i,name;names)920     foreach (i, name; names)
921     {
922         const member = zip2.directory[name];
923         assert(member.index == i, "member " ~ name ~ " had index " ~
924                 member.index.to!string ~ " but we expected index " ~ i.to!string ~
925                 ". The input array was " ~ names.to!string);
926     }
927 }
928 
929 @system unittest
930 {
931     import std.zlib;
932 
933     ubyte[] src = cast(ubyte[])
934 "the quick brown fox jumps over the lazy dog\r
935 the quick brown fox jumps over the lazy dog\r
936 ";
937     auto dst = cast(ubyte[]) compress(cast(void[]) src);
938     auto after = cast(ubyte[]) uncompress(cast(void[]) dst);
939     assert(src == after);
940 }
941 
942 @system unittest
943 {
944     // @system due to ZipArchive.build
945     import std.datetime;
946     ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9];
947 
948     auto ar = new ZipArchive;
949     auto am = new ArchiveMember;  // 10
950     am.name = "buf";
951     am.expandedData = buf;
952     am.compressionMethod = CompressionMethod.deflate;
953     am.time = SysTimeToDosFileTime(Clock.currTime());
954     ar.addMember(am);            // 15
955 
956     auto zip1 = ar.build();
957     auto arAfter = new ZipArchive(zip1);
958     assert(arAfter.directory.length == 1);
959     auto amAfter = arAfter.directory["buf"];
960     arAfter.expand(amAfter);
961     assert(amAfter.name == am.name);
962     assert(amAfter.expandedData == am.expandedData);
963     assert(amAfter.time == am.time);
964 }
965 
966 // Non-Android Posix-only, because we can't rely on the unzip command being
967 // available on Android or Windows
version(Android)968 version (Android) {} else
version(Posix)969 version (Posix) @system unittest
970 {
971     import std.datetime, std.file, std.format, std.path, std.process, std.stdio;
972 
973     auto zr = new ZipArchive();
974     auto am = new ArchiveMember();
975     am.compressionMethod = CompressionMethod.deflate;
976     am.name = "foo.bar";
977     am.time = SysTimeToDosFileTime(Clock.currTime());
978     am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine";
979     zr.addMember(am);
980     auto data2 = zr.build();
981 
982     mkdirRecurse(deleteme);
983     scope(exit) rmdirRecurse(deleteme);
984     string zipFile = buildPath(deleteme, "foo.zip");
985     std.file.write(zipFile, cast(byte[]) data2);
986 
987     auto result = executeShell(format("unzip -l %s", zipFile));
988     scope(failure) writeln(result.output);
989     assert(result.status == 0);
990 }
991