Skip to content

Commit f96963a

Browse files
committed
gh-157889 (os.readlink to support FDs referring to symlinks): Address feedback from MR.
* os.readlink() now returns a `str` (rather than `bytes`) when called with a file descriptor. * os.readlink now added to `os.supports_fd` on the platforms where this feature is supported. * Small refactor inside `os.readlink`: drop *_unavailable variables, generate error messages inline. Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS around each syscall site. * Docs: Android is now mentioned as supported platform for this feature in all relevant places.
1 parent 8196b59 commit f96963a

4 files changed

Lines changed: 58 additions & 49 deletions

File tree

‎Doc/library/os.rst‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2807,9 +2807,13 @@ features:
28072807
This function can also support :ref:`paths relative to directory descriptors
28082808
<dir_fd>`.
28092809

2810-
On MacOS and Linux, *path* can also be a file descriptor referring to a
2811-
symbolic link. In that case, *dir_fd* must be ``None``, and the return
2812-
value will be a ``bytes`` object.
2810+
On Linux, Android and MacOS, *path* can also be a file descriptor referring
2811+
to a symbolic link. In that case, *dir_fd* must be ``None``, and the return
2812+
value will be a string.
2813+
(On Linux and Android, such a file descriptor can be obtained through
2814+
:func:`os.open` with ``os.RDONLY | os.O_PATH | os.O_NOFOLLOW``.
2815+
On MacOS, this is possible by calling :func:`os.open` with
2816+
``os.O_RDONLY | os.O_SYMLINK``.)
28132817
On other operating systems, a ``NotImplementedError`` is raised if *path*
28142818
is an integer.
28152819

@@ -2837,7 +2841,7 @@ features:
28372841

28382842
.. versionchanged:: 3.16
28392843
Accepts file descriptors pointing to symbolic links as *path* on
2840-
Linux and MacOS.
2844+
Linux, Android and MacOS.
28412845

28422846
.. function:: remove(path, *, dir_fd=None)
28432847

‎Lib/os.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ def _add(str, fn):
161161
_add("HAVE_FSTATVFS", "statvfs")
162162
if _exists("statx"):
163163
_set.add(statx)
164+
_add("HAVE_FREADLINK", "readlink")
165+
if sys.platform in ["linux", "android"] and "O_PATH" in _globals:
166+
_add("HAVE_READLINKAT", "readlink")
164167
supports_fd = _set
165168

166169
_set = set()

‎Lib/test/test_os/test_posix.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1908,14 +1908,19 @@ def test_readlink_with_fd(self):
19081908
self.addCleanup(posix.unlink, fullname)
19091909
fd = self._open_symlink_as_fd(fullname)
19101910
self.addCleanup(os.close, fd)
1911-
self.assertEqual(os.readlink(fd), b"symlink")
1911+
self.assertEqual(os.readlink(fd), "symlink")
19121912

19131913
@unittest.skipUnless(_support_readlink_with_fd,
19141914
"feature not supported on this platform")
19151915
def test_readlink_with_fd_not_referring_to_symlink_throws(self):
19161916
with self.prepare_file() as (dir_fd, name, fullname):
19171917
fd = os.open(fullname, os.O_RDONLY)
19181918
self.addCleanup(os.close, fd)
1919+
# on Linux/Android, readlinkat("", fd, ...) fails with ENOENT, which
1920+
# Python translates to a FileNotFoundError, a subclass of OSError.
1921+
# On MacOS, freadlink(fd, ...) fails with EINVAL, which gets raised
1922+
# as a OSError.
1923+
# So catching OSError here covers both cases.
19191924
with self.assertRaises(OSError):
19201925
os.readlink(fd)
19211926

@@ -1942,6 +1947,11 @@ def test_readlink_with_fd_throws_not_implemented_error(self):
19421947
with self.assertRaises(NotImplementedError):
19431948
os.readlink(fd)
19441949

1950+
@unittest.skipUnless(hasattr(os, "supports_fd") and hasattr(os, "readlink"),
1951+
"feature not supported on this platform")
1952+
def test_readlink_is_in_supports_fd_on_supported_platforms(self):
1953+
self.assertEqual(os.readlink in os.supports_fd, self._support_readlink_with_fd)
1954+
19451955
@unittest.skipUnless(os.rename in os.supports_dir_fd, "test needs dir_fd support in os.rename()")
19461956
def test_rename_dir_fd(self):
19471957
with self.prepare_file() as (dir_fd, name, fullname), \

‎Modules/posixmodule.c‎

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11028,77 +11028,69 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
1102811028
#if defined(HAVE_READLINK)
1102911029
char buffer[MAXPATHLEN+1];
1103011030
ssize_t length;
11031-
#ifdef HAVE_READLINKAT
11032-
int readlinkat_unavailable = 0;
11033-
#endif
11034-
#ifdef _Py_HAVE_FREADLINK
11035-
int freadlink_unavailable = 0;
11036-
#endif
1103711031

