From aa898d95695df79dca207c76963138205740506d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 27 Sep 2025 23:35:56 +0200 Subject: [PATCH 01/16] stream: implement initial changes to control filter seeking --- main/streams/filter.c | 17 +++++++++ main/streams/php_stream_filter_api.h | 33 ++++++++++++++++- main/streams/streams.c | 55 +++++++++++++++++++++++++--- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/main/streams/filter.c b/main/streams/filter.c index b63d789190792..a601f2d22c1cf 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -260,6 +260,23 @@ PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval return filter; } +PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_ops *fops, + const php_stream_filter_extra_ops *feops, void *abstract, uint8_t persistent, uint16_t flags STREAMS_DC) +{ + php_stream_filter *filter; + + filter = (php_stream_filter*) pemalloc_rel_orig(sizeof(php_stream_filter), persistent); + memset(filter, 0, sizeof(php_stream_filter)); + + filter->fops = fops; + filter->feops = feops; + filter->seekable = flags & PHP_STREAM_FILTER_SEEKABLE_MASK; + Z_PTR(filter->abstract) = abstract; + filter->is_persistent = persistent; + + return filter; +} + PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC) { php_stream_filter *filter; diff --git a/main/streams/php_stream_filter_api.h b/main/streams/php_stream_filter_api.h index e224b85b2d9a2..cf9f026a4581b 100644 --- a/main/streams/php_stream_filter_api.h +++ b/main/streams/php_stream_filter_api.h @@ -34,6 +34,11 @@ #define PHP_STREAM_FILTER_WRITE 0x0002 #define PHP_STREAM_FILTER_ALL (PHP_STREAM_FILTER_READ | PHP_STREAM_FILTER_WRITE) +#define PHP_STREAM_FILTER_SEEKABLE_NEVER 0 +#define PHP_STREAM_FILTER_SEEKABLE_START 1 +#define PHP_STREAM_FILTER_SEEKABLE_ALWAYS 2 +#define PHP_STREAM_FILTER_SEEKABLE_MASK 3 + typedef struct _php_stream_bucket php_stream_bucket; typedef struct _php_stream_bucket_brigade php_stream_bucket_brigade; @@ -94,6 +99,26 @@ typedef struct _php_stream_filter_ops { } php_stream_filter_ops; +typedef struct _php_stream_filter_extra_ops { + /* it should indicate whether seeking is supported and possibly modify internal state */ + zend_result (*seek)( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ); + + /* this is a generic interface for possible further extensions */ + zend_result (*set_option)( + php_stream *stream, + php_stream_filter *thisfilter, + int option, + void *value, + size_t size + ); + +} php_stream_filter_extra_ops; + typedef struct _php_stream_filter_chain { php_stream_filter *head, *tail; @@ -103,10 +128,12 @@ typedef struct _php_stream_filter_chain { struct _php_stream_filter { const php_stream_filter_ops *fops; + const php_stream_filter_extra_ops *feops; zval abstract; /* for use by filter implementation */ php_stream_filter *next; php_stream_filter *prev; - int is_persistent; + bool is_persistent; + uint8_t seekable; /* link into stream and chain */ php_stream_filter_chain *chain; @@ -128,8 +155,12 @@ PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool fini PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bool call_dtor); PHPAPI void php_stream_filter_free(php_stream_filter *filter); PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC); +PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_ops *fops, + const php_stream_filter_extra_ops *feops, void *abstract, uint8_t persistent, uint16_t flags STREAMS_DC); END_EXTERN_C() #define php_stream_filter_alloc(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_CC) +#define php_stream_filter_alloc_ex(fops, feops, thisptr, persistent) \ + _php_stream_filter_alloc_ex((fops), (feops), (thisptr), (persistent) STREAMS_CC) #define php_stream_filter_alloc_rel(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_REL_CC) #define php_stream_filter_prepend(chain, filter) _php_stream_filter_prepend((chain), (filter)) #define php_stream_filter_append(chain, filter) _php_stream_filter_append((chain), (filter)) diff --git a/main/streams/streams.c b/main/streams/streams.c index 85d2947c28a6c..98978ad3f7daa 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1361,6 +1361,40 @@ PHPAPI zend_off_t _php_stream_tell(const php_stream *stream) return stream->position; } +static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_start_seeking) +{ + while (filter) { + if (filter->seekable == PHP_STREAM_FILTER_SEEKABLE_NEVER || + (!is_start_seeking && filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START)) { + php_error_docref(NULL, E_WARNING, "Stream filter %s is not seekable", filter->fops->label); + return false; + } + filter = filter->next; + } + return true; +} + +static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter, bool is_start_seeking) +{ + while (filter) { + if ((filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START && is_start_seeking && + filter->feops->seek(stream, filter, 0, SEEK_SET))) { + php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label); + return FAILURE; + } + filter = filter->next; + } + return SUCCESS; +} + +static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking) +{ + return php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking) == SUCCESS && + php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking) == SUCCESS; +} + + + PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) { if (stream->fclose_stdiocast == PHP_STREAM_FCLOSE_FOPENCOOKIE) { @@ -1373,6 +1407,18 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) } } + bool is_start_seeking = whence == SEEK_SET && offset == 0; + + if (stream->writefilters.head) { + _php_stream_flush(stream, 0); + if (!php_stream_are_filters_seekable(stream->writefilters.head, is_start_seeking)) { + return -1; + } + } + if (stream->readfilters.head && !php_stream_are_filters_seekable(stream->readfilters.head, is_start_seeking)) { + return -1; + } + /* handle the case where we are in the buffer */ if ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) == 0) { switch(whence) { @@ -1382,6 +1428,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->position += offset; stream->eof = 0; stream->fatal_error = 0; + php_stream_filters_seek_all(stream, is_start_seeking); return 0; } break; @@ -1392,6 +1439,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->position = offset; stream->eof = 0; stream->fatal_error = 0; + php_stream_filters_seek_all(stream, is_start_seeking); return 0; } break; @@ -1401,11 +1449,6 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { int ret; - - if (stream->writefilters.head) { - _php_stream_flush(stream, 0); - } - switch(whence) { case SEEK_CUR: ZEND_ASSERT(stream->position >= 0); @@ -1432,6 +1475,8 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) /* invalidate the buffer contents */ stream->readpos = stream->writepos = 0; + php_stream_filters_seek_all(stream, is_start_seeking); + return ret; } /* else the stream has decided that it can't support seeking after all; From 41aee5a95d6e849efd19a7d5b8b11f650d1a36b8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 21 Nov 2025 12:51:44 +0100 Subject: [PATCH 02/16] stream: break current streams API for cleaner design --- main/streams/filter.c | 21 ++------------ main/streams/php_stream_filter_api.h | 43 ++++++++++------------------ main/streams/streams.c | 5 ++-- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/main/streams/filter.c b/main/streams/filter.c index a601f2d22c1cf..0abe3fb581ff6 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -216,7 +216,7 @@ PHPAPI void php_stream_bucket_unlink(php_stream_bucket *bucket) * match. If that fails, we try "convert.charset.*", then "convert.*" * This means that we don't need to clog up the hashtable with a zillion * charsets (for example) but still be able to provide them all as filters */ -PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, uint8_t persistent) +PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, bool persistent) { HashTable *filter_hash = (FG(stream_filters) ? FG(stream_filters) : &stream_filters_hash); const php_stream_filter_factory *factory = NULL; @@ -260,8 +260,8 @@ PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval return filter; } -PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_ops *fops, - const php_stream_filter_extra_ops *feops, void *abstract, uint8_t persistent, uint16_t flags STREAMS_DC) +PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, + void *abstract, bool persistent, uint32_t flags STREAMS_DC) { php_stream_filter *filter; @@ -269,7 +269,6 @@ PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_op memset(filter, 0, sizeof(php_stream_filter)); filter->fops = fops; - filter->feops = feops; filter->seekable = flags & PHP_STREAM_FILTER_SEEKABLE_MASK; Z_PTR(filter->abstract) = abstract; filter->is_persistent = persistent; @@ -277,20 +276,6 @@ PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_op return filter; } -PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC) -{ - php_stream_filter *filter; - - filter = (php_stream_filter*) pemalloc_rel_orig(sizeof(php_stream_filter), persistent); - memset(filter, 0, sizeof(php_stream_filter)); - - filter->fops = fops; - Z_PTR(filter->abstract) = abstract; - filter->is_persistent = persistent; - - return filter; -} - PHPAPI void php_stream_filter_free(php_stream_filter *filter) { if (filter->fops->dtor) diff --git a/main/streams/php_stream_filter_api.h b/main/streams/php_stream_filter_api.h index cf9f026a4581b..fb62be938b022 100644 --- a/main/streams/php_stream_filter_api.h +++ b/main/streams/php_stream_filter_api.h @@ -36,7 +36,8 @@ #define PHP_STREAM_FILTER_SEEKABLE_NEVER 0 #define PHP_STREAM_FILTER_SEEKABLE_START 1 -#define PHP_STREAM_FILTER_SEEKABLE_ALWAYS 2 +#define PHP_STREAM_FILTER_SEEKABLE_CHECK 2 +#define PHP_STREAM_FILTER_SEEKABLE_ALWAYS 3 #define PHP_STREAM_FILTER_SEEKABLE_MASK 3 typedef struct _php_stream_bucket php_stream_bucket; @@ -93,14 +94,6 @@ typedef struct _php_stream_filter_ops { int flags ); - void (*dtor)(php_stream_filter *thisfilter); - - const char *label; - -} php_stream_filter_ops; - -typedef struct _php_stream_filter_extra_ops { - /* it should indicate whether seeking is supported and possibly modify internal state */ zend_result (*seek)( php_stream *stream, php_stream_filter *thisfilter, @@ -108,16 +101,11 @@ typedef struct _php_stream_filter_extra_ops { int whence ); - /* this is a generic interface for possible further extensions */ - zend_result (*set_option)( - php_stream *stream, - php_stream_filter *thisfilter, - int option, - void *value, - size_t size - ); + void (*dtor)(php_stream_filter *thisfilter); -} php_stream_filter_extra_ops; + const char *label; + +} php_stream_filter_ops; typedef struct _php_stream_filter_chain { php_stream_filter *head, *tail; @@ -128,7 +116,6 @@ typedef struct _php_stream_filter_chain { struct _php_stream_filter { const php_stream_filter_ops *fops; - const php_stream_filter_extra_ops *feops; zval abstract; /* for use by filter implementation */ php_stream_filter *next; php_stream_filter *prev; @@ -154,14 +141,14 @@ PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, p PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool finish); PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bool call_dtor); PHPAPI void php_stream_filter_free(php_stream_filter *filter); -PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, void *abstract, uint8_t persistent STREAMS_DC); -PHPAPI php_stream_filter *_php_stream_filter_alloc_ex(const php_stream_filter_ops *fops, - const php_stream_filter_extra_ops *feops, void *abstract, uint8_t persistent, uint16_t flags STREAMS_DC); +PHPAPI php_stream_filter *_php_stream_filter_alloc(const php_stream_filter_ops *fops, + void *abstract, bool persistent, uint32_t flags STREAMS_DC); + END_EXTERN_C() -#define php_stream_filter_alloc(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_CC) -#define php_stream_filter_alloc_ex(fops, feops, thisptr, persistent) \ - _php_stream_filter_alloc_ex((fops), (feops), (thisptr), (persistent) STREAMS_CC) -#define php_stream_filter_alloc_rel(fops, thisptr, persistent) _php_stream_filter_alloc((fops), (thisptr), (persistent) STREAMS_REL_CC) +#define php_stream_filter_alloc(fops, thisptr, persistent, flags) \ + _php_stream_filter_alloc((fops), (thisptr), (persistent), (flags) STREAMS_CC) +#define php_stream_filter_alloc_rel(fops, thisptr, persistent, flags) \ + _php_stream_filter_alloc((fops), (thisptr), (persistent), (flags) STREAMS_REL_CC) #define php_stream_filter_prepend(chain, filter) _php_stream_filter_prepend((chain), (filter)) #define php_stream_filter_append(chain, filter) _php_stream_filter_append((chain), (filter)) #define php_stream_filter_flush(filter, finish) _php_stream_filter_flush((filter), (finish)) @@ -169,12 +156,12 @@ END_EXTERN_C() #define php_stream_is_filtered(stream) ((stream)->readfilters.head || (stream)->writefilters.head) typedef struct _php_stream_filter_factory { - php_stream_filter *(*create_filter)(const char *filtername, zval *filterparams, uint8_t persistent); + php_stream_filter *(*create_filter)(const char *filtername, zval *filterparams, bool persistent); } php_stream_filter_factory; BEGIN_EXTERN_C() PHPAPI zend_result php_stream_filter_register_factory(const char *filterpattern, const php_stream_filter_factory *factory); PHPAPI zend_result php_stream_filter_unregister_factory(const char *filterpattern); PHPAPI zend_result php_stream_filter_register_factory_volatile(zend_string *filterpattern, const php_stream_filter_factory *factory); -PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, uint8_t persistent); +PHPAPI php_stream_filter *php_stream_filter_create(const char *filtername, zval *filterparams, bool persistent); END_EXTERN_C() diff --git a/main/streams/streams.c b/main/streams/streams.c index 98978ad3f7daa..de556334b3662 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1377,8 +1377,9 @@ static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_s static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter, bool is_start_seeking) { while (filter) { - if ((filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START && is_start_seeking && - filter->feops->seek(stream, filter, 0, SEEK_SET))) { + if (((filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START && is_start_seeking) || + filter->seekable == PHP_STREAM_FILTER_SEEKABLE_CHECK) && + filter->fops->seek(stream, filter, 0, SEEK_SET)) { php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label); return FAILURE; } From 144a9e74b87a8a0e7146a5fc4de682ded83208f6 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 23 Nov 2025 18:36:22 +0100 Subject: [PATCH 03/16] stream: add filter seek support to user filter --- ext/standard/user_filters.c | 81 ++++++++++++++++++++++++++++- ext/standard/user_filters.stub.php | 3 ++ ext/standard/user_filters_arginfo.h | 9 +++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 40ad6a5da37d2..7eaae26ef9358 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -212,8 +212,86 @@ static php_stream_filter_status_t userfilter_filter( return ret; } +static zend_result userfilter_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + zval *obj = &thisfilter->abstract; + zval func_name; + zval retval; + zval args[2]; + int call_result; + + /* the userfilter object probably doesn't exist anymore */ + if (CG(unclean_shutdown)) { + return FAILURE; + } + + /* Check if the seek method exists */ + zend_string *method_name = ZSTR_INIT_LITERAL("seek", 0); + if (!zend_hash_exists(&Z_OBJCE_P(obj)->function_table, method_name)) { + zend_string_release(method_name); + /* Method doesn't exist - consider this a successful seek for BC */ + return SUCCESS; + } + zend_string_release(method_name); + + /* Make sure the stream is not closed while the filter callback executes. */ + uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; + + zval *stream_prop = zend_hash_str_find_ind(Z_OBJPROP_P(obj), "stream", sizeof("stream")-1); + if (stream_prop) { + /* Give the userfilter class a hook back to the stream */ + zval_ptr_dtor(stream_prop); + php_stream_to_zval(stream, stream_prop); + Z_ADDREF_P(stream_prop); + } + + ZVAL_STRINGL(&func_name, "seek", sizeof("seek")-1); + + /* Setup calling arguments */ + ZVAL_LONG(&args[0], offset); + ZVAL_LONG(&args[1], whence); + + call_result = call_user_function(NULL, + obj, + &func_name, + &retval, + 2, args); + + zval_ptr_dtor(&func_name); + + zend_result ret = FAILURE; + if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) { + ret = zend_is_true(&retval) ? SUCCESS : FAILURE; + zval_ptr_dtor(&retval); + } else if (call_result == FAILURE) { + php_error_docref(NULL, E_WARNING, "Failed to call seek function"); + } + + /* filter resources are cleaned up by the stream destructor, + * keeping a reference to the stream resource here would prevent it + * from being destroyed properly */ + if (stream_prop) { + convert_to_null(stream_prop); + } + + zval_ptr_dtor(&args[1]); + zval_ptr_dtor(&args[0]); + + stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= orig_no_fclose; + + return ret; +} + static const php_stream_filter_ops userfilter_ops = { userfilter_filter, + userfilter_seek, userfilter_dtor, "user-filter" }; @@ -282,7 +360,8 @@ static php_stream_filter *user_filter_factory_create(const char *filtername, return NULL; } - filter = php_stream_filter_alloc(&userfilter_ops, NULL, 0); + filter = php_stream_filter_alloc(&userfilter_ops, NULL, false, + PHP_STREAM_FILTER_SEEKABLE_CHECK); /* filtername */ add_property_string(&obj, "filtername", filtername); diff --git a/ext/standard/user_filters.stub.php b/ext/standard/user_filters.stub.php index acaa42bb33c84..475ec58e79e78 100644 --- a/ext/standard/user_filters.stub.php +++ b/ext/standard/user_filters.stub.php @@ -49,6 +49,9 @@ class php_user_filter */ public function filter($in, $out, &$consumed, bool $closing): int {} + /** @tentative-return-type */ + public function seek(int $offset, int $whence): bool {} + /** @tentative-return-type */ public function onCreate(): bool {} diff --git a/ext/standard/user_filters_arginfo.h b/ext/standard/user_filters_arginfo.h index 9fd13204e2322..71348961cec41 100644 --- a/ext/standard/user_filters_arginfo.h +++ b/ext/standard/user_filters_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 33264435fe01a2cc9aa21a4a087dbbf3c4007206 */ + * Stub hash: 593afbcb51bb35207b93ac7556b92ac845043116 */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_filter, 0, 4, IS_LONG, 0) ZEND_ARG_INFO(0, in) @@ -8,6 +8,11 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_ ZEND_ARG_TYPE_INFO(0, closing, _IS_BOOL, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_seek, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, offset, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, whence, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_onCreate, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -15,11 +20,13 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_php_user_filter_ ZEND_END_ARG_INFO() ZEND_METHOD(php_user_filter, filter); +ZEND_METHOD(php_user_filter, seek); ZEND_METHOD(php_user_filter, onCreate); ZEND_METHOD(php_user_filter, onClose); static const zend_function_entry class_php_user_filter_methods[] = { ZEND_ME(php_user_filter, filter, arginfo_class_php_user_filter_filter, ZEND_ACC_PUBLIC) + ZEND_ME(php_user_filter, seek, arginfo_class_php_user_filter_seek, ZEND_ACC_PUBLIC) ZEND_ME(php_user_filter, onCreate, arginfo_class_php_user_filter_onCreate, ZEND_ACC_PUBLIC) ZEND_ME(php_user_filter, onClose, arginfo_class_php_user_filter_onClose, ZEND_ACC_PUBLIC) ZEND_FE_END From 235c0136826712e0e6e8f1fdb69abce046d209cb Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 23 Nov 2025 19:35:19 +0100 Subject: [PATCH 04/16] stream: add filter seek support to standard filters --- ext/standard/filters.c | 131 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 6 deletions(-) diff --git a/ext/standard/filters.c b/ext/standard/filters.c index 34393be54d8b4..5a2188760c17c 100644 --- a/ext/standard/filters.c +++ b/ext/standard/filters.c @@ -59,12 +59,14 @@ static php_stream_filter_status_t strfilter_rot13_filter( static const php_stream_filter_ops strfilter_rot13_ops = { strfilter_rot13_filter, NULL, + NULL, "string.rot13" }; static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, uint8_t persistent) { - return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent); + return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent, + PHP_STREAM_FILTER_SEEKABLE_ALWAYS); } static const php_stream_filter_factory strfilter_rot13_factory = { @@ -135,23 +137,27 @@ static php_stream_filter_status_t strfilter_tolower_filter( static const php_stream_filter_ops strfilter_toupper_ops = { strfilter_toupper_filter, NULL, + NULL, "string.toupper" }; static const php_stream_filter_ops strfilter_tolower_ops = { strfilter_tolower_filter, NULL, + NULL, "string.tolower" }; static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, uint8_t persistent) { - return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent); + return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent, + PHP_STREAM_FILTER_SEEKABLE_ALWAYS); } static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, uint8_t persistent) { - return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent); + return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent, + PHP_STREAM_FILTER_SEEKABLE_ALWAYS); } static const php_stream_filter_factory strfilter_toupper_factory = { @@ -181,14 +187,17 @@ typedef struct _php_conv php_conv; typedef php_conv_err_t (*php_conv_convert_func)(php_conv *, const char **, size_t *, char **, size_t *); typedef void (*php_conv_dtor_func)(php_conv *); +typedef php_conv_err_t (*php_conv_reset_func)(php_conv *); struct _php_conv { php_conv_convert_func convert_op; php_conv_dtor_func dtor; + php_conv_reset_func reset_op; }; #define php_conv_convert(a, b, c, d, e) ((php_conv *)(a))->convert_op((php_conv *)(a), (b), (c), (d), (e)) #define php_conv_dtor(a) ((php_conv *)a)->dtor((a)) +#define php_conv_reset(a) (((php_conv *)(a))->reset_op ? ((php_conv *)(a))->reset_op((php_conv *)(a)) : PHP_CONV_ERR_SUCCESS) /* {{{ php_conv_base64_encode */ typedef struct _php_conv_base64_encode { @@ -206,6 +215,7 @@ typedef struct _php_conv_base64_encode { static php_conv_err_t php_conv_base64_encode_convert(php_conv_base64_encode *inst, const char **in_p, size_t *in_left, char **out_p, size_t *out_left); static void php_conv_base64_encode_dtor(php_conv_base64_encode *inst); +static php_conv_err_t php_conv_base64_encode_reset(php_conv_base64_encode *inst); static const unsigned char b64_tbl_enc[256] = { 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', @@ -226,10 +236,19 @@ static const unsigned char b64_tbl_enc[256] = { 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; +static php_conv_err_t php_conv_base64_encode_reset(php_conv_base64_encode *inst) +{ + /* Reset only mutable state, preserve configuration (lbchars, line_len, etc.) */ + inst->erem_len = 0; + inst->line_ccnt = inst->line_len; + return PHP_CONV_ERR_SUCCESS; +} + static php_conv_err_t php_conv_base64_encode_ctor(php_conv_base64_encode *inst, unsigned int line_len, const char *lbchars, size_t lbchars_len, int lbchars_dup, bool persistent) { inst->_super.convert_op = (php_conv_convert_func) php_conv_base64_encode_convert; inst->_super.dtor = (php_conv_dtor_func) php_conv_base64_encode_dtor; + inst->_super.reset_op = (php_conv_reset_func) php_conv_base64_encode_reset; inst->erem_len = 0; inst->line_ccnt = line_len; inst->line_len = line_len; @@ -449,6 +468,7 @@ typedef struct _php_conv_base64_decode { static php_conv_err_t php_conv_base64_decode_convert(php_conv_base64_decode *inst, const char **in_p, size_t *in_left, char **out_p, size_t *out_left); static void php_conv_base64_decode_dtor(php_conv_base64_decode *inst); +static php_conv_err_t php_conv_base64_decode_reset(php_conv_base64_decode *inst); static unsigned int b64_tbl_dec[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, @@ -469,10 +489,21 @@ static unsigned int b64_tbl_dec[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 }; +static php_conv_err_t php_conv_base64_decode_reset(php_conv_base64_decode *inst) +{ + /* Reset only mutable state */ + inst->urem = 0; + inst->urem_nbits = 0; + inst->ustat = 0; + inst->eos = 0; + return PHP_CONV_ERR_SUCCESS; +} + static void php_conv_base64_decode_ctor(php_conv_base64_decode *inst) { inst->_super.convert_op = (php_conv_convert_func) php_conv_base64_decode_convert; inst->_super.dtor = (php_conv_dtor_func) php_conv_base64_decode_dtor; + inst->_super.reset_op = (php_conv_reset_func) php_conv_base64_decode_reset; inst->urem = 0; inst->urem_nbits = 0; @@ -618,6 +649,7 @@ typedef struct _php_conv_qprint_encode { static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst); static php_conv_err_t php_conv_qprint_encode_convert(php_conv_qprint_encode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p); +static php_conv_err_t php_conv_qprint_encode_reset(php_conv_qprint_encode *inst); static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst) { @@ -627,6 +659,15 @@ static void php_conv_qprint_encode_dtor(php_conv_qprint_encode *inst) } } +static php_conv_err_t php_conv_qprint_encode_reset(php_conv_qprint_encode *inst) +{ + /* Reset only mutable state, preserve configuration */ + inst->line_ccnt = inst->line_len; + inst->lb_ptr = 0; + inst->lb_cnt = 0; + return PHP_CONV_ERR_SUCCESS; +} + #define NEXT_CHAR(ps, icnt, lb_ptr, lb_cnt, lbchars) \ ((lb_ptr) < (lb_cnt) ? (lbchars)[(lb_ptr)] : *(ps)) @@ -832,6 +873,7 @@ static php_conv_err_t php_conv_qprint_encode_ctor(php_conv_qprint_encode *inst, } inst->_super.convert_op = (php_conv_convert_func) php_conv_qprint_encode_convert; inst->_super.dtor = (php_conv_dtor_func) php_conv_qprint_encode_dtor; + inst->_super.reset_op = (php_conv_reset_func) php_conv_qprint_encode_reset; inst->line_ccnt = line_len; inst->line_len = line_len; if (lbchars != NULL) { @@ -862,6 +904,10 @@ typedef struct _php_conv_qprint_decode { unsigned int lb_cnt; } php_conv_qprint_decode; +static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst); +static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p); +static php_conv_err_t php_conv_qprint_decode_reset(php_conv_qprint_decode *inst); + static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst) { assert(inst != NULL); @@ -870,6 +916,16 @@ static void php_conv_qprint_decode_dtor(php_conv_qprint_decode *inst) } } +static php_conv_err_t php_conv_qprint_decode_reset(php_conv_qprint_decode *inst) +{ + /* Reset only mutable state, preserve configuration */ + inst->scan_stat = 0; + inst->next_char = 0; + inst->lb_ptr = 0; + inst->lb_cnt = 0; + return PHP_CONV_ERR_SUCCESS; +} + static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p) { php_conv_err_t err = PHP_CONV_ERR_SUCCESS; @@ -1040,10 +1096,12 @@ static php_conv_err_t php_conv_qprint_decode_convert(php_conv_qprint_decode *ins return err; } + static php_conv_err_t php_conv_qprint_decode_ctor(php_conv_qprint_decode *inst, const char *lbchars, size_t lbchars_len, int lbchars_dup, bool persistent) { inst->_super.convert_op = (php_conv_convert_func) php_conv_qprint_decode_convert; inst->_super.dtor = (php_conv_dtor_func) php_conv_qprint_decode_dtor; + inst->_super.reset_op = (php_conv_reset_func) php_conv_qprint_decode_reset; inst->scan_stat = 0; inst->next_char = 0; inst->lb_ptr = inst->lb_cnt = 0; @@ -1540,6 +1598,29 @@ static php_stream_filter_status_t strfilter_convert_filter( return PSFS_ERR_FATAL; } +static zend_result strfilter_convert_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_convert_filter *inst = (php_convert_filter *)Z_PTR(thisfilter->abstract); + + /* Reset stub buffer */ + inst->stub_len = 0; + + /* Reset the converter state - preserves all configuration/options */ + if (inst->cd != NULL) { + php_conv_err_t err = php_conv_reset(inst->cd); + if (err != PHP_CONV_ERR_SUCCESS) { + return FAILURE; + } + } + + return SUCCESS; +} + static void strfilter_convert_dtor(php_stream_filter *thisfilter) { assert(Z_PTR(thisfilter->abstract) != NULL); @@ -1550,6 +1631,7 @@ static void strfilter_convert_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops strfilter_convert_ops = { strfilter_convert_filter, + strfilter_convert_seek, strfilter_convert_dtor, "convert.*" }; @@ -1590,7 +1672,8 @@ static php_stream_filter *strfilter_convert_create(const char *filtername, zval return NULL; } - return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent); + return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent, + PHP_STREAM_FILTER_SEEKABLE_START); } static const php_stream_filter_factory strfilter_convert_factory = { @@ -1637,6 +1720,22 @@ static php_stream_filter_status_t consumed_filter_filter( return PSFS_PASS_ON; } +static zend_result consumed_filter_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_consumed_filter_data *data = (php_consumed_filter_data *)Z_PTR(thisfilter->abstract); + + /* Reset consumed state */ + data->consumed = 0; + data->offset = ~0; + + return SUCCESS; +} + static void consumed_filter_dtor(php_stream_filter *thisfilter) { if (thisfilter && Z_PTR(thisfilter->abstract)) { @@ -1647,6 +1746,7 @@ static void consumed_filter_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops consumed_filter_ops = { consumed_filter_filter, + consumed_filter_seek, consumed_filter_dtor, "consumed" }; @@ -1667,7 +1767,8 @@ static php_stream_filter *consumed_filter_create(const char *filtername, zval *f data->offset = ~0; fops = &consumed_filter_ops; - return php_stream_filter_alloc(fops, data, persistent); + return php_stream_filter_alloc(fops, data, persistent, + PHP_STREAM_FILTER_SEEKABLE_START); } static const php_stream_filter_factory consumed_filter_factory = { @@ -1851,6 +1952,22 @@ static php_stream_filter_status_t php_chunked_filter( return PSFS_PASS_ON; } +static zend_result php_chunked_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_chunked_filter_data *data = (php_chunked_filter_data *)Z_PTR(thisfilter->abstract); + + /* Reset chunked decoder state */ + data->state = CHUNK_SIZE_START; + data->chunk_size = 0; + + return SUCCESS; +} + static void php_chunked_dtor(php_stream_filter *thisfilter) { if (thisfilter && Z_PTR(thisfilter->abstract)) { @@ -1861,6 +1978,7 @@ static void php_chunked_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops chunked_filter_ops = { php_chunked_filter, + php_chunked_seek, php_chunked_dtor, "dechunk" }; @@ -1881,7 +1999,8 @@ static php_stream_filter *chunked_filter_create(const char *filtername, zval *fi data->persistent = persistent; fops = &chunked_filter_ops; - return php_stream_filter_alloc(fops, data, persistent); + return php_stream_filter_alloc(fops, data, persistent, + PHP_STREAM_FILTER_SEEKABLE_START); } static const php_stream_filter_factory chunked_filter_factory = { From dff82386e456ba93f30a3ade3a9892b86dfa3b65 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 23 Nov 2025 22:49:33 +0100 Subject: [PATCH 05/16] stream: add filter seek support to iconv filter --- ext/iconv/iconv.c | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/ext/iconv/iconv.c b/ext/iconv/iconv.c index 7c812f5af44b4..1d58528e460a0 100644 --- a/ext/iconv/iconv.c +++ b/ext/iconv/iconv.c @@ -2286,7 +2286,6 @@ PHP_FUNCTION(iconv_get_encoding) } /* }}} */ - /* {{{ iconv stream filter */ typedef struct _php_iconv_stream_filter { iconv_t cd; @@ -2554,6 +2553,32 @@ static php_stream_filter_status_t php_iconv_stream_filter_do_filter( } /* }}} */ +/* {{{ php_iconv_stream_filter_seek */ +static zend_result php_iconv_stream_filter_seek( + php_stream *stream, + php_stream_filter *filter, + zend_off_t offset, + int whence) +{ + php_iconv_stream_filter *self = (php_iconv_stream_filter *)Z_PTR(filter->abstract); + + /* Reset stub buffer */ + self->stub_len = 0; + + /* Reset iconv conversion state by closing and reopening the converter */ + iconv_close(self->cd); + + if ((iconv_t)-1 == (self->cd = iconv_open(self->to_charset, self->from_charset))) { + php_error_docref(NULL, E_WARNING, + "iconv stream filter (\"%s\"=>\"%s\"): failed to reset conversion state", + self->from_charset, self->to_charset); + return FAILURE; + } + + return SUCCESS; +} +/* }}} */ + /* {{{ php_iconv_stream_filter_cleanup */ static void php_iconv_stream_filter_cleanup(php_stream_filter *filter) { @@ -2564,6 +2589,7 @@ static void php_iconv_stream_filter_cleanup(php_stream_filter *filter) static const php_stream_filter_ops php_iconv_stream_filter_ops = { php_iconv_stream_filter_do_filter, + php_iconv_stream_filter_seek, php_iconv_stream_filter_cleanup, "convert.iconv.*" }; @@ -2601,7 +2627,8 @@ static php_stream_filter *php_iconv_stream_filter_factory_create(const char *nam return NULL; } - return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent); + return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent, + PHP_STREAM_FILTER_SEEKABLE_START); } /* }}} */ From 36000b5638f8cc4f822fde96b4298aef41adeec1 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 12:49:50 +0100 Subject: [PATCH 06/16] stream: add filter seek support to zlib filters --- ext/zlib/zlib_filter.c | 74 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c index e42132fd0008c..a3c9e213f2257 100644 --- a/ext/zlib/zlib_filter.c +++ b/ext/zlib/zlib_filter.c @@ -143,6 +143,41 @@ static php_stream_filter_status_t php_zlib_inflate_filter( return exit_status; } +static zend_result php_zlib_inflate_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_zlib_filter_data *data; + int status; + + if (!thisfilter || !Z_PTR(thisfilter->abstract)) { + return FAILURE; + } + + data = (php_zlib_filter_data *)(Z_PTR(thisfilter->abstract)); + + if (!data->finished) { + /* Stream is active, just reset it */ + status = inflateReset(&(data->strm)); + if (status != Z_OK) { + php_error_docref(NULL, E_WARNING, "zlib.inflate: failed to reset inflation state"); + return FAILURE; + } + } + + /* Reset our own state */ + data->strm.next_in = data->inbuf; + data->strm.avail_in = 0; + data->strm.next_out = data->outbuf; + data->strm.avail_out = data->outbuf_len; + data->finished = false; + + return SUCCESS; +} + static void php_zlib_inflate_dtor(php_stream_filter *thisfilter) { if (thisfilter && Z_PTR(thisfilter->abstract)) { @@ -158,6 +193,7 @@ static void php_zlib_inflate_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops php_zlib_inflate_ops = { php_zlib_inflate_filter, + php_zlib_inflate_seek, php_zlib_inflate_dtor, "zlib.inflate" }; @@ -259,6 +295,39 @@ static php_stream_filter_status_t php_zlib_deflate_filter( return exit_status; } +static zend_result php_zlib_deflate_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_zlib_filter_data *data; + int status; + + if (!thisfilter || !Z_PTR(thisfilter->abstract)) { + return FAILURE; + } + + data = (php_zlib_filter_data *)(Z_PTR(thisfilter->abstract)); + + /* Reset zlib deflation state */ + status = deflateReset(&(data->strm)); + if (status != Z_OK) { + php_error_docref(NULL, E_WARNING, "zlib.deflate: failed to reset deflation state"); + return FAILURE; + } + + /* Reset our own state */ + data->strm.next_in = data->inbuf; + data->strm.avail_in = 0; + data->strm.next_out = data->outbuf; + data->strm.avail_out = data->outbuf_len; + data->finished = true; + + return SUCCESS; +} + static void php_zlib_deflate_dtor(php_stream_filter *thisfilter) { if (thisfilter && Z_PTR(thisfilter->abstract)) { @@ -272,6 +341,7 @@ static void php_zlib_deflate_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops php_zlib_deflate_ops = { php_zlib_deflate_filter, + php_zlib_deflate_seek, php_zlib_deflate_dtor, "zlib.deflate" }; @@ -315,6 +385,7 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f } data->strm.data_type = Z_ASCII; + data->persistent = persistent; if (strcasecmp(filtername, "zlib.inflate") == 0) { int windowBits = -MAX_WBITS; @@ -401,6 +472,7 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f php_error_docref(NULL, E_WARNING, "Invalid filter parameter, ignored"); } } + status = deflateInit2(&(data->strm), level, Z_DEFLATED, windowBits, memLevel, 0); data->finished = true; fops = &php_zlib_deflate_ops; @@ -416,7 +488,7 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f return NULL; } - return php_stream_filter_alloc(fops, data, persistent); + return php_stream_filter_alloc(fops, data, persistent, PHP_STREAM_FILTER_SEEKABLE_START); } const php_stream_filter_factory php_zlib_filter_factory = { From 9aca3c60f427554abe57718166444d9781d7d756 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 15:02:49 +0100 Subject: [PATCH 07/16] stream: add filter seek support to bz2 filters --- ext/bz2/bz2_filter.c | 80 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c index 27059391dacb9..09740d0aa1a6c 100644 --- a/ext/bz2/bz2_filter.c +++ b/ext/bz2/bz2_filter.c @@ -42,6 +42,10 @@ typedef struct _php_bz2_filter_data { unsigned int is_flushed : 1; /* only for compression */ int persistent; + + /* Configuration for reset - immutable */ + int blockSize100k; /* compress only */ + int workFactor; /* compress only */ } php_bz2_filter_data; /* }}} */ @@ -178,6 +182,38 @@ static php_stream_filter_status_t php_bz2_decompress_filter( return exit_status; } +static zend_result php_bz2_decompress_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_bz2_filter_data *data; + + if (!Z_PTR(thisfilter->abstract)) { + return FAILURE; + } + + data = (php_bz2_filter_data *)Z_PTR(thisfilter->abstract); + + /* End current decompression if running */ + if (data->status == PHP_BZ2_RUNNING) { + BZ2_bzDecompressEnd(&(data->strm)); + } + + /* Reset stream state */ + data->strm.next_in = data->inbuf; + data->strm.avail_in = 0; + data->strm.next_out = data->outbuf; + data->strm.avail_out = data->outbuf_len; + data->status = PHP_BZ2_UNINITIALIZED; + + /* Note: We don't reinitialize here - it will be done on first use in the filter function */ + + return SUCCESS; +} + static void php_bz2_decompress_dtor(php_stream_filter *thisfilter) { if (thisfilter && Z_PTR(thisfilter->abstract)) { @@ -193,6 +229,7 @@ static void php_bz2_decompress_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops php_bz2_decompress_ops = { php_bz2_decompress_filter, + php_bz2_decompress_seek, php_bz2_decompress_dtor, "bzip2.decompress" }; @@ -288,6 +325,42 @@ static php_stream_filter_status_t php_bz2_compress_filter( return exit_status; } +static zend_result php_bz2_compress_seek( + php_stream *stream, + php_stream_filter *thisfilter, + zend_off_t offset, + int whence + ) +{ + php_bz2_filter_data *data; + int status; + + if (!Z_PTR(thisfilter->abstract)) { + return FAILURE; + } + + data = (php_bz2_filter_data *)Z_PTR(thisfilter->abstract); + + /* End current compression */ + BZ2_bzCompressEnd(&(data->strm)); + + /* Reset stream state */ + data->strm.next_in = data->inbuf; + data->strm.avail_in = 0; + data->strm.next_out = data->outbuf; + data->strm.avail_out = data->outbuf_len; + data->is_flushed = 1; + + /* Reinitialize compression with saved configuration */ + status = BZ2_bzCompressInit(&(data->strm), data->blockSize100k, 0, data->workFactor); + if (status != BZ_OK) { + php_error_docref(NULL, E_WARNING, "bzip2.compress: failed to reset compression state"); + return FAILURE; + } + + return SUCCESS; +} + static void php_bz2_compress_dtor(php_stream_filter *thisfilter) { if (Z_PTR(thisfilter->abstract)) { @@ -301,6 +374,7 @@ static void php_bz2_compress_dtor(php_stream_filter *thisfilter) static const php_stream_filter_ops php_bz2_compress_ops = { php_bz2_compress_filter, + php_bz2_compress_seek, php_bz2_compress_dtor, "bzip2.compress" }; @@ -388,6 +462,10 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi } } + /* Save configuration for reset */ + data->blockSize100k = blockSize100k; + data->workFactor = workFactor; + status = BZ2_bzCompressInit(&(data->strm), blockSize100k, 0, workFactor); data->is_flushed = 1; fops = &php_bz2_compress_ops; @@ -403,7 +481,7 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi return NULL; } - return php_stream_filter_alloc(fops, data, persistent); + return php_stream_filter_alloc(fops, data, persistent, PHP_STREAM_FILTER_SEEKABLE_START); } const php_stream_filter_factory php_bz2_filter_factory = { From 3a93a8cf3dec26beab4b0f3bbdd0bd05edde8f47 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 16:21:46 +0100 Subject: [PATCH 08/16] stream: add missing php_user_filter::seek implementation --- ext/standard/user_filters.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 7eaae26ef9358..6881b6c2bb8c5 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -48,6 +48,16 @@ PHP_METHOD(php_user_filter, filter) RETURN_LONG(PSFS_ERR_FATAL); } +PHP_METHOD(php_user_filter, seek) +{ + zend_long offset, whence; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "ll", &offset, &whence) == FAILURE) { + RETURN_THROWS(); + } + + RETURN_TRUE; +} + PHP_METHOD(php_user_filter, onCreate) { ZEND_PARSE_PARAMETERS_NONE(); From a3faa78867e0135310965373cf2e298bd7a3851d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 16:26:40 +0100 Subject: [PATCH 09/16] stream: fix new filter factory create signature --- ext/bz2/bz2_filter.c | 2 +- ext/iconv/iconv.c | 2 +- ext/standard/filters.c | 12 ++++++------ ext/standard/user_filters.c | 2 +- ext/zlib/zlib_filter.c | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c index 09740d0aa1a6c..97222537a2df0 100644 --- a/ext/bz2/bz2_filter.c +++ b/ext/bz2/bz2_filter.c @@ -383,7 +383,7 @@ static const php_stream_filter_ops php_bz2_compress_ops = { /* {{{ bzip2.* common factory */ -static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, bool persistent) { const php_stream_filter_ops *fops = NULL; php_bz2_filter_data *data; diff --git a/ext/iconv/iconv.c b/ext/iconv/iconv.c index 1d58528e460a0..32f53e82d7609 100644 --- a/ext/iconv/iconv.c +++ b/ext/iconv/iconv.c @@ -2595,7 +2595,7 @@ static const php_stream_filter_ops php_iconv_stream_filter_ops = { }; /* {{{ php_iconv_stream_filter_create */ -static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, uint8_t persistent) +static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, bool persistent) { php_iconv_stream_filter *inst; const char *from_charset = NULL, *to_charset = NULL; diff --git a/ext/standard/filters.c b/ext/standard/filters.c index 5a2188760c17c..5538a8defebe6 100644 --- a/ext/standard/filters.c +++ b/ext/standard/filters.c @@ -63,7 +63,7 @@ static const php_stream_filter_ops strfilter_rot13_ops = { "string.rot13" }; -static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, bool persistent) { return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent, PHP_STREAM_FILTER_SEEKABLE_ALWAYS); @@ -148,13 +148,13 @@ static const php_stream_filter_ops strfilter_tolower_ops = { "string.tolower" }; -static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, bool persistent) { return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent, PHP_STREAM_FILTER_SEEKABLE_ALWAYS); } -static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, bool persistent) { return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent, PHP_STREAM_FILTER_SEEKABLE_ALWAYS); @@ -1636,7 +1636,7 @@ static const php_stream_filter_ops strfilter_convert_ops = { "convert.*" }; -static php_stream_filter *strfilter_convert_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *strfilter_convert_create(const char *filtername, zval *filterparams, bool persistent) { php_convert_filter *inst; @@ -1751,7 +1751,7 @@ static const php_stream_filter_ops consumed_filter_ops = { "consumed" }; -static php_stream_filter *consumed_filter_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *consumed_filter_create(const char *filtername, zval *filterparams, bool persistent) { const php_stream_filter_ops *fops = NULL; php_consumed_filter_data *data; @@ -1983,7 +1983,7 @@ static const php_stream_filter_ops chunked_filter_ops = { "dechunk" }; -static php_stream_filter *chunked_filter_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *chunked_filter_create(const char *filtername, zval *filterparams, bool persistent) { const php_stream_filter_ops *fops = NULL; php_chunked_filter_data *data; diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 6881b6c2bb8c5..5ca7e95aa6933 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -307,7 +307,7 @@ static const php_stream_filter_ops userfilter_ops = { }; static php_stream_filter *user_filter_factory_create(const char *filtername, - zval *filterparams, uint8_t persistent) + zval *filterparams, bool persistent) { struct php_user_filter_data *fdat = NULL; php_stream_filter *filter; diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c index a3c9e213f2257..157bfb42c049c 100644 --- a/ext/zlib/zlib_filter.c +++ b/ext/zlib/zlib_filter.c @@ -350,7 +350,7 @@ static const php_stream_filter_ops php_zlib_deflate_ops = { /* {{{ zlib.* common factory */ -static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *filterparams, uint8_t persistent) +static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *filterparams, bool persistent) { const php_stream_filter_ops *fops = NULL; php_zlib_filter_data *data; From 3143a6421f374b6ede63d50a730c4f0cbd8245b8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 17:06:45 +0100 Subject: [PATCH 10/16] stream: use seekable filter for bug 79945 test --- ext/gd/tests/bug79945.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/gd/tests/bug79945.phpt b/ext/gd/tests/bug79945.phpt index 5db958e36d358..46dac358d8e21 100644 --- a/ext/gd/tests/bug79945.phpt +++ b/ext/gd/tests/bug79945.phpt @@ -9,17 +9,17 @@ if (!(imagetypes() & IMG_PNG)) { } set_error_handler(function($errno, $errstr) { if (str_contains($errstr, 'Cannot cast a filtered stream on this system')) { - die('skip: fopencookie not support on this system'); + die('skip: fopencookie not supported on this system'); } }); -imagecreatefrompng('php://filter/read=convert.base64-encode/resource=' . __DIR__ . '/test.png'); +imagecreatefrompng('php://filter/read=string.rot13/resource=' . __DIR__ . '/test.png'); restore_error_handler(); ?> --FILE-- --CLEAN-- --EXPECTF-- -Warning: imagecreatefrompng(): "php://filter/read=convert.base64-encode/resource=%s" is not a valid PNG file in %s on line %d +Warning: imagecreatefrompng(): "php://filter/read=string.rot13/resource=%s" is not a valid PNG file in %s on line %d From 9ababf80f22a8104dc190f21aa2792f950435e29 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 19:19:35 +0100 Subject: [PATCH 11/16] stream: add test for bz2 filter seeking --- ext/bz2/tests/bz2_filter_seek_compress.phpt | 55 +++++++++++++++++++ ext/bz2/tests/bz2_filter_seek_decompress.phpt | 43 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 ext/bz2/tests/bz2_filter_seek_compress.phpt create mode 100644 ext/bz2/tests/bz2_filter_seek_decompress.phpt diff --git a/ext/bz2/tests/bz2_filter_seek_compress.phpt b/ext/bz2/tests/bz2_filter_seek_compress.phpt new file mode 100644 index 0000000000000..40ea62f66f190 --- /dev/null +++ b/ext/bz2/tests/bz2_filter_seek_compress.phpt @@ -0,0 +1,55 @@ +--TEST-- +bzip2.compress filter with seek to start +--EXTENSIONS-- +bz2 +--FILE-- + $size1 ? "YES" : "NO") . "\n"; + +$result = fseek($fp, 50, SEEK_SET); +echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +fclose($fp); + +$fp = fopen($file, 'r'); +stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ); +$content = stream_get_contents($fp); +fclose($fp); + +echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n"; +?> +--CLEAN-- + +--EXPECTF-- +Size after first write: 40 +Seek to start: SUCCESS +Size after second write: 98 +Second write is larger: YES + +Warning: fseek(): Stream filter bzip2.compress is not seekable in %s on line %d +Seek to middle: FAILURE +Decompressed content matches text2: YES diff --git a/ext/bz2/tests/bz2_filter_seek_decompress.phpt b/ext/bz2/tests/bz2_filter_seek_decompress.phpt new file mode 100644 index 0000000000000..89a2bb2ff31b1 --- /dev/null +++ b/ext/bz2/tests/bz2_filter_seek_decompress.phpt @@ -0,0 +1,43 @@ +--TEST-- +bzip2.decompress filter with seek to start +--EXTENSIONS-- +bz2 +--FILE-- + +--CLEAN-- + +--EXPECTF-- +First read (20 bytes): I am the very model +Seek to start: SUCCESS +Content after seek matches: YES + +Warning: fseek(): Stream filter bzip2.decompress is not seekable in %s on line %d +Seek to middle: FAILURE From 6bcc643d060c825c78c7f5d6bf98fa0c990c4801 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 19:19:52 +0100 Subject: [PATCH 12/16] stream: add tests and fix zlib filter seeking --- ext/zlib/tests/zlib_filter_seek_deflate.phpt | 55 ++++++++++++++++++++ ext/zlib/tests/zlib_filter_seek_inflate.phpt | 45 ++++++++++++++++ ext/zlib/zlib_filter.c | 16 +++++- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 ext/zlib/tests/zlib_filter_seek_deflate.phpt create mode 100644 ext/zlib/tests/zlib_filter_seek_inflate.phpt diff --git a/ext/zlib/tests/zlib_filter_seek_deflate.phpt b/ext/zlib/tests/zlib_filter_seek_deflate.phpt new file mode 100644 index 0000000000000..0cf6793331df1 --- /dev/null +++ b/ext/zlib/tests/zlib_filter_seek_deflate.phpt @@ -0,0 +1,55 @@ +--TEST-- +zlib.deflate filter with seek to start +--EXTENSIONS-- +zlib +--FILE-- + $size1 ? "YES" : "NO") . "\n"; + +$result = fseek($fp, 50, SEEK_SET); +echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +fclose($fp); + +$fp = fopen($file, 'r'); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ); +$content = stream_get_contents($fp); +fclose($fp); + +echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n"; +?> +--CLEAN-- + +--EXPECTF-- +Size after first write: %d +Seek to start: SUCCESS +Size after second write: %d +Second write is larger: YES + +Warning: fseek(): Stream filter zlib.deflate is not seekable in %s on line %d +Seek to middle: FAILURE +Decompressed content matches text2: YES diff --git a/ext/zlib/tests/zlib_filter_seek_inflate.phpt b/ext/zlib/tests/zlib_filter_seek_inflate.phpt new file mode 100644 index 0000000000000..2895fc692b748 --- /dev/null +++ b/ext/zlib/tests/zlib_filter_seek_inflate.phpt @@ -0,0 +1,45 @@ +--TEST-- +zlib.inflate filter with seek to start +--EXTENSIONS-- +zlib +--FILE-- + +--CLEAN-- + +--EXPECTF-- +First read (20 bytes): I am the very model +Seek to start: SUCCESS +Position after seek: 0 +Content after seek matches: YES + +Warning: fseek(): Stream filter zlib.inflate is not seekable in %s on line %d +Seek to middle: FAILURE diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c index 157bfb42c049c..e1e201d8e3324 100644 --- a/ext/zlib/zlib_filter.c +++ b/ext/zlib/zlib_filter.c @@ -28,6 +28,7 @@ typedef struct _php_zlib_filter_data { size_t outbuf_len; int persistent; bool finished; /* for zlib.deflate: signals that no flush is pending */ + int windowBits; } php_zlib_filter_data; /* }}} */ @@ -159,7 +160,14 @@ static zend_result php_zlib_inflate_seek( data = (php_zlib_filter_data *)(Z_PTR(thisfilter->abstract)); - if (!data->finished) { + if (data->finished) { + /* Stream was ended, need to reinitialize */ + status = inflateInit2(&(data->strm), data->windowBits); + if (status != Z_OK) { + php_error_docref(NULL, E_WARNING, "zlib.inflate: failed to reinitialize inflation state"); + return FAILURE; + } + } else { /* Stream is active, just reset it */ status = inflateReset(&(data->strm)); if (status != Z_OK) { @@ -405,6 +413,9 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f } } + /* Save configuration for reset */ + data->windowBits = windowBits; + /* RFC 1951 Inflate */ data->finished = false; status = inflateInit2(&(data->strm), windowBits); @@ -473,6 +484,9 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f } } + /* Save configuration for reset */ + data->windowBits = windowBits; + status = deflateInit2(&(data->strm), level, Z_DEFLATED, windowBits, memLevel, 0); data->finished = true; fops = &php_zlib_deflate_ops; From e648236283af0cafac17938d4b5164eabb75b255 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 19:40:48 +0100 Subject: [PATCH 13/16] stream: add test for iconv filter seeking --- ext/iconv/tests/iconv_stream_filter_seek.phpt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ext/iconv/tests/iconv_stream_filter_seek.phpt diff --git a/ext/iconv/tests/iconv_stream_filter_seek.phpt b/ext/iconv/tests/iconv_stream_filter_seek.phpt new file mode 100644 index 0000000000000..b045581f76e19 --- /dev/null +++ b/ext/iconv/tests/iconv_stream_filter_seek.phpt @@ -0,0 +1,43 @@ +--TEST-- +iconv stream filter with seek to start +--EXTENSIONS-- +iconv +--FILE-- + +--CLEAN-- + +--EXPECTF-- +First read (20 bytes): Hello, this is a tes +Seek to start: SUCCESS +Content after seek matches: YES + +Warning: fseek(): Stream filter convert.iconv.* is not seekable in %s on line %d +Seek to middle: FAILURE From 76874b38578d45370c4ce87d3258fcecbc66472c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 20:13:36 +0100 Subject: [PATCH 14/16] stream: test string filters seeking --- .../tests/filters/string_filters_seek.phpt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 ext/standard/tests/filters/string_filters_seek.phpt diff --git a/ext/standard/tests/filters/string_filters_seek.phpt b/ext/standard/tests/filters/string_filters_seek.phpt new file mode 100644 index 0000000000000..60eab2ccb84ae --- /dev/null +++ b/ext/standard/tests/filters/string_filters_seek.phpt @@ -0,0 +1,68 @@ +--TEST-- +string filters (rot13, toupper, tolower) with seek - fully seekable +--FILE-- + 'Uryyb Jbeyq! Guvf vf n grfg sbe fgevat svygre frrxvat.', + 'string.toupper' => 'HELLO WORLD! THIS IS A TEST FOR STRING FILTER SEEKING.', + 'string.tolower' => 'hello world! this is a test for string filter seeking.' +]; + +foreach ($filters as $filter) { + echo "Testing filter: $filter\n"; + + file_put_contents($file, $text); + + $fp = fopen($file, 'r'); + stream_filter_append($fp, $filter, STREAM_FILTER_READ); + + $partial = fread($fp, 20); + echo "First read (20 bytes): $partial\n"; + + $result = fseek($fp, 0, SEEK_SET); + echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + + $full = fread($fp, strlen($text)); + echo "Content matches: " . ($full === $expected[$filter] ? "YES" : "NO") . "\n"; + + $result = fseek($fp, 13, SEEK_SET); + echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + + $from_middle = fread($fp, 10); + $expected_middle = substr($expected[$filter], 13, 10); + echo "Read from middle matches: " . ($from_middle === $expected_middle ? "YES" : "NO") . "\n"; + + fclose($fp); + echo "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +Testing filter: string.rot13 +First read (20 bytes): Uryyb Jbeyq! Guvf vf +Seek to start: SUCCESS +Content matches: YES +Seek to middle: SUCCESS +Read from middle matches: YES + +Testing filter: string.toupper +First read (20 bytes): HELLO WORLD! THIS IS +Seek to start: SUCCESS +Content matches: YES +Seek to middle: SUCCESS +Read from middle matches: YES + +Testing filter: string.tolower +First read (20 bytes): hello world! this is +Seek to start: SUCCESS +Content matches: YES +Seek to middle: SUCCESS +Read from middle matches: YES From ccac13291133e70ce738204dc85e20efcd26d6e3 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 20:13:49 +0100 Subject: [PATCH 15/16] stream: test convert filters seeking --- .../tests/filters/convert_filter_seek.phpt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 ext/standard/tests/filters/convert_filter_seek.phpt diff --git a/ext/standard/tests/filters/convert_filter_seek.phpt b/ext/standard/tests/filters/convert_filter_seek.phpt new file mode 100644 index 0000000000000..5ba488756bc68 --- /dev/null +++ b/ext/standard/tests/filters/convert_filter_seek.phpt @@ -0,0 +1,75 @@ +--TEST-- +convert filters (base64, quoted-printable) with seek to start only +--FILE-- + +--CLEAN-- + +--EXPECTF-- +Testing convert.base64-encode/decode +First read (20 bytes): Hello World! This is +Seek to start: SUCCESS +Content matches: YES + +Warning: fseek(): Stream filter convert.* is not seekable in %s on line %d +Seek to middle: FAILURE + +Testing convert.quoted-printable-encode/decode +First read (10 bytes): 4c696e65310d0a4c696e +Seek to start: SUCCESS +Content matches: YES + +Warning: fseek(): Stream filter convert.* is not seekable in %s on line %d +Seek to middle: FAILURE From 73bcbd9d6a5a22b11b7f5bba8127462fe8b3e885 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 24 Nov 2025 22:14:49 +0100 Subject: [PATCH 16/16] stream: add tests and fix user filter seeking --- .../tests/filters/php_user_filter_04.phpt | 28 +++++++ .../tests/filters/user_filter_seek_01.phpt | 81 +++++++++++++++++++ .../tests/filters/user_filter_seek_02.phpt | 75 +++++++++++++++++ main/streams/streams.c | 28 ++++--- 4 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 ext/standard/tests/filters/php_user_filter_04.phpt create mode 100644 ext/standard/tests/filters/user_filter_seek_01.phpt create mode 100644 ext/standard/tests/filters/user_filter_seek_02.phpt diff --git a/ext/standard/tests/filters/php_user_filter_04.phpt b/ext/standard/tests/filters/php_user_filter_04.phpt new file mode 100644 index 0000000000000..d558bfeebb594 --- /dev/null +++ b/ext/standard/tests/filters/php_user_filter_04.phpt @@ -0,0 +1,28 @@ +--TEST-- +php_user_filter with invalid seek signature +--FILE-- + +--EXPECTF-- +Fatal error: Declaration of InvalidSeekFilter::seek($offset): bool must be compatible with php_user_filter::seek(int $offset, int $whence): bool in %s on line %d \ No newline at end of file diff --git a/ext/standard/tests/filters/user_filter_seek_01.phpt b/ext/standard/tests/filters/user_filter_seek_01.phpt new file mode 100644 index 0000000000000..cb4e9fe72267f --- /dev/null +++ b/ext/standard/tests/filters/user_filter_seek_01.phpt @@ -0,0 +1,81 @@ +--TEST-- +php_user_filter with seek method - always seekable (stateless filter) +--FILE-- +data); $i++) { + $char = $bucket->data[$i]; + if (ctype_alpha($char)) { + $base = ctype_upper($char) ? ord('A') : ord('a'); + $rotated .= chr($base + (ord($char) - $base + $this->rotation) % 26); + } else { + $rotated .= $char; + } + } + $bucket->data = $rotated; + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + return PSFS_PASS_ON; + } + + public function onCreate(): bool + { + if (isset($this->params['rotation'])) { + $this->rotation = (int)$this->params['rotation']; + } + return true; + } + + public function onClose(): void {} + + public function seek(int $offset, int $whence): bool + { + // Stateless filter - always seekable to any position + return true; + } +} + +stream_filter_register('test.rotate', 'RotateFilter'); + +$file = __DIR__ . '/user_filter_seek_001.txt'; +$text = 'Hello World'; + +file_put_contents($file, $text); + +$fp = fopen($file, 'r'); +stream_filter_append($fp, 'test.rotate', STREAM_FILTER_READ, ['rotation' => 13]); + +$partial = fread($fp, 5); +echo "First read: $partial\n"; + +$result = fseek($fp, 0, SEEK_SET); +echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +$full = fread($fp, strlen($text)); +echo "Full content: $full\n"; + +$result = fseek($fp, 6, SEEK_SET); +echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +$from_middle = fread($fp, 5); +echo "Read from middle: $from_middle\n"; + +fclose($fp); +unlink($file); + +?> +--EXPECT-- +First read: Uryyb +Seek to start: SUCCESS +Full content: Uryyb Jbeyq +Seek to middle: SUCCESS +Read from middle: Jbeyq diff --git a/ext/standard/tests/filters/user_filter_seek_02.phpt b/ext/standard/tests/filters/user_filter_seek_02.phpt new file mode 100644 index 0000000000000..39f4c3c66243e --- /dev/null +++ b/ext/standard/tests/filters/user_filter_seek_02.phpt @@ -0,0 +1,75 @@ +--TEST-- +php_user_filter with seek method - stateful filter +--FILE-- +data); $i++) { + $modified .= $bucket->data[$i] . $this->count++; + } + $bucket->data = $modified; + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + return PSFS_PASS_ON; + } + + public function onCreate(): bool + { + return true; + } + + public function onClose(): void {} + + public function seek(int $offset, int $whence): bool + { + if ($offset === 0 && $whence === SEEK_SET) { + $this->count = 0; + return true; + } + return false; + } +} + +stream_filter_register('test.counting', 'CountingFilter'); + +$file = __DIR__ . '/user_filter_seek_002.txt'; +$text = 'ABC'; + +file_put_contents($file, $text); + +$fp = fopen($file, 'r'); +stream_filter_append($fp, 'test.counting', STREAM_FILTER_READ); + +$first = fread($fp, 10); +echo "First read: $first\n"; + +$result = fseek($fp, 0, SEEK_SET); +echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +$second = fread($fp, 10); +echo "Second read after seek: $second\n"; +echo "Counts reset: " . ($first === $second ? "YES" : "NO") . "\n"; + +$result = fseek($fp, 1, SEEK_SET); +echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n"; + +fclose($fp); +unlink($file); + +?> +--EXPECTF-- +First read: A0B1C2 +Seek to start: SUCCESS +Second read after seek: A0B1C2 +Counts reset: YES + +Warning: fseek(): Stream filter seeking for user-filter failed in %s on line %d +Seek to middle: FAILURE diff --git a/main/streams/streams.c b/main/streams/streams.c index de556334b3662..7b60884f98dde 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1374,12 +1374,13 @@ static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_s return true; } -static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter, bool is_start_seeking) +static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter, + bool is_start_seeking, zend_off_t offset, int whence) { while (filter) { if (((filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START && is_start_seeking) || filter->seekable == PHP_STREAM_FILTER_SEEKABLE_CHECK) && - filter->fops->seek(stream, filter, 0, SEEK_SET)) { + filter->fops->seek(stream, filter, offset, whence)) { php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label); return FAILURE; } @@ -1388,10 +1389,17 @@ static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter return SUCCESS; } -static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking) +static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking, + zend_off_t offset, int whence) { - return php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking) == SUCCESS && - php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking) == SUCCESS; + if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking, offset, whence) == FAILURE) { + return FAILURE; + } + if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking, offset, whence) == FAILURE) { + return FAILURE; + } + + return SUCCESS; } @@ -1429,8 +1437,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->position += offset; stream->eof = 0; stream->fatal_error = 0; - php_stream_filters_seek_all(stream, is_start_seeking); - return 0; + return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1; } break; case SEEK_SET: @@ -1440,8 +1447,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) stream->position = offset; stream->eof = 0; stream->fatal_error = 0; - php_stream_filters_seek_all(stream, is_start_seeking); - return 0; + return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1; } break; } @@ -1476,9 +1482,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) /* invalidate the buffer contents */ stream->readpos = stream->writepos = 0; - php_stream_filters_seek_all(stream, is_start_seeking); - - return ret; + return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? ret : -1; } /* else the stream has decided that it can't support seeking after all; * fall through to attempt emulation */