aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/maennchen/zipstream-php/src/File.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maennchen/zipstream-php/src/File.php')
-rw-r--r--vendor/maennchen/zipstream-php/src/File.php420
1 files changed, 420 insertions, 0 deletions
diff --git a/vendor/maennchen/zipstream-php/src/File.php b/vendor/maennchen/zipstream-php/src/File.php
new file mode 100644
index 000000000..0462196e2
--- /dev/null
+++ b/vendor/maennchen/zipstream-php/src/File.php
@@ -0,0 +1,420 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ZipStream;
+
+use Closure;
+use DateTimeInterface;
+use DeflateContext;
+use RuntimeException;
+use ZipStream\Exception\FileSizeIncorrectException;
+use ZipStream\Exception\OverflowException;
+use ZipStream\Exception\ResourceActionException;
+use ZipStream\Exception\SimulationFileUnknownException;
+use ZipStream\Exception\StreamNotReadableException;
+use ZipStream\Exception\StreamNotSeekableException;
+
+/**
+ * @internal
+ */
+class File
+{
+ private const CHUNKED_READ_BLOCK_SIZE = 0x1000000;
+
+ private Version $version;
+
+ private int $compressedSize = 0;
+
+ private int $uncompressedSize = 0;
+
+ private int $crc = 0;
+
+ private int $generalPurposeBitFlag = 0;
+
+ private readonly string $fileName;
+
+ /**
+ * @var resource|null
+ */
+ private $stream;
+
+ /**
+ * @param Closure $dataCallback
+ * @psalm-param Closure(): resource $dataCallback
+ */
+ 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();
+
+ if ($this->enableZeroHeader) {
+ $this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER;
+ }
+
+ $this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE;
+ }
+
+ public function cloneSimulationExecution(): self
+ {
+ 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 !== null) {
+ $this->uncompressedSize = $forecastSize;
+ $this->compressedSize = $forecastSize;
+ } else {
+ $this->readStream(send: false);
+ if (rewind($this->unpackStream()) === false) {
+ throw new ResourceActionException('rewind', $this->unpackStream());
+ }
+ }
+
+ $this->addFileHeader();
+
+ $detectedSize = $forecastSize ?? ($this->compressedSize > 0 ? $this->compressedSize : null);
+
+ if (
+ $this->isSimulation() &&
+ $detectedSize !== null
+ ) {
+ ($this->recordSentBytes)($detectedSize);
+ } else {
+ $this->readStream(send: true);
+ }
+
+ $this->addFileFooter();
+ return $this->getCdrFile();
+ }
+
+ /**
+ * @return resource
+ */
+ private function unpackStream()
+ {
+ if ($this->stream) {
+ return $this->stream;
+ }
+
+ 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 !== null) {
+ 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.
+ */
+ private function addFileHeader(): void
+ {
+ $forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64;
+
+ $footer = $this->buildZip64ExtraBlock($forceEnableZip64);
+
+ $zip64Enabled = $footer !== '';
+
+ if ($zip64Enabled) {
+ $this->version = Version::ZIP64;
+ }
+
+ if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) {
+ // Put the tricky entry to
+ // force Linux unzip to lookup EFS flag.
+ $footer .= Zs\ExtendedInformationExtraField::generate();
+ }
+
+ $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,
+ );
+
+
+ ($this->send)($data);
+ }
+
+ /**
+ * Strip characters that are not legal in Windows filenames
+ * to prevent compatibility issues
+ */
+ private static function filterFilename(
+ /**
+ * Unprocessed filename
+ */
+ string $fileName
+ ): string {
+ // strip leading slashes from file name
+ // (fixes bug in windows archive viewer)
+ $fileName = ltrim($fileName, '/');
+
+ return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName);
+ }
+
+ private function checkEncoding(): void
+ {
+ // 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
+ );
+ }
+
+ $this->compressedSize += strlen($data);
+
+ if ($send) {
+ ($this->send)($data);
+ }
+ }
+
+ if ($this->exactSize !== null && $this->uncompressedSize !== $this->exactSize) {
+ throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize);
+ }
+
+ $this->crc = hexdec(hash_final($hash));
+ }
+
+ private function compressionInit(): ?DeflateContext
+ {
+ switch ($this->compressionMethod) {
+ case CompressionMethod::STORE:
+ // Noting to do
+ return null;
+ case CompressionMethod::DEFLATE:
+ $deflateContext = deflate_init(
+ ZLIB_ENCODING_RAW,
+ ['level' => $this->deflateLevel]
+ );
+
+ 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
+ }
+ }
+
+ private function getCdrFile(): string
+ {
+ $footer = $this->buildZip64ExtraBlock();
+
+ 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,
+ );
+ }
+
+ private function isSimulation(): bool
+ {
+ return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT;
+ }
+}