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
7 changes: 6 additions & 1 deletion ext/bz2/bz2_filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,14 @@ static const php_stream_filter_ops php_bz2_compress_ops = {
static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, bool persistent)
{
const php_stream_filter_ops *fops = NULL;
php_stream_filter_seekable_t write_seekable;
php_bz2_filter_data *data;
int status = BZ_OK;

if (php_stream_filter_parse_write_seek_mode(filterparams, &write_seekable) == FAILURE) {
return NULL;
}

/* Create this filter */
data = pecalloc(1, sizeof(php_bz2_filter_data), persistent);

Expand Down Expand Up @@ -476,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, PSFS_SEEKABLE_START);
return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START, write_seekable);
}

const php_stream_filter_factory php_bz2_filter_factory = {
Expand Down
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
54 changes: 54 additions & 0 deletions ext/bz2/tests/bz2_filter_write_seek_modes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
bzip2.compress write filter: write_seek_mode parameter
--EXTENSIONS--
bz2
--FILE--
<?php
$file = __DIR__ . '/bz2_filter_write_seek_modes.bz2';

$text1 = 'First message that will be discarded.';
$text2 = 'Second message that replaces the first.';

/* "reset" */
$fp = fopen($file, 'w+');
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE,
['write_seek_mode' => 'reset']);
fwrite($fp, $text1);
ftruncate($fp, 0);
var_dump(fseek($fp, 0, SEEK_SET) === 0);
fwrite($fp, $text2);
fclose($fp);

$fp = fopen($file, 'r');
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ);
$decoded = stream_get_contents($fp);
fclose($fp);
var_dump($decoded === $text2);

/* "strict" */
$fp = fopen($file, 'w+');
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE,
['write_seek_mode' => 'strict']);
fwrite($fp, $text1);
var_dump(@fseek($fp, 0, SEEK_SET) === -1);
fclose($fp);

/* Invalid mode: ValueError */
$fp = fopen($file, 'w+');
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE,
['write_seek_mode' => 'nope']);
fclose($fp);

?>
--CLEAN--
<?php
@unlink(__DIR__ . '/bz2_filter_write_seek_modes.bz2');
?>
--EXPECTF--
bool(true)
bool(true)
bool(true)

Warning: stream_filter_append(): "write_seek_mode" filter parameter must be one of "preserve", "reset", or "strict" in %s

Warning: stream_filter_append(): Unable to create or locate filter "bzip2.compress" in %s
7 changes: 6 additions & 1 deletion ext/iconv/iconv.c
Original file line number Diff line number Diff line change
Expand Up @@ -2633,9 +2633,14 @@ static const php_stream_filter_ops php_iconv_stream_filter_ops = {
static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, bool persistent)
{
php_iconv_stream_filter *inst;
php_stream_filter_seekable_t write_seekable;
const char *from_charset = NULL, *to_charset = NULL;
size_t from_charset_len, to_charset_len;

if (php_stream_filter_parse_write_seek_mode(params, &write_seekable) == FAILURE) {
return NULL;
}

if ((from_charset = strchr(name, '.')) == NULL) {
return NULL;
}
Expand Down Expand Up @@ -2663,7 +2668,7 @@ static php_stream_filter *php_iconv_stream_filter_factory_create(const char *nam
}

return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent,
PSFS_SEEKABLE_START);
PSFS_SEEKABLE_START, write_seekable);
}
/* }}} */

Expand Down
21 changes: 14 additions & 7 deletions ext/standard/filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ static const php_stream_filter_ops strfilter_rot13_ops = {

static php_stream_filter *strfilter_rot13_create(const char *filtername, zval *filterparams, bool persistent)
{
return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
return php_stream_filter_alloc(&strfilter_rot13_ops, NULL, persistent,
PSFS_SEEKABLE_ALWAYS, PSFS_SEEKABLE_ALWAYS);
}

static const php_stream_filter_factory strfilter_rot13_factory = {
Expand Down Expand Up @@ -147,12 +148,14 @@ static const php_stream_filter_ops strfilter_tolower_ops = {

static php_stream_filter *strfilter_toupper_create(const char *filtername, zval *filterparams, bool persistent)
{
return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
return php_stream_filter_alloc(&strfilter_toupper_ops, NULL, persistent,
PSFS_SEEKABLE_ALWAYS, PSFS_SEEKABLE_ALWAYS);
}

static php_stream_filter *strfilter_tolower_create(const char *filtername, zval *filterparams, bool persistent)
{
return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent, PSFS_SEEKABLE_ALWAYS);
return php_stream_filter_alloc(&strfilter_tolower_ops, NULL, persistent,
PSFS_SEEKABLE_ALWAYS, PSFS_SEEKABLE_ALWAYS);
}

static const php_stream_filter_factory strfilter_toupper_factory = {
Expand Down Expand Up @@ -1634,7 +1637,7 @@ static const php_stream_filter_ops strfilter_convert_ops = {
static php_stream_filter *strfilter_convert_create(const char *filtername, zval *filterparams, bool persistent)
{
php_convert_filter *inst;

php_stream_filter_seekable_t write_seekable;
const char *dot;
int conv_mode = 0;

Expand All @@ -1648,6 +1651,10 @@ static php_stream_filter *strfilter_convert_create(const char *filtername, zval
}
++dot;

if (php_stream_filter_parse_write_seek_mode(filterparams, &write_seekable) == FAILURE) {
return NULL;
}

inst = pemalloc(sizeof(php_convert_filter), persistent);

if (strcasecmp(dot, "base64-encode") == 0) {
Expand All @@ -1667,7 +1674,7 @@ static php_stream_filter *strfilter_convert_create(const char *filtername, zval
return NULL;
}

return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent, PSFS_SEEKABLE_START);
return php_stream_filter_alloc(&strfilter_convert_ops, inst, persistent, PSFS_SEEKABLE_START, write_seekable);
}

static const php_stream_filter_factory strfilter_convert_factory = {
Expand Down Expand Up @@ -1761,7 +1768,7 @@ 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, PSFS_SEEKABLE_START);
return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START, PSFS_SEEKABLE_ALWAYS);
}

static const php_stream_filter_factory consumed_filter_factory = {
Expand Down Expand Up @@ -1992,7 +1999,7 @@ 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, PSFS_SEEKABLE_START);
return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START, PSFS_SEEKABLE_ALWAYS);
}

