Skip to content

Commit 5aac941

Browse files
committed
Ability to open from streams
1 parent 4a15eb4 commit 5aac941

6 files changed

Lines changed: 229 additions & 22 deletions

File tree

libarchive.c

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ static zend_object *arch_ce_create_object(zend_class_entry *ce)
171171
arch_object *zobj =
172172
emalloc(sizeof(*zobj) + zend_object_properties_size(ce));
173173

174-
zobj->file_location = NULL;
174+
zobj->source_kind = ARCH_SOURCE_NONE;
175175
zobj->archive = NULL;
176176
zobj->arch_disk = NULL;
177177
zobj->write_disk_options = 0;
@@ -184,10 +184,17 @@ static zend_object *arch_ce_create_object(zend_class_entry *ce)
184184
static void arch_oh_free_obj(zend_object *zobj)
185185
{
186186
arch_object *obj = arch_object_fetch(zobj);
187-
if (obj->file_location) {
188-
zend_string_release(obj->file_location);
189-
obj->file_location = NULL;
190-
}
187+
switch (obj->source_kind) {
188+
case ARCH_SOURCE_FILE:
189+
zend_string_release(obj->source.file_location);
190+
break;
191+
case ARCH_SOURCE_STREAM:
192+
zval_ptr_dtor(&obj->source.stream_zv);
193+
break;
194+
case ARCH_SOURCE_NONE:
195+
break;
196+
}
197+
obj->source_kind = ARCH_SOURCE_NONE;
191198
if (obj->archive) {
192199
archive_read_close(obj->archive);
193200
obj->archive = NULL;
@@ -210,47 +217,101 @@ PHP_METHOD(libarchive_Archive, __construct)
210217
}
211218

212219
arch_object *arch_obj = arch_object_from_zv(getThis());
213-
arch_obj->file_location = zend_string_copy(file);
220+
arch_obj->source_kind = ARCH_SOURCE_FILE;
221+
arch_obj->source.file_location = zend_string_copy(file);
214222
arch_obj->write_disk_options = (int)flags;
215223
}
216224

225+
static bool arch_obj_open_read_stream(arch_object *arch_obj);
226+
217227
static bool arch_obj_open_read(arch_object *arch_obj)
218228
{
219229
php_stream *stream = php_stream_open_wrapper(
220-
arch_obj->file_location->val, "rb",
230+
arch_obj->source.file_location->val, "rb",
221231
REPORT_ERRORS | STREAM_WILL_CAST | PHP_STREAM_PREFER_STDIO |
222232
STREAM_MUST_SEEK,
223233
NULL);
224234

225235
if (!stream) {
226236
zend_throw_exception_ex(except_ce, -1, "Could not open %s",
227-
arch_obj->file_location->val);
237+
arch_obj->source.file_location->val);
228238
return false;
229239
}
230240

231241
int fd;
232-
int res = php_stream_cast(stream, PHP_STREAM_AS_FD, (void **)&fd, 0);
242+
if (php_stream_can_cast(stream, PHP_STREAM_AS_FD) == SUCCESS &&
243+
php_stream_cast(stream, PHP_STREAM_AS_FD, (void **)&fd, 0) == SUCCESS) {
244+
arch_obj->archive = archive_read_new();
245+
archive_read_support_filter_all(arch_obj->archive);
246+
archive_read_support_format_all(arch_obj->archive);
247+
int res = archive_read_open_fd(arch_obj->archive, fd, 10240);
248+
if (res != ARCHIVE_OK) {
249+
zend_throw_exception_ex(
250+
except_ce, archive_errno(arch_obj->archive),
251+
"Could not open archive from file descriptor: %s",
252+
archive_error_string(arch_obj->archive));
253+
return false;
254+
}
255+
} else {
256+
/* FD cast not supported by this stream wrapper; fall back to FILE*.
257+
* Switch the source so arch_obj_open_read_stream can take over and
258+
* the stream resource is kept alive for the lifetime of the archive. */
259+
zend_string_release(arch_obj->source.file_location);
260+
arch_obj->source_kind = ARCH_SOURCE_STREAM;
261+
php_stream_to_zval(stream, &arch_obj->source.stream_zv);
262+
return arch_obj_open_read_stream(arch_obj);
263+
}
264+
265+
return true;
266+
}
267+
268+
static bool arch_obj_open_read_stream(arch_object *arch_obj)
269+
{
270+
php_stream *stream;
271+
php_stream_from_zval_no_verify(stream, &arch_obj->source.stream_zv);
272+
if (!stream) {
273+
zend_throw_exception(except_ce, "Invalid stream resource", -1);
274+
return false;
275+
}
276+
277+
FILE *fp;
278+
int res = php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void **)&fp, REPORT_ERRORS);
233279
if (res != SUCCESS) {
234-
zend_throw_exception_ex(except_ce, -1, "Could not cast stream for %s",
235-
arch_obj->file_location->val);
280+
zend_throw_exception(except_ce, "Could not cast stream to FILE*", -1);
236281
return false;
237282
}
238283

