Skip to content

Commit 278d7f2

Browse files
Fix stream filter seeking resetting write filter state on read seeks
1 parent f7a5a32 commit 278d7f2

File tree

3 files changed

+83
-29
lines changed

3 files changed

+83
-29
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
Dechunk write filter state must survive stream seek
3+
--FILE--
4+
<?php
5+
/* The dechunk filter is commonly used as a write filter on php://temp buffers.
6+
* The buffer is written to (through the filter) and then seeked to re-read
7+
* the already-decoded output. Seeking the stream must NOT reset the write
8+
* filter state, otherwise multi-chunk transfers break. */
9+
10+
$buffer = fopen('php://temp', 'w+');
11+
stream_filter_append($buffer, 'dechunk', STREAM_FILTER_WRITE);
12+
13+
/* Write first chunk */
14+
fwrite($buffer, "5\r\nHello\r\n");
15+
16+
/* Read back decoded data — this seeks to offset 0 internally */
17+
$data = stream_get_contents($buffer, -1, 0);
18+
var_dump($data);
19+
20+
/* Write second chunk — filter must still be in the correct state */
21+
fwrite($buffer, "7\r\n, World\r\n");
22+
23+
/* Read all decoded data from the beginning */
24+
$data = stream_get_contents($buffer, -1, 0);
25+
var_dump($data);
26+
27+
/* Write final (terminating) chunk */
28+
fwrite($buffer, "0\r\n\r\n");
29+
30+
/* Read complete decoded output */
31+
$data = stream_get_contents($buffer, -1, 0);
32+
var_dump($data);
33+
34+
fclose($buffer);
35+
36+
/* Also verify that incomplete chunked transfer is still detected:
37+
* writing a non-chunk byte after the filter has been reset by a
38+
* seek should not produce output. */
39+
$buffer = fopen('php://temp', 'w+');
40+
stream_filter_append($buffer, 'dechunk', STREAM_FILTER_WRITE);
41+
42+
fwrite($buffer, "5\r\nHello\r\n");
43+
$data = stream_get_contents($buffer, -1, 0);
44+
var_dump($data);
45+
46+
/* The transfer is still in progress (no terminating 0-chunk seen).
47+
* Verify incomplete state is preserved by checking ftell: the decoded
48+
* write position should reflect only the 5 bytes written so far. */
49+
var_dump(ftell($buffer));
50+
51+
fclose($buffer);
52+
?>
53+
--EXPECT--
54+
string(5) "Hello"
55+
string(12) "Hello, World"
56+
string(12) "Hello, World"
57+
string(5) "Hello"
58+
int(5)
Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,52 @@
11
--TEST--
2-
zlib.deflate filter with seek to start
2+
zlib.deflate write filter is not reset on seek
33
--EXTENSIONS--
44
zlib
55
--FILE--
66
<?php
7+
/* Write filters are not reset on stream seek — seeking only affects the
8+
* stream's read/write position, not the filter pipeline state. This ensures
9+
* seeking a stream with write filters does not disrupt the filter state. */
10+
711
$file = __DIR__ . '/zlib_filter_seek_deflate.zlib';
812

9-
$text1 = 'Short text.';
10-
$text2 = 'This is a much longer text that will completely overwrite the previous compressed data in the file.';
13+
$text = 'Hello, World!';
1114

1215
$fp = fopen($file, 'w+');
13-
stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);
16+
$filter = stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);
17+
18+
fwrite($fp, $text);
1419

15-
fwrite($fp, $text1);
16-
fflush($fp);
20+
/* Remove the filter to finalize compression cleanly before seeking */
21+
stream_filter_remove($filter);
1722

18-
$size1 = ftell($fp);
19-
echo "Size after first write: $size1\n";
23+
$size = ftell($fp);
24+
echo "Size after write: $size\n";
2025

26+
/* Seek to start succeeds — write filters no longer block seeking */
2127
$result = fseek($fp, 0, SEEK_SET);
2228
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
2329

24-
fwrite($fp, $text2);
25-
fflush($fp);
26-
27-
$size2 = ftell($fp);
28-
echo "Size after second write: $size2\n";
29-
echo "Second write is larger: " . ($size2 > $size1 ? "YES" : "NO") . "\n";
30-
30+
/* Seek to middle also succeeds */
3131
$result = fseek($fp, 50, SEEK_SET);
3232
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
3333

3434
fclose($fp);
3535

36+
/* Verify the compressed output is still valid */
3637
$fp = fopen($file, 'r');
3738
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ);
3839
$content = stream_get_contents($fp);
3940
fclose($fp);
4041

41-
echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n";
42+
echo "Decompressed content matches: " . ($content === $text ? "YES" : "NO") . "\n";
4243
?>
4344
--CLEAN--
4445
<?php
4546
@unlink(__DIR__ . '/zlib_filter_seek_deflate.zlib');
4647
?>
4748
--EXPECTF--
48-
Size after first write: %d
49+
Size after write: %d
4950
Seek to start: SUCCESS
50-
Size after second write: %d
51-
Second write is larger: YES
52-
53-
Warning: fseek(): Stream filter zlib.deflate is seekable only to start position in %s on line %d
54-
Seek to middle: FAILURE
55-
Decompressed content matches text2: YES
51+
Seek to middle: SUCCESS
52+
Decompressed content matches: YES

main/streams/streams.c

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,9 +1396,11 @@ static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter
13961396
static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking,
13971397
zend_off_t offset, int whence)
13981398
{
1399-
if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking, offset, whence) == FAILURE) {
1400-
return FAILURE;
1401-
}
1399+
/* Write filters are not reset on seek. Their state tracks the
1400+
* transformation of data written through them and is independent of the
1401+
* stream's read/write position. Resetting them would break stateful write
1402+
* filters (e.g. dechunk on php://temp) whose stream is seeked only to
1403+
* re-read already-filtered output via stream_get_contents(). */
14021404
if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking, offset, whence) == FAILURE) {
14031405
return FAILURE;
14041406
}
@@ -1424,9 +1426,6 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
14241426

14251427
if (stream->writefilters.head) {
14261428
_php_stream_flush(stream, 0);
1427-
if (!php_stream_are_filters_seekable(stream->writefilters.head, is_start_seeking)) {
1428-
return -1;
1429-
}
14301429
}
14311430
if (stream->readfilters.head && !php_stream_are_filters_seekable(stream->readfilters.head, is_start_seeking)) {
14321431
return -1;

0 commit comments

Comments
 (0)