static const php_stream_filter_factory chunked_filter_factory = {
Expand Down
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)
61 changes: 61 additions & 0 deletions ext/standard/tests/filters/convert_filter_write_seek_modes.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--TEST--
convert.* write filter: write_seek_mode parameter
--FILE--
<?php
/* Smoke test: the write_seek_mode parameter is accepted on convert.* filters
* and behaves correctly per mode. The deeper reset semantics are exercised
* via the read path for convert.* (read_seekable = START always resets) and
* via the dedicated zlib/bz2 mode tests. */

foreach (['convert.base64-encode', 'convert.quoted-printable-encode'] as $name) {
/* preserve: seeks succeed (default) */
$fp = fopen('php://memory', 'w+');
stream_filter_append($fp, $name, STREAM_FILTER_WRITE,
['write_seek_mode' => 'preserve']);
fwrite($fp, 'Hello');
var_dump(fseek($fp, 0, SEEK_SET) === 0);
var_dump(fseek($fp, 100, SEEK_SET) === 0);
fclose($fp);

/* reset: seeks succeed, callback dispatched */
$fp = fopen('php://memory', 'w+');
stream_filter_append($fp, $name, STREAM_FILTER_WRITE,
['write_seek_mode' => 'reset']);
fwrite($fp, 'Hello');
var_dump(fseek($fp, 0, SEEK_SET) === 0);
fclose($fp);

/* strict: seek fails */
$fp = fopen('php://memory', 'w+');
stream_filter_append($fp, $name, STREAM_FILTER_WRITE,
['write_seek_mode' => 'strict']);
fwrite($fp, 'Hello');
var_dump(@fseek($fp, 0, SEEK_SET) === -1);
fclose($fp);

/* invalid: ValueError */
$fp = fopen('php://memory', 'w+');
stream_filter_append($fp, $name, STREAM_FILTER_WRITE,
['write_seek_mode' => 42]);
if ($fp) {
fclose($fp);
}
}
?>
--EXPECTF--
bool(true)
bool(true)
bool(true)
bool(true)

Warning: stream_filter_append(): "write_seek_mode" filter parameter must be one of "preserve", "reset", or "strict" in %s

Warning: stream_filter_append(): Unable to create or locate filter "convert.base64-encode" in %s
bool(true)
bool(true)
bool(true)
bool(true)

Warning: stream_filter_append(): "write_seek_mode" filter parameter must be one of "preserve", "reset", or "strict" in %s

Warning: stream_filter_append(): Unable to create or locate filter "convert.quoted-printable-encode" in %s
2 changes: 1 addition & 1 deletion ext/standard/tests/filters/php_user_filter_04.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ class InvalidSeekFilter extends php_user_filter

?>
--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
Fatal error: Declaration of InvalidSeekFilter::seek($offset): bool must be compatible with php_user_filter::seek(int $offset, int $whence, int $chain): bool in %s on line %d
2 changes: 1 addition & 1 deletion ext/standard/tests/filters/user_filter_seek_01.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class RotateFilter extends php_user_filter

public function onClose(): void {}

public function seek(int $offset, int $whence): bool
public function seek(int $offset, int $whence, int $chain): bool
{
// Stateless filter - always seekable to any position
return true;
Expand Down
2 changes: 1 addition & 1 deletion ext/standard/tests/filters/user_filter_seek_02.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class CountingFilter extends php_user_filter

public function onClose(): void {}

public function seek(int $offset, int $whence): bool
public function seek(int $offset, int $whence, int $chain): bool
{
if ($offset === 0 && $whence === SEEK_SET) {
$this->count = 0;
Expand Down
Loading
Loading