Commit c9008f6dd86 for php.net

commit c9008f6dd867c4d6be9e9f5f6d0bb51fc1e4cbe4
Author: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date:   Sun May 5 20:58:06 2024 +0200

    Make buildFromIterator() work with custom SplFileInfo objects

    While it is possible to return a custom SplFileInfo object in the
    iterator used by buildFromIterator(), the data is not actually used from
    that object, instead the data from the underlying internal structure is
    used. This makes it impossible to override some metadata such as the
    path name and modification time.

    The main motivation comes from two reasons:
    - Consistency. We expect our custom methods to be called when having a
      custom object.
    - Support reproducibility. This is the original use case as requested in
      [1].

    Add support for this by calling the getMTime() and getPathname() methods
    if they're overriden by a user class.

    [1] https://github.com/theseer/Autoload/issues/114.

diff --git a/NEWS b/NEWS
index f2e81284bdf..671d2b57a67 100644
--- a/NEWS
+++ b/NEWS
@@ -39,6 +39,8 @@ PHP                                                                        NEWS
   . Support reference values in Phar::mungServer(). (ndossche)
   . Invalid values now throw in Phar::mungServer() instead of being silently
     ignored. (ndossche)
+  . Support overridden methods in SplFileInfo for getMTime() and getPathname()
+    when building a phar. (ndossche)

 - Reflection:
   . Fixed bug GH-20217 (ReflectionClass::isIterable() incorrectly returns true
diff --git a/UPGRADING b/UPGRADING
index 7f0fcaf8943..4e511d09479 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -37,6 +37,11 @@ PHP 8.6 UPGRADE NOTES
     IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
     It is supported from icu 63.

+- Phar:
+  . Overriding the getMTime() and getPathname() methods of SplFileInfo now
+    influences the result of the phar buildFrom family of functions.
+    This makes it possible to override the timestamp and names of files.
+
 - Streams:
   . Added stream socket context option so_reuseaddr that allows disabling
     address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h
index 5fc43545453..144a3c1359f 100644
--- a/ext/phar/phar_internal.h
+++ b/ext/phar/phar_internal.h
@@ -196,6 +196,7 @@ typedef struct _phar_metadata_tracker {
 typedef struct _phar_entry_info {
 	/* first bytes are exactly as in file */
 	uint32_t                 uncompressed_filesize;
+	/* modification time */
 	uint32_t                 timestamp;
 	uint32_t                 compressed_filesize;
 	uint32_t                 crc32;
@@ -464,7 +465,7 @@ void phar_entry_delref(phar_entry_data *idata);

 phar_entry_info *phar_get_entry_info(phar_archive_data *phar, char *path, size_t path_len, char **error, bool security);
 phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, size_t path_len, char dir, char **error, bool security);
-ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security);
+ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security, uint32_t timestamp);
 ZEND_ATTRIBUTE_NONNULL zend_result phar_get_entry_data(phar_entry_data **ret, char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security);
 ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *archive, zend_string *user_stub, bool is_default_stub, char **error);
 ZEND_ATTRIBUTE_NONNULL int phar_flush(phar_archive_data *archive, char **error);
diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c
index 9f0d566b427..cd00880a0bf 100644
--- a/ext/phar/phar_object.c
+++ b/ext/phar/phar_object.c
@@ -1341,6 +1341,44 @@ struct _phar_t {
 	int count;
 };

