See Release Notes
Long Term Support Release
<?php>declare(strict_types=1); namespace ZipStream;> use HashContext;use Psr\Http\Message\StreamInterface;< use RuntimeException; < use ZipStream\Exception\EncodingException;use ZipStream\Exception\FileNotFoundException; use ZipStream\Exception\FileNotReadableException; use ZipStream\Exception\OverflowException; use ZipStream\Option\File as FileOptions; use ZipStream\Option\Method; use ZipStream\Option\Version; class File {< const HASH_ALGORITHM = 'crc32b';> public const HASH_ALGORITHM = 'crc32b'; > > public const BIT_ZERO_HEADER = 0x0008;< const BIT_ZERO_HEADER = 0x0008; < const BIT_EFS_UTF8 = 0x0800;> public const BIT_EFS_UTF8 = 0x0800;< const COMPUTE = 1; < const SEND = 2;> public const COMPUTE = 1; > > public const SEND = 2;private const CHUNKED_READ_BLOCK_SIZE = 1048576; /** * @var string */ public $name; /** * @var FileOptions */ public $opt; /** * @var Bigint */ public $len;>/** * @var Bigint */ public $zlen; /** @var int */ public $crc; /** * @var Bigint */ public $hlen; /** * @var Bigint */ public $ofs; /** * @var int */ public $bits; /** * @var Version */ public $version; /** * @var ZipStream */ public $zip; /** * @var resource */ private $deflate; /**< * @var \HashContext> * @var HashContext*/ private $hash; /** * @var Method */ private $method; /** * @var Bigint */ private $totalLength; public function __construct(ZipStream $zip, string $name, ?FileOptions $opt = null) { $this->zip = $zip; $this->name = $name; $this->opt = $opt ?: new FileOptions(); $this->method = $this->opt->getMethod(); $this->version = Version::STORE(); $this->ofs = new Bigint(); } public function processPath(string $path): void { if (!is_readable($path)) { if (!file_exists($path)) { throw new FileNotFoundException($path); } throw new FileNotReadableException($path); } if ($this->zip->isLargeFile($path) === false) { $data = file_get_contents($path); $this->processData($data); } else { $this->method = $this->zip->opt->getLargeFileMethod();< $stream = new DeflateStream(fopen($path, 'rb'));> $stream = new Stream(fopen($path, 'rb'));$this->processStream($stream); $stream->close(); } } public function processData(string $data): void { $this->len = new Bigint(strlen($data)); $this->crc = crc32($data); // compress data if needed if ($this->method->equals(Method::DEFLATE())) { $data = gzdeflate($data); } $this->zlen = new Bigint(strlen($data)); $this->addFileHeader(); $this->zip->send($data); $this->addFileFooter(); } /** * Create and send zip header for this file. * * @return void * @throws \ZipStream\Exception\EncodingException */ public function addFileHeader(): void { $name = static::filterFilename($this->name); // calculate name length $nameLength = strlen($name); // create dos timestamp $time = static::dosTime($this->opt->getTime()->getTimestamp()); $comment = $this->opt->getComment(); if (!mb_check_encoding($name, 'ASCII') || !mb_check_encoding($comment, 'ASCII')) { // Sets Bit 11: Language encoding flag (EFS). If this bit is set, // the filename and comment fields for this file // MUST be encoded using UTF-8. (see APPENDIX D)< if (!mb_check_encoding($name, 'UTF-8') || < !mb_check_encoding($comment, 'UTF-8')) { < throw new EncodingException( < 'File name and comment should use UTF-8 ' . < 'if one of them does not fit into ASCII range.' < ); < }> if (mb_check_encoding($name, 'UTF-8') && > mb_check_encoding($comment, 'UTF-8')) {$this->bits |= self::BIT_EFS_UTF8; }> }if ($this->method->equals(Method::DEFLATE())) { $this->version = Version::DEFLATE(); }< $force = (boolean)($this->bits & self::BIT_ZERO_HEADER) &&> $force = (bool)($this->bits & self::BIT_ZERO_HEADER) &&$this->zip->opt->isEnableZip64(); $footer = $this->buildZip64ExtraBlock($force); // If this file will start over 4GB limit in ZIP file, // CDR record will have to use Zip64 extension to describe offset // to keep consistency we use the same value here if ($this->zip->ofs->isOver32()) { $this->version = Version::ZIP64(); } $fields = [ ['V', ZipStream::FILE_HEADER_SIGNATURE], ['v', $this->version->getValue()], // Version needed to Extract ['v', $this->bits], // General purpose bit flags - data descriptor flag set ['v', $this->method->getValue()], // Compression method ['V', $time], // Timestamp (DOS Format) ['V', $this->crc], // CRC32 of data (0 -> moved to data descriptor footer) ['V', $this->zlen->getLowFF($force)], // Length of compressed data (forced to 0xFFFFFFFF for zero header) ['V', $this->len->getLowFF($force)], // Length of original data (forced to 0xFFFFFFFF for zero header) ['v', $nameLength], // Length of filename ['v', strlen($footer)], // Extra data (see above) ]; // pack fields and calculate "total" length $header = ZipStream::packFields($fields); // print header and filename $data = $header . $name . $footer; $this->zip->send($data); // save header length $this->hlen = Bigint::init(strlen($data)); } /** * Strip characters that are not legal in Windows filenames * to prevent compatibility issues * * @param string $filename Unprocessed filename * @return string */ public static function filterFilename(string $filename): string { // strip leading slashes from file name // (fixes bug in windows archive viewer) $filename = preg_replace('/^\\/+/', '', $filename); return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename); } /**> * Create and send data descriptor footer for this file. * Convert a UNIX timestamp to a DOS timestamp. > * * > * @return void * @param int $when > */ * @return int DOS Timestamp > public function addFileFooter(): void */ > { final protected static function dosTime(int $when): int > if ($this->bits & self::BIT_ZERO_HEADER) { { > // compressed and uncompressed size // get date array for timestamp > $sizeFormat = 'V'; $d = getdate($when); > if ($this->zip->opt->isEnableZip64()) { > $sizeFormat = 'P'; // set lower-bound on dates > } if ($d['year'] < 1980) { > $fields = [ $d = array( > ['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE], 'year' => 1980, > ['V', $this->crc], // CRC32 'mon' => 1, > [$sizeFormat, $this->zlen], // Length of compressed data 'mday' => 1, > [$sizeFormat, $this->len], // Length of original data 'hours' => 0, > ]; 'minutes' => 0, > 'seconds' => 0 > $footer = ZipStream::packFields($fields); ); > $this->zip->send($footer); } > } else { > $footer = ''; // remove extra years from 1980 > } $d['year'] -= 1980; > $this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer))); > $this->zip->addToCdr($this); // return date string > } return > ($d['year'] << 25) | > public function processStream(StreamInterface $stream): void ($d['mon'] << 21) | > { ($d['mday'] << 16) | > $this->zlen = new Bigint(); ($d['hours'] << 11) | > $this->len = new Bigint(); ($d['minutes'] << 5) | > ($d['seconds'] >> 1); > if ($this->zip->opt->isZeroHeader()) { } > $this->processStreamWithZeroHeader($stream); > } else { protected function buildZip64ExtraBlock(bool $force = false): string > $this->processStreamWithComputedHeader($stream); { > } > } $fields = []; > if ($this->len->isOver32($force)) { > /** $fields[] = ['P', $this->len]; // Length of original data > * Send CDR record for specified file. } > * > * @return string if ($this->len->isOver32($force)) { > */ $fields[] = ['P', $this->zlen]; // Length of compressed data > public function getCdrFile(): string } > { > $name = static::filterFilename($this->name); if ($this->ofs->isOver32()) { > $fields[] = ['P', $this->ofs]; // Offset of local header record > // get attributes } > $comment = $this->opt->getComment(); > if (!empty($fields)) { > // get dos timestamp if (!$this->zip->opt->isEnableZip64()) { > $time = static::dosTime($this->opt->getTime()->getTimestamp()); throw new OverflowException(); > } > $footer = $this->buildZip64ExtraBlock(); > array_unshift( > $fields = [ $fields, > ['V', ZipStream::CDR_FILE_SIGNATURE], // Central file header signature ['v', 0x0001], // 64 bit extension > ['v', ZipStream::ZIP_VERSION_MADE_BY], // Made by version ['v', count($fields) * 8] // Length of data block > ['v', $this->version->getValue()], // Extract by version ); > ['v', $this->bits], // General purpose bit flags - data descriptor flag set $this->version = Version::ZIP64(); > ['v', $this->method->getValue()], // Compression method } > ['V', $time], // Timestamp (DOS Format) > ['V', $this->crc], // CRC32 if ($this->bits & self::BIT_EFS_UTF8) { > ['V', $this->zlen->getLowFF()], // Compressed Data Length // Put the tricky entry to > ['V', $this->len->getLowFF()], // Original Data Length // force Linux unzip to lookup EFS flag. > ['v', strlen($name)], // Length of filename $fields[] = ['v', 0x5653]; // Choose 'ZS' for proprietary usage > ['v', strlen($footer)], // Extra data len (see above) $fields[] = ['v', 0x0000]; // zero length > ['v', strlen($comment)], // Length of comment } > ['v', 0], // Disk number > ['v', 0], // Internal File Attributes return ZipStream::packFields($fields); > ['V', 32], // External File Attributes } > ['V', $this->ofs->getLowFF()], // Relative offset of local header > ]; /** > * Create and send data descriptor footer for this file. > // pack fields, then append name and comment * > $header = ZipStream::packFields($fields); * @return void > */ > return $header . $name . $footer . $comment; > } public function addFileFooter(): void > { > /** > * @return Bigint if ($this->bits & self::BIT_ZERO_HEADER) { > */ // compressed and uncompressed size > public function getTotalLength(): Bigint $sizeFormat = 'V'; > { if ($this->zip->opt->isEnableZip64()) { > return $this->totalLength; $sizeFormat = 'P'; > } } > $fields = [ > /**< $d = array(> $d = [< 'seconds' => 0 < );> 'seconds' => 0, > ];<< /** < * Create and send data descriptor footer for this file. < * < * @return void < */ < < public function addFileFooter(): void < { < < if ($this->bits & self::BIT_ZERO_HEADER) { < // compressed and uncompressed size < $sizeFormat = 'V'; < if ($this->zip->opt->isEnableZip64()) { < $sizeFormat = 'P'; < } < $fields = [ < ['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE], < ['V', $this->crc], // CRC32 < [$sizeFormat, $this->zlen], // Length of compressed data < [$sizeFormat, $this->len], // Length of original data < ]; < < $footer = ZipStream::packFields($fields); < $this->zip->send($footer); < } else { < $footer = ''; < } < $this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer))); < $this->zip->addToCdr($this); < } < < public function processStream(StreamInterface $stream): void < { < $this->zlen = new Bigint(); < $this->len = new Bigint(); < < if ($this->zip->opt->isZeroHeader()) { < $this->processStreamWithZeroHeader($stream); < } else { < $this->processStreamWithComputedHeader($stream); < } < } <if ($options & self::SEND) { $this->zip->send($data); } } $this->deflateFinish($options); } protected function deflateInit(): void { $hash = hash_init(self::HASH_ALGORITHM); $this->hash = $hash; if ($this->method->equals(Method::DEFLATE())) { $this->deflate = deflate_init( ZLIB_ENCODING_RAW, ['level' => $this->opt->getDeflateLevel()] ); } } protected function deflateData(StreamInterface $stream, string &$data, ?int $options = null): void { if ($options & self::COMPUTE) { $this->len = $this->len->add(Bigint::init(strlen($data))); hash_update($this->hash, $data); } if ($this->deflate) { $data = deflate_add( $this->deflate, $data, $stream->eof() ? ZLIB_FINISH : ZLIB_NO_FLUSH ); } if ($options & self::COMPUTE) { $this->zlen = $this->zlen->add(Bigint::init(strlen($data))); } } protected function deflateFinish(?int $options = null): void { if ($options & self::COMPUTE) { $this->crc = hexdec(hash_final($this->hash)); } } protected function processStreamWithComputedHeader(StreamInterface $stream): void { $this->readStream($stream, self::COMPUTE); $stream->rewind();< // incremental compression with deflate_add < // makes this second read unnecessary < // but it is only available from PHP 7.0 < if (!$this->deflate && $stream instanceof DeflateStream && $this->method->equals(Method::DEFLATE())) { < $stream->addDeflateFilter($this->opt); < $this->zlen = new Bigint(); < while (!$stream->eof()) { < $data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE); < $this->zlen = $this->zlen->add(Bigint::init(strlen($data))); < } < $stream->rewind(); < } <$this->addFileHeader(); $this->readStream($stream, self::SEND); $this->addFileFooter();< } < < /** < * Send CDR record for specified file. < * < * @return string < */ < public function getCdrFile(): string < { < $name = static::filterFilename($this->name); < < // get attributes < $comment = $this->opt->getComment(); < < // get dos timestamp < $time = static::dosTime($this->opt->getTime()->getTimestamp()); < < $footer = $this->buildZip64ExtraBlock(); < < $fields = [ < ['V', ZipStream::CDR_FILE_SIGNATURE], // Central file header signature < ['v', ZipStream::ZIP_VERSION_MADE_BY], // Made by version < ['v', $this->version->getValue()], // Extract by version < ['v', $this->bits], // General purpose bit flags - data descriptor flag set < ['v', $this->method->getValue()], // Compression method < ['V', $time], // Timestamp (DOS Format) < ['V', $this->crc], // CRC32 < ['V', $this->zlen->getLowFF()], // Compressed Data Length < ['V', $this->len->getLowFF()], // Original Data Length < ['v', strlen($name)], // Length of filename < ['v', strlen($footer)], // Extra data len (see above) < ['v', strlen($comment)], // Length of comment < ['v', 0], // Disk number < ['v', 0], // Internal File Attributes < ['V', 32], // External File Attributes < ['V', $this->ofs->getLowFF()] // Relative offset of local header < ]; < < // pack fields, then append name and comment < $header = ZipStream::packFields($fields); < < return $header . $name . $footer . $comment; < } < < /** < * @return Bigint < */ < public function getTotalLength(): Bigint < { < return $this->totalLength;} }