See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 declare(strict_types=1); 3 4 namespace ZipStream; 5 6 use Psr\Http\Message\StreamInterface; 7 use RuntimeException; 8 use ZipStream\Exception\EncodingException; 9 use ZipStream\Exception\FileNotFoundException; 10 use ZipStream\Exception\FileNotReadableException; 11 use ZipStream\Exception\OverflowException; 12 use ZipStream\Option\File as FileOptions; 13 use ZipStream\Option\Method; 14 use ZipStream\Option\Version; 15 16 class File 17 { 18 const HASH_ALGORITHM = 'crc32b'; 19 20 const BIT_ZERO_HEADER = 0x0008; 21 const BIT_EFS_UTF8 = 0x0800; 22 23 const COMPUTE = 1; 24 const SEND = 2; 25 26 private const CHUNKED_READ_BLOCK_SIZE = 1048576; 27 28 /** 29 * @var string 30 */ 31 public $name; 32 33 /** 34 * @var FileOptions 35 */ 36 public $opt; 37 38 /** 39 * @var Bigint 40 */ 41 public $len; 42 /** 43 * @var Bigint 44 */ 45 public $zlen; 46 47 /** @var int */ 48 public $crc; 49 50 /** 51 * @var Bigint 52 */ 53 public $hlen; 54 55 /** 56 * @var Bigint 57 */ 58 public $ofs; 59 60 /** 61 * @var int 62 */ 63 public $bits; 64 65 /** 66 * @var Version 67 */ 68 public $version; 69 70 /** 71 * @var ZipStream 72 */ 73 public $zip; 74 75 /** 76 * @var resource 77 */ 78 private $deflate; 79 80 /** 81 * @var \HashContext 82 */ 83 private $hash; 84 85 /** 86 * @var Method 87 */ 88 private $method; 89 90 /** 91 * @var Bigint 92 */ 93 private $totalLength; 94 95 public function __construct(ZipStream $zip, string $name, ?FileOptions $opt = null) 96 { 97 $this->zip = $zip; 98 99 $this->name = $name; 100 $this->opt = $opt ?: new FileOptions(); 101 $this->method = $this->opt->getMethod(); 102 $this->version = Version::STORE(); 103 $this->ofs = new Bigint(); 104 } 105 106 public function processPath(string $path): void 107 { 108 if (!is_readable($path)) { 109 if (!file_exists($path)) { 110 throw new FileNotFoundException($path); 111 } 112 throw new FileNotReadableException($path); 113 } 114 if ($this->zip->isLargeFile($path) === false) { 115 $data = file_get_contents($path); 116 $this->processData($data); 117 } else { 118 $this->method = $this->zip->opt->getLargeFileMethod(); 119 120 $stream = new DeflateStream(fopen($path, 'rb')); 121 $this->processStream($stream); 122 $stream->close(); 123 } 124 } 125 126 public function processData(string $data): void 127 { 128 $this->len = new Bigint(strlen($data)); 129 $this->crc = crc32($data); 130 131 // compress data if needed 132 if ($this->method->equals(Method::DEFLATE())) { 133 $data = gzdeflate($data); 134 } 135 136 $this->zlen = new Bigint(strlen($data)); 137 $this->addFileHeader(); 138 $this->zip->send($data); 139 $this->addFileFooter(); 140 } 141 142 /** 143 * Create and send zip header for this file. 144 * 145 * @return void 146 * @throws \ZipStream\Exception\EncodingException 147 */ 148 public function addFileHeader(): void 149 { 150 $name = static::filterFilename($this->name); 151 152 // calculate name length 153 $nameLength = strlen($name); 154 155 // create dos timestamp 156 $time = static::dosTime($this->opt->getTime()->getTimestamp()); 157 158 $comment = $this->opt->getComment(); 159 160 if (!mb_check_encoding($name, 'ASCII') || 161 !mb_check_encoding($comment, 'ASCII')) { 162 // Sets Bit 11: Language encoding flag (EFS). If this bit is set, 163 // the filename and comment fields for this file 164 // MUST be encoded using UTF-8. (see APPENDIX D) 165 if (!mb_check_encoding($name, 'UTF-8') || 166 !mb_check_encoding($comment, 'UTF-8')) { 167 throw new EncodingException( 168 'File name and comment should use UTF-8 ' . 169 'if one of them does not fit into ASCII range.' 170 ); 171 } 172 $this->bits |= self::BIT_EFS_UTF8; 173 } 174 175 if ($this->method->equals(Method::DEFLATE())) { 176 $this->version = Version::DEFLATE(); 177 } 178 179 $force = (boolean)($this->bits & self::BIT_ZERO_HEADER) && 180 $this->zip->opt->isEnableZip64(); 181 182 $footer = $this->buildZip64ExtraBlock($force); 183 184 // If this file will start over 4GB limit in ZIP file, 185 // CDR record will have to use Zip64 extension to describe offset 186 // to keep consistency we use the same value here 187 if ($this->zip->ofs->isOver32()) { 188 $this->version = Version::ZIP64(); 189 } 190 191 $fields = [ 192 ['V', ZipStream::FILE_HEADER_SIGNATURE], 193 ['v', $this->version->getValue()], // Version needed to Extract 194 ['v', $this->bits], // General purpose bit flags - data descriptor flag set 195 ['v', $this->method->getValue()], // Compression method 196 ['V', $time], // Timestamp (DOS Format) 197 ['V', $this->crc], // CRC32 of data (0 -> moved to data descriptor footer) 198 ['V', $this->zlen->getLowFF($force)], // Length of compressed data (forced to 0xFFFFFFFF for zero header) 199 ['V', $this->len->getLowFF($force)], // Length of original data (forced to 0xFFFFFFFF for zero header) 200 ['v', $nameLength], // Length of filename 201 ['v', strlen($footer)], // Extra data (see above) 202 ]; 203 204 // pack fields and calculate "total" length 205 $header = ZipStream::packFields($fields); 206 207 // print header and filename 208 $data = $header . $name . $footer; 209 $this->zip->send($data); 210 211 // save header length 212 $this->hlen = Bigint::init(strlen($data)); 213 } 214 215 /** 216 * Strip characters that are not legal in Windows filenames 217 * to prevent compatibility issues 218 * 219 * @param string $filename Unprocessed filename 220 * @return string 221 */ 222 public static function filterFilename(string $filename): string 223 { 224 // strip leading slashes from file name 225 // (fixes bug in windows archive viewer) 226 $filename = preg_replace('/^\\/+/', '', $filename); 227 228 return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename); 229 } 230 231 /** 232 * Convert a UNIX timestamp to a DOS timestamp. 233 * 234 * @param int $when 235 * @return int DOS Timestamp 236 */ 237 final protected static function dosTime(int $when): int 238 { 239 // get date array for timestamp 240 $d = getdate($when); 241 242 // set lower-bound on dates 243 if ($d['year'] < 1980) { 244 $d = array( 245 'year' => 1980, 246 'mon' => 1, 247 'mday' => 1, 248 'hours' => 0, 249 'minutes' => 0, 250 'seconds' => 0 251 ); 252 } 253 254 // remove extra years from 1980 255 $d['year'] -= 1980; 256 257 // return date string 258 return 259 ($d['year'] << 25) | 260 ($d['mon'] << 21) | 261 ($d['mday'] << 16) | 262 ($d['hours'] << 11) | 263 ($d['minutes'] << 5) | 264 ($d['seconds'] >> 1); 265 } 266 267 protected function buildZip64ExtraBlock(bool $force = false): string 268 { 269 270 $fields = []; 271 if ($this->len->isOver32($force)) { 272 $fields[] = ['P', $this->len]; // Length of original data 273 } 274 275 if ($this->len->isOver32($force)) { 276 $fields[] = ['P', $this->zlen]; // Length of compressed data 277 } 278 279 if ($this->ofs->isOver32()) { 280 $fields[] = ['P', $this->ofs]; // Offset of local header record 281 } 282 283 if (!empty($fields)) { 284 if (!$this->zip->opt->isEnableZip64()) { 285 throw new OverflowException(); 286 } 287 288 array_unshift( 289 $fields, 290 ['v', 0x0001], // 64 bit extension 291 ['v', count($fields) * 8] // Length of data block 292 ); 293 $this->version = Version::ZIP64(); 294 } 295 296 if ($this->bits & self::BIT_EFS_UTF8) { 297 // Put the tricky entry to 298 // force Linux unzip to lookup EFS flag. 299 $fields[] = ['v', 0x5653]; // Choose 'ZS' for proprietary usage 300 $fields[] = ['v', 0x0000]; // zero length 301 } 302 303 return ZipStream::packFields($fields); 304 } 305 306 /** 307 * Create and send data descriptor footer for this file. 308 * 309 * @return void 310 */ 311 312 public function addFileFooter(): void 313 { 314 315 if ($this->bits & self::BIT_ZERO_HEADER) { 316 // compressed and uncompressed size 317 $sizeFormat = 'V'; 318 if ($this->zip->opt->isEnableZip64()) { 319 $sizeFormat = 'P'; 320 } 321 $fields = [ 322 ['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE], 323 ['V', $this->crc], // CRC32 324 [$sizeFormat, $this->zlen], // Length of compressed data 325 [$sizeFormat, $this->len], // Length of original data 326 ]; 327 328 $footer = ZipStream::packFields($fields); 329 $this->zip->send($footer); 330 } else { 331 $footer = ''; 332 } 333 $this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer))); 334 $this->zip->addToCdr($this); 335 } 336 337 public function processStream(StreamInterface $stream): void 338 { 339 $this->zlen = new Bigint(); 340 $this->len = new Bigint(); 341 342 if ($this->zip->opt->isZeroHeader()) { 343 $this->processStreamWithZeroHeader($stream); 344 } else { 345 $this->processStreamWithComputedHeader($stream); 346 } 347 } 348 349 protected function processStreamWithZeroHeader(StreamInterface $stream): void 350 { 351 $this->bits |= self::BIT_ZERO_HEADER; 352 $this->addFileHeader(); 353 $this->readStream($stream, self::COMPUTE | self::SEND); 354 $this->addFileFooter(); 355 } 356 357 protected function readStream(StreamInterface $stream, ?int $options = null): void 358 { 359 $this->deflateInit(); 360 $total = 0; 361 $size = $this->opt->getSize(); 362 while (!$stream->eof() && ($size === 0 || $total < $size)) { 363 $data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE); 364 $total += strlen($data); 365 if ($size > 0 && $total > $size) { 366 $data = substr($data, 0 , strlen($data)-($total - $size)); 367 } 368 $this->deflateData($stream, $data, $options); 369 if ($options & self::SEND) { 370 $this->zip->send($data); 371 } 372 } 373 $this->deflateFinish($options); 374 } 375 376 protected function deflateInit(): void 377 { 378 $hash = hash_init(self::HASH_ALGORITHM); 379 $this->hash = $hash; 380 if ($this->method->equals(Method::DEFLATE())) { 381 $this->deflate = deflate_init( 382 ZLIB_ENCODING_RAW, 383 ['level' => $this->opt->getDeflateLevel()] 384 ); 385 } 386 } 387 388 protected function deflateData(StreamInterface $stream, string &$data, ?int $options = null): void 389 { 390 if ($options & self::COMPUTE) { 391 $this->len = $this->len->add(Bigint::init(strlen($data))); 392 hash_update($this->hash, $data); 393 } 394 if ($this->deflate) { 395 $data = deflate_add( 396 $this->deflate, 397 $data, 398 $stream->eof() 399 ? ZLIB_FINISH 400 : ZLIB_NO_FLUSH 401 ); 402 } 403 if ($options & self::COMPUTE) { 404 $this->zlen = $this->zlen->add(Bigint::init(strlen($data))); 405 } 406 } 407 408 protected function deflateFinish(?int $options = null): void 409 { 410 if ($options & self::COMPUTE) { 411 $this->crc = hexdec(hash_final($this->hash)); 412 } 413 } 414 415 protected function processStreamWithComputedHeader(StreamInterface $stream): void 416 { 417 $this->readStream($stream, self::COMPUTE); 418 $stream->rewind(); 419 420 // incremental compression with deflate_add 421 // makes this second read unnecessary 422 // but it is only available from PHP 7.0 423 if (!$this->deflate && $stream instanceof DeflateStream && $this->method->equals(Method::DEFLATE())) { 424 $stream->addDeflateFilter($this->opt); 425 $this->zlen = new Bigint(); 426 while (!$stream->eof()) { 427 $data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE); 428 $this->zlen = $this->zlen->add(Bigint::init(strlen($data))); 429 } 430 $stream->rewind(); 431 } 432 433 $this->addFileHeader(); 434 $this->readStream($stream, self::SEND); 435 $this->addFileFooter(); 436 } 437 438 /** 439 * Send CDR record for specified file. 440 * 441 * @return string 442 */ 443 public function getCdrFile(): string 444 { 445 $name = static::filterFilename($this->name); 446 447 // get attributes 448 $comment = $this->opt->getComment(); 449 450 // get dos timestamp 451 $time = static::dosTime($this->opt->getTime()->getTimestamp()); 452 453 $footer = $this->buildZip64ExtraBlock(); 454 455 $fields = [ 456 ['V', ZipStream::CDR_FILE_SIGNATURE], // Central file header signature 457 ['v', ZipStream::ZIP_VERSION_MADE_BY], // Made by version 458 ['v', $this->version->getValue()], // Extract by version 459 ['v', $this->bits], // General purpose bit flags - data descriptor flag set 460 ['v', $this->method->getValue()], // Compression method 461 ['V', $time], // Timestamp (DOS Format) 462 ['V', $this->crc], // CRC32 463 ['V', $this->zlen->getLowFF()], // Compressed Data Length 464 ['V', $this->len->getLowFF()], // Original Data Length 465 ['v', strlen($name)], // Length of filename 466 ['v', strlen($footer)], // Extra data len (see above) 467 ['v', strlen($comment)], // Length of comment 468 ['v', 0], // Disk number 469 ['v', 0], // Internal File Attributes 470 ['V', 32], // External File Attributes 471 ['V', $this->ofs->getLowFF()] // Relative offset of local header 472 ]; 473 474 // pack fields, then append name and comment 475 $header = ZipStream::packFields($fields); 476 477 return $header . $name . $footer . $comment; 478 } 479 480 /** 481 * @return Bigint 482 */ 483 public function getTotalLength(): Bigint 484 { 485 return $this->totalLength; 486 } 487 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body