Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Lib/test/test_io/test_memoryio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,18 @@ class CStringIOTest(PyStringIOTest):
ioclass = io.StringIO
UnsupportedOperation = io.UnsupportedOperation

def test_truncate_size_closes_stream(self):
stream = self.ioclass()
stream.write("abc")

class Size:
def __index__(self):
stream.close()
return 0

with self.assertRaises(ValueError):
stream.truncate(Size())

def test_truncate_large_size(self):
# Sizes above 2**31 - 1 must work where C long is 32-bit.
if sys.maxsize > 2**31:
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_io/test_textio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,17 @@ def make_text(buffer):
wrapper.write('x')
self.assertRaisesRegex(ValueError, "detached", wrapper.read)

def test_read_reinitialized_as_write_only(self):
class Buffer(self.BytesIO):
def read(self, size=-1):
stream.__init__(writer, encoding="ascii")
return b"a"

with (self.BufferedWriter(self.BytesIO()) as writer,
self.TextIOWrapper(Buffer(), encoding="ascii") as stream):
with self.assertRaises(self.UnsupportedOperation):
stream.read()

def test_reentrant_detach_during_read(self):
# gh-157363, gh-157364: The buffer must stay alive until its active
# read operation returns.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix crashes in :class:`io.TextIOWrapper` and
:class:`io.IncrementalNewlineDecoder` when a callback changes the active
codec or decoded buffer. :class:`io.StringIO` now raises :exc:`ValueError`
if converting the size passed to ``truncate()`` closes the stream.
3 changes: 3 additions & 0 deletions Modules/_io/stringio.c
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,9 @@ _io_StringIO_truncate_impl(stringio *self, PyObject *pos)
"negative pos value %zd", size);
return NULL;
}
/* __index__ may have closed or reinitialized the stream. */
CHECK_INITIALIZED(self);
CHECK_CLOSED(self);
}

if (size < self->string_size) {
Expand Down
29 changes: 18 additions & 11 deletions Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,11 @@ _PyIncrementalNewlineDecoder_decode(PyObject *myself,

/* decode input (with the eventual \r from a previous pass) */
if (self->decoder != Py_None) {
output = PyObject_CallMethodObjArgs(self->decoder,
/* The wrapper can be reinitialized by a decoding callback. */
PyObject *decoder = Py_NewRef(self->decoder);
output = PyObject_CallMethodObjArgs(decoder,
&_Py_ID(decode), input, final ? Py_True : Py_False, NULL);
Py_DECREF(decoder);
}
else {
output = Py_NewRef(input);
Expand Down Expand Up @@ -1001,11 +1004,17 @@ _textiowrapper_decode(_PyIO_State *state, PyObject *decoder, PyObject *bytes,
{
PyObject *chars;

if (decoder == NULL) {
return _unsupported(state, "not readable");
}
/* A codec callback may replace the wrapper's decoder. */
Py_INCREF(decoder);
if (Py_IS_TYPE(decoder, state->PyIncrementalNewlineDecoder_Type))
chars = _PyIncrementalNewlineDecoder_decode(decoder, bytes, eof);
else
chars = PyObject_CallMethodObjArgs(decoder, &_Py_ID(decode), bytes,
eof ? Py_True : Py_False, NULL);
Py_DECREF(decoder);

if (check_decoded(chars) < 0)
// check_decoded already decreases refcount
Expand Down Expand Up @@ -1784,7 +1793,10 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
self->encoding_start_of_stream = 0;
}
else {
b = PyObject_CallMethodOneArg(self->encoder, &_Py_ID(encode), text);
/* A codec callback may reconfigure the stream. */
PyObject *encoder = Py_NewRef(self->encoder);
b = PyObject_CallMethodOneArg(encoder, &_Py_ID(encode), text);
Py_DECREF(encoder);
}

Py_DECREF(text);
Expand Down Expand Up @@ -2024,8 +2036,9 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
if (decoded_chars == NULL)
goto fail;

textiowrapper_set_decoded_chars(self, decoded_chars);
/* The old chunk's finalizer can clear the new chunk. */
nchars = PyUnicode_GET_LENGTH(decoded_chars);
textiowrapper_set_decoded_chars(self, decoded_chars);
if (nchars > 0)
self->b2cratio = (double) nbytes / nchars;
else
Expand Down Expand Up @@ -2097,15 +2110,9 @@ _io_TextIOWrapper_read_impl(textio *self, Py_ssize_t n)
return NULL;
}

_PyIO_State *state = self->state;
if (Py_IS_TYPE(self->decoder, state->PyIncrementalNewlineDecoder_Type))
decoded = _PyIncrementalNewlineDecoder_decode(self->decoder,
bytes, 1);
else
decoded = PyObject_CallMethodObjArgs(
self->decoder, &_Py_ID(decode), bytes, Py_True, NULL);
decoded = _textiowrapper_decode(self->state, self->decoder, bytes, 1);
Py_DECREF(bytes);
if (check_decoded(decoded) < 0)
if (decoded == NULL)
goto fail;

result = textiowrapper_get_decoded_chars(self, -1);
Expand Down
Loading