From c721fda806472c0a193aa18487eb4f60fab0a979 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 12 Jul 2026 01:15:29 +0000 Subject: [PATCH] Zip: procedural zip_open/read/close + zip_entry_* API (#6370) Implement legacy procedural zip API in pure PHP over ZipEngine, with zip archive/entry resource placeholders in VmFs. php-src ref: ext/zip/php_zip.c. Co-authored-by: Cursor --- docs/capabilities.md | 8 + docs/pages/capability-comparison.html | 10 +- ext/standard/VmFs.php | 81 ++++++ ext/standard/is_resource_.php | 7 +- ext/zip/Module.php | 27 +- ext/zip/VmZipProcedural.php | 232 ++++++++++++++++++ ext/zip/VmZipResourceArg.php | 70 ++++++ ext/zip/ZipProceduralFunction.php | 19 ++ ext/zip/zip_close.php | 26 ++ ext/zip/zip_entry_close.php | 26 ++ ext/zip/zip_entry_filesize.php | 32 +++ ext/zip/zip_entry_name.php | 32 +++ ext/zip/zip_entry_open.php | 36 +++ ext/zip/zip_entry_read.php | 40 +++ ext/zip/zip_open.php | 43 ++++ ext/zip/zip_read.php | 32 +++ script/capability-matrix.php | 1 + .../cases/zip/zip_procedural_basic.phpt | 47 ++++ test/repro/zip_procedural_basic.php | 86 +++++++ test/unit/ZipModuleTest.php | 8 +- 20 files changed, 858 insertions(+), 5 deletions(-) create mode 100644 ext/zip/VmZipProcedural.php create mode 100644 ext/zip/VmZipResourceArg.php create mode 100644 ext/zip/ZipProceduralFunction.php create mode 100644 ext/zip/zip_close.php create mode 100644 ext/zip/zip_entry_close.php create mode 100644 ext/zip/zip_entry_filesize.php create mode 100644 ext/zip/zip_entry_name.php create mode 100644 ext/zip/zip_entry_open.php create mode 100644 ext/zip/zip_entry_read.php create mode 100644 ext/zip/zip_open.php create mode 100644 ext/zip/zip_read.php create mode 100644 test/compliance/cases/zip/zip_procedural_basic.phpt create mode 100644 test/repro/zip_procedural_basic.php diff --git a/docs/capabilities.md b/docs/capabilities.md index b0d97bbdc05..b8d9626465f 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -948,6 +948,14 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `wordwrap` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `zend_thread_id` | no | no | no | standard | impl present; gated (8.4.0 ref, since 8.4.0); JIT PHPT; AOT PHPT | | `zend_version` | yes | yes | yes | standard | JIT PHPT | +| `zip_close` | yes | yes | yes | zip | | +| `zip_entry_close` | yes | yes | yes | zip | | +| `zip_entry_filesize` | yes | yes | yes | zip | | +| `zip_entry_name` | yes | yes | yes | zip | | +| `zip_entry_open` | yes | yes | yes | zip | | +| `zip_entry_read` | yes | yes | yes | zip | | +| `zip_open` | yes | yes | yes | zip | | +| `zip_read` | yes | yes | yes | zip | | | `zlib_decode` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `zlib_encode` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `zlib_get_coding_type` | yes | yes | yes | standard | | diff --git a/docs/pages/capability-comparison.html b/docs/pages/capability-comparison.html index 7112afccbf2..c12bc498300 100644 --- a/docs/pages/capability-comparison.html +++ b/docs/pages/capability-comparison.html @@ -64,7 +64,7 @@

PHP capability comparison

Stdlib builtins
- 896 / 945 full (VM+JIT+AOT) + 904 / 953 full (VM+JIT+AOT)

24 partial · 25 gaps

@@ -1127,6 +1127,14 @@

Stdlib builtins

`wordwrap`yesyesyesyes— `zend_thread_id`yesnonono— `zend_version`yesyesyesyes— +`zip_close`yesyesyesyes— +`zip_entry_close`yesyesyesyes— +`zip_entry_filesize`yesyesyesyes— +`zip_entry_name`yesyesyesyes— +`zip_entry_open`yesyesyesyes— +`zip_entry_read`yesyesyesyes— +`zip_open`yesyesyesyes— +`zip_read`yesyesyesyes— `zlib_decode`yesyesyesyes— `zlib_encode`yesyesyesyes— `zlib_get_coding_type`yesyesyesyes— diff --git a/ext/standard/VmFs.php b/ext/standard/VmFs.php index 9223d008011..d5d8c3f31bf 100644 --- a/ext/standard/VmFs.php +++ b/ext/standard/VmFs.php @@ -44,6 +44,12 @@ final class VmFs /** @var array bz* stream placeholders — I/O via VmBz2StreamPure (#17301) */ private static array $bzNativePlaceholders = []; + /** @var array zip_open() archive placeholders — state in VmZipProcedural (#6370) */ + private static array $zipArchivePlaceholders = []; + + /** @var array zip_read() entry placeholders — parent archive handle (#6370) */ + private static array $zipEntryPlaceholders = []; + /** @var array host stream identity => outstanding VM handle ids (#3384 pfsockopen persistent) */ private static array $hostResourceRefcounts = []; @@ -1036,6 +1042,75 @@ public static function isBzNativePlaceholder(int $handle): bool return isset(self::$bzNativePlaceholders[$handle]); } + /** + * Register a VM stream handle for zip_open() procedural archives (#6370). + * + * @return int|false + */ + public static function adoptZipArchivePlaceholder(string $path) + { + $id = VmPhpMemoryStream::open('php://memory', 'r+b'); + if (false === $id) { + return false; + } + self::$handlePaths[$id] = 'zip://'.$path; + self::$zipArchivePlaceholders[$id] = $path; + + return $id; + } + + public static function isZipArchivePlaceholder(int $handle): bool + { + return isset(self::$zipArchivePlaceholders[$handle]); + } + + public static function releaseZipArchivePlaceholder(int $handle): void + { + if (!isset(self::$zipArchivePlaceholders[$handle])) { + return; + } + unset(self::$zipArchivePlaceholders[$handle]); + if (VmPhpMemoryStream::isValidHandle($handle)) { + VmPhpMemoryStream::close($handle); + unset(self::$handlePaths[$handle], self::$handleModes[$handle], self::$handleBlocked[$handle]); + } + } + + /** + * @return int|false + */ + public static function adoptZipEntryPlaceholder(int $archiveHandle) + { + if (!isset(self::$zipArchivePlaceholders[$archiveHandle])) { + return false; + } + $id = VmPhpMemoryStream::open('php://memory', 'r+b'); + if (false === $id) { + return false; + } + self::$handlePaths[$id] = 'zip-entry://'.$archiveHandle; + self::$zipEntryPlaceholders[$id] = $archiveHandle; + + return $id; + } + + public static function isZipEntryPlaceholder(int $handle): bool + { + return isset(self::$zipEntryPlaceholders[$handle]); + } + + public static function releaseZipEntryPlaceholder(int $handle): void + { + if (!isset(self::$zipEntryPlaceholders[$handle])) { + return; + } + unset(self::$zipEntryPlaceholders[$handle]); + if (VmPhpMemoryStream::isValidHandle($handle)) { + VmPhpMemoryStream::close($handle); + unset(self::$handlePaths[$handle], self::$handleModes[$handle], self::$handleBlocked[$handle]); + } + } + public static function releaseBzNativePlaceholder(int $handle): void { if (!isset(self::$bzNativePlaceholders[$handle])) { @@ -2391,6 +2466,12 @@ public static function resourceTypeForStreamTag(int $handle): string if (isset(self::$bzNativePlaceholders[$handle])) { return 'bzip2'; } + if (isset(self::$zipArchivePlaceholders[$handle])) { + return 'Zip Archive'; + } + if (isset(self::$zipEntryPlaceholders[$handle])) { + return 'Zip Entry'; + } if (VmPhpMemoryStream::isValidHandle($handle)) { return 'stream'; } diff --git a/ext/standard/is_resource_.php b/ext/standard/is_resource_.php index 30607a60dee..ba2fd0a07bd 100644 --- a/ext/standard/is_resource_.php +++ b/ext/standard/is_resource_.php @@ -73,7 +73,12 @@ public static function isResource(Variable $v): bool $handle = \PHPCompiler\VM\ResourceSupport::resolveHandle($v); return null !== $handle - && (VmFs::isValidHandle($handle) || VmFs::isFailedStreamHandle($handle)); + && ( + VmFs::isValidHandle($handle) + || VmFs::isFailedStreamHandle($handle) + || VmFs::isZipArchivePlaceholder($handle) + || VmFs::isZipEntryPlaceholder($handle) + ); } if ($v->isDirResource()) { $handle = \PHPCompiler\VM\ResourceSupport::resolveHandle($v); diff --git a/ext/zip/Module.php b/ext/zip/Module.php index f43c5bfbc4a..436f99d6887 100644 --- a/ext/zip/Module.php +++ b/ext/zip/Module.php @@ -8,9 +8,9 @@ use PHPCompiler\Runtime; /** - * zip extension module entry (php-src ext/zip/php_zip.c; issues #5869, #3337). + * zip extension module entry (php-src ext/zip/php_zip.c; issues #5869, #3337, #6370). * - * ZipArchive uses pure-PHP store engine ({@see ZipEngine}) without libzip. + * ZipArchive uses pure-PHP store engine ({@see ZipEngine}); procedural zip_* API in {@see VmZipProcedural}. */ class Module extends ModuleAbstract { @@ -19,4 +19,27 @@ public function init(Runtime $runtime): void parent::init($runtime); BuiltinClasses::register($runtime->vmContext); } + + public function getExtensionName(): string + { + return 'zip'; + } + + public function getFunctions(): array + { + if (!ZipExtensionPolicy::advertisesExtension()) { + return []; + } + + return [ + new zip_open(), + new zip_close(), + new zip_read(), + new zip_entry_open(), + new zip_entry_close(), + new zip_entry_read(), + new zip_entry_name(), + new zip_entry_filesize(), + ]; + } } diff --git a/ext/zip/VmZipProcedural.php b/ext/zip/VmZipProcedural.php new file mode 100644 index 00000000000..2ca00f20605 --- /dev/null +++ b/ext/zip/VmZipProcedural.php @@ -0,0 +1,232 @@ +, index: int}> */ + private static array $archives = []; + + /** @var array */ + private static array $entries = []; + + public static function zipOpen(string $filename): int|false + { + if (!is_file($filename)) { + return false; + } + $read = ZipEngine::readArchive($filename); + if (!$read['ok']) { + return false; + } + $handle = VmFs::adoptZipArchivePlaceholder($filename); + if (false === $handle) { + return false; + } + self::$archives[$handle] = [ + 'path' => $filename, + 'entries' => $read['entries'], + 'index' => 0, + ]; + + return $handle; + } + + public static function zipClose(int $handle): bool + { + if (!self::isArchiveHandle($handle)) { + return false; + } + foreach (self::$entries as $entryHandle => $entry) { + if ($entry['archive'] === $handle) { + unset(self::$entries[$entryHandle]); + VmFs::releaseZipEntryPlaceholder($entryHandle); + } + } + unset(self::$archives[$handle]); + VmFs::releaseZipArchivePlaceholder($handle); + + return true; + } + + public static function zipRead(int $archiveHandle): int|false + { + $archive = self::$archives[$archiveHandle] ?? null; + if (null === $archive) { + return false; + } + if ($archive['index'] >= \count($archive['entries'])) { + return false; + } + $entryHandle = VmFs::adoptZipEntryPlaceholder($archiveHandle); + if (false === $entryHandle) { + return false; + } + self::$entries[$entryHandle] = [ + 'archive' => $archiveHandle, + 'entryIndex' => $archive['index'], + 'pos' => 0, + 'open' => false, + ]; + ++self::$archives[$archiveHandle]['index']; + + return $entryHandle; + } + + public static function zipEntryOpen(int $archiveHandle, int $entryHandle, string $mode = 'rb'): bool + { + if (!self::isArchiveHandle($archiveHandle) || !self::isEntryHandle($entryHandle)) { + return false; + } + $entry = self::$entries[$entryHandle]; + if ($entry['archive'] !== $archiveHandle) { + return false; + } + if ('' !== $mode && 'r' !== $mode && 'rb' !== $mode) { + return false; + } + self::$entries[$entryHandle]['open'] = true; + self::$entries[$entryHandle]['pos'] = 0; + + return true; + } + + public static function zipEntryClose(int $entryHandle): bool + { + if (!self::isEntryHandle($entryHandle)) { + return false; + } + self::$entries[$entryHandle]['open'] = false; + self::$entries[$entryHandle]['pos'] = 0; + + return true; + } + + public static function zipEntryRead(int $entryHandle, int $length = 1024): string|false + { + $entry = self::$entries[$entryHandle] ?? null; + if (null === $entry || !$entry['open']) { + return false; + } + if ($length < 0) { + return false; + } + $payload = self::entryData($entry); + if (null === $payload) { + return false; + } + if (0 === $length) { + return ''; + } + $remaining = \strlen($payload) - $entry['pos']; + if ($remaining <= 0) { + return ''; + } + $take = \min($length, $remaining); + $chunk = \substr($payload, $entry['pos'], $take); + self::$entries[$entryHandle]['pos'] += $take; + + return $chunk; + } + + public static function zipEntryName(int $entryHandle): string|false + { + $entry = self::$entries[$entryHandle] ?? null; + if (null === $entry) { + return false; + } + $archive = self::$archives[$entry['archive']] ?? null; + if (null === $archive) { + return false; + } + $zipEntry = $archive['entries'][$entry['entryIndex']] ?? null; + if (null === $zipEntry) { + return false; + } + + return $zipEntry['name']; + } + + public static function zipEntryFilesize(int $entryHandle): int|false + { + $entry = self::$entries[$entryHandle] ?? null; + if (null === $entry) { + return false; + } + $archive = self::$archives[$entry['archive']] ?? null; + if (null === $archive) { + return false; + } + $zipEntry = $archive['entries'][$entry['entryIndex']] ?? null; + if (null === $zipEntry) { + return false; + } + + return (int) $zipEntry['size']; + } + + public static function isArchiveHandle(int $handle): bool + { + return isset(self::$archives[$handle]) && VmFs::isZipArchivePlaceholder($handle); + } + + public static function isEntryHandle(int $handle): bool + { + return isset(self::$entries[$handle]) && VmFs::isZipEntryPlaceholder($handle); + } + + public static function requireArchiveHandle(Variable $var, string $function, int $argNum = 1): int + { + $var = $var->resolveIndirect(); + $handle = VmZipResourceArg::resolveHandle($var); + if (null === $handle || !self::isArchiveHandle($handle)) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type resource, %s given', + $function, + $argNum, + 1 === $argNum ? 'filename' : 'zip', + VmZipResourceArg::debugTypeName($var) + )); + } + + return $handle; + } + + public static function requireEntryHandle(Variable $var, string $function, int $argNum = 1): int + { + $var = $var->resolveIndirect(); + $handle = VmZipResourceArg::resolveHandle($var); + if (null === $handle || !self::isEntryHandle($handle)) { + throw new \TypeError(\sprintf( + '%s(): Argument #%d ($%s) must be of type resource, %s given', + $function, + $argNum, + 'zip_entry', + VmZipResourceArg::debugTypeName($var) + )); + } + + return $handle; + } + + /** @return ?string */ + private static function entryData(array $entry): ?string + { + $archive = self::$archives[$entry['archive']] ?? null; + if (null === $archive) { + return null; + } + + return $archive['entries'][$entry['entryIndex']]['data'] ?? null; + } +} diff --git a/ext/zip/VmZipResourceArg.php b/ext/zip/VmZipResourceArg.php new file mode 100644 index 00000000000..0d3f29c9c6a --- /dev/null +++ b/ext/zip/VmZipResourceArg.php @@ -0,0 +1,70 @@ +resolveIndirect(); + if (!$var->isStreamResource()) { + return null; + } + $handle = ResourceSupport::resolveHandle($var); + if (null === $handle) { + return null; + } + if (VmFs::isZipArchivePlaceholder($handle) || VmFs::isZipEntryPlaceholder($handle)) { + return $handle; + } + + return null; + } + + public static function isZipResource(Variable $var): bool + { + $handle = self::resolveHandle($var); + + return null !== $handle; + } + + public static function debugTypeName(Variable $var): string + { + $var = $var->resolveIndirect(); + if (EnumCaseSupport::isEnumCaseVariable($var)) { + return EnumCaseSupport::typeNameForVariable($var); + } + $handle = self::resolveHandle($var); + if (null !== $handle) { + if (VmFs::isZipArchivePlaceholder($handle)) { + return 'resource'; + } + if (VmFs::isZipEntryPlaceholder($handle)) { + return 'resource'; + } + } + $resourceDebug = ResourceSupport::debugTypeName($var); + if (null !== $resourceDebug) { + return $resourceDebug; + } + + return match ($var->type) { + Variable::TYPE_NULL => 'null', + Variable::TYPE_BOOLEAN => 'bool', + Variable::TYPE_INTEGER => 'int', + Variable::TYPE_DOUBLE => 'float', + Variable::TYPE_STRING => 'string', + Variable::TYPE_ARRAY => 'array', + Variable::TYPE_OBJECT => $var->toObject()->class->name, + default => 'mixed', + }; + } +} diff --git a/ext/zip/ZipProceduralFunction.php b/ext/zip/ZipProceduralFunction.php new file mode 100644 index 00000000000..d8d95623996 --- /dev/null +++ b/ext/zip/ZipProceduralFunction.php @@ -0,0 +1,19 @@ +getName().'() is not lowered for JIT/AOT in this compiler build (#6370)'); + } +} diff --git a/ext/zip/zip_close.php b/ext/zip/zip_close.php new file mode 100644 index 00000000000..2a4d5213707 --- /dev/null +++ b/ext/zip/zip_close.php @@ -0,0 +1,26 @@ +requireExactArgCount($frame, 'zip_close', 1); + if (null === $frame->returnVar) { + return; + } + $handle = VmZipProcedural::requireArchiveHandle($frame->calledArgs[0], 'zip_close', 1); + $frame->returnVar->bool(VmZipProcedural::zipClose($handle)); + } +} diff --git a/ext/zip/zip_entry_close.php b/ext/zip/zip_entry_close.php new file mode 100644 index 00000000000..a20f4a53cca --- /dev/null +++ b/ext/zip/zip_entry_close.php @@ -0,0 +1,26 @@ +requireExactArgCount($frame, 'zip_entry_close', 1); + if (null === $frame->returnVar) { + return; + } + $entry = VmZipProcedural::requireEntryHandle($frame->calledArgs[0], 'zip_entry_close', 1); + $frame->returnVar->bool(VmZipProcedural::zipEntryClose($entry)); + } +} diff --git a/ext/zip/zip_entry_filesize.php b/ext/zip/zip_entry_filesize.php new file mode 100644 index 00000000000..01df554f542 --- /dev/null +++ b/ext/zip/zip_entry_filesize.php @@ -0,0 +1,32 @@ +requireExactArgCount($frame, 'zip_entry_filesize', 1); + if (null === $frame->returnVar) { + return; + } + $entry = VmZipProcedural::requireEntryHandle($frame->calledArgs[0], 'zip_entry_filesize', 1); + $size = VmZipProcedural::zipEntryFilesize($entry); + if (false === $size) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->int($size); + } +} diff --git a/ext/zip/zip_entry_name.php b/ext/zip/zip_entry_name.php new file mode 100644 index 00000000000..a1f4540526a --- /dev/null +++ b/ext/zip/zip_entry_name.php @@ -0,0 +1,32 @@ +requireExactArgCount($frame, 'zip_entry_name', 1); + if (null === $frame->returnVar) { + return; + } + $entry = VmZipProcedural::requireEntryHandle($frame->calledArgs[0], 'zip_entry_name', 1); + $name = VmZipProcedural::zipEntryName($entry); + if (false === $name) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->string($name); + } +} diff --git a/ext/zip/zip_entry_open.php b/ext/zip/zip_entry_open.php new file mode 100644 index 00000000000..fb0031c10de --- /dev/null +++ b/ext/zip/zip_entry_open.php @@ -0,0 +1,36 @@ +calledArgs); + if ($argc < 2 || $argc > 3) { + throw new \ArgumentCountError( + 'zip_entry_open() expects at least 2 arguments, '.$argc.' given' + ); + } + if (null === $frame->returnVar) { + return; + } + $archive = VmZipProcedural::requireArchiveHandle($frame->calledArgs[0], 'zip_entry_open', 1); + $entry = VmZipProcedural::requireEntryHandle($frame->calledArgs[1], 'zip_entry_open', 2); + $mode = 3 === $argc + ? VmString::coerceStringBuiltinArg($frame->calledArgs[2], 'zip_entry_open', 3, 'mode') + : 'rb'; + $frame->returnVar->bool(VmZipProcedural::zipEntryOpen($archive, $entry, $mode)); + } +} diff --git a/ext/zip/zip_entry_read.php b/ext/zip/zip_entry_read.php new file mode 100644 index 00000000000..3e87e31efd9 --- /dev/null +++ b/ext/zip/zip_entry_read.php @@ -0,0 +1,40 @@ +calledArgs); + if ($argc < 1 || $argc > 2) { + throw new \ArgumentCountError( + 'zip_entry_read() expects at least 1 argument, '.$argc.' given' + ); + } + if (null === $frame->returnVar) { + return; + } + $entry = VmZipProcedural::requireEntryHandle($frame->calledArgs[0], 'zip_entry_read', 1); + $length = 2 === $argc + ? VmZipArchive::coerceIntArg($frame->calledArgs[1], 'zip_entry_read', 2, 'length', 1024) + : 1024; + $data = VmZipProcedural::zipEntryRead($entry, $length); + if (false === $data) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->string($data); + } +} diff --git a/ext/zip/zip_open.php b/ext/zip/zip_open.php new file mode 100644 index 00000000000..5b2ee02c2cc --- /dev/null +++ b/ext/zip/zip_open.php @@ -0,0 +1,43 @@ +requireExactArgCount($frame, 'zip_open', 1); + if (null === $frame->returnVar) { + return; + } + $filename = VmString::coerceStringBuiltinArg($frame->calledArgs[0], 'zip_open', 1, 'filename'); + $handle = VmZipProcedural::zipOpen($filename); + if (false === $handle) { + if (null !== $frame->vmContext) { + $frame->vmContext->errors->triggerError( + \sprintf('zip_open(): Unable to open file: %s', $filename), + ErrorReporter::E_WARNING, + '' !== $frame->scriptPath ? $frame->scriptPath : null, + $frame->vmContext, + $frame + ); + } + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->streamHandle($handle, $frame->vmContext); + } +} diff --git a/ext/zip/zip_read.php b/ext/zip/zip_read.php new file mode 100644 index 00000000000..534381ea48c --- /dev/null +++ b/ext/zip/zip_read.php @@ -0,0 +1,32 @@ +requireExactArgCount($frame, 'zip_read', 1); + if (null === $frame->returnVar) { + return; + } + $archive = VmZipProcedural::requireArchiveHandle($frame->calledArgs[0], 'zip_read', 1); + $entry = VmZipProcedural::zipRead($archive); + if (false === $entry) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->streamHandle($entry, $frame->vmContext); + } +} diff --git a/script/capability-matrix.php b/script/capability-matrix.php index 9cccbcd54a5..7d9e4f5a7ff 100755 --- a/script/capability-matrix.php +++ b/script/capability-matrix.php @@ -37,6 +37,7 @@ function collectCapabilities(string $root): array 'sodium' => new PHPCompiler\ext\sodium\Module(), 'sockets' => new PHPCompiler\ext\sockets\Module(), 'curl' => new PHPCompiler\ext\curl\Module(), + 'zip' => new PHPCompiler\ext\zip\Module(), 'inotify' => new PHPCompiler\ext\inotify\Module(), ]; diff --git a/test/compliance/cases/zip/zip_procedural_basic.phpt b/test/compliance/cases/zip/zip_procedural_basic.phpt new file mode 100644 index 00000000000..e891f73c88b --- /dev/null +++ b/test/compliance/cases/zip/zip_procedural_basic.phpt @@ -0,0 +1,47 @@ +--TEST-- +zip procedural API round-trip (ext/zip/php_zip.c, #6370) +--FILE-- +open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE); +$zip->addFromString('hello.txt', $payload); +$zip->close(); + +echo function_exists('zip_open') ? '1' : '0', "\n"; + +$zh = zip_open($archive); +echo is_resource($zh) ? '1' : '0', "\n"; +echo get_resource_type($zh), "\n"; + +$entry = zip_read($zh); +echo is_resource($entry) ? '1' : '0', "\n"; +echo get_resource_type($entry), "\n"; +echo zip_entry_name($entry), "\n"; +echo zip_entry_filesize($entry), "\n"; +echo zip_entry_open($zh, $entry) ? '1' : '0', "\n"; +echo zip_entry_read($entry, strlen($payload)), "\n"; +echo zip_entry_close($entry) ? '1' : '0', "\n"; +echo zip_read($zh) === false ? '1' : '0', "\n"; +echo zip_close($zh) ? '1' : '0', "\n"; + +@unlink($archive); +@rmdir($tmpdir); +?> +--EXPECT-- +1 +1 +Zip Archive +1 +Zip Entry +hello.txt +22 +1 +zip procedural payload +1 +1 +1 diff --git a/test/repro/zip_procedural_basic.php b/test/repro/zip_procedural_basic.php new file mode 100644 index 00000000000..41134bbcf5b --- /dev/null +++ b/test/repro/zip_procedural_basic.php @@ -0,0 +1,86 @@ +open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { + fwrite(STDERR, "fail: could not create fixture archive\n"); + exit(1); +} +$zip->addFromString('hello.txt', $payload); +$zip->close(); + +try { + if (!function_exists('zip_open')) { + fwrite(STDERR, "fail: zip_open missing\n"); + exit(1); + } + + $zh = zip_open($archive); + if (!is_resource($zh)) { + fwrite(STDERR, "fail: zip_open did not return resource\n"); + exit(1); + } + if ('Zip Archive' !== get_resource_type($zh)) { + fwrite(STDERR, 'fail: archive type=' . get_resource_type($zh) . "\n"); + exit(1); + } + + $entry = zip_read($zh); + if (!is_resource($entry)) { + fwrite(STDERR, "fail: zip_read did not return resource\n"); + exit(1); + } + if ('Zip Entry' !== get_resource_type($entry)) { + fwrite(STDERR, 'fail: entry type=' . get_resource_type($entry) . "\n"); + exit(1); + } + + $name = zip_entry_name($entry); + if ('hello.txt' !== $name) { + fwrite(STDERR, "fail: entry name={$name}\n"); + exit(1); + } + + $size = zip_entry_filesize($entry); + if (\strlen($payload) !== $size) { + fwrite(STDERR, "fail: entry size={$size}\n"); + exit(1); + } + + if (!zip_entry_open($zh, $entry)) { + fwrite(STDERR, "fail: zip_entry_open\n"); + exit(1); + } + + $data = zip_entry_read($entry, $size); + if ($payload !== $data) { + fwrite(STDERR, "fail: entry data mismatch\n"); + exit(1); + } + + if (!zip_entry_close($entry)) { + fwrite(STDERR, "fail: zip_entry_close\n"); + exit(1); + } + + if (false !== zip_read($zh)) { + fwrite(STDERR, "fail: expected false after last entry\n"); + exit(1); + } + + if (!zip_close($zh)) { + fwrite(STDERR, "fail: zip_close\n"); + exit(1); + } + + echo "ok\n"; +} finally { + @unlink($archive); + @rmdir($tmpdir); +} diff --git a/test/unit/ZipModuleTest.php b/test/unit/ZipModuleTest.php index 8de1c82a6da..29f462159ae 100644 --- a/test/unit/ZipModuleTest.php +++ b/test/unit/ZipModuleTest.php @@ -20,14 +20,20 @@ public function test_zip_module_skeleton_class(): void $ctx = $runtime->vmContext; self::assertTrue(VmReflection::classExists($ctx, 'ZipArchive')); + self::assertTrue(VmReflection::functionExists($ctx, 'zip_open')); + self::assertTrue(VmReflection::functionExists($ctx, 'zip_read')); + self::assertTrue(VmReflection::functionExists($ctx, 'zip_close')); + self::assertTrue(VmReflection::functionExists($ctx, 'zip_entry_name')); $code = <<<'PHP' parseAndCompile($code, 'zip_module.php'); ob_start(); $runtime->run($block); - self::assertSame('1', ob_get_clean()); + self::assertSame("1\n1", ob_get_clean()); } }