Commit 8d5b0d6fb4a for php.net

commit 8d5b0d6fb4a9e6175f6a7e636d3e9f5c0ace7862
Author: David Carlier <devnexen@gmail.com>
Date:   Sat Aug 8 21:27:14 2026 +0100

    ext/zip: addGlob() and addPattern() ignore their default options.

    php_zip_parse_options() held the defaults but only ran for a non-empty
    options array, so opts stayed zeroed otherwise: comp_method 0 is CM_STORE
    and flags 0 drops FL_OVERWRITE. Entries were therefore stored uncompressed,
    and an already present entry name failed the call instead of being replaced.
    The defaults now live in PHP_ZIP_DEFAULT_OPTIONS, applied at declaration.

    Close GH-23108

diff --git a/NEWS b/NEWS
index 9cc94c63eaf..99de3bd4b2a 100644
--- a/NEWS
+++ b/NEWS
@@ -69,6 +69,10 @@ PHP                                                                        NEWS
   . Fixed out-of-bounds write when shm_attach() opens an existing segment with
     a size larger than the segment actually is. (David Carlier)

+- Zip:
+  . Fixed ZipArchive::addGlob() and ZipArchive::addPattern() ignoring their
+    default options when no options array is given. (David Carlier)
+
 30 Jul 2026, PHP 8.4.24

 - BCMath:
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index ccc474bc715..fecb9396ace 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -64,6 +64,12 @@ static int le_zip_entry;
 	}
 /* }}} */