239284
arch_obj->archive = archive_read_new();
240285
archive_read_support_filter_all(arch_obj->archive);
241286
archive_read_support_format_all(arch_obj->archive);
242-
res = archive_read_open_fd(arch_obj->archive, fd, 10240);
287+
res = archive_read_open_FILE(arch_obj->archive, fp);
243288
if (res != ARCHIVE_OK) {
244289
zend_throw_exception_ex(
245290
except_ce, archive_errno(arch_obj->archive),
246-
"Could not open archive from file descriptor: %s",
291+
"Could not open archive from stream: %s",
247292
archive_error_string(arch_obj->archive));
248293
return false;
249294
}
250295

251296
return true;
252297
}
253298

299+
PHP_METHOD(libarchive_Archive, fromStream)
300+
{
301+
zval *stream_zv;
302+
zend_long flags = 0;
303+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r|l", &stream_zv, &flags) ==
304+
FAILURE) {
305+
return;
306+
}
307+
308+
object_init_ex(return_value, arch_ce);
309+
arch_object *arch_obj = arch_object_from_zv(return_value);
310+
arch_obj->source_kind = ARCH_SOURCE_STREAM;
311+
ZVAL_COPY(&arch_obj->source.stream_zv, stream_zv);
312+
arch_obj->write_disk_options = (int)flags;
313+
}
314+
254315
PHP_METHOD(libarchive_Archive, currentEntryStream)
255316
{
256317
ZEND_PARSE_PARAMETERS_NONE();
@@ -396,7 +457,7 @@ static zend_object_iterator *arch_ce_get_iterator(zend_class_entry *ce,
396457
}
397458

398459
arch_object *arch_obj = arch_object_from_zv(object);
399-
if (arch_obj->file_location == NULL) {
460+
if (arch_obj->source_kind == ARCH_SOURCE_NONE) {
400461
php_error_docref(
401462
NULL, E_ERROR,
402463
"The Archive object has not been properly constructed");
@@ -407,8 +468,14 @@ static zend_object_iterator *arch_ce_get_iterator(zend_class_entry *ce,
407468
-1);
408469
return NULL;
409470
}
410-
if (!arch_obj_open_read(arch_obj)) {
411-
return NULL;
471+
if (arch_obj->source_kind == ARCH_SOURCE_FILE) {
472+
if (!arch_obj_open_read(arch_obj)) {
473+
return NULL;
474+
}
475+
} else {
476+
if (!arch_obj_open_read_stream(arch_obj)) {
477+
return NULL;
478+
}
412479
}
413480

414481
arch_iterator *it = emalloc(sizeof *it);

libarchive.stub.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,22 @@ final class Archive implements \IteratorAggregate
285285
*/
286286
public function __construct(string $file, int $flags = 0) {}
287287

288+
/**
289+
* Create an Archive reader from an already-open PHP stream.
290+
*
291+
* This method does not seek to the beginning of the stream; reading
292+
* starts at the current position.
293+
*
294+
* The stream is cast to a C {@code FILE *} via {@code
295+
* php_stream_cast(PHP_STREAM_AS_STDIO)}, which generally succeeds in
296+
* Linux via fopencookie.
297+
*
298+
* @param resource $stream An open, readable PHP stream.
299+
* @param int $flags Bitmask of EXTRACT_* constants forwarded to
300+
* {@see Archive::extractCurrent()}.
301+
*/
302+
public static function fromStream(mixed $stream, int $flags = 0): static {}
303+
288304
/**
289305
* Extract the current archive entry to disk.
290306
*

libarchive_arginfo.h

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: d41c11ce5a6a024796710fff87e0402e77446549 */
2+
* Stub hash: e2b78feab80afeed324b595b8e65cfecc5be6d95 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_libarchive_Entry___construct, 0, 0, 0)
55
ZEND_END_ARG_INFO()
@@ -9,6 +9,11 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_libarchive_Archive___construct, 0, 0, 1)
99
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0")
1010
ZEND_END_ARG_INFO()
1111

12+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_libarchive_Archive_fromStream, 0, 1, IS_STATIC, 0)
13+
ZEND_ARG_TYPE_INFO(0, stream, IS_MIXED, 0)
14+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0")
15+
ZEND_END_ARG_INFO()
16+
1217
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_libarchive_Archive_extractCurrent, 0, 1, IS_VOID, 0)
1318
ZEND_ARG_OBJ_INFO(0, entry, libarchive\\Entry, 0)
1419
ZEND_END_ARG_INFO()
@@ -19,19 +24,29 @@ ZEND_END_ARG_INFO()
1924
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_libarchive_Archive_getIterator, 0, 0, Traversable, 0)
2025
ZEND_END_ARG_INFO()
2126

