Skip to content

Commit 9b8c4fd

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 cd2e060 commit 9b8c4fd

11 files changed

Lines changed: 217 additions & 16 deletions

ext/zip/php_zip.c

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,10 @@ 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 out_str is non-null and the archive was opened
577+
* as a string, the final contents of the archive will be assigned to *out_str
578+
* and that string will afterwards be owned by the caller. */
579+
static bool php_zipobj_close(ze_zip_object *obj, zend_string **out_str) /* {{{ */
578580
{
579581
struct zip *intern = obj->za;
580582
bool success = false;
@@ -606,7 +608,19 @@ static bool php_zipobj_close(ze_zip_object *obj) /* {{{ */
606608
obj->filename_len = 0;
607609
}
608610

611+
if (obj->out_str) {
612+
if (out_str) {
613+
*out_str = obj->out_str;
614+
} else {
615+
zend_string_release(obj->out_str);
616+
}
617+
obj->out_str = NULL;
618+
} else {
619+
ZEND_ASSERT(!out_str);
620+
}
621+
609622
obj->za = NULL;
623+
obj->from_string = false;
610624
return success;
611625
}
612626
/* }}} */
@@ -1060,7 +1074,7 @@ static void php_zip_object_free_storage(zend_object *object) /* {{{ */
10601074
{
10611075
ze_zip_object * intern = php_zip_fetch_object(object);
10621076

1063-
php_zipobj_close(intern);
1077+
php_zipobj_close(intern, NULL);
10641078

10651079
#ifdef HAVE_PROGRESS_CALLBACK
10661080
/* if not properly called by libzip */
@@ -1467,7 +1481,7 @@ PHP_METHOD(ZipArchive, open)
14671481
}
14681482

14691483
/* If we already have an opened zip, free it */
1470-
php_zipobj_close(ze_obj);
1484+
php_zipobj_close(ze_obj, NULL);
14711485

14721486
/* open for write without option to empty the archive */
14731487
if ((flags & (ZIP_TRUNCATE | ZIP_RDONLY)) == 0) {
@@ -1491,28 +1505,34 @@ PHP_METHOD(ZipArchive, open)
14911505
ze_obj->filename = resolved_path;
14921506
ze_obj->filename_len = strlen(resolved_path);
14931507
ze_obj->za = intern;
1508+
ze_obj->from_string = false;
14941509
RETURN_TRUE;
14951510
}
14961511
/* }}} */
14971512

1498-
/* {{{ Create new read-only zip using given string */
1513+
/* {{{ Create new zip from a string, or a create an empty zip to be saved to a string */
14991514
PHP_METHOD(ZipArchive, openString)
15001515
{
1501-
zend_string *buffer;
1516+
zend_string *buffer = NULL;
1517+
zend_long flags = 0;
15021518
zval *self = ZEND_THIS;
15031519

1504-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &buffer) == FAILURE) {
1520+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "|Sl", &buffer, &flags) == FAILURE) {
15051521
RETURN_THROWS();
15061522
}
15071523

1524+
if (!buffer) {
1525+
buffer = ZSTR_EMPTY_ALLOC();
1526+
}
1527+
15081528
ze_zip_object *ze_obj = Z_ZIP_P(self);
15091529

1510-
php_zipobj_close(ze_obj);
1530+
php_zipobj_close(ze_obj, NULL);
15111531

15121532
zip_error_t err;
15131533
zip_error_init(&err);
15141534

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

15171537
if (!zip_source) {
15181538
ze_obj->err_zip = zip_error_code_zip(&err);
@@ -1521,7 +1541,7 @@ PHP_METHOD(ZipArchive, openString)
15211541
RETURN_LONG(ze_obj->err_zip);
15221542
}
15231543

1524-
struct zip *intern = zip_open_from_source(zip_source, ZIP_RDONLY, &err);
1544+
struct zip *intern = zip_open_from_source(zip_source, flags, &err);
15251545
if (!intern) {
15261546
ze_obj->err_zip = zip_error_code_zip(&err);
15271547
ze_obj->err_sys = zip_error_code_system(&err);
@@ -1530,6 +1550,7 @@ PHP_METHOD(ZipArchive, openString)
15301550
RETURN_LONG(ze_obj->err_zip);
15311551
}
15321552

1553+
ze_obj->from_string = true;
15331554
ze_obj->za = intern;
15341555
zip_error_fini(&err);
15351556
RETURN_TRUE;
@@ -1568,7 +1589,32 @@ PHP_METHOD(ZipArchive, close)
15681589

15691590
ZIP_FROM_OBJECT(intern, self);
15701591

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

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
--EXPECT--
23+
1.
24+
string(0) ""
25+
No error
26+
2.
27+
string
28+
bool(true)

ext/zip/tests/ZipArchive_openString.phpt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ ZipArchive::openString() method
44
zip
55
--FILE--
66
<?php
7+
echo "1. Open read-only and read directory\n";
8+
$input = file_get_contents(__DIR__."/test_procedural.zip");
79
$zip = new ZipArchive();
8-
$zip->openString(file_get_contents(__DIR__."/test_procedural.zip"));
10+
$zip->openString($input, ZipArchive::RDONLY);
911

1012
for ($i = 0; $i < $zip->numFiles; $i++) {
1113
$stat = $zip->statIndex($i);
@@ -17,12 +19,33 @@ var_dump($zip->addFromString("foobar/baz", "baz"));
1719
var_dump($zip->addEmptyDir("blub"));
1820

1921
var_dump($zip->close());
22+
23+
echo "2. CREATE and EXCL flags\n";
24+
$zip = new ZipArchive();
25+
var_dump($zip->openString($input, ZipArchive::CREATE));
26+
var_dump($zip->openString($input, ZipArchive::EXCL));
27+
echo $zip->getStatusString() . "\n";
28+
29+
echo "3. CHECKCONS flag\n";
30+
$inconsistent = file_get_contents(__DIR__ . '/checkcons.zip');
31+
$zip = new ZipArchive();
32+
var_dump($zip->openString($inconsistent));
33+
var_dump($zip->openString($inconsistent, ZipArchive::CHECKCONS));
34+
2035
?>
2136
--EXPECTF--
37+
1.
2238
foo
2339
bar
2440
foobar/
2541
foobar/baz
2642
bool(false)
2743
bool(false)
2844
bool(true)
45+
2.
46+
bool(true)
47+
int(10)
48+
File already exists
49+
3.
50+
bool(true)
51+
int(%d)

ext/zip/tests/checkcons.zip

181 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)