Skip to content

Commit c9008f6

Browse files
committed
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] theseer/Autoload#114.
1 parent 521c0c5 commit c9008f6

File tree

13 files changed

+608
-25
lines changed

13 files changed

+608
-25
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ PHP NEWS
3939
. Support reference values in Phar::mungServer(). (ndossche)
4040
. Invalid values now throw in Phar::mungServer() instead of being silently
4141
ignored. (ndossche)
42+
. Support overridden methods in SplFileInfo for getMTime() and getPathname()
43+
when building a phar. (ndossche)
4244

4345
- Reflection:
4446
. Fixed bug GH-20217 (ReflectionClass::isIterable() incorrectly returns true

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ PHP 8.6 UPGRADE NOTES
3737
IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
3838
It is supported from icu 63.
3939

40+
- Phar:
41+
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
42+
influences the result of the phar buildFrom family of functions.
43+
This makes it possible to override the timestamp and names of files.
44+
4045
- Streams:
4146
. Added stream socket context option so_reuseaddr that allows disabling
4247
address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on

ext/phar/phar_internal.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ typedef struct _phar_metadata_tracker {
196196
typedef struct _phar_entry_info {
197197
/* first bytes are exactly as in file */
198198
uint32_t uncompressed_filesize;
199+
/* modification time */
199200
uint32_t timestamp;
200201
uint32_t compressed_filesize;
201202
uint32_t crc32;
@@ -464,7 +465,7 @@ void phar_entry_delref(phar_entry_data *idata);
464465

465466
phar_entry_info *phar_get_entry_info(phar_archive_data *phar, char *path, size_t path_len, char **error, bool security);
466467
phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, size_t path_len, char dir, char **error, bool security);
467-
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);
468+
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);
468469
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);
469470
ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *archive, zend_string *user_stub, bool is_default_stub, char **error);
470471
ZEND_ATTRIBUTE_NONNULL int phar_flush(phar_archive_data *archive, char **error);

ext/phar/phar_object.c

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,44 @@ struct _phar_t {
13411341
int count;
13421342
};
13431343

1344+
static zend_always_inline void phar_call_method_with_unwrap(zend_object *obj, const char *name, zval *rv)
1345+
{
1346+
zend_call_method_with_0_params(obj, obj->ce, NULL, name, rv);
1347+
if (Z_ISREF_P(rv)) {
1348+
zend_unwrap_reference(rv);
1349+
}
1350+
}
1351+
1352+
/* This is the same as phar_get_or_create_entry_data(), but allows overriding metadata via SplFileInfo. */
1353+
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)
1354+
{
1355+
uint32_t timestamp;
1356+
1357+
/* Expects an instance of SplFileInfo if it is an object, which is verified in phar_build(). */
1358+
if (Z_TYPE_P(file_info) == IS_OBJECT && Z_OBJCE_P(file_info)->type == ZEND_USER_CLASS) {
1359+
zval rv;
1360+
phar_call_method_with_unwrap(Z_OBJ_P(file_info), "getMTime", &rv);
1361+
1362+
if (UNEXPECTED(Z_TYPE(rv) != IS_LONG)) {
1363+
/* Either it's a tentative type failure, an exception happened, or the function returned false to indicate failure. */
1364+
*error = estrdup("getMTime() must return an int");
1365+
return NULL;
1366+
}
1367+
1368+
/* Sanity check bounds. See GH-14141. */
1369+
if (ZEND_LONG_UINT_OVFL(Z_LVAL(rv))) {
1370+
*error = estrdup("timestamp is limited to 32-bit");
1371+
return NULL;
1372+
}
1373+
1374+
timestamp = Z_LVAL(rv);
1375+
} else {
1376+
timestamp = time(NULL);
1377+
}
1378+
1379+
return phar_get_or_create_entry_data(fname, fname_len, path, path_len, "w+b", 0, error, true, timestamp);
1380+
}
1381+
13441382
static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
13451383
{
13461384
zval *value;
@@ -1351,7 +1389,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
13511389
php_stream *fp;
13521390
size_t fname_len;
13531391
size_t contents_len;
1354-
char *fname, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL;
1392+
char *fname = NULL, *error = NULL, *base = ZSTR_VAL(p_obj->base), *save = NULL, *temp = NULL;
13551393
zend_string *opened;
13561394
char *str_key;
13571395
zend_class_entry *ce = p_obj->c;
@@ -1418,26 +1456,49 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
14181456
return ZEND_HASH_APPLY_STOP;
14191457
}
14201458

