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