Commit 08dad097025 for php.net
commit 08dad097025bddd5a6d11cd864866a25b0966e67
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Apr 13 06:35:20 2026 -0400
Fix GH-8561, GH-8562, GH-8563, GH-8564: SplFileObject iterator desync (#21679)
* Fix GH-8562: SplFileObject::current() returns wrong value after next()
SplFileObject::next() without READ_AHEAD cleared the cached line and
incremented current_line_num but didn't advance the stream. When called
without a preceding current() (e.g. rewind() then next()), the stream
position stayed put, so the subsequent current() read stale data.
Read a line to advance the stream when next() is called with no cached
line.
Closes GH-8562
* Fix GH-8561: SplFileObject key()/current() desync after fgets()
fgets() read a line into the cache and incremented the line counter,
but left the cached line in place. The subsequent current() returned
the stale cached value instead of reading the next line from the
stream's actual position.
Clear the cached line after fgets() copies it to the return value.
This forces current() to re-read from the stream, which has already
advanced past the fgets'd line.
Closes GH-8561
* Fix GH-8563, GH-8564: SplFileObject EOF handling for seek() and next()
spl_filesystem_file_read_ex() treated a NULL buffer from
php_stream_get_line as a successful read of an empty line, creating a
phantom cached line at EOF. This caused seek() to give inconsistent
results between SplFileObject and SplTempFileObject (GH-8563), and
next() to increment the line counter indefinitely past EOF (GH-8564).
Return FAILURE when php_stream_get_line returns NULL, matching the
behavior of the existing php_stream_eof check. In seek(), break out
of the loop on EOF instead of returning, so the post-loop cleanup
runs consistently. In next(), return early at EOF without incrementing.
Make __toString() return empty string at EOF instead of throwing.
Closes GH-8563
Closes GH-8564
* Refine fgets() to reuse cached line when present
When current() reads a line into the cache without advancing line_num,
a subsequent fgets() would re-read the stream and return the next line,
skipping the cached one and leaving key() out of sync with current()
for the rest of the iteration.
Use the cached line if present; otherwise read a fresh line. Either way,
advance line_num by one.
diff --git a/ext/spl/spl_directory.c b/ext/spl/spl_directory.c
index cae8ffabc67..1468cec6ccf 100644
--- a/ext/spl/spl_directory.c
+++ b/ext/spl/spl_directory.c
@@ -1811,21 +1811,24 @@ static zend_result spl_filesystem_file_read_ex(spl_filesystem_object *intern, bo
}
if (!buf) {
- intern->u.file.current_line = ZSTR_EMPTY_ALLOC();
- } else {
- if (!csv && SPL_HAS_FLAG(intern->flags, SPL_FILE_OBJECT_DROP_NEW_LINE)) {
- if (line_len > 0 && buf[line_len - 1] == '\n') {
+ if (!silent) {
+ spl_filesystem_file_cannot_read(intern);
+ }
+ return FAILURE;
+ }
+
+ if (!csv && SPL_HAS_FLAG(intern->flags, SPL_FILE_OBJECT_DROP_NEW_LINE)) {
+ if (line_len > 0 && buf[line_len - 1] == '\n') {
+ line_len--;
+ if (line_len > 0 && buf[line_len - 1] == '\r') {
line_len--;
- if (line_len > 0 && buf[line_len - 1] == '\r') {
- line_len--;
- }
- buf[line_len] = '\0';
}
+ buf[line_len] = '\0';
}
-
- intern->u.file.current_line = zend_string_init(buf, line_len, /* persistent */ false);
- efree(buf);
}
+
+ intern->u.file.current_line = zend_string_init(buf, line_len, /* persistent */ false);
+ efree(buf);
intern->u.file.current_line_num += line_add;
return SUCCESS;
@@ -2091,10 +2094,17 @@ PHP_METHOD(SplFileObject, fgets)
CHECK_SPL_FILE_OBJECT_IS_INITIALIZED(intern);
- if (spl_filesystem_file_read_ex(intern, /* silent */ false, /* line_add */ 1, /* csv */ false) == FAILURE) {
- RETURN_THROWS();
+ if (intern->u.file.current_line) {
+ RETVAL_STR_COPY(intern->u.file.current_line);
+ spl_filesystem_file_free_line(intern);
+ intern->u.file.current_line_num++;
+ } else {
+ if (spl_filesystem_file_read_ex(intern, /* silent */ false, /* line_add */ 1, /* csv */ false) == FAILURE) {
+ RETURN_THROWS();
+ }
+ RETVAL_STR_COPY(intern->u.file.current_line);
+ spl_filesystem_file_free_line(intern);
}
- RETURN_STR_COPY(intern->u.file.current_line);
} /* }}} */
/* {{{ Return current line from file */
@@ -2140,6 +2150,12 @@ PHP_METHOD(SplFileObject, next)
ZEND_PARSE_PARAMETERS_NONE();
+ if (!intern->u.file.current_line && Z_ISUNDEF(intern->u.file.current_zval)) {
+ if (spl_filesystem_file_read_line(ZEND_THIS, intern, true) == FAILURE) {
+ return;
+ }
+ }
+
spl_filesystem_file_free_line(intern);
if (SPL_HAS_FLAG(intern->flags, SPL_FILE_OBJECT_READ_AHEAD)) {
spl_filesystem_file_read_line(ZEND_THIS, intern, true);
@@ -2627,7 +2643,7 @@ PHP_METHOD(SplFileObject, seek)
for (i = 0; i < line_pos; i++) {
if (spl_filesystem_file_read_line(ZEND_THIS, intern, true) == FAILURE) {
- return;
+ break;
}
}
if (line_pos > 0 && !SPL_HAS_FLAG(intern->flags, SPL_FILE_OBJECT_READ_AHEAD)) {
@@ -2646,9 +2662,8 @@ PHP_METHOD(SplFileObject, __toString)
if (!intern->u.file.current_line) {
ZEND_ASSERT(Z_ISUNDEF(intern->u.file.current_zval));
- zend_result result = spl_filesystem_file_read_line(ZEND_THIS, intern, false);
- if (UNEXPECTED(result != SUCCESS)) {
- RETURN_THROWS();
+ if (spl_filesystem_file_read_line(ZEND_THIS, intern, true) == FAILURE) {
+ RETURN_EMPTY_STRING();
}
}
diff --git a/ext/spl/tests/SplFileObject/SplFileObject_key_error001.phpt b/ext/spl/tests/SplFileObject/SplFileObject_key_error001.phpt
index 0c21d0b905e..7d0e3ae8d96 100644
--- a/ext/spl/tests/SplFileObject/SplFileObject_key_error001.phpt
+++ b/ext/spl/tests/SplFileObject/SplFileObject_key_error001.phpt
@@ -18,5 +18,5 @@
var_dump($s->valid());
?>
--EXPECT--
-int(14)
+int(12)
bool(false)
diff --git a/ext/spl/tests/SplFileObject/SplFileObject_key_error002.phpt b/ext/spl/tests/SplFileObject/SplFileObject_key_error002.phpt
index 8fc9b7fef0a..0834dbc0524 100644
--- a/ext/spl/tests/SplFileObject/SplFileObject_key_error002.phpt
+++ b/ext/spl/tests/SplFileObject/SplFileObject_key_error002.phpt
@@ -18,5 +18,5 @@
var_dump($s->valid());
?>
--EXPECT--
-int(13)
+int(12)
bool(false)
diff --git a/ext/spl/tests/SplFileObject/bug81477.phpt b/ext/spl/tests/SplFileObject/bug81477.phpt
index f7730a791aa..421c74dc4d6 100644
--- a/ext/spl/tests/SplFileObject/bug81477.phpt
+++ b/ext/spl/tests/SplFileObject/bug81477.phpt
@@ -21,7 +21,6 @@
"
string(10) "more,data
"
-string(0) ""
--CLEAN--
<?php
@unlink(__DIR__ . '/bug81477.csv');
diff --git a/ext/spl/tests/SplFileObject/fgetcsv_blank_file.phpt b/ext/spl/tests/SplFileObject/fgetcsv_blank_file.phpt
index a2dadecae41..7bd97a158c4 100644
--- a/ext/spl/tests/SplFileObject/fgetcsv_blank_file.phpt
+++ b/ext/spl/tests/SplFileObject/fgetcsv_blank_file.phpt
@@ -18,8 +18,5 @@
var_dump($file->fgetcsv());
?>
--EXPECT--
-array(1) {
- [0]=>
- NULL
-}
+bool(false)
bool(false)
diff --git a/ext/spl/tests/SplFileObject/gh8561.phpt b/ext/spl/tests/SplFileObject/gh8561.phpt
new file mode 100644
index 00000000000..adf36afb8b2
--- /dev/null
+++ b/ext/spl/tests/SplFileObject/gh8561.phpt
@@ -0,0 +1,30 @@
+--TEST--
+GH-8561 (SplFileObject: key() and current() unsynchronized after fgets())
+--FILE--
+<?php
+$file = new SplTempFileObject();
+for ($i = 0; $i < 5; $i++) {
+ $file->fwrite("line {$i}" . PHP_EOL);
+}
+
+// Case 1: rewind + fgets, then key/current
+$file->rewind();
+$file->fgets();
+echo "After rewind+fgets: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+
+// Case 2: multiple fgets
+$file->rewind();
+$file->fgets();
+$file->fgets();
+echo "After rewind+fgets+fgets: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+
+// Case 3: current then fgets
+$file->rewind();
+$file->current();
+$file->fgets();
+echo "After current+fgets: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+?>
+--EXPECT--
+After rewind+fgets: key=1 current=line 1
+After rewind+fgets+fgets: key=2 current=line 2
+After current+fgets: key=1 current=line 1
diff --git a/ext/spl/tests/SplFileObject/gh8563.phpt b/ext/spl/tests/SplFileObject/gh8563.phpt
new file mode 100644
index 00000000000..03891750f8b
--- /dev/null
+++ b/ext/spl/tests/SplFileObject/gh8563.phpt
@@ -0,0 +1,29 @@
+--TEST--
+GH-8563 (Different results for seek() on SplFileObject and SplTempFileObject)
+--FILE--
+<?php
+$path = __DIR__ . '/gh8563.txt';
+$file_01 = new SplFileObject($path, 'w+');
+$file_02 = new SplTempFileObject();
+
+for ($i = 0; $i < 5; $i++) {
+ $file_01->fwrite("line {$i}" . PHP_EOL);
+ $file_02->fwrite("line {$i}" . PHP_EOL);
+}
+
+$file_01->rewind();
+$file_02->rewind();
+
+$file_01->seek(10);
+$file_02->seek(10);
+
+echo 'SplFileObject: ' . $file_01->key() . "\n";
+echo 'SplTempFileObject: ' . $file_02->key() . "\n";
+?>
+--CLEAN--
+<?php
+unlink(__DIR__ . '/gh8563.txt');
+?>
+--EXPECT--
+SplFileObject: 5
+SplTempFileObject: 5
diff --git a/ext/spl/tests/SplFileObject/gh8564.phpt b/ext/spl/tests/SplFileObject/gh8564.phpt
new file mode 100644
index 00000000000..ff16893c4c6
--- /dev/null
+++ b/ext/spl/tests/SplFileObject/gh8564.phpt
@@ -0,0 +1,20 @@
+--TEST--
+GH-8564 (SplFileObject: next() moves to nonexistent indexes)
+--FILE--
+<?php
+$file = new SplTempFileObject();
+for ($i = 0; $i < 5; $i++) {
+ $file->fwrite("line {$i}" . PHP_EOL);
+}
+
+$file->rewind();
+for ($i = 0; $i < 10; $file->next(), $i++);
+echo "next() 10x: key=" . $file->key() . " valid=" . var_export($file->valid(), true) . "\n";
+
+$file->rewind();
+$file->seek(10);
+echo "seek(10): key=" . $file->key() . " valid=" . var_export($file->valid(), true) . "\n";
+?>
+--EXPECT--
+next() 10x: key=5 valid=false
+seek(10): key=5 valid=false
diff --git a/ext/spl/tests/gh13685.phpt b/ext/spl/tests/gh13685.phpt
index 0f679d0e93f..2bdddec4584 100644
--- a/ext/spl/tests/gh13685.phpt
+++ b/ext/spl/tests/gh13685.phpt
@@ -44,9 +44,9 @@
string(14) ""A", "B", "C"
"
string(13) ""D", "E", "F""
-Cannot read from file php://temp
+string(0) ""
--- Use csv control ---
string(14) ""A", "B", "C"
"
string(13) ""D", "E", "F""
-Cannot read from file php://temp
+string(0) ""
diff --git a/ext/spl/tests/gh8562.phpt b/ext/spl/tests/gh8562.phpt
new file mode 100644
index 00000000000..40b2554f579
--- /dev/null
+++ b/ext/spl/tests/gh8562.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GH-8562 (SplFileObject::current() returns wrong result after call to next())
+--FILE--
+<?php
+$file = new SplTempFileObject();
+for ($i = 0; $i < 5; $i++) {
+ $file->fwrite("line {$i}" . PHP_EOL);
+}
+
+$file->rewind();
+$file->next();
+echo "After rewind+next: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+
+$file->rewind();
+$file->next();
+$file->next();
+echo "After rewind+next+next: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+
+$file->rewind();
+$file->current();
+$file->next();
+echo "After current+next: key=" . $file->key() . " current=" . trim($file->current()) . "\n";
+?>
+--EXPECT--
+After rewind+next: key=1 current=line 1
+After rewind+next+next: key=2 current=line 2
+After current+next: key=1 current=line 1