Skip to content

Commit 48a7181

Browse files
committed
Fix bug #49874: ftell() and fseek() inconsistency when using stream filters
Currently filter seeking does not work correctly for most streams. The idea is to extend API to allow seeking for some streams. There are couple of cases: - filter is always seekable - e.g. string.rot13, string.toupper, string.tolower - filter is seekable only when sought to start or when it re-run the data from the start (it means when there is some data buffered) - this is for pretty much all other filters that keep some state. - user filters are seekable by default for BC reason but if new seek method is implemented, then it is called and based on the bool result the seeking either fails or succeed.
1 parent c45b2be commit 48a7181

23 files changed

+1103
-66
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ PHP NEWS
153153
. Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream).
154154
(ilutov)
155155
. Improved stream_socket_server() bind failure error reporting. (ilutov)
156+
. Fixed bug #49874 (ftell() and fseek() inconsistency when using stream
157+
filters). (Jakub Zelenka)
156158

157159
- Zip:
158160
. Fixed ZipArchive callback being called after executor has shut down.

UPGRADING.INTERNALS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES
7474
longer is a pointer, but a directly embedded HashTable struct.
7575
. Added a C23_ENUM() helper macro to define forward-compatible fixed-size
7676
enums.
77+
. Extended php_stream_filter_ops with seek method.
7778

7879
========================
7980
2. Build system changes

ext/bz2/bz2_filter.c

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ typedef struct _php_bz2_filter_data {
4242
unsigned int is_flushed : 1; /* only for compression */
4343

4444
int persistent;
45+
46+
/* Configuration for reset - immutable */
47+
int blockSize100k; /* compress only */
48+
int workFactor; /* compress only */
4549
} php_bz2_filter_data;
4650

4751
/* }}} */
@@ -178,6 +182,36 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
178182
return exit_status;
179183
}
180184

185+
static zend_result php_bz2_decompress_seek(
186+
php_stream *stream,
187+
php_stream_filter *thisfilter,
188+
zend_off_t offset,
189+
int whence
190+
)
191+
{
192+
if (!Z_PTR(thisfilter->abstract)) {
193+
return FAILURE;
194+
}
195+
196+
php_bz2_filter_data *data = Z_PTR(thisfilter->abstract);
197+
198+
/* End current decompression if running */
199+
if (data->status == PHP_BZ2_RUNNING) {
200+
BZ2_bzDecompressEnd(&(data->strm));
201+
}
202+
203+
/* Reset stream state */
204+
data->strm.next_in = data->inbuf;
205+
data->strm.avail_in = 0;
206+
data->strm.next_out = data->outbuf;
207+
data->strm.avail_out = data->outbuf_len;
208+
data->status = PHP_BZ2_UNINITIALIZED;
209+
210+
/* Note: We don't reinitialize here - it will be done on first use in the filter function */
211+
212+
return SUCCESS;
213+
}
214+
181215
static void php_bz2_decompress_dtor(php_stream_filter *thisfilter)
182216
{
183217
if (thisfilter && Z_PTR(thisfilter->abstract)) {
@@ -193,6 +227,7 @@ static void php_bz2_decompress_dtor(php_stream_filter *thisfilter)
193227

194228
static const php_stream_filter_ops php_bz2_decompress_ops = {
195229
php_bz2_decompress_filter,
230+
php_bz2_decompress_seek,
196231
php_bz2_decompress_dtor,
197232
"bzip2.decompress"
198233
};
@@ -288,6 +323,41 @@ static php_stream_filter_status_t php_bz2_compress_filter(
288323
return exit_status;
289324
}
290325

326+
static zend_result php_bz2_compress_seek(
327+
php_stream *stream,
328+
php_stream_filter *thisfilter,
329+
zend_off_t offset,
330+
int whence
331+
)
332+
{
333+
int status;
334+
335+
if (!Z_PTR(thisfilter->abstract)) {
336+
return FAILURE;
337+
}
338+
339+
php_bz2_filter_data *data = Z_PTR(thisfilter->abstract);
340+
341+
/* End current compression */
342+
BZ2_bzCompressEnd(&(data->strm));
343+
344+
/* Reset stream state */
345+
data->strm.next_in = data->inbuf;
346+
data->strm.avail_in = 0;
347+
data->strm.next_out = data->outbuf;
348+
data->strm.avail_out = data->outbuf_len;
349+
data->is_flushed = 1;
350+
351+
/* Reinitialize compression with saved configuration */
352+
status = BZ2_bzCompressInit(&(data->strm), data->blockSize100k, 0, data->workFactor);
353+
if (status != BZ_OK) {
354+
php_error_docref(NULL, E_WARNING, "bzip2.compress: failed to reset compression state");
355+
return FAILURE;
356+
}
357+
358+
return SUCCESS;
359+
}
360+
291361
static void php_bz2_compress_dtor(php_stream_filter *thisfilter)
292362
{
293363
if (Z_PTR(thisfilter->abstract)) {
@@ -301,6 +371,7 @@ static void php_bz2_compress_dtor(php_stream_filter *thisfilter)
301371

302372
static const php_stream_filter_ops php_bz2_compress_ops = {
303373
php_bz2_compress_filter,
374+
php_bz2_compress_seek,
304375
php_bz2_compress_dtor,
305376
"bzip2.compress"
306377
};
@@ -309,7 +380,7 @@ static const php_stream_filter_ops php_bz2_compress_ops = {
309380

310381
/* {{{ bzip2.* common factory */
311382

312-
static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, uint8_t persistent)
383+
static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *filterparams, bool persistent)
313384
{
314385
const php_stream_filter_ops *fops = NULL;
315386
php_bz2_filter_data *data;
@@ -388,6 +459,10 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
388459
}
389460
}
390461

462+
/* Save configuration for reset */
463+
data->blockSize100k = blockSize100k;
464+
data->workFactor = workFactor;
465+
391466
status = BZ2_bzCompressInit(&(data->strm), blockSize100k, 0, workFactor);
392467
data->is_flushed = 1;
393468
fops = &php_bz2_compress_ops;
@@ -403,7 +478,7 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
403478
return NULL;
404479
}
405480

406-
return php_stream_filter_alloc(fops, data, persistent);
481+
return php_stream_filter_alloc(fops, data, persistent, PSFS_SEEKABLE_START);
407482
}
408483

