diff --git a/public/lib/thirdpartylibs.xml b/public/lib/thirdpartylibs.xml index 5da9e86072f..121eade2c00 100644 --- a/public/lib/thirdpartylibs.xml +++ b/public/lib/thirdpartylibs.xml @@ -611,7 +611,7 @@ All rights reserved. zipstream ZipStream-PHP PHP ZIP Streaming Library - 3.1.2 + 3.2.0 MIT https://github.com/maennchen/ZipStream-PHP diff --git a/public/lib/zipstream/composer.json b/public/lib/zipstream/composer.json index 6ecd503a66c..529c2445ddf 100644 --- a/public/lib/zipstream/composer.json +++ b/public/lib/zipstream/composer.json @@ -22,12 +22,12 @@ } ], "require": { - "php-64bit": "^8.2", + "php-64bit": "^8.3", "ext-mbstring": "*", "ext-zlib": "*" }, "require-dev": { - "phpunit/phpunit": "^11.0", + "phpunit/phpunit": "^12.0", "guzzlehttp/guzzle": "^7.5", "ext-zip": "*", "mikey179/vfsstream": "^1.6", @@ -57,7 +57,7 @@ "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 --trust-gpg-keys 0x8AC0BAA79732DD42", + "install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656 --trust-gpg-keys 0x8AC0BAA79732DD42 --trust-gpg-keys 0x6DA3ACC4991FFAE5", "docs:generate": "tools/phpdocumentor --sourcecode" }, "autoload": { diff --git a/public/lib/zipstream/src/CompressionMethod.php b/public/lib/zipstream/src/CompressionMethod.php index 51e43637025..2e1289cc366 100644 --- a/public/lib/zipstream/src/CompressionMethod.php +++ b/public/lib/zipstream/src/CompressionMethod.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace ZipStream; +/** + * @api + */ enum CompressionMethod: int { /** diff --git a/public/lib/zipstream/src/Exception.php b/public/lib/zipstream/src/Exception.php index 2e81e307bb3..b3f42e5f268 100644 --- a/public/lib/zipstream/src/Exception.php +++ b/public/lib/zipstream/src/Exception.php @@ -4,4 +4,7 @@ declare(strict_types=1); namespace ZipStream; +/** + * @api + */ abstract class Exception extends \Exception {} diff --git a/public/lib/zipstream/src/Exception/DosTimeOverflowException.php b/public/lib/zipstream/src/Exception/DosTimeOverflowException.php index b8d0508085d..360b5f8342c 100644 --- a/public/lib/zipstream/src/Exception/DosTimeOverflowException.php +++ b/public/lib/zipstream/src/Exception/DosTimeOverflowException.php @@ -8,7 +8,9 @@ use DateTimeInterface; use ZipStream\Exception; /** - * This Exception gets invoked if a file wasn't found + * This Exception gets invoked if a DOS time is overflowing + * + * @api */ class DosTimeOverflowException extends Exception { diff --git a/public/lib/zipstream/src/Exception/FileNotFoundException.php b/public/lib/zipstream/src/Exception/FileNotFoundException.php index 350a7bfe56f..b9b1a2b18ec 100644 --- a/public/lib/zipstream/src/Exception/FileNotFoundException.php +++ b/public/lib/zipstream/src/Exception/FileNotFoundException.php @@ -8,6 +8,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a file wasn't found + * + * @api */ class FileNotFoundException extends Exception { diff --git a/public/lib/zipstream/src/Exception/FileNotReadableException.php b/public/lib/zipstream/src/Exception/FileNotReadableException.php index 93d0c6c64ea..2950423727e 100644 --- a/public/lib/zipstream/src/Exception/FileNotReadableException.php +++ b/public/lib/zipstream/src/Exception/FileNotReadableException.php @@ -7,7 +7,9 @@ namespace ZipStream\Exception; use ZipStream\Exception; /** - * This Exception gets invoked if a file wasn't found + * This Exception gets invoked if a file isn't readable + * + * @api */ class FileNotReadableException extends Exception { diff --git a/public/lib/zipstream/src/Exception/FileSizeIncorrectException.php b/public/lib/zipstream/src/Exception/FileSizeIncorrectException.php index 11f0b67b34d..e700669709f 100644 --- a/public/lib/zipstream/src/Exception/FileSizeIncorrectException.php +++ b/public/lib/zipstream/src/Exception/FileSizeIncorrectException.php @@ -8,6 +8,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a file is not as large as it was specified. + * + * @api */ class FileSizeIncorrectException extends Exception { diff --git a/public/lib/zipstream/src/Exception/OverflowException.php b/public/lib/zipstream/src/Exception/OverflowException.php index 09bdafb2895..d96c4417533 100644 --- a/public/lib/zipstream/src/Exception/OverflowException.php +++ b/public/lib/zipstream/src/Exception/OverflowException.php @@ -8,6 +8,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a counter value exceeds storage size + * + * @api */ class OverflowException extends Exception { diff --git a/public/lib/zipstream/src/Exception/ResourceActionException.php b/public/lib/zipstream/src/Exception/ResourceActionException.php index cbd9b0bb95e..81a2ea59868 100644 --- a/public/lib/zipstream/src/Exception/ResourceActionException.php +++ b/public/lib/zipstream/src/Exception/ResourceActionException.php @@ -8,6 +8,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a resource like `fread` returns false + * + * @api */ class ResourceActionException extends Exception { diff --git a/public/lib/zipstream/src/Exception/SimulationFileUnknownException.php b/public/lib/zipstream/src/Exception/SimulationFileUnknownException.php index 717c1aafef3..240f825123e 100644 --- a/public/lib/zipstream/src/Exception/SimulationFileUnknownException.php +++ b/public/lib/zipstream/src/Exception/SimulationFileUnknownException.php @@ -9,6 +9,8 @@ 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. + * + * @api */ class SimulationFileUnknownException extends Exception { diff --git a/public/lib/zipstream/src/Exception/StreamNotReadableException.php b/public/lib/zipstream/src/Exception/StreamNotReadableException.php index c1446735a6c..b058f55d352 100644 --- a/public/lib/zipstream/src/Exception/StreamNotReadableException.php +++ b/public/lib/zipstream/src/Exception/StreamNotReadableException.php @@ -8,6 +8,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a stream can't be read. + * + * @api */ class StreamNotReadableException extends Exception { diff --git a/public/lib/zipstream/src/Exception/StreamNotSeekableException.php b/public/lib/zipstream/src/Exception/StreamNotSeekableException.php index 606f11f1443..ecfd6e3f4cd 100644 --- a/public/lib/zipstream/src/Exception/StreamNotSeekableException.php +++ b/public/lib/zipstream/src/Exception/StreamNotSeekableException.php @@ -9,6 +9,8 @@ use ZipStream\Exception; /** * This Exception gets invoked if a non seekable stream is * provided and zero headers are disabled. + * + * @api */ class StreamNotSeekableException extends Exception { diff --git a/public/lib/zipstream/src/File.php b/public/lib/zipstream/src/File.php index 1498e4d6be2..148fc55b232 100644 --- a/public/lib/zipstream/src/File.php +++ b/public/lib/zipstream/src/File.php @@ -18,7 +18,7 @@ use ZipStream\Exception\StreamNotSeekableException; /** * @internal */ -class File +final class File { private const CHUNKED_READ_BLOCK_SIZE = 0x1000000; diff --git a/public/lib/zipstream/src/OperationMode.php b/public/lib/zipstream/src/OperationMode.php index dd650f0702f..ae45e3186fc 100644 --- a/public/lib/zipstream/src/OperationMode.php +++ b/public/lib/zipstream/src/OperationMode.php @@ -6,6 +6,8 @@ namespace ZipStream; /** * ZipStream execution operation modes + * + * @api */ enum OperationMode { diff --git a/public/lib/zipstream/src/PackField.php b/public/lib/zipstream/src/PackField.php index 892b4009a62..bf7834035d5 100644 --- a/public/lib/zipstream/src/PackField.php +++ b/public/lib/zipstream/src/PackField.php @@ -10,7 +10,7 @@ use RuntimeException; * @internal * TODO: Make class readonly when requiring PHP 8.2 exclusively */ -class PackField +final class PackField { public const MAX_V = 0xFFFFFFFF; diff --git a/public/lib/zipstream/src/Stream/CallbackStreamWrapper.php b/public/lib/zipstream/src/Stream/CallbackStreamWrapper.php new file mode 100644 index 00000000000..fb354879cd4 --- /dev/null +++ b/public/lib/zipstream/src/Stream/CallbackStreamWrapper.php @@ -0,0 +1,253 @@ + Map of stream IDs to callback functions */ + private static array $callbacks = []; + + /** @var string|null Unique identifier for this stream instance */ + private ?string $id = null; + + /** @var int Current position in the stream */ + private int $pos = 0; + + /** + * Destructor - ensures cleanup even if stream_close() isn't called. + * Prevents memory leaks in long-running processes. + */ + public function __destruct() + { + $this->stream_close(); + } + + /** + * Create a new callback stream. + * + * @param callable(string):void $callback Function to call with written data + * @return resource|false Stream resource or false on failure + */ + public static function open(callable $callback) + { + if (!in_array(self::PROTOCOL, stream_get_wrappers(), true)) { + if (!stream_wrapper_register(self::PROTOCOL, self::class)) { + return false; + } + } + + // Generate cryptographically secure unique ID to prevent collisions + $id = 'cb_' . bin2hex(random_bytes(16)); + self::$callbacks[$id] = $callback; + + return fopen(self::PROTOCOL . "://{$id}", 'wb'); + } + + /** + * Clean up all registered callbacks (useful for testing). + * + * @internal + */ + public static function cleanup(): void + { + self::$callbacks = []; + } + + /** + * Open the stream. + * + * @param string $path Stream path containing the callback ID + * @param string $mode File mode (must contain 'w' for writing) + * @param int $options Stream options (required by interface, unused) + * @param string|null $opened_path Opened path reference (required by interface, unused) + * @return bool True if stream opened successfully + * @psalm-suppress UnusedParam $options and $opened_path are required by the stream wrapper interface + */ + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool + { + if (!str_contains($mode, 'w')) { + return false; + } + + $host = parse_url($path, PHP_URL_HOST); + if ($host === false || $host === null) { + return false; + } + + $this->id = $host; + return isset(self::$callbacks[$this->id]); + } + + /** + * Write data to the callback. + * + * @param string $data Data to write + * @return int Number of bytes written + * @throws RuntimeException If callback execution fails + */ + public function stream_write(string $data): int + { + if ($this->id === null) { + trigger_error('Stream not properly initialized', E_USER_WARNING); + return 0; + } + + $callback = self::$callbacks[$this->id] ?? null; + if ($callback === null) { + trigger_error('Callback not found for stream', E_USER_WARNING); + return 0; + } + + try { + $callback($data); + } catch (Throwable $e) { + throw new RuntimeException( + 'Callback function failed during stream write: ' . $e->getMessage(), + 0, + $e + ); + } + + $length = strlen($data); + $this->pos += $length; + return $length; + } + + /** + * Get current position in stream. + * + * @return int Current position + */ + public function stream_tell(): int + { + return $this->pos; + } + + /** + * Check if stream has reached end of file. + * + * @return bool Always false for write-only streams + */ + public function stream_eof(): bool + { + return false; + } + + /** + * Flush stream buffers. + * + * @return bool Always true (no buffering) + */ + public function stream_flush(): bool + { + return true; + } + + /** + * Close the stream and clean up callback. + */ + public function stream_close(): void + { + if ($this->id !== null) { + unset(self::$callbacks[$this->id]); + $this->id = null; + } + } + + /** + * Get stream statistics. + * + * @return array Stream statistics + */ + public function stream_stat(): array + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0o100666, // Regular file, read/write permissions + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->pos, + 'atime' => time(), + 'mtime' => time(), + 'ctime' => time(), + 'blksize' => 4096, + 'blocks' => ceil($this->pos / 4096), + ]; + } + + /** + * Read data from stream (not supported - write-only stream). + * + * @param int $count Number of bytes to read (required by interface, unused) + * @return string Always empty string + * @psalm-suppress UnusedParam $count is required by the stream wrapper interface + */ + public function stream_read(int $count): string + { + trigger_error('Read operations not supported on callback streams', E_USER_WARNING); + return ''; + } + + /** + * Seek to position in stream (not supported). + * + * @param int $offset Offset to seek to (required by interface, unused) + * @param int $whence Seek mode (required by interface, unused) + * @return bool Always false + * @psalm-suppress UnusedParam $offset and $whence are required by the stream wrapper interface + */ + public function stream_seek(int $offset, int $whence = SEEK_SET): bool + { + trigger_error('Seek operations not supported on callback streams', E_USER_WARNING); + return false; + } + + /** + * Set options on stream (not supported). + * + * @param int $option Option to set (required by interface, unused) + * @param int $arg1 First argument (required by interface, unused) + * @param int $arg2 Second argument (required by interface, unused) + * @return bool Always false + * @psalm-suppress UnusedParam All parameters are required by the stream wrapper interface + */ + public function stream_set_option(int $option, int $arg1, int $arg2): bool + { + return false; + } + + /** + * Truncate stream (not supported). + * + * @param int $new_size New size (required by interface, unused) + * @return bool Always false + * @psalm-suppress UnusedParam $new_size is required by the stream wrapper interface + */ + public function stream_truncate(int $new_size): bool + { + trigger_error('Truncate operations not supported on callback streams', E_USER_WARNING); + return false; + } +} diff --git a/public/lib/zipstream/src/Version.php b/public/lib/zipstream/src/Version.php index c014f8a104b..483d25a5870 100644 --- a/public/lib/zipstream/src/Version.php +++ b/public/lib/zipstream/src/Version.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace ZipStream; +/** + * @api + */ enum Version: int { case STORE = 0x000A; // 1.00 diff --git a/public/lib/zipstream/src/ZipStream.php b/public/lib/zipstream/src/ZipStream.php index 698ffbb32be..d7b939dddb7 100644 --- a/public/lib/zipstream/src/ZipStream.php +++ b/public/lib/zipstream/src/ZipStream.php @@ -64,6 +64,8 @@ use ZipStream\Exception\ResourceActionException; * // write archive footer to stream * $zip->finish(); * ``` + * + * @api */ class ZipStream { @@ -691,7 +693,7 @@ class ZipStream /** * Write zip footer to stream. * - * The clase is left in an unusable state after `finish`. + * The class is left in an unusable state after `finish`. * * ##### Example *