Subiendo proyecto completo sin restricciones de git ignore

This commit is contained in:
Jose Sanchez
2023-08-17 11:44:02 -04:00
parent a0d4f5ba3b
commit 20f1c60600
19921 changed files with 2509159 additions and 45 deletions

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

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

View 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),
);
}
}

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

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace ZipStream;
abstract class Exception extends \Exception
{
}

View 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.");
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a file wasn't found
*/
class FileNotFoundException extends Exception
{
/**
* @internal
*/
public function __construct(
public readonly string $path
) {
parent::__construct("The file with the path $path wasn't found.");
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a file wasn't found
*/
class FileNotReadableException extends Exception
{
/**
* @internal
*/
public function __construct(
public readonly string $path
) {
parent::__construct("The file with the path $path isn't readable.");
}
}

View 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.");
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a counter value exceeds storage size
*/
class OverflowException extends Exception
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.');
}
}

View 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.');
}
}

View File

@@ -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.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace ZipStream\Exception;
use ZipStream\Exception;
/**
* This Exception gets invoked if a stream can't be read.
*/
class StreamNotReadableException extends Exception
{
/**
* @internal
*/
public function __construct()
{
parent::__construct('The stream could not be read.');
}
}

View 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');
}
}

View File

@@ -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) {
$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;
if (
$this->isSimulation() &&
$detectedSize > 0
) {
($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) {
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 && $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;
}
}

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

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

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

View 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);
}
}

View 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);
}
}

View 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
}

View 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),
);
}
}

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

View 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),
);
}
}

View 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),
]),
);
}
}

View File

