Skip to content

Commit db5b845

Browse files
committed
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.
1 parent f97ff59 commit db5b845

12 files changed

Lines changed: 238 additions & 16 deletions

ext/zip/php_zip.c

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,12 @@ static char * php_zipobj_get_zip_comment(ze_zip_object *obj, int *len) /* {{{ */
573573
}
574574
/* }}} */
575575

576-
/* Close and free the zip_t */
577-
static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */
576+
/* Close and free the zip_t. If the archive was opened as a string, the
577+
* final contents of the archive will be assigned to *out_str and that
578+
* string will afterwards be owned by the caller.
579+
*
580+
* If out_str is NULL, the final string contents, if any, will be discarded. */
581+
static bool php_zipobj_close(ze_zip_object *obj, zend_string **out_str) /* {{{ */
578582
{
579583
struct zip *intern = obj->za;
580584
bool success = false;
@@ -606,7 +610,19 @@ static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */
606610
obj->filename_len = 0;
607611
}
608612

613+
if (obj->out_str) {
614+
if (out_str) {
615+
*out_str = obj->out_str;
616+
} else {
617+
zend_string_release(obj->out_str);
618+
}
619+
obj->out_str = NULL;
620+
} else {
621+
ZEND_ASSERT(!out_str);
622+
}
623+
609624
obj->za = NULL;
625+
obj->from_string = false;
610626
return success;
611627
}
612628
/* }}} */
@@ -1060,7 +1076,7 @@ static void php_zip_object_free_storage(zend_object *object) /* {{{ */
10601076
{
10611077
ze_zip_object * intern = php_zip_fetch_object(object);
10621078

1063-
php_zipobj_close(intern);
1079+
php_zipobj_close(intern, NULL);
10641080

10651081
#ifdef HAVE_PROGRESS_CALLBACK
10661082
/* if not properly called by libzip */
@@ -1467,7 +1483,7 @@ PHP_METHOD(ZipArchive, open)
14671483
}
14681484

14691485
/* If we already have an opened zip, free it */
1470-
php_zipobj_close(ze_obj);
1486+
php_zipobj_close(ze_obj, NULL);
14711487

14721488
/* open for write without option to empty the archive */
14731489
if ((flags & (ZIP_TRUNCATE | ZIP_RDONLY)) == 0) {
@@ -1491,28 +1507,34 @@ PHP_METHOD(ZipArchive, open)
14911507
ze_obj->filename = resolved_path;
14921508
ze_obj->filename_len = strlen(resolved_path);
14931509
ze_obj->za = intern;
1510+
ze_obj->from_string = false;
14941511
RETURN_TRUE;
14951512
}
14961513
/* }}} */
14971514

1498-
/* {{{ Create new read-only zip using given string */
1515+
/* {{{ Create new zip from a string, or a create an empty zip to be saved to a string */
14991516
PHP_METHOD(ZipArchive, openString)
15001517
{
1501-
zend_string *buffer;
1518+
zend_string *buffer = NULL;
1519+
zend_long flags = 0;
15021520
zval *self = ZEND_THIS;
15031521

1504-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &buffer) == FAILURE) {
1522+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "|Sl", &buffer, &flags) == FAILURE) {
15051523
RETURN_THROWS();
15061524
}
15071525

1526+
if (!buffer) {
1527+
buffer = ZSTR_EMPTY_ALLOC();
1528+
}
1529+
15081530
ze_zip_object *ze_obj = Z_ZIP_P(self);
15091531

1510-
php_zipobj_close(ze_obj);
1532+
php_zipobj_close(ze_obj, NULL);
15111533

15121534
zip_error_t err;
15131535
zip_error_init(&err);
15141536

1515-
zip_source_t * zip_source = php_zip_create_string_source(buffer, NULL, &err);
1537+
zip_source_t * zip_source = php_zip_create_string_source(buffer, &ze_obj->out_str, &err);
15161538

