diff options
Diffstat (limited to 'vendor/maennchen/zipstream-php/src/ZipStream.php')
-rw-r--r-- | vendor/maennchen/zipstream-php/src/ZipStream.php | 865 |
1 files changed, 0 insertions, 865 deletions
diff --git a/vendor/maennchen/zipstream-php/src/ZipStream.php b/vendor/maennchen/zipstream-php/src/ZipStream.php deleted file mode 100644 index 3f4f481a8..000000000 --- a/vendor/maennchen/zipstream-php/src/ZipStream.php +++ /dev/null @@ -1,865 +0,0 @@ -<?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->addFileFromPath(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->addFileFromPath( - * 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); - } - - $fileTime = filemtime($path); - if ($fileTime !== false) { - $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->addFileFromCallback( - * 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->addDirectory(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 !== null) { - // 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; - } - } -} |