409484
const php_stream_filter_factory php_bz2_filter_factory = {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
bzip2.compress filter with seek to start
3+
--EXTENSIONS--
4+
bz2
5+
--FILE--
6+
<?php
7+
$file = __DIR__ . '/bz2_filter_seek_compress.bz2';
8+
9+
$text1 = 'Short text.';
10+
$text2 = 'This is a much longer text that will completely overwrite the previous compressed data in the file.';
11+
12+
$fp = fopen($file, 'w+');
13+
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);
14+
15+
fwrite($fp, $text1);
16+
fflush($fp);
17+
18+
$size1 = ftell($fp);
19+
echo "Size after first write: $size1\n";
20+
21+
$result = fseek($fp, 0, SEEK_SET);
22+
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
23+
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+
31+
$result = fseek($fp, 50, SEEK_SET);
32+
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
33+
34+
fclose($fp);
35+
36+
$fp = fopen($file, 'r');
37+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ);
38+
$content = stream_get_contents($fp);
39+
fclose($fp);
40+
41+
echo "Decompressed content matches text2: " . ($content === $text2 ? "YES" : "NO") . "\n";
42+
?>
43+
--CLEAN--
44+
<?php
45+
@unlink(__DIR__ . '/bz2_filter_seek_compress.bz2');
46+
?>
47+
--EXPECTF--
48+
Size after first write: 40
49+
Seek to start: SUCCESS
50+
Size after second write: 98
51+
Second write is larger: YES
52+
53+
Warning: fseek(): Stream filter bzip2.compress is seekable only to start position in %s on line %d
54+
Seek to middle: FAILURE
55+
Decompressed content matches text2: YES
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
bzip2.decompress filter with seek to start
3+
--EXTENSIONS--
4+
bz2
5+
--FILE--
6+
<?php
7+
$file = __DIR__ . '/bz2_filter_seek_decompress.bz2';
8+
9+
$text = 'I am the very model of a modern major general, I\'ve information vegetable, animal, and mineral.';
10+
11+
$fp = fopen($file, 'w');
12+
stream_filter_append($fp, 'bzip2.compress', STREAM_FILTER_WRITE);
13+
fwrite($fp, $text);
14+
fclose($fp);
15+
16+
$fp = fopen($file, 'r');
17+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ);
18+
19+
$partial = fread($fp, 20);
20+
echo "First read (20 bytes): " . $partial . "\n";
21+
22+
$result = fseek($fp, 0, SEEK_SET);
23+
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
24+
25+
$full = stream_get_contents($fp);
26+
echo "Content after seek matches: " . ($full === $text ? "YES" : "NO") . "\n";
27+
28+
$result = fseek($fp, 50, SEEK_SET);
29+
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
30+
31+
fclose($fp);
32+
?>
33+
--CLEAN--
34+
<?php
35+
@unlink(__DIR__ . '/bz2_filter_seek_decompress.bz2');
36+
?>
37+
--EXPECTF--
38+
First read (20 bytes): I am the very model
39+
Seek to start: SUCCESS
40+
Content after seek matches: YES
41+
42+
Warning: fseek(): Stream filter bzip2.decompress is seekable only to start position in %s on line %d
43+
Seek to middle: FAILURE

ext/gd/tests/bug79945.phpt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ if (!(imagetypes() & IMG_PNG)) {
99
}
1010
set_error_handler(function($errno, $errstr) {
1111
if (str_contains($errstr, 'Cannot cast a filtered stream on this system')) {
12-
die('skip: fopencookie not support on this system');
12+
die('skip: fopencookie not supported on this system');
1313
}
1414
});
15-
imagecreatefrompng('php://filter/read=convert.base64-encode/resource=' . __DIR__ . '/test.png');
15+
imagecreatefrompng('php://filter/read=string.rot13/resource=' . __DIR__ . '/test.png');
1616
restore_error_handler();
1717
?>
1818
--FILE--
1919
<?php
20-
imagecreatefrompng('php://filter/read=convert.base64-encode/resource=' . __DIR__ . '/test.png');
20+
imagecreatefrompng('php://filter/read=string.rot13/resource=' . __DIR__ . '/test.png');
2121
?>
2222
--CLEAN--
2323
--EXPECTF--
2424

