Skip to content

Commit 4199b88

Browse files
committed
bpo-20082: fix misbehavior of buffered writes to raw files in append mode
1 parent 0274d83 commit 4199b88

2 files changed

Lines changed: 87 additions & 19 deletions

File tree

Lib/test/test_io/test_bufferedio.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,30 @@ def test_interleaved_readline_write(self):
14731473
f.flush()
14741474
self.assertEqual(raw.getvalue(), b'1b\n2def\n3\n')
14751475

1476+
def test_append_write(self):
1477+
# Uses a real FileIO so that append behavior is reproduced accurately
1478+
with self.FileIO(os_helper.TESTFN, 'wb') as f:
1479+
f.write(b'test test')
1480+
1481+
with self.FileIO(os_helper.TESTFN, 'ab+') as raw:
1482+
with self.tp(raw) as f:
1483+
self.assertEqual(f.tell(), 9)
1484+
f.write(b'A')
1485+
f.seek(0)
1486+
self.assertEqual(f.read(), b'test testA')
1487+
f.seek(0)
1488+
self.assertEqual(f.read(1), b't')
1489+
self.assertEqual(f.write(b'B'), 1)
1490+
f.seek(0)
1491+
1492+
# This read previously returned b'tBst testA' but that is
1493+
# inccorect if the underlying raw file is in append mode;
1494+
# see bpo-20082
1495+
self.assertEqual(f.read(), b'test testAB')
1496+
f.flush()
1497+
f.seek(0)
1498+
self.assertEqual(f.read(), b'test testAB')
1499+
14761500
# You can't construct a BufferedRandom over a non-seekable stream.
14771501
test_unseekable = None
14781502

Modules/_io/bufferedio.c

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ typedef struct {
228228
int detached;
229229
int readable;
230230
int writable;
231+
int appending;
231232
char finalizing;
232233

233234
/* True if this is a vanilla Buffered object (rather than a user derived
@@ -1350,6 +1351,33 @@ _io__Buffered_tell_impl(buffered *self)
13501351
return PyLong_FromOff_t(pos);
13511352
}
13521353

1354+
static PyObject *
1355+
_buffered_seek_unlocked(buffered *self, Py_off_t target, int whence)
1356+
{
1357+
Py_off_t n;
1358+
PyObject *res = NULL;
1359+
1360+
if (self->writable) {
1361+
res = _bufferedwriter_flush_unlocked(self);
1362+
if (res == NULL)
1363+
return res;
1364+
Py_CLEAR(res);
1365+
}
1366+
1367+
/* TODO: align on block boundary and read buffer if needed? */
1368+
if (whence == 1)
1369+
target -= RAW_OFFSET(self);
1370+
n = _buffered_raw_seek(self, target, whence);
1371+
if (n == -1)
1372+
return res;
1373+
self->raw_pos = -1;
1374+
res = PyLong_FromOff_t(n);
1375+
if (res != NULL && self->readable)
1376+
_bufferedreader_reset_buf(self);
1377+
1378+
return res;
1379+
}
1380+
13531381
/*[clinic input]
13541382
@critical_section
13551383
_io._Buffered.seek
@@ -1362,7 +1390,7 @@ static PyObject *
13621390
_io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
13631391
/*[clinic end generated code: output=7ae0e8dc46efdefb input=b5a12be70e0ad07b]*/
13641392
{
1365-
Py_off_t target, n;
1393+
Py_off_t target;
13661394
PyObject *res = NULL;
13671395

13681396
CHECK_INITIALIZED(self)
@@ -1430,25 +1458,8 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
14301458
return NULL;
14311459

14321460
/* Fallback: invoke raw seek() method and clear buffer */
1433-
if (self->writable) {
1434-
res = _bufferedwriter_flush_unlocked(self);
1435-
if (res == NULL)
1436-
goto end;
1437-
Py_CLEAR(res);
1438-
}
1461+
res = _buffered_seek_unlocked(self, target, whence);
14391462

1440-
/* TODO: align on block boundary and read buffer if needed? */
1441-
if (whence == 1)
1442-
target -= RAW_OFFSET(self);
1443-
n = _buffered_raw_seek(self, target, whence);
1444-
if (n == -1)
1445-
goto end;
1446-
self->raw_pos = -1;
1447-
res = PyLong_FromOff_t(n);
1448-
if (res != NULL && self->readable)
1449-
_bufferedreader_reset_buf(self);
1450-
1451-
end:
14521463
LEAVE_BUFFERED(self)
14531464
return res;
14541465
}
@@ -1926,6 +1937,27 @@ _bufferedwriter_reset_buf(buffered *self)
19261937
self->write_end = -1;
19271938
}
19281939

1940+
static void
1941+
_bufferedwriter_set_append(buffered *self)
1942+
{
1943+
PyObject *mode;
1944+
1945+
mode = _PyObject_GetAttrId(self->raw, &PyId_mode);
1946+
if (mode != NULL) {
1947+
/* Raw fileobj has no mode string so as far as we can know it has
1948+
normal write behavior */
1949+
if (PyUnicode_FindChar(mode, 'a', 0, PyUnicode_GET_LENGTH(mode), 1) != -1) {
1950+
self->appending = 1;
1951+
} else {
1952+
self->appending = 0;
1953+
}
1954+
Py_DECREF(mode);
1955+
} else {
1956+
PyErr_Clear();
1957+
self->appending = 0;
1958+
}
1959+
}
1960+
19291961
/*[clinic input]
19301962
_io.BufferedWriter.__init__
19311963
raw: object
@@ -1956,6 +1988,8 @@ _io_BufferedWriter___init___impl(buffered *self, PyObject *raw,
19561988
self->readable = 0;
19571989
self->writable = 1;
19581990

1991+
_bufferedwriter_set_append(self);
1992+
19591993
self->buffer_size = buffer_size;
19601994
if (_buffered_init(self) < 0)
19611995
return -1;
@@ -2111,6 +2145,14 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer)
21112145
self->pos = 0;
21122146
self->raw_pos = 0;
21132147
}
2148+
2149+
if (self->appending) {
2150+
res = _buffered_seek_unlocked(self, 0, SEEK_END);
2151+
if (res == NULL)
2152+
goto error;
2153+
Py_DECREF(res);
2154+
}
2155+
21142156
avail = Py_SAFE_DOWNCAST(self->buffer_size - self->pos, Py_off_t, Py_ssize_t);
21152157
if (buffer->len <= avail && buffer->len < self->buffer_size) {
21162158
memcpy(self->buffer + self->pos, buffer->buf, buffer->len);
@@ -2504,6 +2546,8 @@ _io_BufferedRandom___init___impl(buffered *self, PyObject *raw,
25042546
self->readable = 1;
25052547
self->writable = 1;
25062548

2549+
_bufferedwriter_set_append(self);
2550+
25072551
if (_buffered_init(self) < 0)
25082552
return -1;
25092553
_bufferedreader_reset_buf(self);

0 commit comments

Comments
 (0)