Skip to content

Commit e0b5b20

Browse files
authored
bpo-34384: Fix os.readlink() on Windows (GH-8740)
os.readlink() now accepts path-like and bytes objects on Windows. Previously, support for path-like and bytes objects was only implemented on Unix. This commit also merges Unix and Windows implementations of os.readlink() in one function and adds basic unit tests to increase test coverage of the function.
1 parent 7c4ab2a commit e0b5b20

File tree

4 files changed

+86
-60
lines changed

4 files changed

+86
-60
lines changed

Doc/library/os.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2014,8 +2014,10 @@ features:
20142014
The *dir_fd* argument.
20152015

20162016
.. versionchanged:: 3.6
2017-
Accepts a :term:`path-like object`.
2017+
Accepts a :term:`path-like object` on Unix.
20182018

2019+
.. versionchanged:: 3.8
2020+
Accepts a :term:`path-like object` and a bytes object on Windows.
20192021

20202022
.. function:: remove(path, *, dir_fd=None)
20212023

Lib/test/test_os.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,6 +2039,53 @@ def test_listdir_extended_path(self):
20392039
[os.fsencode(path) for path in self.created_paths])
20402040

20412041

2042+
@unittest.skipUnless(hasattr(os, 'readlink'), 'needs os.readlink()')
2043+
class ReadlinkTests(unittest.TestCase):
2044+
filelink = 'readlinktest'
2045+
filelink_target = os.path.abspath(__file__)
2046+
filelinkb = os.fsencode(filelink)
2047+
filelinkb_target = os.fsencode(filelink_target)
2048+
2049+
def setUp(self):
2050+
self.assertTrue(os.path.exists(self.filelink_target))
2051+
self.assertTrue(os.path.exists(self.filelinkb_target))
2052+
self.assertFalse(os.path.exists(self.filelink))
2053+
self.assertFalse(os.path.exists(self.filelinkb))
2054+
2055+
def test_not_symlink(self):
2056+
filelink_target = FakePath(self.filelink_target)
2057+
self.assertRaises(OSError, os.readlink, self.filelink_target)
2058+
self.assertRaises(OSError, os.readlink, filelink_target)
2059+
2060+
def test_missing_link(self):
2061+
self.assertRaises(FileNotFoundError, os.readlink, 'missing-link')
2062+
self.assertRaises(FileNotFoundError, os.readlink,
2063+
FakePath('missing-link'))
2064+
2065+
@support.skip_unless_symlink
2066+
def test_pathlike(self):
2067+
os.symlink(self.filelink_target, self.filelink)
2068+
self.addCleanup(support.unlink, self.filelink)
2069+
filelink = FakePath(self.filelink)
2070+
self.assertEqual(os.readlink(filelink), self.filelink_target)
2071+
2072+
@support.skip_unless_symlink
2073+
def test_pathlike_bytes(self):
2074+
os.symlink(self.filelinkb_target, self.filelinkb)
2075+
self.addCleanup(support.unlink, self.filelinkb)
2076+
path = os.readlink(FakePath(self.filelinkb))
2077+
self.assertEqual(path, self.filelinkb_target)
2078+
self.assertIsInstance(path, bytes)
2079+
2080+
@support.skip_unless_symlink
2081+
def test_bytes(self):
2082+
os.symlink(self.filelinkb_target, self.filelinkb)
2083+
self.addCleanup(support.unlink, self.filelinkb)
2084+
path = os.readlink(self.filelinkb)
2085+
self.assertEqual(path, self.filelinkb_target)
2086+
self.assertIsInstance(path, bytes)
2087+
2088+
20422089
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
20432090
@support.skip_unless_symlink
20442091
class Win32SymlinkTests(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`os.readlink` now accepts :term:`path-like <path-like object>` and
2+
:class:`bytes` objects on Windows.

Modules/posixmodule.c

Lines changed: 34 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7414,18 +7414,24 @@ If dir_fd is not None, it should be a file descriptor open to a directory,\n\
74147414
and path should be relative; path will then be relative to that directory.\n\
74157415
dir_fd may not be implemented on your platform.\n\
74167416
If it is unavailable, using it will raise a NotImplementedError.");
7417-
#endif
7418-
7419-
#ifdef HAVE_READLINK
74207417

7421-
/* AC 3.5: merge win32 and not together */
74227418
static PyObject *
74237419
posix_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
74247420
{
74257421
path_t path;
74267422
int dir_fd = DEFAULT_DIR_FD;
7423+
#if defined(HAVE_READLINK)
74277424
char buffer[MAXPATHLEN+1];
74287425
ssize_t length;
7426+
#elif defined(MS_WINDOWS)
7427+
DWORD n_bytes_returned;
7428+
DWORD io_result;
7429+
HANDLE reparse_point_handle;
7430+
7431+
char target_buffer[_Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
7432+
_Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer;
7433+
const wchar_t *print_name;
7434+
#endif
74297435
PyObject *return_value = NULL;
74307436
static char *keywords[] = {"path", "dir_fd", NULL};
74317437

@@ -7436,6 +7442,7 @@ posix_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
74367442
READLINKAT_DIR_FD_CONVERTER, &dir_fd))
74377443
return NULL;
74387444

7445+
#if defined(HAVE_READLINK)
74397446
Py_BEGIN_ALLOW_THREADS
74407447
#ifdef HAVE_READLINKAT
74417448
if (dir_fd != DEFAULT_DIR_FD)
@@ -7455,45 +7462,11 @@ posix_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
74557462
return_value = PyUnicode_DecodeFSDefaultAndSize(buffer, length);
74567463
else
74577464
return_value = PyBytes_FromStringAndSize(buffer, length);
7458-
exit:
7459-
path_cleanup(&path);
7460-
return return_value;
7461-
}
7462-
7463-
#endif /* HAVE_READLINK */
7464-
7465-
#if !defined(HAVE_READLINK) && defined(MS_WINDOWS)
7466-
7467-
static PyObject *
7468-
win_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
7469-
{
7470-
const wchar_t *path;
7471-
DWORD n_bytes_returned;
7472-
DWORD io_result;
7473-
PyObject *po, *result;
7474-
int dir_fd;
7475-
HANDLE reparse_point_handle;
7476-
7477-
char target_buffer[_Py_MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
7478-
_Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer;
7479-
const wchar_t *print_name;
7480-
7481-
static char *keywords[] = {"path", "dir_fd", NULL};
7482-
7483-
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "U|$O&:readlink", keywords,
7484-
&po,
7485-
dir_fd_unavailable, &dir_fd
7486-
))
7487-
return NULL;
7488-
7489-
path = _PyUnicode_AsUnicode(po);
7490-
if (path == NULL)
7491-
return NULL;
7492-
7465+
#elif defined(MS_WINDOWS)
74937466
/* First get a handle to the reparse point */
74947467
Py_BEGIN_ALLOW_THREADS
74957468
reparse_point_handle = CreateFileW(
7496-
path,
7469+
path.wide,
74977470
0,
74987471
0,
74997472
0,
@@ -7502,8 +7475,10 @@ win_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
75027475
0);
75037476
Py_END_ALLOW_THREADS
75047477