+static zend_always_inline void phar_call_method_with_unwrap(zend_object *obj, const char *name, zval *rv)
+{
+	zend_call_method_with_0_params(obj, obj->ce, NULL, name, rv);
+	if (Z_ISREF_P(rv)) {
+		zend_unwrap_reference(rv);
+	}
+}
+
+/* This is the same as phar_get_or_create_entry_data(), but allows overriding metadata via SplFileInfo. */
+static phar_entry_data *phar_build_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, char **error, zval *file_info)
+{
+	uint32_t timestamp;
+
+	/* Expects an instance of SplFileInfo if it is an object, which is verified in phar_build(). */
+	if (Z_TYPE_P(file_info) == IS_OBJECT && Z_OBJCE_P(file_info)->type == ZEND_USER_CLASS) {
+		zval rv;
+		phar_call_method_with_unwrap(Z_OBJ_P(file_info), "getMTime", &rv);
+
+		if (UNEXPECTED(Z_TYPE(rv) != IS_LONG)) {
+			/* Either it's a tentative type failure, an exception happened, or the function returned false to indicate failure. */
+			*error = estrdup("getMTime() must return an int");
+			return NULL;
+		}
+
+		/* Sanity check bounds. See GH-14141. */
+		if (ZEND_LONG_UINT_OVFL(Z_LVAL(rv))) {
+			*error = estrdup("timestamp is limited to 32-bit");
+			return NULL;
+		}
+
+		timestamp = Z_LVAL(rv);
+	} else {
+		timestamp = time(NULL);
+	}
+
+	return phar_get_or_create_entry_data(fname, fname_len, path, path_len, "w+b", 0, error, true, timestamp);
+}
+
 static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
 {
 	zval *value;
@@ -1351,7 +1389,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
 	php_stream *fp;
 	size_t fname_len;
 	size_t contents_len;
-	char *fname, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL;
+	char *fname = NULL, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL;
 	zend_string *opened;
 	char *str_key;
 	zend_class_entry *ce = p_obj->c;
@@ -1418,26 +1456,49 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
 					return ZEND_HASH_APPLY_STOP;
 				}

-				switch (intern->type) {
-					case SPL_FS_DIR: {
-						char *tmp;
-						zend_string *test_str = spl_filesystem_object_get_path(intern);
-						fname_len = spprintf(&tmp, 0, "%s%c%s", ZSTR_VAL(test_str), DEFAULT_SLASH, intern->u.dir.entry.d_name);
-						zend_string_release_ex(test_str, /* persistent */ false);
-						if (php_stream_stat_path(tmp, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) {
-							/* ignore directories */
-							efree(tmp);
-							return ZEND_HASH_APPLY_KEEP;
+				zend_string *tmp_dir_str = NULL;
+
+				/* Take into account that SplFileObject may be overridden.
+				 * The purpose here is to grab the path name of the file to add. */
+				if (Z_OBJCE_P(value)->type == ZEND_USER_CLASS) {
+					zval rv;
+					phar_call_method_with_unwrap(Z_OBJ_P(value), "getPathname", &rv);
+
+					if (UNEXPECTED(Z_TYPE(rv) != IS_STRING)) {
+						zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "getPathname() must return a string");
+						return ZEND_HASH_APPLY_STOP;
+					}
+					tmp_dir_str = Z_STR(rv);
+				} else {
+					/* Not a user class, so we can grab the data internally quickly. */
+					switch (intern->type) {
+						case SPL_FS_DIR: {
+							zend_string *test_str = spl_filesystem_object_get_path(intern);
+							const char slash = DEFAULT_SLASH;
+							tmp_dir_str = zend_string_concat3(
+								ZSTR_VAL(test_str), ZSTR_LEN(test_str),
+								&slash, 1,
+								intern->u.dir.entry.d_name, strlen(intern->u.dir.entry.d_name)
+							);
+							zend_string_release_ex(test_str, /* persistent */ false);
+							break;
 						}
+						case SPL_FS_INFO:
+						case SPL_FS_FILE:
+							fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL);
+							break;
+					}
+				}

-						fname = expand_filepath(tmp, NULL);
-						efree(tmp);
-						break;
+				if (tmp_dir_str) {
+					if (php_stream_stat_path(ZSTR_VAL(tmp_dir_str), &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) {
+						/* ignore directories */
+						zend_string_release_ex(tmp_dir_str, /* persistent */ false);
+						return ZEND_HASH_APPLY_KEEP;
 					}
-					case SPL_FS_INFO:
-					case SPL_FS_FILE:
-						fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL);
-						break;
+
+					fname = expand_filepath(ZSTR_VAL(tmp_dir_str), NULL);
+					zend_string_release_ex(tmp_dir_str, /* persistent */ false);
 				}

 				if (!fname) {
@@ -1578,7 +1639,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
 		return ZEND_HASH_APPLY_KEEP;
 	}

-	if (!(data = phar_get_or_create_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, "w+b", 0, &error, true))) {
+	if (!(data = phar_build_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, &error, value))) {
 		zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error);
 		efree(error);

@@ -3536,7 +3597,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con
 	}
 #endif

