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` | yes | yes | yes | yes | — |
`zend_thread_id` | yes | no | no | no | — |
`zend_version` | yes | yes | yes | yes | — |
+`zip_close` | yes | yes | yes | yes | — |
+`zip_entry_close` | yes | yes | yes | yes | — |
+`zip_entry_filesize` | yes | yes | yes | yes | — |
+`zip_entry_name` | yes | yes | yes | yes | — |
+`zip_entry_open` | yes | yes | yes | yes | — |
+`zip_entry_read` | yes | yes | yes | yes | — |
+`zip_open` | yes | yes | yes | yes | — |
+`zip_read` | yes | yes | yes | yes | — |
`zlib_decode` | yes | yes | yes | yes | — |
`zlib_encode` | yes | yes | yes | yes | — |
`zlib_get_coding_type` | yes | yes | yes | yes | — |
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());
}
}