25-
Warning: imagecreatefrompng(): "php://filter/read=convert.base64-encode/resource=%s" is not a valid PNG file in %s on line %d
25+
Warning: imagecreatefrompng(): "php://filter/read=string.rot13/resource=%s" is not a valid PNG file in %s on line %d

ext/iconv/iconv.c

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,33 @@ static php_stream_filter_status_t php_iconv_stream_filter_do_filter(
25852585
}
25862586
/* }}} */
25872587

2588+
/* {{{ php_iconv_stream_filter_seek */
2589+
static zend_result php_iconv_stream_filter_seek(
2590+
php_stream *stream,
2591+
php_stream_filter *filter,
2592+
zend_off_t offset,
2593+
int whence)
2594+
{
2595+
php_iconv_stream_filter *self = (php_iconv_stream_filter *)Z_PTR(filter->abstract);
2596+
2597+
/* Reset stub buffer */
2598+
self->stub_len = 0;
2599+
2600+
/* Reset iconv conversion state by closing and reopening the converter */
2601+
iconv_close(self->cd);
2602+
2603+
self->cd = iconv_open(self->to_charset, self->from_charset);
2604+
if ((iconv_t)-1 == self->cd) {
2605+
php_error_docref(NULL, E_WARNING,
2606+
"iconv stream filter (\"%s\"=>\"%s\"): failed to reset conversion state",
2607+
self->from_charset, self->to_charset);
2608+
return FAILURE;
2609+
}
2610+
2611+
return SUCCESS;
2612+
}
2613+
/* }}} */
2614+
25882615
/* {{{ php_iconv_stream_filter_cleanup */
25892616
static void php_iconv_stream_filter_cleanup(php_stream_filter *filter)
25902617
{
@@ -2595,12 +2622,13 @@ static void php_iconv_stream_filter_cleanup(php_stream_filter *filter)
25952622

25962623
static const php_stream_filter_ops php_iconv_stream_filter_ops = {
25972624
php_iconv_stream_filter_do_filter,
2625+
php_iconv_stream_filter_seek,
25982626
php_iconv_stream_filter_cleanup,
25992627
"convert.iconv.*"
26002628
};
26012629

26022630
/* {{{ php_iconv_stream_filter_create */
2603-
static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, uint8_t persistent)
2631+
static php_stream_filter *php_iconv_stream_filter_factory_create(const char *name, zval *params, bool persistent)
26042632
{
26052633
php_iconv_stream_filter *inst;
26062634
const char *from_charset = NULL, *to_charset = NULL;
@@ -2632,7 +2660,8 @@ static php_stream_filter *php_iconv_stream_filter_factory_create(const char *nam
26322660
return NULL;
26332661
}
26342662

2635-
return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent);
2663+
return php_stream_filter_alloc(&php_iconv_stream_filter_ops, inst, persistent,
2664+
PSFS_SEEKABLE_START);
26362665
}
26372666
/* }}} */
26382667

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
iconv stream filter with seek to start
3+
--EXTENSIONS--
4+
iconv
5+
--FILE--
6+
<?php
7+
$file = __DIR__ . '/iconv_stream_filter_seek.txt';
8+
9+
$text = 'Hello, this is a test for iconv stream filter seeking functionality.';
10+
11+
$fp = fopen($file, 'w');
12+
stream_filter_append($fp, 'convert.iconv.ISO-2022-JP/UTF-8');
13+
fwrite($fp, $text);
14+
fclose($fp);
15+
16+
$fp = fopen($file, 'r');
17+
stream_filter_append($fp, 'convert.iconv.UTF-8/ISO-2022-JP');
18+
19+
$partial = fread($fp, 20);
20+
echo "First read (20 bytes): " . $partial . "\n";
21+
22+
$result = fseek($fp, 0, SEEK_SET);
23+
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
24+
25+
$full = fread($fp, strlen($text));
26+
echo "Content after seek matches: " . ($full === $text ? "YES" : "NO") . "\n";
27+
28+
$result = fseek($fp, 50, SEEK_SET);
29+
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
30+
31+
fclose($fp);
32+
?>
33+
--CLEAN--
34+
<?php
35+
@unlink(__DIR__ . '/iconv_stream_filter_seek.txt');
36+
?>
37+
--EXPECTF--
38+
First read (20 bytes): Hello, this is a tes
39+
Seek to start: SUCCESS
40+
Content after seek matches: YES
41+
42+
Warning: fseek(): Stream filter convert.iconv.* is seekable only to start position in %s on line %d
43+
Seek to middle: FAILURE

0 commit comments

Comments
 (0)