Commit e49be5f8e29 for php.net
commit e49be5f8e2903dbefa262847f1494c8e4c86004e
Author: Jakub Zelenka <bukka@php.net>
Date: Thu Nov 20 17:18:35 2025 +0100
Allow filtered streams to be casted as fd for select
This removes the artificial limitation that is not necessary. The fact
that some streams can have some data buffered is not a problem because
the similar situation is already present for OpenSSL streams where
OpenSSL can internally buffer data for the unprocessed part of the
record.
Closes GH-20540
diff --git a/NEWS b/NEWS
index 3b20cbb026c..14b47a44d38 100644
--- a/NEWS
+++ b/NEWS
@@ -79,6 +79,7 @@ PHP NEWS
address resuse.
. Fixed bug GH-20370 (User stream filters could violate typed property
constraints). (alexandre-daubois)
+ . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka)
- Zip:
. Fixed ZipArchive callback being called after executor has shut down.
diff --git a/UPGRADING b/UPGRADING
index f9e50623519..85cad750f08 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -49,6 +49,7 @@ PHP 8.6 UPGRADE NOTES
. Added stream socket context options so_keepalive, tcp_keepidle,
tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive
options.
+ . Allowed casting casting filtered streams as file descriptor for select.
========================================
3. Changes in SAPI modules
diff --git a/ext/standard/tests/streams/stream_select_filtered_base64_basic.phpt b/ext/standard/tests/streams/stream_select_filtered_base64_basic.phpt
new file mode 100644
index 00000000000..f7ec2b6a4b5
--- /dev/null
+++ b/ext/standard/tests/streams/stream_select_filtered_base64_basic.phpt
@@ -0,0 +1,32 @@
+--TEST--
+stream_select() base64-decode filter basic usage
+--FILE--
+<?php
+$file = fopen('php://temp', 'r+');
+
+// Write complete base64 "Hello World"
+fwrite($file, 'SGVsbG8gV29ybGQ=');
+
+stream_filter_append($file, 'convert.base64-decode');
+rewind($file);
+
+$read = [$file];
+$write = null;
+$except = null;
+
+$result = stream_select($read, $write, $except, 0);
+
+if ($result !== false) {
+ echo "stream_select succeeded\n";
+ if (count($read) > 0) {
+ echo fread($file, 1024) . "\n";
+ }
+} else {
+ echo "stream_select failed\n";
+}
+
+fclose($file);
+?>
+--EXPECT--
+stream_select succeeded
+Hello World
diff --git a/ext/standard/tests/streams/stream_select_filtered_base64_buffered.phpt b/ext/standard/tests/streams/stream_select_filtered_base64_buffered.phpt
new file mode 100644
index 00000000000..4d11bf718a1
--- /dev/null
+++ b/ext/standard/tests/streams/stream_select_filtered_base64_buffered.phpt
@@ -0,0 +1,59 @@
+--TEST--
+stream_select() base64-decode filter buffered data usage
+--FILE--
+<?php
+$domain = (strtoupper(substr(PHP_OS, 0, 3) == 'WIN') ? STREAM_PF_INET : STREAM_PF_UNIX);
+$pipes = stream_socket_pair($domain, STREAM_SOCK_STREAM, 0);
+if ($pipes === false) {
+ die("Failed to create socket pair");
+}
+
+list($read_pipe, $write_pipe) = $pipes;
+
+stream_set_blocking($read_pipe, false);
+stream_filter_append($read_pipe, 'convert.base64-decode');
+
+// Write incomplete base64 data: "SGVs" decodes to "Hel" but "SGVs" is incomplete
+fwrite($write_pipe, 'SGVs');
+fflush($write_pipe);
+
+$read = [$read_pipe];
+$write = null;
+$except = null;
+
+$result = fread($read_pipe, 1024);
+echo "Decoded content (before select): " . $result . "\n";
+
+// Should succeed but stream should NOT be readable yet (data buffered in filter)
+$result = stream_select($read, $write, $except, 0, 1000);
+echo "After incomplete data - select result: " . $result . "\n";
+echo "After incomplete data - readable streams: " . count($read) . "\n";
+
+// Now complete the base64 sequence: "SGVsbG8=" decodes to "Hello"
+fwrite($write_pipe, 'bG8=');
+fflush($write_pipe);
+
+$read = [$read_pipe];
+$write = null;
+$except = null;
+
+// Now stream should be readable
+$result = stream_select($read, $write, $except, 0, 1000);
+echo "After complete data - select result: " . $result . "\n";
+echo "After complete data - readable streams: " . count($read) . "\n";
+
+if (count($read) > 0) {
+ $content = fread($read_pipe, 1024);
+ echo "Decoded content: " . $content . "\n";
+}
+
+fclose($read_pipe);
+fclose($write_pipe);
+?>
+--EXPECT--
+Decoded content (before select): Hel
+After incomplete data - select result: 0
+After incomplete data - readable streams: 0
+After complete data - select result: 1
+After complete data - readable streams: 1
+Decoded content: lo
diff --git a/main/streams/cast.c b/main/streams/cast.c
index 4b718302457..4dc8ddb5f6a 100644
--- a/main/streams/cast.c
+++ b/main/streams/cast.c
@@ -297,7 +297,7 @@ PHPAPI zend_result _php_stream_cast(php_stream *stream, int castas, void **ret,
}
}
- if (php_stream_is_filtered(stream)) {
+ if (php_stream_is_filtered(stream) && castas != PHP_STREAM_AS_FD_FOR_SELECT) {
if (show_err) {
php_error_docref(NULL, E_WARNING, "Cannot cast a filtered stream on this system");
}