Skip to content

Commit 1269a78

Browse files
committed
bpo-20082: fix misbehavior of buffered writes to raw files in append mode
1 parent e3c971c commit 1269a78

2 files changed

Lines changed: 88 additions & 19 deletions

File tree

Lib/test/test_io.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,6 +2396,31 @@ def test_interleaved_readline_write(self):
23962396
f.flush()
23972397
self.assertEqual(raw.getvalue(), b'1b\n2def\n3\n')
23982398

2399+
def test_append_write(self):
2400+
# Uses a real FileIO so that append behavior is reproduced accurately
2401+
with self.FileIO(os_helper.TESTFN, 'wb') as f:
2402+
f.write(b'test test')
2403+
2404+
with self.FileIO(os_helper.TESTFN, 'ab+') as raw:
2405+
with self.tp(raw) as f:
2406+
self.assertEqual(f.tell(), 9)
2407+
f.write(b'A')
2408+
f.seek(0)
2409+
self.assertEqual(f.read(), b'test testA')
2410+
f.seek(0)
2411+
self.assertEqual(f.read(1), b't')
2412+
self.assertEqual(f.write(b'B'), 1)
2413+
f.seek(0)
2414+
2415+
# This read previously returned b'tBst testA' but that is
2416+
# inccorect if the underlying raw file is in append mode;
2417+
# see bpo-20082
2418+
self.assertEqual(f.read(), b'test testAB')
2419+
f.flush()
2420+
f.seek(0)
2421+
self.assertEqual(f.read(), b'test testAB')
2422+
2423+
23992424
# You can't construct a BufferedRandom over a non-seekable stream.
24002425
test_unseekable = None
24012426

Modules/_io/bufferedio.c

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ typedef struct {
204204
int detached;
205205
int readable;
206206
int writable;
207+
int appending;
207208
char finalizing;
208209

209210
/* True if this is a vanilla Buffered object (rather than a user derived
@@ -1204,6 +1205,33 @@ buffered_tell(buffered *self, PyObject *Py_UNUSED(ignored))
12041205
return PyLong_FromOff_t(pos);
12051206
}
12061207

1208+
static PyObject *
1209+
_buffered_seek_unlocked(buffered *self, Py_off_t target, int whence)
1210+
{
1211+
Py_off_t n;
1212+
PyObject *res = NULL;
1213+
1214+
if (self->writable) {
1215+
res = _bufferedwriter_flush_unlocked(self);
1216+
if (res == NULL)
1217+
return res;
1218+
Py_CLEAR(res);
1219+
}
1220+
1221+
/* TODO: align on block boundary and read buffer if needed? */
1222+
if (whence == 1)
1223+
target -= RAW_OFFSET(self);
1224+
n = _buffered_raw_seek(self, target, whence);
1225+
if (n == -1)
1226+
return res;
1227+
self->raw_pos = -1;
1228+
res = PyLong_FromOff_t(n);
1229+
if (res != NULL && self->readable)
1230+
_bufferedreader_reset_buf(self);
1231+
1232+
return res;
1233+
}
1234+
12071235
/*[clinic input]
12081236
_io._Buffered.seek
12091237
target as targetobj: object
@@ -1215,7 +1243,7 @@ static PyObject *
12151243
_io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
12161244
/*[clinic end generated code: output=7ae0e8dc46efdefb input=a9c4920bfcba6163]*/
12171245
{
1218-
Py_off_t target, n;
1246+
Py_off_t target;
12191247
PyObject *res = NULL;
12201248

12211249
CHECK_INITIALIZED(self)
@@ -1276,25 +1304,8 @@ _io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
12761304
return NULL;
12771305

12781306
/* Fallback: invoke raw seek() method and clear buffer */
1279-
if (self->writable) {
1280-
res = _bufferedwriter_flush_unlocked(self);
1281-
if (res == NULL)
1282-
goto end;
1283-
Py_CLEAR(res);
1284-
}
1307+
res = _buffered_seek_unlocked(self, target, whence);
12851308

1286-
/* TODO: align on block boundary and read buffer if needed? */
1287-
if (whence == 1)
1288-
target -= RAW_OFFSET(self);
1289-
n = _buffered_raw_seek(self, target, whence);
1290-
if (n == -1)
1291-
goto end;
1292-
self->raw_pos = -1;
1293-
res = PyLong_FromOff_t(n);
1294-
if (res != NULL && self->readable)
1295-
_bufferedreader_reset_buf(self);
1296-
1297-
end:
12981309
LEAVE_BUFFERED(self)
12991310
return res;
13001311
}
@@ -1767,6 +1778,27 @@ _bufferedwriter_reset_buf(buffered *self)
17671778
self->write_end = -1;
17681779
}
17691780

1781+
static void
1782+
_bufferedwriter_set_append(buffered *self)
1783+
{
1784+
PyObject *mode;
1785+
1786+
mode = _PyObject_GetAttrId(self->raw, &PyId_mode);
1787+
if (mode != NULL) {
1788+
/* Raw fileobj has no mode string so as far as we can know it has
1789+
normal write behavior */
1790+
if (PyUnicode_FindChar(mode, 'a', 0, PyUnicode_GET_LENGTH(mode), 1) != -1) {
1791+
self->appending = 1;
1792+
} else {
1793+
self->appending = 0;
1794+
}
1795+
Py_DECREF(mode);
1796+
} else {
1797+
PyErr_Clear();
1798+
self->appending = 0;
1799+
}
1800+
}
1801+
17701802
/*[clinic input]
17711803
_io.BufferedWriter.__init__
17721804
raw: object
@@ -1795,6 +1827,8 @@ _io_BufferedWriter___init___impl(buffered *self, PyObject *raw,
17951827
self->readable = 0;
17961828
self->writable = 1;
17971829

1830+
_bufferedwriter_set_append(self);
1831+
17981832
self->buffer_size = buffer_size;
17991833
if (_buffered_init(self) < 0)
18001834
return -1;
@@ -1944,6 +1978,14 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer)
19441978
self->pos = 0;
19451979
self->raw_pos = 0;
19461980
}
1981+
1982+
if (self->appending) {
1983+
res = _buffered_seek_unlocked(self, 0, SEEK_END);
1984+
if (res == NULL)
1985+
goto error;
1986+
Py_DECREF(res);
1987+
}
1988+
19471989
avail = Py_SAFE_DOWNCAST(self->buffer_size - self->pos, Py_off_t, Py_ssize_t);
19481990
if (buffer->len <= avail) {
19491991
memcpy(self->buffer + self->pos, buffer->buf, buffer->len);
@@ -2309,6 +2351,8 @@ _io_BufferedRandom___init___impl(buffered *self, PyObject *raw,
23092351
self->readable = 1;
23102352
self->writable = 1;
23112353

2354+
_bufferedwriter_set_append(self);
2355+
23122356
if (_buffered_init(self) < 0)
23132357
return -1;
23142358
_bufferedreader_reset_buf(self);

0 commit comments

Comments
 (0)