7505-
if (reparse_point_handle==INVALID_HANDLE_VALUE)
7506-
return win32_error_object("readlink", po);
7478+
if (reparse_point_handle == INVALID_HANDLE_VALUE) {
7479+
return_value = path_error(&path);
7480+
goto exit;
7481+
}
75077482

75087483
Py_BEGIN_ALLOW_THREADS
75097484
/* New call DeviceIoControl to read the reparse point */
@@ -7518,26 +7493,31 @@ win_readlink(PyObject *self, PyObject *args, PyObject *kwargs)
75187493
CloseHandle(reparse_point_handle);
75197494
Py_END_ALLOW_THREADS
75207495

7521-
if (io_result==0)
7522-
return win32_error_object("readlink", po);
7496+
if (io_result == 0) {
7497+
return_value = path_error(&path);
7498+
goto exit;
7499+
}
75237500

75247501
if (rdb->ReparseTag != IO_REPARSE_TAG_SYMLINK)
75257502
{
75267503
PyErr_SetString(PyExc_ValueError,
75277504
"not a symbolic link");
7528-
return NULL;
7505+
goto exit;
75297506
}
75307507
print_name = (wchar_t *)((char*)rdb->SymbolicLinkReparseBuffer.PathBuffer +
75317508
rdb->SymbolicLinkReparseBuffer.PrintNameOffset);
75327509

7533-
result = PyUnicode_FromWideChar(print_name,
7534-
rdb->SymbolicLinkReparseBuffer.PrintNameLength / sizeof(wchar_t));
7535-
return result;
7510+
return_value = PyUnicode_FromWideChar(print_name,
7511+
rdb->SymbolicLinkReparseBuffer.PrintNameLength / sizeof(wchar_t));
7512+
if (path.narrow) {
7513+
Py_SETREF(return_value, PyUnicode_EncodeFSDefault(return_value));
7514+
}
7515+
#endif
7516+
exit:
7517+
path_cleanup(&path);
7518+
return return_value;
75367519
}
7537-
7538-
#endif /* !defined(HAVE_READLINK) && defined(MS_WINDOWS) */
7539-
7540-
7520+
#endif /* defined(HAVE_READLINK) || defined(MS_WINDOWS) */
75417521

75427522
#ifdef HAVE_SYMLINK
75437523

@@ -12950,16 +12930,11 @@ static PyMethodDef posix_methods[] = {
1295012930
OS_GETPRIORITY_METHODDEF
1295112931
OS_SETPRIORITY_METHODDEF
1295212932
OS_POSIX_SPAWN_METHODDEF
12953-
#ifdef HAVE_READLINK
12933+
#if defined(HAVE_READLINK) || defined(MS_WINDOWS)
1295412934
{"readlink", (PyCFunction)posix_readlink,
1295512935
METH_VARARGS | METH_KEYWORDS,
1295612936
readlink__doc__},
12957-
#endif /* HAVE_READLINK */
12958-
#if !defined(HAVE_READLINK) && defined(MS_WINDOWS)
12959-
{"readlink", (PyCFunction)win_readlink,
12960-
METH_VARARGS | METH_KEYWORDS,
12961-
readlink__doc__},
12962-
#endif /* !defined(HAVE_READLINK) && defined(MS_WINDOWS) */
12937+
#endif /* defined(HAVE_READLINK) || defined(MS_WINDOWS) */
1296312938
OS_RENAME_METHODDEF
1296412939
OS_REPLACE_METHODDEF
1296512940
OS_RMDIR_METHODDEF

0 commit comments

Comments
 (0)