1 // Written in the D programming language. 2 3 /** 4 * Read/write data in the $(LINK2 http://www.info-zip.org, _zip archive) format. 5 * Makes use of the etc.c.zlib compression library. 6 * 7 * Bugs: 8 * $(UL 9 * $(LI Multi-disk zips not supported.) 10 * $(LI Only Zip version 20 formats are supported.) 11 * $(LI Only supports compression modes 0 (no compression) and 8 (deflate).) 12 * $(LI Does not support encryption.) 13 * $(LI $(BUGZILLA 592)) 14 * $(LI $(BUGZILLA 2137)) 15 * ) 16 * 17 * Example: 18 * --- 19 // Read existing zip file. 20 import std.digest.crc, std.file, std.stdio, std.zip; 21 22 void main(string[] args) 23 { 24 // read a zip file into memory 25 auto zip = new ZipArchive(read(args[1])); 26 writeln("Archive: ", args[1]); 27 writefln("%-10s %-8s Name", "Length", "CRC-32"); 28 // iterate over all zip members 29 foreach (name, am; zip.directory) 30 { 31 // print some data about each member 32 writefln("%10s %08x %s", am.expandedSize, am.crc32, name); 33 assert(am.expandedData.length == 0); 34 // decompress the archive member 35 zip.expand(am); 36 assert(am.expandedData.length == am.expandedSize); 37 } 38 } 39 40 // Create and write new zip file. 41 import std.file : write; 42 import std.string : representation; 43 44 void main() 45 { 46 char[] data = "Test data.\n".dup; 47 // Create an ArchiveMember for the test file. 48 ArchiveMember am = new ArchiveMember(); 49 am.name = "test.txt"; 50 am.expandedData(data.representation); 51 // Create an archive and add the member. 52 ZipArchive zip = new ZipArchive(); 53 zip.addMember(am); 54 // Build the archive 55 void[] compressed_data = zip.build(); 56 // Write to a file 57 write("test.zip", compressed_data); 58 } 59 * --- 60 * 61 * Copyright: Copyright Digital Mars 2000 - 2009. 62 * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 63 * Authors: $(HTTP digitalmars.com, Walter Bright) 64 * Source: $(PHOBOSSRC std/_zip.d) 65 */ 66 67 /* Copyright Digital Mars 2000 - 2009. 68 * Distributed under the Boost Software License, Version 1.0. 69 * (See accompanying file LICENSE_1_0.txt or copy at 70 * http://www.boost.org/LICENSE_1_0.txt) 71 */ 72 module std.zip; 73 74 //debug=print; 75 76 /** Thrown on error. 77 */ 78 class ZipException : Exception 79 { 80 this(string msg) @safe 81 { 82 super("ZipException: " ~ msg); 83 } 84 } 85 86 /** 87 * Compression method used by ArchiveMember 88 */ 89 enum CompressionMethod : ushort 90 { 91 none = 0, /// No compression, just archiving 92 deflate = 8 /// Deflate algorithm. Use zlib library to compress 93 } 94 95 /** 96 * A member of the ZipArchive. 97 */ 98 final class ArchiveMember 99 { 100 import std.conv : to, octal; 101 import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime; 102 103 /** 104 * Read/Write: Usually the file name of the archive member; it is used to 105 * index the archive directory for the member. Each member must have a unique 106 * name[]. Do not change without removing member from the directory first. 107 */ 108 string name; 109 110 ubyte[] extra; /// Read/Write: extra data for this member. 111 string comment; /// Read/Write: comment associated with this member. 112 113 private ubyte[] _compressedData; 114 private ubyte[] _expandedData; 115 private uint offset; 116 private uint _crc32; 117 private uint _compressedSize; 118 private uint _expandedSize; 119 private CompressionMethod _compressionMethod; 120 private ushort _madeVersion = 20; 121 private ushort _extractVersion = 20; 122 private ushort _diskNumber; 123 private uint _externalAttributes; 124 private DosFileTime _time; 125 // by default, no explicit order goes after explicit order 126 private uint _index = uint.max; 127 128 ushort flags; /// Read/Write: normally set to 0 129 ushort internalAttributes; /// Read/Write 130 131 @property ushort extractVersion() { return _extractVersion; } /// Read Only 132 @property uint crc32() { return _crc32; } /// Read Only: cyclic redundancy check (CRC) value 133 134 /// Read Only: size of data of member in compressed form. 135 @property uint compressedSize() { return _compressedSize; } 136 137 /// Read Only: size of data of member in expanded form. 138 @property uint expandedSize() { return _expandedSize; } 139 @property ushort diskNumber() { return _diskNumber; } /// Read Only: should be 0. 140 141 /// Read Only: data of member in compressed form. 142 @property ubyte[] compressedData() { return _compressedData; } 143 144 /// Read data of member in uncompressed form. 145 @property ubyte[] expandedData() { return _expandedData; } 146 147 /// Write data of member in uncompressed form. 148 @property @safe void expandedData(ubyte[] ed) 149 { 150 _expandedData = ed; 151 _expandedSize = to!uint(_expandedData.length); 152 153 // Clean old compressed data, if any 154 _compressedData.length = 0; 155 _compressedSize = 0; 156 } 157 158 /** 159 * Set the OS specific file attributes, as obtained by 160 * $(REF getAttributes, std,file) or $(REF DirEntry.attributes, std,file), for this archive member. 161 */ 162 @property @safe void fileAttributes(uint attr) 163 { 164 version (Posix) 165 { 166 _externalAttributes = (attr & 0xFFFF) << 16; 167 _madeVersion &= 0x00FF; 168 _madeVersion |= 0x0300; // attributes are in UNIX format 169 } 170 else version (Windows) 171 { 172 _externalAttributes = attr; 173 _madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format 174 } 175 else 176 { 177 static assert(0, "Unimplemented platform"); 178 } 179 } 180 181 version (Posix) @safe unittest 182 { 183 auto am = new ArchiveMember(); 184 am.fileAttributes = octal!100644; 185 assert(am._externalAttributes == octal!100644 << 16); 186 assert((am._madeVersion & 0xFF00) == 0x0300); 187 } 188 189 /** 190 * Get the OS specific file attributes for the archive member. 191 * 192 * Returns: The file attributes or 0 if the file attributes were 193 * encoded for an incompatible OS (Windows vs. Posix). 194 * 195 */ 196 @property uint fileAttributes() const 197 { 198 version (Posix) 199 { 200 if ((_madeVersion & 0xFF00) == 0x0300) 201 return _externalAttributes >> 16; 202 return 0; 203 } 204 else version (Windows) 205 { 206 if ((_madeVersion & 0xFF00) == 0x0000) 207 return _externalAttributes; 208 return 0; 209 } 210 else 211 { 212 static assert(0, "Unimplemented platform"); 213 } 214 } 215 216 /// Set the last modification time for this member. 217 @property void time(SysTime time) 218 { 219 _time = SysTimeToDosFileTime(time); 220 } 221 222 /// ditto 223 @property void time(DosFileTime time) 224 { 225 _time = time; 226 } 227 228 /// Get the last modification time for this member. 229 @property DosFileTime time() const 230 { 231 return _time; 232 } 233 234 /** 235 * Read compression method used for this member 236 * See_Also: 237 * CompressionMethod 238 **/ 239 @property @safe CompressionMethod compressionMethod() { return _compressionMethod; } 240 241 /** 242 * Write compression method used for this member 243 * See_Also: 244 * CompressionMethod 245 **/ 246 @property void compressionMethod(CompressionMethod cm) 247 { 248 if (cm == _compressionMethod) return; 249 250 if (_compressedSize > 0) 251 throw new ZipException("Can't change compression method for a compressed element"); 252 253 _compressionMethod = cm; 254 } 255 256 /** 257 * The index of this archive member within the archive. 258 */ 259 @property uint index() const pure nothrow @nogc { return _index; } 260 @property uint index(uint value) pure nothrow @nogc { return _index = value; } 261 262 debug(print) 263 { 264 void print() 265 { 266 printf("name = '%.*s'\n", name.length, name.ptr); 267 printf("\tcomment = '%.*s'\n", comment.length, comment.ptr); 268 printf("\tmadeVersion = x%04x\n", _madeVersion); 269 printf("\textractVersion = x%04x\n", extractVersion); 270 printf("\tflags = x%04x\n", flags); 271 printf("\tcompressionMethod = %d\n", compressionMethod); 272 printf("\ttime = %d\n", time); 273 printf("\tcrc32 = x%08x\n", crc32); 274 printf("\texpandedSize = %d\n", expandedSize); 275 printf("\tcompressedSize = %d\n", compressedSize); 276 printf("\tinternalAttributes = x%04x\n", internalAttributes); 277 printf("\texternalAttributes = x%08x\n", externalAttributes); 278 printf("\tindex = x%08x\n", index); 279 } 280 } 281 } 282 283 /** 284 * Object representing the entire archive. 285 * ZipArchives are collections of ArchiveMembers. 286 */ 287 final class ZipArchive 288 { 289 import std.algorithm.comparison : max; 290 import std.bitmanip : littleEndianToNative, nativeToLittleEndian; 291 import std.conv : to; 292 import std.datetime.systime : DosFileTime; 293 294 string comment; /// Read/Write: the archive comment. Must be less than 65536 bytes in length. 295 296 private ubyte[] _data; 297 private uint endrecOffset; 298 299 private uint _diskNumber; 300 private uint _diskStartDir; 301 private uint _numEntries; 302 private uint _totalEntries; 303 private bool _isZip64; 304 static const ushort zip64ExtractVersion = 45; 305 static const int digiSignLength = 6; 306 static const int eocd64LocLength = 20; 307 static const int eocd64Length = 56; 308 309 /// Read Only: array representing the entire contents of the archive. 310 @property @safe ubyte[] data() { return _data; } 311 312 /// Read Only: 0 since multi-disk zip archives are not supported. 313 @property @safe uint diskNumber() { return _diskNumber; } 314 315 /// Read Only: 0 since multi-disk zip archives are not supported 316 @property @safe uint diskStartDir() { return _diskStartDir; } 317 318 /// Read Only: number of ArchiveMembers in the directory. 319 @property @safe uint numEntries() { return _numEntries; } 320 @property @safe uint totalEntries() { return _totalEntries; } /// ditto 321 322 /// True when the archive is in Zip64 format. 323 @property @safe bool isZip64() { return _isZip64; } 324 325 /// Set this to true to force building a Zip64 archive. 326 @property @safe void isZip64(bool value) { _isZip64 = value; } 327 /** 328 * Read Only: array indexed by the name of each member of the archive. 329 * All the members of the archive can be accessed with a foreach loop: 330 * Example: 331 * -------------------- 332 * ZipArchive archive = new ZipArchive(data); 333 * foreach (ArchiveMember am; archive.directory) 334 * { 335 * writefln("member name is '%s'", am.name); 336 * } 337 * -------------------- 338 */ 339 @property @safe ArchiveMember[string] directory() { return _directory; } 340 341 private ArchiveMember[string] _directory; 342 343 debug (print) 344 { 345 @safe void print() 346 { 347 printf("\tdiskNumber = %u\n", diskNumber); 348 printf("\tdiskStartDir = %u\n", diskStartDir); 349 printf("\tnumEntries = %u\n", numEntries); 350 printf("\ttotalEntries = %u\n", totalEntries); 351 printf("\tcomment = '%.*s'\n", comment.length, comment.ptr); 352 } 353 } 354 355 /* ============ Creating a new archive =================== */ 356 357 /** Constructor to use when creating a new archive. 358 */ 359 this() @safe 360 { 361 } 362 363 /** Add de to the archive. The file is compressed on the fly. 364 */ 365 @safe void addMember(ArchiveMember de) 366 { 367 _directory[de.name] = de; 368 if (!de._compressedData.length) 369 { 370 switch (de.compressionMethod) 371 { 372 case CompressionMethod.none: 373 de._compressedData = de._expandedData; 374 break; 375 376 case CompressionMethod.deflate: 377 import std.zlib : compress; 378 () @trusted 379 { 380 de._compressedData = cast(ubyte[]) compress(cast(void[]) de._expandedData); 381 }(); 382 de._compressedData = de._compressedData[2 .. de._compressedData.length - 4]; 383 break; 384 385 default: 386 throw new ZipException("unsupported compression method"); 387 } 388 389 de._compressedSize = to!uint(de._compressedData.length); 390 import std.zlib : crc32; 391 () @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }(); 392 } 393 assert(de._compressedData.length == de._compressedSize); 394 } 395 396 /** Delete de from the archive. 397 */ 398 @safe void deleteMember(ArchiveMember de) 399 { 400 _directory.remove(de.name); 401 } 402 403 /** 404 * Construct an archive out of the current members of the archive. 405 * 406 * Fills in the properties data[], diskNumber, diskStartDir, numEntries, 407 * totalEntries, and directory[]. 408 * For each ArchiveMember, fills in properties crc32, compressedSize, 409 * compressedData[]. 410 * 411 * Returns: array representing the entire archive. 412 */ 413 void[] build() 414 { 415 import std.algorithm.sorting : sort; 416 uint i; 417 uint directoryOffset; 418 419 if (comment.length > 0xFFFF) 420 throw new ZipException("archive comment longer than 65535"); 421 422 // Compress each member; compute size 423 uint archiveSize = 0; 424 uint directorySize = 0; 425 auto directory = _directory.values().sort!((x, y) => x.index < y.index).release; 426 foreach (ArchiveMember de; directory) 427 { 428 if (to!ulong(archiveSize) + 30 + de.name.length + de.extra.length + de.compressedSize 429 + directorySize + 46 + de.name.length + de.extra.length + de.comment.length 430 + 22 + comment.length + eocd64LocLength + eocd64Length > uint.max) 431 throw new ZipException("zip files bigger than 4 GB are unsupported"); 432 433 archiveSize += 30 + de.name.length + 434 de.extra.length + 435 de.compressedSize; 436 directorySize += 46 + de.name.length + 437 de.extra.length + 438 de.comment.length; 439 } 440 441 if (!isZip64 && _directory.length > ushort.max) 442 _isZip64 = true; 443 uint dataSize = archiveSize + directorySize + 22 + cast(uint) comment.length; 444 if (isZip64) 445 dataSize += eocd64LocLength + eocd64Length; 446 447 _data = new ubyte[dataSize]; 448 449 // Populate the data[] 450 451 // Store each archive member 452 i = 0; 453 foreach (ArchiveMember de; directory) 454 { 455 de.offset = i; 456 _data[i .. i + 4] = cast(ubyte[])"PK\x03\x04"; 457 putUshort(i + 4, de.extractVersion); 458 putUshort(i + 6, de.flags); 459 putUshort(i + 8, de._compressionMethod); 460 putUint (i + 10, cast(uint) de.time); 461 putUint (i + 14, de.crc32); 462 putUint (i + 18, de.compressedSize); 463 putUint (i + 22, to!uint(de.expandedSize)); 464 putUshort(i + 26, cast(ushort) de.name.length); 465 putUshort(i + 28, cast(ushort) de.extra.length); 466 i += 30; 467 468 _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[]; 469 i += de.name.length; 470 _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[]; 471 i += de.extra.length; 472 _data[i .. i + de.compressedSize] = de.compressedData[]; 473 i += de.compressedSize; 474 } 475 476 // Write directory 477 directoryOffset = i; 478 _numEntries = 0; 479 foreach (ArchiveMember de; directory) 480 { 481 _data[i .. i + 4] = cast(ubyte[])"PK\x01\x02"; 482 putUshort(i + 4, de._madeVersion); 483 putUshort(i + 6, de.extractVersion); 484 putUshort(i + 8, de.flags); 485 putUshort(i + 10, de._compressionMethod); 486 putUint (i + 12, cast(uint) de.time); 487 putUint (i + 16, de.crc32); 488 putUint (i + 20, de.compressedSize); 489 putUint (i + 24, de.expandedSize); 490 putUshort(i + 28, cast(ushort) de.name.length); 491 putUshort(i + 30, cast(ushort) de.extra.length); 492 putUshort(i + 32, cast(ushort) de.comment.length); 493 putUshort(i + 34, de.diskNumber); 494 putUshort(i + 36, de.internalAttributes); 495 putUint (i + 38, de._externalAttributes); 496 putUint (i + 42, de.offset); 497 i += 46; 498 499 _data[i .. i + de.name.length] = (cast(ubyte[]) de.name)[]; 500 i += de.name.length; 501 _data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[]; 502 i += de.extra.length; 503 _data[i .. i + de.comment.length] = (cast(ubyte[]) de.comment)[]; 504 i += de.comment.length; 505 _numEntries++; 506 } 507 _totalEntries = numEntries; 508 509 if (isZip64) 510 { 511 // Write zip64 end of central directory record 512 uint eocd64Offset = i; 513 _data[i .. i + 4] = cast(ubyte[])"PK\x06\x06"; 514 putUlong (i + 4, eocd64Length - 12); 515 putUshort(i + 12, zip64ExtractVersion); 516 putUshort(i + 14, zip64ExtractVersion); 517 putUint (i + 16, diskNumber); 518 putUint (i + 20, diskStartDir); 519 putUlong (i + 24, numEntries); 520 putUlong (i + 32, totalEntries); 521 putUlong (i + 40, directorySize); 522 putUlong (i + 48, directoryOffset); 523 i += eocd64Length; 524 525 // Write zip64 end of central directory record locator 526 _data[i .. i + 4] = cast(ubyte[])"PK\x06\x07"; 527 putUint (i + 4, diskNumber); 528 putUlong (i + 8, eocd64Offset); 529 putUint (i + 16, 1); 530 i += eocd64LocLength; 531 } 532 533 // Write end record 534 endrecOffset = i; 535 _data[i .. i + 4] = cast(ubyte[])"PK\x05\x06"; 536 putUshort(i + 4, cast(ushort) diskNumber); 537 putUshort(i + 6, cast(ushort) diskStartDir); 538 putUshort(i + 8, (numEntries > ushort.max ? ushort.max : cast(ushort) numEntries)); 539 putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries)); 540 putUint (i + 12, directorySize); 541 putUint (i + 16, directoryOffset); 542 putUshort(i + 20, cast(ushort) comment.length); 543 i += 22; 544 545 // Write archive comment 546 assert(i + comment.length == data.length); 547 _data[i .. data.length] = (cast(ubyte[]) comment)[]; 548 549 return cast(void[]) data; 550 } 551 552 /* ============ Reading an existing archive =================== */ 553 554 /** 555 * Constructor to use when reading an existing archive. 556 * 557 * Fills in the properties data[], diskNumber, diskStartDir, numEntries, 558 * totalEntries, comment[], and directory[]. 559 * For each ArchiveMember, fills in 560 * properties madeVersion, extractVersion, flags, compressionMethod, time, 561 * crc32, compressedSize, expandedSize, compressedData[], diskNumber, 562 * internalAttributes, externalAttributes, name[], extra[], comment[]. 563 * Use expand() to get the expanded data for each ArchiveMember. 564 * 565 * Params: 566 * buffer = the entire contents of the archive. 567 */ 568 569 this(void[] buffer) 570 { uint iend; 571 uint i; 572 int endcommentlength; 573 uint directorySize; 574 uint directoryOffset; 575 576 this._data = cast(ubyte[]) buffer; 577 578 if (data.length > uint.max - 2) 579 throw new ZipException("zip files bigger than 4 GB are unsupported"); 580 581 // Find 'end record index' by searching backwards for signature 582 iend = (data.length > 66_000 ? to!uint(data.length - 66_000) : 0); 583 for (i = to!uint(data.length) - 22; 1; i--) 584 { 585 if (i < iend || i >= data.length) 586 throw new ZipException("no end record"); 587 588 if (_data[i .. i + 4] == cast(ubyte[])"PK\x05\x06") 589 { 590 endcommentlength = getUshort(i + 20); 591 if (i + 22 + endcommentlength > data.length 592 || i + 22 + endcommentlength < i) 593 continue; 594 comment = cast(string)(_data[i + 22 .. i + 22 + endcommentlength]); 595 endrecOffset = i; 596 597 uint k = i - eocd64LocLength; 598 if (k < i && _data[k .. k + 4] == cast(ubyte[])"PK\x06\x07") 599 { 600 _isZip64 = true; 601 i = k; 602 } 603 604 break; 605 } 606 } 607 608 if (isZip64) 609 { 610 // Read Zip64 record data 611 ulong eocdOffset = getUlong(i + 8); 612 if (eocdOffset + eocd64Length > _data.length) 613 throw new ZipException("corrupted directory"); 614 615 i = to!uint(eocdOffset); 616 if (_data[i .. i + 4] != cast(ubyte[])"PK\x06\x06") 617 throw new ZipException("invalid Zip EOCD64 signature"); 618 619 ulong eocd64Size = getUlong(i + 4); 620 if (eocd64Size + i - 12 > data.length) 621 throw new ZipException("invalid Zip EOCD64 size"); 622 623 _diskNumber = getUint(i + 16); 624 _diskStartDir = getUint(i + 20); 625 626 ulong numEntriesUlong = getUlong(i + 24); 627 ulong totalEntriesUlong = getUlong(i + 32); 628 ulong directorySizeUlong = getUlong(i + 40); 629 ulong directoryOffsetUlong = getUlong(i + 48); 630 631 if (numEntriesUlong > uint.max) 632 throw new ZipException("supposedly more than 4294967296 files in archive"); 633 634 if (numEntriesUlong != totalEntriesUlong) 635 throw new ZipException("multiple disk zips not supported"); 636 637 if (directorySizeUlong > i || directoryOffsetUlong > i 638 || directorySizeUlong + directoryOffsetUlong > i) 639 throw new ZipException("corrupted directory"); 640 641 _numEntries = to!uint(numEntriesUlong); 642 _totalEntries = to!uint(totalEntriesUlong); 643 directorySize = to!uint(directorySizeUlong); 644 directoryOffset = to!uint(directoryOffsetUlong); 645 } 646 else 647 { 648 // Read end record data 649 _diskNumber = getUshort(i + 4); 650 _diskStartDir = getUshort(i + 6); 651 652 _numEntries = getUshort(i + 8); 653 _totalEntries = getUshort(i + 10); 654 655 if (numEntries != totalEntries) 656 throw new ZipException("multiple disk zips not supported"); 657 658 directorySize = getUint(i + 12); 659 directoryOffset = getUint(i + 16); 660 661 if (directoryOffset + directorySize > i) 662 throw new ZipException("corrupted directory"); 663 } 664 665 i = directoryOffset; 666 for (int n = 0; n < numEntries; n++) 667 { 668 /* The format of an entry is: 669 * 'PK' 1, 2 670 * directory info 671 * path 672 * extra data 673 * comment 674 */ 675 676 uint namelen; 677 uint extralen; 678 uint commentlen; 679 680 if (_data[i .. i + 4] != cast(ubyte[])"PK\x01\x02") 681 throw new ZipException("invalid directory entry 1"); 682 ArchiveMember de = new ArchiveMember(); 683 de._index = n; 684 de._madeVersion = getUshort(i + 4); 685 de._extractVersion = getUshort(i + 6); 686 de.flags = getUshort(i + 8); 687 de._compressionMethod = cast(CompressionMethod) getUshort(i + 10); 688 de.time = cast(DosFileTime) getUint(i + 12); 689 de._crc32 = getUint(i + 16); 690 de._compressedSize = getUint(i + 20); 691 de._expandedSize = getUint(i + 24); 692 namelen = getUshort(i + 28); 693 extralen = getUshort(i + 30); 694 commentlen = getUshort(i + 32); 695 de._diskNumber = getUshort(i + 34); 696 de.internalAttributes = getUshort(i + 36); 697 de._externalAttributes = getUint(i + 38); 698 de.offset = getUint(i + 42); 699 i += 46; 700 701 if (i + namelen + extralen + commentlen > directoryOffset + directorySize) 702 throw new ZipException("invalid directory entry 2"); 703 704 de.name = cast(string)(_data[i .. i + namelen]); 705 i += namelen; 706 de.extra = _data[i .. i + extralen]; 707 i += extralen; 708 de.comment = cast(string)(_data[i .. i + commentlen]); 709 i += commentlen; 710 711 immutable uint dataOffset = de.offset + 30 + namelen + extralen; 712 if (dataOffset + de.compressedSize > endrecOffset) 713 throw new ZipException("Invalid directory entry offset or size."); 714 de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize]; 715 716 _directory[de.name] = de; 717 718 } 719 if (i != directoryOffset + directorySize) 720 throw new ZipException("invalid directory entry 3"); 721 } 722 723 /***** 724 * Decompress the contents of archive member de and return the expanded 725 * data. 726 * 727 * Fills in properties extractVersion, flags, compressionMethod, time, 728 * crc32, compressedSize, expandedSize, expandedData[], name[], extra[]. 729 */ 730 ubyte[] expand(ArchiveMember de) 731 { uint namelen; 732 uint extralen; 733 734 if (_data[de.offset .. de.offset + 4] != cast(ubyte[])"PK\x03\x04") 735 throw new ZipException("invalid directory entry 4"); 736 737 // These values should match what is in the main zip archive directory 738 de._extractVersion = getUshort(de.offset + 4); 739 de.flags = getUshort(de.offset + 6); 740 de._compressionMethod = cast(CompressionMethod) getUshort(de.offset + 8); 741 de.time = cast(DosFileTime) getUint(de.offset + 10); 742 de._crc32 = getUint(de.offset + 14); 743 de._compressedSize = max(getUint(de.offset + 18), de.compressedSize); 744 de._expandedSize = max(getUint(de.offset + 22), de.expandedSize); 745 namelen = getUshort(de.offset + 26); 746 extralen = getUshort(de.offset + 28); 747 748 debug(print) 749 { 750 printf("\t\texpandedSize = %d\n", de.expandedSize); 751 printf("\t\tcompressedSize = %d\n", de.compressedSize); 752 printf("\t\tnamelen = %d\n", namelen); 753 printf("\t\textralen = %d\n", extralen); 754 } 755 756 if (de.flags & 1) 757 throw new ZipException("encryption not supported"); 758 759 int i; 760 i = de.offset + 30 + namelen + extralen; 761 if (i + de.compressedSize > endrecOffset) 762 throw new ZipException("invalid directory entry 5"); 763 764 de._compressedData = _data[i .. i + de.compressedSize]; 765 debug(print) arrayPrint(de.compressedData); 766 767 switch (de.compressionMethod) 768 { 769 case CompressionMethod.none: 770 de._expandedData = de.compressedData; 771 return de.expandedData; 772 773 case CompressionMethod.deflate: 774 // -15 is a magic value used to decompress zip files. 775 // It has the effect of not requiring the 2 byte header 776 // and 4 byte trailer. 777 import std.zlib : uncompress; 778 de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15); 779 return de.expandedData; 780 781 default: 782 throw new ZipException("unsupported compression method"); 783 } 784 } 785 786 /* ============ Utility =================== */ 787 788 @safe ushort getUshort(int i) 789 { 790 ubyte[2] result = data[i .. i + 2]; 791 return littleEndianToNative!ushort(result); 792 } 793 794 @safe uint getUint(int i) 795 { 796 ubyte[4] result = data[i .. i + 4]; 797 return littleEndianToNative!uint(result); 798 } 799 800 @safe ulong getUlong(int i) 801 { 802 ubyte[8] result = data[i .. i + 8]; 803 return littleEndianToNative!ulong(result); 804 } 805 806 @safe void putUshort(int i, ushort us) 807 { 808 data[i .. i + 2] = nativeToLittleEndian(us); 809 } 810 811 @safe void putUint(int i, uint ui) 812 { 813 data[i .. i + 4] = nativeToLittleEndian(ui); 814 } 815 816 @safe void putUlong(int i, ulong ul) 817 { 818 data[i .. i + 8] = nativeToLittleEndian(ul); 819 } 820 } 821 822 debug(print) 823 { 824 @safe void arrayPrint(ubyte[] array) 825 { 826 printf("array %p,%d\n", cast(void*) array, array.length); 827 for (int i = 0; i < array.length; i++) 828 { 829 printf("%02x ", array[i]); 830 if (((i + 1) & 15) == 0) 831 printf("\n"); 832 } 833 printf("\n"); 834 } 835 } 836 837 @system unittest 838 { 839 // @system due to (at least) ZipArchive.build 840 auto zip1 = new ZipArchive(); 841 auto zip2 = new ZipArchive(); 842 auto am1 = new ArchiveMember(); 843 am1.name = "foo"; 844 am1.expandedData = new ubyte[](1024); 845 zip1.addMember(am1); 846 auto data1 = zip1.build(); 847 zip2.addMember(zip1.directory["foo"]); 848 zip2.build(); 849 auto am2 = zip2.directory["foo"]; 850 zip2.expand(am2); 851 assert(am1.expandedData == am2.expandedData); 852 auto zip3 = new ZipArchive(data1); 853 zip3.build(); 854 assert(zip3.directory["foo"].compressedSize == am1.compressedSize); 855 856 // Test if packing and unpacking produces the original data 857 import std.conv, std.stdio; 858 import std.random : uniform, MinstdRand0; 859 MinstdRand0 gen; 860 const uint itemCount = 20, minSize = 10, maxSize = 500; 861 foreach (variant; 0 .. 2) 862 { 863 bool useZip64 = !!variant; 864 zip1 = new ZipArchive(); 865 zip1.isZip64 = useZip64; 866 ArchiveMember[itemCount] ams; 867 foreach (i; 0 .. itemCount) 868 { 869 ams[i] = new ArchiveMember(); 870 ams[i].name = to!string(i); 871 ams[i].expandedData = new ubyte[](uniform(minSize, maxSize)); 872 foreach (ref ubyte c; ams[i].expandedData) 873 c = cast(ubyte)(uniform(0, 256)); 874 ams[i].compressionMethod = CompressionMethod.deflate; 875 zip1.addMember(ams[i]); 876 } 877 auto zippedData = zip1.build(); 878 zip2 = new ZipArchive(zippedData); 879 assert(zip2.isZip64 == useZip64); 880 foreach (am; ams) 881 { 882 am2 = zip2.directory[am.name]; 883 zip2.expand(am2); 884 assert(am.crc32 == am2.crc32); 885 assert(am.expandedData == am2.expandedData); 886 } 887 } 888 } 889 890 @system unittest 891 { 892 import std.conv : to; 893 import std.random : Mt19937, randomShuffle; 894 // Test if packing and unpacking preserves order. 895 auto rand = Mt19937(15966); 896 string[] names; 897 int value = 0; 898 // Generate a series of unique numbers as filenames. 899 foreach (i; 0 .. 20) 900 { 901 value += 1 + rand.front & 0xFFFF; 902 rand.popFront; 903 names ~= value.to!string; 904 } 905 // Insert them in a random order. 906 names.randomShuffle(rand); 907 auto zip1 = new ZipArchive(); 908 foreach (i, name; names) 909 { 910 auto member = new ArchiveMember(); 911 member.name = name; 912 member.expandedData = cast(ubyte[]) name; 913 member.index = cast(int) i; 914 zip1.addMember(member); 915 } 916 auto data = zip1.build(); 917 918 // Ensure that they appear in the same order. 919 auto zip2 = new ZipArchive(data); 920 foreach (i, name; names) 921 { 922 const member = zip2.directory[name]; 923 assert(member.index == i, "member " ~ name ~ " had index " ~ 924 member.index.to!string ~ " but we expected index " ~ i.to!string ~ 925 ". The input array was " ~ names.to!string); 926 } 927 } 928 929 @system unittest 930 { 931 import std.zlib; 932 933 ubyte[] src = cast(ubyte[]) 934 "the quick brown fox jumps over the lazy dog\r 935 the quick brown fox jumps over the lazy dog\r 936 "; 937 auto dst = cast(ubyte[]) compress(cast(void[]) src); 938 auto after = cast(ubyte[]) uncompress(cast(void[]) dst); 939 assert(src == after); 940 } 941 942 @system unittest 943 { 944 // @system due to ZipArchive.build 945 import std.datetime; 946 ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9]; 947 948 auto ar = new ZipArchive; 949 auto am = new ArchiveMember; // 10 950 am.name = "buf"; 951 am.expandedData = buf; 952 am.compressionMethod = CompressionMethod.deflate; 953 am.time = SysTimeToDosFileTime(Clock.currTime()); 954 ar.addMember(am); // 15 955 956 auto zip1 = ar.build(); 957 auto arAfter = new ZipArchive(zip1); 958 assert(arAfter.directory.length == 1); 959 auto amAfter = arAfter.directory["buf"]; 960 arAfter.expand(amAfter); 961 assert(amAfter.name == am.name); 962 assert(amAfter.expandedData == am.expandedData); 963 assert(amAfter.time == am.time); 964 } 965 966 // Non-Android Posix-only, because we can't rely on the unzip command being 967 // available on Android or Windows 968 version (Android) {} else 969 version (Posix) @system unittest 970 { 971 import std.datetime, std.file, std.format, std.path, std.process, std.stdio; 972 973 if (executeShell("unzip").status != 0) 974 { 975 writeln("Can't run unzip, skipping unzip test"); 976 return; 977 } 978 979 auto zr = new ZipArchive(); 980 auto am = new ArchiveMember(); 981 am.compressionMethod = CompressionMethod.deflate; 982 am.name = "foo.bar"; 983 am.time = SysTimeToDosFileTime(Clock.currTime()); 984 am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine"; 985 zr.addMember(am); 986 auto data2 = zr.build(); 987 988 mkdirRecurse(deleteme); 989 scope(exit) rmdirRecurse(deleteme); 990 string zipFile = buildPath(deleteme, "foo.zip"); 991 std.file.write(zipFile, cast(byte[]) data2); 992 993 auto result = executeShell(format("unzip -l %s", zipFile)); 994 scope(failure) writeln(result.output); 995 assert(result.status == 0); 996 } 997