15171539
if (!zip_source) {
15181540
ze_obj->err_zip = zip_error_code_zip(&err);
@@ -1521,7 +1543,7 @@ PHP_METHOD(ZipArchive, openString)
15211543
RETURN_LONG(ze_obj->err_zip);
15221544
}
15231545

1524-
struct zip *intern = zip_open_from_source(zip_source, ZIP_RDONLY, &err);
1546+
struct zip *intern = zip_open_from_source(zip_source, flags, &err);
15251547
if (!intern) {
15261548
ze_obj->err_zip = zip_error_code_zip(&err);
15271549
ze_obj->err_sys = zip_error_code_system(&err);
@@ -1530,6 +1552,7 @@ PHP_METHOD(ZipArchive, openString)
15301552
RETURN_LONG(ze_obj->err_zip);
15311553
}
15321554

1555+
ze_obj->from_string = true;
15331556
ze_obj->za = intern;
15341557
zip_error_fini(&err);
15351558
RETURN_TRUE;
@@ -1568,7 +1591,34 @@ PHP_METHOD(ZipArchive, close)
15681591

15691592
ZIP_FROM_OBJECT(intern, self);
15701593

1571-
RETURN_BOOL(php_zipobj_close(Z_ZIP_P(self)));
1594+
RETURN_BOOL(php_zipobj_close(Z_ZIP_P(self), NULL));
1595+
}
1596+
/* }}} */
1597+
1598+
/* {{{ close the zip archive and get the result as a string */
1599+
PHP_METHOD(ZipArchive, closeString)
1600+
{
1601+
struct zip *intern;
1602+
zval *self = ZEND_THIS;
1603+
1604+
ZEND_PARSE_PARAMETERS_NONE();
1605+
1606+
ZIP_FROM_OBJECT(intern, self);
1607+
1608+
if (!Z_ZIP_P(self)->from_string) {
1609+
zend_throw_error(NULL, "ZipArchive::closeString can only be called on "
1610+
"an archive opened with ZipArchive::openString");
1611+
RETURN_THROWS();
1612+
}
1613+
1614+
zend_string *ret = NULL;
1615+
bool success = php_zipobj_close(Z_ZIP_P(self), &ret);
1616+
ZEND_ASSERT(ret);
1617+
if (success) {
1618+
RETURN_STR(ret);
1619+
}
1620+
zend_string_release(ret);
1621+
RETURN_FALSE;
15721622
}
15731623
/* }}} */
15741624

