Skip to content

Commit ecd7c90

Browse files
committed
stream: add tests and fix user filter seeking
1 parent 0fa736c commit ecd7c90

File tree

4 files changed

+200
-12
lines changed

4 files changed

+200
-12
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
php_user_filter with invalid seek signature
3+
--FILE--
4+
<?php
5+
6+
class InvalidSeekFilter extends php_user_filter
7+
{
8+
public function filter($in, $out, &$consumed, bool $closing): int
9+
{
10+
return PSFS_PASS_ON;
11+
}
12+
13+
public function onCreate(): bool
14+
{
15+
return true;
16+
}
17+
18+
public function onClose(): void {}
19+
20+
public function seek($offset): bool
21+
{
22+
return true;
23+
}
24+
}
25+
26+
?>
27+
--EXPECTF--
28+
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
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
--TEST--
2+
php_user_filter with seek method - always seekable (stateless filter)
3+
--FILE--
4+
<?php
5+
6+
class RotateFilter extends php_user_filter
7+
{
8+
private $rotation = 0;
9+
10+
public function filter($in, $out, &$consumed, bool $closing): int
11+
{
12+
while ($bucket = stream_bucket_make_writeable($in)) {
13+
$rotated = '';
14+
for ($i = 0; $i < strlen($bucket->data); $i++) {
15+
$char = $bucket->data[$i];
16+
if (ctype_alpha($char)) {
17+
$base = ctype_upper($char) ? ord('A') : ord('a');
18+
$rotated .= chr($base + (ord($char) - $base + $this->rotation) % 26);
19+
} else {
20+
$rotated .= $char;
21+
}
22+
}
23+
$bucket->data = $rotated;
24+
$consumed += $bucket->datalen;
25+
stream_bucket_append($out, $bucket);
26+
}
27+
return PSFS_PASS_ON;
28+
}
29+
30+
public function onCreate(): bool
31+
{
32+
if (isset($this->params['rotation'])) {
33+
$this->rotation = (int)$this->params['rotation'];
34+
}
35+
return true;
36+
}
37+
38+
public function onClose(): void {}
39+
40+
public function seek(int $offset, int $whence): bool
41+
{
42+
// Stateless filter - always seekable to any position
43+
return true;
44+
}
45+
}
46+
47+
stream_filter_register('test.rotate', 'RotateFilter');
48+
49+
$file = __DIR__ . '/user_filter_seek_001.txt';
50+
$text = 'Hello World';
51+
52+
file_put_contents($file, $text);
53+
54+
$fp = fopen($file, 'r');
55+
stream_filter_append($fp, 'test.rotate', STREAM_FILTER_READ, ['rotation' => 13]);
56+
57+
$partial = fread($fp, 5);
58+
echo "First read: $partial\n";
59+
60+
$result = fseek($fp, 0, SEEK_SET);
61+
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
62+
63+
$full = fread($fp, strlen($text));
64+
echo "Full content: $full\n";
65+
66+
$result = fseek($fp, 6, SEEK_SET);
67+
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
68+
69+
$from_middle = fread($fp, 5);
70+
echo "Read from middle: $from_middle\n";
71+
72+
fclose($fp);
73+
unlink($file);
74+
75+
?>
76+
--EXPECT--
77+
First read: Uryyb
78+
Seek to start: SUCCESS
79+
Full content: Uryyb Jbeyq
80+
Seek to middle: SUCCESS
81+
Read from middle: Jbeyq
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
php_user_filter with seek method - stateful filter
3+
--FILE--
4+
<?php
5+
6+
class CountingFilter extends php_user_filter
7+
{
8+
private $count = 0;
9+
10+
public function filter($in, $out, &$consumed, bool $closing): int
11+
{
12+
while ($bucket = stream_bucket_make_writeable($in)) {
13+
$modified = '';
14+
for ($i = 0; $i < strlen($bucket->data); $i++) {
15+
$modified .= $bucket->data[$i] . $this->count++;
16+
}
17+
$bucket->data = $modified;
18+
$consumed += $bucket->datalen;
19+
stream_bucket_append($out, $bucket);
20+
}
21+
return PSFS_PASS_ON;
22+
}
23+
24+
public function onCreate(): bool
25+
{
26+
return true;
27+
}
28+
29+
public function onClose(): void {}
30+
31+
public function seek(int $offset, int $whence): bool
32+
{
33+
if ($offset === 0 && $whence === SEEK_SET) {
34+
$this->count = 0;
35+
return true;
36+
}
37+
return false;
38+
}
39+
}
40+
41+
stream_filter_register('test.counting', 'CountingFilter');
42+
43+
$file = __DIR__ . '/user_filter_seek_002.txt';
44+
$text = 'ABC';
45+
46+
file_put_contents($file, $text);
47+
48+
$fp = fopen($file, 'r');
49+
stream_filter_append($fp, 'test.counting', STREAM_FILTER_READ);
50+
51+
$first = fread($fp, 10);
52+
echo "First read: $first\n";
53+
54+
$result = fseek($fp, 0, SEEK_SET);
55+
echo "Seek to start: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
56+
57+
$second = fread($fp, 10);
58+
echo "Second read after seek: $second\n";
59+
echo "Counts reset: " . ($first === $second ? "YES" : "NO") . "\n";
60+
61+
$result = fseek($fp, 1, SEEK_SET);
62+
echo "Seek to middle: " . ($result === 0 ? "SUCCESS" : "FAILURE") . "\n";
63+
64+
fclose($fp);
65+
unlink($file);
66+
67+
?>
68+
--EXPECTF--
69+
First read: A0B1C2
70+
Seek to start: SUCCESS
71+
Second read after seek: A0B1C2
72+
Counts reset: YES
73+
74+
Warning: fseek(): Stream filter seeking for user-filter failed in %s on line %d
75+
Seek to middle: FAILURE