1421-
switch (intern->type) {
1422-
case SPL_FS_DIR: {
1423-
char *tmp;
1424-
zend_string *test_str = spl_filesystem_object_get_path(intern);
1425-
fname_len = spprintf(&tmp, 0, "%s%c%s", ZSTR_VAL(test_str), DEFAULT_SLASH, intern->u.dir.entry.d_name);
1426-
zend_string_release_ex(test_str, /* persistent */ false);
1427-
if (php_stream_stat_path(tmp, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) {
1428-
/* ignore directories */
1429-
efree(tmp);
1430-
return ZEND_HASH_APPLY_KEEP;
1459+
zend_string *tmp_dir_str = NULL;
1460+
1461+
/* Take into account that SplFileObject may be overridden.
1462+
* The purpose here is to grab the path name of the file to add. */
1463+
if (Z_OBJCE_P(value)->type == ZEND_USER_CLASS) {
1464+
zval rv;
1465+
phar_call_method_with_unwrap(Z_OBJ_P(value), "getPathname", &rv);
1466+
1467+
if (UNEXPECTED(Z_TYPE(rv) != IS_STRING)) {
1468+
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "getPathname() must return a string");
1469+
return ZEND_HASH_APPLY_STOP;
1470+
}
1471+
tmp_dir_str = Z_STR(rv);
1472+
} else {
1473+
/* Not a user class, so we can grab the data internally quickly. */
1474+
switch (intern->type) {
1475+
case SPL_FS_DIR: {
1476+
zend_string *test_str = spl_filesystem_object_get_path(intern);
1477+
const char slash = DEFAULT_SLASH;
1478+
tmp_dir_str = zend_string_concat3(
1479+
ZSTR_VAL(test_str), ZSTR_LEN(test_str),
1480+
&slash, 1,
1481+
intern->u.dir.entry.d_name, strlen(intern->u.dir.entry.d_name)
1482+
);
1483+
zend_string_release_ex(test_str, /* persistent */ false);
1484+
break;
14311485
}
1486+
case SPL_FS_INFO:
1487+
case SPL_FS_FILE:
1488+
fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL);
1489+
break;
1490+
}
1491+
}
14321492

1433-
fname = expand_filepath(tmp, NULL);
1434-
efree(tmp);
1435-
break;
1493+
if (tmp_dir_str) {
1494+
if (php_stream_stat_path(ZSTR_VAL(tmp_dir_str), &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) {
1495+
/* ignore directories */
1496+
zend_string_release_ex(tmp_dir_str, /* persistent */ false);
1497+
return ZEND_HASH_APPLY_KEEP;
14361498
}
1437-
case SPL_FS_INFO:
1438-
case SPL_FS_FILE:
1439-
fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL);
1440-
break;
1499+
1500+
fname = expand_filepath(ZSTR_VAL(tmp_dir_str), NULL);
1501+
zend_string_release_ex(tmp_dir_str, /* persistent */ false);
14411502
}
14421503

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

1581-
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))) {
1642+
if (!(data = phar_build_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, &error, value))) {
15821643
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error);
15831644
efree(error);
15841645

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

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