1103811032
if (path_and_dir_fd_invalid("readlink", path, dir_fd)) {
1103911033
return NULL;
1104011034
}
1104111035

1104211036
if (path->is_fd) {
11043-
#if defined(__APPLE__) && defined(_Py_HAVE_FREADLINK)
11044-
/* nop, freadlink is called below */
11045-
#elif defined(__linux__) && defined(HAVE_READLINKAT)
11046-
// Linux: readlinkat(dir_fd, "", ...) reads the symbolic link
11047-
// pointed to by dir_fd
11048-
dir_fd = path->fd;
11049-
path->narrow = "";
11037+
#if defined(_Py_HAVE_FREADLINK)
11038+
if (HAVE_FREADLINK_RUNTIME) {
11039+
Py_BEGIN_ALLOW_THREADS
11040+
length = freadlink(path->fd, buffer, MAXPATHLEN);
11041+
Py_END_ALLOW_THREADS
11042+
} else {
11043+
PyErr_SetString(PyExc_NotImplementedError,
11044+
"readlink cannot read file descriptors on this platform, "
11045+
"freadlink() is unavailable");
11046+
return NULL;
11047+
}
11048+
#elif defined(HAVE_READLINKAT) && defined(O_PATH)
11049+
// linux/android:
11050+
// readlinkat(fd, "", ...) reads the link that fd refers to.
11051+
if (HAVE_READLINKAT_RUNTIME) {
11052+
Py_BEGIN_ALLOW_THREADS
11053+
length = readlinkat(path->fd, "", buffer, MAXPATHLEN);
11054+
Py_END_ALLOW_THREADS
11055+
} else {
11056+
// this should be unreachable:
11057+
// HAVE_READLINKAT_RUNTIME is always 1 on Linux/Android.
11058+
// Leaving it here as a safeguard.
11059+
PyErr_SetString(PyExc_NotImplementedError,
11060+
"readlink cannot read file descriptors on this platform, "
11061+
"readlinkat() is unavailable");
11062+
return NULL;
11063+
}
1105011064
#else
1105111065
PyErr_SetString(PyExc_NotImplementedError,
1105211066
"readlink cannot read file descriptors on this platform");
1105311067
return NULL;
1105411068
#endif
11055-
}
11056-
11057-
Py_BEGIN_ALLOW_THREADS
11058-
#ifdef _Py_HAVE_FREADLINK
11059-
if (path->is_fd) {
11060-
if (HAVE_FREADLINK_RUNTIME) {
11061-
length = freadlink(path->fd, buffer, MAXPATHLEN);
11062-
} else {
11063-
freadlink_unavailable = 1;
11064-
}
1106511069
} else
11066-
#endif
1106711070
#ifdef HAVE_READLINKAT
11068-
if (dir_fd != DEFAULT_DIR_FD || path->is_fd) {
11071+
if (dir_fd != DEFAULT_DIR_FD) {
1106911072
if (HAVE_READLINKAT_RUNTIME) {
11073+
Py_BEGIN_ALLOW_THREADS
1107011074
length = readlinkat(dir_fd, path->narrow, buffer, MAXPATHLEN);
11075+
Py_END_ALLOW_THREADS
1107111076
} else {
11072-
readlinkat_unavailable = 1;
11077+
argument_unavailable_error(NULL, "dir_fd");
11078+
return NULL;
1107311079
}
1107411080
} else
1107511081
#endif
11082+
{
11083+
Py_BEGIN_ALLOW_THREADS
1107611084
length = readlink(path->narrow, buffer, MAXPATHLEN);
11077-
Py_END_ALLOW_THREADS
11078-
11079-
11080-
#ifdef _Py_HAVE_FREADLINK
11081-
if (freadlink_unavailable) {
11082-
PyErr_Format(PyExc_NotImplementedError,
11083-
"readlink cannot read file descriptors on this platform, "
11084-
"freadlink() is unavailable");
11085-
return NULL;
11086-
}
11087-
#endif
11088-
11089-
#ifdef HAVE_READLINKAT
11090-
if (readlinkat_unavailable) {
11091-
argument_unavailable_error(NULL, "dir_fd");
11092-
return NULL;
11085+
Py_END_ALLOW_THREADS
1109311086
}
11094-
#endif
1109511087

1109611088
if (length < 0) {
1109711089
return path_error(path);
1109811090
}
1109911091
buffer[length] = '\0';
1110011092

11101-
if (PyUnicode_Check(path->object))
11093+
if (path->is_fd || PyUnicode_Check(path->object))
1110211094
return PyUnicode_DecodeFSDefaultAndSize(buffer, length);
1110311095
else
1110411096
return PyBytes_FromStringAndSize(buffer, length);

0 commit comments

Comments
 (0)