]> granicus.if.org Git - python/commitdiff
SF bug 801631: file.truncate fault on windows.
authorTim Peters <tim.peters@gmail.com>
Sun, 7 Sep 2003 03:30:18 +0000 (03:30 +0000)
committerTim Peters <tim.peters@gmail.com>
Sun, 7 Sep 2003 03:30:18 +0000 (03:30 +0000)
file_truncate():  C doesn't define what fflush(fp) does if fp is open
for update, and the preceding I/O operation on fp was input.  On Windows,
fflush() actually changes the current file position then.  Because
Windows doesn't support ftruncate() directly, this not only caused
Python's file.truncate() to change the file position (contra our docs),
it also caused the file not to change size.

Repaired by getting the initial file position at the start, restoring
it at the end, and tossing all the complicated micro-efficiency checks
trying to avoid "provably unnecessary" seeks.  file.truncate() can't
be a frequent operation, and seeking to the current file position has
got to be cheap anyway.

Bugfix candidate.

Lib/test/test_file.py
Misc/NEWS
Objects/fileobject.c

index 677dafc3f0dfa95a1d430f098c831e0cdafdfadf..b8bcab7bd8f7db643aa26bd586be0d0551100a3e 100644 (file)
@@ -132,3 +132,31 @@ else:
     raise TestFailed, 'file.writelines([]) on a closed file should raise a ValueError'
 
 os.unlink(TESTFN)
+
+def bug801631():
+    # SF bug <http://www.python.org/sf/801631>
+    # "file.truncate fault on windows"
+    f = file(TESTFN, 'wb')
+    f.write('12345678901')   # 11 bytes
+    f.close()
+
+    f = file(TESTFN,'rb+')
+    data = f.read(5)
+    if data != '12345':
+        raise TestFailed("Read on file opened for update failed %r" % data)
+    if f.tell() != 5:
+        raise TestFailed("File pos after read wrong %d" % f.tell())
+
+    f.truncate()
+    if f.tell() != 5:
+        raise TestFailed("File pos after ftruncate wrong %d" % f.tell())
+
+    f.close()
+    size = os.path.getsize(TESTFN)
+    if size != 5:
+        raise TestFailed("File size after ftruncate wrong %d" % size)
+
+try:
+    bug801631()
+finally:
+    os.unlink(TESTFN)
index a3b3a6c1e542a30bcaa5fc813e744d8453f5bb97..1632b65700c1b345774dc99f5ed7efc18c708bc4 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -94,6 +94,10 @@ Tests
 Windows
 -------
 
+- file.truncate() could misbehave if the file was open for update
+  (modes r+, rb+, w+, wb+), and the most recent file operation before
+  the truncate() call was an input operation.  SF bug 801631.
+
 Mac
 ----
 
index bddd81e93ad23e64a74a5d0d2a9b1f633d9c4e2f..cdcacc41aff08edee6f666c94ce3c04e69b58821 100644 (file)
@@ -96,7 +96,7 @@ dircheck(PyFileObject* f)
 #else
                char *msg = "Is a directory";
 #endif
