Skip to content

Support custom SplFileInfo objects in Phar::buildFromIterator() #14143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 90 additions & 35 deletions ext/phar/phar_object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,44 @@ struct _phar_t {
int count;
};

/* 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)
{
bool override_timestamp = false;
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;
/* Phars only have a single timestamp.
* Use modification time to be consistent with the zip and tar file format. */
zend_call_method_with_0_params(Z_OBJ_P(file_info), Z_OBJCE_P(file_info), NULL, "getMTime", &rv);

if (UNEXPECTED(Z_TYPE(rv) != IS_LONG)) {
/* Either an exception happened, or the function returned false to indicate failure. */
ZEND_ASSERT(Z_TYPE(rv) == IS_UNDEF || Z_TYPE(rv) == IS_FALSE);
*error = estrdup("getMTime() failed");
return NULL;
}

/* Sanity check bounds. See GH-14141. */
if (Z_LVAL(rv) > UINT32_MAX) {
*error = estrdup("timestamp is limited to 32-bit");
return NULL;
}

override_timestamp = true;
timestamp = Z_LVAL(rv);
}

phar_entry_data *data = phar_get_or_create_entry_data(fname, fname_len, path, path_len, "w+b", 0, error, 1);
if (data && override_timestamp) {
data->internal_file->timestamp = timestamp;
}

return data;
}

static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
{
zval *value;
Expand All @@ -1392,7 +1430,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;
Expand Down Expand Up @@ -1452,51 +1490,65 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */
goto after_open_fp;
case IS_OBJECT:
if (instanceof_function(Z_OBJCE_P(value), spl_ce_SplFileInfo)) {
char *test = NULL;
spl_filesystem_object *intern = (spl_filesystem_object*)((char*)Z_OBJ_P(value) - Z_OBJ_P(value)->handlers->offset);

if (!base_len) {
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Iterator %s returns an SplFileInfo object, so base directory must be specified", ZSTR_VAL(ce->name));
return ZEND_HASH_APPLY_STOP;
}

switch (intern->type) {
case SPL_FS_DIR: {
zend_string *test_str = spl_filesystem_object_get_path(intern);
fname_len = spprintf(&fname, 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(fname, &ssb) == 0 && S_ISDIR(ssb.sb.st_mode)) {
/* ignore directories */
efree(fname);
return ZEND_HASH_APPLY_KEEP;
}

test = expand_filepath(fname, NULL);
efree(fname);
zend_string *tmp_dir_str = NULL;

if (test) {
fname = test;
fname_len = strlen(fname);
} else {
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path");
return ZEND_HASH_APPLY_STOP;
/* 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;
zend_call_method_with_0_params(Z_OBJ_P(value), Z_OBJCE_P(value), NULL, "getPathname", &rv);
if (UNEXPECTED(Z_TYPE(rv) != IS_STRING)) {
ZEND_ASSERT(EG(exception));
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;
}
}

save = fname;
goto phar_spl_fileinfo;
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(tmp_dir_str);
return ZEND_HASH_APPLY_KEEP;
}
case SPL_FS_INFO:
case SPL_FS_FILE:
fname = expand_filepath(ZSTR_VAL(intern->file_name), NULL);
if (!fname) {
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path");
return ZEND_HASH_APPLY_STOP;
}

fname_len = strlen(fname);
save = fname;
goto phar_spl_fileinfo;
fname = expand_filepath(ZSTR_VAL(tmp_dir_str), NULL);
zend_string_release_ex(tmp_dir_str, /* persistent */ false);
}

if (!fname) {
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Could not resolve file path");
return ZEND_HASH_APPLY_STOP;
}

fname_len = strlen(fname);
save = fname;
goto phar_spl_fileinfo;
}
ZEND_FALLTHROUGH;
default:
Expand Down Expand Up @@ -1627,8 +1679,11 @@ 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, 1))) {
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error);
if (!(data = phar_build_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, &error, value))) {
if (!EG(exception)) {
/* User methods could've already thrown an exception. */
zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error);
}
efree(error);

if (save) {
Expand Down
87 changes: 87 additions & 0 deletions ext/phar/tests/buildFromIterator_user_overrides/001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
--TEST--
buildFromIterator with user overrides 001 - getMTime()
--EXTENSIONS--
phar
--INI--
phar.readonly=0
phar.require_hash=0
--CREDITS--
Arne Blankerts
Niels Dossche
--FILE--
<?php

class MySplFileInfo extends SplFileInfo {
public function getPathname(): string {
echo "[Pathname]\n";
return parent::getPathname();
}

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__.'/001';
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__.'/001';
@unlink($workdir . '/test.phar');
@unlink($workdir . '/content/hello.txt');
@rmdir($workdir . '/content');
@rmdir($workdir);
?>
--EXPECTF--
[ Found: %shello.txt ]
[Pathname]
[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)
95 changes: 95 additions & 0 deletions ext/phar/tests/buildFromIterator_user_overrides/002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
--TEST--
buildFromIterator with user overrides 002 - 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
Niels 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 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());
}
}
}

$workdir = __DIR__.'/002';
mkdir($workdir . '/content', recursive: true);
file_put_contents($workdir . '/content/hello.txt', "Hello world.");

for ($i = 0; $i < 3; $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";
}
}

?>
--CLEAN--
<?php
$workdir = __DIR__.'/002';
@unlink($workdir . '/content/hello.txt');
@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() failed
--- Iteration 2 ---
[ Found: %shello.txt ]
[MTime]
Throwing an exception inside getMTime()
Loading
Loading