3611-
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))) {
3672+
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)))) {
36123673
if (error) {
36133674
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Directory %s does not exist and cannot be created: %s", ZSTR_VAL(dir_name), error);
36143675
efree(error);

ext/phar/stream.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const cha
191191
/* strip leading "/" */
192192
internal_file = estrndup(ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1);
193193
if (mode[0] == 'w' || (mode[0] == 'r' && mode[1] == '+')) {
194-
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))) {
194+
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)))) {
195195
if (error) {
196196
php_stream_wrapper_log_error(wrapper, options, "%s", error);
197197
efree(error);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
--TEST--
2+
buildFromIterator with user overrides - getMTime()
3+
--EXTENSIONS--
4+
phar
5+
--INI--
6+
phar.readonly=0
7+
phar.require_hash=0
8+
--CREDITS--
9+
Arne Blankerts
10+
N. Dossche
11+
--FILE--
12+
<?php
13+
14+
class MySplFileInfo extends SplFileInfo {
15+
public function getMTime(): int|false {
16+
echo "[MTime]\n";
17+
return 123;
18+
}
19+
20+
public function getCTime(): int {
21+
// This should not get called
22+
echo "[CTime]\n";
23+
return 0;
24+
}
25+
26+
public function getATime(): int {
27+
// This should not get called
28+
echo "[ATime]\n";
29+
return 0;
30+
}
31+
}
32+
33+
class MyIterator extends RecursiveDirectoryIterator {
34+
public function current(): SplFileInfo {
35+
echo "[ Found: " . parent::current()->getPathname() . " ]\n";
36+
return new MySplFileInfo(parent::current()->getPathname());
37+
}
38+
}
39+
40+
$workdir = __DIR__.'/getMTime';
41+
mkdir($workdir . '/content', recursive: true);
42+
file_put_contents($workdir . '/content/hello.txt', "Hello world.");
43+
44+
$phar = new \Phar($workdir . '/test.phar');
45+
$phar->startBuffering();
46+
$phar->buildFromIterator(
47+
new RecursiveIteratorIterator(
48+
new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
49+
),
50+
$workdir
51+
);
52+
$phar->stopBuffering();
53+
54+
55+
$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
56+
var_dump($result['content/hello.txt']);
57+
var_dump($result['content/hello.txt']->getATime());
58+
var_dump($result['content/hello.txt']->getMTime());
59+
var_dump($result['content/hello.txt']->getCTime());
60+
61+
?>
62+
--CLEAN--
63+
<?php
64+
$workdir = __DIR__.'/getMTime';
65+
@unlink($workdir . '/test.phar');
66+
@unlink($workdir . '/content/hello.txt');
67+
@rmdir($workdir . '/content');
68+
@rmdir($workdir);
69+
?>
70+
--EXPECTF--
71+
[ Found: %shello.txt ]
72+
[MTime]
73+
object(PharFileInfo)#%d (2) {
74+
["pathName":"SplFileInfo":private]=>
75+
string(%d) "phar://%shello.txt"
76+
["fileName":"SplFileInfo":private]=>
77+
string(%d) "hello.txt"
78+
}
79+
int(123)
80+
int(123)
81+
int(123)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
--TEST--
2+
buildFromIterator with user overrides - getMTime() by ref
3+
--EXTENSIONS--
4+
phar
5+
--INI--
6+
phar.readonly=0
7+
phar.require_hash=0
8+
--CREDITS--
9+
Arne Blankerts
10+
N. Dossche
11+
--FILE--
12+
<?php
13+
14+
class MySplFileInfo extends SplFileInfo {
15+
public function &getMTime(): int|false {
16+
static $data = 123;
17+
echo "[MTime]\n";
18+
return $data;
19+
}
20+
}
21+
22+
class MyIterator extends RecursiveDirectoryIterator {
23+
public function current(): SplFileInfo {
24+
echo "[ Found: " . parent::current()->getPathname() . " ]\n";
25+
return new MySplFileInfo(parent::current()->getPathname());
26+
}
27+
}
28+
29+
$workdir = __DIR__.'/getMTime_byRef';
30+
mkdir($workdir . '/content', recursive: true);
31+
file_put_contents($workdir . '/content/hello.txt', "Hello world.");
32+
33+
$phar = new \Phar($workdir . '/test.phar');
34+
$phar->startBuffering();
35+
$phar->buildFromIterator(
36+
new RecursiveIteratorIterator(
37+
new MyIterator($workdir . '/content', FilesystemIterator::SKIP_DOTS)
38+
),
39+
$workdir
40+
);
41+
$phar->stopBuffering();
42+
43+
44+
$result = new \Phar($workdir . '/test.phar', 0, 'test.phar');
45+
var_dump($result['content/hello.txt']);
46+
var_dump($result['content/hello.txt']->getATime());
47+
var_dump($result['content/hello.txt']->getMTime());
48+
var_dump($result['content/hello.txt']->getCTime());
49+
50+
?>
51+
--CLEAN--
52+
<?php
53+
$workdir = __DIR__.'/getMTime_byRef';
54+
@unlink($workdir . '/test.phar');
55+
@unlink($workdir . '/content/hello.txt');
56+
@rmdir($workdir . '/content');
57+
@rmdir($workdir);
58+
?>
59+
--EXPECTF--
60+
[ Found: %shello.txt ]
61+
[MTime]
62+
object(PharFileInfo)#%d (2) {
63+
["pathName":"SplFileInfo":private]=>
64+
string(%d) "phar://%shello.txt"
65+
["fileName":"SplFileInfo":private]=>
66+
string(%d) "hello.txt"
67+
}
68+
int(123)
69+
int(123)
70+
int(123)

0 commit comments

Comments
 (0)