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