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