-               PyObject *exc = PyObject_CallFunction(PyExc_IOError, "(is)", 
+               PyObject *exc = PyObject_CallFunction(PyExc_IOError, "(is)",
                                                      EISDIR, msg);
                PyErr_SetObject(PyExc_IOError, exc);
                Py_XDECREF(exc);
@@ -137,7 +137,7 @@ fill_file_fields(PyFileObject *f, FILE *fp, char *name, char *mode,
 #endif
        Py_INCREF(Py_None);
        f->f_encoding = Py_None;
-       
+
        if (f->f_name == NULL || f->f_mode == NULL)
                return NULL;
        f->f_fp = fp;
@@ -189,8 +189,8 @@ open_the_file(PyFileObject *f, char *name, char *mode)
 #endif
 #ifdef MS_WINDOWS
                if (PyUnicode_Check(f->f_name)) {
-                       PyObject *wmode; 
-                       wmode = PyUnicode_DecodeASCII(mode, strlen(mode), NULL); 
+                       PyObject *wmode;
+                       wmode = PyUnicode_DecodeASCII(mode, strlen(mode), NULL);
                        if (f->f_name && wmode) {
                                Py_BEGIN_ALLOW_THREADS
                                /* PyUnicode_AS_UNICODE OK without thread
@@ -529,18 +529,33 @@ file_seek(PyFileObject *f, PyObject *args)
 static PyObject *
 file_truncate(PyFileObject *f, PyObject *args)
 {
-       int ret;
        Py_off_t newsize;
-       PyObject *newsizeobj;
+       PyObject *newsizeobj = NULL;
+       Py_off_t initialpos;
+       int ret;
 
        if (f->f_fp == NULL)
                return err_closed();
-       newsizeobj = NULL;
        if (!PyArg_UnpackTuple(args, "truncate", 0, 1, &newsizeobj))
                return NULL;
 
+       /* Get current file position.  If the file happens to be open for
+        * update and the last operation was an input operation, C doesn't
+        * define what the later fflush() will do, but we promise truncate()
+        * won't change the current position (and fflush() *does* change it
+        * then at least on Windows).  The easiest thing is to capture
+        * current pos now and seek back to it at the end.
+        */
+       Py_BEGIN_ALLOW_THREADS
+       errno = 0;
+       initialpos = _portable_ftell(f->f_fp);
+       Py_END_ALLOW_THREADS
+       if (initialpos == -1)
+               goto onioerror;
+
        /* Set newsize to current postion if newsizeobj NULL, else to the
-          specified value. */
+        * specified value.
+        */
        if (newsizeobj != NULL) {
 #if !defined(HAVE_LARGEFILE_SUPPORT)
                newsize = PyInt_AsLong(newsizeobj);
@@ -552,17 +567,13 @@ file_truncate(PyFileObject *f, PyObject *args)
                if (PyErr_Occurred())
                        return NULL;
        }
-       else {
-               /* Default to current position. */
-               Py_BEGIN_ALLOW_THREADS
-               errno = 0;
-               newsize = _portable_ftell(f->f_fp);
-               Py_END_ALLOW_THREADS
-               if (newsize == -1)
-                       goto onioerror;
-       }
+       else /* default to current position */
+               newsize = initialpos;
 
-       /* Flush the file. */
+       /* Flush the stream.  We're mixing stream-level I/O with lower-level
+        * I/O, and a flush may be necessary to synch both platform views
+        * of the current file state.
+        */
        Py_BEGIN_ALLOW_THREADS
        errno = 0;
        ret = fflush(f->f_fp);
@@ -574,66 +585,47 @@ file_truncate(PyFileObject *f, PyObject *args)
        /* MS _chsize doesn't work if newsize doesn't fit in 32 bits,
           so don't even try using it. */
        {
-               Py_off_t current;       /* current file position */
                HANDLE hFile;
-               int error;
 
-               /* current <- current file postion. */
-               if (newsizeobj == NULL)
-                       current = newsize;
-               else {
-                       Py_BEGIN_ALLOW_THREADS
-                       errno = 0;
-                       current = _portable_ftell(f->f_fp);
-                       Py_END_ALLOW_THREADS
-                       if (current == -1)
-                               goto onioerror;
-               }
-
-               /* Move to newsize. */
-               if (current != newsize) {
-                       Py_BEGIN_ALLOW_THREADS
-                       errno = 0;
-                       error = _portable_fseek(f->f_fp, newsize, SEEK_SET)
-                               != 0;
-                       Py_END_ALLOW_THREADS
-                       if (error)
-                               goto onioerror;
-               }
+               /* Have to move current pos to desired endpoint on Windows. */
+               Py_BEGIN_ALLOW_THREADS
+               errno = 0;
+               ret = _portable_fseek(f->f_fp, newsize, SEEK_SET) != 0;
+               Py_END_ALLOW_THREADS
+               if (ret)
+                       goto onioerror;
 
                /* Truncate.  Note that this may grow the file! */
                Py_BEGIN_ALLOW_THREADS
                errno = 0;
                hFile = (HANDLE)_get_osfhandle(fileno(f->f_fp));
-               error = hFile == (HANDLE)-1;
-               if (!error) {
-                       error = SetEndOfFile(hFile) == 0;
-                       if (error)
+               ret = hFile == (HANDLE)-1;
+               if (ret == 0) {
+                       ret = SetEndOfFile(hFile) == 0;
+                       if (ret)
                                errno = EACCES;
                }
                Py_END_ALLOW_THREADS
-               if (error)
+               if (ret)
                        goto onioerror;
-
-               /* Restore original file position. */
-               if (current != newsize) {
-                       Py_BEGIN_ALLOW_THREADS
-                       errno = 0;
-                       error = _portable_fseek(f->f_fp, current, SEEK_SET)
-                               != 0;
-                       Py_END_ALLOW_THREADS
-                       if (error)
-                               goto onioerror;
-               }
        }
 #else
        Py_BEGIN_ALLOW_THREADS
        errno = 0;
        ret = ftruncate(fileno(f->f_fp), newsize);
        Py_END_ALLOW_THREADS
-       if (ret != 0) goto onioerror;
+       if (ret != 0)
+               goto onioerror;
 #endif /* !MS_WINDOWS */
 
+       /* Restore original file position. */
+       Py_BEGIN_ALLOW_THREADS
+       errno = 0;
+       ret = _portable_fseek(f->f_fp, initialpos, SEEK_SET) != 0;
+       Py_END_ALLOW_THREADS
+       if (ret)
+               goto onioerror;
+
        Py_INCREF(Py_None);
        return Py_None;
 
@@ -866,7 +858,7 @@ file_readinto(PyFileObject *f, PyObject *args)
        while (ntodo > 0) {
                Py_BEGIN_ALLOW_THREADS
                errno = 0;
-               nnow = Py_UniversalNewlineFread(ptr+ndone, ntodo, f->f_fp, 
+               nnow = Py_UniversalNewlineFread(ptr+ndone, ntodo, f->f_fp,
                                                (PyObject *)f);
                Py_END_ALLOW_THREADS
                if (nnow == 0) {
@@ -1137,8 +1129,8 @@ get_line(PyFileObject *f, int n)
                                if (skipnextlf ) {
                                        skipnextlf = 0;
                                        if (c == '\n') {
-                                               /* Seeing a \n here with 
-                                                * skipnextlf true means we 
+                                               /* Seeing a \n here with
+                                                * skipnextlf true means we
                                                 * saw a \r before.
                                                 */
                                                newlinetypes |= NEWLINE_CRLF;
@@ -1734,8 +1726,8 @@ get_newlines(PyFileObject *f, void *closure)
        case NEWLINE_CR|NEWLINE_LF|NEWLINE_CRLF:
                return Py_BuildValue("(sss)", "\r", "\n", "\r\n");
        default:
-               PyErr_Format(PyExc_SystemError, 
-                            "Unknown newlines value 0x%x\n", 
+               PyErr_Format(PyExc_SystemError,
+                            "Unknown newlines value 0x%x\n",
                             f->f_newlinetypes);
                return NULL;
        }
@@ -1745,7 +1737,7 @@ get_newlines(PyFileObject *f, void *closure)
 static PyGetSetDef file_getsetlist[] = {
        {"closed", (getter)get_closed, NULL, "True if the file is closed"},
 #ifdef WITH_UNIVERSAL_NEWLINES
-       {"newlines", (getter)get_newlines, NULL, 
+       {"newlines", (getter)get_newlines, NULL,
         "end-of-line convention used in this file"},
 #endif
        {0},
@@ -1760,8 +1752,8 @@ drop_readahead(PyFileObject *f)
        }
 }
 
-/* Make sure that file has a readahead buffer with at least one byte 
-   (unless at EOF) and no more than bufsize.  Returns negative value on 
+/* Make sure that file has a readahead buffer with at least one byte
+   (unless at EOF) and no more than bufsize.  Returns negative value on
    error */
 static int
 readahead(PyFileObject *f, int bufsize)
@@ -1769,7 +1761,7 @@ readahead(PyFileObject *f, int bufsize)
        int chunksize;
 
        if (f->f_buf != NULL) {
-               if( (f->f_bufend - f->f_bufptr) >= 1) 
+               if( (f->f_bufend - f->f_bufptr) >= 1)
                        return 0;
                else
                        drop_readahead(f);
@@ -1796,8 +1788,8 @@ readahead(PyFileObject *f, int bufsize)
 }
 
 /* Used by file_iternext.  The returned string will start with 'skip'
-   uninitialized bytes followed by the remainder of the line. Don't be 
-   horrified by the recursive call: maximum recursion depth is limited by 
+   uninitialized bytes followed by the remainder of the line. Don't be
+   horrified by the recursive call: maximum recursion depth is limited by
    logarithmic buffer growth to about 50 even when reading a 1gb line. */
 
 static PyStringObject *
@@ -1809,11 +1801,11 @@ readahead_get_line_skip(PyFileObject *f, int skip, int bufsize)
        int len;
 
        if (f->f_buf == NULL)
-               if (readahead(f, bufsize) < 0) 
+               if (readahead(f, bufsize) < 0)
                        return NULL;
 
        len = f->f_bufend - f->f_bufptr;
-       if (len == 0) 
+       if (len == 0)
                return (PyStringObject *)
                        PyString_FromStringAndSize(NULL, skip);
        bufptr = memchr(f->f_bufptr, '\n', len);
@@ -1822,7 +1814,7 @@ readahead_get_line_skip(PyFileObject *f, int skip, int bufsize)
                len = bufptr - f->f_bufptr;
                s = (PyStringObject *)
                        PyString_FromStringAndSize(NULL, skip+len);
-               if (s == NULL) 
+               if (s == NULL)
                        return NULL;
                memcpy(PyString_AS_STRING(s)+skip, f->f_bufptr, len);
                f->f_bufptr = bufptr;
@@ -2079,7 +2071,7 @@ PyFile_WriteObject(PyObject *v, PyObject *f, int flags)
                        return -1;
                }
 #ifdef Py_USING_UNICODE
-                if ((flags & Py_PRINT_RAW) && 
+                if ((flags & Py_PRINT_RAW) &&
                    PyUnicode_Check(v) && enc != Py_None) {
                        char *cenc = PyString_AS_STRING(enc);
                        value = PyUnicode_AsEncodedString(v, cenc, "strict");