Skip to content

Commit fe6dd19

Browse files
committed
pythongh-141314: Fix TextIOWrapper.tell() assertion failure with standalone carriage return
When TextIOWrapper.tell() is called after reading a line that ends with a standalone carriage return (\r), the tell optimization algorithm incorrectly assumes there is buffered data to search through. This causes an assertion failure when skip_back=1 exceeds the empty buffer size. The fix detects when next_input is empty and skips the optimization phase, falling back to the byte-by-byte decoding method which always works correctly. This properly handles the architectural constraint that buffer optimization cannot function without buffered data.
1 parent d13ee0a commit fe6dd19

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

Lib/test/test_io/test_textio.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,24 @@ def test_multibyte_seek_and_tell(self):
686686
self.assertEqual(f.tell(), p1)
687687
f.close()
688688

689+
def test_tell_after_readline_with_cr(self):
690+
# Test for gh-141314: TextIOWrapper.tell() assertion failure
691+
# when dealing with standalone carriage returns
692+
data = b'line1=1\r'
693+
with self.open(os_helper.TESTFN, "wb") as f:
694+
f.write(data)
695+
696+
with self.open(os_helper.TESTFN, "r") as f:
697+
# Read line that ends with \r
698+
line = f.readline()
699+
self.assertEqual(line, "line1=1\n")
700+
# This should not cause an assertion failure
701+
pos = f.tell()
702+
# Verify we can seek back to this position
703+
f.seek(pos)
704+
remaining = f.read()
705+
self.assertEqual(remaining, "")
706+
689707
def test_seek_with_encoder_state(self):
690708
f = self.open(os_helper.TESTFN, "w", encoding="euc_jis_2004")
691709
f.write("\u00e6\u0300")

Modules/_io/textio.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,10 +2844,16 @@ _io_TextIOWrapper_tell_impl(textio *self)
28442844
/* Fast search for an acceptable start point, close to our
28452845
current pos */
28462846
skip_bytes = (Py_ssize_t) (self->b2cratio * chars_to_skip);
2847-
skip_back = 1;
2848-
assert(skip_back <= PyBytes_GET_SIZE(next_input));
2849-
input = PyBytes_AS_STRING(next_input);
2850-
while (skip_bytes > 0) {
2847+
2848+
/* Skip the optimization if next_input is empty */
2849+
if (PyBytes_GET_SIZE(next_input) == 0) {
2850+
skip_bytes = 0;
2851+
}
2852+
else {
2853+
skip_back = 1;
2854+
assert(skip_back <= PyBytes_GET_SIZE(next_input));
2855+
input = PyBytes_AS_STRING(next_input);
2856+
while (skip_bytes > 0) {
28512857
/* Decode up to temptative start point */
28522858
if (_textiowrapper_decoder_setstate(self, &cookie) < 0)
28532859
goto fail;
@@ -2870,6 +2876,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
28702876
skip_back *= 2;
28712877
}
28722878
}
2879+
}
28732880
if (skip_bytes <= 0) {
28742881
skip_bytes = 0;
28752882
if (_textiowrapper_decoder_setstate(self, &cookie) < 0)

0 commit comments

Comments
 (0)