-	if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, filename, filename_len, "w+b", 0, &error, true))) {
+	if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, filename, filename_len, "w+b", 0, &error, true, time(NULL)))) {
 		if (error) {
 			zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s does not exist and cannot be created: %s", filename, error);
 			efree(error);
@@ -3608,7 +3669,7 @@ static void phar_mkdir(phar_archive_data **pphar, zend_string *dir_name)
 	char *error;
 	phar_entry_data *data;

-	if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, ZSTR_VAL(dir_name), ZSTR_LEN(dir_name), "w+b", 2, &error, true))) {
+	if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, ZSTR_VAL(dir_name), ZSTR_LEN(dir_name), "w+b", 2, &error, true, time(NULL)))) {
 		if (error) {
 			zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Directory %s does not exist and cannot be created: %s", ZSTR_VAL(dir_name), error);
 			efree(error);
diff --git a/ext/phar/stream.c b/ext/phar/stream.c
index bb60f00d8f1..08da1847cd9 100644
--- a/ext/phar/stream.c
+++ b/ext/phar/stream.c
@@ -191,7 +191,7 @@ static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const cha
 	/* strip leading "/" */
 	internal_file = estrndup(ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1);
 	if (mode[0] == 'w' || (mode[0] == 'r' && mode[1] == '+')) {
-		if (NULL == (idata = phar_get_or_create_entry_data(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), mode, 0, &error, true))) {
+		if (NULL == (idata = phar_get_or_create_entry_data(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), mode, 0, &error, true, time(NULL)))) {
 			if (error) {
 				php_stream_wrapper_log_error(wrapper, options, "%s", error);
 				efree(error);
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt
new file mode 100644
index 00000000000..eefe352a0cb
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime.phpt
@@ -0,0 +1,81 @@
+--TEST--
+buildFromIterator with user overrides - getMTime()
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MySplFileInfo extends SplFileInfo {
+    public function getMTime(): int|false {
+        echo "[MTime]\n";
+        return 123;
+    }
+
+    public function getCTime(): int {
+        // This should not get called
+        echo "[CTime]\n";
+        return 0;
+    }
+
+    public function getATime(): int {
+        // This should not get called
+        echo "[ATime]\n";
+        return 0;
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MySplFileInfo(parent::current()->getPathname());
+    }
+}
+
+$workdir = __DIR__.'/getMTime';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world.");
+
+$phar = new \Phar($workdir . '/test.phar');
+$phar->startBuffering();
+$phar->buildFromIterator(
+    new RecursiveIteratorIterator(
+        new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+    ),
+    $workdir
+);
+$phar->stopBuffering();
+
+
+$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
+var_dump($result['content/hello.txt']);
+var_dump($result['content/hello.txt']->getATime());
+var_dump($result['content/hello.txt']->getMTime());
+var_dump($result['content/hello.txt']->getCTime());
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getMTime';
+@unlink($workdir . '/test.phar');
+@unlink($workdir . '/content/hello.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %shello.txt ]
+[MTime]
+object(PharFileInfo)#%d (2) {
+  ["pathName":"SplFileInfo":private]=>
+  string(%d) "phar://%shello.txt"
+  ["fileName":"SplFileInfo":private]=>
+  string(%d) "hello.txt"
+}
+int(123)
+int(123)
+int(123)
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt
new file mode 100644
index 00000000000..a7f81709a34
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_byRef.phpt
@@ -0,0 +1,70 @@
+--TEST--
+buildFromIterator with user overrides - getMTime() by ref
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MySplFileInfo extends SplFileInfo {
+    public function &getMTime(): int|false {
+        static $data = 123;
+        echo "[MTime]\n";
+        return $data;
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MySplFileInfo(parent::current()->getPathname());
+    }
+}
+
+$workdir = __DIR__.'/getMTime_byRef';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world.");
+
+$phar = new \Phar($workdir . '/test.phar');
+$phar->startBuffering();
+$phar->buildFromIterator(
+    new RecursiveIteratorIterator(
+        new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+    ),
+    $workdir
+);
+$phar->stopBuffering();
+
+
+$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
+var_dump($result['content/hello.txt']);
+var_dump($result['content/hello.txt']->getATime());
+var_dump($result['content/hello.txt']->getMTime());
+var_dump($result['content/hello.txt']->getCTime());
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getMTime_byRef';
+@unlink($workdir . '/test.phar');
+@unlink($workdir . '/content/hello.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %shello.txt ]
+[MTime]
+object(PharFileInfo)#%d (2) {
+  ["pathName":"SplFileInfo":private]=>
+  string(%d) "phar://%shello.txt"
+  ["fileName":"SplFileInfo":private]=>
+  string(%d) "hello.txt"
+}
+int(123)
+int(123)
+int(123)
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt
new file mode 100644
index 00000000000..f43a7a496a7
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getMTime_errors.phpt
@@ -0,0 +1,117 @@
+--TEST--
+buildFromIterator with user overrides - errors in getMTime()
+--EXTENSIONS--
+phar
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip 64-bit only");
+?>
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MySplFileInfo1 extends SplFileInfo {
+    public function getMTime(): int|false {
+        echo "[MTime]\n";
+        return PHP_INT_MAX;
+    }
+}
+
+class MySplFileInfo2 extends SplFileInfo {
+    public function getMTime(): int|false {
+        echo "[MTime]\n";
+        return false;
+    }
+}
+
+class MySplFileInfo3 extends SplFileInfo {
+    public function getMTime(): int|false {
+        echo "[MTime]\n";
+        throw new Error('Throwing an exception inside getMTime()');
+    }
+}
+
+class MySplFileInfo4 extends SplFileInfo {
+    #[\ReturnTypeWillChange]
+    public function getMTime(): string {
+        echo "[MTime]\n";
+        return "wrong type";
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        static $counter = 0;
+        $counter++;
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        if ($counter === 1) {
+            return new MySplFileInfo1(parent::current()->getPathname());
+        } else if ($counter === 2) {
+            return new MySplFileInfo2(parent::current()->getPathname());
+        } else if ($counter === 3) {
+            return new MySplFileInfo3(parent::current()->getPathname());
+        } else if ($counter === 4) {
+            return new MySplFileInfo4(parent::current()->getPathname());
+        }
+    }
+}
+
+$workdir = __DIR__.'/getMTime_errors';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world.");
+
+for ($i = 0; $i < 4; $i++) {
+    echo "--- Iteration $i ---\n";
+    try {
+        $phar = new \Phar($workdir . "/test$i.phar");
+        $phar->startBuffering();
+        $phar->buildFromIterator(
+            new RecursiveIteratorIterator(
+                new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+            ),
+            $workdir
+        );
+        $phar->stopBuffering();
+    } catch (Throwable $e) {
+        echo $e->getMessage(), "\n";
+        if ($previous = $e->getPrevious()) {
+            echo "Previous: ", $previous->getMessage(), "\n";
+        }
+    }
+}
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getMTime_errors';
+@unlink($workdir . '/content/hello.txt');
+@unlink($workdir . '/test3.phar');
+@unlink($workdir . '/test2.phar');
+@unlink($workdir . '/test1.phar');
+@unlink($workdir . '/test0.phar');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+--- Iteration 0 ---
+[ Found: %shello.txt ]
+[MTime]
+Entry content%chello.txt cannot be created: timestamp is limited to 32-bit
+--- Iteration 1 ---
+[ Found: %shello.txt ]
+[MTime]
+Entry content%chello.txt cannot be created: getMTime() must return an int
+--- Iteration 2 ---
+[ Found: %shello.txt ]
+[MTime]
+Entry content%chello.txt cannot be created: getMTime() must return an int
+Previous: Throwing an exception inside getMTime()
+--- Iteration 3 ---
+[ Found: %shello.txt ]
+[MTime]
+Entry content%chello.txt cannot be created: getMTime() must return an int
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt
new file mode 100644
index 00000000000..144eed1d874
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname.phpt
@@ -0,0 +1,67 @@
+--TEST--
+buildFromIterator with user overrides - getPathname()
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MyGlobIterator extends GlobIterator {
+    public function getPathname(): string {
+        echo "[getPathname]\n";
+        var_dump(parent::getPathname());
+        // For testing: always return hello2 such that it will be the only file
+        return str_replace("hello1.txt", "hello2.txt", parent::getPathname());
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MyGlobIterator(parent::current()->getPath() . '/*');
+    }
+}
+
+$workdir = __DIR__.'/getPathname';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello1.txt', "Hello world 1.");
+file_put_contents($workdir . '/content/hello2.txt', "Hello world 2.");
+
+$phar = new \Phar($workdir . "/test.phar");
+$phar->startBuffering();
+$phar->buildFromIterator(
+    new RecursiveIteratorIterator(
+        new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+    ),
+    $workdir
+);
+$phar->stopBuffering();
+
+$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
+var_dump(isset($result['content/hello1.txt']));
+var_dump(isset($result['content/hello2.txt']));
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getPathname';
+@unlink($workdir . '/test.phar');
+@unlink($workdir . '/content/hello1.txt');
+@unlink($workdir . '/content/hello2.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %shello%d.txt ]
+[getPathname]
+string(%d) "%shello1.txt"
+[ Found: %shello%d.txt ]
+[getPathname]
+string(%d) "%shello1.txt"
+bool(false)
+bool(true)
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt
new file mode 100644
index 00000000000..0ae2946ce1a
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_byRef.phpt
@@ -0,0 +1,58 @@
+--TEST--
+buildFromIterator with user overrides - getPathname() by ref
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MyGlobIterator extends GlobIterator {
+    public function &getPathname(): string {
+        echo "[getPathname]\n";
+        static $data = parent::getPathname();
+        return $data;
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MyGlobIterator(parent::current()->getPath() . '/*');
+    }
+}
+
+$workdir = __DIR__.'/getPathname_byRef';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world 1.");
+
+$phar = new \Phar($workdir . "/test.phar");
+$phar->startBuffering();
+$phar->buildFromIterator(
+    new RecursiveIteratorIterator(
+        new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+    ),
+    $workdir
+);
+$phar->stopBuffering();
+
+$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
+var_dump(isset($result['content/hello.txt']));
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getPathname_byRef';
+@unlink($workdir . '/test.phar');
+@unlink($workdir . '/content/hello.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %scontent%chello.txt ]
+[getPathname]
+bool(true)
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt
new file mode 100644
index 00000000000..86b66050a97
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_exception.phpt
@@ -0,0 +1,63 @@
+--TEST--
+buildFromIterator with user overrides - exception in getPathname()
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MyGlobIterator extends GlobIterator {
+    public function getPathname(): string {
+        echo "[getPathname]\n";
+        var_dump(parent::getPathname());
+        throw new Error('exception in getPathname()');
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MyGlobIterator(parent::current()->getPath() . '/*');
+    }
+}
+
+$workdir = __DIR__.'/getPathname_exception';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world.");
+
+$phar = new \Phar($workdir . "/test.phar");
+$phar->startBuffering();
+try {
+    $phar->buildFromIterator(
+        new RecursiveIteratorIterator(
+            new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+        ),
+        $workdir
+    );
+} catch (Throwable $e) {
+    echo $e->getMessage(), "\n";
+    if ($previous = $e->getPrevious()) {
+        echo "Previous: ", $previous->getMessage(), "\n";
+    }
+}
+$phar->stopBuffering();
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getPathname_exception';
+@unlink($workdir . '/content/hello.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %shello.txt ]
+[getPathname]
+string(%d) "%shello.txt"
+getPathname() must return a string
+Previous: exception in getPathname()
diff --git a/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt
new file mode 100644
index 00000000000..dfb3fb5f2c5
--- /dev/null
+++ b/ext/phar/tests/buildFromIterator_user_overrides/getPathname_wrong_type.phpt
@@ -0,0 +1,58 @@
+--TEST--
+buildFromIterator with user overrides - wrong return type in getPathname()
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+phar.require_hash=0
+--CREDITS--
+Arne Blankerts
+N. Dossche
+--FILE--
+<?php
+
+class MyGlobIterator extends GlobIterator {
+    #[\ReturnTypeWillChange]
+    public function getPathname(): int {
+        echo "[getPathname]\n";
+        return 1;
+    }
+}
+
+class MyIterator extends RecursiveDirectoryIterator {
+    public function current(): SplFileInfo {
+        echo "[ Found: " . parent::current()->getPathname() . " ]\n";
+        return new MyGlobIterator(parent::current()->getPath() . '/*');
+    }
+}
+
+$workdir = __DIR__.'/getPathname_wrong_type';
+mkdir($workdir . '/content', recursive: true);
+file_put_contents($workdir . '/content/hello.txt', "Hello world.");
+
+$phar = new \Phar($workdir . "/test.phar");
+$phar->startBuffering();
+try {
+    $phar->buildFromIterator(
+        new RecursiveIteratorIterator(
+            new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
+        ),
+        $workdir
+    );
+} catch (Throwable $e) {
+    echo $e->getMessage(), "\n";
+}
+$phar->stopBuffering();
+
+?>
+--CLEAN--
+<?php
+$workdir = __DIR__.'/getPathname_wrong_type';
+@unlink($workdir . '/content/hello.txt');
+@rmdir($workdir . '/content');
+@rmdir($workdir);
+?>
+--EXPECTF--
+[ Found: %scontent%chello.txt ]
+[getPathname]
+getPathname() must return a string
diff --git a/ext/phar/util.c b/ext/phar/util.c
index 60607006f67..82c784e6217 100644
--- a/ext/phar/util.c
+++ b/ext/phar/util.c
@@ -606,7 +606,7 @@ ZEND_ATTRIBUTE_NONNULL zend_result phar_get_entry_data(phar_entry_data **ret, ch
 /**
  * Create a new dummy file slot within a writeable phar for a newly created file
  */
-ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security) /* {{{ */
+ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fname, size_t fname_len, char *path, size_t path_len, const char *mode, char allow_dir, char **error, bool security, uint32_t timestamp) /* {{{ */
 {
 	phar_archive_data *phar;
 	phar_entry_info *entry, etemp;
@@ -668,7 +668,7 @@ ZEND_ATTRIBUTE_NONNULL phar_entry_data *phar_get_or_create_entry_data(char *fnam

 	phar_add_virtual_dirs(phar, path, path_len);
 	etemp.is_modified = 1;
-	etemp.timestamp = time(0);
+	etemp.timestamp = timestamp;
 	etemp.is_crc_checked = 1;
 	etemp.phar = phar;
 	etemp.filename = zend_string_init(path, path_len, false);