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);