File: /home/icsla/public_html/wp-content/plugins/link-whisper/vendor/maennchen/zipstream-php/src/File.php
<?php
declare (strict_types=1);
namespace LWVendor\ZipStream;
use Closure;
use DateTimeInterface;
use DeflateContext;
use RuntimeException;
use LWVendor\ZipStream\Exception\FileSizeIncorrectException;
use LWVendor\ZipStream\Exception\OverflowException;
use LWVendor\ZipStream\Exception\ResourceActionException;
use LWVendor\ZipStream\Exception\SimulationFileUnknownException;
use LWVendor\ZipStream\Exception\StreamNotReadableException;
use LWVendor\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->uncompressedSize = $detectedSize;
$this->compressedSize = $detectedSize;
($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);
if ($data === \false) {
throw new ResourceActionException('fread', $this->unpackStream());
}
\hash_update($hash, $data);
$this->uncompressedSize += \strlen($data);
if ($deflate) {
$data = \deflate_add($deflate, $data, \feof($this->unpackStream()) ? \ZLIB_FINISH : \ZLIB_NO_FLUSH);
if ($data === \false) {
throw new RuntimeException('deflate_add failed');
}
}
$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));
}
}
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;
}
}