+#ifdef HAVE_ENCRYPTION
+#define PHP_ZIP_DEFAULT_OPTIONS { .enc_method = -1, .comp_method = -1, .flags = ZIP_FL_OVERWRITE }
+#else
+#define PHP_ZIP_DEFAULT_OPTIONS { .comp_method = -1, .flags = ZIP_FL_OVERWRITE }
+#endif
+
 /* {{{ php_zip_set_file_comment */
 static bool php_zip_set_file_comment(struct zip *za, zip_uint64_t index, const char *comment, size_t comment_len)
 {
@@ -373,13 +379,6 @@ static int php_zip_parse_options(HashTable *options, zip_options *opts)
 {
 	zval *option;

-	/* default values */
-	opts->flags = ZIP_FL_OVERWRITE;
-	opts->comp_method = -1; /* -1 to not change default */
-#ifdef HAVE_ENCRYPTION
-	opts->enc_method = -1;  /* -1 to not change default */
-#endif
-
 	if ((option = zend_hash_str_find(options, "remove_all_path", sizeof("remove_all_path") - 1)) != NULL) {
 		if (Z_TYPE_P(option) != IS_FALSE && Z_TYPE_P(option) != IS_TRUE) {
 			php_error_docref(NULL, E_WARNING, "Option \"remove_all_path\" must be of type bool, %s given",
@@ -1757,7 +1756,7 @@ static void php_zip_add_from_pattern(INTERNAL_FUNCTION_PARAMETERS, int type) /*
 	size_t  path_len = 1;
 	zend_long glob_flags = 0;
 	HashTable *options = NULL;
-	zip_options opts = {0};
+	zip_options opts = PHP_ZIP_DEFAULT_OPTIONS;
 	int found;
 	zend_string *pattern;

diff --git a/ext/zip/tests/addGlob_default_options.phpt b/ext/zip/tests/addGlob_default_options.phpt
new file mode 100644
index 00000000000..f1951e849e0
--- /dev/null
+++ b/ext/zip/tests/addGlob_default_options.phpt
@@ -0,0 +1,79 @@
+--TEST--
+ZipArchive::addGlob() uses the default options when none are supplied
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$dir = __DIR__ . '/addglob_default_options_dir';
+@mkdir($dir);
+$src = $dir . '/data.txt';
+file_put_contents($src, str_repeat('The quick brown fox. ', 3000));
+$archive = $dir . '/test.zip';
+
+function stat_first(string $archive): array
+{
+    $zip = new ZipArchive();
+    $zip->open($archive);
+    $sb = $zip->statIndex(0);
+    $zip->close();
+
+    return $sb;
+}
+
+function add_glob(string $archive, string $pattern, ?array $options): bool
+{
+    $zip = new ZipArchive();
+    $zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+    $added = $options === null
+        ? $zip->addGlob($pattern)
+        : $zip->addGlob($pattern, 0, $options);
+    $zip->close();
+
+    return is_array($added);
+}
+
+/* comp_method defaults to "leave it to libzip", i.e. deflate, not CM_STORE. */
+foreach (['no options' => null, 'empty options' => []] as $label => $options) {
+    echo "-- $label --", PHP_EOL;
+    @unlink($archive);
+    var_dump(add_glob($archive, $dir . '/*.txt', $options));
+    $sb = stat_first($archive);
+    var_dump($sb['comp_method'] === ZipArchive::CM_DEFLATE);
+    var_dump($sb['comp_size'] < $sb['size']);
+}
+
+/* flags defaults to FL_OVERWRITE, so an existing entry is replaced. */
+echo '-- overwrites an existing entry --', PHP_EOL;
+@unlink($archive);
+$zip = new ZipArchive();
+$zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString($src, 'placeholder');
+var_dump(is_array($zip->addGlob($dir . '/*.txt')));
+$zip->close();
+
+$zip = new ZipArchive();
+$zip->open($archive);
+var_dump($zip->numFiles);
+var_dump($zip->getFromName($src) === file_get_contents($src));
+$zip->close();
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/addglob_default_options_dir';
+@unlink($dir . '/test.zip');
+@unlink($dir . '/data.txt');
+@rmdir($dir);
+?>
+--EXPECT--
+-- no options --
+bool(true)
+bool(true)
+bool(true)
+-- empty options --
+bool(true)
+bool(true)
+bool(true)
+-- overwrites an existing entry --
+bool(true)
+int(1)
+bool(true)
diff --git a/ext/zip/tests/addPattern_default_options.phpt b/ext/zip/tests/addPattern_default_options.phpt
new file mode 100644
index 00000000000..404e47e5291
--- /dev/null
+++ b/ext/zip/tests/addPattern_default_options.phpt
@@ -0,0 +1,79 @@
+--TEST--
+ZipArchive::addPattern() uses the default options when none are supplied
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$dir = __DIR__ . DIRECTORY_SEPARATOR . 'addpattern_default_options_dir';
+@mkdir($dir);
+$src = $dir . DIRECTORY_SEPARATOR . 'data.txt';
+file_put_contents($src, str_repeat('The quick brown fox. ', 3000));
+$archive = $dir . DIRECTORY_SEPARATOR . 'test.zip';
+
+function stat_first(string $archive): array
+{
+    $zip = new ZipArchive();
+    $zip->open($archive);
+    $sb = $zip->statIndex(0);
+    $zip->close();
+
+    return $sb;
+}
+
+function add_pattern(string $archive, string $pattern, string $path, ?array $options): bool
+{
+    $zip = new ZipArchive();
+    $zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+    $added = $options === null
+        ? $zip->addPattern($pattern, $path)
+        : $zip->addPattern($pattern, $path, $options);
+    $zip->close();
+
+    return is_array($added);
+}
+
+/* comp_method defaults to "leave it to libzip", i.e. deflate, not CM_STORE. */
+foreach (['no options' => null, 'empty options' => []] as $label => $options) {
+    echo "-- $label --", PHP_EOL;
+    @unlink($archive);
+    var_dump(add_pattern($archive, '/^data\.txt$/', $dir, $options));
+    $sb = stat_first($archive);
+    var_dump($sb['comp_method'] === ZipArchive::CM_DEFLATE);
+    var_dump($sb['comp_size'] < $sb['size']);
+}
+
+/* flags defaults to FL_OVERWRITE, so an existing entry is replaced. */
+echo '-- overwrites an existing entry --', PHP_EOL;
+@unlink($archive);
+$zip = new ZipArchive();
+$zip->open($archive, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString($src, 'placeholder');
+var_dump(is_array($zip->addPattern('/^data\.txt$/', $dir)));
+$zip->close();
+
+$zip = new ZipArchive();
+$zip->open($archive);
+var_dump($zip->numFiles);
+var_dump($zip->getFromIndex(0) === file_get_contents($src));
+$zip->close();
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . DIRECTORY_SEPARATOR . 'addpattern_default_options_dir';
+@unlink($dir . DIRECTORY_SEPARATOR . 'test.zip');
+@unlink($dir . DIRECTORY_SEPARATOR . 'data.txt');
+@rmdir($dir);
+?>
+--EXPECT--
+-- no options --
+bool(true)
+bool(true)
+bool(true)
+-- empty options --
+bool(true)
+bool(true)
+bool(true)
+-- overwrites an existing entry --
+bool(true)
+int(1)
+bool(true)