27+
2228
ZEND_METHOD(libarchive_Entry, __construct);
2329
ZEND_METHOD(libarchive_Archive, __construct);
30+
ZEND_METHOD(libarchive_Archive, fromStream);
2431
ZEND_METHOD(libarchive_Archive, extractCurrent);
2532
ZEND_METHOD(libarchive_Archive, currentEntryStream);
2633
ZEND_METHOD(libarchive_Archive, getIterator);
2734

35+
36+
static const zend_function_entry class_libarchive_Exception_methods[] = {
37+
ZEND_FE_END
38+
};
39+
40+
2841
static const zend_function_entry class_libarchive_Entry_methods[] = {
2942
ZEND_ME(libarchive_Entry, __construct, arginfo_class_libarchive_Entry___construct, ZEND_ACC_PRIVATE)
3043
ZEND_FE_END
3144
};
3245

46+
3347
static const zend_function_entry class_libarchive_Archive_methods[] = {
3448
ZEND_ME(libarchive_Archive, __construct, arginfo_class_libarchive_Archive___construct, ZEND_ACC_PUBLIC)
49+
ZEND_ME(libarchive_Archive, fromStream, arginfo_class_libarchive_Archive_fromStream, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
3550
ZEND_ME(libarchive_Archive, extractCurrent, arginfo_class_libarchive_Archive_extractCurrent, ZEND_ACC_PUBLIC)
3651
ZEND_ME(libarchive_Archive, currentEntryStream, arginfo_class_libarchive_Archive_currentEntryStream, ZEND_ACC_PUBLIC)
3752
ZEND_ME(libarchive_Archive, getIterator, arginfo_class_libarchive_Archive_getIterator, ZEND_ACC_PUBLIC)
@@ -64,8 +79,9 @@ static zend_class_entry *register_class_libarchive_Exception(zend_class_entry *c
6479
{
6580
zend_class_entry ce, *class_entry;
6681

67-
INIT_NS_CLASS_ENTRY(ce, "libarchive", "Exception", NULL);
68-
class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Exception, ZEND_ACC_FINAL);
82+
INIT_NS_CLASS_ENTRY(ce, "libarchive", "Exception", class_libarchive_Exception_methods);
83+
class_entry = zend_register_internal_class_ex(&ce, class_entry_Exception);
84+
class_entry->ce_flags |= ZEND_ACC_FINAL;
6985

7086
return class_entry;
7187
}
@@ -75,7 +91,8 @@ static zend_class_entry *register_class_libarchive_Entry(void)
7591
zend_class_entry ce, *class_entry;
7692

7793
INIT_NS_CLASS_ENTRY(ce, "libarchive", "Entry", class_libarchive_Entry_methods);
78-
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);
94+
class_entry = zend_register_internal_class_ex(&ce, NULL);
95+
class_entry->ce_flags |= ZEND_ACC_FINAL;
7996

8097
return class_entry;
8198
}
@@ -85,7 +102,8 @@ static zend_class_entry *register_class_libarchive_Archive(zend_class_entry *cla
85102
zend_class_entry ce, *class_entry;
86103

87104
INIT_NS_CLASS_ENTRY(ce, "libarchive", "Archive", class_libarchive_Archive_methods);
88-
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);
105+
class_entry = zend_register_internal_class_ex(&ce, NULL);
106+
class_entry->ce_flags |= ZEND_ACC_FINAL;
89107
zend_class_implements(class_entry, 1, class_entry_IteratorAggregate);
90108

