diff --git a/ext/standard/tests/streams/gh22841.phpt b/ext/standard/tests/streams/gh22841.phpt new file mode 100644 index 000000000000..0f03bf83da26 --- /dev/null +++ b/ext/standard/tests/streams/gh22841.phpt @@ -0,0 +1,68 @@ +--TEST-- +GH-22841 (php_stream_copy_to_stream_ex() drops progress notifications) +--EXTENSIONS-- +zlib +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--CONFLICTS-- +server +--FILE-- + "CONNECT", + STREAM_NOTIFY_MIME_TYPE_IS => "MIME_TYPE_IS", + STREAM_NOTIFY_FILE_SIZE_IS => "FILE_SIZE_IS", + STREAM_NOTIFY_PROGRESS => "PROGRESS", + STREAM_NOTIFY_COMPLETED => "COMPLETED", +]; + +$events = []; +$context = stream_context_create(); +stream_context_set_params($context, ["notification" => + function ($code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) + use (&$events, $names) { + $name = $names[$code] ?? $code; + $event = $name . " " . ($code === STREAM_NOTIFY_FILE_SIZE_IS ? $bytes_max : $bytes_transferred); + /* The body may arrive in more than one read, so collapse consecutive + * progress events into the latest one to keep the output stable. */ + if ($name === "PROGRESS" && str_starts_with(end($events) ?: "", "PROGRESS ")) { + array_pop($events); + } + $events[] = $event; + } +]); + +/* compress.zlib:// opens the inner stream with STREAM_MUST_SEEK, which copies + * the HTTP stream into a temporary file through php_stream_copy_to_stream_ex(). */ +$fp = fopen("compress.zlib://http://" . PHP_CLI_SERVER_ADDRESS . "/", "r", false, $context); +var_dump(stream_get_contents($fp)); +fclose($fp); + +echo implode(PHP_EOL, $events), PHP_EOL; + +?> +--EXPECT-- +string(13) "Hello World! +" +CONNECT 0 +MIME_TYPE_IS 0 +FILE_SIZE_IS 43 +PROGRESS 43 +COMPLETED 43 diff --git a/main/streams/streams.c b/main/streams/streams.c index 3d8830d7291f..2664c07e2f1f 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1302,7 +1302,12 @@ static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start return SUCCESS; } - +static bool php_stream_copy_context(php_stream *stream) +{ + php_stream_context *context = PHP_STREAM_CONTEXT(stream); + /* The fd-level copy cannot emit progress notifications. */ + return context && context->notifier; +} PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) { @@ -1655,7 +1660,8 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de * are empty, so the fd offsets match the logical stream positions */ if (!php_stream_is(src, PHP_STREAM_IS_USERSPACE) && !php_stream_is(dest, PHP_STREAM_IS_USERSPACE) && src->writepos == src->readpos && dest->writepos == dest->readpos && - !php_stream_is_filtered(src) && !php_stream_is_filtered(dest)) { + !php_stream_is_filtered(src) && !php_stream_is_filtered(dest) && + !php_stream_copy_context(src) && !php_stream_copy_context(dest)) { php_io_fd src_copy_fd, dest_copy_fd; if (php_stream_cast(src, PHP_STREAM_AS_FD_FOR_COPY, (void *) &src_copy_fd, 0) == SUCCESS &&