JFIFxxC      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbrzipstream-php/composer.json000064400000004452150364333200012075 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" ] } } zipstream-php/src/Version.php000064400000000262150364333200012273 0ustar00addFile(fileName: 'world.txt', data: 'Hello World'); * * // add second file * $zip->addFile(fileName: 'moon.txt', data: 'Hello Moon'); * ``` * * 3. Finish the zip stream: * * ```php * $zip->finish(); * ``` * * You can also add an archive comment, add comments to individual files, * and adjust the timestamp of files. See the API documentation for each * method below for additional information. * * ## Example * * ```php * // create a new zip stream object * $zip = new ZipStream(outputName: 'some_files.zip'); * * // list of local files * $files = array('foo.txt', 'bar.jpg'); * * // read and add each file to the archive * foreach ($files as $path) * $zip->addFileFormPath(fileName: $path, $path); * * // write archive footer to stream * $zip->finish(); * ``` */ class ZipStream { /** * This number corresponds to the ZIP version/OS used (2 bytes) * From: https://www.iana.org/assignments/media-types/application/zip * The upper byte (leftmost one) indicates the host system (OS) for the * file. Software can use this information to determine * the line record format for text files etc. The current * mappings are: * * 0 - MS-DOS and OS/2 (F.A.T. file systems) * 1 - Amiga 2 - VAX/VMS * 3 - *nix 4 - VM/CMS * 5 - Atari ST 6 - OS/2 H.P.F.S. * 7 - Macintosh 8 - Z-System * 9 - CP/M 10 thru 255 - unused * * The lower byte (rightmost one) indicates the version number of the * software used to encode the file. The value/10 * indicates the major version number, and the value * mod 10 is the minor version number. * Here we are using 6 for the OS, indicating OS/2 H.P.F.S. * to prevent file permissions issues upon extract (see #84) * 0x603 is 00000110 00000011 in binary, so 6 and 3 * * @internal */ public const ZIP_VERSION_MADE_BY = 0x603; private bool $ready = true; private int $offset = 0; /** * @var string[] */ private array $centralDirectoryRecords = []; /** * @var resource */ private $outputStream; private readonly Closure $httpHeaderCallback; /** * @var File[] */ private array $recordedSimulation = []; /** * Create a new ZipStream object. * * ##### Examples * * ```php * // create a new zip file named 'foo.zip' * $zip = new ZipStream(outputName: 'foo.zip'); * * // create a new zip file named 'bar.zip' with a comment * $zip = new ZipStream( * outputName: 'bar.zip', * comment: 'this is a comment for the zip file.', * ); * ``` * * @param OperationMode $operationMode * The mode can be used to switch between `NORMAL` and `SIMULATION_*` modes. * For details see the `OperationMode` documentation. * * Default to `NORMAL`. * * @param string $comment * Archive Level Comment * * @param StreamInterface|resource|null $outputStream * Override the output of the archive to a different target. * * By default the archive is sent to `STDOUT`. * * @param CompressionMethod $defaultCompressionMethod * How to handle file compression. Legal values are * `CompressionMethod::DEFLATE` (the default), or * `CompressionMethod::STORE`. `STORE` sends the file raw and is * significantly faster, while `DEFLATE` compresses the file and * is much, much slower. * * @param int $defaultDeflateLevel * Default deflation level. Only relevant if `compressionMethod` * is `DEFLATE`. * * See details of [`deflate_init`](https://www.php.net/manual/en/function.deflate-init.php#refsect1-function.deflate-init-parameters) * * @param bool $enableZip64 * Enable Zip64 extension, supporting very large * archives (any size > 4 GB or file count > 64k) * * @param bool $defaultEnableZeroHeader * Enable streaming files with single read. * * When the zero header is set, the file is streamed into the output * and the size & checksum are added at the end of the file. This is the * fastest method and uses the least memory. Unfortunately not all * ZIP clients fully support this and can lead to clients reporting * the generated ZIP files as corrupted in combination with other * circumstances. (Zip64 enabled, using UTF8 in comments / names etc.) * * When the zero header is not set, the length & checksum need to be * defined before the file is actually added. To prevent loading all * the data into memory, the data has to be read twice. If the data * which is added is not seekable, this call will fail. * * @param bool $sendHttpHeaders * Boolean indicating whether or not to send * the HTTP headers for this file. * * @param ?Closure $httpHeaderCallback * The method called to send HTTP headers * * @param string|null $outputName * The name of the created archive. * * Only relevant if `$sendHttpHeaders = true`. * * @param string $contentDisposition * HTTP Content-Disposition * * Only relevant if `sendHttpHeaders = true`. * * @param string $contentType * HTTP Content Type * * Only relevant if `sendHttpHeaders = true`. * * @param bool $flushOutput * Enable flush after every write to output stream. * * @return self */ public function __construct( private OperationMode $operationMode = OperationMode::NORMAL, private readonly string $comment = '', $outputStream = null, private readonly CompressionMethod $defaultCompressionMethod = CompressionMethod::DEFLATE, private readonly int $defaultDeflateLevel = 6, private readonly bool $enableZip64 = true, private readonly bool $defaultEnableZeroHeader = true, private bool $sendHttpHeaders = true, ?Closure $httpHeaderCallback = null, private readonly ?string $outputName = null, private readonly string $contentDisposition = 'attachment', private readonly string $contentType = 'application/x-zip', private bool $flushOutput = false, ) { $this->outputStream = self::normalizeStream($outputStream); $this->httpHeaderCallback = $httpHeaderCallback ?? header(...); } /** * Add a file to the archive. * * ##### File Options * * See {@see addFileFromPsr7Stream()} * * ##### Examples * * ```php * // add a file named 'world.txt' * $zip->addFile(fileName: 'world.txt', data: 'Hello World!'); * * // add a file named 'bar.jpg' with a comment and a last-modified * // time of two hours ago * $zip->addFile( * fileName: 'bar.jpg', * data: $data, * comment: 'this is a comment about bar.jpg', * lastModificationDateTime: new DateTime('2 hours ago'), * ); * ``` * * @param string $data * * contents of file */ public function addFile( string $fileName, string $data, string $comment = '', ?CompressionMethod $compressionMethod = null, ?int $deflateLevel = null, ?DateTimeInterface $lastModificationDateTime = null, ?int $maxSize = null, ?int $exactSize = null, ?bool $enableZeroHeader = null, ): void { $this->addFileFromCallback( fileName: $fileName, callback: fn () => $data, comment: $comment, compressionMethod: $compressionMethod, deflateLevel: $deflateLevel, lastModificationDateTime: $lastModificationDateTime, maxSize: $maxSize, exactSize: $exactSize, enableZeroHeader: $enableZeroHeader, ); } /** * Add a file at path to the archive. * * ##### File Options * * See {@see addFileFromPsr7Stream()} * * ###### Examples * * ```php * // add a file named 'foo.txt' from the local file '/tmp/foo.txt' * $zip->addFileFromPath( * fileName: 'foo.txt', * path: '/tmp/foo.txt', * ); * * // add a file named 'bigfile.rar' from the local file * // '/usr/share/bigfile.rar' with a comment and a last-modified * // time of two hours ago * $zip->addFile( * fileName: 'bigfile.rar', * path: '/usr/share/bigfile.rar', * comment: 'this is a comment about bigfile.rar', * lastModificationDateTime: new DateTime('2 hours ago'), * ); * ``` * * @throws \ZipStream\Exception\FileNotFoundException * @throws \ZipStream\Exception\FileNotReadableException */ public function addFileFromPath( /** * name of file in archive (including directory path). */ string $fileName, /** * path to file on disk (note: paths should be encoded using * UNIX-style forward slashes -- e.g '/path/to/some/file'). */ string $path, string $comment = '', ?CompressionMethod $compressionMethod = null, ?int $deflateLevel = null, ?DateTimeInterface $lastModificationDateTime = null, ?int $maxSize = null, ?int $exactSize = null, ?bool $enableZeroHeader = null, ): void { if (!is_readable($path)) { if (!file_exists($path)) { throw new FileNotFoundException($path); } throw new FileNotReadableException($path); } if ($fileTime = filemtime($path)) { $lastModificationDateTime ??= (new DateTimeImmutable())->setTimestamp($fileTime); } $this->addFileFromCallback( fileName: $fileName, callback: function () use ($path) { $stream = fopen($path, 'rb'); if (!$stream) { // @codeCoverageIgnoreStart throw new ResourceActionException('fopen'); // @codeCoverageIgnoreEnd } return $stream; }, comment: $comment, compressionMethod: $compressionMethod, deflateLevel: $deflateLevel, lastModificationDateTime: $lastModificationDateTime, maxSize: $maxSize, exactSize: $exactSize, enableZeroHeader: $enableZeroHeader, ); } /** * Add an open stream (resource) to the archive. * * ##### File Options * * See {@see addFileFromPsr7Stream()} * * ##### Examples * * ```php * // create a temporary file stream and write text to it * $filePointer = tmpfile(); * fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.'); * * // add a file named 'streamfile.txt' from the content of the stream * $archive->addFileFromStream( * fileName: 'streamfile.txt', * stream: $filePointer, * ); * ``` * * @param resource $stream contents of file as a stream resource */ public function addFileFromStream( string $fileName, $stream, string $comment = '', ?CompressionMethod $compressionMethod = null, ?int $deflateLevel = null, ?DateTimeInterface $lastModificationDateTime = null, ?int $maxSize = null, ?int $exactSize = null, ?bool $enableZeroHeader = null, ): void { $this->addFileFromCallback( fileName: $fileName, callback: fn () => $stream, comment: $comment, compressionMethod: $compressionMethod, deflateLevel: $deflateLevel, lastModificationDateTime: $lastModificationDateTime, maxSize: $maxSize, exactSize: $exactSize, enableZeroHeader: $enableZeroHeader, ); } /** * Add an open stream to the archive. * * ##### Examples * * ```php * $stream = $response->getBody(); * // add a file named 'streamfile.txt' from the content of the stream * $archive->addFileFromPsr7Stream( * fileName: 'streamfile.txt', * stream: $stream, * ); * ``` * * @param string $fileName * path of file in archive (including directory) * * @param StreamInterface $stream * contents of file as a stream resource * * @param string $comment * ZIP comment for this file * * @param ?CompressionMethod $compressionMethod * Override `defaultCompressionMethod` * * See {@see __construct()} * * @param ?int $deflateLevel * Override `defaultDeflateLevel` * * See {@see __construct()} * * @param ?DateTimeInterface $lastModificationDateTime * Set last modification time of file. * * Default: `now` * * @param ?int $maxSize * Only read `maxSize` bytes from file. * * The file is considered done when either reaching `EOF` * or the `maxSize`. * * @param ?int $exactSize * Read exactly `exactSize` bytes from file. * If `EOF` is reached before reading `exactSize` bytes, an error will be * thrown. The parameter allows for faster size calculations if the `stream` * does not support `fstat` size or is slow and otherwise known beforehand. * * @param ?bool $enableZeroHeader * Override `defaultEnableZeroHeader` * * See {@see __construct()} */ public function addFileFromPsr7Stream( string $fileName, StreamInterface $stream, string $comment = '', ?CompressionMethod $compressionMethod = null, ?int $deflateLevel = null, ?DateTimeInterface $lastModificationDateTime = null, ?int $maxSize = null, ?int $exactSize = null, ?bool $enableZeroHeader = null, ): void { $this->addFileFromCallback( fileName: $fileName, callback: fn () => $stream, comment: $comment, compressionMethod: $compressionMethod, deflateLevel: $deflateLevel, lastModificationDateTime: $lastModificationDateTime, maxSize: $maxSize, exactSize: $exactSize, enableZeroHeader: $enableZeroHeader, ); } /** * Add a file based on a callback. * * This is useful when you want to simulate a lot of files without keeping * all of the file handles open at the same time. * * ##### Examples * * ```php * foreach($files as $name => $size) { * $archive->addFileFromPsr7Stream( * fileName: 'streamfile.txt', * exactSize: $size, * callback: function() use($name): Psr\Http\Message\StreamInterface { * $response = download($name); * return $response->getBody(); * } * ); * } * ``` * * @param string $fileName * path of file in archive (including directory) * * @param Closure $callback * @psalm-param Closure(): (resource|StreamInterface|string) $callback * A callback to get the file contents in the shape of a PHP stream, * a Psr StreamInterface implementation, or a string. * * @param string $comment * ZIP comment for this file * * @param ?CompressionMethod $compressionMethod * Override `defaultCompressionMethod` * * See {@see __construct()} * * @param ?int $deflateLevel * Override `defaultDeflateLevel` * * See {@see __construct()} * * @param ?DateTimeInterface $lastModificationDateTime * Set last modification time of file. * * Default: `now` * * @param ?int $maxSize * Only read `maxSize` bytes from file. * * The file is considered done when either reaching `EOF` * or the `maxSize`. * * @param ?int $exactSize * Read exactly `exactSize` bytes from file. * If `EOF` is reached before reading `exactSize` bytes, an error will be * thrown. The parameter allows for faster size calculations if the `stream` * does not support `fstat` size or is slow and otherwise known beforehand. * * @param ?bool $enableZeroHeader * Override `defaultEnableZeroHeader` * * See {@see __construct()} */ public function addFileFromCallback( string $fileName, Closure $callback, string $comment = '', ?CompressionMethod $compressionMethod = null, ?int $deflateLevel = null, ?DateTimeInterface $lastModificationDateTime = null, ?int $maxSize = null, ?int $exactSize = null, ?bool $enableZeroHeader = null, ): void { $file = new File( dataCallback: function () use ($callback, $maxSize) { $data = $callback(); if(is_resource($data)) { return $data; } if($data instanceof StreamInterface) { return StreamWrapper::getResource($data); } $stream = fopen('php://memory', 'rw+'); if ($stream === false) { // @codeCoverageIgnoreStart throw new ResourceActionException('fopen'); // @codeCoverageIgnoreEnd } if ($maxSize !== null && fwrite($stream, $data, $maxSize) === false) { // @codeCoverageIgnoreStart throw new ResourceActionException('fwrite', $stream); // @codeCoverageIgnoreEnd } elseif (fwrite($stream, $data) === false) { // @codeCoverageIgnoreStart throw new ResourceActionException('fwrite', $stream); // @codeCoverageIgnoreEnd } if (rewind($stream) === false) { // @codeCoverageIgnoreStart throw new ResourceActionException('rewind', $stream); // @codeCoverageIgnoreEnd } return $stream; }, send: $this->send(...), recordSentBytes: $this->recordSentBytes(...), operationMode: $this->operationMode, fileName: $fileName, startOffset: $this->offset, compressionMethod: $compressionMethod ?? $this->defaultCompressionMethod, comment: $comment, deflateLevel: $deflateLevel ?? $this->defaultDeflateLevel, lastModificationDateTime: $lastModificationDateTime ?? new DateTimeImmutable(), maxSize: $maxSize, exactSize: $exactSize, enableZip64: $this->enableZip64, enableZeroHeader: $enableZeroHeader ?? $this->defaultEnableZeroHeader, ); if($this->operationMode !== OperationMode::NORMAL) { $this->recordedSimulation[] = $file; } $this->centralDirectoryRecords[] = $file->process(); } /** * Add a directory to the archive. * * ##### File Options * * See {@see addFileFromPsr7Stream()} * * ##### Examples * * ```php * // add a directory named 'world/' * $zip->addFile(fileName: 'world/'); * ``` */ public function addDirectory( string $fileName, string $comment = '', ?DateTimeInterface $lastModificationDateTime = null, ): void { if (!str_ends_with($fileName, '/')) { $fileName .= '/'; } $this->addFile( fileName: $fileName, data: '', comment: $comment, compressionMethod: CompressionMethod::STORE, deflateLevel: null, lastModificationDateTime: $lastModificationDateTime, maxSize: 0, exactSize: 0, enableZeroHeader: false, ); } /** * Executes a previously calculated simulation. * * ##### Example * * ```php * $zip = new ZipStream( * outputName: 'foo.zip', * operationMode: OperationMode::SIMULATE_STRICT, * ); * * $zip->addFile('test.txt', 'Hello World'); * * $size = $zip->finish(); * * header('Content-Length: '. $size); * * $zip->executeSimulation(); * ``` */ public function executeSimulation(): void { if($this->operationMode !== OperationMode::NORMAL) { throw new RuntimeException('Zip simulation is not finished.'); } foreach($this->recordedSimulation as $file) { $this->centralDirectoryRecords[] = $file->cloneSimulationExecution()->process(); } $this->finish(); } /** * Write zip footer to stream. * * The clase is left in an unusable state after `finish`. * * ##### Example * * ```php * // write footer to stream * $zip->finish(); * ``` */ public function finish(): int { $centralDirectoryStartOffsetOnDisk = $this->offset; $sizeOfCentralDirectory = 0; // add trailing cdr file records foreach ($this->centralDirectoryRecords as $centralDirectoryRecord) { $this->send($centralDirectoryRecord); $sizeOfCentralDirectory += strlen($centralDirectoryRecord); } // Add 64bit headers (if applicable) if (count($this->centralDirectoryRecords) >= 0xFFFF || $centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF || $sizeOfCentralDirectory > 0xFFFFFFFF) { if (!$this->enableZip64) { throw new OverflowException(); } $this->send(Zip64\EndOfCentralDirectory::generate( versionMadeBy: self::ZIP_VERSION_MADE_BY, versionNeededToExtract: Version::ZIP64->value, numberOfThisDisk: 0, numberOfTheDiskWithCentralDirectoryStart: 0, numberOfCentralDirectoryEntriesOnThisDisk: count($this->centralDirectoryRecords), numberOfCentralDirectoryEntries: count($this->centralDirectoryRecords), sizeOfCentralDirectory: $sizeOfCentralDirectory, centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk, extensibleDataSector: '', )); $this->send(Zip64\EndOfCentralDirectoryLocator::generate( numberOfTheDiskWithZip64CentralDirectoryStart: 0x00, zip64centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk + $sizeOfCentralDirectory, totalNumberOfDisks: 1, )); } // add trailing cdr eof record $numberOfCentralDirectoryEntries = min(count($this->centralDirectoryRecords), 0xFFFF); $this->send(EndOfCentralDirectory::generate( numberOfThisDisk: 0x00, numberOfTheDiskWithCentralDirectoryStart: 0x00, numberOfCentralDirectoryEntriesOnThisDisk: $numberOfCentralDirectoryEntries, numberOfCentralDirectoryEntries: $numberOfCentralDirectoryEntries, sizeOfCentralDirectory: min($sizeOfCentralDirectory, 0xFFFFFFFF), centralDirectoryStartOffsetOnDisk: min($centralDirectoryStartOffsetOnDisk, 0xFFFFFFFF), zipFileComment: $this->comment, )); $size = $this->offset; // The End $this->clear(); return $size; } /** * @param StreamInterface|resource|null $outputStream * @return resource */ private static function normalizeStream($outputStream) { if ($outputStream instanceof StreamInterface) { return StreamWrapper::getResource($outputStream); } if (is_resource($outputStream)) { return $outputStream; } return fopen('php://output', 'wb'); } /** * Record sent bytes */ private function recordSentBytes(int $sentBytes): void { $this->offset += $sentBytes; } /** * Send string, sending HTTP headers if necessary. * Flush output after write if configure option is set. */ private function send(string $data): void { if (!$this->ready) { throw new RuntimeException('Archive is already finished'); } if ($this->operationMode === OperationMode::NORMAL && $this->sendHttpHeaders) { $this->sendHttpHeaders(); $this->sendHttpHeaders = false; } $this->recordSentBytes(strlen($data)); if ($this->operationMode === OperationMode::NORMAL) { if (fwrite($this->outputStream, $data) === false) { throw new ResourceActionException('fwrite', $this->outputStream); } if ($this->flushOutput) { // flush output buffer if it is on and flushable $status = ob_get_status(); if (isset($status['flags']) && is_int($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) { ob_flush(); } // Flush system buffers after flushing userspace output buffer flush(); } } } /** * Send HTTP headers for this stream. */ private function sendHttpHeaders(): void { // grab content disposition $disposition = $this->contentDisposition; if ($this->outputName) { // Various different browsers dislike various characters here. Strip them all for safety. $safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName)); // Check if we need to UTF-8 encode the filename $urlencoded = rawurlencode($safeOutput); $disposition .= "; filename*=UTF-8''{$urlencoded}"; } $headers = [ 'Content-Type' => $this->contentType, 'Content-Disposition' => $disposition, 'Pragma' => 'public', 'Cache-Control' => 'public, must-revalidate', 'Content-Transfer-Encoding' => 'binary', ]; foreach ($headers as $key => $val) { ($this->httpHeaderCallback)("$key: $val"); } } /** * Clear all internal variables. Note that the stream object is not * usable after this. */ private function clear(): void { $this->centralDirectoryRecords = []; $this->offset = 0; if($this->operationMode === OperationMode::NORMAL) { $this->ready = false; $this->recordedSimulation = []; } else { $this->operationMode = OperationMode::NORMAL; } } } zipstream-php/src/CompressionMethod.php000064400000004132150364333200014310 0ustar00format; }, ''); $args = array_map(function (self $field) { switch($field->format) { case 'V': if ($field->value > self::MAX_V) { throw new RuntimeException(print_r($field->value, true) . ' is larger than 32 bits'); } break; case 'v': if ($field->value > self::MAX_v) { throw new RuntimeException(print_r($field->value, true) . ' is larger than 16 bits'); } break; case 'P': break; default: break; } return $field->value; }, $fields); return pack($fmt, ...$args); } } zipstream-php/src/LocalFileHeader.php000064400000002477150364333200013623 0ustar00value), new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)), new PackField(format: 'V', value: $crc32UncompressedData), new PackField(format: 'V', value: $compressedSize), new PackField(format: 'V', value: $uncompressedSize), new PackField(format: 'v', value: strlen($fileName)), new PackField(format: 'v', value: strlen($extraField)), ) . $fileName . $extraField; } } zipstream-php/src/Zip64/EndOfCentralDirectoryLocator.php000064400000001451150364333200017300 0ustar00format(DateTimeInterface::ATOM) . " can't be represented as DOS time / date."); } } zipstream-php/src/Exception/ResourceActionException.php000064400000001063150364333200017410 0ustar00resource = $resource; parent::__construct('Function ' . $function . 'failed on resource.'); } } zipstream-php/src/Exception/SimulationFileUnknownException.php000064400000000742150364333200020772 0ustar00fileName = self::filterFilename($fileName); $this->checkEncoding(); if ($this->enableZeroHeader) { $this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER; } $this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE; } public function cloneSimulationExecution(): self { return new self( $this->fileName, $this->dataCallback, OperationMode::NORMAL, $this->startOffset, $this->compressionMethod, $this->comment, $this->lastModificationDateTime, $this->deflateLevel, $this->maxSize, $this->exactSize, $this->enableZip64, $this->enableZeroHeader, $this->send, $this->recordSentBytes, ); } public function process(): string { $forecastSize = $this->forecastSize(); if ($this->enableZeroHeader) { // No calculation required } elseif ($this->isSimulation() && $forecastSize) { $this->uncompressedSize = $forecastSize; $this->compressedSize = $forecastSize; } else { $this->readStream(send: false); if (rewind($this->unpackStream()) === false) { throw new ResourceActionException('rewind', $this->unpackStream()); } } $this->addFileHeader(); $detectedSize = $forecastSize ?? $this->compressedSize; if ( $this->isSimulation() && $detectedSize > 0 ) { ($this->recordSentBytes)($detectedSize); } else { $this->readStream(send: true); } $this->addFileFooter(); return $this->getCdrFile(); } /** * @return resource */ private function unpackStream() { if ($this->stream) { return $this->stream; } if ($this->operationMode === OperationMode::SIMULATE_STRICT) { throw new SimulationFileUnknownException(); } $this->stream = ($this->dataCallback)(); if (!$this->enableZeroHeader && !stream_get_meta_data($this->stream)['seekable']) { throw new StreamNotSeekableException(); } if (!( str_contains(stream_get_meta_data($this->stream)['mode'], 'r') || str_contains(stream_get_meta_data($this->stream)['mode'], 'w+') || str_contains(stream_get_meta_data($this->stream)['mode'], 'a+') || str_contains(stream_get_meta_data($this->stream)['mode'], 'x+') || str_contains(stream_get_meta_data($this->stream)['mode'], 'c+') )) { throw new StreamNotReadableException(); } return $this->stream; } private function forecastSize(): ?int { if ($this->compressionMethod !== CompressionMethod::STORE) { return null; } if ($this->exactSize) { return $this->exactSize; } $fstat = fstat($this->unpackStream()); if (!$fstat || !array_key_exists('size', $fstat) || $fstat['size'] < 1) { return null; } if ($this->maxSize !== null && $this->maxSize < $fstat['size']) { return $this->maxSize; } return $fstat['size']; } /** * Create and send zip header for this file. */ private function addFileHeader(): void { $forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64; $footer = $this->buildZip64ExtraBlock($forceEnableZip64); $zip64Enabled = $footer !== ''; if($zip64Enabled) { $this->version = Version::ZIP64; } if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) { // Put the tricky entry to // force Linux unzip to lookup EFS flag. $footer .= Zs\ExtendedInformationExtraField::generate(); } $data = LocalFileHeader::generate( versionNeededToExtract: $this->version->value, generalPurposeBitFlag: $this->generalPurposeBitFlag, compressionMethod: $this->compressionMethod, lastModificationDateTime: $this->lastModificationDateTime, crc32UncompressedData: $this->crc, compressedSize: $zip64Enabled ? 0xFFFFFFFF : $this->compressedSize, uncompressedSize: $zip64Enabled ? 0xFFFFFFFF : $this->uncompressedSize, fileName: $this->fileName, extraField: $footer, ); ($this->send)($data); } /** * Strip characters that are not legal in Windows filenames * to prevent compatibility issues */ private static function filterFilename( /** * Unprocessed filename */ string $fileName ): string { // strip leading slashes from file name // (fixes bug in windows archive viewer) $fileName = ltrim($fileName, '/'); return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName); } private function checkEncoding(): void { // Sets Bit 11: Language encoding flag (EFS). If this bit is set, // the filename and comment fields for this file // MUST be encoded using UTF-8. (see APPENDIX D) if (mb_check_encoding($this->fileName, 'UTF-8') && mb_check_encoding($this->comment, 'UTF-8')) { $this->generalPurposeBitFlag |= GeneralPurposeBitFlag::EFS; } } private function buildZip64ExtraBlock(bool $force = false): string { $outputZip64ExtraBlock = false; $originalSize = null; if ($force || $this->uncompressedSize > 0xFFFFFFFF) { $outputZip64ExtraBlock = true; $originalSize = $this->uncompressedSize; } $compressedSize = null; if ($force || $this->compressedSize > 0xFFFFFFFF) { $outputZip64ExtraBlock = true; $compressedSize = $this->compressedSize; } // If this file will start over 4GB limit in ZIP file, // CDR record will have to use Zip64 extension to describe offset // to keep consistency we use the same value here $relativeHeaderOffset = null; if ($this->startOffset > 0xFFFFFFFF) { $outputZip64ExtraBlock = true; $relativeHeaderOffset = $this->startOffset; } if (!$outputZip64ExtraBlock) { return ''; } if (!$this->enableZip64) { throw new OverflowException(); } return Zip64\ExtendedInformationExtraField::generate( originalSize: $originalSize, compressedSize: $compressedSize, relativeHeaderOffset: $relativeHeaderOffset, diskStartNumber: null, ); } private function addFileFooter(): void { if (($this->compressedSize > 0xFFFFFFFF || $this->uncompressedSize > 0xFFFFFFFF) && $this->version !== Version::ZIP64) { throw new OverflowException(); } if (!$this->enableZeroHeader) { return; } if ($this->version === Version::ZIP64) { $footer = Zip64\DataDescriptor::generate( crc32UncompressedData: $this->crc, compressedSize: $this->compressedSize, uncompressedSize: $this->uncompressedSize, ); } else { $footer = DataDescriptor::generate( crc32UncompressedData: $this->crc, compressedSize: $this->compressedSize, uncompressedSize: $this->uncompressedSize, ); } ($this->send)($footer); } private function readStream(bool $send): void { $this->compressedSize = 0; $this->uncompressedSize = 0; $hash = hash_init('crc32b'); $deflate = $this->compressionInit(); while ( !feof($this->unpackStream()) && ($this->maxSize === null || $this->uncompressedSize < $this->maxSize) && ($this->exactSize === null || $this->uncompressedSize < $this->exactSize) ) { $readLength = min( ($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize, ($this->exactSize ?? PHP_INT_MAX) - $this->uncompressedSize, self::CHUNKED_READ_BLOCK_SIZE ); $data = fread($this->unpackStream(), $readLength); hash_update($hash, $data); $this->uncompressedSize += strlen($data); if ($deflate) { $data = deflate_add( $deflate, $data, feof($this->unpackStream()) ? ZLIB_FINISH : ZLIB_NO_FLUSH ); } $this->compressedSize += strlen($data); if ($send) { ($this->send)($data); } } if ($this->exactSize && $this->uncompressedSize !== $this->exactSize) { throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize); } $this->crc = hexdec(hash_final($hash)); } private function compressionInit(): ?DeflateContext { switch($this->compressionMethod) { case CompressionMethod::STORE: // Noting to do return null; case CompressionMethod::DEFLATE: $deflateContext = deflate_init( ZLIB_ENCODING_RAW, ['level' => $this->deflateLevel] ); if (!$deflateContext) { // @codeCoverageIgnoreStart throw new RuntimeException("Can't initialize deflate context."); // @codeCoverageIgnoreEnd } // False positive, resource is no longer returned from this function return $deflateContext; default: // @codeCoverageIgnoreStart throw new RuntimeException('Unsupported Compression Method ' . print_r($this->compressionMethod, true)); // @codeCoverageIgnoreEnd } } private function getCdrFile(): string { $footer = $this->buildZip64ExtraBlock(); return CentralDirectoryFileHeader::generate( versionMadeBy: ZipStream::ZIP_VERSION_MADE_BY, versionNeededToExtract:$this->version->value, generalPurposeBitFlag: $this->generalPurposeBitFlag, compressionMethod: $this->compressionMethod, lastModificationDateTime: $this->lastModificationDateTime, crc32: $this->crc, compressedSize: $this->compressedSize > 0xFFFFFFFF ? 0xFFFFFFFF : $this->compressedSize, uncompressedSize: $this->uncompressedSize > 0xFFFFFFFF ? 0xFFFFFFFF : $this->uncompressedSize, fileName: $this->fileName, extraField: $footer, fileComment: $this->comment, diskNumberStart: 0, internalFileAttributes: 0, externalFileAttributes: 32, relativeOffsetOfLocalHeader: $this->startOffset > 0xFFFFFFFF ? 0xFFFFFFFF : $this->startOffset, ); } private function isSimulation(): bool { return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT; } } zipstream-php/src/OperationMode.php000064400000001443150364333200013415 0ustar00value), new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)), new PackField(format: 'V', value: $crc32), new PackField(format: 'V', value: $compressedSize), new PackField(format: 'V', value: $uncompressedSize), new PackField(format: 'v', value: strlen($fileName)), new PackField(format: 'v', value: strlen($extraField)), new PackField(format: 'v', value: strlen($fileComment)), new PackField(format: 'v', value: $diskNumberStart), new PackField(format: 'v', value: $internalFileAttributes), new PackField(format: 'V', value: $externalFileAttributes), new PackField(format: 'V', value: $relativeOffsetOfLocalHeader), ) . $fileName . $extraField . $fileComment; } } zipstream-php/src/Time.php000064400000002133150364333200011543 0ustar00getTimestamp() < $dosMinimumDate->getTimestamp()) { throw new DosTimeOverflowException(dateTime: $dateTime); } $dateTime = DateTimeImmutable::createFromInterface($dateTime)->sub(new DateInterval('P1980Y')); ['year' => $year, 'mon' => $month, 'mday' => $day, 'hours' => $hour, 'minutes' => $minute, 'seconds' => $second ] = getdate($dateTime->getTimestamp()); return ($year << 25) | ($month << 21) | ($day << 16) | ($hour << 11) | ($minute << 5) | ($second >> 1); } } zipstream-php/src/DataDescriptor.php000064400000001150150364333200013553 0ustar00 * @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); zipstream-php/README.md000064400000017753150364333200010642 0ustar00# ZipStream-PHP [![Main Branch](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml/badge.svg)](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml) [![Coverage Status](https://coveralls.io/repos/github/maennchen/ZipStream-PHP/badge.svg?branch=main)](https://coveralls.io/github/maennchen/ZipStream-PHP?branch=main) [![Latest Stable Version](https://poser.pugx.org/maennchen/zipstream-php/v/stable)](https://packagist.org/packages/maennchen/zipstream-php) [![Total Downloads](https://poser.pugx.org/maennchen/zipstream-php/downloads)](https://packagist.org/packages/maennchen/zipstream-php) [![Financial Contributors on Open Collective](https://opencollective.com/zipstream/all/badge.svg?label=financial+contributors)](https://opencollective.com/zipstream) [![License](https://img.shields.io/github/license/maennchen/zipstream-php.svg)](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 - https://pablotron.org/ - Jonatan Männchen - https://maennchen.dev - Jesse G. Donat - https://donatstudios.com - Nicolas CARPi - https://www.deltablot.com - Nik Barham - https://www.brokencube.co.uk ## Contributors ### Code Contributors This project exists thanks to all the people who contribute. [[Contribute](.github/CONTRIBUTING.md)]. ### Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/zipstream/contribute)] #### Individuals #### 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)] zipstream-php/.phive/phars.xml000064400000000306150364333200012375 0ustar00 zipstream-php/.tool-versions000064400000000012150364333200012163 0ustar00php 8.2.5 zipstream-php/phpdoc.dist.xml000064400000002412150364333200012306 0ustar00 💾 ZipStream-PHP docs latest src api php public ZipStream true guides guide