From 26e61596e1cb98823ef3580ed35cc4fd9746bbe6 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Tue, 7 Apr 2026 09:46:26 +1000 Subject: [PATCH] Add ZipArchive::closeString() - Add a $flags parameter to ZipArchive::openString(), by analogy with ZipArchive::open(). This allows the string to be opened read/write. - Have the $data parameter to ZipArchive::openString() default to an empty string, for convenience of callers that want to create an empty archive. This works on all versions of libzip since the change in 1.6.0 only applied to files, it's opt-in for generic sources. - Add ZipArchive::closeString() which closes the archive and returns the resulting string. For consistency with openString(), return an empty string if the archive is empty. --- ext/zip/php_zip.c | 66 +++++++++++++++--- ext/zip/php_zip.h | 2 + ext/zip/php_zip.stub.php | 4 +- ext/zip/php_zip_arginfo.h | 12 +++- .../tests/ZipArchive_closeString_basic.phpt | 25 +++++++ .../tests/ZipArchive_closeString_error.phpt | 47 +++++++++++++ .../tests/ZipArchive_closeString_false.phpt | 22 ++++++ .../ZipArchive_closeString_variation.phpt | 28 ++++++++ ext/zip/tests/ZipArchive_openString.phpt | 25 ++++++- ext/zip/tests/checkcons.zip | Bin 0 -> 181 bytes ext/zip/tests/wrong-file-size.zip | Bin 0 -> 110 bytes 11 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 ext/zip/tests/ZipArchive_closeString_basic.phpt create mode 100644 ext/zip/tests/ZipArchive_closeString_error.phpt create mode 100644 ext/zip/tests/ZipArchive_closeString_false.phpt create mode 100644 ext/zip/tests/ZipArchive_closeString_variation.phpt create mode 100644 ext/zip/tests/checkcons.zip create mode 100644 ext/zip/tests/wrong-file-size.zip diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index a8289a41a304..30ed56947b53 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -573,8 +573,10 @@ static char * php_zipobj_get_zip_comment(ze_zip_object *obj, int *len) /* {{{ */ } /* }}} */ -/* Close and free the zip_t */ -static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */ +/* Close and free the zip_t. If out_str is non-null and the archive was opened + * as a string, the final contents of the archive will be assigned to *out_str + * and that string will afterwards be owned by the caller. */ +static bool php_zipobj_close(ze_zip_object *obj, zend_string **out_str) /* {{{ */ { struct zip *intern = obj->za; bool success = false; @@ -606,7 +608,17 @@ static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */ obj->filename_len = 0; } + if (obj->out_str) { + if (out_str) { + *out_str = obj->out_str; + } else { + zend_string_release(obj->out_str); + } + obj->out_str = NULL; + } + obj->za = NULL; + obj->from_string = false; return success; } /* }}} */ @@ -1060,7 +1072,7 @@ static void php_zip_object_free_storage(zend_object *object) /* {{{ */ { ze_zip_object * intern = php_zip_fetch_object(object); - php_zipobj_close(intern); + php_zipobj_close(intern, NULL); #ifdef HAVE_PROGRESS_CALLBACK /* if not properly called by libzip */ @@ -1467,7 +1479,7 @@ PHP_METHOD(ZipArchive, open) } /* If we already have an opened zip, free it */ - php_zipobj_close(ze_obj); + php_zipobj_close(ze_obj, NULL); /* open for write without option to empty the archive */ if ((flags & (ZIP_TRUNCATE | ZIP_RDONLY)) == 0) { @@ -1491,28 +1503,34 @@ PHP_METHOD(ZipArchive, open) ze_obj->filename = resolved_path; ze_obj->filename_len = strlen(resolved_path); ze_obj->za = intern; + ze_obj->from_string = false; RETURN_TRUE; } /* }}} */ -/* {{{ Create new read-only zip using given string */ +/* {{{ Create new zip from a string, or a create an empty zip to be saved to a string */ PHP_METHOD(ZipArchive, openString) { - zend_string *buffer; + zend_string *buffer = NULL; + zend_long flags = 0; zval *self = ZEND_THIS; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &buffer) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "|Sl", &buffer, &flags) == FAILURE) { RETURN_THROWS(); } + if (!buffer) { + buffer = ZSTR_EMPTY_ALLOC(); + } + ze_zip_object *ze_obj = Z_ZIP_P(self); - php_zipobj_close(ze_obj); + php_zipobj_close(ze_obj, NULL); zip_error_t err; zip_error_init(&err); - zip_source_t * zip_source = php_zip_create_string_source(buffer, NULL, &err); + zip_source_t * zip_source = php_zip_create_string_source(buffer, &ze_obj->out_str, &err); if (!zip_source) { ze_obj->err_zip = zip_error_code_zip(&err); @@ -1521,7 +1539,7 @@ PHP_METHOD(ZipArchive, openString) RETURN_LONG(ze_obj->err_zip); } - struct zip *intern = zip_open_from_source(zip_source, ZIP_RDONLY, &err); + struct zip *intern = zip_open_from_source(zip_source, flags, &err); if (!intern) { ze_obj->err_zip = zip_error_code_zip(&err); ze_obj->err_sys = zip_error_code_system(&err); @@ -1530,6 +1548,7 @@ PHP_METHOD(ZipArchive, openString) RETURN_LONG(ze_obj->err_zip); } + ze_obj->from_string = true; ze_obj->za = intern; zip_error_fini(&err); RETURN_TRUE; @@ -1568,7 +1587,32 @@ PHP_METHOD(ZipArchive, close) ZIP_FROM_OBJECT(intern, self); - RETURN_BOOL(php_zipobj_close(Z_ZIP_P(self))); + RETURN_BOOL(php_zipobj_close(Z_ZIP_P(self), NULL)); +} +/* }}} */ + +/* {{{ close the zip archive and get the result as a string */ +PHP_METHOD(ZipArchive, closeString) +{ + struct zip *intern; + zval *self = ZEND_THIS; + + ZEND_PARSE_PARAMETERS_NONE(); + + ZIP_FROM_OBJECT(intern, self); + + if (!Z_ZIP_P(self)->from_string) { + zend_throw_error(NULL, "ZipArchive::closeString can only be called on " + "an archive opened with ZipArchive::openString"); + RETURN_THROWS(); + } + + zend_string *ret = NULL; + bool success = php_zipobj_close(Z_ZIP_P(self), &ret); + if (success) { + RETURN_STR(ret ? ret : ZSTR_EMPTY_ALLOC()); + } + RETURN_FALSE; } /* }}} */ diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h index cdd72ba36367..2e0a7dcb3d12 100644 --- a/ext/zip/php_zip.h +++ b/ext/zip/php_zip.h @@ -69,6 +69,8 @@ typedef struct _ze_zip_object { HashTable *prop_handler; char *filename; size_t filename_len; + zend_string *out_str; + bool from_string; zip_int64_t last_id; int err_zip; int err_sys; diff --git a/ext/zip/php_zip.stub.php b/ext/zip/php_zip.stub.php index 19ea67e07fba..49dd19e53553 100644 --- a/ext/zip/php_zip.stub.php +++ b/ext/zip/php_zip.stub.php @@ -646,7 +646,7 @@ class ZipArchive implements Countable /** @tentative-return-type */ public function open(string $filename, int $flags = 0): bool|int {} - public function openString(string $data): bool|int {} + public function openString(string $data = '', int $flags = 0): bool|int {} /** * @tentative-return-type @@ -656,6 +656,8 @@ public function setPassword(#[\SensitiveParameter] string $password): bool {} /** @tentative-return-type */ public function close(): bool {} + public function closeString(): string|false {} + /** @tentative-return-type */ public function count(): int {} diff --git a/ext/zip/php_zip_arginfo.h b/ext/zip/php_zip_arginfo.h index ae2569400efe..faa6feb1cb1e 100644 --- a/ext/zip/php_zip_arginfo.h +++ b/ext/zip/php_zip_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_zip.stub.php instead. - * Stub hash: bf6706496639628a3287d0026f68f57ecebc4a55 */ + * Stub hash: d623efdfe5ac46f07aebf8fb120050c818f3d793 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_zip_open, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) @@ -45,8 +45,9 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_ZipArchive_open, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_ZipArchive_openString, 0, 1, MAY_BE_BOOL|MAY_BE_LONG) - ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_ZipArchive_openString, 0, 0, MAY_BE_BOOL|MAY_BE_LONG) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, "\'\'") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ZipArchive_setPassword, 0, 1, _IS_BOOL, 0) @@ -56,6 +57,9 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ZipArchive_close, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_ZipArchive_closeString, 0, 0, MAY_BE_STRING|MAY_BE_FALSE) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ZipArchive_count, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -320,6 +324,7 @@ ZEND_METHOD(ZipArchive, open); ZEND_METHOD(ZipArchive, openString); ZEND_METHOD(ZipArchive, setPassword); ZEND_METHOD(ZipArchive, close); +ZEND_METHOD(ZipArchive, closeString); ZEND_METHOD(ZipArchive, count); ZEND_METHOD(ZipArchive, getStatusString); ZEND_METHOD(ZipArchive, clearError); @@ -399,6 +404,7 @@ static const zend_function_entry class_ZipArchive_methods[] = { ZEND_ME(ZipArchive, openString, arginfo_class_ZipArchive_openString, ZEND_ACC_PUBLIC) ZEND_ME(ZipArchive, setPassword, arginfo_class_ZipArchive_setPassword, ZEND_ACC_PUBLIC) ZEND_ME(ZipArchive, close, arginfo_class_ZipArchive_close, ZEND_ACC_PUBLIC) + ZEND_ME(ZipArchive, closeString, arginfo_class_ZipArchive_closeString, ZEND_ACC_PUBLIC) ZEND_ME(ZipArchive, count, arginfo_class_ZipArchive_count, ZEND_ACC_PUBLIC) ZEND_ME(ZipArchive, getStatusString, arginfo_class_ZipArchive_getStatusString, ZEND_ACC_PUBLIC) ZEND_ME(ZipArchive, clearError, arginfo_class_ZipArchive_clearError, ZEND_ACC_PUBLIC) diff --git a/ext/zip/tests/ZipArchive_closeString_basic.phpt b/ext/zip/tests/ZipArchive_closeString_basic.phpt new file mode 100644 index 000000000000..852a7ebf53f3 --- /dev/null +++ b/ext/zip/tests/ZipArchive_closeString_basic.phpt @@ -0,0 +1,25 @@ +--TEST-- +ZipArchive::closeString() basic +--EXTENSIONS-- +zip +--FILE-- +openString(); +$zip->addFromString('test1', '1'); +$zip->addFromString('test2', '2'); +$contents = $zip->closeString(); +echo $contents ? "OK\n" : "FAILED\n"; + +$zip = new ZipArchive(); +$zip->openString($contents); +var_dump($zip->getFromName('test1')); +var_dump($zip->getFromName('test2')); +var_dump($zip->getFromName('nonexistent')); + +?> +--EXPECT-- +OK +string(1) "1" +string(1) "2" +bool(false) diff --git a/ext/zip/tests/ZipArchive_closeString_error.phpt b/ext/zip/tests/ZipArchive_closeString_error.phpt new file mode 100644 index 000000000000..d5dca14a97fc --- /dev/null +++ b/ext/zip/tests/ZipArchive_closeString_error.phpt @@ -0,0 +1,47 @@ +--TEST-- +ZipArchive::closeString() error cases +--EXTENSIONS-- +zip +--FILE-- +openString(); +var_dump($zip->open(__DIR__ . '/test.zip')); +try { + $zip->closeString(); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +echo "2.\n"; +$zip = new ZipArchive(); +$zip->openString('...'); +echo $zip->getStatusString() . "\n"; +try { + $zip->closeString(); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +echo "3.\n"; +$zip = new ZipArchive(); +$zip->openString(file_get_contents(__DIR__ . '/test.zip')); +echo gettype($zip->closeString()) . "\n"; +try { + $zip->closeString(); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +1. +bool(true) +ZipArchive::closeString can only be called on an archive opened with ZipArchive::openString +2. +Not a zip archive +Invalid or uninitialized Zip object +3. +string +Invalid or uninitialized Zip object diff --git a/ext/zip/tests/ZipArchive_closeString_false.phpt b/ext/zip/tests/ZipArchive_closeString_false.phpt new file mode 100644 index 000000000000..8a7d942527f6 --- /dev/null +++ b/ext/zip/tests/ZipArchive_closeString_false.phpt @@ -0,0 +1,22 @@ +--TEST-- +ZipArchive::closeString() false return +--EXTENSIONS-- +zip +--FILE-- +openString($input)); +$zip->setCompressionIndex(0, ZipArchive::CM_DEFLATE); +var_dump($zip->closeString()); +echo $zip->getStatusString() . "\n"; +?> +--EXPECTREGEX-- +bool\(true\) + +Warning: ZipArchive::closeString\(\): (Zip archive inconsistent|Unexpected length of data).* +bool\(false\) +(Zip archive inconsistent|Unexpected length of data) diff --git a/ext/zip/tests/ZipArchive_closeString_variation.phpt b/ext/zip/tests/ZipArchive_closeString_variation.phpt new file mode 100644 index 000000000000..2fae7ebd3895 --- /dev/null +++ b/ext/zip/tests/ZipArchive_closeString_variation.phpt @@ -0,0 +1,28 @@ +--TEST-- +ZipArchive::closeString() variations +--EXTENSIONS-- +zip +--FILE-- +openString(); +var_dump($zip->closeString()); +echo $zip->getStatusString() . "\n"; + +echo "2. Update existing archive\n"; +$input = file_get_contents(__DIR__ . '/test.zip'); +$zip = new ZipArchive(); +$zip->openString($input); +$zip->addFromString('entry1.txt', ''); +$result = $zip->closeString(); +echo gettype($result) . "\n"; +var_dump($input !== $result); +?> +--EXPECT-- +1. Empty archive creation +string(0) "" +No error +2. Update existing archive +string +bool(true) diff --git a/ext/zip/tests/ZipArchive_openString.phpt b/ext/zip/tests/ZipArchive_openString.phpt index f787b4a84933..2a11505fcf69 100644 --- a/ext/zip/tests/ZipArchive_openString.phpt +++ b/ext/zip/tests/ZipArchive_openString.phpt @@ -4,8 +4,10 @@ ZipArchive::openString() method zip --FILE-- openString(file_get_contents(__DIR__."/test_procedural.zip")); +$zip->openString($input, ZipArchive::RDONLY); for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); @@ -17,8 +19,22 @@ var_dump($zip->addFromString("foobar/baz", "baz")); var_dump($zip->addEmptyDir("blub")); var_dump($zip->close()); + +echo "2. CREATE and EXCL flags\n"; +$zip = new ZipArchive(); +var_dump($zip->openString($input, ZipArchive::CREATE)); +var_dump($zip->openString($input, ZipArchive::EXCL)); +echo $zip->getStatusString() . "\n"; + +echo "3. CHECKCONS flag\n"; +$inconsistent = file_get_contents(__DIR__ . '/checkcons.zip'); +$zip = new ZipArchive(); +var_dump($zip->openString($inconsistent)); +var_dump($zip->openString($inconsistent, ZipArchive::CHECKCONS)); + ?> --EXPECTF-- +1. Open read-only and read directory foo bar foobar/ @@ -26,3 +42,10 @@ foobar/baz bool(false) bool(false) bool(true) +2. CREATE and EXCL flags +bool(true) +int(10) +File already exists +3. CHECKCONS flag +bool(true) +int(%d) diff --git a/ext/zip/tests/checkcons.zip b/ext/zip/tests/checkcons.zip new file mode 100644 index 0000000000000000000000000000000000000000..50bcea128a46a40417700a9594c9cdbecb38a55e GIT binary patch literal 181 zcmWIWW@h1HfB=21$!yn+7=auR=4Oy#aL!3AF4jv1k)a`+49pi6C0c=SX$3a}Bg+eB z1_m&}72wUtB*%=)90{=TATyUVf|w|#vO-KnGcmxMl?|kn5eNf;v>S-S5K@$wnWLwt F2LP)EAYK3f literal 0 HcmV?d00001 diff --git a/ext/zip/tests/wrong-file-size.zip b/ext/zip/tests/wrong-file-size.zip new file mode 100644 index 0000000000000000000000000000000000000000..fc9fa1a434c7ddb3188117f5b125cc3cef92e56b GIT binary patch literal 110 zcmWIWW@Zs#0DP1W^zjtZX1Q NBM=$^X*Cds0RYQL62Jfe literal 0 HcmV?d00001