ext/zip/php_zip.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ typedef struct _ze_zip_object {
6969
HashTable *prop_handler;
7070
char *filename;
7171
size_t filename_len;
72+
zend_string *out_str;
73+
bool from_string;
7274
zip_int64_t last_id;
7375
int err_zip;
7476
int err_sys;

ext/zip/php_zip.stub.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ class ZipArchive implements Countable
646646
/** @tentative-return-type */
647647
public function open(string $filename, int $flags = 0): bool|int {}
648648

649-
public function openString(string $data): bool|int {}
649+
public function openString(string $data = '', int $flags = 0): bool|int {}
650650

651651
/**
652652
* @tentative-return-type
@@ -656,6 +656,8 @@ public function setPassword(#[\SensitiveParameter] string $password): bool {}
656656
/** @tentative-return-type */
657657
public function close(): bool {}
658658

659+
public function closeString(): string|false {}
660+
659661
/** @tentative-return-type */
660662
public function count(): int {}
661663

ext/zip/php_zip_arginfo.h

Lines changed: 9 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
ZipArchive::closeString() basic
3+
--EXTENSIONS--
4+
zip
5+
--FILE--
6+
<?php
7+
$zip = new ZipArchive();
8+
$zip->openString();
9+
$zip->addFromString('test1', '1');
10+
$zip->addFromString('test2', '2');
11+
$contents = $zip->closeString();
12+
echo $contents ? "OK\n" : "FAILED\n";
13+
14+
$zip = new ZipArchive();
15+
$zip->openString($contents);
16+
var_dump($zip->getFromName('test1'));
17+
var_dump($zip->getFromName('test2'));
18+
var_dump($zip->getFromName('nonexistent'));
19+
20+
?>
21+
--EXPECT--
22+
OK
23+
string(1) "1"
24+
string(1) "2"
25+
bool(false)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
ZipArchive::closeString() error cases
3+
--EXTENSIONS--
4+
zip
5+
--FILE--
6+
<?php
7+
echo "1.\n";
8+
$zip = new ZipArchive();
9+
$zip->openString();
10+
var_dump($zip->open(__DIR__ . '/test.zip'));
11+
try {
12+
$zip->closeString();
13+
} catch (Error $e) {
14+
echo $e->getMessage() . "\n";
15+
}
16+
17+
echo "2.\n";
18+
$zip = new ZipArchive();
19+
$zip->openString('...');
20+
echo $zip->getStatusString() . "\n";
21+
try {
22+
$zip->closeString();
23+
} catch (Error $e) {
24+
echo $e->getMessage() . "\n";
25+
}
26+
27+
echo "3.\n";
28+
$zip = new ZipArchive();
29+
$zip->openString(file_get_contents(__DIR__ . '/test.zip'));
30+
echo gettype($zip->closeString()) . "\n";
31+
try {
32+
$zip->closeString();
33+
} catch (Error $e) {
34+
echo $e->getMessage() . "\n";
35+
}
36+
37+
?>
38+
--EXPECT--
39+
1.
40+
bool(true)
41+
ZipArchive::closeString can only be called on an archive opened with ZipArchive::openString
42+
2.
43+
Not a zip archive
44+
Invalid or uninitialized Zip object
45+
3.
46+
string
47+
Invalid or uninitialized Zip object
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
ZipArchive::closeString() false return
3+
--EXTENSIONS--
4+
zip
5+
--FILE--
6+
<?php
7+
$zip = new ZipArchive();
8+
// The "compressed size" fields are wrong, causing an error when reading the contents.
9+
// The error is reported on close when we rewrite the member with setCompressionIndex().
10+
// The error code is ER_DATA_LENGTH in libzip 1.10.0+ or ER_INCONS otherwise.
11+
$input = file_get_contents(__DIR__ . '/wrong-file-size.zip');
12+
var_dump($zip->openString($input));
13+
$zip->setCompressionIndex(0, ZipArchive::CM_DEFLATE);
14+
var_dump($zip->closeString());
15+
echo $zip->getStatusString() . "\n";
16+
?>
17+
--EXPECTREGEX--
18+
bool\(true\)
19+
20+
Warning: ZipArchive::closeString\(\): (Zip archive inconsistent|Unexpected length of data).*
21+
bool\(false\)
22+
(Zip archive inconsistent|Unexpected length of data)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
ZipArchive::closeString() variations
3+
--EXTENSIONS--
4+
zip
5+
--FILE--
6+
<?php
7+
echo "1. Empty archive creation\n";
8+
$zip = new ZipArchive();
9+
$zip->openString();
10+
var_dump($zip->closeString());
11+
echo $zip->getStatusString() . "\n";
12+
13+
echo "2. Update existing archive\n";
14+
$input = file_get_contents(__DIR__ . '/test.zip');
15+
$zip = new ZipArchive();
16+
$zip->openString($input);
17+
$zip->addFromString('entry1.txt', '');
18+
$result = $zip->closeString();
19+
echo gettype($result) . "\n";
20+
var_dump($input !== $result);
21+
22+
echo "3. Unchanged existing archive\n";
23+
$zip = new ZipArchive();
24+
$zip->openString($input);
25+
$result = $zip->closeString();
26+
echo gettype($result) . "\n";
27+
var_dump($input === $result);
28+
29+
?>
30+
--EXPECT--
31+
1. Empty archive creation
32+
string(0) ""
33+
No error
34+
2. Update existing archive
35+
string
36+
bool(true)
37+
3. Unchanged existing archive
38+
string
39+
bool(true)

0 commit comments

Comments
 (0)