Skip to content

Commit e0c66fe

Browse files
committed
gh-160811: fix TextIOWrapper.tell() after seek() past a carriage return
seek() to a cookie with chars_to_skip replayed the input chunk without updating the bytes-per-char ratio, so tell() could compute skip_bytes larger than the snapshot buffer. In the C implementation this read past the end of the buffer, and _pyio returned a wrong position.
1 parent e7ab898 commit e0c66fe

4 files changed

Lines changed: 32 additions & 0 deletions

File tree

‎Lib/_pyio.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,6 +2602,10 @@ def _reset_encoder(position):
26022602
self._set_decoded_chars(
26032603
self._decoder.decode(input_chunk, need_eof))
26042604
self._snapshot = (dec_flags, input_chunk)
2605+
if self._decoded_chars:
2606+
self._b2cratio = len(input_chunk) / len(self._decoded_chars)
2607+
else:
2608+
self._b2cratio = 0.0
26052609

26062610
# Skip chars_to_skip of the decoded characters.
26072611
if len(self._decoded_chars) < chars_to_skip:

‎Lib/test/test_io/test_textio.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,22 @@ def test_tell_after_readline_with_cr(self):
705705
remaining = f.read()
706706
self.assertEqual(remaining, "")
707707

708+
def test_tell_after_seek_past_cr(self):
709+
# seek() to a position after a carriage return must not leave a
710+
# stale bytes-per-char ratio behind for tell().
711+
for data in (b'one\rtwo\rthree\r', 'ab\r\u20ac\u20ac\u20ac'.encode()):
712+
expected = data.decode().replace('\r', '\n')
713+
for i in range(len(expected)):
714+
with self.subTest(data=data, i=i):
715+
f = self.TextIOWrapper(self.BytesIO(data),
716+
encoding='utf-8')
717+
f.read(i)
718+
pos = f.tell()
719+
f.read(1)
720+
f.seek(pos)
721+
self.assertEqual(f.read(1), expected[i])
722+
f.seek(f.tell())
723+
self.assertEqual(f.read(), expected[i + 1:])
708724

709725
def test_seek_with_encoder_state(self):
710726
f = self.open(os_helper.TESTFN, "w", encoding="euc_jis_2004")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`io.TextIOWrapper.tell` after :meth:`~io.TextIOWrapper.seek` to a
2+
position that follows a carriage return. The bytes-per-char ratio was not
3+
updated when the input chunk was replayed, so ``tell()`` could read past an
4+
internal buffer and return a wrong position.

‎Modules/_io/textio.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,6 +2812,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
28122812
goto fail;
28132813
}
28142814

2815+
Py_ssize_t nbytes = PyBytes_GET_SIZE(input_chunk);
28152816
snapshot = Py_BuildValue("iN", cookie.dec_flags, input_chunk);
28162817
if (snapshot == NULL) {
28172818
goto fail;
@@ -2824,7 +2825,14 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
28242825
if (check_decoded(decoded) < 0)
28252826
goto fail;
28262827

2828+
Py_ssize_t nchars = PyUnicode_GET_LENGTH(decoded);
28272829
textiowrapper_set_decoded_chars(self, decoded);
2830+
if (nchars > 0) {
2831+
self->b2cratio = (double) nbytes / nchars;
2832+
}
2833+
else {
2834+
self->b2cratio = 0.0;
2835+
}
28282836

28292837
/* Skip chars_to_skip of the decoded characters. */
28302838
if (PyUnicode_GetLength(self->decoded_chars) < cookie.chars_to_skip) {

0 commit comments

Comments
 (0)