]> granicus.if.org Git - python/commitdiff
Fix #12084. os.stat on Windows wasn't working properly with relative symlinks.
authorBrian Curtin <brian@python.org>
Mon, 13 Jun 2011 20:16:04 +0000 (15:16 -0500)
committerBrian Curtin <brian@python.org>
Mon, 13 Jun 2011 20:16:04 +0000 (15:16 -0500)
Use of DeviceIoControl to obtain the symlink path via the reparse tag was
removed. The code now uses GetFinalPathNameByHandle in the case of a
symbolic link and works properly given the added test which creates a symbolic
link and calls os.stat on it from multiple locations.

Victor Stinner also noticed an issue with os.lstat following the os.stat
code path when being passed bytes. The posix_lstat function was adjusted to
properly hook up win32_lstat instead of the previous STAT macro (win32_stat).

Lib/test/support.py
Lib/test/test_os.py
Misc/NEWS
Modules/posixmodule.c

index 2aedf24846ee4986d4dedffcba039dd9bc9cb981..9c8f6d3dcc69096690928f50e0564130eae55716 100644 (file)
@@ -1490,7 +1490,7 @@ def can_symlink():
     try:
         os.symlink(TESTFN, TESTFN + "can_symlink")
         can = True
-    except (OSError, NotImplementedError):
+    except (OSError, NotImplementedError, AttributeError):
         can = False
     _can_symlink = can
     return can
index 56be375b78c32b800e6b2ddc7c776c7c6b863441..f58a5c1647a3089192a5db793b27b8db0d6ba66b 100644 (file)
@@ -1243,6 +1243,51 @@ class Win32SymlinkTests(unittest.TestCase):
         self.assertEqual(os.stat(link), os.stat(target))
         self.assertNotEqual(os.lstat(link), os.stat(link))
 
+        bytes_link = os.fsencode(link)
+        self.assertEqual(os.stat(bytes_link), os.stat(target))
+        self.assertNotEqual(os.lstat(bytes_link), os.stat(bytes_link))
+
+    def test_12084(self):
+        level1 = os.path.abspath(support.TESTFN)
+        level2 = os.path.join(level1, "level2")
+        level3 = os.path.join(level2, "level3")
+        try:
+            os.mkdir(level1)
+            os.mkdir(level2)
+            os.mkdir(level3)
+
+            file1 = os.path.abspath(os.path.join(level1, "file1"))
+
+            with open(file1, "w") as f:
+                f.write("file1")
+
+            orig_dir = os.getcwd()
+            try:
+                os.chdir(level2)
+                link = os.path.join(level2, "link")
+                os.symlink(os.path.relpath(file1), "link")
+                self.assertIn("link", os.listdir(os.getcwd()))
+
+                # Check os.stat calls from the same dir as the link
+                self.assertEqual(os.stat(file1), os.stat("link"))
+
+                # Check os.stat calls from a dir below the link
+                os.chdir(level1)
+                self.assertEqual(os.stat(file1),
+                                 os.stat(os.path.relpath(link)))
+
+                # Check os.stat calls from a dir above the link
+                os.chdir(level3)
+                self.assertEqual(os.stat(file1),
+                                 os.stat(os.path.relpath(link)))
+            finally:
+                os.chdir(orig_dir)
+        except OSError as err:
+            self.fail(err)
+        finally:
+            os.remove(file1)
+            shutil.rmtree(level1)
+
 
 class FSEncodingTests(unittest.TestCase):
     def test_nop(self):
index 16a0f290fb9e6dc84e3bbcb95d0f70ef2ed38b39..06c08a9c20afd857d2150161a70cb6a59e93507a 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,9 @@ What's New in Python 3.2.1 release candidate 2?
 Core and Builtins
 -----------------
 
+- Issue #12084: os.stat on Windows now works properly with relative symbolic
+  links when called from any directory.
+
 - Issue #1195: my_fgets() now always clears errors before calling fgets(). Fix
   the following case: sys.stdin.read() stopped with CTRL+d (end of file),
   raw_input() interrupted by CTRL+c.
