1 module squiz_box.box.zip;
2 
3 import squiz_box.c.zlib;
4 import squiz_box.box;
5 import squiz_box.squiz;
6 import squiz_box.priv;
7 
8 import std.exception;
9 import std.traits : isIntegral;
10 import std.range;
11 import std.stdio : File;
12 
13 auto createZipArchive(I)(I entries, size_t chunkSize = defaultChunkSize)
14         if (isCreateEntryRange!I)
15 {
16     return ZipArchiveCreate!I(entries, chunkSize);
17 }
18 
19 private struct ZipArchiveCreate(I)
20 {
21     private I entries;
22 
23     private ubyte[] outBuffer;
24     private ubyte[] outChunk;
25 
26     private ubyte[] localHeaderBuffer;
27     private ubyte[] currentLocalHeader;
28     private ulong localHeaderOffset;
29 
30     private Deflater deflater;
31     private ubyte[] currentDeflated;
32 
33     private ubyte[] centralHeaderBuffer;
34     private ubyte[] centralDirectory;
35     private ulong centralDirEntries;
36     private ulong centralDirOffset;
37     private ulong centralDirSize;
38 
39     private ubyte[] endOfCentralDirectory;
40     private bool endOfCentralDirReady;
41 
42     enum madeBy = 45;
43 
44     this(I entries, ulong chunkSize)
45     {
46         this.entries = entries;
47         outBuffer = new ubyte[chunkSize];
48         deflater = new Deflater;
49 
50         static if (isForwardRange!I)
51         {
52             preallocate(entries.save);
53         }
54 
55         prime();
56     }
57 
58     @property bool empty()
59     {
60         return outChunk.length == 0;
61     }
62 
63     @property ByteChunk front()
64     {
65         return outChunk;
66     }
67 
68     void popFront()
69     {
70         prime();
71     }
72 
73     private void preallocate(I entries)
74     {
75         import std.algorithm : max;
76 
77         size_t maxHeaderSize;
78         size_t centralDirectorySize;
79 
80         foreach (entry; entries)
81         {
82             // Note: this do not check for Zip64 extra field.
83             // more header allocation will be needed if Zip64 extra field is needed.
84 
85             version (Posix)
86                 size_t extraFieldLength = UnixExtraField.computeTotalLength(entry.linkname);
87             else
88                 size_t extraFieldLength;
89 
90             const path = entry.path;
91 
92             maxHeaderSize = max(
93                 maxHeaderSize,
94                 LocalFileHeader.computeTotalLength(path, null) +
95                     extraFieldLength +
96                     SquizBoxExtraField.sizeof
97             );
98             centralDirectorySize += CentralFileHeader.computeTotalLength(path, null, null) + extraFieldLength;
99         }
100 
101         auto buf = new ubyte[maxHeaderSize + centralDirectorySize];
102         localHeaderBuffer = buf[0 .. maxHeaderSize];
103         centralHeaderBuffer = buf[maxHeaderSize .. $];
104     }
105 
106     private void processNextEntry()
107     in (!entries.empty)
108     {
109         import std.datetime.systime : SysTimeToDosFileTime;
110 
111         auto entry = entries.front;
112 
113         deflater.deflateEntry(entry.byChunk());
114         currentDeflated = deflater.deflated;
115 
116         string path = entry.path;
117 
118         version (Windows)
119         {
120             import std.string : replace;
121 
122             path = replace(path, '\\', '/');
123         }
124 
125         ushort extractVersion = 20;
126         bool zip64;
127 
128         ExtraFieldInfo efInfo;
129 
130         if (deflater.inflatedSize >= 0xffff_ffff ||
131             currentDeflated.length >= 0xffff_ffff ||
132             localHeaderOffset >= 0xffff_ffff)
133         {
134             zip64 = true;
135             extractVersion = madeBy;
136             efInfo.addZip64(deflater.inflatedSize, currentDeflated.length, localHeaderOffset);
137         }
138         version (Posix)
139         {
140             efInfo.addUnix(entry.linkname, entry.timeLastModified, entry.ownerId, entry.groupId);
141         }
142         efInfo.addSquizBox(entry.attributes);
143 
144         const localExtraFieldData = efInfo.toZipData();
145         const localHeaderLength = LocalFileHeader.computeTotalLength(path, localExtraFieldData);
146 
147         const centralExtraFieldData = localExtraFieldData[0 .. $ - SquizBoxExtraField.sizeof];
148         const centralHeaderLength = CentralFileHeader.computeTotalLength(path, centralExtraFieldData, null);
149 
150         if (localHeaderBuffer.length < localHeaderLength)
151             localHeaderBuffer.length = localHeaderLength;
152         centralHeaderBuffer.length += centralHeaderLength;
153 
154         LocalFileHeader local = void;
155         local.signature = LocalFileHeader.expectedSignature;
156         local.extractVersion = extractVersion;
157         local.flag = 0;
158         local.compressionMethod = 8;
159         local.lastModDosTime = SysTimeToDosFileTime(entry.timeLastModified);
160         local.crc32 = deflater.calculatedCrc32;
161         local.compressedSize = zip64 ? 0xffff_ffff : cast(uint) currentDeflated.length;
162         local.uncompressedSize = zip64 ? 0xffff_ffff : cast(uint) deflater.inflatedSize;
163         // TODO: use store instead of deflate if smaller
164         local.fileNameLength = cast(ushort) path.length;
165         local.extraFieldLength = cast(ushort) localExtraFieldData.length;
166         currentLocalHeader = local.writeTo(localHeaderBuffer, path, localExtraFieldData);
167 
168         version (Posix)
169         {
170             const versionMadeBy = madeBy | 0x0300;
171             const externalAttributes = (entry.attributes & 0xffff) << 16;
172         }
173         else
174         {
175             const versionMadeBy = madeBy;
176             const externalAttributes = entry.attributes;
177         }
178 
179         CentralFileHeader central = void;
180         central.signature = CentralFileHeader.expectedSignature;
181         central.versionMadeBy = versionMadeBy;
182         central.extractVersion = extractVersion;
183         central.flag = 0;
184         central.compressionMethod = 8;
185         central.lastModDosTime = SysTimeToDosFileTime(entry.timeLastModified);
186         central.crc32 = deflater.calculatedCrc32;
187         central.compressedSize = zip64 ? 0xffff_ffff : cast(uint) currentDeflated.length;
188         central.uncompressedSize = zip64 ? 0xffff_ffff : cast(uint) deflater.inflatedSize;
189         central.fileNameLength = cast(ushort) path.length;
190         central.extraFieldLength = cast(ushort) centralExtraFieldData.length;
191         central.fileCommentLength = 0;
192         central.diskNumberStart = 0;
193         central.internalFileAttributes = 0;
194         central.externalFileAttributes = externalAttributes;
195         central.relativeLocalHeaderOffset = zip64 ? 0xffff_ffff : cast(uint) localHeaderOffset;
196         central.writeTo(
197             centralHeaderBuffer[centralDirectory.length .. centralDirectory.length + centralHeaderLength],
198             path, centralExtraFieldData, null
199         );
200 
201         const entryLen = localHeaderLength + currentDeflated.length;
202         localHeaderOffset += entryLen;
203         centralDirectory = centralHeaderBuffer[0 .. centralDirectory.length + centralHeaderLength];
204 
205         centralDirEntries += 1;
206         centralDirOffset += entryLen;
207         centralDirSize += centralHeaderLength;
208 
209         entries.popFront();
210     }
211 
212     private void prepareEndOfCentralDir()
213     {
214         const zip64 = centralDirEntries >= 0xffff || centralDirSize >= 0xffff_ffff || centralDirOffset >= 0xffff_ffff;
215 
216         auto len = EndOfCentralDirectory.sizeof;
217         if (zip64)
218             len += Zip64EndOfCentralDirRecord.sizeof + Zip64EndOfCentralDirLocator.sizeof;
219 
220         endOfCentralDirectory = new ubyte[len];
221         size_t offset;
222 
223         if (zip64)
224         {
225             auto record = cast(Zip64EndOfCentralDirRecord*)&endOfCentralDirectory[offset];
226             record.signature = Zip64EndOfCentralDirRecord.expectedSignature;
227             record.zip64EndOfCentralDirRecordSize = Zip64EndOfCentralDirRecord.sizeof - 12;
228             version (Posix)
229                 record.versionMadeBy = madeBy | 0x0300;
230             else
231                 record.versionMadeBy = madeBy;
232             record.extractVersion = madeBy;
233             record.centralDirEntriesOnThisDisk = centralDirEntries;
234             record.centralDirEntries = centralDirEntries;
235             record.centralDirSize = centralDirSize;
236             record.centralDirOffset = centralDirOffset;
237             offset += Zip64EndOfCentralDirRecord.sizeof;
238 
239             auto locator = cast(Zip64EndOfCentralDirLocator*)&endOfCentralDirectory[offset];
240             locator.signature = Zip64EndOfCentralDirLocator.expectedSignature;
241             locator.zip64EndOfCentralDirRecordOffset = centralDirOffset + centralDirSize;
242             offset += Zip64EndOfCentralDirLocator.sizeof;
243         }
244 
245         auto footer = cast(EndOfCentralDirectory*)&endOfCentralDirectory[offset];
246         footer.signature = EndOfCentralDirectory.expectedSignature;
247         footer.centralDirEntriesOnThisDisk = zip64 ? 0xffff : cast(ushort) centralDirEntries;
248         footer.centralDirEntries = zip64 ? 0xffff : cast(ushort) centralDirEntries;
249         footer.centralDirSize = zip64 ? 0xffff_ffff : cast(uint) centralDirSize;
250         footer.centralDirOffset = zip64 ? 0xffff_ffff : cast(uint) centralDirOffset;
251 
252         endOfCentralDirReady = true;
253     }
254 
255     private bool needNextEntry()
256     {
257         return currentLocalHeader.length == 0 && currentDeflated.length == 0;
258     }
259 
260     private void prime()
261     {
262         import std.algorithm : min;
263 
264         ubyte[] outAvail = outBuffer;
265 
266         void writeOut(ref ubyte[] inBuffer)
267         {
268             const len = min(inBuffer.length, outAvail.length);
269             outAvail[0 .. len] = inBuffer[0 .. len];
270             outAvail = outAvail[len .. $];
271             inBuffer = inBuffer[len .. $];
272         }
273 
274         while (outAvail.length)
275         {
276             if (needNextEntry() && !entries.empty)
277                 processNextEntry();
278 
279             if (currentLocalHeader.length)
280             {
281                 writeOut(currentLocalHeader);
282                 continue;
283             }
284 
285             if (currentDeflated.length)
286             {
287                 writeOut(currentDeflated);
288                 continue;
289             }
290 
291             assert(entries.empty);
292 
293             if (centralDirectory.length)
294             {
295                 writeOut(centralDirectory);
296                 continue;
297             }
298 
299             if (deflater)
300             {
301                 deflater.end();
302             }
303 
304             if (!endOfCentralDirReady)
305             {
306                 prepareEndOfCentralDir();
307             }
308 
309             if (endOfCentralDirectory.length)
310             {
311                 writeOut(endOfCentralDirectory);
312                 continue;
313             }
314 
315             break;
316         }
317 
318         outChunk = outBuffer[0 .. $ - outAvail.length];
319     }
320 }
321 
322 // Deflates entries successively while reusing the allocated resources from one entry to the next.
323 // deflateBuffer, deflated, inflatedSize and crc are invalidated each time deflateEntry is called
324 private class Deflater
325 {
326     Deflate algo;
327     StreamType!Deflate stream;
328 
329     // buffer that receive compressed data. Only grows from one entry to the next
330     ubyte[] deflateBuffer;
331     // slice of buffer that contains compressed data of the last entry.
332     ubyte[] deflated;
333     // unompressed size of the last entry
334     ulong inflatedSize;
335     // CRC32 checksum of the last entry
336     uint calculatedCrc32;
337 
338     this()
339     {
340         algo.format = ZlibFormat.raw;
341     }
342 
343     void end()
344     {
345         algo.end(stream);
346     }
347 
348     void deflateEntry(ByteRange input)
349     {
350         if (!stream)
351         {
352             stream = algo.initialize();
353             // arbitrary initial buffer size
354             deflateBuffer = new ubyte[64 * 1024];
355         }
356         else
357         {
358             // the stream was used, we have to reset it
359             algo.reset(stream);
360             deflated = null;
361             inflatedSize = 0;
362         }
363 
364         calculatedCrc32 = crc32(0, null, 0);
365 
366         while (true)
367         {
368             if (stream.input.length == 0 && !input.empty)
369             {
370                 auto inp = input.front;
371                 inflatedSize += inp.length;
372                 calculatedCrc32 = crc32(calculatedCrc32, inp.ptr, cast(uint)(inp.length));
373                 stream.input = inp;
374             }
375 
376             if (deflated.length == deflateBuffer.length)
377             {
378                 deflateBuffer.length += 8192;
379                 deflated = deflateBuffer[0 .. deflated.length];
380             }
381 
382             stream.output = deflateBuffer[deflated.length .. $];
383 
384             const ended = algo.process(stream, cast(Flag!"lastChunk")input.empty);
385 
386             deflated = deflateBuffer[0 .. $ - stream.output.length];
387 
388             if (stream.input.length == 0 && !input.empty)
389                 input.popFront();
390 
391             if (ended)
392                 break;
393         }
394     }
395 }
396 
397 auto readZipArchive(I)(I input) if (isByteRange!I)
398 {
399     auto stream = new ByteRangeCursor!I(input);
400     return ZipArchiveRead!Cursor(stream);
401 }
402 
403 auto readZipArchive(File input)
404 {
405     auto stream = new FileCursor(input);
406     return ZipArchiveRead!SearchableCursor(stream);
407 }
408 
409 auto readZipArchive(ubyte[] zipData)
410 {
411     auto stream = new ArrayCursor(zipData);
412     return ZipArchiveRead!SearchableCursor(stream);
413 }
414 
415 private struct ZipArchiveRead(C) if (is(C : Cursor))
416 {
417     enum isSearchable = is(C : SearchableCursor);
418 
419     private C input;
420     private ArchiveExtractEntry currentEntry;
421     ubyte[] fieldBuf;
422     ulong nextHeader;
423 
424     static if (isSearchable)
425     {
426         struct CentralDirInfo
427         {
428             ulong numEntries;
429             ulong pos;
430             ulong size;
431         }
432 
433         ZipEntryInfo[string] centralDirectory;
434     }
435 
436     this(C input)
437     {
438         this.input = input;
439         fieldBuf = new ubyte[ushort.max];
440 
441         static if (isSearchable)
442         {
443             readCentralDirectory();
444         }
445 
446         readEntry();
447     }
448 
449     @property bool empty()
450     {
451         return !currentEntry;
452     }
453 
454     @property ArchiveExtractEntry front()
455     {
456         return currentEntry;
457     }
458 
459     void popFront()
460     {
461         assert(input.pos <= nextHeader);
462 
463         if (input.pos < nextHeader)
464         {
465             // the current entry was not fully read, we move the stream forward
466             // up to the next header
467             const dist = nextHeader - input.pos;
468             input.ffw(dist);
469         }
470         currentEntry = null;
471         readEntry();
472     }
473 
474     static if (isSearchable)
475     {
476         private void readCentralDirectory()
477         {
478             import std.datetime.systime : DosFileTimeToSysTime;
479 
480             auto cdi = readCentralDirInfo();
481             input.seek(cdi.pos);
482 
483             while (cdi.numEntries != 0)
484             {
485                 CentralFileHeader header = void;
486                 input.readValue(&header);
487                 enforce(
488                     header.signature == CentralFileHeader.expectedSignature,
489                     "Corrupted Zip: Expected Central directory header"
490                 );
491 
492                 ZipEntryInfo info = void;
493 
494                 info.path = cast(string) input.read(fieldBuf[0 .. header.fileNameLength.val]).idup;
495                 enforce(info.path.length == header.fileNameLength.val, "Unexpected end of input");
496 
497                 const extraFieldData = input.read(fieldBuf[0 .. header.extraFieldLength.val]);
498                 enforce(extraFieldData.length == header.extraFieldLength.val, "Unexpected end of input");
499 
500                 const efInfo = ExtraFieldInfo.parse(extraFieldData);
501 
502                 if (header.fileCommentLength.val)
503                     input.ffw(header.fileCommentLength.val);
504 
505                 fillEntryInfo(info, efInfo, header);
506 
507                 // will be added later to entrySize: LocalFileHeader size + name and extra fields
508                 info.entrySize = info.compressedSize +
509                     CentralFileHeader.sizeof +
510                     header.fileNameLength.val +
511                     header.extraFieldLength.val +
512                     header.fileCommentLength.val;
513 
514                 version (Posix)
515                 {
516                     if ((header.versionMadeBy.val & 0xff00) == 0x0300)
517                         info.attributes = header.externalFileAttributes.val >> 16;
518                     else
519                         info.attributes = 0;
520                 }
521                 else
522                 {
523                     if ((header.versionMadeBy.val & 0xff00) == 0x0000)
524                         info.attributes = header.externalFileAttributes.val;
525                     else
526                         info.attributes = 0;
527                 }
528 
529                 cdi.numEntries -= 1;
530 
531                 centralDirectory[info.path] = info;
532             }
533 
534             input.seek(0);
535         }
536 
537         private CentralDirInfo readCentralDirInfo()
538         {
539             import std.algorithm : max;
540 
541             enforce(
542                 input.size > EndOfCentralDirectory.sizeof, "Not a Zip file"
543             );
544             ulong pos = input.size - EndOfCentralDirectory.sizeof;
545             enum maxCommentSz = 0xffff;
546             const ulong stopSearch = max(pos, maxCommentSz) - maxCommentSz;
547             while (pos != stopSearch)
548             {
549                 input.seek(pos);
550                 EndOfCentralDirectory record = void;
551                 input.readValue(&record);
552                 if (record.signature == EndOfCentralDirectory.expectedSignature)
553                 {
554                     enforce(
555                         record.thisDisk == 0 && record.centralDirDisk == 0,
556                         "multi-disk Zip archives are not supported"
557                     );
558                     if (record.centralDirEntries == 0xffff ||
559                         record.centralDirOffset == 0xffff_ffff ||
560                         record.centralDirSize == 0xffff_ffff)
561                     {
562                         return readZip64CentralDirInfo(pos);
563                     }
564                     return CentralDirInfo(
565                         record.centralDirEntries.val,
566                         record.centralDirOffset.val,
567                         record.centralDirSize.val,
568                     );
569                 }
570                 // we are likely in the zip file comment.
571                 // we continue backward until we hit the signature
572                 // of the end of central directory record
573                 pos -= 1;
574             }
575             throw new Exception("Corrupted Zip: Could not find end of central directory record");
576         }
577 
578         private CentralDirInfo readZip64CentralDirInfo(size_t endCentralDirRecordPos)
579         {
580             enforce(
581                 endCentralDirRecordPos > Zip64EndOfCentralDirLocator.sizeof,
582                 "Corrupted Zip: Not enough bytes"
583             );
584 
585             input.seek(endCentralDirRecordPos - Zip64EndOfCentralDirLocator.sizeof);
586             Zip64EndOfCentralDirLocator locator = void;
587             input.readValue(&locator);
588             enforce(
589                 locator.signature == Zip64EndOfCentralDirLocator.expectedSignature,
590                 "Corrupted Zip: Expected Zip64 end of central directory locator"
591             );
592 
593             input.seek(locator.zip64EndOfCentralDirRecordOffset.val);
594             Zip64EndOfCentralDirRecord record = void;
595             input.readValue(&record);
596             enforce(
597                 record.signature == Zip64EndOfCentralDirRecord.expectedSignature,
598                 "Corrupted Zip: Expected Zip64 end of central directory record"
599             );
600 
601             return CentralDirInfo(
602                 record.centralDirEntries.val,
603                 record.centralDirOffset.val,
604                 record.centralDirSize.val,
605             );
606         }
607     }
608 
609     private void fillEntryInfo(H)(ref ZipEntryInfo info, const ref ExtraFieldInfo efInfo, const ref H header)
610             if (is(H == LocalFileHeader) || is(H == CentralFileHeader))
611     {
612         const flag = cast(ZipFlag) header.flag.val;
613         enforce(
614             (flag & ZipFlag.encryption) == ZipFlag.none,
615             "Zip encryption unsupported"
616         );
617         enforce(
618             (flag & ZipFlag.dataDescriptor) == ZipFlag.none,
619             "Zip format unsupported (data descriptor)"
620         );
621         enforce(
622             header.compressionMethod.val == 0 || header.compressionMethod.val == 8,
623             "Unsupported Zip compression method"
624         );
625 
626         info.deflated = header.compressionMethod.val == 8;
627         info.expectedCrc32 = header.crc32.val;
628 
629         if (efInfo.has(KnownExtraField.zip64))
630         {
631             info.size = efInfo.uncompressedSize;
632             info.compressedSize = efInfo.compressedSize;
633         }
634         else
635         {
636             info.size = header.uncompressedSize.val;
637             info.compressedSize = header.compressedSize.val;
638         }
639 
640         if (efInfo.has(KnownExtraField.squizBox))
641         {
642             info.attributes = efInfo.attributes;
643         }
644 
645         info.type = info.compressedSize == 0 ? EntryType.directory : EntryType.regular;
646 
647         version (Posix)
648         {
649             if (efInfo.has(KnownExtraField.unix))
650             {
651                 info.linkname = efInfo.linkname;
652                 if (info.linkname)
653                     info.type = EntryType.symlink;
654                 info.timeLastModified = efInfo.timeLastModified;
655                 info.ownerId = efInfo.ownerId;
656                 info.groupId = efInfo.groupId;
657             }
658             else
659             {
660                 info.timeLastModified = DosFileTimeToSysTime(header.lastModDosTime.val);
661             }
662         }
663         else
664         {
665             info.timeLastModified = DosFileTimeToSysTime(header.lastModDosTime.val);
666         }
667 
668     }
669 
670     private void readEntry()
671     {
672         import std.datetime.systime : DosFileTimeToSysTime, unixTimeToStdTime, SysTime;
673 
674         LocalFileHeader header = void;
675         input.readValue(&header);
676         if (header.signature == CentralFileHeader.expectedSignature)
677         {
678             // last entry was consumed
679             input.ffw(ulong.max);
680             return;
681         }
682 
683         enforce(
684             header.signature == LocalFileHeader.expectedSignature,
685             "Corrupted Zip: Expected a Zip local header signature."
686         );
687 
688         // TODO check for presence of encryption header and data descriptor
689         const path = cast(string) input.read(fieldBuf[0 .. header.fileNameLength.val]).idup;
690         enforce(path.length == header.fileNameLength.val, "Unexpected end of input");
691 
692         const extraFieldData = input.read(fieldBuf[0 .. header.extraFieldLength.val]);
693         enforce(extraFieldData.length == header.extraFieldLength.val, "Unexpected end of input");
694 
695         const efInfo = ExtraFieldInfo.parse(extraFieldData);
696 
697         static if (isSearchable)
698         {
699             auto info = centralDirectory[path];
700             info.entrySize += header.totalLength();
701         }
702         else
703         {
704             ZipEntryInfo info;
705             info.path = path;
706             fillEntryInfo(info, efInfo, header);
707             // educated guess for the size in the central directory
708             info.entrySize = header.totalLength() +
709                 info.compressedSize +
710                 CentralFileHeader.sizeof +
711                 path.length +
712                 extraFieldData.length;
713             if (efInfo.has(KnownExtraField.squizBox))
714             {
715                 // central directory do not have squiz box extra field
716                 info.entrySize -= SquizBoxExtraField.sizeof;
717             }
718         }
719 
720         nextHeader = input.pos + info.compressedSize;
721 
722         currentEntry = new ZipArchiveExtractEntry!C(input, info);
723     }
724 }
725 
726 private struct ZipEntryInfo
727 {
728     string path;
729     string linkname;
730     EntryType type;
731     ulong size;
732     ulong entrySize;
733     ulong compressedSize;
734     SysTime timeLastModified;
735     uint attributes;
736     bool deflated;
737     uint expectedCrc32;
738 
739     version (Posix)
740     {
741         int ownerId;
742         int groupId;
743     }
744 }
745 
746 private enum KnownExtraField
747 {
748     none = 0,
749     zip64 = 1,
750     unix = 2,
751     squizBox = 4,
752 }
753 
754 private struct ExtraFieldInfo
755 {
756     KnownExtraField fields;
757 
758     // zip64
759     ulong uncompressedSize;
760     ulong compressedSize;
761     ulong localHeaderPos;
762 
763     // unix
764     version (Posix)
765     {
766         string linkname;
767         SysTime timeLastModified;
768         int ownerId;
769         int groupId;
770     }
771 
772     // squizBox
773     uint attributes;
774 
775     bool has(KnownExtraField f) const
776     {
777         return (fields & f) != KnownExtraField.none;
778     }
779 
780     void addZip64(ulong uncompressedSize, ulong compressedSize, ulong localHeaderPos)
781     {
782         fields |= KnownExtraField.zip64;
783         this.uncompressedSize = uncompressedSize;
784         this.compressedSize = compressedSize;
785         this.localHeaderPos = localHeaderPos;
786     }
787 
788     version (Posix)
789     {
790         void addUnix(string linkname, SysTime timeLastModified, int ownerId, int groupId)
791         {
792             fields |= KnownExtraField.unix;
793             this.linkname = linkname;
794             this.timeLastModified = timeLastModified;
795             this.ownerId = ownerId;
796             this.groupId = groupId;
797         }
798     }
799 
800     void addSquizBox(uint attributes)
801     {
802         fields |= KnownExtraField.squizBox;
803         this.attributes = attributes;
804     }
805 
806     size_t computeLength()
807     {
808         size_t sz;
809 
810         if (has(KnownExtraField.zip64))
811             sz += Zip64ExtraField.sizeof;
812         version (Posix)
813         {
814             if (has(KnownExtraField.unix))
815                 sz += UnixExtraField.computeTotalLength(linkname);
816         }
817         if (has(KnownExtraField.squizBox))
818             sz += SquizBoxExtraField.sizeof;
819 
820         return sz;
821     }
822 
823     static ExtraFieldInfo parse(const(ubyte)[] data)
824     {
825         ExtraFieldInfo info;
826 
827         while (data.length != 0)
828         {
829             enforce(data.length >= 4, "Corrupted Zip File (incomplete extra-field)");
830 
831             auto header = cast(const(ExtraFieldHeader)*) data.ptr;
832 
833             const efLen = header.size.val + 4;
834             enforce(data.length >= efLen, "Corrupted Zip file (incomplete extra-field)");
835 
836             switch (header.id.val)
837             {
838             case Zip64ExtraField.expectedId:
839                 info.fields |= KnownExtraField.zip64;
840                 auto ef = cast(Zip64ExtraField*) data.ptr;
841                 info.uncompressedSize = ef.uncompressedSize.val;
842                 info.compressedSize = ef.compressedSize.val;
843                 info.localHeaderPos = ef.localHeaderPos.val;
844                 break;
845             case SquizBoxExtraField.expectedId:
846                 info.fields |= KnownExtraField.squizBox;
847                 auto ef = cast(SquizBoxExtraField*) data.ptr;
848                 info.attributes = ef.attributes.val;
849                 break;
850                 // dfmt off
851             version (Posix)
852             {
853                 case UnixExtraField.expectedId:
854                     info.fields |= KnownExtraField.unix;
855                     auto ef = cast(UnixExtraField*) data.ptr;
856                     info.timeLastModified = SysTime(unixTimeToStdTime(ef.mtime.val));
857                     info.ownerId = ef.uid.val;
858                     info.groupId = ef.gid.val;
859                     if (efLen > UnixExtraField.sizeof)
860                     {
861                         info.linkname = cast(string)
862                             data[UnixExtraField.sizeof .. efLen].idup;
863                     }
864                     break;
865             }
866             // dfmt on
867             default:
868                 break;
869             }
870 
871             data = data[efLen .. $];
872         }
873 
874         return info;
875     }
876 
877     ubyte[] toZipData()
878     {
879         const sz = computeLength();
880 
881         auto data = new ubyte[sz];
882         size_t pos;
883 
884         if (has(KnownExtraField.zip64))
885         {
886             auto f = cast(Zip64ExtraField*)&data[pos];
887             f.id = Zip64ExtraField.expectedId;
888             f.size = Zip64ExtraField.sizeof - 4;
889             f.uncompressedSize = uncompressedSize;
890             f.compressedSize = compressedSize;
891             f.localHeaderPos = localHeaderPos;
892             f.diskStartNumber = 0;
893             pos += Zip64ExtraField.sizeof;
894         }
895         version (Posix)
896         {
897             if (has(KnownExtraField.unix))
898             {
899                 import std.datetime.systime : Clock, stdTimeToUnixTime;
900 
901                 auto f = cast(UnixExtraField*)&data[pos];
902                 f.id = UnixExtraField.expectedId;
903                 f.size = cast(ushort)(UnixExtraField.sizeof - 4 + linkname.length);
904                 f.atime = stdTimeToUnixTime!int(Clock.currStdTime);
905                 f.mtime = stdTimeToUnixTime!int(timeLastModified.stdTime);
906                 f.uid = cast(ushort) ownerId;
907                 f.gid = cast(ushort) groupId;
908                 pos += UnixExtraField.sizeof;
909                 if (linkname.length)
910                 {
911                     data[pos .. pos + linkname.length] = cast(const(ubyte)[]) linkname;
912                     pos += linkname.length;
913                 }
914             }
915         }
916         if (has(KnownExtraField.squizBox))
917         {
918             auto f = cast(SquizBoxExtraField*)&data[pos];
919             f.id = SquizBoxExtraField.expectedId;
920             f.size = SquizBoxExtraField.sizeof - 4;
921             f.attributes = attributes;
922             pos += SquizBoxExtraField.sizeof;
923         }
924 
925         assert(pos == sz);
926         return data;
927     }
928 }
929 
930 private class ZipArchiveExtractEntry(C) : ArchiveExtractEntry if (is(C : Cursor))
931 {
932     enum isSearchable = is(C : SearchableCursor);
933 
934     C input;
935     ulong startPos;
936     ZipEntryInfo info;
937 
938     this(C input, ZipEntryInfo info)
939     {
940         this.input = input;
941         this.startPos = input.pos;
942         this.info = info;
943     }
944 
945     @property EntryMode mode()
946     {
947         return EntryMode.extraction;
948     }
949 
950     @property string path()
951     {
952         return info.path;
953     }
954 
955     @property EntryType type()
956     {
957         return info.type;
958     }
959 
960     @property string linkname()
961     {
962         return info.linkname;
963     }
964 
965     @property ulong size()
966     {
967         return info.size;
968     }
969 
970     @property ulong entrySize()
971     {
972         return info.entrySize;
973     }
974 
975     @property SysTime timeLastModified()
976     {
977         return info.timeLastModified;
978     }
979 
980     @property uint attributes()
981     {
982         return info.attributes;
983     }
984 
985     version (Posix)
986     {
987         @property int ownerId()
988         {
989             return info.ownerId;
990         }
991 
992         @property int groupId()
993         {
994             return info.groupId;
995         }
996     }
997 
998     ByteRange byChunk(size_t chunkSize)
999     {
1000         static if (!isSearchable)
1001             enforce(
1002                 input.pos == startPos,
1003                 "Data cursor has moved, this entry is not valid anymore"
1004             );
1005 
1006         if (info.deflated)
1007             return new InflateByChunk!C(input, startPos, info.compressedSize, chunkSize, info.expectedCrc32);
1008         else
1009             return new StoredByChunk!C(input, startPos, info.compressedSize, chunkSize, info.expectedCrc32);
1010     }
1011 }
1012 
1013 /// common code between InflateByChunk and StoredByChunk
1014 private abstract class ZipByChunk : ByteRange
1015 {
1016     ubyte[] moveFront()
1017     {
1018         throw new Exception(
1019             "Cannot move the front of a(n) Zip `Inflater`"
1020         );
1021     }
1022 
1023     int opApply(scope int delegate(ByteChunk) dg)
1024     {
1025         int res;
1026 
1027         while (!empty)
1028         {
1029             res = dg(front);
1030             if (res)
1031                 break;
1032             popFront();
1033         }
1034 
1035         return res;
1036     }
1037 
1038     int opApply(scope int delegate(size_t, ByteChunk) dg)
1039     {
1040         int res;
1041 
1042         size_t i = 0;
1043 
1044         while (!empty)
1045         {
1046             res = dg(i, front);
1047             if (res)
1048                 break;
1049             i++;
1050             popFront();
1051         }
1052 
1053         return res;
1054     }
1055 }
1056 
1057 /// implements byChunk for stored entries (no compression)
1058 private class StoredByChunk(C) : ZipByChunk if (is(C : Cursor))
1059 {
1060     enum isSearchable = is(C : SearchableCursor);
1061 
1062     C input;
1063     ulong currentPos;
1064     ulong size;
1065     ubyte[] chunkBuffer;
1066     ubyte[] chunk;
1067     uint calculatedCrc32;
1068     uint expectedCrc32;
1069     bool ended;
1070 
1071     this(C input, ulong startPos, ulong size, size_t chunkSize, uint expectedCrc32)
1072     {
1073         static if (!isSearchable)
1074             assert(input.pos == startPos);
1075 
1076         this.input = input;
1077         this.currentPos = startPos;
1078         this.size = size;
1079         this.chunkBuffer = new ubyte[chunkSize];
1080         this.expectedCrc32 = expectedCrc32;
1081 
1082         this.calculatedCrc32 = crc32(0, null, 0);
1083 
1084         prime();
1085     }
1086 
1087     @property bool empty()
1088     {
1089         return size == 0 && chunk.length == 0;
1090     }
1091 
1092     @property ByteChunk front()
1093     {
1094         return chunk;
1095     }
1096 
1097     void popFront()
1098     {
1099         chunk = null;
1100         if (!ended)
1101             prime();
1102     }
1103 
1104     private void prime()
1105     {
1106         import std.algorithm : min;
1107 
1108         static if (isSearchable)
1109             input.seek(currentPos);
1110         else
1111             enforce(input.pos == currentPos,
1112                 "Data cursor has moved. Entry is no longer valid."
1113             );
1114 
1115         const len = min(size, chunkBuffer.length);
1116         chunk = input.read(chunkBuffer[0 .. len]);
1117         enforce(chunk.length == len, "Corrupted Zip file: unexpected end of input");
1118         currentPos += len;
1119         size -= len;
1120 
1121         calculatedCrc32 = crc32(calculatedCrc32, chunk.ptr, cast(uint) len);
1122 
1123         if (size == 0)
1124         {
1125             ended = true;
1126             enforce(
1127                 calculatedCrc32 == expectedCrc32,
1128                 "Corrupted Zip file: Wrong CRC32 checkum"
1129             );
1130         }
1131     }
1132 }
1133 
1134 /// implements byChunk for deflated entries
1135 private class InflateByChunk(C) : ZipByChunk if (is(C : Cursor))
1136 {
1137     enum isSearchable = is(C : SearchableCursor);
1138 
1139     Inflate algo;
1140     StreamType!Inflate stream;
1141     C input;
1142     ulong currentPos;
1143     ulong compressedSz;
1144     ubyte[] chunkBuffer;
1145     ubyte[] chunk;
1146     ubyte[] inBuffer;
1147     uint calculatedCrc32;
1148     uint expectedCrc32;
1149     Flag!"streamEnded" ended;
1150 
1151     this(C input, ulong startPos, ulong compressedSz, size_t chunkSize, uint expectedCrc32)
1152     {
1153         static if (!isSearchable)
1154             assert(input.pos == startPos);
1155 
1156         this.input = input;
1157         this.currentPos = startPos;
1158         this.compressedSz = compressedSz;
1159         this.chunkBuffer = new ubyte[chunkSize];
1160         this.inBuffer = new ubyte[defaultChunkSize];
1161         this.expectedCrc32 = expectedCrc32;
1162 
1163         this.calculatedCrc32 = crc32(0, null, 0);
1164 
1165         algo.format = ZlibFormat.raw;
1166         stream = algo.initialize();
1167 
1168         prime();
1169     }
1170 
1171     @property bool empty()
1172     {
1173         return compressedSz == 0 && chunk.length == 0;
1174     }
1175 
1176     @property ByteChunk front()
1177     {
1178         return chunk;
1179     }
1180 
1181     void popFront()
1182     {
1183         chunk = null;
1184         if (!ended)
1185             prime();
1186     }
1187 
1188     private void prime()
1189     {
1190         import std.algorithm : min;
1191 
1192         while (chunk.length < chunkBuffer.length)
1193         {
1194             if (stream.input.length == 0 && compressedSz != 0)
1195             {
1196                 static if (isSearchable)
1197                     input.seek(currentPos);
1198                 else
1199                     enforce(input.pos == currentPos,
1200                         "Data cursor has moved. Entry is no longer valid."
1201                     );
1202 
1203                 const len = min(compressedSz, inBuffer.length);
1204                 auto inp = input.read(inBuffer[0 .. len]);
1205                 enforce(inp.length == len, "Corrupted Zip file: unexpected end of input");
1206                 stream.input = inp;
1207                 currentPos += len;
1208                 compressedSz -= len;
1209             }
1210 
1211             stream.output = chunkBuffer[chunk.length .. $];
1212 
1213             ended = algo.process(stream, cast(Flag!"lastChunk")input.eoi);
1214 
1215             chunk = chunkBuffer[0 .. $ - stream.output.length];
1216 
1217             calculatedCrc32 = crc32(calculatedCrc32, chunk.ptr, cast(uint) chunk.length);
1218 
1219             if (ended)
1220             {
1221                 enforce(
1222                     calculatedCrc32 == expectedCrc32,
1223                     "Corrupted Zip file: Wrong CRC32 checkum"
1224                 );
1225                 algo.end(stream);
1226                 break;
1227             }
1228         }
1229     }
1230 }
1231 
1232 private void writeField(T)(ubyte[] buffer, const(T)[] field, ref size_t offset)
1233         if (T.sizeof == 1)
1234 in (buffer.length >= field.length + offset)
1235 {
1236     if (field.length)
1237     {
1238         buffer[offset .. offset + field.length] = cast(const(ubyte)[]) field;
1239         offset += field.length;
1240     }
1241 }
1242 
1243 private enum ZipFlag : ushort
1244 {
1245     none = 0,
1246     encryption = 1 << 0,
1247     compress1 = 1 << 1,
1248     compress2 = 1 << 2,
1249     dataDescriptor = 1 << 3,
1250     compressedPatch = 1 << 5,
1251     strongEncryption = 1 << 6,
1252     efs = 1 << 11,
1253     masking = 1 << 13,
1254 }
1255 
1256 private struct LocalFileHeader
1257 {
1258     enum expectedSignature = 0x04034b50;
1259 
1260     LittleEndian!4 signature;
1261     LittleEndian!2 extractVersion;
1262     LittleEndian!2 flag;
1263     LittleEndian!2 compressionMethod;
1264     LittleEndian!4 lastModDosTime;
1265     LittleEndian!4 crc32;
1266     LittleEndian!4 compressedSize;
1267     LittleEndian!4 uncompressedSize;
1268     LittleEndian!2 fileNameLength;
1269     LittleEndian!2 extraFieldLength;
1270 
1271     static size_t computeTotalLength(string fileName, const(ubyte)[] extraField)
1272     {
1273         return LocalFileHeader.sizeof + fileName.length + extraField.length;
1274     }
1275 
1276     size_t totalLength()
1277     {
1278         return LocalFileHeader.sizeof + fileNameLength.val + extraFieldLength.val;
1279     }
1280 
1281     ubyte[] writeTo(ubyte[] buffer, string fileName, const(ubyte)[] extraField)
1282     {
1283         assert(fileName.length == fileNameLength.val);
1284         assert(extraField.length == extraFieldLength.val);
1285 
1286         assert(buffer.length >= totalLength());
1287 
1288         auto ptr = signature.data.ptr;
1289         buffer[0 .. LocalFileHeader.sizeof] = ptr[0 .. LocalFileHeader.sizeof];
1290 
1291         size_t offset = LocalFileHeader.sizeof;
1292         writeField(buffer, fileName, offset);
1293         writeField(buffer, extraField, offset);
1294 
1295         return buffer[0 .. offset];
1296     }
1297 }
1298 
1299 private struct CentralFileHeader
1300 {
1301     enum expectedSignature = 0x02014b50;
1302 
1303     LittleEndian!4 signature;
1304     LittleEndian!2 versionMadeBy;
1305     LittleEndian!2 extractVersion;
1306     LittleEndian!2 flag;
1307     LittleEndian!2 compressionMethod;
1308     LittleEndian!4 lastModDosTime;
1309     LittleEndian!4 crc32;
1310     LittleEndian!4 compressedSize;
1311     LittleEndian!4 uncompressedSize;
1312     LittleEndian!2 fileNameLength;
1313     LittleEndian!2 extraFieldLength;
1314     LittleEndian!2 fileCommentLength;
1315     LittleEndian!2 diskNumberStart;
1316     LittleEndian!2 internalFileAttributes;
1317     LittleEndian!4 externalFileAttributes;
1318     LittleEndian!4 relativeLocalHeaderOffset;
1319 
1320     static size_t computeTotalLength(string fileName, const(ubyte)[] extraField, string fileComment)
1321     {
1322         return CentralFileHeader.sizeof + fileName.length + extraField.length + fileComment.length;
1323     }
1324 
1325     size_t totalLength()
1326     {
1327         return CentralFileHeader.sizeof + fileNameLength.val +
1328             extraFieldLength.val + fileCommentLength.val;
1329     }
1330 
1331     ubyte[] writeTo(ubyte[] buffer, string fileName, const(ubyte)[] extraField, string fileComment)
1332     {
1333         assert(fileName.length == fileNameLength.val);
1334         assert(extraField.length == extraFieldLength.val);
1335         assert(fileComment.length == fileCommentLength.val);
1336 
1337         assert(buffer.length >= totalLength());
1338 
1339         auto ptr = signature.data.ptr;
1340         buffer[0 .. CentralFileHeader.sizeof] = ptr[0 .. CentralFileHeader.sizeof];
1341 
1342         size_t offset = CentralFileHeader.sizeof;
1343         writeField(buffer, fileName, offset);
1344         writeField(buffer, extraField, offset);
1345         writeField(buffer, fileComment, offset);
1346 
1347         return buffer[0 .. offset];
1348     }
1349 }
1350 
1351 private struct Zip64EndOfCentralDirRecord
1352 {
1353     enum expectedSignature = 0x06064b50;
1354 
1355     LittleEndian!4 signature;
1356     LittleEndian!8 zip64EndOfCentralDirRecordSize;
1357     LittleEndian!2 versionMadeBy;
1358     LittleEndian!2 extractVersion;
1359     LittleEndian!4 thisDisk;
1360     LittleEndian!4 centralDirDisk;
1361     LittleEndian!8 centralDirEntriesOnThisDisk;
1362     LittleEndian!8 centralDirEntries;
1363     LittleEndian!8 centralDirSize;
1364     LittleEndian!8 centralDirOffset;
1365 
1366 }
1367 
1368 private struct Zip64EndOfCentralDirLocator
1369 {
1370     enum expectedSignature = 0x07064b50;
1371 
1372     LittleEndian!4 signature;
1373     LittleEndian!4 zip64EndOfCentralDirDisk;
1374     LittleEndian!8 zip64EndOfCentralDirRecordOffset;
1375     LittleEndian!4 diskCount;
1376 }
1377 
1378 private struct EndOfCentralDirectory
1379 {
1380     enum expectedSignature = 0x06054b50;
1381 
1382     LittleEndian!4 signature;
1383     LittleEndian!2 thisDisk;
1384     LittleEndian!2 centralDirDisk;
1385     LittleEndian!2 centralDirEntriesOnThisDisk;
1386     LittleEndian!2 centralDirEntries;
1387     LittleEndian!4 centralDirSize;
1388     LittleEndian!4 centralDirOffset;
1389     LittleEndian!2 fileCommentLength;
1390 
1391     static size_t computeTotalLength(string comment)
1392     {
1393         return EndOfCentralDirectory.sizeof + comment.length;
1394     }
1395 
1396     size_t totalLength()
1397     {
1398         return EndOfCentralDirectory.sizeof + fileCommentLength.val;
1399     }
1400 
1401     ubyte[] writeTo(ubyte[] buffer, string comment)
1402     {
1403         assert(comment.length == fileCommentLength.val);
1404 
1405         assert(buffer.length >= totalLength());
1406 
1407         auto ptr = signature.data.ptr;
1408         buffer[0 .. EndOfCentralDirectory.sizeof] = ptr[0 .. EndOfCentralDirectory.sizeof];
1409 
1410         size_t offset = EndOfCentralDirectory.sizeof;
1411         writeField(buffer, comment, offset);
1412 
1413         return buffer[0 .. offset];
1414     }
1415 }
1416 
1417 static assert(LocalFileHeader.sizeof == 30);
1418 static assert(CentralFileHeader.sizeof == 46);
1419 static assert(Zip64EndOfCentralDirRecord.sizeof == 56);
1420 static assert(Zip64EndOfCentralDirLocator.sizeof == 20);
1421 static assert(EndOfCentralDirectory.sizeof == 22);
1422 
1423 private struct ExtraFieldHeader
1424 {
1425     LittleEndian!2 id;
1426     LittleEndian!2 size;
1427 }
1428 
1429 private struct Zip64ExtraField
1430 {
1431     enum expectedId = 0x0001;
1432 
1433     LittleEndian!2 id;
1434     LittleEndian!2 size;
1435     LittleEndian!8 uncompressedSize;
1436     LittleEndian!8 compressedSize;
1437     LittleEndian!8 localHeaderPos;
1438     LittleEndian!4 diskStartNumber;
1439 }
1440 
1441 static assert(Zip64ExtraField.sizeof == 32);
1442 
1443 version (Posix)
1444 {
1445     private struct UnixExtraField
1446     {
1447         enum expectedId = 0x000d;
1448 
1449         LittleEndian!2 id;
1450         LittleEndian!2 size;
1451         LittleEndian!4 atime;
1452         LittleEndian!4 mtime;
1453         LittleEndian!2 uid;
1454         LittleEndian!2 gid;
1455 
1456         static size_t computeTotalLength(string linkname)
1457         {
1458             return UnixExtraField.sizeof + linkname.length;
1459         }
1460 
1461         ubyte[] writeTo(ubyte[] buffer, string linkname)
1462         {
1463             assert(linkname.length == size.val - 12);
1464 
1465             assert(buffer.length >= computeTotalLength(linkname));
1466 
1467             auto ptr = id.data.ptr;
1468             buffer[0 .. UnixExtraField.sizeof] = ptr[0 .. UnixExtraField.sizeof];
1469 
1470             size_t offset = UnixExtraField.sizeof;
1471             writeField(buffer, linkname, offset);
1472 
1473             return buffer[0 .. offset];
1474         }
1475     }
1476 
1477     static assert(UnixExtraField.sizeof == 16);
1478 }
1479 
1480 // Extra field that places the file attributes in the local header
1481 private struct SquizBoxExtraField
1482 {
1483     enum expectedId = 0x4273; // SB
1484 
1485     LittleEndian!2 id = expectedId;
1486     LittleEndian!2 size = 4;
1487     LittleEndian!4 attributes;
1488 
1489     void writeTo(ubyte[] buffer)
1490     {
1491         assert(buffer.length == 8);
1492         auto ptr = id.data.ptr;
1493         buffer[0 .. SquizBoxExtraField.sizeof] = ptr[0 .. SquizBoxExtraField.sizeof];
1494     }
1495 }
1496 
1497 static assert(SquizBoxExtraField.sizeof == 8);