main/streams/streams.c

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,12 +1374,13 @@ static bool php_stream_are_filters_seekable(php_stream_filter *filter, bool is_s
13741374
return true;
13751375
}
13761376

1377-
static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter, bool is_start_seeking)
1377+
static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter *filter,
1378+
bool is_start_seeking, zend_off_t offset, int whence)
13781379
{
13791380
while (filter) {
13801381
if (((filter->seekable == PHP_STREAM_FILTER_SEEKABLE_START && is_start_seeking) ||
13811382
filter->seekable == PHP_STREAM_FILTER_SEEKABLE_CHECK) &&
1382-
filter->fops->seek(stream, filter, 0, SEEK_SET)) {
1383+
filter->fops->seek(stream, filter, offset, whence)) {
13831384
php_error_docref(NULL, E_WARNING, "Stream filter seeking for %s failed", filter->fops->label);
13841385
return FAILURE;
13851386
}
@@ -1388,10 +1389,17 @@ static zend_result php_stream_filters_seek(php_stream *stream, php_stream_filter
13881389
return SUCCESS;
13891390
}
13901391

1391-
static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking)
1392+
static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start_seeking,
1393+
zend_off_t offset, int whence)
13921394
{
1393-
return php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking) == SUCCESS &&
1394-
php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking) == SUCCESS;
1395+
if (php_stream_filters_seek(stream, stream->writefilters.head, is_start_seeking, offset, whence) == FAILURE) {
1396+
return FAILURE;
1397+
}
1398+
if (php_stream_filters_seek(stream, stream->readfilters.head, is_start_seeking, offset, whence) == FAILURE) {
1399+
return FAILURE;
1400+
}
1401+
1402+
return SUCCESS;
13951403
}
13961404

13971405

@@ -1429,8 +1437,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
14291437
stream->position += offset;
14301438
stream->eof = 0;
14311439
stream->fatal_error = 0;
1432-
php_stream_filters_seek_all(stream, is_start_seeking);
1433-
return 0;
1440+
return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1;
14341441
}
14351442
break;
14361443
case SEEK_SET:
@@ -1440,8 +1447,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
14401447
stream->position = offset;
14411448
stream->eof = 0;
14421449
stream->fatal_error = 0;
1443-
php_stream_filters_seek_all(stream, is_start_seeking);
1444-
return 0;
1450+
return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? 0 : -1;
14451451
}
14461452
break;
14471453
}
@@ -1476,9 +1482,7 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
14761482
/* invalidate the buffer contents */
14771483
stream->readpos = stream->writepos = 0;
14781484

1479-
php_stream_filters_seek_all(stream, is_start_seeking);
1480-
1481-
return ret;
1485+
return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? ret : -1;
14821486
}
14831487
/* else the stream has decided that it can't support seeking after all;
14841488
* fall through to attempt emulation */

0 commit comments

Comments
 (0)