mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 12:32:08 +02:00
MDL-78256 lib: Upgrade Zipstream to 3.1.0
This commit is contained in:
parent
328b48ebc5
commit
6972e9c83d
@ -1,174 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use OverflowException;
|
||||
|
||||
class Bigint
|
||||
{
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $bytes = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
/**
|
||||
* Initialize the bytes array
|
||||
*
|
||||
* @param int $value
|
||||
*/
|
||||
public function __construct(int $value = 0)
|
||||
{
|
||||
$this->fillBytes($value, 0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance
|
||||
*
|
||||
* @param int $value
|
||||
* @return Bigint
|
||||
*/
|
||||
public static function init(int $value = 0): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill bytes from low to high
|
||||
*
|
||||
* @param int $low
|
||||
* @param int $high
|
||||
* @return Bigint
|
||||
*/
|
||||
public static function fromLowHigh(int $low, int $high): self
|
||||
{
|
||||
$bigint = new self();
|
||||
$bigint->fillBytes($low, 0, 4);
|
||||
$bigint->fillBytes($high, 4, 4);
|
||||
return $bigint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high 32
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getHigh32(): int
|
||||
{
|
||||
return $this->getValue(4, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from bytes array
|
||||
*
|
||||
* @param int $end
|
||||
* @param int $length
|
||||
* @return int
|
||||
*/
|
||||
public function getValue(int $end = 0, int $length = 8): int
|
||||
{
|
||||
$result = 0;
|
||||
for ($i = $end + $length - 1; $i >= $end; $i--) {
|
||||
$result <<= 8;
|
||||
$result |= $this->bytes[$i];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get low FF
|
||||
*
|
||||
* @param bool $force
|
||||
* @return float
|
||||
*/
|
||||
public function getLowFF(bool $force = false): float
|
||||
{
|
||||
if ($force || $this->isOver32()) {
|
||||
return (float)0xFFFFFFFF;
|
||||
}
|
||||
return (float)$this->getLow32();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if is over 32
|
||||
*
|
||||
* @psalm-suppress ArgumentTypeCoercion
|
||||
* @param bool $force
|
||||
* @return bool
|
||||
*/
|
||||
public function isOver32(bool $force = false): bool
|
||||
{
|
||||
// value 0xFFFFFFFF already needs a Zip64 header
|
||||
return $force ||
|
||||
max(array_slice($this->bytes, 4, 4)) > 0 ||
|
||||
min(array_slice($this->bytes, 0, 4)) === 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get low 32
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLow32(): int
|
||||
{
|
||||
return $this->getValue(0, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hexadecimal
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHex64(): string
|
||||
{
|
||||
$result = '0x';
|
||||
for ($i = 7; $i >= 0; $i--) {
|
||||
$result .= sprintf('%02X', $this->bytes[$i]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add
|
||||
*
|
||||
* @param Bigint $other
|
||||
* @return Bigint
|
||||
*/
|
||||
public function add(self $other): self
|
||||
{
|
||||
$result = clone $this;
|
||||
$overflow = false;
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$result->bytes[$i] += $other->bytes[$i];
|
||||
if ($overflow) {
|
||||
$result->bytes[$i]++;
|
||||
$overflow = false;
|
||||
}
|
||||
if ($result->bytes[$i] & 0x100) {
|
||||
$overflow = true;
|
||||
$result->bytes[$i] &= 0xFF;
|
||||
}
|
||||
}
|
||||
if ($overflow) {
|
||||
throw new OverflowException();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the bytes field with int
|
||||
*
|
||||
* @param int $value
|
||||
* @param int $start
|
||||
* @param int $count
|
||||
* @return void
|
||||
*/
|
||||
protected function fillBytes(int $value, int $start, int $count): void
|
||||
{
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$this->bytes[$start + $i] = $i >= PHP_INT_SIZE ? 0 : $value & 0xFF;
|
||||
$value >>= 8;
|
||||
}
|
||||
}
|
||||
}
|
52
lib/zipstream/src/CentralDirectoryFileHeader.php
Normal file
52
lib/zipstream/src/CentralDirectoryFileHeader.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class CentralDirectoryFileHeader
|
||||
{
|
||||
private const SIGNATURE = 0x02014b50;
|
||||
|
||||
public static function generate(
|
||||
int $versionMadeBy,
|
||||
int $versionNeededToExtract,
|
||||
int $generalPurposeBitFlag,
|
||||
CompressionMethod $compressionMethod,
|
||||
DateTimeInterface $lastModificationDateTime,
|
||||
int $crc32,
|
||||
int $compressedSize,
|
||||
int $uncompressedSize,
|
||||
string $fileName,
|
||||
string $extraField,
|
||||
string $fileComment,
|
||||
int $diskNumberStart,
|
||||
int $internalFileAttributes,
|
||||
int $externalFileAttributes,
|
||||
int $relativeOffsetOfLocalHeader,
|
||||
): string {
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: self::SIGNATURE),
|
||||
new PackField(format: 'v', value: $versionMadeBy),
|
||||
new PackField(format: 'v', value: $versionNeededToExtract),
|
||||
new PackField(format: 'v', value: $generalPurposeBitFlag),
|
||||
new PackField(format: 'v', value: $compressionMethod->value),
|
||||
new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)),
|
||||
new PackField(format: 'V', value: $crc32),
|
||||
new PackField(format: 'V', value: $compressedSize),
|
||||
new PackField(format: 'V', value: $uncompressedSize),
|
||||
new PackField(format: 'v', value: strlen($fileName)),
|
||||
new PackField(format: 'v', value: strlen($extraField)),
|
||||
new PackField(format: 'v', value: strlen($fileComment)),
|
||||
new PackField(format: 'v', value: $diskNumberStart),
|
||||
new PackField(format: 'v', value: $internalFileAttributes),
|
||||
new PackField(format: 'V', value: $externalFileAttributes),
|
||||
new PackField(format: 'V', value: $relativeOffsetOfLocalHeader),
|
||||
) . $fileName . $extraField . $fileComment;
|
||||
}
|
||||
}
|
106
lib/zipstream/src/CompressionMethod.php
Normal file
106
lib/zipstream/src/CompressionMethod.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
enum CompressionMethod: int
|
||||
{
|
||||
/**
|
||||
* The file is stored (no compression)
|
||||
*/
|
||||
case STORE = 0x00;
|
||||
|
||||
// 0x01: legacy algorithm - The file is Shrunk
|
||||
// 0x02: legacy algorithm - The file is Reduced with compression factor 1
|
||||
// 0x03: legacy algorithm - The file is Reduced with compression factor 2
|
||||
// 0x04: legacy algorithm - The file is Reduced with compression factor 3
|
||||
// 0x05: legacy algorithm - The file is Reduced with compression factor 4
|
||||
// 0x06: legacy algorithm - The file is Imploded
|
||||
// 0x07: Reserved for Tokenizing compression algorithm
|
||||
|
||||
/**
|
||||
* The file is Deflated
|
||||
*/
|
||||
case DEFLATE = 0x08;
|
||||
|
||||
// /**
|
||||
// * Enhanced Deflating using Deflate64(tm)
|
||||
// */
|
||||
// case DEFLATE_64 = 0x09;
|
||||
|
||||
// /**
|
||||
// * PKWARE Data Compression Library Imploding (old IBM TERSE)
|
||||
// */
|
||||
// case PKWARE = 0x0a;
|
||||
|
||||
// // 0x0b: Reserved by PKWARE
|
||||
|
||||
// /**
|
||||
// * File is compressed using BZIP2 algorithm
|
||||
// */
|
||||
// case BZIP2 = 0x0c;
|
||||
|
||||
// // 0x0d: Reserved by PKWARE
|
||||
|
||||
// /**
|
||||
// * LZMA
|
||||
// */
|
||||
// case LZMA = 0x0e;
|
||||
|
||||
// // 0x0f: Reserved by PKWARE
|
||||
|
||||
// /**
|
||||
// * IBM z/OS CMPSC Compression
|
||||
// */
|
||||
// case IBM_ZOS_CMPSC = 0x10;
|
||||
|
||||
// // 0x11: Reserved by PKWARE
|
||||
|
||||
// /**
|
||||
// * File is compressed using IBM TERSE
|
||||
// */
|
||||
// case IBM_TERSE = 0x12;
|
||||
|
||||
// /**
|
||||
// * IBM LZ77 z Architecture
|
||||
// */
|
||||
// case IBM_LZ77 = 0x13;
|
||||
|
||||
// // 0x14: deprecated (use method 93 for zstd)
|
||||
|
||||
// /**
|
||||
// * Zstandard (zstd) Compression
|
||||
// */
|
||||
// case ZSTD = 0x5d;
|
||||
|
||||
// /**
|
||||
// * MP3 Compression
|
||||
// */
|
||||
// case MP3 = 0x5e;
|
||||
|
||||
// /**
|
||||
// * XZ Compression
|
||||
// */
|
||||
// case XZ = 0x5f;
|
||||
|
||||
// /**
|
||||
// * JPEG variant
|
||||
// */
|
||||
// case JPEG = 0x60;
|
||||
|
||||
// /**
|
||||
// * WavPack compressed data
|
||||
// */
|
||||
// case WAV_PACK = 0x61;
|
||||
|
||||
// /**
|
||||
// * PPMd version I, Rev 1
|
||||
// */
|
||||
// case PPMD_1_1 = 0x62;
|
||||
|
||||
// /**
|
||||
// * AE-x encryption marker
|
||||
// */
|
||||
// case AE_X_ENCRYPTION = 0x63;
|
||||
}
|
26
lib/zipstream/src/DataDescriptor.php
Normal file
26
lib/zipstream/src/DataDescriptor.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class DataDescriptor
|
||||
{
|
||||
private const SIGNATURE = 0x08074b50;
|
||||
|
||||
public static function generate(
|
||||
int $crc32UncompressedData,
|
||||
int $compressedSize,
|
||||
int $uncompressedSize,
|
||||
): string {
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: self::SIGNATURE),
|
||||
new PackField(format: 'V', value: $crc32UncompressedData),
|
||||
new PackField(format: 'V', value: $compressedSize),
|
||||
new PackField(format: 'V', value: $uncompressedSize),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
class DeflateStream extends Stream
|
||||
{
|
||||
public function __construct($stream)
|
||||
{
|
||||
parent::__construct($stream);
|
||||
trigger_error('Class ' . __CLASS__ . ' is deprecated, delation will be handled internally instead', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
public function removeDeflateFilter(): void
|
||||
{
|
||||
trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
public function addDeflateFilter(Option\File $options): void
|
||||
{
|
||||
trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
35
lib/zipstream/src/EndOfCentralDirectory.php
Normal file
35
lib/zipstream/src/EndOfCentralDirectory.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class EndOfCentralDirectory
|
||||
{
|
||||
private const SIGNATURE = 0x06054b50;
|
||||
|
||||
public static function generate(
|
||||
int $numberOfThisDisk,
|
||||
int $numberOfTheDiskWithCentralDirectoryStart,
|
||||
int $numberOfCentralDirectoryEntriesOnThisDisk,
|
||||
int $numberOfCentralDirectoryEntries,
|
||||
int $sizeOfCentralDirectory,
|
||||
int $centralDirectoryStartOffsetOnDisk,
|
||||
string $zipFileComment,
|
||||
): string {
|
||||
/** @psalm-suppress MixedArgument */
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: static::SIGNATURE),
|
||||
new PackField(format: 'v', value: $numberOfThisDisk),
|
||||
new PackField(format: 'v', value: $numberOfTheDiskWithCentralDirectoryStart),
|
||||
new PackField(format: 'v', value: $numberOfCentralDirectoryEntriesOnThisDisk),
|
||||
new PackField(format: 'v', value: $numberOfCentralDirectoryEntries),
|
||||
new PackField(format: 'V', value: $sizeOfCentralDirectory),
|
||||
new PackField(format: 'V', value: $centralDirectoryStartOffsetOnDisk),
|
||||
new PackField(format: 'v', value: strlen($zipFileComment)),
|
||||
) . $zipFileComment;
|
||||
}
|
||||
}
|
@ -4,9 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* This class is only for inheriting
|
||||
*/
|
||||
abstract class Exception extends \Exception
|
||||
{
|
||||
}
|
||||
|
23
lib/zipstream/src/Exception/DosTimeOverflowException.php
Normal file
23
lib/zipstream/src/Exception/DosTimeOverflowException.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use DateTimeInterface;
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if a file wasn't found
|
||||
*/
|
||||
class DosTimeOverflowException extends Exception
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly DateTimeInterface $dateTime
|
||||
) {
|
||||
parent::__construct('The date ' . $dateTime->format(DateTimeInterface::ATOM) . " can't be represented as DOS time / date.");
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if file or comment encoding is incorrect
|
||||
*/
|
||||
class EncodingException extends Exception
|
||||
{
|
||||
}
|
@ -12,12 +12,11 @@ use ZipStream\Exception;
|
||||
class FileNotFoundException extends Exception
|
||||
{
|
||||
/**
|
||||
* Constructor of the Exception
|
||||
*
|
||||
* @param String $path - The path which wasn't found
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $path
|
||||
) {
|
||||
parent::__construct("The file with the path $path wasn't found.");
|
||||
}
|
||||
}
|
||||
|
@ -12,12 +12,11 @@ use ZipStream\Exception;
|
||||
class FileNotReadableException extends Exception
|
||||
{
|
||||
/**
|
||||
* Constructor of the Exception
|
||||
*
|
||||
* @param String $path - The path which wasn't found
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $path
|
||||
) {
|
||||
parent::__construct("The file with the path $path isn't readable.");
|
||||
}
|
||||
}
|
||||
|
23
lib/zipstream/src/Exception/FileSizeIncorrectException.php
Normal file
23
lib/zipstream/src/Exception/FileSizeIncorrectException.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if a file is not as large as it was specified.
|
||||
*/
|
||||
class FileSizeIncorrectException extends Exception
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $expectedSize,
|
||||
public readonly int $actualSize
|
||||
) {
|
||||
parent::__construct("File is {$actualSize} instead of {$expectedSize} bytes large. Adjust `exactSize` parameter.");
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if options are incompatible
|
||||
*/
|
||||
class IncompatibleOptionsException extends Exception
|
||||
{
|
||||
}
|
@ -11,6 +11,9 @@ use ZipStream\Exception;
|
||||
*/
|
||||
class OverflowException extends Exception
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.');
|
||||
|
29
lib/zipstream/src/Exception/ResourceActionException.php
Normal file
29
lib/zipstream/src/Exception/ResourceActionException.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if a resource like `fread` returns false
|
||||
*/
|
||||
class ResourceActionException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var ?resource
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $function,
|
||||
$resource = null,
|
||||
) {
|
||||
$this->resource = $resource;
|
||||
parent::__construct('Function ' . $function . 'failed on resource.');
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if a strict simulation is executed and the file
|
||||
* information can't be determined without reading the entire file.
|
||||
*/
|
||||
class SimulationFileUnknownException extends Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The details of the strict simulation file could not be determined without reading the entire file.');
|
||||
}
|
||||
}
|
@ -7,17 +7,15 @@ namespace ZipStream\Exception;
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if `fread` fails on a stream.
|
||||
* This Exception gets invoked if a stream can't be read.
|
||||
*/
|
||||
class StreamNotReadableException extends Exception
|
||||
{
|
||||
/**
|
||||
* Constructor of the Exception
|
||||
*
|
||||
* @param string $fileName - The name of the file which the stream belongs to.
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(string $fileName)
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct("The stream for $fileName could not be read.");
|
||||
parent::__construct('The stream could not be read.');
|
||||
}
|
||||
}
|
||||
|
22
lib/zipstream/src/Exception/StreamNotSeekableException.php
Normal file
22
lib/zipstream/src/Exception/StreamNotSeekableException.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Exception;
|
||||
|
||||
use ZipStream\Exception;
|
||||
|
||||
/**
|
||||
* This Exception gets invoked if a non seekable stream is
|
||||
* provided and zero headers are disabled.
|
||||
*/
|
||||
class StreamNotSeekableException extends Exception
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('enableZeroHeader must be enable to add non seekable streams');
|
||||
}
|
||||
}
|
@ -4,467 +4,417 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use HashContext;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use ZipStream\Exception\FileNotFoundException;
|
||||
use ZipStream\Exception\FileNotReadableException;
|
||||
use Closure;
|
||||
use DateTimeInterface;
|
||||
use DeflateContext;
|
||||
use RuntimeException;
|
||||
use ZipStream\Exception\FileSizeIncorrectException;
|
||||
use ZipStream\Exception\OverflowException;
|
||||
use ZipStream\Option\File as FileOptions;
|
||||
use ZipStream\Option\Method;
|
||||
use ZipStream\Option\Version;
|
||||
use ZipStream\Exception\ResourceActionException;
|
||||
use ZipStream\Exception\SimulationFileUnknownException;
|
||||
use ZipStream\Exception\StreamNotReadableException;
|
||||
use ZipStream\Exception\StreamNotSeekableException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class File
|
||||
{
|
||||
public const HASH_ALGORITHM = 'crc32b';
|
||||
private const CHUNKED_READ_BLOCK_SIZE = 0x1000000;
|
||||
|
||||
public const BIT_ZERO_HEADER = 0x0008;
|
||||
private Version $version;
|
||||
|
||||
public const BIT_EFS_UTF8 = 0x0800;
|
||||
private int $compressedSize = 0;
|
||||
|
||||
public const COMPUTE = 1;
|
||||
private int $uncompressedSize = 0;
|
||||
|
||||
public const SEND = 2;
|
||||
private int $crc = 0;
|
||||
|
||||
private const CHUNKED_READ_BLOCK_SIZE = 1048576;
|
||||
private int $generalPurposeBitFlag = 0;
|
||||
|
||||
private readonly string $fileName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var resource|null
|
||||
*/
|
||||
public $name;
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* @var FileOptions
|
||||
* @param Closure $dataCallback
|
||||
* @psalm-param Closure(): resource $dataCallback
|
||||
*/
|
||||
public $opt;
|
||||
public function __construct(
|
||||
string $fileName,
|
||||
private readonly Closure $dataCallback,
|
||||
private readonly OperationMode $operationMode,
|
||||
private readonly int $startOffset,
|
||||
private readonly CompressionMethod $compressionMethod,
|
||||
private readonly string $comment,
|
||||
private readonly DateTimeInterface $lastModificationDateTime,
|
||||
private readonly int $deflateLevel,
|
||||
private readonly ?int $maxSize,
|
||||
private readonly ?int $exactSize,
|
||||
private readonly bool $enableZip64,
|
||||
private readonly bool $enableZeroHeader,
|
||||
private readonly Closure $send,
|
||||
private readonly Closure $recordSentBytes,
|
||||
) {
|
||||
$this->fileName = self::filterFilename($fileName);
|
||||
$this->checkEncoding();
|
||||
|
||||
/**
|
||||
* @var Bigint
|
||||
*/
|
||||
public $len;
|
||||
if ($this->enableZeroHeader) {
|
||||
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
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();
|
||||
$this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE;
|
||||
}
|
||||
|
||||
public function processPath(string $path): void
|
||||
public function cloneSimulationExecution(): self
|
||||
{
|
||||
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);
|
||||
return new self(
|
||||
$this->fileName,
|
||||
$this->dataCallback,
|
||||
OperationMode::NORMAL,
|
||||
$this->startOffset,
|
||||
$this->compressionMethod,
|
||||
$this->comment,
|
||||
$this->lastModificationDateTime,
|
||||
$this->deflateLevel,
|
||||
$this->maxSize,
|
||||
$this->exactSize,
|
||||
$this->enableZip64,
|
||||
$this->enableZeroHeader,
|
||||
$this->send,
|
||||
$this->recordSentBytes,
|
||||
);
|
||||
}
|
||||
|
||||
public function process(): string
|
||||
{
|
||||
$forecastSize = $this->forecastSize();
|
||||
|
||||
if ($this->enableZeroHeader) {
|
||||
// No calculation required
|
||||
} elseif ($this->isSimulation() && $forecastSize) {
|
||||
$this->uncompressedSize = $forecastSize;
|
||||
$this->compressedSize = $forecastSize;
|
||||
} else {
|
||||
$this->method = $this->zip->opt->getLargeFileMethod();
|
||||
|
||||
$stream = new Stream(fopen($path, 'rb'));
|
||||
$this->processStream($stream);
|
||||
$stream->close();
|
||||
$this->readStream(send: false);
|
||||
if (rewind($this->unpackStream()) === false) {
|
||||
throw new ResourceActionException('rewind', $this->unpackStream());
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFileHeader();
|
||||
|
||||
$detectedSize = $forecastSize ?? $this->compressedSize;
|
||||
|
||||
if (
|
||||
$this->isSimulation() &&
|
||||
$detectedSize > 0
|
||||
) {
|
||||
($this->recordSentBytes)($detectedSize);
|
||||
} else {
|
||||
$this->readStream(send: true);
|
||||
}
|
||||
|
||||
$this->addFileFooter();
|
||||
return $this->getCdrFile();
|
||||
}
|
||||
|
||||
public function processData(string $data): void
|
||||
/**
|
||||
* @return resource
|
||||
*/
|
||||
private function unpackStream()
|
||||
{
|
||||
$this->len = new Bigint(strlen($data));
|
||||
$this->crc = crc32($data);
|
||||
|
||||
// compress data if needed
|
||||
if ($this->method->equals(Method::DEFLATE())) {
|
||||
$data = gzdeflate($data);
|
||||
if ($this->stream) {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
$this->zlen = new Bigint(strlen($data));
|
||||
$this->addFileHeader();
|
||||
$this->zip->send($data);
|
||||
$this->addFileFooter();
|
||||
if ($this->operationMode === OperationMode::SIMULATE_STRICT) {
|
||||
throw new SimulationFileUnknownException();
|
||||
}
|
||||
|
||||
$this->stream = ($this->dataCallback)();
|
||||
|
||||
if (!$this->enableZeroHeader && !stream_get_meta_data($this->stream)['seekable']) {
|
||||
throw new StreamNotSeekableException();
|
||||
}
|
||||
if (!(
|
||||
str_contains(stream_get_meta_data($this->stream)['mode'], 'r')
|
||||
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'w+')
|
||||
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'a+')
|
||||
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'x+')
|
||||
|| str_contains(stream_get_meta_data($this->stream)['mode'], 'c+')
|
||||
)) {
|
||||
throw new StreamNotReadableException();
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
private function forecastSize(): ?int
|
||||
{
|
||||
if ($this->compressionMethod !== CompressionMethod::STORE) {
|
||||
return null;
|
||||
}
|
||||
if ($this->exactSize) {
|
||||
return $this->exactSize;
|
||||
}
|
||||
$fstat = fstat($this->unpackStream());
|
||||
if (!$fstat || !array_key_exists('size', $fstat) || $fstat['size'] < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->maxSize !== null && $this->maxSize < $fstat['size']) {
|
||||
return $this->maxSize;
|
||||
}
|
||||
|
||||
return $fstat['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send zip header for this file.
|
||||
*
|
||||
* @return void
|
||||
* @throws \ZipStream\Exception\EncodingException
|
||||
*/
|
||||
public function addFileHeader(): void
|
||||
private function addFileHeader(): void
|
||||
{
|
||||
$name = static::filterFilename($this->name);
|
||||
$forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64;
|
||||
|
||||
// calculate name length
|
||||
$nameLength = strlen($name);
|
||||
$footer = $this->buildZip64ExtraBlock($forceEnableZip64);
|
||||
|
||||
// create dos timestamp
|
||||
$time = static::dosTime($this->opt->getTime()->getTimestamp());
|
||||
$zip64Enabled = $footer !== '';
|
||||
|
||||
$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')) {
|
||||
$this->bits |= self::BIT_EFS_UTF8;
|
||||
}
|
||||
if($zip64Enabled) {
|
||||
$this->version = Version::ZIP64;
|
||||
}
|
||||
|
||||
if ($this->method->equals(Method::DEFLATE())) {
|
||||
$this->version = Version::DEFLATE();
|
||||
if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) {
|
||||
// Put the tricky entry to
|
||||
// force Linux unzip to lookup EFS flag.
|
||||
$footer .= Zs\ExtendedInformationExtraField::generate();
|
||||
}
|
||||
|
||||
$force = (bool)($this->bits & self::BIT_ZERO_HEADER) &&
|
||||
$this->zip->opt->isEnableZip64();
|
||||
$data = LocalFileHeader::generate(
|
||||
versionNeededToExtract: $this->version->value,
|
||||
generalPurposeBitFlag: $this->generalPurposeBitFlag,
|
||||
compressionMethod: $this->compressionMethod,
|
||||
lastModificationDateTime: $this->lastModificationDateTime,
|
||||
crc32UncompressedData: $this->crc,
|
||||
compressedSize: $zip64Enabled
|
||||
? 0xFFFFFFFF
|
||||
: $this->compressedSize,
|
||||
uncompressedSize: $zip64Enabled
|
||||
? 0xFFFFFFFF
|
||||
: $this->uncompressedSize,
|
||||
fileName: $this->fileName,
|
||||
extraField: $footer,
|
||||
);
|
||||
|
||||
$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));
|
||||
($this->send)($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
|
||||
{
|
||||
private static function filterFilename(
|
||||
/**
|
||||
* Unprocessed filename
|
||||
*/
|
||||
string $fileName
|
||||
): string {
|
||||
// strip leading slashes from file name
|
||||
// (fixes bug in windows archive viewer)
|
||||
$filename = preg_replace('/^\\/+/', '', $filename);
|
||||
$fileName = ltrim($fileName, '/');
|
||||
|
||||
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename);
|
||||
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send data descriptor footer for this file.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addFileFooter(): void
|
||||
private function checkEncoding(): void
|
||||
{
|
||||
if ($this->bits & self::BIT_ZERO_HEADER) {
|
||||
// compressed and uncompressed size
|
||||
$sizeFormat = 'V';
|
||||
if ($this->zip->opt->isEnableZip64()) {
|
||||
$sizeFormat = 'P';
|
||||
// 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($this->fileName, 'UTF-8') &&
|
||||
mb_check_encoding($this->comment, 'UTF-8')) {
|
||||
$this->generalPurposeBitFlag |= GeneralPurposeBitFlag::EFS;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildZip64ExtraBlock(bool $force = false): string
|
||||
{
|
||||
$outputZip64ExtraBlock = false;
|
||||
|
||||
$originalSize = null;
|
||||
if ($force || $this->uncompressedSize > 0xFFFFFFFF) {
|
||||
$outputZip64ExtraBlock = true;
|
||||
$originalSize = $this->uncompressedSize;
|
||||
}
|
||||
|
||||
$compressedSize = null;
|
||||
if ($force || $this->compressedSize > 0xFFFFFFFF) {
|
||||
$outputZip64ExtraBlock = true;
|
||||
$compressedSize = $this->compressedSize;
|
||||
}
|
||||
|
||||
// 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
|
||||
$relativeHeaderOffset = null;
|
||||
if ($this->startOffset > 0xFFFFFFFF) {
|
||||
$outputZip64ExtraBlock = true;
|
||||
$relativeHeaderOffset = $this->startOffset;
|
||||
}
|
||||
|
||||
if (!$outputZip64ExtraBlock) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->enableZip64) {
|
||||
throw new OverflowException();
|
||||
}
|
||||
|
||||
return Zip64\ExtendedInformationExtraField::generate(
|
||||
originalSize: $originalSize,
|
||||
compressedSize: $compressedSize,
|
||||
relativeHeaderOffset: $relativeHeaderOffset,
|
||||
diskStartNumber: null,
|
||||
);
|
||||
}
|
||||
|
||||
private function addFileFooter(): void
|
||||
{
|
||||
if (($this->compressedSize > 0xFFFFFFFF || $this->uncompressedSize > 0xFFFFFFFF) && $this->version !== Version::ZIP64) {
|
||||
throw new OverflowException();
|
||||
}
|
||||
|
||||
if (!$this->enableZeroHeader) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->version === Version::ZIP64) {
|
||||
$footer = Zip64\DataDescriptor::generate(
|
||||
crc32UncompressedData: $this->crc,
|
||||
compressedSize: $this->compressedSize,
|
||||
uncompressedSize: $this->uncompressedSize,
|
||||
);
|
||||
} else {
|
||||
$footer = DataDescriptor::generate(
|
||||
crc32UncompressedData: $this->crc,
|
||||
compressedSize: $this->compressedSize,
|
||||
uncompressedSize: $this->uncompressedSize,
|
||||
);
|
||||
}
|
||||
|
||||
($this->send)($footer);
|
||||
}
|
||||
|
||||
private function readStream(bool $send): void
|
||||
{
|
||||
$this->compressedSize = 0;
|
||||
$this->uncompressedSize = 0;
|
||||
$hash = hash_init('crc32b');
|
||||
|
||||
$deflate = $this->compressionInit();
|
||||
|
||||
while (
|
||||
!feof($this->unpackStream()) &&
|
||||
($this->maxSize === null || $this->uncompressedSize < $this->maxSize) &&
|
||||
($this->exactSize === null || $this->uncompressedSize < $this->exactSize)
|
||||
) {
|
||||
$readLength = min(
|
||||
($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize,
|
||||
($this->exactSize ?? PHP_INT_MAX) - $this->uncompressedSize,
|
||||
self::CHUNKED_READ_BLOCK_SIZE
|
||||
);
|
||||
|
||||
$data = fread($this->unpackStream(), $readLength);
|
||||
|
||||
hash_update($hash, $data);
|
||||
|
||||
$this->uncompressedSize += strlen($data);
|
||||
|
||||
if ($deflate) {
|
||||
$data = deflate_add(
|
||||
$deflate,
|
||||
$data,
|
||||
feof($this->unpackStream()) ? ZLIB_FINISH : ZLIB_NO_FLUSH
|
||||
);
|
||||
}
|
||||
$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->compressedSize += strlen($data);
|
||||
|
||||
if ($send) {
|
||||
($this->send)($data);
|
||||
}
|
||||
}
|
||||
$this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer)));
|
||||
$this->zip->addToCdr($this);
|
||||
|
||||
if ($this->exactSize && $this->uncompressedSize !== $this->exactSize) {
|
||||
throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize);
|
||||
}
|
||||
|
||||
$this->crc = hexdec(hash_final($hash));
|
||||
}
|
||||
|
||||
public function processStream(StreamInterface $stream): void
|
||||
private function compressionInit(): ?DeflateContext
|
||||
{
|
||||
$this->zlen = new Bigint();
|
||||
$this->len = new Bigint();
|
||||
switch($this->compressionMethod) {
|
||||
case CompressionMethod::STORE:
|
||||
// Noting to do
|
||||
return null;
|
||||
case CompressionMethod::DEFLATE:
|
||||
$deflateContext = deflate_init(
|
||||
ZLIB_ENCODING_RAW,
|
||||
['level' => $this->deflateLevel]
|
||||
);
|
||||
|
||||
if ($this->zip->opt->isZeroHeader()) {
|
||||
$this->processStreamWithZeroHeader($stream);
|
||||
} else {
|
||||
$this->processStreamWithComputedHeader($stream);
|
||||
if (!$deflateContext) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new RuntimeException("Can't initialize deflate context.");
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// False positive, resource is no longer returned from this function
|
||||
return $deflateContext;
|
||||
default:
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new RuntimeException('Unsupported Compression Method ' . print_r($this->compressionMethod, true));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send CDR record for specified file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCdrFile(): string
|
||||
private 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 CentralDirectoryFileHeader::generate(
|
||||
versionMadeBy: ZipStream::ZIP_VERSION_MADE_BY,
|
||||
versionNeededToExtract:$this->version->value,
|
||||
generalPurposeBitFlag: $this->generalPurposeBitFlag,
|
||||
compressionMethod: $this->compressionMethod,
|
||||
lastModificationDateTime: $this->lastModificationDateTime,
|
||||
crc32: $this->crc,
|
||||
compressedSize: $this->compressedSize > 0xFFFFFFFF
|
||||
? 0xFFFFFFFF
|
||||
: $this->compressedSize,
|
||||
uncompressedSize: $this->uncompressedSize > 0xFFFFFFFF
|
||||
? 0xFFFFFFFF
|
||||
: $this->uncompressedSize,
|
||||
fileName: $this->fileName,
|
||||
extraField: $footer,
|
||||
fileComment: $this->comment,
|
||||
diskNumberStart: 0,
|
||||
internalFileAttributes: 0,
|
||||
externalFileAttributes: 32,
|
||||
relativeOffsetOfLocalHeader: $this->startOffset > 0xFFFFFFFF
|
||||
? 0xFFFFFFFF
|
||||
: $this->startOffset,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Bigint
|
||||
*/
|
||||
public function getTotalLength(): Bigint
|
||||
private function isSimulation(): bool
|
||||
{
|
||||
return $this->totalLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UNIX timestamp to a DOS timestamp.
|
||||
*
|
||||
* @param int $when
|
||||
* @return int DOS Timestamp
|
||||
*/
|
||||
final protected static function dosTime(int $when): int
|
||||
{
|
||||
// get date array for timestamp
|
||||
$d = getdate($when);
|
||||
|
||||
// set lower-bound on dates
|
||||
if ($d['year'] < 1980) {
|
||||
$d = [
|
||||
'year' => 1980,
|
||||
'mon' => 1,
|
||||
'mday' => 1,
|
||||
'hours' => 0,
|
||||
'minutes' => 0,
|
||||
'seconds' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// remove extra years from 1980
|
||||
$d['year'] -= 1980;
|
||||
|
||||
// return date string
|
||||
return
|
||||
($d['year'] << 25) |
|
||||
($d['mon'] << 21) |
|
||||
($d['mday'] << 16) |
|
||||
($d['hours'] << 11) |
|
||||
($d['minutes'] << 5) |
|
||||
($d['seconds'] >> 1);
|
||||
}
|
||||
|
||||
protected function buildZip64ExtraBlock(bool $force = false): string
|
||||
{
|
||||
$fields = [];
|
||||
if ($this->len->isOver32($force)) {
|
||||
$fields[] = ['P', $this->len]; // Length of original data
|
||||
}
|
||||
|
||||
if ($this->len->isOver32($force)) {
|
||||
$fields[] = ['P', $this->zlen]; // Length of compressed data
|
||||
}
|
||||
|
||||
if ($this->ofs->isOver32()) {
|
||||
$fields[] = ['P', $this->ofs]; // Offset of local header record
|
||||
}
|
||||
|
||||
if (!empty($fields)) {
|
||||
if (!$this->zip->opt->isEnableZip64()) {
|
||||
throw new OverflowException();
|
||||
}
|
||||
|
||||
array_unshift(
|
||||
$fields,
|
||||
['v', 0x0001], // 64 bit extension
|
||||
['v', count($fields) * 8] // Length of data block
|
||||
);
|
||||
$this->version = Version::ZIP64();
|
||||
}
|
||||
|
||||
if ($this->bits & self::BIT_EFS_UTF8) {
|
||||
// Put the tricky entry to
|
||||
// force Linux unzip to lookup EFS flag.
|
||||
$fields[] = ['v', 0x5653]; // Choose 'ZS' for proprietary usage
|
||||
$fields[] = ['v', 0x0000]; // zero length
|
||||
}
|
||||
|
||||
return ZipStream::packFields($fields);
|
||||
}
|
||||
|
||||
protected function processStreamWithZeroHeader(StreamInterface $stream): void
|
||||
{
|
||||
$this->bits |= self::BIT_ZERO_HEADER;
|
||||
$this->addFileHeader();
|
||||
$this->readStream($stream, self::COMPUTE | self::SEND);
|
||||
$this->addFileFooter();
|
||||
}
|
||||
|
||||
protected function readStream(StreamInterface $stream, ?int $options = null): void
|
||||
{
|
||||
$this->deflateInit();
|
||||
$total = 0;
|
||||
$size = $this->opt->getSize();
|
||||
while (!$stream->eof() && ($size === 0 || $total < $size)) {
|
||||
$data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE);
|
||||
$total += strlen($data);
|
||||
if ($size > 0 && $total > $size) {
|
||||
$data = substr($data, 0, strlen($data)-($total - $size));
|
||||
}
|
||||
$this->deflateData($stream, $data, $options);
|
||||
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();
|
||||
|
||||
$this->addFileHeader();
|
||||
$this->readStream($stream, self::SEND);
|
||||
$this->addFileFooter();
|
||||
return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT;
|
||||
}
|
||||
}
|
||||
|
89
lib/zipstream/src/GeneralPurposeBitFlag.php
Normal file
89
lib/zipstream/src/GeneralPurposeBitFlag.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class GeneralPurposeBitFlag
|
||||
{
|
||||
/**
|
||||
* If set, indicates that the file is encrypted.
|
||||
*/
|
||||
public const ENCRYPTED = 1 << 0;
|
||||
|
||||
/**
|
||||
* (For Methods 8 and 9 - Deflating)
|
||||
* Normal (-en) compression option was used.
|
||||
*/
|
||||
public const DEFLATE_COMPRESSION_NORMAL = 0 << 1;
|
||||
|
||||
/**
|
||||
* (For Methods 8 and 9 - Deflating)
|
||||
* Maximum (-exx/-ex) compression option was used.
|
||||
*/
|
||||
public const DEFLATE_COMPRESSION_MAXIMUM = 1 << 1;
|
||||
|
||||
/**
|
||||
* (For Methods 8 and 9 - Deflating)
|
||||
* Fast (-ef) compression option was used.
|
||||
*/
|
||||
public const DEFLATE_COMPRESSION_FAST = 10 << 1;
|
||||
|
||||
/**
|
||||
* (For Methods 8 and 9 - Deflating)
|
||||
* Super Fast (-es) compression option was used.
|
||||
*/
|
||||
public const DEFLATE_COMPRESSION_SUPERFAST = 11 << 1;
|
||||
|
||||
/**
|
||||
* If the compression method used was type 14,
|
||||
* LZMA, then this bit, if set, indicates
|
||||
* an end-of-stream (EOS) marker is used to
|
||||
* mark the end of the compressed data stream.
|
||||
* If clear, then an EOS marker is not present
|
||||
* and the compressed data size must be known
|
||||
* to extract.
|
||||
*/
|
||||
public const LZMA_EOS = 1 << 1;
|
||||
|
||||
/**
|
||||
* If this bit is set, the fields crc-32, compressed
|
||||
* size and uncompressed size are set to zero in the
|
||||
* local header. The correct values are put in the
|
||||
* data descriptor immediately following the compressed
|
||||
* data.
|
||||
*/
|
||||
public const ZERO_HEADER = 1 << 3;
|
||||
|
||||
/**
|
||||
* If this bit is set, this indicates that the file is
|
||||
* compressed patched data.
|
||||
*/
|
||||
public const COMPRESSED_PATCHED_DATA = 1 << 5;
|
||||
|
||||
/**
|
||||
* Strong encryption. If this bit is set, you MUST
|
||||
* set the version needed to extract value to at least
|
||||
* 50 and you MUST also set bit 0. If AES encryption
|
||||
* is used, the version needed to extract value MUST
|
||||
* be at least 51.
|
||||
*/
|
||||
public const STRONG_ENCRYPTION = 1 << 6;
|
||||
|
||||
/**
|
||||
* Language encoding flag (EFS). If this bit is set,
|
||||
* the filename and comment fields for this file
|
||||
* MUST be encoded using UTF-8.
|
||||
*/
|
||||
public const EFS = 1 << 11;
|
||||
|
||||
/**
|
||||
* Set when encrypting the Central Directory to indicate
|
||||
* selected data values in the Local Header are masked to
|
||||
* hide their actual values.
|
||||
*/
|
||||
public const ENCRYPT_CENTRAL_DIRECTORY = 1 << 13;
|
||||
}
|
40
lib/zipstream/src/LocalFileHeader.php
Normal file
40
lib/zipstream/src/LocalFileHeader.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class LocalFileHeader
|
||||
{
|
||||
private const SIGNATURE = 0x04034b50;
|
||||
|
||||
public static function generate(
|
||||
int $versionNeededToExtract,
|
||||
int $generalPurposeBitFlag,
|
||||
CompressionMethod $compressionMethod,
|
||||
DateTimeInterface $lastModificationDateTime,
|
||||
int $crc32UncompressedData,
|
||||
int $compressedSize,
|
||||
int $uncompressedSize,
|
||||
string $fileName,
|
||||
string $extraField,
|
||||
): string {
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: self::SIGNATURE),
|
||||
new PackField(format: 'v', value: $versionNeededToExtract),
|
||||
new PackField(format: 'v', value: $generalPurposeBitFlag),
|
||||
new PackField(format: 'v', value: $compressionMethod->value),
|
||||
new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)),
|
||||
new PackField(format: 'V', value: $crc32UncompressedData),
|
||||
new PackField(format: 'V', value: $compressedSize),
|
||||
new PackField(format: 'V', value: $uncompressedSize),
|
||||
new PackField(format: 'v', value: strlen($fileName)),
|
||||
new PackField(format: 'v', value: strlen($extraField)),
|
||||
) . $fileName . $extraField;
|
||||
}
|
||||
}
|
35
lib/zipstream/src/OperationMode.php
Normal file
35
lib/zipstream/src/OperationMode.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
/**
|
||||
* ZipStream execution operation modes
|
||||
*/
|
||||
enum OperationMode
|
||||
{
|
||||
/**
|
||||
* Stream file into output stream
|
||||
*/
|
||||
case NORMAL;
|
||||
|
||||
/**
|
||||
* Simulate the zip to figure out the resulting file size
|
||||
*
|
||||
* This only supports entries where the file size is known beforehand and
|
||||
* deflation is disabled.
|
||||
*/
|
||||
case SIMULATE_STRICT;
|
||||
|
||||
/**
|
||||
* Simulate the zip to figure out the resulting file size
|
||||
*
|
||||
* If the file size is not known beforehand or deflation is enabled, the
|
||||
* entry streams will be read and rewound.
|
||||
*
|
||||
* If the entry does not support rewinding either, you will not be able to
|
||||
* use the same stream in a later operation mode like `NORMAL`.
|
||||
*/
|
||||
case SIMULATE_LAX;
|
||||
}
|
@ -1,276 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Option;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
final class Archive
|
||||
{
|
||||
public const DEFAULT_DEFLATE_LEVEL = 6;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $comment = '';
|
||||
|
||||
/**
|
||||
* Size, in bytes, of the largest file to try
|
||||
* and load into memory (used by
|
||||
* addFileFromPath()). Large files may also
|
||||
* be compressed differently; see the
|
||||
* 'largeFileMethod' option. Default is ~20 Mb.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $largeFileSize = 20 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* How to handle large files. Legal values are
|
||||
* Method::STORE() (the default), or
|
||||
* Method::DEFLATE(). STORE sends the file
|
||||
* raw and is significantly
|
||||
* faster, while DEFLATE compresses the file
|
||||
* and is much, much slower. Note that DEFLATE
|
||||
* must compress the file twice and is extremely slow.
|
||||
*
|
||||
* @var Method
|
||||
*/
|
||||
private $largeFileMethod;
|
||||
|
||||
/**
|
||||
* Boolean indicating whether or not to send
|
||||
* the HTTP headers for this file.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $sendHttpHeaders = false;
|
||||
|
||||
/**
|
||||
* The method called to send headers
|
||||
*
|
||||
* @var Callable
|
||||
*/
|
||||
private $httpHeaderCallback = 'header';
|
||||
|
||||
/**
|
||||
* Enable Zip64 extension, supporting very large
|
||||
* archives (any size > 4 GB or file count > 64k)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $enableZip64 = true;
|
||||
|
||||
/**
|
||||
* Enable streaming files with single read where
|
||||
* general purpose bit 3 indicates local file header
|
||||
* contain zero values in crc and size fields,
|
||||
* these appear only after file contents
|
||||
* in data descriptor block.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $zeroHeader = false;
|
||||
|
||||
/**
|
||||
* Enable reading file stat for determining file size.
|
||||
* When a 32-bit system reads file size that is
|
||||
* over 2 GB, invalid value appears in file size
|
||||
* due to integer overflow. Should be disabled on
|
||||
* 32-bit systems with method addFileFromPath
|
||||
* if any file may exceed 2 GB. In this case file
|
||||
* will be read in blocks and correct size will be
|
||||
* determined from content.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $statFiles = true;
|
||||
|
||||
/**
|
||||
* Enable flush after every write to output stream.
|
||||
* @var bool
|
||||
*/
|
||||
private $flushOutput = false;
|
||||
|
||||
/**
|
||||
* HTTP Content-Disposition. Defaults to
|
||||
* 'attachment', where
|
||||
* FILENAME is the specified filename.
|
||||
*
|
||||
* Note that this does nothing if you are
|
||||
* not sending HTTP headers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $contentDisposition = 'attachment';
|
||||
|
||||
/**
|
||||
* Note that this does nothing if you are
|
||||
* not sending HTTP headers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $contentType = 'application/x-zip';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $deflateLevel = 6;
|
||||
|
||||
/**
|
||||
* @var StreamInterface|resource
|
||||
*/
|
||||
private $outputStream;
|
||||
|
||||
/**
|
||||
* Options constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->largeFileMethod = Method::STORE();
|
||||
$this->outputStream = fopen('php://output', 'wb');
|
||||
}
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
public function setComment(string $comment): void
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
public function getLargeFileSize(): int
|
||||
{
|
||||
return $this->largeFileSize;
|
||||
}
|
||||
|
||||
public function setLargeFileSize(int $largeFileSize): void
|
||||
{
|
||||
$this->largeFileSize = $largeFileSize;
|
||||
}
|
||||
|
||||
public function getLargeFileMethod(): Method
|
||||
{
|
||||
return $this->largeFileMethod;
|
||||
}
|
||||
|
||||
public function setLargeFileMethod(Method $largeFileMethod): void
|
||||
{
|
||||
$this->largeFileMethod = $largeFileMethod;
|
||||
}
|
||||
|
||||
public function isSendHttpHeaders(): bool
|
||||
{
|
||||
return $this->sendHttpHeaders;
|
||||
}
|
||||
|
||||
public function setSendHttpHeaders(bool $sendHttpHeaders): void
|
||||
{
|
||||
$this->sendHttpHeaders = $sendHttpHeaders;
|
||||
}
|
||||
|
||||
public function getHttpHeaderCallback(): callable
|
||||
{
|
||||
return $this->httpHeaderCallback;
|
||||
}
|
||||
|
||||
public function setHttpHeaderCallback(callable $httpHeaderCallback): void
|
||||
{
|
||||
$this->httpHeaderCallback = $httpHeaderCallback;
|
||||
}
|
||||
|
||||
public function isEnableZip64(): bool
|
||||
{
|
||||
return $this->enableZip64;
|
||||
}
|
||||
|
||||
public function setEnableZip64(bool $enableZip64): void
|
||||
{
|
||||
$this->enableZip64 = $enableZip64;
|
||||
}
|
||||
|
||||
public function isZeroHeader(): bool
|
||||
{
|
||||
return $this->zeroHeader;
|
||||
}
|
||||
|
||||
public function setZeroHeader(bool $zeroHeader): void
|
||||
{
|
||||
$this->zeroHeader = $zeroHeader;
|
||||
}
|
||||
|
||||
public function isFlushOutput(): bool
|
||||
{
|
||||
return $this->flushOutput;
|
||||
}
|
||||
|
||||
public function setFlushOutput(bool $flushOutput): void
|
||||
{
|
||||
$this->flushOutput = $flushOutput;
|
||||
}
|
||||
|
||||
public function isStatFiles(): bool
|
||||
{
|
||||
return $this->statFiles;
|
||||
}
|
||||
|
||||
public function setStatFiles(bool $statFiles): void
|
||||
{
|
||||
$this->statFiles = $statFiles;
|
||||
}
|
||||
|
||||
public function getContentDisposition(): string
|
||||
{
|
||||
return $this->contentDisposition;
|
||||
}
|
||||
|
||||
public function setContentDisposition(string $contentDisposition): void
|
||||
{
|
||||
$this->contentDisposition = $contentDisposition;
|
||||
}
|
||||
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
public function setContentType(string $contentType): void
|
||||
{
|
||||
$this->contentType = $contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StreamInterface|resource
|
||||
*/
|
||||
public function getOutputStream()
|
||||
{
|
||||
return $this->outputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StreamInterface|resource $outputStream
|
||||
*/
|
||||
public function setOutputStream($outputStream): void
|
||||
{
|
||||
$this->outputStream = $outputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDeflateLevel(): int
|
||||
{
|
||||
return $this->deflateLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $deflateLevel
|
||||
*/
|
||||
public function setDeflateLevel(int $deflateLevel): void
|
||||
{
|
||||
$this->deflateLevel = $deflateLevel;
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Option;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
|
||||
final class File
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $comment = '';
|
||||
|
||||
/**
|
||||
* @var Method
|
||||
*/
|
||||
private $method;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $deflateLevel;
|
||||
|
||||
/**
|
||||
* @var DateTimeInterface
|
||||
*/
|
||||
private $time;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $size = 0;
|
||||
|
||||
public function defaultTo(Archive $archiveOptions): void
|
||||
{
|
||||
$this->deflateLevel = $this->deflateLevel ?: $archiveOptions->getDeflateLevel();
|
||||
$this->time = $this->time ?: new DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comment
|
||||
*/
|
||||
public function setComment(string $comment): void
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Method
|
||||
*/
|
||||
public function getMethod(): Method
|
||||
{
|
||||
return $this->method ?: Method::DEFLATE();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Method $method
|
||||
*/
|
||||
public function setMethod(Method $method): void
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDeflateLevel(): int
|
||||
{
|
||||
return $this->deflateLevel ?: Archive::DEFAULT_DEFLATE_LEVEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $deflateLevel
|
||||
*/
|
||||
public function setDeflateLevel(int $deflateLevel): void
|
||||
{
|
||||
$this->deflateLevel = $deflateLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeInterface
|
||||
*/
|
||||
public function getTime(): DateTimeInterface
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTimeInterface $time
|
||||
*/
|
||||
public function setTime(DateTimeInterface $time): void
|
||||
{
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $size
|
||||
*/
|
||||
public function setSize(int $size): void
|
||||
{
|
||||
$this->size = $size;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Option;
|
||||
|
||||
use MyCLabs\Enum\Enum;
|
||||
|
||||
/**
|
||||
* Methods enum
|
||||
*
|
||||
* @method static STORE(): Method
|
||||
* @method static DEFLATE(): Method
|
||||
* @psalm-immutable
|
||||
* @psalm-template int
|
||||
* @extends Enum<int>
|
||||
*/
|
||||
class Method extends Enum
|
||||
{
|
||||
public const STORE = 0x00;
|
||||
|
||||
public const DEFLATE = 0x08;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Option;
|
||||
|
||||
use MyCLabs\Enum\Enum;
|
||||
|
||||
/**
|
||||
* Class Version
|
||||
* @package ZipStream\Option
|
||||
*
|
||||
* @method static STORE(): Version
|
||||
* @method static DEFLATE(): Version
|
||||
* @method static ZIP64(): Version
|
||||
* @psalm-immutable
|
||||
* @psalm-template int
|
||||
* @extends Enum<int>
|
||||
*/
|
||||
class Version extends Enum
|
||||
{
|
||||
public const STORE = 0x000A; // 1.00
|
||||
|
||||
public const DEFLATE = 0x0014; // 2.00
|
||||
|
||||
public const ZIP64 = 0x002D; // 4.50
|
||||
}
|
57
lib/zipstream/src/PackField.php
Normal file
57
lib/zipstream/src/PackField.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* TODO: Make class readonly when requiring PHP 8.2 exclusively
|
||||
*/
|
||||
class PackField
|
||||
{
|
||||
public const MAX_V = 0xFFFFFFFF;
|
||||
|
||||
public const MAX_v = 0xFFFF;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $format,
|
||||
public readonly int|string $value
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a format string and argument list for pack(), then call
|
||||
* pack() and return the result.
|
||||
*/
|
||||
public static function pack(self ...$fields): string
|
||||
{
|
||||
$fmt = array_reduce($fields, function (string $acc, self $field) {
|
||||
return $acc . $field->format;
|
||||
}, '');
|
||||
|
||||
$args = array_map(function (self $field) {
|
||||
switch($field->format) {
|
||||
case 'V':
|
||||
if ($field->value > self::MAX_V) {
|
||||
throw new RuntimeException(print_r($field->value, true) . ' is larger than 32 bits');
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
if ($field->value > self::MAX_v) {
|
||||
throw new RuntimeException(print_r($field->value, true) . ' is larger than 16 bits');
|
||||
}
|
||||
break;
|
||||
case 'P': break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return $field->value;
|
||||
}, $fields);
|
||||
|
||||
return pack($fmt, ...$args);
|
||||
}
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use function mb_strlen;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Describes a data stream.
|
||||
*
|
||||
* Typically, an instance will wrap a PHP stream; this interface provides
|
||||
* a wrapper around the most common operations, including serialization of
|
||||
* the entire stream to a string.
|
||||
*/
|
||||
class Stream implements StreamInterface
|
||||
{
|
||||
protected $stream;
|
||||
|
||||
public function __construct($stream)
|
||||
{
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all data from the stream into a string, from the beginning to end.
|
||||
*
|
||||
* This method MUST attempt to seek to the beginning of the stream before
|
||||
* reading data and read the stream until the end is reached.
|
||||
*
|
||||
* Warning: This could attempt to load a large amount of data into memory.
|
||||
*
|
||||
* This method MUST NOT raise an exception in order to conform with PHP's
|
||||
* string casting operations.
|
||||
*
|
||||
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
try {
|
||||
$this->seek(0);
|
||||
} catch (RuntimeException $e) {
|
||||
}
|
||||
return (string) stream_get_contents($this->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the stream and any underlying resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (is_resource($this->stream)) {
|
||||
fclose($this->stream);
|
||||
}
|
||||
$this->detach();
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates any underlying resources from the stream.
|
||||
*
|
||||
* After the stream has been detached, the stream is in an unusable state.
|
||||
*
|
||||
* @return resource|null Underlying PHP stream, if any
|
||||
*/
|
||||
public function detach()
|
||||
{
|
||||
$result = $this->stream;
|
||||
$this->stream = null;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
*
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @param int $offset Stream offset
|
||||
* @param int $whence Specifies how the cursor position will be calculated
|
||||
* based on the seek offset. Valid values are identical to the built-in
|
||||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
|
||||
* offset bytes SEEK_CUR: Set position to current location plus offset
|
||||
* SEEK_END: Set position to end-of-stream plus offset.
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function seek($offset, $whence = SEEK_SET): void
|
||||
{
|
||||
if (!$this->isSeekable()) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if (fseek($this->stream, $offset, $whence) !== 0) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is seekable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
return (bool)$this->getMetadata('seekable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stream metadata as an associative array or retrieve a specific key.
|
||||
*
|
||||
* The keys returned are identical to the keys returned from PHP's
|
||||
* stream_get_meta_data() function.
|
||||
*
|
||||
* @link http://php.net/manual/en/function.stream-get-meta-data.php
|
||||
* @param string $key Specific metadata to retrieve.
|
||||
* @return array|mixed|null Returns an associative array if no key is
|
||||
* provided. Returns a specific key value if a key is provided and the
|
||||
* value is found, or null if the key is not found.
|
||||
*/
|
||||
public function getMetadata($key = null)
|
||||
{
|
||||
$metadata = stream_get_meta_data($this->stream);
|
||||
return $key !== null ? @$metadata[$key] : $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the stream if known.
|
||||
*
|
||||
* @return int|null Returns the size in bytes if known, or null if unknown.
|
||||
*/
|
||||
public function getSize(): ?int
|
||||
{
|
||||
$stats = fstat($this->stream);
|
||||
return $stats['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the file read/write pointer
|
||||
*
|
||||
* @return int Position of the file pointer
|
||||
* @throws RuntimeException on error.
|
||||
*/
|
||||
public function tell(): int
|
||||
{
|
||||
$position = ftell($this->stream);
|
||||
if ($position === false) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return $position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the stream is at the end of the stream.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function eof(): bool
|
||||
{
|
||||
return feof($this->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the beginning of the stream.
|
||||
*
|
||||
* If the stream is not seekable, this method will raise an exception;
|
||||
* otherwise, it will perform a seek(0).
|
||||
*
|
||||
* @see seek()
|
||||
* @link http://www.php.net/manual/en/function.fseek.php
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the stream.
|
||||
*
|
||||
* @param string $string The string that is to be written.
|
||||
* @return int Returns the number of bytes written to the stream.
|
||||
* @throws RuntimeException on failure.
|
||||
*/
|
||||
public function write($string): int
|
||||
{
|
||||
if (!$this->isWritable()) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if (fwrite($this->stream, $string) === false) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return mb_strlen($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is writable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isWritable(): bool
|
||||
{
|
||||
$mode = $this->getMetadata('mode');
|
||||
if (!is_string($mode)) {
|
||||
throw new RuntimeException('Could not get stream mode from metadata!');
|
||||
}
|
||||
return preg_match('/[waxc+]/', $mode) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data from the stream.
|
||||
*
|
||||
* @param int $length Read up to $length bytes from the object and return
|
||||
* them. Fewer than $length bytes may be returned if underlying stream
|
||||
* call returns fewer bytes.
|
||||
* @return string Returns the data read from the stream, or an empty string
|
||||
* if no bytes are available.
|
||||
* @throws RuntimeException if an error occurs.
|
||||
*/
|
||||
public function read($length): string
|
||||
{
|
||||
if (!$this->isReadable()) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
$result = fread($this->stream, $length);
|
||||
if ($result === false) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the stream is readable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
$mode = $this->getMetadata('mode');
|
||||
if (!is_string($mode)) {
|
||||
throw new RuntimeException('Could not get stream mode from metadata!');
|
||||
}
|
||||
return preg_match('/[r+]/', $mode) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remaining contents in a string
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException if unable to read or an error occurs while
|
||||
* reading.
|
||||
*/
|
||||
public function getContents(): string
|
||||
{
|
||||
if (!$this->isReadable()) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
$result = stream_get_contents($this->stream);
|
||||
if ($result === false) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
45
lib/zipstream/src/Time.php
Normal file
45
lib/zipstream/src/Time.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use ZipStream\Exception\DosTimeOverflowException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class Time
|
||||
{
|
||||
private const DOS_MINIMUM_DATE = '1980-01-01 00:00:00Z';
|
||||
|
||||
public static function dateTimeToDosTime(DateTimeInterface $dateTime): int
|
||||
{
|
||||
$dosMinimumDate = new DateTimeImmutable(self::DOS_MINIMUM_DATE);
|
||||
|
||||
if ($dateTime->getTimestamp() < $dosMinimumDate->getTimestamp()) {
|
||||
throw new DosTimeOverflowException(dateTime: $dateTime);
|
||||
}
|
||||
|
||||
$dateTime = DateTimeImmutable::createFromInterface($dateTime)->sub(new DateInterval('P1980Y'));
|
||||
|
||||
['year' => $year,
|
||||
'mon' => $month,
|
||||
'mday' => $day,
|
||||
'hours' => $hour,
|
||||
'minutes' => $minute,
|
||||
'seconds' => $second
|
||||
] = getdate($dateTime->getTimestamp());
|
||||
|
||||
return
|
||||
($year << 25) |
|
||||
($month << 21) |
|
||||
($day << 16) |
|
||||
($hour << 11) |
|
||||
($minute << 5) |
|
||||
($second >> 1);
|
||||
}
|
||||
}
|
12
lib/zipstream/src/Version.php
Normal file
12
lib/zipstream/src/Version.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream;
|
||||
|
||||
enum Version: int
|
||||
{
|
||||
case STORE = 0x000A; // 1.00
|
||||
case DEFLATE = 0x0014; // 2.00
|
||||
case ZIP64 = 0x002D; // 4.50
|
||||
}
|
28
lib/zipstream/src/Zip64/DataDescriptor.php
Normal file
28
lib/zipstream/src/Zip64/DataDescriptor.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Zip64;
|
||||
|
||||
use ZipStream\PackField;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class DataDescriptor
|
||||
{
|
||||
private const SIGNATURE = 0x08074b50;
|
||||
|
||||
public static function generate(
|
||||
int $crc32UncompressedData,
|
||||
int $compressedSize,
|
||||
int $uncompressedSize,
|
||||
): string {
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: self::SIGNATURE),
|
||||
new PackField(format: 'V', value: $crc32UncompressedData),
|
||||
new PackField(format: 'P', value: $compressedSize),
|
||||
new PackField(format: 'P', value: $uncompressedSize),
|
||||
);
|
||||
}
|
||||
}
|
43
lib/zipstream/src/Zip64/EndOfCentralDirectory.php
Normal file
43
lib/zipstream/src/Zip64/EndOfCentralDirectory.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Zip64;
|
||||
|
||||
use ZipStream\PackField;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class EndOfCentralDirectory
|
||||
{
|
||||
private const SIGNATURE = 0x06064b50;
|
||||
|
||||
public static function generate(
|
||||
int $versionMadeBy,
|
||||
int $versionNeededToExtract,
|
||||
int $numberOfThisDisk,
|
||||
int $numberOfTheDiskWithCentralDirectoryStart,
|
||||
int $numberOfCentralDirectoryEntriesOnThisDisk,
|
||||
int $numberOfCentralDirectoryEntries,
|
||||
int $sizeOfCentralDirectory,
|
||||
int $centralDirectoryStartOffsetOnDisk,
|
||||
string $extensibleDataSector,
|
||||
): string {
|
||||
$recordSize = 44 + strlen($extensibleDataSector); // (length of block - 12) = 44;
|
||||
|
||||
/** @psalm-suppress MixedArgument */
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: static::SIGNATURE),
|
||||
new PackField(format: 'P', value: $recordSize),
|
||||
new PackField(format: 'v', value: $versionMadeBy),
|
||||
new PackField(format: 'v', value: $versionNeededToExtract),
|
||||
new PackField(format: 'V', value: $numberOfThisDisk),
|
||||
new PackField(format: 'V', value: $numberOfTheDiskWithCentralDirectoryStart),
|
||||
new PackField(format: 'P', value: $numberOfCentralDirectoryEntriesOnThisDisk),
|
||||
new PackField(format: 'P', value: $numberOfCentralDirectoryEntries),
|
||||
new PackField(format: 'P', value: $sizeOfCentralDirectory),
|
||||
new PackField(format: 'P', value: $centralDirectoryStartOffsetOnDisk),
|
||||
) . $extensibleDataSector;
|
||||
}
|
||||
}
|
29
lib/zipstream/src/Zip64/EndOfCentralDirectoryLocator.php
Normal file
29
lib/zipstream/src/Zip64/EndOfCentralDirectoryLocator.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Zip64;
|
||||
|
||||
use ZipStream\PackField;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class EndOfCentralDirectoryLocator
|
||||
{
|
||||
private const SIGNATURE = 0x07064b50;
|
||||
|
||||
public static function generate(
|
||||
int $numberOfTheDiskWithZip64CentralDirectoryStart,
|
||||
int $zip64centralDirectoryStartOffsetOnDisk,
|
||||
int $totalNumberOfDisks,
|
||||
): string {
|
||||
/** @psalm-suppress MixedArgument */
|
||||
return PackField::pack(
|
||||
new PackField(format: 'V', value: static::SIGNATURE),
|
||||
new PackField(format: 'V', value: $numberOfTheDiskWithZip64CentralDirectoryStart),
|
||||
new PackField(format: 'P', value: $zip64centralDirectoryStartOffsetOnDisk),
|
||||
new PackField(format: 'V', value: $totalNumberOfDisks),
|
||||
);
|
||||
}
|
||||
}
|
46
lib/zipstream/src/Zip64/ExtendedInformationExtraField.php
Normal file
46
lib/zipstream/src/Zip64/ExtendedInformationExtraField.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Zip64;
|
||||
|
||||
use ZipStream\PackField;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class ExtendedInformationExtraField
|
||||
{
|
||||
private const TAG = 0x0001;
|
||||
|
||||
public static function generate(
|
||||
?int $originalSize = null,
|
||||
?int $compressedSize = null,
|
||||
?int $relativeHeaderOffset = null,
|
||||
?int $diskStartNumber = null,
|
||||
): string {
|
||||
return PackField::pack(
|
||||
new PackField(format: 'v', value: self::TAG),
|
||||
new PackField(
|
||||
format: 'v',
|
||||
value:
|
||||
($originalSize === null ? 0 : 8) +
|
||||
($compressedSize === null ? 0 : 8) +
|
||||
($relativeHeaderOffset === null ? 0 : 8) +
|
||||
($diskStartNumber === null ? 0 : 4)
|
||||
),
|
||||
...($originalSize === null ? [] : [
|
||||
new PackField(format: 'P', value: $originalSize),
|
||||
]),
|
||||
...($compressedSize === null ? [] : [
|
||||
new PackField(format: 'P', value: $compressedSize),
|
||||
]),
|
||||
...($relativeHeaderOffset === null ? [] : [
|
||||
new PackField(format: 'P', value: $relativeHeaderOffset),
|
||||
]),
|
||||
...($diskStartNumber === null ? [] : [
|
||||
new PackField(format: 'V', value: $diskStartNumber),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
23
lib/zipstream/src/Zs/ExtendedInformationExtraField.php
Normal file
23
lib/zipstream/src/Zs/ExtendedInformationExtraField.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZipStream\Zs;
|
||||
|
||||
use ZipStream\PackField;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class ExtendedInformationExtraField
|
||||
{
|
||||
private const TAG = 0x5653;
|
||||
|
||||
public static function generate(): string
|
||||
{
|
||||
return PackField::pack(
|
||||
new PackField(format: 'v', value: self::TAG),
|
||||
new PackField(format: 'v', value: 0x0000),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user