Fix #12084. os.stat on Windows wasn't working properly with relative symlinks.
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).
This commit is contained in:
parent
5b52f95797
commit
d25aef55c8
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
static int
|
||||
win32_xstat_impl(const char *path, struct win32_stat *result, BOOL traverse, int depth)
|
||||
check_GetFinalPathNameByHandle()
|
||||
{
|
||||
int code;
|
||||
HANDLE hFile;
|
||||
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_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, 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);
|
||||
|
|
Loading…
Reference in New Issue