Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 400 and 403] [Versions 401 and 403]
1 <?php 2 3 declare(strict_types=1); 4 5 namespace ZipStream; 6 7 use Psr\Http\Message\StreamInterface; 8 use ZipStream\Exception\OverflowException; 9 use ZipStream\Option\Archive as ArchiveOptions; 10 use ZipStream\Option\File as FileOptions; 11 use ZipStream\Option\Version; 12 13 /** 14 * ZipStream 15 * 16 * Streamed, dynamically generated zip archives. 17 * 18 * Usage: 19 * 20 * Streaming zip archives is a simple, three-step process: 21 * 22 * 1. Create the zip stream: 23 * 24 * $zip = new ZipStream('example.zip'); 25 * 26 * 2. Add one or more files to the archive: 27 * 28 * * add first file 29 * $data = file_get_contents('some_file.gif'); 30 * $zip->addFile('some_file.gif', $data); 31 * 32 * * add second file 33 * $data = file_get_contents('some_file.gif'); 34 * $zip->addFile('another_file.png', $data); 35 * 36 * 3. Finish the zip stream: 37 * 38 * $zip->finish(); 39 * 40 * You can also add an archive comment, add comments to individual files, 41 * and adjust the timestamp of files. See the API documentation for each 42 * method below for additional information. 43 * 44 * Example: 45 * 46 * // create a new zip stream object 47 * $zip = new ZipStream('some_files.zip'); 48 * 49 * // list of local files 50 * $files = array('foo.txt', 'bar.jpg'); 51 * 52 * // read and add each file to the archive 53 * foreach ($files as $path) 54 * $zip->addFile($path, file_get_contents($path)); 55 * 56 * // write archive footer to stream 57 * $zip->finish(); 58 */ 59 class ZipStream 60 { 61 /** 62 * This number corresponds to the ZIP version/OS used (2 bytes) 63 * From: https://www.iana.org/assignments/media-types/application/zip 64 * The upper byte (leftmost one) indicates the host system (OS) for the 65 * file. Software can use this information to determine 66 * the line record format for text files etc. The current 67 * mappings are: 68 * 69 * 0 - MS-DOS and OS/2 (F.A.T. file systems) 70 * 1 - Amiga 2 - VAX/VMS 71 * 3 - *nix 4 - VM/CMS 72 * 5 - Atari ST 6 - OS/2 H.P.F.S. 73 * 7 - Macintosh 8 - Z-System 74 * 9 - CP/M 10 thru 255 - unused 75 * 76 * The lower byte (rightmost one) indicates the version number of the 77 * software used to encode the file. The value/10 78 * indicates the major version number, and the value 79 * mod 10 is the minor version number. 80 * Here we are using 6 for the OS, indicating OS/2 H.P.F.S. 81 * to prevent file permissions issues upon extract (see #84) 82 * 0x603 is 00000110 00000011 in binary, so 6 and 3 83 */ 84 public const ZIP_VERSION_MADE_BY = 0x603; 85 86 /** 87 * The following signatures end with 0x4b50, which in ASCII is PK, 88 * the initials of the inventor Phil Katz. 89 * See https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers 90 */ 91 public const FILE_HEADER_SIGNATURE = 0x04034b50; 92 93 public const CDR_FILE_SIGNATURE = 0x02014b50; 94 95 public const CDR_EOF_SIGNATURE = 0x06054b50; 96 97 public const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; 98 99 public const ZIP64_CDR_EOF_SIGNATURE = 0x06064b50; 100 101 public const ZIP64_CDR_LOCATOR_SIGNATURE = 0x07064b50; 102 103 /** 104 * Global Options 105 * 106 * @var ArchiveOptions 107 */ 108 public $opt; 109 110 /** 111 * @var array 112 */ 113 public $files = []; 114 115 /** 116 * @var Bigint 117 */ 118 public $cdr_ofs; 119 120 /** 121 * @var Bigint 122 */ 123 public $ofs; 124 125 /** 126 * @var bool 127 */ 128 protected $need_headers; 129 130 /** 131 * @var null|String 132 */ 133 protected $output_name; 134 135 /** 136 * Create a new ZipStream object. 137 * 138 * Parameters: 139 * 140 * @param String $name - Name of output file (optional). 141 * @param ArchiveOptions $opt - Archive Options 142 * 143 * Large File Support: 144 * 145 * By default, the method addFileFromPath() will send send files 146 * larger than 20 megabytes along raw rather than attempting to 147 * compress them. You can change both the maximum size and the 148 * compression behavior using the largeFile* options above, with the 149 * following caveats: 150 * 151 * * For "small" files (e.g. files smaller than largeFileSize), the 152 * memory use can be up to twice that of the actual file. In other 153 * words, adding a 10 megabyte file to the archive could potentially 154 * occupy 20 megabytes of memory. 155 * 156 * * Enabling compression on large files (e.g. files larger than 157 * large_file_size) is extremely slow, because ZipStream has to pass 158 * over the large file once to calculate header information, and then 159 * again to compress and send the actual data. 160 * 161 * Examples: 162 * 163 * // create a new zip file named 'foo.zip' 164 * $zip = new ZipStream('foo.zip'); 165 * 166 * // create a new zip file named 'bar.zip' with a comment 167 * $opt->setComment = 'this is a comment for the zip file.'; 168 * $zip = new ZipStream('bar.zip', $opt); 169 * 170 * Notes: 171 * 172 * In order to let this library send HTTP headers, a filename must be given 173 * _and_ the option `sendHttpHeaders` must be `true`. This behavior is to 174 * allow software to send its own headers (including the filename), and 175 * still use this library. 176 */ 177 public function __construct(?string $name = null, ?ArchiveOptions $opt = null) 178 { 179 $this->opt = $opt ?: new ArchiveOptions(); 180 181 $this->output_name = $name; 182 $this->need_headers = $name && $this->opt->isSendHttpHeaders(); 183 184 $this->cdr_ofs = new Bigint(); 185 $this->ofs = new Bigint(); 186 } 187 188 /** 189 * addFile 190 * 191 * Add a file to the archive. 192 * 193 * @param String $name - path of file in archive (including directory). 194 * @param String $data - contents of file 195 * @param FileOptions $options 196 * 197 * File Options: 198 * time - Last-modified timestamp (seconds since the epoch) of 199 * this file. Defaults to the current time. 200 * comment - Comment related to this file. 201 * method - Storage method for file ("store" or "deflate") 202 * 203 * Examples: 204 * 205 * // add a file named 'foo.txt' 206 * $data = file_get_contents('foo.txt'); 207 * $zip->addFile('foo.txt', $data); 208 * 209 * // add a file named 'bar.jpg' with a comment and a last-modified 210 * // time of two hours ago 211 * $data = file_get_contents('bar.jpg'); 212 * $opt->setTime = time() - 2 * 3600; 213 * $opt->setComment = 'this is a comment about bar.jpg'; 214 * $zip->addFile('bar.jpg', $data, $opt); 215 */ 216 public function addFile(string $name, string $data, ?FileOptions $options = null): void 217 { 218 $options = $options ?: new FileOptions(); 219 $options->defaultTo($this->opt); 220 221 $file = new File($this, $name, $options); 222 $file->processData($data); 223 } 224 225 /** 226 * addFileFromPath 227 * 228 * Add a file at path to the archive. 229 * 230 * Note that large files may be compressed differently than smaller 231 * files; see the "Large File Support" section above for more 232 * information. 233 * 234 * @param String $name - name of file in archive (including directory path). 235 * @param String $path - path to file on disk (note: paths should be encoded using 236 * UNIX-style forward slashes -- e.g '/path/to/some/file'). 237 * @param FileOptions $options 238 * 239 * File Options: 240 * time - Last-modified timestamp (seconds since the epoch) of 241 * this file. Defaults to the current time. 242 * comment - Comment related to this file. 243 * method - Storage method for file ("store" or "deflate") 244 * 245 * Examples: 246 * 247 * // add a file named 'foo.txt' from the local file '/tmp/foo.txt' 248 * $zip->addFileFromPath('foo.txt', '/tmp/foo.txt'); 249 * 250 * // add a file named 'bigfile.rar' from the local file 251 * // '/usr/share/bigfile.rar' with a comment and a last-modified 252 * // time of two hours ago 253 * $path = '/usr/share/bigfile.rar'; 254 * $opt->setTime = time() - 2 * 3600; 255 * $opt->setComment = 'this is a comment about bar.jpg'; 256 * $zip->addFileFromPath('bigfile.rar', $path, $opt); 257 * 258 * @return void 259 * @throws \ZipStream\Exception\FileNotFoundException 260 * @throws \ZipStream\Exception\FileNotReadableException 261 */ 262 public function addFileFromPath(string $name, string $path, ?FileOptions $options = null): void 263 { 264 $options = $options ?: new FileOptions(); 265 $options->defaultTo($this->opt); 266 267 $file = new File($this, $name, $options); 268 $file->processPath($path); 269 } 270 271 /** 272 * addFileFromStream 273 * 274 * Add an open stream to the archive. 275 * 276 * @param String $name - path of file in archive (including directory). 277 * @param resource $stream - contents of file as a stream resource 278 * @param FileOptions $options 279 * 280 * File Options: 281 * time - Last-modified timestamp (seconds since the epoch) of 282 * this file. Defaults to the current time. 283 * comment - Comment related to this file. 284 * 285 * Examples: 286 * 287 * // create a temporary file stream and write text to it 288 * $fp = tmpfile(); 289 * fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); 290 * 291 * // add a file named 'streamfile.txt' from the content of the stream 292 * $x->addFileFromStream('streamfile.txt', $fp); 293 * 294 * @return void 295 */ 296 public function addFileFromStream(string $name, $stream, ?FileOptions $options = null): void 297 { 298 $options = $options ?: new FileOptions(); 299 $options->defaultTo($this->opt); 300 301 $file = new File($this, $name, $options); 302 $file->processStream(new Stream($stream)); 303 } 304 305 /** 306 * addFileFromPsr7Stream 307 * 308 * Add an open stream to the archive. 309 * 310 * @param String $name - path of file in archive (including directory). 311 * @param StreamInterface $stream - contents of file as a stream resource 312 * @param FileOptions $options 313 * 314 * File Options: 315 * time - Last-modified timestamp (seconds since the epoch) of 316 * this file. Defaults to the current time. 317 * comment - Comment related to this file. 318 * 319 * Examples: 320 * 321 * $stream = $response->getBody(); 322 * // add a file named 'streamfile.txt' from the content of the stream 323 * $x->addFileFromPsr7Stream('streamfile.txt', $stream); 324 * 325 * @return void 326 */ 327 public function addFileFromPsr7Stream( 328 string $name, 329 StreamInterface $stream, 330 ?FileOptions $options = null 331 ): void { 332 $options = $options ?: new FileOptions(); 333 $options->defaultTo($this->opt); 334 335 $file = new File($this, $name, $options); 336 $file->processStream($stream); 337 } 338 339 /** 340 * finish 341 * 342 * Write zip footer to stream. 343 * 344 * Example: 345 * 346 * // add a list of files to the archive 347 * $files = array('foo.txt', 'bar.jpg'); 348 * foreach ($files as $path) 349 * $zip->addFile($path, file_get_contents($path)); 350 * 351 * // write footer to stream 352 * $zip->finish(); 353 * @return void 354 * 355 * @throws OverflowException 356 */ 357 public function finish(): void 358 { 359 // add trailing cdr file records 360 foreach ($this->files as $cdrFile) { 361 $this->send($cdrFile); 362 $this->cdr_ofs = $this->cdr_ofs->add(Bigint::init(strlen($cdrFile))); 363 } 364 365 // Add 64bit headers (if applicable) 366 if (count($this->files) >= 0xFFFF || 367 $this->cdr_ofs->isOver32() || 368 $this->ofs->isOver32()) { 369 if (!$this->opt->isEnableZip64()) { 370 throw new OverflowException(); 371 } 372 373 $this->addCdr64Eof(); 374 $this->addCdr64Locator(); 375 } 376 377 // add trailing cdr eof record 378 $this->addCdrEof(); 379 380 // The End 381 $this->clear(); 382 } 383 384 /** 385 * Create a format string and argument list for pack(), then call 386 * pack() and return the result. 387 * 388 * @param array $fields 389 * @return string 390 */ 391 public static function packFields(array $fields): string 392 { 393 $fmt = ''; 394 $args = []; 395 396 // populate format string and argument list 397 foreach ($fields as [$format, $value]) { 398 if ($format === 'P') { 399 $fmt .= 'VV'; 400 if ($value instanceof Bigint) { 401 $args[] = $value->getLow32(); 402 $args[] = $value->getHigh32(); 403 } else { 404 $args[] = $value; 405 $args[] = 0; 406 } 407 } else { 408 if ($value instanceof Bigint) { 409 $value = $value->getLow32(); 410 } 411 $fmt .= $format; 412 $args[] = $value; 413 } 414 } 415 416 // prepend format string to argument list 417 array_unshift($args, $fmt); 418 419 // build output string from header and compressed data 420 return pack(...$args); 421 } 422 423 /** 424 * Send string, sending HTTP headers if necessary. 425 * Flush output after write if configure option is set. 426 * 427 * @param String $str 428 * @return void 429 */ 430 public function send(string $str): void 431 { 432 if ($this->need_headers) { 433 $this->sendHttpHeaders(); 434 } 435 $this->need_headers = false; 436 437 $outputStream = $this->opt->getOutputStream(); 438 439 if ($outputStream instanceof StreamInterface) { 440 $outputStream->write($str); 441 } else { 442 fwrite($outputStream, $str); 443 } 444 445 if ($this->opt->isFlushOutput()) { 446 // flush output buffer if it is on and flushable 447 $status = ob_get_status(); 448 if (isset($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) { 449 ob_flush(); 450 } 451 452 // Flush system buffers after flushing userspace output buffer 453 flush(); 454 } 455 } 456 457 /** 458 * Is this file larger than large_file_size? 459 * 460 * @param string $path 461 * @return bool 462 */ 463 public function isLargeFile(string $path): bool 464 { 465 if (!$this->opt->isStatFiles()) { 466 return false; 467 } 468 $stat = stat($path); 469 return $stat['size'] > $this->opt->getLargeFileSize(); 470 } 471 472 /** 473 * Save file attributes for trailing CDR record. 474 * 475 * @param File $file 476 * @return void 477 */ 478 public function addToCdr(File $file): void 479 { 480 $file->ofs = $this->ofs; 481 $this->ofs = $this->ofs->add($file->getTotalLength()); 482 $this->files[] = $file->getCdrFile(); 483 } 484 485 /** 486 * Send ZIP64 CDR EOF (Central Directory Record End-of-File) record. 487 * 488 * @return void 489 */ 490 protected function addCdr64Eof(): void 491 { 492 $num_files = count($this->files); 493 $cdr_length = $this->cdr_ofs; 494 $cdr_offset = $this->ofs; 495 496 $fields = [ 497 ['V', static::ZIP64_CDR_EOF_SIGNATURE], // ZIP64 end of central file header signature 498 ['P', 44], // Length of data below this header (length of block - 12) = 44 499 ['v', static::ZIP_VERSION_MADE_BY], // Made by version 500 ['v', Version::ZIP64], // Extract by version 501 ['V', 0x00], // disk number 502 ['V', 0x00], // no of disks 503 ['P', $num_files], // no of entries on disk 504 ['P', $num_files], // no of entries in cdr 505 ['P', $cdr_length], // CDR size 506 ['P', $cdr_offset], // CDR offset 507 ]; 508 509 $ret = static::packFields($fields); 510 $this->send($ret); 511 } 512 513 /** 514 * Send HTTP headers for this stream. 515 * 516 * @return void 517 */ 518 protected function sendHttpHeaders(): void 519 { 520 // grab content disposition 521 $disposition = $this->opt->getContentDisposition(); 522 523 if ($this->output_name) { 524 // Various different browsers dislike various characters here. Strip them all for safety. 525 $safe_output = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->output_name)); 526 527 // Check if we need to UTF-8 encode the filename 528 $urlencoded = rawurlencode($safe_output); 529 $disposition .= "; filename*=UTF-8''{$urlencoded}"; 530 } 531 532 $headers = [ 533 'Content-Type' => $this->opt->getContentType(), 534 'Content-Disposition' => $disposition, 535 'Pragma' => 'public', 536 'Cache-Control' => 'public, must-revalidate', 537 'Content-Transfer-Encoding' => 'binary', 538 ]; 539 540 $call = $this->opt->getHttpHeaderCallback(); 541 foreach ($headers as $key => $val) { 542 $call("$key: $val"); 543 } 544 } 545 546 /** 547 * Send ZIP64 CDR Locator (Central Directory Record Locator) record. 548 * 549 * @return void 550 */ 551 protected function addCdr64Locator(): void 552 { 553 $cdr_offset = $this->ofs->add($this->cdr_ofs); 554 555 $fields = [ 556 ['V', static::ZIP64_CDR_LOCATOR_SIGNATURE], // ZIP64 end of central file header signature 557 ['V', 0x00], // Disc number containing CDR64EOF 558 ['P', $cdr_offset], // CDR offset 559 ['V', 1], // Total number of disks 560 ]; 561 562 $ret = static::packFields($fields); 563 $this->send($ret); 564 } 565 566 /** 567 * Send CDR EOF (Central Directory Record End-of-File) record. 568 * 569 * @return void 570 */ 571 protected function addCdrEof(): void 572 { 573 $num_files = count($this->files); 574 $cdr_length = $this->cdr_ofs; 575 $cdr_offset = $this->ofs; 576 577 // grab comment (if specified) 578 $comment = $this->opt->getComment(); 579 580 $fields = [ 581 ['V', static::CDR_EOF_SIGNATURE], // end of central file header signature 582 ['v', 0x00], // disk number 583 ['v', 0x00], // no of disks 584 ['v', min($num_files, 0xFFFF)], // no of entries on disk 585 ['v', min($num_files, 0xFFFF)], // no of entries in cdr 586 ['V', $cdr_length->getLowFF()], // CDR size 587 ['V', $cdr_offset->getLowFF()], // CDR offset 588 ['v', strlen($comment)], // Zip Comment size 589 ]; 590 591 $ret = static::packFields($fields) . $comment; 592 $this->send($ret); 593 } 594 595 /** 596 * Clear all internal variables. Note that the stream object is not 597 * usable after this. 598 * 599 * @return void 600 */ 601 protected function clear(): void 602 { 603 $this->files = []; 604 $this->ofs = new Bigint(); 605 $this->cdr_ofs = new Bigint(); 606 $this->opt = new ArchiveOptions(); 607 } 608 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body