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