<?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;
}
}