@@ -0,0 +1,864 @@
<?php
declare(strict_types=1);
namespace ZipStream;
use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use GuzzleHttp\Psr7\StreamWrapper;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use ZipStream\Exception\FileNotFoundException;
use ZipStream\Exception\FileNotReadableException;
use ZipStream\Exception\OverflowException;
use ZipStream\Exception\ResourceActionException;
/**
* Streamed, dynamically generated zip archives.
*
* ## Usage
*
* Streaming zip archives is a simple, three-step process:
*
* 1. Create the zip stream:
*
* ```php
* $zip = new ZipStream(outputName: 'example.zip');
* ```
*
* 2. Add one or more files to the archive:
*
* ```php
* // add first file
* $zip->addFile(fileName: 'world.txt', data: 'Hello World');
*
* // add second file
* $zip->addFile(fileName: 'moon.txt', data: 'Hello Moon');
* ```
*
* 3. Finish the zip stream:
*
* ```php
* $zip->finish();
* ```
*
* You can also add an archive comment, add comments to individual files,
* and adjust the timestamp of files. See the API documentation for each
* method below for additional information.
*
* ## Example
*
* ```php
* // create a new zip stream object
* $zip = new ZipStream(outputName: 'some_files.zip');
*
* // list of local files
* $files = array('foo.txt', 'bar.jpg');
*
* // read and add each file to the archive
* foreach ($files as $path)
* $zip->addFileFormPath(fileName: $path, $path);
*
* // write archive footer to stream
* $zip->finish();
* ```
*/
class ZipStream
{
/**
* This number corresponds to the ZIP version/OS used (2 bytes)
* From: https://www.iana.org/assignments/media-types/application/zip
* The upper byte (leftmost one) indicates the host system (OS) for the
* file. Software can use this information to determine
* the line record format for text files etc. The current
* mappings are:
*
* 0 - MS-DOS and OS/2 (F.A.T. file systems)
* 1 - Amiga 2 - VAX/VMS
* 3 - *nix 4 - VM/CMS
* 5 - Atari ST 6 - OS/2 H.P.F.S.
* 7 - Macintosh 8 - Z-System
* 9 - CP/M 10 thru 255 - unused
*
* The lower byte (rightmost one) indicates the version number of the
* software used to encode the file. The value/10
* indicates the major version number, and the value
* mod 10 is the minor version number.
* Here we are using 6 for the OS, indicating OS/2 H.P.F.S.
* to prevent file permissions issues upon extract (see #84)
* 0x603 is 00000110 00000011 in binary, so 6 and 3
*
* @internal
*/
public const ZIP_VERSION_MADE_BY = 0x603;
private bool $ready = true;
private int $offset = 0;
/**
* @var string[]
*/
private array $centralDirectoryRecords = [];
/**
* @var resource
*/
private $outputStream;
private readonly Closure $httpHeaderCallback;
/**
* @var File[]
*/
private array $recordedSimulation = [];
/**
* Create a new ZipStream object.
*
* ##### Examples
*
* ```php
* // create a new zip file named 'foo.zip'
* $zip = new ZipStream(outputName: 'foo.zip');
*
* // create a new zip file named 'bar.zip' with a comment
* $zip = new ZipStream(
* outputName: 'bar.zip',
* comment: 'this is a comment for the zip file.',
* );
* ```
*
* @param OperationMode $operationMode
* The mode can be used to switch between `NORMAL` and `SIMULATION_*` modes.
* For details see the `OperationMode` documentation.
*
* Default to `NORMAL`.
*
* @param string $comment
* Archive Level Comment
*
* @param StreamInterface|resource|null $outputStream
* Override the output of the archive to a different target.
*
* By default the archive is sent to `STDOUT`.
*
* @param CompressionMethod $defaultCompressionMethod
* How to handle file compression. Legal values are
* `CompressionMethod::DEFLATE` (the default), or
* `CompressionMethod::STORE`. `STORE` sends the file raw and is
* significantly faster, while `DEFLATE` compresses the file and
* is much, much slower.
*
* @param int $defaultDeflateLevel
* Default deflation level. Only relevant if `compressionMethod`
* is `DEFLATE`.
*
* See details of [`deflate_init`](https://www.php.net/manual/en/function.deflate-init.php#refsect1-function.deflate-init-parameters)
*
* @param bool $enableZip64
* Enable Zip64 extension, supporting very large
* archives (any size > 4 GB or file count > 64k)
*
* @param bool $defaultEnableZeroHeader
* Enable streaming files with single read.
*
* When the zero header is set, the file is streamed into the output
* and the size & checksum are added at the end of the file. This is the
* fastest method and uses the least memory. Unfortunately not all
* ZIP clients fully support this and can lead to clients reporting
* the generated ZIP files as corrupted in combination with other
* circumstances. (Zip64 enabled, using UTF8 in comments / names etc.)
*
* When the zero header is not set, the length & checksum need to be
* defined before the file is actually added. To prevent loading all
* the data into memory, the data has to be read twice. If the data
* which is added is not seekable, this call will fail.
*
* @param bool $sendHttpHeaders
* Boolean indicating whether or not to send
* the HTTP headers for this file.
*
* @param ?Closure $httpHeaderCallback
* The method called to send HTTP headers
*
* @param string|null $outputName
* The name of the created archive.
*
* Only relevant if `$sendHttpHeaders = true`.
*
* @param string $contentDisposition
* HTTP Content-Disposition
*
* Only relevant if `sendHttpHeaders = true`.
*
* @param string $contentType
* HTTP Content Type
*
* Only relevant if `sendHttpHeaders = true`.
*
* @param bool $flushOutput
* Enable flush after every write to output stream.
*
* @return self
*/
public function __construct(
private OperationMode $operationMode = OperationMode::NORMAL,
private readonly string $comment = '',
$outputStream = null,
private readonly CompressionMethod $defaultCompressionMethod = CompressionMethod::DEFLATE,
private readonly int $defaultDeflateLevel = 6,
private readonly bool $enableZip64 = true,
private readonly bool $defaultEnableZeroHeader = true,
private bool $sendHttpHeaders = true,
?Closure $httpHeaderCallback = null,
private readonly ?string $outputName = null,
private readonly string $contentDisposition = 'attachment',
private readonly string $contentType = 'application/x-zip',
private bool $flushOutput = false,
) {
$this->outputStream = self::normalizeStream($outputStream);
$this->httpHeaderCallback = $httpHeaderCallback ?? header(...);
}
/**
* Add a file to the archive.
*
* ##### File Options
*
* See {@see addFileFromPsr7Stream()}
*
* ##### Examples
*
* ```php
* // add a file named 'world.txt'
* $zip->addFile(fileName: 'world.txt', data: 'Hello World!');
*
* // add a file named 'bar.jpg' with a comment and a last-modified
* // time of two hours ago
* $zip->addFile(
* fileName: 'bar.jpg',
* data: $data,
* comment: 'this is a comment about bar.jpg',
* lastModificationDateTime: new DateTime('2 hours ago'),
* );
* ```
*
* @param string $data
*
* contents of file
*/
public function addFile(
string $fileName,
string $data,
string $comment = '',
?CompressionMethod $compressionMethod = null,
?int $deflateLevel = null,
?DateTimeInterface $lastModificationDateTime = null,
?int $maxSize = null,
?int $exactSize = null,
?bool $enableZeroHeader = null,
): void {
$this->addFileFromCallback(
fileName: $fileName,
callback: fn () => $data,
comment: $comment,
compressionMethod: $compressionMethod,
deflateLevel: $deflateLevel,
lastModificationDateTime: $lastModificationDateTime,
maxSize: $maxSize,
exactSize: $exactSize,
enableZeroHeader: $enableZeroHeader,
);
}
/**
* Add a file at path to the archive.
*
* ##### File Options
*
* See {@see addFileFromPsr7Stream()}
*
* ###### Examples
*
* ```php
* // add a file named 'foo.txt' from the local file '/tmp/foo.txt'
* $zip->addFileFromPath(
* fileName: 'foo.txt',
* path: '/tmp/foo.txt',
* );
*
* // add a file named 'bigfile.rar' from the local file
* // '/usr/share/bigfile.rar' with a comment and a last-modified
* // time of two hours ago
* $zip->addFile(
* fileName: 'bigfile.rar',
* path: '/usr/share/bigfile.rar',
* comment: 'this is a comment about bigfile.rar',
* lastModificationDateTime: new DateTime('2 hours ago'),
* );
* ```
*
* @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException
*/
public function addFileFromPath(
/**
* name of file in archive (including directory path).
*/
string $fileName,
/**
* path to file on disk (note: paths should be encoded using
* UNIX-style forward slashes -- e.g '/path/to/some/file').
*/
string $path,
string $comment = '',
?CompressionMethod $compressionMethod = null,
?int $deflateLevel = null,
?DateTimeInterface $lastModificationDateTime = null,
?int $maxSize = null,
?int $exactSize = null,
?bool $enableZeroHeader = null,
): void {
if (!is_readable($path)) {
if (!file_exists($path)) {
throw new FileNotFoundException($path);
}
throw new FileNotReadableException($path);
}
if ($fileTime = filemtime($path)) {
$lastModificationDateTime ??= (new DateTimeImmutable())->setTimestamp($fileTime);
}
$this->addFileFromCallback(
fileName: $fileName,
callback: function () use ($path) {
$stream = fopen($path, 'rb');
if (!$stream) {
// @codeCoverageIgnoreStart
throw new ResourceActionException('fopen');
// @codeCoverageIgnoreEnd
}
return $stream;
},
comment: $comment,
compressionMethod: $compressionMethod,
deflateLevel: $deflateLevel,
lastModificationDateTime: $lastModificationDateTime,
maxSize: $maxSize,
exactSize: $exactSize,
enableZeroHeader: $enableZeroHeader,
);
}
/**
* Add an open stream (resource) to the archive.
*
* ##### File Options
*
* See {@see addFileFromPsr7Stream()}
*
* ##### Examples
*
* ```php
* // create a temporary file stream and write text to it
* $filePointer = tmpfile();
* fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.');
*
* // add a file named 'streamfile.txt' from the content of the stream
* $archive->addFileFromStream(
* fileName: 'streamfile.txt',
* stream: $filePointer,
* );
* ```
*
* @param resource $stream contents of file as a stream resource
*/
public function addFileFromStream(
string $fileName,
$stream,
string $comment = '',
?CompressionMethod $compressionMethod = null,
?int $deflateLevel = null,
?DateTimeInterface $lastModificationDateTime = null,
?int $maxSize = null,
?int $exactSize = null,
?bool $enableZeroHeader = null,
): void {
$this->addFileFromCallback(
fileName: $fileName,
callback: fn () => $stream,
comment: $comment,
compressionMethod: $compressionMethod,
deflateLevel: $deflateLevel,
lastModificationDateTime: $lastModificationDateTime,
maxSize: $maxSize,
exactSize: $exactSize,
enableZeroHeader: $enableZeroHeader,
);
}
/**
* Add an open stream to the archive.
*
* ##### Examples
*
* ```php
* $stream = $response->getBody();
* // add a file named 'streamfile.txt' from the content of the stream
* $archive->addFileFromPsr7Stream(
* fileName: 'streamfile.txt',
* stream: $stream,
* );
* ```
*
* @param string $fileName
* path of file in archive (including directory)
*
* @param StreamInterface $stream
* contents of file as a stream resource
*
* @param string $comment
* ZIP comment for this file
*
* @param ?CompressionMethod $compressionMethod
* Override `defaultCompressionMethod`
*
* See {@see __construct()}
*
* @param ?int $deflateLevel
* Override `defaultDeflateLevel`
*
* See {@see __construct()}
*
* @param ?DateTimeInterface $lastModificationDateTime
* Set last modification time of file.
*
* Default: `now`
*
* @param ?int $maxSize
* Only read `maxSize` bytes from file.
*
* The file is considered done when either reaching `EOF`
* or the `maxSize`.
*
* @param ?int $exactSize
* Read exactly `exactSize` bytes from file.
* If `EOF` is reached before reading `exactSize` bytes, an error will be
* thrown. The parameter allows for faster size calculations if the `stream`
* does not support `fstat` size or is slow and otherwise known beforehand.
*
* @param ?bool $enableZeroHeader
* Override `defaultEnableZeroHeader`
*
* See {@see __construct()}
*/
public function addFileFromPsr7Stream(
string $fileName,
StreamInterface $stream,
string $comment = '',
?CompressionMethod $compressionMethod = null,
?int $deflateLevel = null,
?DateTimeInterface $lastModificationDateTime = null,
?int $maxSize = null,
?int $exactSize = null,
?bool $enableZeroHeader = null,
): void {
$this->addFileFromCallback(
fileName: $fileName,
callback: fn () => $stream,
comment: $comment,
compressionMethod: $compressionMethod,
deflateLevel: $deflateLevel,
lastModificationDateTime: $lastModificationDateTime,
maxSize: $maxSize,
exactSize: $exactSize,
enableZeroHeader: $enableZeroHeader,
);
}
/**
* Add a file based on a callback.
*
* This is useful when you want to simulate a lot of files without keeping
* all of the file handles open at the same time.
*
* ##### Examples
*
* ```php
* foreach($files as $name => $size) {
* $archive->addFileFromPsr7Stream(
* fileName: 'streamfile.txt',
* exactSize: $size,
* callback: function() use($name): Psr\Http\Message\StreamInterface {
* $response = download($name);
* return $response->getBody();
* }
* );
* }
* ```
*
* @param string $fileName
* path of file in archive (including directory)
*
* @param Closure $callback
* @psalm-param Closure(): (resource|StreamInterface|string) $callback
* A callback to get the file contents in the shape of a PHP stream,
* a Psr StreamInterface implementation, or a string.
*
* @param string $comment
* ZIP comment for this file
*
* @param ?CompressionMethod $compressionMethod
* Override `defaultCompressionMethod`
*
* See {@see __construct()}
*
* @param ?int $deflateLevel
* Override `defaultDeflateLevel`
*
* See {@see __construct()}
*
* @param ?DateTimeInterface $lastModificationDateTime
* Set last modification time of file.
*
* Default: `now`
*
* @param ?int $maxSize
* Only read `maxSize` bytes from file.
*
* The file is considered done when either reaching `EOF`
* or the `maxSize`.
*
* @param ?int $exactSize
* Read exactly `exactSize` bytes from file.
* If `EOF` is reached before reading `exactSize` bytes, an error will be
* thrown. The parameter allows for faster size calculations if the `stream`
* does not support `fstat` size or is slow and otherwise known beforehand.
*
* @param ?bool $enableZeroHeader
* Override `defaultEnableZeroHeader`
*
* See {@see __construct()}
*/
public function addFileFromCallback(
string $fileName,
Closure $callback,
string $comment = '',
?CompressionMethod $compressionMethod = null,
?int $deflateLevel = null,
?DateTimeInterface $lastModificationDateTime = null,
?int $maxSize = null,
?int $exactSize = null,
?bool $enableZeroHeader = null,
): void {
$file = new File(
dataCallback: function () use ($callback, $maxSize) {
$data = $callback();
if(is_resource($data)) {
return $data;
}
if($data instanceof StreamInterface) {
return StreamWrapper::getResource($data);
}
$stream = fopen('php://memory', 'rw+');
if ($stream === false) {
// @codeCoverageIgnoreStart
throw new ResourceActionException('fopen');
// @codeCoverageIgnoreEnd
}
if ($maxSize !== null && fwrite($stream, $data, $maxSize) === false) {
// @codeCoverageIgnoreStart
throw new ResourceActionException('fwrite', $stream);
// @codeCoverageIgnoreEnd
} elseif (fwrite($stream, $data) === false) {
// @codeCoverageIgnoreStart
throw new ResourceActionException('fwrite', $stream);
// @codeCoverageIgnoreEnd
}
if (rewind($stream) === false) {
// @codeCoverageIgnoreStart
throw new ResourceActionException('rewind', $stream);
// @codeCoverageIgnoreEnd
}
return $stream;
},
send: $this->send(...),
recordSentBytes: $this->recordSentBytes(...),
operationMode: $this->operationMode,
fileName: $fileName,
startOffset: $this->offset,
compressionMethod: $compressionMethod ?? $this->defaultCompressionMethod,
comment: $comment,
deflateLevel: $deflateLevel ?? $this->defaultDeflateLevel,
lastModificationDateTime: $lastModificationDateTime ?? new DateTimeImmutable(),
maxSize: $maxSize,
exactSize: $exactSize,
enableZip64: $this->enableZip64,
enableZeroHeader: $enableZeroHeader ?? $this->defaultEnableZeroHeader,
);
if($this->operationMode !== OperationMode::NORMAL) {
$this->recordedSimulation[] = $file;
}
$this->centralDirectoryRecords[] = $file->process();
}
/**
* Add a directory to the archive.
*
* ##### File Options
*
* See {@see addFileFromPsr7Stream()}
*
* ##### Examples
*
* ```php
* // add a directory named 'world/'
* $zip->addFile(fileName: 'world/');
* ```
*/
public function addDirectory(
string $fileName,
string $comment = '',
?DateTimeInterface $lastModificationDateTime = null,
): void {
if (!str_ends_with($fileName, '/')) {
$fileName .= '/';
}
$this->addFile(
fileName: $fileName,
data: '',
comment: $comment,
compressionMethod: CompressionMethod::STORE,
deflateLevel: null,
lastModificationDateTime: $lastModificationDateTime,
maxSize: 0,
exactSize: 0,
enableZeroHeader: false,
);
}
/**
* Executes a previously calculated simulation.
*
* ##### Example
*
* ```php
* $zip = new ZipStream(
* outputName: 'foo.zip',
* operationMode: OperationMode::SIMULATE_STRICT,
* );
*
* $zip->addFile('test.txt', 'Hello World');
*
* $size = $zip->finish();
*
* header('Content-Length: '. $size);
*
* $zip->executeSimulation();
* ```
*/
public function executeSimulation(): void
{
if($this->operationMode !== OperationMode::NORMAL) {
throw new RuntimeException('Zip simulation is not finished.');
}
foreach($this->recordedSimulation as $file) {
$this->centralDirectoryRecords[] = $file->cloneSimulationExecution()->process();
}
$this->finish();
}
/**
* Write zip footer to stream.
*
* The clase is left in an unusable state after `finish`.
*
* ##### Example
*
* ```php
* // write footer to stream
* $zip->finish();
* ```
*/
public function finish(): int
{
$centralDirectoryStartOffsetOnDisk = $this->offset;
$sizeOfCentralDirectory = 0;
// add trailing cdr file records
foreach ($this->centralDirectoryRecords as $centralDirectoryRecord) {
$this->send($centralDirectoryRecord);
$sizeOfCentralDirectory += strlen($centralDirectoryRecord);
}
// Add 64bit headers (if applicable)
if (count($this->centralDirectoryRecords) >= 0xFFFF ||
$centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF ||
$sizeOfCentralDirectory > 0xFFFFFFFF) {
if (!$this->enableZip64) {
throw new OverflowException();
}
$this->send(Zip64\EndOfCentralDirectory::generate(
versionMadeBy: self::ZIP_VERSION_MADE_BY,
versionNeededToExtract: Version::ZIP64->value,
numberOfThisDisk: 0,
numberOfTheDiskWithCentralDirectoryStart: 0,
numberOfCentralDirectoryEntriesOnThisDisk: count($this->centralDirectoryRecords),
numberOfCentralDirectoryEntries: count($this->centralDirectoryRecords),
sizeOfCentralDirectory: $sizeOfCentralDirectory,
centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk,
extensibleDataSector: '',
));
$this->send(Zip64\EndOfCentralDirectoryLocator::generate(
numberOfTheDiskWithZip64CentralDirectoryStart: 0x00,
zip64centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk + $sizeOfCentralDirectory,
totalNumberOfDisks: 1,
));
}
// add trailing cdr eof record
$numberOfCentralDirectoryEntries = min(count($this->centralDirectoryRecords), 0xFFFF);
$this->send(EndOfCentralDirectory::generate(
numberOfThisDisk: 0x00,
numberOfTheDiskWithCentralDirectoryStart: 0x00,
numberOfCentralDirectoryEntriesOnThisDisk: $numberOfCentralDirectoryEntries,
numberOfCentralDirectoryEntries: $numberOfCentralDirectoryEntries,
sizeOfCentralDirectory: min($sizeOfCentralDirectory, 0xFFFFFFFF),
centralDirectoryStartOffsetOnDisk: min($centralDirectoryStartOffsetOnDisk, 0xFFFFFFFF),
zipFileComment: $this->comment,
));
$size = $this->offset;
// The End
$this->clear();
return $size;
}
/**
* @param StreamInterface|resource|null $outputStream
* @return resource
*/
private static function normalizeStream($outputStream)
{
if ($outputStream instanceof StreamInterface) {
return StreamWrapper::getResource($outputStream);
}
if (is_resource($outputStream)) {
return $outputStream;
}
return fopen('php://output', 'wb');
}
/**
* Record sent bytes
*/
private function recordSentBytes(int $sentBytes): void
{
$this->offset += $sentBytes;
}
/**
* Send string, sending HTTP headers if necessary.
* Flush output after write if configure option is set.
*/
private function send(string $data): void
{
if (!$this->ready) {
throw new RuntimeException('Archive is already finished');
}
if ($this->operationMode === OperationMode::NORMAL && $this->sendHttpHeaders) {
$this->sendHttpHeaders();
$this->sendHttpHeaders = false;
}
$this->recordSentBytes(strlen($data));
if ($this->operationMode === OperationMode::NORMAL) {
if (fwrite($this->outputStream, $data) === false) {
throw new ResourceActionException('fwrite', $this->outputStream);
}
if ($this->flushOutput) {
// flush output buffer if it is on and flushable
$status = ob_get_status();
if (isset($status['flags']) && is_int($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) {
ob_flush();
}
// Flush system buffers after flushing userspace output buffer
flush();
}
}
}
/**
* Send HTTP headers for this stream.
*/
private function sendHttpHeaders(): void
{
// grab content disposition
$disposition = $this->contentDisposition;
if ($this->outputName) {
// Various different browsers dislike various characters here. Strip them all for safety.
$safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName));
// Check if we need to UTF-8 encode the filename
$urlencoded = rawurlencode($safeOutput);
$disposition .= "; filename*=UTF-8''{$urlencoded}";
}
$headers = [
'Content-Type' => $this->contentType,
'Content-Disposition' => $disposition,
'Pragma' => 'public',
'Cache-Control' => 'public, must-revalidate',
'Content-Transfer-Encoding' => 'binary',
];
foreach ($headers as $key => $val) {
($this->httpHeaderCallback)("$key: $val");
}
}
/**
* Clear all internal variables. Note that the stream object is not
* usable after this.
*/
private function clear(): void
{
$this->centralDirectoryRecords = [];
$this->offset = 0;
if($this->operationMode === OperationMode::NORMAL) {
$this->ready = false;
$this->recordedSimulation = [];
} else {
$this->operationMode = OperationMode::NORMAL;
}
}
}

View 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),
);
}
}