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