uawdijnntqw1x1x1
IP : 216.73.216.153
Hostname : 6.87.74.97.host.secureserver.net
Kernel : Linux 6.87.74.97.host.secureserver.net 4.18.0-553.83.1.el8_10.x86_64 #1 SMP Mon Nov 10 04:22:44 EST 2025 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
home
/
emeraadmin
/
.htpasswds
/
..
/
public_html
/
ADVIA
/
..
/
4d695
/
zipstream-php.tar
/
/
src/GeneralPurposeBitFlag.php000064400000004613151677430260012247 0ustar00<?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; } src/Time.php000064400000002133151677430260006754 0ustar00<?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); } } src/CompressionMethod.php000064400000004132151677430260011521 0ustar00<?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; } src/PackField.php000064400000002700151677430260007700 0ustar00<?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); } } src/ZipStream.php000064400000066126151677430260010010 0ustar00<?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; } } } src/LocalFileHeader.php000064400000002477151677430260011034 0ustar00<?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; } } src/Exception/FileSizeIncorrectException.php000064400000001007151677430260015255 0ustar00<?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."); } } src/Exception/FileNotFoundException.php000064400000000613151677430260014230 0ustar00<?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."); } } src/Exception/OverflowException.php000064400000000620151677430260013475 0ustar00<?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.'); } } src/Exception/StreamNotSeekableException.php000064400000000653151677430260015250 0ustar00<?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'); } } src/Exception/StreamNotReadableException.php000064400000000541151677430260015230 0ustar00<?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.'); } } src/Exception/DosTimeOverflowException.php000064400000000756151677430260014774 0ustar00<?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."); } } src/Exception/ResourceActionException.php000064400000001063151677430260014621 0ustar00<?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.'); } } src/Exception/SimulationFileUnknownException.php000064400000000742151677430260016203 0ustar00<?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.'); } } src/Exception/FileNotReadableException.php000064400000000620151677430260014652 0ustar00<?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."); } } src/Zip64/ExtendedInformationExtraField.php000064400000002536151677430260014717 0ustar00<?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), ]), ); } } src/Zip64/EndOfCentralDirectoryLocator.php000064400000001451151677430260014511 0ustar00<?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), ); } } src/Zip64/DataDescriptor.php000064400000001210151677430260011675 0ustar00<?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), ); } } src/Zip64/EndOfCentralDirectory.php000064400000003046151677430260013167 0ustar00<?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; } } src/Zs/ExtendedInformationExtraField.php000064400000000621151677430260014370 0ustar00<?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), ); } } src/CentralDirectoryFileHeader.php000064400000003646151677430260013256 0ustar00<?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; } } src/Exception.php000064400000000147151677430260010017 0ustar00<?php declare(strict_types=1); namespace ZipStream; abstract class Exception extends \Exception { } src/DataDescriptor.php000064400000001150151677430260010764 0ustar00<?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), ); } } src/EndOfCentralDirectory.php000064400000002343151677430260012252 0ustar00<?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; } } src/OperationMode.php000064400000001443151677430260010626 0ustar00<?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; } src/File.php000064400000032073151677430260006743 0ustar00<?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; } } src/Version.php000064400000000262151677430260007504 0ustar00<?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 } .phive/phars.xml000064400000000306151677430260007606 0ustar00<?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> composer.json000064400000004452151677430260007306 0ustar00{ "name": "maennchen/zipstream-php", "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", "keywords": ["zip", "stream"], "type": "library", "license": "MIT", "authors": [{ "name": "Paul Duncan", "email": "pabs@pablotron.org" }, { "name": "Jonatan Männchen", "email": "jonatan@maennchen.ch" }, { "name": "Jesse Donat", "email": "donatj@gmail.com" }, { "name": "András Kolesár", "email": "kolesar@kolesar.hu" } ], "require": { "php-64bit": "^8.1", "ext-mbstring": "*", "ext-zlib": "*" }, "require-dev": { "phpunit/phpunit": "^10.0", "guzzlehttp/guzzle": "^7.5", "ext-zip": "*", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.5", "friendsofphp/php-cs-fixer": "^3.16", "vimeo/psalm": "^5.0" }, "suggest": { "psr/http-message": "^2.0", "guzzlehttp/psr7": "^2.4" }, "scripts": { "format": "php-cs-fixer fix", "test": [ "@test:unit", "@test:formatted", "@test:lint" ], "test:unit": "phpunit --coverage-clover=coverage.clover.xml --coverage-html cov", "test:unit:slow": "@test:unit --group slow", "test:unit:fast": "@test:unit --exclude-group slow", "test:formatted": "@format --dry-run --stop-on-violation --using-cache=no", "test:lint": "psalm --stats --show-info=true --find-unused-psalm-suppress", "coverage:report": "php-coveralls --coverage_clover=coverage.clover.xml --json_path=coveralls-upload.json --insecure", "install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656", "docs:generate": "tools/phpdocumentor --sourcecode" }, "autoload": { "psr-4": { "ZipStream\\": "src/" } }, "autoload-dev": { "psr-4": { "ZipStream\\Test\\": "test/" } }, "archive": { "exclude": [ "/composer.lock", "/docs", "/.gitattributes", "/.github", "/.gitignore", "/guides", "/.phive", "/.php-cs-fixer.cache", "/.php-cs-fixer.dist.php", "/.phpdoc", "/phpdoc.dist.xml", "/.phpunit.result.cache", "/phpunit.xml.dist", "/psalm.xml", "/test", "/tools", "/.tool-versions", "/vendor" ] } } phpdoc.dist.xml000064400000002412151677430260007517 0ustar00<?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>.editorconfig000064400000000445151677430260007237 0ustar00root = 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 phpunit.xml.dist000064400000000736151677430260007740 0ustar00<?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> test/ZipStreamTest.php000064400000114654151677430260011040 0ustar00<?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}"); } } test/EndOfCentralDirectoryTest.php000064400000002552151677430260013304 0ustar00<?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') ); } } test/EndlessCycleStream.php000064400000003762151677430260012010 0ustar00<?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 : []; } } test/Zip64/EndOfCentralDirectoryTest.php000064400000003413151677430260014215 0ustar00<?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') ); } } test/Zip64/DataDescriptorTest.php000064400000001465151677430260012741 0ustar00<?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 ); } } test/Zip64/EndOfCentralDirectoryLocatorTest.php000064400000001730151677430260015541 0ustar00<?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 ); } } test/Zip64/ExtendedInformationExtraFieldTest.php000064400000002577151677430260015754 0ustar00<?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 ); } } test/CentralDirectoryFileHeaderTest.php000064400000004332151677430260014277 0ustar00<?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 ); } } test/Zs/ExtendedInformationExtraFieldTest.php000064400000001017151677430260015420 0ustar00<?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 ); } } test/DataDescriptorTest.php000064400000001332151677430260012016 0ustar00<?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 ); } } test/ResourceStream.php000064400000007565151677430260011227 0ustar00<?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; } } test/Util.php000064400000006765151677430260007202 0ustar00<?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; } } test/TimeTest.php000064400000001570151677430260010010 0ustar00<?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')); } } test/bootstrap.php000064400000000161151677430260010262 0ustar00<?php declare(strict_types=1); date_default_timezone_set('UTC'); require __DIR__ . '/../vendor/autoload.php'; test/Assertions.php000064400000002032151677430260010376 0ustar00<?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); } } test/PackFieldTest.php000064400000001656151677430260010741 0ustar00<?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', ); } } test/LocalFileHeaderTest.php000064400000003311151677430260012050 0ustar00<?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 ); } } test/FaultInjectionResource.php000064400000006163151677430260012703 0ustar00<?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); } } LICENSE000064400000002361151677430260005566 0ustar00MIT License Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch> Copyright (C) 2014 Jesse G. Donat <donatj@gmail.com> Copyright (C) 2018 Nicolas CARPi <nicolas.carpi@curie.fr> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. guides/StreamOutput.rst000064400000001470151677430260011247 0ustar00Stream Output =============== Stream to S3 Bucket --------------- .. code-block:: php use Aws\S3\S3Client; use Aws\Credentials\CredentialProvider; use ZipStream\ZipStream; $bucket = 'your bucket name'; $client = new S3Client([ 'region' => 'your region', 'version' => 'latest', 'bucketName' => $bucket, 'credentials' => CredentialProvider::defaultProvider(), ]); $client->registerStreamWrapper(); $zipFile = fopen("s3://$bucket/example.zip", 'w'); $zip = new ZipStream( enableZip64: false, outputStream: $zipFile, ); $zip->addFile( fileName: 'file1.txt', data: 'File1 data', ); $zip->addFile( fileName: 'file2.txt', data: 'File2 data', ); $zip->finish(); fclose($zipFile); guides/Options.rst000064400000004260151677430260010226 0ustar00Available options =============== Here is the full list of options available to you. You can also have a look at ``src/ZipStream.php`` file. .. code-block:: php use ZipStream\ZipStream; require_once 'vendor/autoload.php'; $zip = new ZipStream( // Define output stream // (argument is eiter a resource or implementing // `Psr\Http\Message\StreamInterface`) // // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies // required when using `Psr\Http\Message\StreamInterface`. outputStream: $filePointer, // Set the deflate level (default is 6; use -1 to disable it) defaultDeflateLevel: 6, // Add a comment to the zip file comment: 'This is a comment.', // Send http headers (default is true) sendHttpHeaders: false, // HTTP Content-Disposition. // Defaults to 'attachment', where FILENAME is the specified filename. // Note that this does nothing if you are not sending HTTP headers. contentDisposition: 'attachment', // Output Name for HTTP Content-Disposition // Defaults to no name outputName: "example.zip", // HTTP Content-Type. // Defaults to 'application/x-zip'. // Note that this does nothing if you are not sending HTTP headers. contentType: 'application/x-zip', // Set the function called for setting headers. // Default is the `header()` of PHP httpHeaderCallback: header(...), // Enable streaming files with single read where general purpose bit 3 // indicates local file header contain zero values in crc and size // fields, these appear only after file contents in data descriptor // block. // Set to true if your input stream is remote // (used with addFileFromStream()). // Default is false. defaultEnableZeroHeader: false, // Enable zip64 extension, allowing very large archives // (> 4Gb or file count > 64k) // Default is true enableZip64: true, // Flush output buffer after every write // Default is false flushOutput: true, ); guides/PSR7Streams.rst000064400000001032151677430260010657 0ustar00Usage with PSR 7 Streams =============== PSR-7 streams are `standardized streams <https://www.php-fig.org/psr/psr-7/>`_. ZipStream-PHP supports working with these streams with the function ``addFileFromPsr7Stream``. For all parameters of the function see the API documentation. Example --------------- .. code-block:: php $stream = $response->getBody(); // add a file named 'streamfile.txt' from the content of the stream $zip->addFileFromPsr7Stream( fileName: 'streamfile.txt', stream: $stream, ); guides/index.rst000064400000007670151677430260007712 0ustar00ZipStream PHP ============= A fast and simple streaming zip file downloader for PHP. Using this library will save you from having to write the Zip to disk. You can directly send it to the user, which is much faster. It can work with S3 buckets or any PSR7 Stream. .. toctree:: index Symfony Options StreamOutput FlySystem PSR7Streams Nginx Varnish ContentLength Installation --------------- Simply add a dependency on ``maennchen/zipstream-php`` to your project's ``composer.json`` file if you use Composer to manage the dependencies of your project. Use following command to add the package to your project's dependencies: .. code-block:: sh composer require maennchen/zipstream-php If you want to use``addFileFromPsr7Stream``` (``Psr\Http\Message\StreamInterface``) or use a stream instead of a ``resource`` as ``outputStream``, the following dependencies must be installed as well: .. code-block:: sh composer require psr/http-message guzzlehttp/psr7 If ``composer install`` yields the following error, your installation is missing the `mbstring extension <https://www.php.net/manual/en/book.mbstring.php>`_, either `install it <https://www.php.net/manual/en/mbstring.installation.php>`_ or run the follwoing command: .. code-block:: Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires PHP extension ext-mbstring * but it is missing from your system. Install or enable PHP's mbstrings extension. .. code-block:: sh composer require symfony/polyfill-mbstring Usage Intro --------------- Here's a simple example: .. code-block:: php // Autoload the dependencies require 'vendor/autoload.php'; // create a new zipstream object $zip = new ZipStream\ZipStream( outputName: 'example.zip', // enable output of HTTP headers sendHttpHeaders: true, ); // create a file named 'hello.txt' $zip->addFile( fileName: 'hello.txt', data: 'This is the contents of hello.txt', ); // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' $zip->addFileFromPath( fileName: 'some_image.jpg', path: 'path/to/image.jpg', ); // add a file named 'goodbye.txt' from an open stream resource $filePointer = tmpfile(); fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.'); rewind($filePointer); $zip->addFileFromStream( fileName: 'goodbye.txt', stream: $filePointer, ); fclose($filePointer); // add a file named 'streamfile.txt' from the body of a `guzzle` response // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies required. $zip->addFileFromPsr7Stream( fileName: 'streamfile.txt', stream: $response->getBody(), ); // finish the zip stream $zip->finish(); You can also add comments, modify file timestamps, and customize (or disable) the HTTP headers. It is also possible to specify the storage method when adding files, the current default storage method is ``DEFLATE`` i.e files are stored with Compression mode 0x08. Known Issues --------------- The native Mac OS archive extraction tool prior to macOS 10.15 might not open archives in some conditions. A workaround is to disable the Zip64 feature with the option ``enableZip64: false``. This limits the archive to 4 Gb and 64k files but will allow users on macOS 10.14 and below to open them without issue. See `#116 <https://github.com/maennchen/ZipStream-PHP/issues/146>`_. The linux ``unzip`` utility might not handle properly unicode characters. It is recommended to extract with another tool like `7-zip <https://www.7-zip.org/>`_. See `#146 <https://github.com/maennchen/ZipStream-PHP/issues/146>`_. It is the responsability of the client code to make sure that files are not saved with the same path, as it is not possible for the library to figure it out while streaming a zip. See `#154 <https://github.com/maennchen/ZipStream-PHP/issues/154>`_. guides/Varnish.rst000064400000001207151677430260010203 0ustar00Usage with Varnish ============= Serving a big zip with varnish in between can cause random stream close. This can be solved by adding attached code to the vcl file. To avoid the problem, add the following to your varnish config file: .. code-block:: sub vcl_recv { # Varnish can’t intercept the discussion anymore # helps for streaming big zips if (req.url ~ "\.(tar|gz|zip|7z|exe)$") { return (pipe); } } # Varnish can’t intercept the discussion anymore # helps for streaming big zips sub vcl_pipe { set bereq.http.connection = "close"; return (pipe); } guides/ContentLength.rst000064400000002534151677430260011351 0ustar00Adding Content-Length header ============= Adding a ``Content-Length`` header for ``ZipStream`` can be achieved by using the options ``SIMULATION_STRICT`` or ``SIMULATION_LAX`` in the ``operationMode`` parameter. In the ``SIMULATION_STRICT`` mode, ``ZipStream`` will not allow to calculate the size based on reading the whole file. ``SIMULATION_LAX`` will read the whole file if neccessary. ``SIMULATION_STRICT`` is therefore useful to make sure that the size can be calculated efficiently. .. code-block:: php use ZipStream\OperationMode; use ZipStream\ZipStream; $zip = new ZipStream( operationMode: OperationMode::SIMULATE_STRICT, // or SIMULATE_LAX defaultEnableZeroHeader: false, sendHttpHeaders: true, outputStream: $stream, ); // Normally add files $zip->addFile('sample.txt', 'Sample String Data'); // Use addFileFromCallback and exactSize if you want to defer opening of // the file resource $zip->addFileFromCallback( 'sample.txt', exactSize: 18, callback: function () { return fopen('...'); } ); // Read resulting file size $size = $zip->finish(); // Tell it to the browser header('Content-Length: '. $size); // Execute the Simulation and stream the actual zip to the client $zip->executeSimulation(); guides/Symfony.rst000064400000011067151677430260010242 0ustar00Usage with Symfony =============== Overview for using ZipStream in Symfony -------- Using ZipStream in Symfony requires use of Symfony's ``StreamedResponse`` when used in controller actions. Wrap your call to the relevant ``ZipStream`` stream method (i.e. ``addFile``, ``addFileFromPath``, ``addFileFromStream``) in Symfony's ``StreamedResponse`` function passing in any required arguments for your use case. Using Symfony's ``StreamedResponse`` will allow Symfony to stream output from ZipStream correctly to users' browsers and avoid a corrupted final zip landing on the users' end. Example for using ``ZipStream`` in a controller action to zip stream files stored in an AWS S3 bucket by key: .. code-block:: php use Symfony\Component\HttpFoundation\StreamedResponse; use Aws\S3\S3Client; use ZipStream; //... /** * @Route("/zipstream", name="zipstream") */ public function zipStreamAction() { // sample test file on s3 $s3keys = array( "ziptestfolder/file1.txt" ); $s3Client = $this->get('app.amazon.s3'); //s3client service $s3Client->registerStreamWrapper(); //required // using StreamedResponse to wrap ZipStream functionality // for files on AWS s3. $response = new StreamedResponse(function() use($s3keys, $s3Client) { // Define suitable options for ZipStream Archive. // this is needed to prevent issues with truncated zip files //initialise zipstream with output zip filename and options. $zip = new ZipStream\ZipStream( outputName: 'test.zip', defaultEnableZeroHeader: true, contentType: 'application/octet-stream', ); //loop keys - useful for multiple files foreach ($s3keys as $key) { // Get the file name in S3 key so we can save it to the zip //file using the same name. $fileName = basename($key); // concatenate s3path. // replace with your bucket name or get from parameters file. $bucket = 'bucketname'; $s3path = "s3://" . $bucket . "/" . $key; //addFileFromStream if ($streamRead = fopen($s3path, 'r')) { $zip->addFileFromStream( fileName: $fileName, stream: $streamRead, ); } else { die('Could not open stream for reading'); } } $zip->finish(); }); return $response; } In the above example, files on AWS S3 are being streamed from S3 to the Symfon application via ``fopen`` call when the s3Client has ``registerStreamWrapper`` applied. This stream is then passed to ``ZipStream`` via the ``addFileFromStream`` function, which ZipStream then streams as a zip to the client browser via Symfony's ``StreamedResponse``. No Zip is created server side, which makes this approach a more efficient solution for streaming zips to the client browser especially for larger files. For the above use case you will need to have installed `aws/aws-sdk-php-symfony <https://github.com/aws/aws-sdk-php-symfony>`_ to support accessing S3 objects in your Symfony web application. This is not required for locally stored files on you server you intend to stream via ``ZipStream``. See official Symfony documentation for details on `Symfony's StreamedResponse <https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response>`_ ``Symfony\Component\HttpFoundation\StreamedResponse``. Note from `S3 documentation <https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-stream-wrapper.html>`_: Streams opened in "r" mode only allow data to be read from the stream, and are not seekable by default. This is so that data can be downloaded from Amazon S3 in a truly streaming manner, where previously read bytes do not need to be buffered into memory. If you need a stream to be seekable, you can pass seekable into the stream context options of a function. Make sure to configure your S3 context correctly! Uploading a file -------- You need to add correct permissions (see `#120 <https://github.com/maennchen/ZipStream-PHP/issues/120>`_) **example code** .. code-block:: php $path = "s3://{$adapter->getBucket()}/{$this->getArchivePath()}"; // the important bit $outputContext = stream_context_create([ 's3' => ['ACL' => 'public-read'], ]); fopen($path, 'w', null, $outputContext); guides/FlySystem.rst000064400000002070151677430260010527 0ustar00Usage with FlySystem =============== For saving or uploading the generated zip, you can use the `Flysystem <https://flysystem.thephpleague.com>`_ package, and its many adapters. For that you will need to provide another stream than the ``php://output`` default one, and pass it to Flysystem ``putStream`` method. .. code-block:: php // Open Stream only once for read and write since it's a memory stream and // the content is lost when closing the stream / opening another one $tempStream = fopen('php://memory', 'w+'); // Create Zip Archive $zipStream = new ZipStream( outputStream: $tempStream, outputName: 'test.zip', ); $zipStream->addFile('test.txt', 'text'); $zipStream->finish(); // Store File // (see Flysystem documentation, and all its framework integration) // Can be any adapter (AWS, Google, Ftp, etc.) $adapter = new Local(__DIR__.'/path/to/folder'); $filesystem = new Filesystem($adapter); $filesystem->writeStream('test.zip', $tempStream) // Close Stream fclose($tempStream); guides/Nginx.rst000064400000001057151677430260007657 0ustar00Usage with nginx ============= If you are using nginx as a webserver, it will try to buffer the response. So you'll want to disable this with a custom header: .. code-block:: php header('X-Accel-Buffering: no'); # or with the Response class from Symfony $response->headers->set('X-Accel-Buffering', 'no'); Alternatively, you can tweak the `fastcgi cache parameters <https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_buffers>`_ within nginx config. See `original issue <https://github.com/maennchen/ZipStream-PHP/issues/77>`_..tool-versions000064400000000012151677430260007374 0ustar00php 8.2.5 README.md000064400000017753151677430260006053 0ustar00# ZipStream-PHP [](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml) [](https://coveralls.io/github/maennchen/ZipStream-PHP?branch=main) [](https://packagist.org/packages/maennchen/zipstream-php) [](https://packagist.org/packages/maennchen/zipstream-php) [](https://opencollective.com/zipstream) [](LICENSE) ## Unstable Branch The `main` branch is not stable. Please see the [releases](https://github.com/maennchen/ZipStream-PHP/releases) for a stable version. ## Overview A fast and simple streaming zip file downloader for PHP. Using this library will save you from having to write the Zip to disk. You can directly send it to the user, which is much faster. It can work with S3 buckets or any PSR7 Stream. Please see the [LICENSE](LICENSE) file for licensing and warranty information. ## Installation Simply add a dependency on maennchen/zipstream-php to your project's `composer.json` file if you use Composer to manage the dependencies of your project. Use following command to add the package to your project's dependencies: ```bash composer require maennchen/zipstream-php ``` ## Usage For detailed instructions, please check the [Documentation](https://maennchen.github.io/ZipStream-PHP/). ```php // Autoload the dependencies require 'vendor/autoload.php'; // create a new zipstream object $zip = new ZipStream\ZipStream( outputName: 'example.zip', // enable output of HTTP headers sendHttpHeaders: true, ); // create a file named 'hello.txt' $zip->addFile( fileName: 'hello.txt', data: 'This is the contents of hello.txt', ); // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' $zip->addFileFromPath( fileName: 'some_image.jpg', path: 'path/to/image.jpg', ); // finish the zip stream $zip->finish(); ``` ## Upgrade to version 3.0.0 ### General - Minimum PHP Version: `8.1` - Only 64bit Architecture is supported. - The class `ZipStream\Option\Method` has been replaced with the enum `ZipStream\CompressionMethod`. - Most clases have been flagged as `@internal` and should not be used from the outside. If you're using internal resources to extend this library, please open an issue so that a clean interface can be added & published. The externally available classes & enums are: - `ZipStream\CompressionMethod` - `ZipStream\Exception*` - `ZipStream\ZipStream` ### Archive Options - The class `ZipStream\Option\Archive` has been replaced in favor of named arguments in the `ZipStream\ZipStream` constuctor. - The archive options `largeFileSize` & `largeFileMethod` has been removed. If you want different `compressionMethods` based on the file size, you'll have to implement this yourself. - The archive option `httpHeaderCallback` changed the type from `callable` to `Closure`. - The archive option `zeroHeader` has been replaced with the option `defaultEnableZeroHeader` and can be overridden for every file. Its default value changed from `false` to `true`. - The archive option `statFiles` was removed since the library no longer checks filesizes this way. - The archive option `deflateLevel` has been replaced with the option `defaultDeflateLevel` and can be overridden for every file. - The first argument (`name`) of the `ZipStream\ZipStream` constuctor has been replaced with the named argument `outputName`. - Headers are now also sent if the `outputName` is empty. If you do not want to automatically send http headers, set `sendHttpHeaders` to `false`. ### File Options - The class `ZipStream\Option\File` has been replaced in favor of named arguments in the `ZipStream\ZipStream->addFile*` functions. - The file option `method` has been renamed to `compressionMethod`. - The file option `time` has been renamed to `lastModificationDateTime`. - The file option `size` has been renamed to `maxSize`. ## Upgrade to version 2.0.0 https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-200 ## Upgrade to version 1.0.0 https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-100 ## Contributing ZipStream-PHP is a collaborative project. Please take a look at the [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) file. ## Version Support Versions are supported according to the table below. Please do not open any pull requests contradicting the current version support status. Careful: Always check the `README` on `main` for up-to-date information. | Version | New Features | Bugfixes | Security | |---------|--------------|----------|----------| | *3* | ✓ | ✓ | ✓ | | *2* | ✗ | ✓ | ✓ | | *1* | ✗ | ✗ | ✓ | | *0* | ✗ | ✗ | ✗ | This library aligns itself with the PHP core support. New features and bugfixes will only target PHP versions according to their current status. See: https://www.php.net/supported-versions.php ## About the Authors - Paul Duncan <pabs@pablotron.org> - https://pablotron.org/ - Jonatan Männchen <jonatan@maennchen.ch> - https://maennchen.dev - Jesse G. Donat <donatj@gmail.com> - https://donatstudios.com - Nicolas CARPi <nico-git@deltablot.email> - https://www.deltablot.com - Nik Barham <nik@brokencube.co.uk> - https://www.brokencube.co.uk ## Contributors ### Code Contributors This project exists thanks to all the people who contribute. [[Contribute](.github/CONTRIBUTING.md)]. <a href="https://github.com/maennchen/ZipStream-PHP/graphs/contributors"><img src="https://opencollective.com/zipstream/contributors.svg?width=890&button=false" /></a> ### Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/zipstream/contribute)] #### Individuals <a href="https://opencollective.com/zipstream"><img src="https://opencollective.com/zipstream/individuals.svg?width=890"></a> #### Organizations Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/zipstream/contribute)] <a href="https://opencollective.com/zipstream/organization/0/website"><img src="https://opencollective.com/zipstream/organization/0/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/1/website"><img src="https://opencollective.com/zipstream/organization/1/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/2/website"><img src="https://opencollective.com/zipstream/organization/2/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/3/website"><img src="https://opencollective.com/zipstream/organization/3/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/4/website"><img src="https://opencollective.com/zipstream/organization/4/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/5/website"><img src="https://opencollective.com/zipstream/organization/5/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/6/website"><img src="https://opencollective.com/zipstream/organization/6/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/7/website"><img src="https://opencollective.com/zipstream/organization/7/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/8/website"><img src="https://opencollective.com/zipstream/organization/8/avatar.svg"></a> <a href="https://opencollective.com/zipstream/organization/9/website"><img src="https://opencollective.com/zipstream/organization/9/avatar.svg"></a> .php-cs-fixer.dist.php000064400000004267151677430260010626 0ustar00<?php declare(strict_types=1); /** * PHP-CS-Fixer config for ZipStream-PHP * @author Nicolas CARPi <nico-git@deltablot.email> * @copyright 2022 Nicolas CARPi * @see https://github.com/maennchen/ZipStream-PHP * @license MIT * @package maennchen/ZipStream-PHP */ use PhpCsFixer\Config; use PhpCsFixer\Finder; $finder = Finder::create() ->exclude('.github') ->exclude('.phpdoc') ->exclude('docs') ->exclude('tools') ->exclude('vendor') ->in(__DIR__); $config = new Config(); return $config->setRules([ '@PER' => true, '@PER:risky' => true, '@PHP82Migration' => true, '@PHPUnit84Migration:risky' => true, 'array_syntax' => ['syntax' => 'short'], 'class_attributes_separation' => true, 'declare_strict_types' => true, 'dir_constant' => true, 'is_null' => true, 'no_homoglyph_names' => true, 'no_null_property_initialization' => true, 'no_php4_constructor' => true, 'no_unused_imports' => true, 'no_useless_else' => true, 'non_printable_character' => true, 'ordered_imports' => true, 'ordered_class_elements' => true, 'php_unit_construct' => true, 'pow_to_exponentiation' => true, 'psr_autoloading' => true, 'random_api_migration' => true, 'return_assignment' => true, 'self_accessor' => true, 'semicolon_after_instruction' => true, 'short_scalar_cast' => true, 'simplified_null_return' => true, 'single_blank_line_before_namespace' => true, 'single_class_element_per_statement' => true, 'single_line_comment_style' => true, 'single_quote' => true, 'space_after_semicolon' => true, 'standardize_not_equals' => true, 'strict_param' => true, 'ternary_operator_spaces' => true, 'trailing_comma_in_multiline' => true, 'trim_array_spaces' => true, 'unary_operator_spaces' => true, 'global_namespace_import' => [ 'import_classes' => true, 'import_functions' => true, 'import_constants' => true, ], ]) ->setFinder($finder) ->setRiskyAllowed(true); psalm.xml000064400000001433151677430260006416 0ustar00<?xml version="1.0"?> <psalm errorLevel="1" resolveFromConfigFile="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" findUnusedBaselineEntry="true" findUnusedCode="true" > <projectFiles> <directory name="src" /> <ignoreFiles> <directory name="vendor" /> </ignoreFiles> </projectFiles> <issueHandlers> <!-- Turn off dead code warnings for externally called functions --> <PossiblyUnusedProperty errorLevel="suppress" /> <PossiblyUnusedMethod errorLevel="suppress" /> <PossiblyUnusedReturnValue errorLevel="suppress" /> </issueHandlers> </psalm> .phpdoc/template/base.html.twig000064400000001262151677430260012477 0ustar00{% 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"}, ] } %}
/home/emeraadmin/.htpasswds/../public_html/ADVIA/../4d695/zipstream-php.tar