Commit 214ed7eb93c for php.net

commit 214ed7eb93c181cc6f53dc3a4de2d72a38776b5a
Author: David Carlier <devnexen@gmail.com>
Date:   Thu Sep 10 22:47:31 2026 +0100

    ext/zip: ZipArchive::extractTo() ignores files given in a non-list array

    The array form walked the entries by synthetic 0..n-1 indices, so a files
    array with non-sequential keys (as returned by array_filter() and similar)
    or an integer element extracted nothing while still returning true. It now
    iterates the array values, honouring every entry regardless of its keys.

    Close GH-23647

diff --git a/NEWS b/NEWS
index 1fd676d2a64..33e9859d219 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,10 @@ PHP                                                                        NEWS
     registrations are freed while still reachable from the cycle collector.
     (Ilia Alshanetsky)

+- Zip:
+  . Fixed ZipArchive::extractTo() ignoring files given in a non-list array.
+    (David Carlier)
+

 24 Sep 2026, PHP 8.4.26

diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index 69b81b88753..682736d4fe7 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -2963,7 +2963,7 @@ PHP_METHOD(ZipArchive, extractTo)
 		}
 	}

-	uint32_t nelems, i;
+	uint32_t nelems;

 	if (files_str) {
 		if (!php_zip_extract_file(intern, pathto, ZSTR_VAL(files_str), ZSTR_LEN(files_str), -1)) {
@@ -2974,20 +2974,19 @@ PHP_METHOD(ZipArchive, extractTo)
 		if (nelems == 0 ) {
 			RETURN_FALSE;
 		}
-		for (i = 0; i < nelems; i++) {
-			zval *zval_file;
-			if ((zval_file = zend_hash_index_find_deref(files_ht, i)) != NULL) {
-				switch (Z_TYPE_P(zval_file)) {
-					case IS_LONG:
-						break;
-					case IS_STRING:
-						if (!php_zip_extract_file(intern, pathto, Z_STRVAL_P(zval_file), Z_STRLEN_P(zval_file), -1)) {
-							RETURN_FALSE;
-						}
-						break;
-				}
+		zval *zval_file;
+		ZEND_HASH_FOREACH_VAL(files_ht, zval_file) {
+			ZVAL_DEREF(zval_file);
+			switch (Z_TYPE_P(zval_file)) {
+				case IS_LONG:
+					break;
+				case IS_STRING:
+					if (!php_zip_extract_file(intern, pathto, Z_STRVAL_P(zval_file), Z_STRLEN_P(zval_file), -1)) {
+						RETURN_FALSE;
+					}
+					break;
 			}
-		}
+		} ZEND_HASH_FOREACH_END();
 	} else {
 		/* Extract all files */
 		zip_int64_t i, filecount = zip_get_num_entries(intern, 0);
diff --git a/ext/zip/tests/oo_extract_array_keys.phpt b/ext/zip/tests/oo_extract_array_keys.phpt
new file mode 100644
index 00000000000..c825bbefbbb
--- /dev/null
+++ b/ext/zip/tests/oo_extract_array_keys.phpt
@@ -0,0 +1,38 @@
+--TEST--
+ZipArchive::extractTo() with a non-list files array (non-sequential keys)
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$archive = __DIR__ . "/oo_extract_array_keys.zip";
+
+$zip = new ZipArchive();
+$zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString("file0.txt", "zero");
+$zip->addFromString("file1.txt", "one");
+$zip->close();
+
+$target = __DIR__ . "/oo_extract_array_keys";
+mkdir($target);
+
+// array_filter() (and array_unique/array_diff) preserve keys, so this is [1 => "file1.txt"],
+// a perfectly valid list of entry names that is not a packed 0-based array.
+$files = array_filter(["file0.txt", "file1.txt"], fn($f) => $f === "file1.txt");
+
+$zip = new ZipArchive();
+$zip->open($archive);
+var_dump($zip->extractTo($target, $files));
+$zip->close();
+
+var_dump(is_file("$target/file1.txt"));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+--CLEAN--
+<?php
+@unlink(__DIR__ . "/oo_extract_array_keys.zip");
+$target = __DIR__ . "/oo_extract_array_keys";
+@unlink("$target/file1.txt");
+@rmdir($target);
+?>