index 89d3f2f2d0b783d32eb2a9307692f9d6ce5bfe29..674eeb2920e25e9e7aabf14bdfb40e99c3cf3695 100644 (file)
@@ -480,14 +480,11 @@ typedef struct _REPARSE_DATA_BUFFER {
 #define MAXIMUM_REPARSE_DATA_BUFFER_SIZE  ( 16 * 1024 )
 
 static int
-win32_read_link(HANDLE reparse_point_handle, ULONG *reparse_tag, wchar_t **target_path)
+win32_get_reparse_tag(HANDLE reparse_point_handle, ULONG *reparse_tag)
 {
     char target_buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
     REPARSE_DATA_BUFFER *rdb = (REPARSE_DATA_BUFFER *)target_buffer;
     DWORD n_bytes_returned;
-    const wchar_t *ptr;
-    wchar_t *buf;
-    size_t len;
 
     if (0 == DeviceIoControl(
         reparse_point_handle,
@@ -496,41 +493,12 @@ win32_read_link(HANDLE reparse_point_handle, ULONG *reparse_tag, wchar_t **targe
         target_buffer, sizeof(target_buffer),
         &n_bytes_returned,
         NULL)) /* we're not using OVERLAPPED_IO */
-        return -1;
+        return FALSE;
 
     if (reparse_tag)
         *reparse_tag = rdb->ReparseTag;
 
-    if (target_path) {
-        switch (rdb->ReparseTag) {
-        case IO_REPARSE_TAG_SYMLINK:
-            /* XXX: Maybe should use SubstituteName? */
-            ptr = rdb->SymbolicLinkReparseBuffer.PathBuffer +
-                  rdb->SymbolicLinkReparseBuffer.PrintNameOffset/sizeof(WCHAR);
-            len = rdb->SymbolicLinkReparseBuffer.PrintNameLength/sizeof(WCHAR);
-            break;
-        case IO_REPARSE_TAG_MOUNT_POINT:
-            ptr = rdb->MountPointReparseBuffer.PathBuffer +
-                  rdb->MountPointReparseBuffer.SubstituteNameOffset/sizeof(WCHAR);
-            len = rdb->MountPointReparseBuffer.SubstituteNameLength/sizeof(WCHAR);
-            break;
-        default:
-            SetLastError(ERROR_REPARSE_TAG_MISMATCH); /* XXX: Proper error code? */
-            return -1;
-        }
-        buf = (wchar_t *)malloc(sizeof(wchar_t)*(len+1));
-        if (!buf) {
-            SetLastError(ERROR_OUTOFMEMORY);
-            return -1;
-        }
-        wcsncpy(buf, ptr, len);
-        buf[len] = L'\0';
-        if (wcsncmp(buf, L"\\??\\", 4) == 0)
-            buf[1] = L'\\';
-        *target_path = buf;
-    }
-
-    return 0;
+    return TRUE;
 }
 #endif /* MS_WINDOWS */
 
@@ -1096,36 +1064,97 @@ attributes_from_dir_w(LPCWSTR pszFile, BY_HANDLE_FILE_INFORMATION *info, ULONG *
     return TRUE;
 }
 
-#ifndef SYMLOOP_MAX
-#define SYMLOOP_MAX ( 88 )
-#endif
-
+/* Grab GetFinalPathNameByHandle dynamically from kernel32 */
+static int has_GetFinalPathNameByHandle = 0;
+static DWORD (CALLBACK *Py_GetFinalPathNameByHandleA)(HANDLE, LPSTR, DWORD,
+                                                      DWORD);
+static DWORD (CALLBACK *Py_GetFinalPathNameByHandleW)(HANDLE, LPWSTR, DWORD,
+                                                      DWORD);
 static int
-win32_xstat_impl_w(const wchar_t *path, struct win32_stat *result, BOOL traverse, int depth);
+check_GetFinalPathNameByHandle()
+{
+    HINSTANCE hKernel32;
+    /* only recheck */
+    if (!has_GetFinalPathNameByHandle)
+    {
+        hKernel32 = GetModuleHandle("KERNEL32");
+        *(FARPROC*)&Py_GetFinalPathNameByHandleA = GetProcAddress(hKernel32,
+                                                "GetFinalPathNameByHandleA");
+        *(FARPROC*)&Py_GetFinalPathNameByHandleW = GetProcAddress(hKernel32,
+                                                "GetFinalPathNameByHandleW");
+        has_GetFinalPathNameByHandle = Py_GetFinalPathNameByHandleA &&
+                                       Py_GetFinalPathNameByHandleW;
+    }
+    return has_GetFinalPathNameByHandle;
+}
+
+static BOOL
+get_target_path(HANDLE hdl, wchar_t **target_path)
+{
+    int buf_size, result_length;
+    wchar_t *buf;
+
+    /* We have a good handle to the target, use it to determine
+       the target path name (then we'll call lstat on it). */
+    buf_size = Py_GetFinalPathNameByHandleW(hdl, 0, 0,
+                                            VOLUME_NAME_DOS);
+    if(!buf_size)
+        return FALSE;
+
+    buf = (wchar_t *)malloc((buf_size+1)*sizeof(wchar_t));
+    result_length = Py_GetFinalPathNameByHandleW(hdl,
+                       buf, buf_size, VOLUME_NAME_DOS);
+
+    if(!result_length) {
+        free(buf);
+        return FALSE;
+    }
+
+    if(!CloseHandle(hdl)) {
+        free(buf);
+        return FALSE;
+    }
+
+    buf[result_length] = 0;
+
+    *target_path = buf;
+    return TRUE;
+}
 
 static int
-win32_xstat_impl(const char *path, struct win32_stat *result, BOOL traverse, int depth)
+win32_xstat_impl_w(const wchar_t *path, struct win32_stat *result,
+                   BOOL traverse);
+static int
+win32_xstat_impl(const char *path, struct win32_stat *result,
+                 BOOL traverse)
 {
-    int code;
-    HANDLE hFile;
+    int code; 
+    HANDLE hFile, hFile2;
     BY_HANDLE_FILE_INFORMATION info;
     ULONG reparse_tag = 0;
-    wchar_t *target_path;
+       wchar_t *target_path;
     const char *dot;
 
-    if (depth > SYMLOOP_MAX) {
-        SetLastError(ERROR_CANT_RESOLVE_FILENAME); /* XXX: ELOOP? */
+    if(!check_GetFinalPathNameByHandle()) {
+        /* If the OS doesn't have GetFinalPathNameByHandle, return a
+           NotImplementedError. */
+        PyErr_SetString(PyExc_NotImplementedError,
+            "GetFinalPathNameByHandle not available on this platform");
         return -1;
     }
 
     hFile = CreateFileA(
         path,
-        0, /* desired access */
+        FILE_READ_ATTRIBUTES, /* desired access */
         0, /* share mode */
         NULL, /* security attributes */
         OPEN_EXISTING,
         /* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */
-        FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT,
+        /* FILE_FLAG_OPEN_REPARSE_POINT does not follow the symlink.
+           Because of this, calls like GetFinalPathNameByHandle will return
+           the symlink path agin and not the actual final path. */
+        FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS|
+            FILE_FLAG_OPEN_REPARSE_POINT,
         NULL);
 
     if (hFile == INVALID_HANDLE_VALUE) {
@@ -1149,15 +1178,32 @@ win32_xstat_impl(const char *path, struct win32_stat *result, BOOL traverse, int
     } else {
         if (!GetFileInformationByHandle(hFile, &info)) {
             CloseHandle(hFile);
-            return -1;;
+            return -1;
         }
         if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
-            code = win32_read_link(hFile, &reparse_tag, traverse ? &target_path : NULL);
-            CloseHandle(hFile);
-            if (code < 0)
-                return code;
+            if (!win32_get_reparse_tag(hFile, &reparse_tag))
+                return -1;
+
+            /* Close the outer open file handle now that we're about to
+               reopen it with different flags. */
+            if (!CloseHandle(hFile))
+                return -1;
+
             if (traverse) {
-                code = win32_xstat_impl_w(target_path, result, traverse, depth + 1);
+                /* In order to call GetFinalPathNameByHandle we need to open
+                   the file without the reparse handling flag set. */
+                hFile2 = CreateFileA(
+                           path, FILE_READ_ATTRIBUTES, FILE_SHARE_READ,
+                           NULL, OPEN_EXISTING,
+                           FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS,
+                           NULL);
+                if (hFile2 == INVALID_HANDLE_VALUE)
+                    return -1;
+
+                if (!get_target_path(hFile2, &target_path))
+                    return -1;
+
+                code = win32_xstat_impl_w(target_path, result, FALSE);
                 free(target_path);
                 return code;
             }
@@ -1177,28 +1223,36 @@ win32_xstat_impl(const char *path, struct win32_stat *result, BOOL traverse, int
 }
 
 static int
-win32_xstat_impl_w(const wchar_t *path, struct win32_stat *result, BOOL traverse, int depth)
+win32_xstat_impl_w(const wchar_t *path, struct win32_stat *result,
+                   BOOL traverse)
 {
     int code;
-    HANDLE hFile;
+    HANDLE hFile, hFile2;
     BY_HANDLE_FILE_INFORMATION info;
     ULONG reparse_tag = 0;
        wchar_t *target_path;
     const wchar_t *dot;
 
-    if (depth > SYMLOOP_MAX) {
-        SetLastError(ERROR_CANT_RESOLVE_FILENAME); /* XXX: ELOOP? */
+    if(!check_GetFinalPathNameByHandle()) {
+        /* If the OS doesn't have GetFinalPathNameByHandle, return a
+           NotImplementedError. */
+        PyErr_SetString(PyExc_NotImplementedError,
+            "GetFinalPathNameByHandle not available on this platform");
         return -1;
     }
 
     hFile = CreateFileW(
         path,
-        0, /* desired access */
+        FILE_READ_ATTRIBUTES, /* desired access */
         0, /* share mode */
         NULL, /* security attributes */
         OPEN_EXISTING,
         /* FILE_FLAG_BACKUP_SEMANTICS is required to open a directory */
-        FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT,
+        /* FILE_FLAG_OPEN_REPARSE_POINT does not follow the symlink.
+           Because of this, calls like GetFinalPathNameByHandle will return
+           the symlink path agin and not the actual final path. */
+        FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS| 
+            FILE_FLAG_OPEN_REPARSE_POINT,
         NULL);
 
     if (hFile == INVALID_HANDLE_VALUE) {
@@ -1222,15 +1276,32 @@ win32_xstat_impl_w(const wchar_t *path, struct win32_stat *result, BOOL traverse
     } else {
         if (!GetFileInformationByHandle(hFile, &info)) {
             CloseHandle(hFile);
-            return -1;;
+            return -1;
         }
         if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
-            code = win32_read_link(hFile, &reparse_tag, traverse ? &target_path : NULL);
-            CloseHandle(hFile);
-            if (code < 0)
-                return code;
+            if (!win32_get_reparse_tag(hFile, &reparse_tag))
+                return -1;
+
+            /* Close the outer open file handle now that we're about to
+               reopen it with different flags. */
+            if (!CloseHandle(hFile))
+                return -1;
+
             if (traverse) {
-                code = win32_xstat_impl_w(target_path, result, traverse, depth + 1);
+                /* In order to call GetFinalPathNameByHandle we need to open
+                   the file without the reparse handling flag set. */
+                hFile2 = CreateFileW(
+                           path, FILE_READ_ATTRIBUTES, FILE_SHARE_READ,
+                           NULL, OPEN_EXISTING,
+                           FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS,
+                           NULL);
+                if (hFile2 == INVALID_HANDLE_VALUE)
+                    return -1;
+
+                if (!get_target_path(hFile2, &target_path))
+                    return -1;
+
+                code = win32_xstat_impl_w(target_path, result, FALSE);
                 free(target_path);
                 return code;
             }
@@ -1254,7 +1325,7 @@ win32_xstat(const char *path, struct win32_stat *result, BOOL traverse)
 {
     /* Protocol violation: we explicitly clear errno, instead of
        setting it to a POSIX error. Callers should use GetLastError. */
-    int code = win32_xstat_impl(path, result, traverse, 0);
+    int code = win32_xstat_impl(path, result, traverse);
     errno = 0;
     return code;
 }
@@ -1264,13 +1335,11 @@ win32_xstat_w(const wchar_t *path, struct win32_stat *result, BOOL traverse)
 {
     /* Protocol violation: we explicitly clear errno, instead of
        setting it to a POSIX error. Callers should use GetLastError. */
-    int code = win32_xstat_impl_w(path, result, traverse, 0);
+    int code = win32_xstat_impl_w(path, result, traverse);
     errno = 0;
     return code;
 }
-
-/* About the following functions: win32_lstat, win32_lstat_w, win32_stat,
-   win32_stat_w
+/* About the following functions: win32_lstat_w, win32_stat, win32_stat_w
 
    In Posix, stat automatically traverses symlinks and returns the stat
    structure for the target.  In Windows, the equivalent GetFileAttributes by
@@ -1283,7 +1352,7 @@ win32_xstat_w(const wchar_t *path, struct win32_stat *result, BOOL traverse)
 
    The _w represent Unicode equivalents of the aforementioned ANSI functions. */
 
-static int 
+static int
 win32_lstat(const char* path, struct win32_stat *result)
 {
     return win32_xstat(path, result, FALSE);
@@ -2707,29 +2776,7 @@ posix__getfullpathname(PyObject *self, PyObject *args)
     return PyBytes_FromString(outbuf);
 } /* end of posix__getfullpathname */
 
-/* Grab GetFinalPathNameByHandle dynamically from kernel32 */
-static int has_GetFinalPathNameByHandle = 0;
-static DWORD (CALLBACK *Py_GetFinalPathNameByHandleA)(HANDLE, LPSTR, DWORD,
-                                                      DWORD);
-static DWORD (CALLBACK *Py_GetFinalPathNameByHandleW)(HANDLE, LPWSTR, DWORD,
-                                                      DWORD);
-static int
-check_GetFinalPathNameByHandle()
-{
-    HINSTANCE hKernel32;
-    /* only recheck */
-    if (!has_GetFinalPathNameByHandle)
-    {
-        hKernel32 = GetModuleHandle("KERNEL32");
-        *(FARPROC*)&Py_GetFinalPathNameByHandleA = GetProcAddress(hKernel32,
-                                                "GetFinalPathNameByHandleA");
-        *(FARPROC*)&Py_GetFinalPathNameByHandleW = GetProcAddress(hKernel32,
-                                                "GetFinalPathNameByHandleW");
-        has_GetFinalPathNameByHandle = Py_GetFinalPathNameByHandleA &&
-                                       Py_GetFinalPathNameByHandleW;
-    }
-    return has_GetFinalPathNameByHandle;
-}
+
 
 /* A helper function for samepath on windows */
 static PyObject *
@@ -5068,7 +5115,7 @@ posix_lstat(PyObject *self, PyObject *args)
     return posix_do_stat(self, args, "O&:lstat", lstat, NULL, NULL);
 #else /* !HAVE_LSTAT */
 #ifdef MS_WINDOWS
-    return posix_do_stat(self, args, "O&:lstat", STAT, "U:lstat",
+    return posix_do_stat(self, args, "O&:lstat", win32_lstat, "U:lstat",
                          win32_lstat_w);
 #else
     return posix_do_stat(self, args, "O&:lstat", STAT, NULL, NULL);