Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 19 additions & 23 deletions ext/bz2/tests/bz2_filter_seek_compress.phpt
Original file line number Diff line number Diff line change
@@ -1,55 +1,51 @@
--TEST--
bzip2.compress filter with seek to start
bzip2.compress write filter is not reset on seek
--EXTENSIONS--
bz2
--FILE--
<?php
/* Write filters are not reset on stream seek; seeking only affects the
* stream's read/write position, not the filter pipeline state. */

$file = __DIR__ . '/bz2_filter_seek_compress.bz2';

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

$fp = fopen($file, 'w+');
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);
$filter = stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);

fwrite($fp, $text);

fwrite($fp, $text1);
fflush($fp);
/* Remove the filter to finalize compression cleanly before seeking */
stream_filter_remove($filter);

$size1 = ftell($fp);
echo "Size after first write: $size1\n";
$size = ftell($fp);
echo "Size after write: $size\n";

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

fwrite($fp, $text2);
fflush($fp);

$size2 = ftell($fp);
echo "Size after second write: $size2\n";
echo "Second write is larger: " . ($size2 > $size1 ? "YES" : "NO") . "\n";

/* Seek to middle also succeeds */
$result = fseek($fp, 50, SEEK_SET);
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";

fclose($fp);

/* Verify the compressed output is still valid */
$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";
echo "Decompressed content matches: " . ($content === $text ? "YES" : "NO") . "\n";
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/bz2_filter_seek_compress.bz2');
?>
--EXPECTF--
Size after first write: 40
Size after write: %d
Seek to start: SUCCESS
Size after second write: 98
Second write is larger: YES

Warning: fseek(): Stream filter bzip2.compress is seekable only to start position in %s on line %d
Seek to middle: FAILURE
Decompressed content matches text2: YES
Seek to middle: SUCCESS
Decompressed content matches: YES
58 changes: 58 additions & 0 deletions ext/standard/tests/filters/chunked_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
--TEST--
Dechunk write filter state must survive stream seek
--FILE--
<?php
/* The dechunk filter is commonly used as a write filter on php://temp buffers.
* The buffer is written to (through the filter) and then seeked to re-read
* the already-decoded output. Seeking the stream must NOT reset the write
* filter state, otherwise multi-chunk transfers break. */

$buffer = fopen('php://temp', 'w+');
stream_filter_append($buffer, 'dechunk', STREAM_FILTER_WRITE);

/* Write first chunk */
fwrite($buffer, "5\r\nHello\r\n");

/* Read back decoded data; this seeks to offset 0 internally */
$data = stream_get_contents($buffer, -1, 0);
var_dump($data);

/* Write second chunk; filter must still be in the correct state */
fwrite($buffer, "7\r\n, World\r\n");

/* Read all decoded data from the beginning */
$data = stream_get_contents($buffer, -1, 0);
var_dump($data);

/* Write final (terminating) chunk */
fwrite($buffer, "0\r\n\r\n");

/* Read complete decoded output */
$data = stream_get_contents($buffer, -1, 0);
var_dump($data);

fclose($buffer);

/* Also verify that incomplete chunked transfer is still detected:
* writing a non-chunk byte after the filter has been reset by a
* seek should not produce output. */
$buffer = fopen('php://temp', 'w+');
stream_filter_append($buffer, 'dechunk', STREAM_FILTER_WRITE);

fwrite($buffer, "5\r\nHello\r\n");
$data = stream_get_contents($buffer, -1, 0);
var_dump($data);

/* The transfer is still in progress (no terminating 0-chunk seen).
* Verify incomplete state is preserved by checking ftell: the decoded
* write position should reflect only the 5 bytes written so far. */
var_dump(ftell($buffer));

fclose($buffer);
?>
--EXPECT--
string(5) "Hello"
string(12) "Hello, World"
string(12) "Hello, World"
string(5) "Hello"
int(5)
43 changes: 20 additions & 23 deletions ext/zlib/tests/zlib_filter_seek_deflate.phpt
Original file line number Diff line number Diff line change
@@ -1,55 +1,52 @@
--TEST--
zlib.deflate filter with seek to start
zlib.deflate write filter is not reset on seek
--EXTENSIONS--
zlib
--FILE--
<?php
/* Write filters are not reset on stream seek; seeking only affects the
* stream's read/write position, not the filter pipeline state. This ensures
* seeking a stream with write filters does not disrupt the filter state. */

$file = __DIR__ . '/zlib_filter_seek_deflate.zlib';

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

$fp = fopen($file, 'w+');
stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);
$filter = stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE);

fwrite($fp, $text);

fwrite($fp, $text1);
fflush($fp);
/* Remove the filter to finalize compression cleanly before seeking */
stream_filter_remove($filter);

$size1 = ftell($fp);
echo "Size after first write: $size1\n";
$size = ftell($fp);
echo "Size after write: $size\n";

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

fwrite($fp, $text2);
fflush($fp);

$size2 = ftell($fp);
echo "Size after second write: $size2\n";
echo "Second write is larger: " . ($size2 > $size1 ? "YES" : "NO") . "\n";

/* Seek to middle also succeeds */
$result = fseek($fp, 50, SEEK_SET);
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";

fclose($fp);

/* Verify the compressed output is still valid */
$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";
echo "Decompressed content matches: " . ($content === $text ? "YES" : "NO") . "\n";
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/zlib_filter_seek_deflate.zlib');
?>
--EXPECTF--
Size after first write: %d
Size after write: %d
Seek to start: SUCCESS
Size after second write: %d
Second write is larger: YES

Warning: fseek(): Stream filter zlib.deflate is seekable only to start position in %s on line %d
Seek to middle: FAILURE
Decompressed content matches text2: YES
Seek to middle: SUCCESS
Decompressed content matches: YES
11 changes: 5 additions & 6 deletions main/streams/streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -1396,9 +1396,11 @@ static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter
static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking,
zend_off_t offset, int whence)
{
if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking, offset, whence) == FAILURE) {
return FAILURE;
}
/* Write filters are not reset on seek. Their state tracks the
* transformation of data written through them and is independent of the
* stream's read/write position. Resetting them would break stateful write
* filters (e.g. dechunk on php://temp) whose stream is seeked only to
* re-read already-filtered output via stream_get_contents(). */
if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking, offset, whence) == FAILURE) {
return FAILURE;
}
Expand All @@ -1424,9 +1426,6 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)

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;
Expand Down
Loading