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