91109
return class_entry;

php_libarchive.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,18 @@ ZEND_TSRMLS_CACHE_EXTERN()
3131
#define nullable _Nullable
3232
#define unspecnull _Null_unspecified
3333

34+
typedef enum {
35+
ARCH_SOURCE_NONE,
36+
ARCH_SOURCE_FILE,
37+
ARCH_SOURCE_STREAM,
38+
} arch_source_kind;
39+
3440
typedef struct _arch_object {
35-
zend_string *nullable file_location;
41+
arch_source_kind source_kind;
42+
union {
43+
zend_string *file_location; /* ARCH_SOURCE_FILE */
44+
zval stream_zv; /* ARCH_SOURCE_STREAM */
45+
} source;
3646
struct archive *nullable archive;
3747
struct archive *nullable arch_disk;
3848
int write_disk_options;

tests/from-stream.phpt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Archive::fromStream() opens from an existing PHP stream resource
3+
--FILE--
4+
<?php
5+
6+
include 'lib.inc';
7+
8+
echo "-- fopen'd file --\n";
9+
$f = fopen(__DIR__ . '/arch/basic.7z', 'rb');
10+
foreach (libarchive\Archive::fromStream($f) as $e) {
11+
echo $e->pathname, ' ', $e->size, "\n";
12+
}
13+
fclose($f);
14+
15+
echo "-- php://memory (non-fd stream) --\n";
16+
$mem = fopen('php://memory', 'rb+');
17+
fwrite($mem, file_get_contents(__DIR__ . '/arch/basic.7z'));
18+
rewind($mem);
19+
foreach (libarchive\Archive::fromStream($mem) as $e) {
20+
echo $e->pathname, ' ', $e->size, "\n";
21+
}
22+
fclose($mem);
23+
24+
echo "-- current position is honoured (stream at EOF) --\n";
25+
$f = fopen(__DIR__ . '/arch/basic.7z', 'rb');
26+
fseek($f, 0, SEEK_END);
27+
try {
28+
foreach (libarchive\Archive::fromStream($f) as $e) {
29+
echo $e->pathname, "\n";
30+
}
31+
echo "no entries\n";
32+
} catch (libarchive\Exception $e) {
33+
echo "Exception caught\n";
34+
}
35+
fclose($f);
36+
37+
?>
38+
--EXPECT--
39+
-- fopen'd file --
40+
myfile.txt 35
41+
-- php://memory (non-fd stream) --
42+
myfile.txt 35
43+
-- current position is honoured (stream at EOF) --
44+
no entries

tests/url-no-fd.phpt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
Constructor falls back to FILE* for stream wrappers that cannot be cast to a file descriptor
3+
--FILE--
4+
<?php
5+
6+
/* A stream wrapper that delegates reads/seeks to a real file but never
7+
* exposes a file descriptor (no stream_cast method). */
8+
class NoFdWrapper {
9+
private $fp;
10+
11+
public function stream_open(string $path, string $mode,
12+
int $options, ?string &$opened_path): bool {
13+
$file = substr($path, strlen('nofd://'));
14+
$this->fp = fopen($file, $mode);
15+
return $this->fp !== false;
16+
}
17+
18+
public function stream_read(int $count): string|false {
19+
return fread($this->fp, $count);
20+
}
21+
22+
public function stream_seek(int $offset, int $whence): bool {
23+
return fseek($this->fp, $offset, $whence) === 0;
24+
}
25+
26+
public function stream_tell(): int {
27+
return ftell($this->fp);
28+
}
29+
30+
public function stream_eof(): bool {
31+
return feof($this->fp);
32+
}
33+
34+
public function stream_close(): void {
35+
fclose($this->fp);
36+
}
37+
38+
public function stream_cast(int $cast_as): mixed {
39+
return false;
40+
}
41+
}
42+
43+
stream_wrapper_register('nofd', NoFdWrapper::class);
44+
45+
$a = new libarchive\Archive('nofd://' . __DIR__ . '/arch/basic.7z');
46+
foreach ($a as $e) {
47+
echo $e->pathname, ' ', $e->size, "\n";
48+
}
49+
50+
?>
51+
--EXPECT--
52+
myfile.txt 35

0 commit comments

Comments
 (0)