Файловый менеджер - Редактировать - /home/easybachat/hisabat365.com/4a7891/maennchen.tar
Ðазад
zipstream-php/.tool-versions 0000644 00000000012 15060132257 0012165 0 ustar 00 php 8.2.5 zipstream-php/.phpdoc/template/base.html.twig 0000644 00000001262 15060132257 0015270 0 ustar 00 {% extends 'layout.html.twig' %} {% set topMenu = { "menu": [ { "name": "Guides", "url": "https://maennchen.dev/ZipStream-PHP/guide/index.html"}, { "name": "API", "url": "https://maennchen.dev/ZipStream-PHP/classes/ZipStream-ZipStream.html"}, { "name": "Issues", "url": "https://github.com/maennchen/ZipStream-PHP/issues"}, ], "social": [ { "iconClass": "fab fa-github", "url": "https://github.com/maennchen/ZipStream-PHP"}, { "iconClass": "fas fa-envelope-open-text", "url": "https://github.com/maennchen/ZipStream-PHP/discussions"}, { "iconClass": "fas fa-money-bill", "url": "https://opencollective.com/zipstream"}, ] } %} zipstream-php/.editorconfig 0000644 00000000445 15060132257 0012030 0 ustar 00 root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 [*.{yml,md,xml}] indent_style = space indent_size = 2 [*.{rst,php}] indent_style = space indent_size = 4 [composer.json] indent_style = space indent_size = 2 [composer.lock] indent_style = space indent_size = 4 zipstream-php/src/OperationMode.php 0000644 00000001443 15060132257 0013417 0 ustar 00 <?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; } zipstream-php/src/CentralDirectoryFileHeader.php 0000644 00000003646 15060132257 0016047 0 ustar 00 <?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; } } zipstream-php/src/Zs/ExtendedInformationExtraField.php 0000644 00000000621 15060132257 0017161 0 ustar 00 <?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), ); } } zipstream-php/src/Exception/FileNotFoundException.php 0000644 00000000613 15060132257 0017021 0 ustar 00 <?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."); } } zipstream-php/src/Exception/DosTimeOverflowException.php 0000644 00000000756 15060132257 0017565 0 ustar 00 <?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."); } } zipstream-php/src/Exception/OverflowException.php 0000644 00000000620 15060132257 0016266 0 ustar 00 <?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.'); } } zipstream-php/src/Exception/ResourceActionException.php 0000644 00000001063 15060132257 0017412 0 ustar 00 <?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.'); } } zipstream-php/src/Exception/FileNotReadableException.php 0000644 00000000620 15060132257 0017443 0 ustar 00 <?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."); } } zipstream-php/src/Exception/StreamNotSeekableException.php 0000644 00000000653 15060132257 0020041 0 ustar 00 <?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'); } } zipstream-php/src/Exception/FileSizeIncorrectException.php 0000644 00000001007 15060132257 0020046 0 ustar 00 <?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."); } } zipstream-php/src/Exception/SimulationFileUnknownException.php 0000644 00000000742 15060132257 0020774 0 ustar 00 <?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.'); } } zipstream-php/src/Exception/StreamNotReadableException.php 0000644 00000000541 15060132257 0020021 0 ustar 00 <?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.'); } } zipstream-php/src/ZipStream.php 0000644 00000066126 15060132257 0012601 0 ustar 00 <?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; } } } zipstream-php/src/DataDescriptor.php 0000644 00000001150 15060132257 0013555 0 ustar 00 <?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), ); } } zipstream-php/src/Version.php 0000644 00000000262 15060132257 0012275 0 ustar 00 <?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 } zipstream-php/src/PackField.php 0000644 00000002700 15060132257 0012471 0 ustar 00 <?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); } } zipstream-php/src/EndOfCentralDirectory.php 0000644 00000002343 15060132257 0015043 0 ustar 00 <?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; } } zipstream-php/src/Zip64/DataDescriptor.php 0000644 00000001210 15060132257 0014466 0 ustar 00 <?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), ); } } zipstream-php/src/Zip64/ExtendedInformationExtraField.php 0000644 00000002536 15060132257 0017510 0 ustar 00 <?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), ]), ); } } zipstream-php/src/Zip64/EndOfCentralDirectory.php 0000644 00000003046 15060132257 0015760 0 ustar 00 <?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; } } zipstream-php/src/Zip64/EndOfCentralDirectoryLocator.php 0000644 00000001451 15060132257 0017302 0 ustar 00 <?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), ); } } zipstream-php/src/Exception.php 0000644 00000000147 15060132257 0012610 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream; abstract class Exception extends \Exception { } zipstream-php/src/GeneralPurposeBitFlag.php 0000644 00000004613 15060132257 0015040 0 ustar 00 <?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; } zipstream-php/src/Time.php 0000644 00000002133 15060132257 0011545 0 ustar 00 <?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); } } zipstream-php/src/File.php 0000644 00000032073 15060132257 0011534 0 ustar 00 <?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; } } zipstream-php/src/LocalFileHeader.php 0000644 00000002477 15060132257 0013625 0 ustar 00 <?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; } } zipstream-php/src/CompressionMethod.php 0000644 00000004132 15060132257 0014312 0 ustar 00 <?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; } zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php 0000644 00000001017 15060132257 0020211 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test\Zs; use PHPUnit\Framework\TestCase; use ZipStream\Zs\ExtendedInformationExtraField; class ExtendedInformationExtraFieldTest extends TestCase { public function testSerializesCorrectly(): void { $extraField = ExtendedInformationExtraField::generate(); $this->assertSame( bin2hex((string) $extraField), '5356' . // 2 bytes; Tag for this "extra" block type '0000' // 2 bytes; TODO: Document ); } } zipstream-php/test/ResourceStream.php 0000644 00000007565 15060132257 0014020 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use Psr\Http\Message\StreamInterface; use RuntimeException; /** * @internal */ class ResourceStream implements StreamInterface { public function __construct( /** * @var resource */ private $stream ) { } public function __toString(): string { if ($this->isSeekable()) { $this->seek(0); } return (string) stream_get_contents($this->stream); } public function close(): void { $stream = $this->detach(); if ($stream) { fclose($stream); } } public function detach() { $result = $this->stream; // According to the interface, the stream is left in an unusable state; /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ $this->stream = null; return $result; } public function seek(int $offset, int $whence = SEEK_SET): void { if (!$this->isSeekable()) { throw new RuntimeException(); } if (fseek($this->stream, $offset, $whence) !== 0) { // @codeCoverageIgnoreStart throw new RuntimeException(); // @codeCoverageIgnoreEnd } } public function isSeekable(): bool { return (bool)$this->getMetadata('seekable'); } public function getMetadata(?string $key = null) { $metadata = stream_get_meta_data($this->stream); return $key !== null ? @$metadata[$key] : $metadata; } public function getSize(): ?int { $stats = fstat($this->stream); return $stats['size']; } public function tell(): int { $position = ftell($this->stream); if ($position === false) { // @codeCoverageIgnoreStart throw new RuntimeException(); // @codeCoverageIgnoreEnd } return $position; } public function eof(): bool { return feof($this->stream); } public function rewind(): void { $this->seek(0); } public function write(string $string): int { if (!$this->isWritable()) { throw new RuntimeException(); } if (fwrite($this->stream, $string) === false) { // @codeCoverageIgnoreStart throw new RuntimeException(); // @codeCoverageIgnoreEnd } return strlen($string); } public function isWritable(): bool { $mode = $this->getMetadata('mode'); if (!is_string($mode)) { // @codeCoverageIgnoreStart throw new RuntimeException('Could not get stream mode from metadata!'); // @codeCoverageIgnoreEnd } return preg_match('/[waxc+]/', $mode) === 1; } public function read(int $length): string { if (!$this->isReadable()) { throw new RuntimeException(); } $result = fread($this->stream, $length); if ($result === false) { // @codeCoverageIgnoreStart throw new RuntimeException(); // @codeCoverageIgnoreEnd } return $result; } public function isReadable(): bool { $mode = $this->getMetadata('mode'); if (!is_string($mode)) { // @codeCoverageIgnoreStart throw new RuntimeException('Could not get stream mode from metadata!'); // @codeCoverageIgnoreEnd } return preg_match('/[r+]/', $mode) === 1; } public function getContents(): string { if (!$this->isReadable()) { throw new RuntimeException(); } $result = stream_get_contents($this->stream); if ($result === false) { // @codeCoverageIgnoreStart throw new RuntimeException(); // @codeCoverageIgnoreEnd } return $result; } } zipstream-php/test/FaultInjectionResource.php 0000644 00000006163 15060132257 0015474 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; class FaultInjectionResource { public const NAME = 'zipstream-php-test-broken-resource'; /** @var resource */ public $context; private array $injectFaults; private string $mode; /** * @return resource */ public static function getResource(array $injectFaults) { self::register(); return fopen(self::NAME . '://foobar', 'rw+', false, self::createStreamContext($injectFaults)); } public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool { $options = stream_context_get_options($this->context); if (!isset($options[self::NAME]['injectFaults'])) { return false; } $this->mode = $mode; $this->injectFaults = $options[self::NAME]['injectFaults']; if ($this->shouldFail(__FUNCTION__)) { return false; } return true; } public function stream_write(string $data) { if ($this->shouldFail(__FUNCTION__)) { return false; } return true; } public function stream_eof() { return true; } public function stream_seek(int $offset, int $whence): bool { if ($this->shouldFail(__FUNCTION__)) { return false; } return true; } public function stream_tell(): int { if ($this->shouldFail(__FUNCTION__)) { return false; } return 0; } public static function register(): void { if (!in_array(self::NAME, stream_get_wrappers(), true)) { stream_wrapper_register(self::NAME, __CLASS__); } } public function stream_stat(): array { static $modeMap = [ 'r' => 33060, 'rb' => 33060, 'r+' => 33206, 'w' => 33188, 'wb' => 33188, ]; return [ 'dev' => 0, 'ino' => 0, 'mode' => $modeMap[$this->mode], 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, ]; } public function url_stat(string $path, int $flags): array { return [ 'dev' => 0, 'ino' => 0, 'mode' => 0, 'nlink' => 0, 'uid' => 0, 'gid' => 0, 'rdev' => 0, 'size' => 0, 'atime' => 0, 'mtime' => 0, 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, ]; } private static function createStreamContext(array $injectFaults) { return stream_context_create([ self::NAME => ['injectFaults' => $injectFaults], ]); } private function shouldFail(string $function): bool { return in_array($function, $this->injectFaults, true); } } zipstream-php/test/PackFieldTest.php 0000644 00000001656 15060132257 0013532 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use PHPUnit\Framework\TestCase; use RuntimeException; use ZipStream\PackField; class PackFieldTest extends TestCase { public function testPacksFields(): void { $this->assertSame( bin2hex(PackField::pack(new PackField(format: 'v', value: 0x1122))), '2211', ); } public function testOverflow2(): void { $this->expectException(RuntimeException::class); PackField::pack(new PackField(format: 'v', value: 0xFFFFF)); } public function testOverflow4(): void { $this->expectException(RuntimeException::class); PackField::pack(new PackField(format: 'V', value: 0xFFFFFFFFF)); } public function testUnknownOperator(): void { $this->assertSame( bin2hex(PackField::pack(new PackField(format: 'a', value: 0x1122))), '34', ); } } zipstream-php/test/EndlessCycleStream.php 0000644 00000003762 15060132257 0014601 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use Psr\Http\Message\StreamInterface; use RuntimeException; class EndlessCycleStream implements StreamInterface { private int $offset = 0; public function __construct(private readonly string $toRepeat = '0') { } public function __toString(): string { throw new RuntimeException('Infinite Stream!'); } public function close(): void { $this->detach(); } /** * @return null */ public function detach() { return; } public function getSize(): ?int { return null; } public function tell(): int { return $this->offset; } public function eof(): bool { return false; } public function isSeekable(): bool { return true; } public function seek(int $offset, int $whence = SEEK_SET): void { switch($whence) { case SEEK_SET: $this->offset = $offset; break; case SEEK_CUR: $this->offset += $offset; break; case SEEK_END: throw new RuntimeException('Infinite Stream!'); break; } } public function rewind(): void { $this->seek(0); } public function isWritable(): bool { return false; } public function write(string $string): int { throw new RuntimeException('Not writeable'); } public function isReadable(): bool { return true; } public function read(int $length): string { $this->offset += $length; return substr(str_repeat($this->toRepeat, (int) ceil($length / strlen($this->toRepeat))), 0, $length); } public function getContents(): string { throw new RuntimeException('Infinite Stream!'); } public function getMetadata(?string $key = null): array|null { return $key !== null ? null : []; } } zipstream-php/test/Util.php 0000644 00000006765 15060132257 0011773 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use function fgets; use function pclose; use function popen; use function preg_match; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use function strtolower; use ZipArchive; trait Util { protected function getTmpFileStream(): array { $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); $stream = fopen($tmp, 'wb+'); return [$tmp, $stream]; } protected function cmdExists(string $command): bool { if (strtolower(\substr(PHP_OS, 0, 3)) === 'win') { $fp = popen("where $command", 'r'); $result = fgets($fp, 255); $exists = !preg_match('#Could not find files#', $result); pclose($fp); } else { // non-Windows $fp = popen("which $command", 'r'); $result = fgets($fp, 255); $exists = !empty($result); pclose($fp); } return $exists; } protected function dumpZipContents(string $path): string { if (!$this->cmdExists('hexdump')) { return ''; } $output = []; if (!exec("hexdump -C \"$path\" | head -n 50", $output)) { return ''; } return "\nHexdump:\n" . implode("\n", $output); } protected function validateAndExtractZip(string $zipPath): string { $tmpDir = $this->getTmpDir(); $zipArchive = new ZipArchive(); $result = $zipArchive->open($zipPath); if ($result !== true) { $codeName = $this->zipArchiveOpenErrorCodeName($result); $debugInformation = $this->dumpZipContents($zipPath); $this->fail("Failed to open {$zipPath}. Code: $result ($codeName)$debugInformation"); return $tmpDir; } $this->assertSame(0, $zipArchive->status); $this->assertSame(0, $zipArchive->statusSys); $zipArchive->extractTo($tmpDir); $zipArchive->close(); return $tmpDir; } protected function zipArchiveOpenErrorCodeName(int $code): string { switch($code) { case ZipArchive::ER_EXISTS: return 'ER_EXISTS'; case ZipArchive::ER_INCONS: return 'ER_INCONS'; case ZipArchive::ER_INVAL: return 'ER_INVAL'; case ZipArchive::ER_MEMORY: return 'ER_MEMORY'; case ZipArchive::ER_NOENT: return 'ER_NOENT'; case ZipArchive::ER_NOZIP: return 'ER_NOZIP'; case ZipArchive::ER_OPEN: return 'ER_OPEN'; case ZipArchive::ER_READ: return 'ER_READ'; case ZipArchive::ER_SEEK: return 'ER_SEEK'; default: return 'unknown'; } } protected function getTmpDir(): string { $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); unlink($tmp); mkdir($tmp) or $this->fail('Failed to make directory'); return $tmp; } /** * @return string[] */ protected function getRecursiveFileList(string $path, bool $includeDirectories = false): array { $data = []; $path = (string)realpath($path); $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); $pathLen = strlen($path); foreach ($files as $file) { $filePath = $file->getRealPath(); if (is_dir($filePath) && !$includeDirectories) { continue; } $data[] = substr($filePath, $pathLen + 1); } sort($data); return $data; } } zipstream-php/test/EndOfCentralDirectoryTest.php 0000644 00000002552 15060132257 0016075 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use PHPUnit\Framework\TestCase; use ZipStream\EndOfCentralDirectory; class EndOfCentralDirectoryTest extends TestCase { public function testSerializesCorrectly(): void { $this->assertSame( bin2hex(EndOfCentralDirectory::generate( numberOfThisDisk: 0x00, numberOfTheDiskWithCentralDirectoryStart: 0x00, numberOfCentralDirectoryEntriesOnThisDisk: 0x10, numberOfCentralDirectoryEntries: 0x10, sizeOfCentralDirectory: 0x22, centralDirectoryStartOffsetOnDisk: 0x33, zipFileComment: 'foo', )), '504b0506' . // 4 bytes; end of central dir signature 0x06054b50 '0000' . // 2 bytes; number of this disk '0000' . // 2 bytes; number of the disk with the start of the central directory '1000' . // 2 bytes; total number of entries in the central directory on this disk '1000' . // 2 bytes; total number of entries in the central directory '22000000' . // 4 bytes; size of the central directory '33000000' . // 4 bytes; offset of start of central directory with respect to the starting disk number '0300' . // 2 bytes; .ZIP file comment length bin2hex('foo') ); } } zipstream-php/test/bootstrap.php 0000644 00000000161 15060132257 0013053 0 ustar 00 <?php declare(strict_types=1); date_default_timezone_set('UTC'); require __DIR__ . '/../vendor/autoload.php'; zipstream-php/test/DataDescriptorTest.php 0000644 00000001332 15060132257 0014607 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use PHPUnit\Framework\TestCase; use ZipStream\DataDescriptor; class DataDescriptorTest extends TestCase { public function testSerializesCorrectly(): void { $this->assertSame( bin2hex(DataDescriptor::generate( crc32UncompressedData: 0x11111111, compressedSize: 0x77777777, uncompressedSize: 0x99999999, )), '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50 '11111111' . // 4 bytes; CRC-32 of uncompressed data '77777777' . // 4 bytes; Compressed size '99999999' // 4 bytes; Uncompressed size ); } } zipstream-php/test/TimeTest.php 0000644 00000001570 15060132260 0012573 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use DateTimeImmutable; use PHPUnit\Framework\TestCase; use ZipStream\Exception\DosTimeOverflowException; use ZipStream\Time; class TimeTest extends TestCase { public function testNormalDateToDosTime(): void { $this->assertSame( Time::dateTimeToDosTime(new DateTimeImmutable('2014-11-17T17:46:08Z')), 1165069764 ); // January 1 1980 - DOS Epoch. $this->assertSame( Time::dateTimeToDosTime(new DateTimeImmutable('1980-01-01T00:00:00+00:00')), 2162688 ); } public function testTooEarlyDateToDosTime(): void { $this->expectException(DosTimeOverflowException::class); // January 1 1980 is the minimum DOS Epoch. Time::dateTimeToDosTime(new DateTimeImmutable('1970-01-01T00:00:00+00:00')); } } zipstream-php/test/LocalFileHeaderTest.php 0000644 00000003311 15060132260 0014633 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use DateTimeImmutable; use PHPUnit\Framework\TestCase; use ZipStream\CompressionMethod; use ZipStream\LocalFileHeader; class LocalFileHeaderTest extends TestCase { public function testSerializesCorrectly(): void { $dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z'); $header = LocalFileHeader::generate( versionNeededToExtract: 0x002D, generalPurposeBitFlag: 0x2222, compressionMethod: CompressionMethod::DEFLATE, lastModificationDateTime: $dateTime, crc32UncompressedData: 0x11111111, compressedSize: 0x77777777, uncompressedSize: 0x99999999, fileName: 'test.png', extraField: 'some content' ); $this->assertSame( bin2hex((string) $header), '504b0304' . // 4 bytes; Local file header signature '2d00' . // 2 bytes; Version needed to extract (minimum) '2222' . // 2 bytes; General purpose bit flag '0800' . // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8 '2008' . // 2 bytes; File last modification time '2154' . // 2 bytes; File last modification date '11111111' . // 4 bytes; CRC-32 of uncompressed data '77777777' . // 4 bytes; Compressed size (or 0xffffffff for ZIP64) '99999999' . // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64) '0800' . // 2 bytes; File name length (n) '0c00' . // 2 bytes; Extra field length (m) '746573742e706e67' . // n bytes; File name '736f6d6520636f6e74656e74' // m bytes; Extra field ); } } zipstream-php/test/ZipStreamTest.php 0000644 00000114654 15060132260 0013623 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use DateTimeImmutable; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\StreamWrapper; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; use RuntimeException; use ZipArchive; use ZipStream\CompressionMethod; use ZipStream\Exception\FileNotFoundException; use ZipStream\Exception\FileNotReadableException; use ZipStream\Exception\FileSizeIncorrectException; use ZipStream\Exception\OverflowException; use ZipStream\Exception\ResourceActionException; use ZipStream\Exception\SimulationFileUnknownException; use ZipStream\Exception\StreamNotReadableException; use ZipStream\Exception\StreamNotSeekableException; use ZipStream\OperationMode; use ZipStream\PackField; use ZipStream\ZipStream; class ZipStreamTest extends TestCase { use Util; use Assertions; public function testAddFile(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $zip->addFile('sample.txt', 'Sample String Data'); $zip->addFile('test/sample.txt', 'More Simple Sample Data'); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); } public function testAddFileUtf8NameComment(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $name = 'árvíztűrő tükörfúrógép.txt'; $content = 'Sample String Data'; $comment = 'Filename has every special characters ' . 'from Hungarian language in lowercase. ' . 'In uppercase: ÁÍŰŐÜÖÚÓÉ'; $zip->addFile(fileName: $name, data: $content, comment: $comment); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame([$name], $files); $this->assertStringEqualsFile($tmpDir . '/' . $name, $content); $zipArchive = new ZipArchive(); $zipArchive->open($tmp); $this->assertSame($comment, $zipArchive->getCommentName($name)); } public function testAddFileUtf8NameNonUtfComment(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $name = 'á.txt'; $content = 'any'; $comment = mb_convert_encoding('á', 'ISO-8859-2', 'UTF-8'); // @see https://libzip.org/documentation/zip_file_get_comment.html // // mb_convert_encoding hasn't CP437. // nearly CP850 (DOS-Latin-1) $guessComment = mb_convert_encoding($comment, 'UTF-8', 'CP850'); $zip->addFile(fileName: $name, data: $content, comment: $comment); $zip->finish(); fclose($stream); $zipArch = new ZipArchive(); $zipArch->open($tmp); $this->assertSame($guessComment, $zipArch->getCommentName($name)); $this->assertSame($comment, $zipArch->getCommentName($name, ZipArchive::FL_ENC_RAW)); } public function testAddFileWithStorageMethod(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $zip->addFile(fileName: 'sample.txt', data: 'Sample String Data', compressionMethod: CompressionMethod::STORE); $zip->addFile(fileName: 'test/sample.txt', data: 'More Simple Sample Data'); $zip->finish(); fclose($stream); $zipArchive = new ZipArchive(); $zipArchive->open($tmp); $sample1 = $zipArchive->statName('sample.txt'); $sample12 = $zipArchive->statName('test/sample.txt'); $this->assertSame($sample1['comp_method'], CompressionMethod::STORE->value); $this->assertSame($sample12['comp_method'], CompressionMethod::DEFLATE->value); $zipArchive->close(); } public function testAddFileFromPath(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); [$tmpExample, $streamExample] = $this->getTmpFileStream(); fwrite($streamExample, 'Sample String Data'); fclose($streamExample); $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample); [$tmpExample, $streamExample] = $this->getTmpFileStream(); fwrite($streamExample, 'More Simple Sample Data'); fclose($streamExample); $zip->addFileFromPath(fileName: 'test/sample.txt', path: $tmpExample); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); } public function testAddFileFromPathFileNotFoundException(): void { $this->expectException(FileNotFoundException::class); [, $stream] = $this->getTmpFileStream(); // Get ZipStream Object $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); // Trigger error by adding a file which doesn't exist $zip->addFileFromPath(fileName: 'foobar.php', path: '/foo/bar/foobar.php'); } public function testAddFileFromPathFileNotReadableException(): void { $this->expectException(FileNotReadableException::class); [, $stream] = $this->getTmpFileStream(); // create new virtual filesystem $root = vfsStream::setup('vfs'); // create a virtual file with no permissions $file = vfsStream::newFile('foo.txt', 0)->at($root)->setContent('bar'); // Get ZipStream Object $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $zip->addFileFromPath('foo.txt', $file->url()); } public function testAddFileFromPathWithStorageMethod(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); [$tmpExample, $streamExample] = $this->getTmpFileStream(); fwrite($streamExample, 'Sample String Data'); fclose($streamExample); $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample, compressionMethod: CompressionMethod::STORE); [$tmpExample, $streamExample] = $this->getTmpFileStream(); fwrite($streamExample, 'More Simple Sample Data'); fclose($streamExample); $zip->addFileFromPath('test/sample.txt', $tmpExample); $zip->finish(); fclose($stream); $zipArchive = new ZipArchive(); $zipArchive->open($tmp); $sample1 = $zipArchive->statName('sample.txt'); $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']); $sample2 = $zipArchive->statName('test/sample.txt'); $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']); $zipArchive->close(); } public function testAddLargeFileFromPath(): void { foreach ([CompressionMethod::DEFLATE, CompressionMethod::STORE] as $compressionMethod) { foreach ([false, true] as $zeroHeader) { foreach ([false, true] as $zip64) { if ($zeroHeader && $compressionMethod === CompressionMethod::DEFLATE) { continue; } $this->addLargeFileFileFromPath( compressionMethod: $compressionMethod, zeroHeader: $zeroHeader, zip64: $zip64 ); } } } } public function testAddFileFromStream(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); // In this test we can't use temporary stream to feed data // because zlib.deflate filter gives empty string before PHP 7 // it works fine with file stream $streamExample = fopen(__FILE__, 'rb'); $zip->addFileFromStream('sample.txt', $streamExample); fclose($streamExample); $streamExample2 = fopen('php://temp', 'wb+'); fwrite($streamExample2, 'More Simple Sample Data'); rewind($streamExample2); // move the pointer back to the beginning of file. $zip->addFileFromStream('test/sample.txt', $streamExample2); //, $fileOptions); fclose($streamExample2); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); $this->assertStringEqualsFile(__FILE__, file_get_contents($tmpDir . '/sample.txt')); $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); } public function testAddFileFromStreamUnreadableInput(): void { $this->expectException(StreamNotReadableException::class); [, $stream] = $this->getTmpFileStream(); [$tmpInput] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $streamUnreadable = fopen($tmpInput, 'w'); $zip->addFileFromStream('sample.json', $streamUnreadable); } public function testAddFileFromStreamBrokenOutputWrite(): void { $this->expectException(ResourceActionException::class); $outputStream = FaultInjectionResource::getResource(['stream_write']); $zip = new ZipStream( outputStream: $outputStream, sendHttpHeaders: false, ); $zip->addFile('sample.txt', 'foobar'); } public function testAddFileFromStreamBrokenInputRewind(): void { $this->expectException(ResourceActionException::class); [,$stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, defaultEnableZeroHeader: false, ); $fileStream = FaultInjectionResource::getResource(['stream_seek']); $zip->addFileFromStream('sample.txt', $fileStream, maxSize: 0); } public function testAddFileFromStreamUnseekableInputWithoutZeroHeader(): void { $this->expectException(StreamNotSeekableException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, defaultEnableZeroHeader: false, ); if (file_exists('/dev/null')) { $streamUnseekable = fopen('/dev/null', 'w+'); } elseif (file_exists('NUL')) { $streamUnseekable = fopen('NUL', 'w+'); } else { $this->markTestSkipped('Needs file /dev/null'); } $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 2); } public function testAddFileFromStreamUnseekableInputWithZeroHeader(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, defaultEnableZeroHeader: true, defaultCompressionMethod: CompressionMethod::STORE, ); $streamUnseekable = StreamWrapper::getResource(new class ('test') extends EndlessCycleStream { public function isSeekable(): bool { return false; } public function seek(int $offset, int $whence = SEEK_SET): void { throw new RuntimeException('Not seekable'); } }); $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 7); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt'], $files); $this->assertSame(filesize($tmpDir . '/sample.txt'), 7); } public function testAddFileFromStreamWithStorageMethod(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $streamExample = fopen('php://temp', 'wb+'); fwrite($streamExample, 'Sample String Data'); rewind($streamExample); // move the pointer back to the beginning of file. $zip->addFileFromStream('sample.txt', $streamExample, compressionMethod: CompressionMethod::STORE); fclose($streamExample); $streamExample2 = fopen('php://temp', 'bw+'); fwrite($streamExample2, 'More Simple Sample Data'); rewind($streamExample2); // move the pointer back to the beginning of file. $zip->addFileFromStream('test/sample.txt', $streamExample2, compressionMethod: CompressionMethod::DEFLATE); fclose($streamExample2); $zip->finish(); fclose($stream); $zipArchive = new ZipArchive(); $zipArchive->open($tmp); $sample1 = $zipArchive->statName('sample.txt'); $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']); $sample2 = $zipArchive->statName('test/sample.txt'); $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']); $zipArchive->close(); } public function testAddFileFromPsr7Stream(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $body = 'Sample String Data'; $response = new Response(200, [], $body); $zip->addFileFromPsr7Stream('sample.json', $response->getBody()); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); } /** * @group slow */ public function testAddLargeFileFromPsr7Stream(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: true, ); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: new EndlessCycleStream('0'), maxSize: 0x100000000, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertFileIsReadable($tmpDir . '/sample.json'); $this->assertStringStartsWith('000000', file_get_contents(filename: $tmpDir . '/sample.json', length: 20)); } public function testContinueFinishedZip(): void { $this->expectException(RuntimeException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $zip->finish(); $zip->addFile('sample.txt', '1234'); } /** * @group slow */ public function testManyFilesWithoutZip64(): void { $this->expectException(OverflowException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: false, ); for ($i = 0; $i <= 0xFFFF; $i++) { $zip->addFile('sample' . $i, ''); } $zip->finish(); } /** * @group slow */ public function testManyFilesWithZip64(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: true, ); for ($i = 0; $i <= 0xFFFF; $i++) { $zip->addFile('sample' . $i, ''); } $zip->finish(); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(count($files), 0x10000); } /** * @group slow */ public function testLongZipWithout64(): void { $this->expectException(OverflowException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: false, defaultCompressionMethod: CompressionMethod::STORE, ); for ($i = 0; $i < 4; $i++) { $zip->addFileFromPsr7Stream( fileName: 'sample' . $i, stream: new EndlessCycleStream('0'), maxSize: 0xFFFFFFFF, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); } } /** * @group slow */ public function testLongZipWith64(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: true, defaultCompressionMethod: CompressionMethod::STORE, ); for ($i = 0; $i < 4; $i++) { $zip->addFileFromPsr7Stream( fileName: 'sample' . $i, stream: new EndlessCycleStream('0'), maxSize: 0x5FFFFFFF, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); } $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample0', 'sample1', 'sample2', 'sample3'], $files); } /** * @group slow */ public function testAddLargeFileWithoutZip64WithZeroHeader(): void { $this->expectException(OverflowException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: false, defaultEnableZeroHeader: true, ); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: new EndlessCycleStream('0'), maxSize: 0x100000000, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); } /** * @group slow */ public function testAddsZip64HeaderWhenNeeded(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: true, defaultEnableZeroHeader: false, ); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: new EndlessCycleStream('0'), maxSize: 0x100000000, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); $zip->finish(); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertFileContains($tmp, PackField::pack( new PackField(format: 'V', value: 0x06064b50) )); } /** * @group slow */ public function testDoesNotAddZip64HeaderWhenNotNeeded(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: true, defaultEnableZeroHeader: false, ); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: new EndlessCycleStream('0'), maxSize: 0x10, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); $zip->finish(); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertFileDoesNotContain($tmp, PackField::pack( new PackField(format: 'V', value: 0x06064b50) )); } /** * @group slow */ public function testAddLargeFileWithoutZip64WithoutZeroHeader(): void { $this->expectException(OverflowException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, enableZip64: false, defaultEnableZeroHeader: false, ); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: new EndlessCycleStream('0'), maxSize: 0x100000000, compressionMethod: CompressionMethod::STORE, lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), ); } public function testAddFileFromPsr7StreamWithOutputToPsr7Stream(): void { [$tmp, $resource] = $this->getTmpFileStream(); $psr7OutputStream = new ResourceStream($resource); $zip = new ZipStream( outputStream: $psr7OutputStream, sendHttpHeaders: false, ); $body = 'Sample String Data'; $response = new Response(200, [], $body); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: $response->getBody(), compressionMethod: CompressionMethod::STORE, ); $zip->finish(); $psr7OutputStream->close(); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); } public function testAddFileFromPsr7StreamWithFileSizeSet(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $body = 'Sample String Data'; $fileSize = strlen($body); // Add fake padding $fakePadding = "\0\0\0\0\0\0"; $response = new Response(200, [], $body . $fakePadding); $zip->addFileFromPsr7Stream( fileName: 'sample.json', stream: $response->getBody(), compressionMethod: CompressionMethod::STORE, maxSize: $fileSize ); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.json'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); } public function testCreateArchiveHeaders(): void { [, $stream] = $this->getTmpFileStream(); $headers = []; $httpHeaderCallback = function (string $header) use (&$headers) { $headers[] = $header; }; $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: true, outputName: 'example.zip', httpHeaderCallback: $httpHeaderCallback, ); $zip->addFile( fileName: 'sample.json', data: 'foo', ); $zip->finish(); fclose($stream); $this->assertContains('Content-Type: application/x-zip', $headers); $this->assertContains("Content-Disposition: attachment; filename*=UTF-8''example.zip", $headers); $this->assertContains('Pragma: public', $headers); $this->assertContains('Cache-Control: public, must-revalidate', $headers); $this->assertContains('Content-Transfer-Encoding: binary', $headers); } public function testCreateArchiveWithFlushOptionSet(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, flushOutput: true, sendHttpHeaders: false, ); $zip->addFile('sample.txt', 'Sample String Data'); $zip->addFile('test/sample.txt', 'More Simple Sample Data'); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); } public function testCreateArchiveWithOutputBufferingOffAndFlushOptionSet(): void { // WORKAROUND (1/2): remove phpunit's output buffer in order to run test without any buffering ob_end_flush(); $this->assertSame(0, ob_get_level()); [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, flushOutput: true, sendHttpHeaders: false, ); $zip->addFile('sample.txt', 'Sample String Data'); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); // WORKAROUND (2/2): add back output buffering so that PHPUnit doesn't complain that it is missing ob_start(); } public function testAddEmptyDirectory(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, ); $zip->addDirectory('foo'); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir, includeDirectories: true); $this->assertContains('foo', $files); $this->assertFileExists($tmpDir . DIRECTORY_SEPARATOR . 'foo'); $this->assertDirectoryExists($tmpDir . DIRECTORY_SEPARATOR . 'foo'); } public function testAddFileSimulate(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultEnableZeroHeader: true, outputStream: $stream, ); $zip->addFile('sample.txt', 'Sample String Data'); $zip->addFile('test/sample.txt', 'More Simple Sample Data'); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithMaxSize(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultCompressionMethod: CompressionMethod::STORE, defaultEnableZeroHeader: true, outputStream: $stream, ); $zip->addFile('sample.txt', 'Sample String Data', maxSize: 0); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithFstat(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultCompressionMethod: CompressionMethod::STORE, defaultEnableZeroHeader: true, outputStream: $stream, ); $zip->addFile('sample.txt', 'Sample String Data'); $zip->addFile('test/sample.txt', 'More Simple Sample Data'); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithExactSizeZero(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultCompressionMethod: CompressionMethod::STORE, defaultEnableZeroHeader: true, outputStream: $stream, ); $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithExactSizeInitial(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultCompressionMethod: CompressionMethod::STORE, defaultEnableZeroHeader: false, outputStream: $stream, ); $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithZeroSizeInFstat(): void { [, $stream] = $this->getTmpFileStream(); $create = function (OperationMode $operationMode) use ($stream): int { $zip = new ZipStream( sendHttpHeaders: false, operationMode: $operationMode, defaultCompressionMethod: CompressionMethod::STORE, defaultEnableZeroHeader: false, outputStream: $stream, ); $zip->addFileFromPsr7Stream('sample.txt', new class () implements StreamInterface { public $pos = 0; public function __toString(): string { return 'test'; } public function close(): void { } public function detach() { } public function getSize(): ?int { return null; } public function tell(): int { return $this->pos; } public function eof(): bool { return $this->pos >= 4; } public function isSeekable(): bool { return true; } public function seek(int $offset, int $whence = SEEK_SET): void { $this->pos = $offset; } public function rewind(): void { $this->pos = 0; } public function isWritable(): bool { return false; } public function write(string $string): int { return 0; } public function isReadable(): bool { return true; } public function read(int $length): string { $data = substr('test', $this->pos, $length); $this->pos += strlen($data); return $data; } public function getContents(): string { return $this->read(4); } public function getMetadata(?string $key = null) { return $key !== null ? null : []; } }); return $zip->finish(); }; $sizeExpected = $create(OperationMode::NORMAL); $sizeActual = $create(OperationMode::SIMULATE_LAX); $this->assertEquals($sizeExpected, $sizeActual); } public function testAddFileSimulateWithWrongExactSize(): void { $this->expectException(FileSizeIncorrectException::class); $zip = new ZipStream( sendHttpHeaders: false, operationMode: OperationMode::SIMULATE_LAX, ); $zip->addFile('sample.txt', 'Sample String Data', exactSize: 1000); } public function testAddFileSimulateStrictZero(): void { $this->expectException(SimulationFileUnknownException::class); $zip = new ZipStream( sendHttpHeaders: false, operationMode: OperationMode::SIMULATE_STRICT, defaultEnableZeroHeader: true ); $zip->addFile('sample.txt', 'Sample String Data'); } public function testAddFileSimulateStrictInitial(): void { $this->expectException(SimulationFileUnknownException::class); $zip = new ZipStream( sendHttpHeaders: false, operationMode: OperationMode::SIMULATE_STRICT, defaultEnableZeroHeader: false ); $zip->addFile('sample.txt', 'Sample String Data'); } public function testAddFileCallbackStrict(): void { $this->expectException(SimulationFileUnknownException::class); $zip = new ZipStream( sendHttpHeaders: false, operationMode: OperationMode::SIMULATE_STRICT, defaultEnableZeroHeader: false ); $zip->addFileFromCallback('sample.txt', callback: function () { return ''; }); } public function testAddFileCallbackLax(): void { $zip = new ZipStream( operationMode: OperationMode::SIMULATE_LAX, defaultEnableZeroHeader: false, sendHttpHeaders: false, ); $zip->addFileFromCallback('sample.txt', callback: function () { return 'Sample String Data'; }); $size = $zip->finish(); $this->assertEquals($size, 142); } public function testExecuteSimulation(): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( operationMode: OperationMode::SIMULATE_LAX, defaultEnableZeroHeader: false, sendHttpHeaders: false, outputStream: $stream, ); $zip->addFileFromCallback( 'sample.txt', exactSize: 18, callback: function () { return 'Sample String Data'; } ); $size = $zip->finish(); $this->assertEquals(filesize($tmp), 0); $zip->executeSimulation(); fclose($stream); clearstatcache(); $this->assertEquals(filesize($tmp), $size); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt'], $files); } public function testExecuteSimulationBeforeFinish(): void { $this->expectException(RuntimeException::class); [, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( operationMode: OperationMode::SIMULATE_LAX, defaultEnableZeroHeader: false, sendHttpHeaders: false, outputStream: $stream, ); $zip->executeSimulation(); } private function addLargeFileFileFromPath(CompressionMethod $compressionMethod, $zeroHeader, $zip64): void { [$tmp, $stream] = $this->getTmpFileStream(); $zip = new ZipStream( outputStream: $stream, sendHttpHeaders: false, defaultEnableZeroHeader: $zeroHeader, enableZip64: $zip64, ); [$tmpExample, $streamExample] = $this->getTmpFileStream(); for ($i = 0; $i <= 10000; $i++) { fwrite($streamExample, sha1((string)$i)); if ($i % 100 === 0) { fwrite($streamExample, "\n"); } } fclose($streamExample); $shaExample = sha1_file($tmpExample); $zip->addFileFromPath('sample.txt', $tmpExample); unlink($tmpExample); $zip->finish(); fclose($stream); $tmpDir = $this->validateAndExtractZip($tmp); $files = $this->getRecursiveFileList($tmpDir); $this->assertSame(['sample.txt'], $files); $this->assertSame(sha1_file($tmpDir . '/sample.txt'), $shaExample, "SHA-1 Mismatch Method: {$compressionMethod->value}"); } } zipstream-php/test/CentralDirectoryFileHeaderTest.php 0000644 00000004332 15060132260 0017062 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; use DateTimeImmutable; use PHPUnit\Framework\TestCase; use ZipStream\CentralDirectoryFileHeader; use ZipStream\CompressionMethod; class CentralDirectoryFileHeaderTest extends TestCase { public function testSerializesCorrectly(): void { $dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z'); $header = CentralDirectoryFileHeader::generate( versionMadeBy: 0x603, versionNeededToExtract: 0x002D, generalPurposeBitFlag: 0x2222, compressionMethod: CompressionMethod::DEFLATE, lastModificationDateTime: $dateTime, crc32: 0x11111111, compressedSize: 0x77777777, uncompressedSize: 0x99999999, fileName: 'test.png', extraField: 'some content', fileComment: 'some comment', diskNumberStart: 0, internalFileAttributes: 0, externalFileAttributes: 32, relativeOffsetOfLocalHeader: 0x1234, ); $this->assertSame( bin2hex($header), '504b0102' . // 4 bytes; central file header signature '0306' . // 2 bytes; version made by '2d00' . // 2 bytes; version needed to extract '2222' . // 2 bytes; general purpose bit flag '0800' . // 2 bytes; compression method '2008' . // 2 bytes; last mod file time '2154' . // 2 bytes; last mod file date '11111111' . // 4 bytes; crc-32 '77777777' . // 4 bytes; compressed size '99999999' . // 4 bytes; uncompressed size '0800' . // 2 bytes; file name length (n) '0c00' . // 2 bytes; extra field length (m) '0c00' . // 2 bytes; file comment length (o) '0000' . // 2 bytes; disk number start '0000' . // 2 bytes; internal file attributes '20000000' . // 4 bytes; external file attributes '34120000' . // 4 bytes; relative offset of local header '746573742e706e67' . // n bytes; file name '736f6d6520636f6e74656e74' . // m bytes; extra field '736f6d6520636f6d6d656e74' // o bytes; file comment ); } } zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php 0000644 00000002577 15060132260 0020537 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test\Zip64; use PHPUnit\Framework\TestCase; use ZipStream\Zip64\ExtendedInformationExtraField; class ExtendedInformationExtraFieldTest extends TestCase { public function testSerializesCorrectly(): void { $extraField = ExtendedInformationExtraField::generate( originalSize: (0x77777777 << 32) + 0x66666666, compressedSize: (0x99999999 << 32) + 0x88888888, relativeHeaderOffset: (0x22222222 << 32) + 0x11111111, diskStartNumber: 0x33333333, ); $this->assertSame( bin2hex($extraField), '0100' . // 2 bytes; Tag for this "extra" block type '1c00' . // 2 bytes; Size of this "extra" block '6666666677777777' . // 8 bytes; Original uncompressed file size '8888888899999999' . // 8 bytes; Size of compressed data '1111111122222222' . // 8 bytes; Offset of local header record '33333333' // 4 bytes; Number of the disk on which this file starts ); } public function testSerializesEmptyCorrectly(): void { $extraField = ExtendedInformationExtraField::generate(); $this->assertSame( bin2hex($extraField), '0100' . // 2 bytes; Tag for this "extra" block type '0000' // 2 bytes; Size of this "extra" block ); } } zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php 0000644 00000003413 15060132260 0017000 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test\Zip64; use PHPUnit\Framework\TestCase; use ZipStream\Zip64\EndOfCentralDirectory; class EndOfCentralDirectoryTest extends TestCase { public function testSerializesCorrectly(): void { $descriptor = EndOfCentralDirectory::generate( versionMadeBy: 0x3333, versionNeededToExtract: 0x4444, numberOfThisDisk: 0x55555555, numberOfTheDiskWithCentralDirectoryStart: 0x66666666, numberOfCentralDirectoryEntriesOnThisDisk: (0x77777777 << 32) + 0x88888888, numberOfCentralDirectoryEntries: (0x99999999 << 32) + 0xAAAAAAAA, sizeOfCentralDirectory: (0xBBBBBBBB << 32) + 0xCCCCCCCC, centralDirectoryStartOffsetOnDisk: (0xDDDDDDDD << 32) + 0xEEEEEEEE, extensibleDataSector: 'foo', ); $this->assertSame( bin2hex($descriptor), '504b0606' . // 4 bytes;zip64 end of central dir signature - 0x06064b50 '2f00000000000000' . // 8 bytes; size of zip64 end of central directory record '3333' . // 2 bytes; version made by '4444' . // 2 bytes; version needed to extract '55555555' . // 4 bytes; number of this disk '66666666' . // 4 bytes; number of the disk with the start of the central directory '8888888877777777' . // 8 bytes; total number of entries in the central directory on this disk 'aaaaaaaa99999999' . // 8 bytes; total number of entries in the central directory 'ccccccccbbbbbbbb' . // 8 bytes; size of the central directory 'eeeeeeeedddddddd' . // 8 bytes; offset of start of central directory with respect to the starting disk number bin2hex('foo') ); } } zipstream-php/test/Zip64/DataDescriptorTest.php 0000644 00000001465 15060132260 0015524 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test\Zip64; use PHPUnit\Framework\TestCase; use ZipStream\Zip64\DataDescriptor; class DataDescriptorTest extends TestCase { public function testSerializesCorrectly(): void { $descriptor = DataDescriptor::generate( crc32UncompressedData: 0x11111111, compressedSize: (0x77777777 << 32) + 0x66666666, uncompressedSize: (0x99999999 << 32) + 0x88888888, ); $this->assertSame( bin2hex($descriptor), '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50 '11111111' . // 4 bytes; CRC-32 of uncompressed data '6666666677777777' . // 8 bytes; Compressed size '8888888899999999' // 8 bytes; Uncompressed size ); } } zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php 0000644 00000001730 15060132260 0020324 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test\Zip64; use PHPUnit\Framework\TestCase; use ZipStream\Zip64\EndOfCentralDirectoryLocator; class EndOfCentralDirectoryLocatorTest extends TestCase { public function testSerializesCorrectly(): void { $descriptor = EndOfCentralDirectoryLocator::generate( numberOfTheDiskWithZip64CentralDirectoryStart: 0x11111111, zip64centralDirectoryStartOffsetOnDisk: (0x22222222 << 32) + 0x33333333, totalNumberOfDisks: 0x44444444, ); $this->assertSame( bin2hex($descriptor), '504b0607' . // 4 bytes; zip64 end of central dir locator signature - 0x07064b50 '11111111' . // 4 bytes; number of the disk with the start of the zip64 end of central directory '3333333322222222' . // 28 bytes; relative offset of the zip64 end of central directory record '44444444' // 4 bytes;total number of disks ); } } zipstream-php/test/Assertions.php 0000644 00000002032 15060132260 0013161 0 ustar 00 <?php declare(strict_types=1); namespace ZipStream\Test; trait Assertions { protected function assertFileContains(string $filePath, string $needle): void { $last = ''; $handle = fopen($filePath, 'r'); while (!feof($handle)) { $line = fgets($handle, 1024); if(str_contains($last . $line, $needle)) { fclose($handle); return; } $last = $line; } fclose($handle); $this->fail("File {$filePath} must contain {$needle}"); } protected function assertFileDoesNotContain(string $filePath, string $needle): void { $last = ''; $handle = fopen($filePath, 'r'); while (!feof($handle)) { $line = fgets($handle, 1024); if(str_contains($last . $line, $needle)) { fclose($handle); $this->fail("File {$filePath} must not contain {$needle}"); } $last = $line; } fclose($handle); } } zipstream-php/.phive/phars.xml 0000644 00000000306 15060132260 0012371 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <phive xmlns="https://phar.io/phive"> <phar name="phpdocumentor" version="^3.3.1" installed="3.3.1" location="./tools/phpdocumentor" copy="false"/> </phive> zipstream-php/phpunit.xml.dist 0000644 00000000736 15060132260 0012523 0 ustar 00 <?xml version="1.0"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="test/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" cacheDirectory=".phpunit.cache"> <coverage/> <testsuites> <testsuite name="Application"> <directory>test</directory> </testsuite> </testsuites> <logging/> <source> <include> <directory suffix=".php">src</directory> </include> </source> </phpunit> zipstream-php/phpdoc.dist.xml 0000644 00000002412 15060132260 0012302 0 ustar 00 <?xml version="1.0" encoding="UTF-8" ?> <phpdocumentor configVersion="3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://www.phpdoc.org" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/phpDocumentor/phpDocumentor/master/data/xsd/phpdoc.xsd" > <title>💾 ZipStream-PHP</title> <paths> <output>docs</output> </paths> <version number="3.0.0"> <folder>latest</folder> <api> <source dsn="."> <path>src</path> </source> <output>api</output> <ignore hidden="true" symlinks="true"> <path>tests/**/*</path> <path>vendor/**/*</path> </ignore> <extensions> <extension>php</extension> </extensions> <visibility>public</visibility> <default-package-name>ZipStream</default-package-name> <include-source>true</include-source> </api> <guide> <source dsn="."> <path>guides</path> </source> <output>guide</output> </guide> </version> <setting name="guides.enabled" value="true"/> <template name="default" /> </phpdocumentor>