mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-157899: Add support for fds pointing to symlinks in os.readlink(). (GH-157915)
This commit is contained in:
10 files changed
+227
-24
No files matched your search
+18
-5
@@ -2799,14 +2799,23 @@ features:
|
||||
may be converted to an absolute pathname using
|
||||
``os.path.join(os.path.dirname(path), result)``.
|
||||
|
||||
If the *path* is a string object (directly or indirectly through a
|
||||
:class:`PathLike` interface), the result will also be a string object,
|
||||
and the call may raise a UnicodeDecodeError. If the *path* is a bytes
|
||||
object (direct or indirectly), the result will be a bytes object.
|
||||
|
||||
This function can also support :ref:`paths relative to directory descriptors
|
||||
<dir_fd>`.
|
||||
|
||||
On Linux, Android, macOS and iOS, *path* can also be a file descriptor referring
|
||||
to a symbolic link. In that case, *dir_fd* must be ``None``.
|
||||
(On Linux and Android, such a file descriptor must be obtained through
|
||||
:func:`os.open` with ``os.O_RDONLY | os.O_PATH | os.O_NOFOLLOW``.
|
||||
On macOS and iOS, :func:`os.open` must be called with
|
||||
``os.O_RDONLY | os.O_SYMLINK``.)
|
||||
On other operating systems, a ``NotImplementedError`` is raised if *path*
|
||||
is an integer.
|
||||
|
||||
If the *path* is a string object (directly or indirectly through a
|
||||
:class:`PathLike` interface) or a file descriptor, the result will be a
|
||||
string object, and the call may raise a UnicodeDecodeError. If the *path*
|
||||
is a bytes object (direct or indirectly), the result will be a bytes object.
|
||||
|
||||
When trying to resolve a path that may contain links, use
|
||||
:func:`~os.path.realpath` to properly handle recursion and platform
|
||||
differences.
|
||||
@@ -2829,6 +2838,10 @@ features:
|
||||
substitution path (which typically includes ``\\?\`` prefix) rather
|
||||
than the optional "print name" field that was previously returned.
|
||||
|
||||
.. versionchanged:: next
|
||||
Accepts file descriptors pointing to symbolic links as *path* on
|
||||
Linux, Android, macOS and iOS.
|
||||
|
||||
.. function:: remove(path, *, dir_fd=None)
|
||||
|
||||
Remove (delete) the file *path*. If *path* is a directory, an
|
||||
|
||||
@@ -531,6 +531,10 @@ os
|
||||
now raises :exc:`PermissionError` instead of the functions being unavailable.
|
||||
(Contributed by Md Arif in :gh:`152936`.)
|
||||
|
||||
* :func:`os.readlink` now accepts a file descriptor referring to a symlink on
|
||||
Linux, Android, macOS and iOS.
|
||||
(Contributed by OOTS in :gh:`157899`.)
|
||||
|
||||
|
||||
pydoc
|
||||
-----
|
||||
|
||||
@@ -161,6 +161,9 @@ if _exists("_have_functions"):
|
||||
_add("HAVE_FSTATVFS", "statvfs")
|
||||
if _exists("statx"):
|
||||
_set.add(statx)
|
||||
_add("HAVE_FREADLINK", "readlink")
|
||||
if sys.platform in ["linux", "android"] and "O_PATH" in _globals:
|
||||
_add("HAVE_READLINKAT", "readlink")
|
||||
supports_fd = _set
|
||||
|
||||
_set = set()
|
||||
|
||||
@@ -1881,6 +1881,74 @@ class TestPosixDirFd(unittest.TestCase):
|
||||
self.addCleanup(posix.unlink, fullname)
|
||||
self.assertEqual(posix.readlink(name, dir_fd=dir_fd), 'symlink')
|
||||
|
||||
|
||||
_support_readlink_with_fd = (
|
||||
hasattr(os, 'readlink')
|
||||
and os.readlink in os.supports_fd
|
||||
)
|
||||
|
||||
def _open_symlink_as_fd(self, path):
|
||||
open_flags = os.O_RDONLY
|
||||
if hasattr(os, "O_SYMLINK"): # macOS/iOS
|
||||
open_flags |= os.O_SYMLINK
|
||||
elif hasattr(os, "O_NOFOLLOW") and hasattr(os, "O_PATH"): # Linux/Android
|
||||
open_flags |= os.O_NOFOLLOW | os.O_PATH
|
||||
else:
|
||||
self.fail("lacking open flags for this test")
|
||||
return os.open(path, open_flags)
|
||||
|
||||
@unittest.skipUnless(_support_readlink_with_fd,
|
||||
"feature not supported on this platform")
|
||||
def test_readlink_with_fd(self):
|
||||
with self.prepare() as (dir_fd, name, fullname):
|
||||
os.symlink("symlink", fullname)
|
||||
self.addCleanup(posix.unlink, fullname)
|
||||
fd = self._open_symlink_as_fd(fullname)
|
||||
self.addCleanup(os.close, fd)
|
||||
self.assertEqual(os.readlink(fd), "symlink")
|
||||
|
||||
@unittest.skipUnless(_support_readlink_with_fd,
|
||||
"feature not supported on this platform")
|
||||
def test_readlink_with_fd_not_referring_to_symlink_throws(self):
|
||||
with self.prepare_file() as (dir_fd, name, fullname):
|
||||
fd = os.open(fullname, os.O_RDONLY)
|
||||
self.addCleanup(os.close, fd)
|
||||
# on Linux/Android, readlinkat("", fd, ...) fails with ENOENT, which
|
||||
# Python translates to a FileNotFoundError, a subclass of OSError.
|
||||
# On macOS, freadlink(fd, ...) fails with EINVAL, which gets raised
|
||||
# as a OSError.
|
||||
# So catching OSError here covers both cases.
|
||||
with self.assertRaises(OSError):
|
||||
os.readlink(fd)
|
||||
|
||||
@unittest.skipUnless(_support_readlink_with_fd,
|
||||
"feature not supported on this platform")
|
||||
def test_readlink_with_fd_throws_if_both_fd_and_dir_fd_are_given(self):
|
||||
with self.prepare() as (dir_fd, name, fullname):
|
||||
os.symlink("symlink", fullname)
|
||||
self.addCleanup(posix.unlink, fullname)
|
||||
fd = self._open_symlink_as_fd(fullname)
|
||||
self.addCleanup(os.close, fd)
|
||||
with self.assertRaises(ValueError):
|
||||
os.readlink(fd, dir_fd=dir_fd)
|
||||
|
||||
@unittest.skipIf(_support_readlink_with_fd,
|
||||
"feature is supported on this platform")
|
||||
def test_readlink_with_fd_throws_not_implemented_error(self):
|
||||
# on unsupported platforms, we may not even be able to get a
|
||||
# file descriptor for a symlink, so use a fd for an ordinary file
|
||||
os_helper.create_empty_file(os_helper.TESTFN)
|
||||
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
|
||||
fd = os.open(os_helper.TESTFN, os.O_RDONLY)
|
||||
self.addCleanup(os.close, fd)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
os.readlink(fd)
|
||||
|
||||
@unittest.skipUnless(sys.platform in ["linux", "android", "darwin", "ios"],
|
||||
"feature not supported on this platform")
|
||||
def test_readlink_is_marked_as_supports_fd(self):
|
||||
self.assertIn(os.readlink, os.supports_fd)
|
||||
|
||||
@unittest.skipUnless(os.rename in os.supports_dir_fd, "test needs dir_fd support in os.rename()")
|
||||
def test_rename_dir_fd(self):
|
||||
with self.prepare_file() as (dir_fd, name, fullname), \
|
||||
@@ -2620,6 +2688,17 @@ class TestPosixWeaklinking(unittest.TestCase):
|
||||
with self.assertRaisesRegex(NotImplementedError, "dir_fd unavailable"):
|
||||
os.readlink("path", dir_fd=0)
|
||||
|
||||
def test_freadlink(self):
|
||||
self._verify_available("HAVE_FREADLINK")
|
||||
if self.mac_ver >= (13, 0):
|
||||
self.assertIn("HAVE_FREADLINK", posix._have_functions)
|
||||
|
||||
else:
|
||||
self.assertNotIn("HAVE_FREADLINK", posix._have_functions)
|
||||
errmsg = "readlink cannot read file descriptors on this platform"
|
||||
with self.assertRaisesRegex(NotImplementedError, errmsg):
|
||||
os.readlink(0)
|
||||
|
||||
def test_symlink(self):
|
||||
self._verify_available("HAVE_SYMLINKAT")
|
||||
if self.mac_ver >= (10, 10):
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
:func:`os.readlink` now accepts a file descriptor referring to a
|
||||
symlink on Linux, Android, macOS and iOS.
|
||||
Generated
+6
-3
@@ -6647,7 +6647,10 @@ PyDoc_STRVAR(os_readlink__doc__,
|
||||
"that directory.\n"
|
||||
"\n"
|
||||
"dir_fd may not be implemented on your platform. If it is unavailable,\n"
|
||||
"using it will raise a NotImplementedError.");
|
||||
"using it will raise a NotImplementedError.\n"
|
||||
"\n"
|
||||
"On Linux, Android, macOS and iOS, path may be a file descriptor\n"
|
||||
"referring to a symlink. If it is, dir_fd must be None.");
|
||||
|
||||
#define OS_READLINK_METHODDEF \
|
||||
{"readlink", _PyCFunction_CAST(os_readlink), METH_FASTCALL|METH_KEYWORDS, os_readlink__doc__},
|
||||
@@ -6688,7 +6691,7 @@ os_readlink(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject
|
||||
#undef KWTUPLE
|
||||
PyObject *argsbuf[2];
|
||||
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
|
||||
path_t path = PATH_T_INITIALIZE_P("readlink", "path", 0, 0, 0, 0);
|
||||
path_t path = PATH_T_INITIALIZE_P("readlink", "path", 0, 0, 0, 1);
|
||||
int dir_fd = DEFAULT_DIR_FD;
|
||||
|
||||
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
|
||||
@@ -13760,4 +13763,4 @@ exit:
|
||||
#ifndef OS__EMSCRIPTEN_LOG_METHODDEF
|
||||
#define OS__EMSCRIPTEN_LOG_METHODDEF
|
||||
#endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */
|
||||
/*[clinic end generated code: output=f1113bf7c270d271 input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=8bf88a95035a0188 input=a9049054013a1b77]*/
|
||||
+69
-16
@@ -505,6 +505,7 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
|
||||
# define HAVE_UNLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
|
||||
# define HAVE_OPENAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
|
||||
# define HAVE_READLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
|
||||
# define HAVE_FREADLINK_RUNTIME __builtin_available(macOS 13.0, iOS 16.0, *)
|
||||
# define HAVE_SYMLINKAT_RUNTIME __builtin_available(macOS 10.10, iOS 8.0, *)
|
||||
# define HAVE_FUTIMENS_RUNTIME __builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
|
||||
# define HAVE_UTIMENSAT_RUNTIME __builtin_available(macOS 10.13, iOS 11.0, tvOS 11.0, watchOS 4.0, *)
|
||||
@@ -571,6 +572,10 @@ static const unsigned int _Py_STATX_KNOWN = (STATX_BASIC_STATS | STATX_BTIME
|
||||
# define HAVE_READLINKAT_RUNTIME (readlinkat != NULL)
|
||||
# endif
|
||||
|
||||
# ifdef _Py_HAVE_FREADLINK
|
||||
# define HAVE_FREADLINK_RUNTIME (freadlink != NULL)
|
||||
# endif
|
||||
|
||||
# ifdef HAVE_SYMLINKAT
|
||||
# define HAVE_SYMLINKAT_RUNTIME (symlinkat != NULL)
|
||||
# endif
|
||||
@@ -10979,11 +10984,19 @@ os_unshare_impl(PyObject *module, int flags)
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#if defined(HAVE_READLINK) || defined(MS_WINDOWS)
|
||||
|
||||
#if (defined(__linux__) || defined(__ANDROID__)) && defined(O_PATH)
|
||||
// readlinkat(fd, "", ...) reads the symlink that fd refers to.
|
||||
// supported since Linux 2.6.39 (same version that O_PATH was introduced).
|
||||
#define _Py_READLINKAT_SUPPORTS_EMPTY_PATH
|
||||
#endif
|
||||
|
||||
/*[clinic input]
|
||||
os.readlink
|
||||
|
||||
path: path_t
|
||||
path: path_t(allow_fd=True)
|
||||
*
|
||||
dir_fd: dir_fd(requires='readlinkat') = None
|
||||
|
||||
@@ -10995,48 +11008,74 @@ that directory.
|
||||
|
||||
dir_fd may not be implemented on your platform. If it is unavailable,
|
||||
using it will raise a NotImplementedError.
|
||||
|
||||
On Linux, Android, macOS and iOS, path may be a file descriptor
|
||||
referring to a symlink. If it is, dir_fd must be None.
|
||||
[clinic start generated code]*/
|
||||
|
||||
static PyObject *
|
||||
os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
|
||||
/*[clinic end generated code: output=d21b732a2e814030 input=03d10130870dbca8]*/
|
||||
/*[clinic end generated code: output=d21b732a2e814030 input=d90bddfeee86f673]*/
|
||||
{
|
||||
#if defined(HAVE_READLINK)
|
||||
char buffer[MAXPATHLEN+1];
|
||||
ssize_t length;
|
||||
#ifdef HAVE_READLINKAT
|
||||
int readlinkat_unavailable = 0;
|
||||
#endif
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
if (path_and_dir_fd_invalid("readlink", path, dir_fd)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (path->is_fd) {
|
||||
#if defined(_Py_HAVE_FREADLINK)
|
||||
if (HAVE_FREADLINK_RUNTIME) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
length = freadlink(path->fd, buffer, MAXPATHLEN);
|
||||
Py_END_ALLOW_THREADS
|
||||
} else
|
||||
#elif defined(HAVE_READLINKAT) && defined(_Py_READLINKAT_SUPPORTS_EMPTY_PATH)
|
||||
// linux/android:
|
||||
// readlinkat(fd, "", ...) reads the link that fd refers to.
|
||||
if (HAVE_READLINKAT_RUNTIME) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
length = readlinkat(path->fd, "", buffer, MAXPATHLEN);
|
||||
Py_END_ALLOW_THREADS
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
PyErr_SetString(PyExc_NotImplementedError,
|
||||
"readlink cannot read file descriptors on this platform");
|
||||
return NULL;
|
||||
}
|
||||
} else
|
||||
#ifdef HAVE_READLINKAT
|
||||
if (dir_fd != DEFAULT_DIR_FD) {
|
||||
if (HAVE_READLINKAT_RUNTIME) {
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
length = readlinkat(dir_fd, path->narrow, buffer, MAXPATHLEN);
|
||||
Py_END_ALLOW_THREADS
|
||||
} else {
|
||||
readlinkat_unavailable = 1;
|
||||
argument_unavailable_error(NULL, "dir_fd");
|
||||
return NULL;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
length = readlink(path->narrow, buffer, MAXPATHLEN);
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
#ifdef HAVE_READLINKAT
|
||||
if (readlinkat_unavailable) {
|
||||
argument_unavailable_error(NULL, "dir_fd");
|
||||
return NULL;
|
||||
Py_END_ALLOW_THREADS
|
||||
}
|
||||
#endif
|
||||
|
||||
if (length < 0) {
|
||||
return path_error(path);
|
||||
}
|
||||
buffer[length] = '\0';
|
||||
|
||||
if (PyUnicode_Check(path->object))
|
||||
if (path->is_fd || PyUnicode_Check(path->object)) {
|
||||
return PyUnicode_DecodeFSDefaultAndSize(buffer, length);
|
||||
else
|
||||
}
|
||||
else {
|
||||
return PyBytes_FromStringAndSize(buffer, length);
|
||||
}
|
||||
#elif defined(MS_WINDOWS)
|
||||
DWORD n_bytes_returned;
|
||||
DWORD io_result = 0;
|
||||
@@ -11045,6 +11084,12 @@ os_readlink_impl(PyObject *module, path_t *path, int dir_fd)
|
||||
_Py_REPARSE_DATA_BUFFER *rdb = (_Py_REPARSE_DATA_BUFFER *)target_buffer;
|
||||
PyObject *result = NULL;
|
||||
|
||||
if (path->is_fd) {
|
||||
PyErr_SetString(PyExc_NotImplementedError,
|
||||
"readlink cannot read file descriptors on this platform");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* First get a handle to the reparse point */
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
reparse_point_handle = CreateFileW(
|
||||
@@ -18878,6 +18923,10 @@ PROBE(probe_openat, HAVE_OPENAT_RUNTIME)
|
||||
PROBE(probe_readlinkat, HAVE_READLINKAT_RUNTIME)
|
||||
#endif
|
||||
|
||||
#ifdef _Py_HAVE_FREADLINK
|
||||
PROBE(probe_freadlink, HAVE_FREADLINK_RUNTIME)
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SYMLINKAT
|
||||
PROBE(probe_symlinkat, HAVE_SYMLINKAT_RUNTIME)
|
||||
#endif
|
||||
@@ -18945,6 +18994,10 @@ static const struct have_function {
|
||||
{ "HAVE_FPATHCONF", NULL },
|
||||
#endif
|
||||
|
||||
#ifdef _Py_HAVE_FREADLINK
|
||||
{ "HAVE_FREADLINK", probe_freadlink },
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_FSTATAT
|
||||
{ "HAVE_FSTATAT", probe_fstatat },
|
||||
#endif
|
||||
|
||||
@@ -21732,6 +21732,48 @@ fi
|
||||
|
||||
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for freadlink" >&5
|
||||
printf %s "checking for freadlink... " >&6; }
|
||||
if test ${ac_cv_func_freadlink+y}
|
||||
then :
|
||||
printf %s "(cached) " >&6
|
||||
else case e in #(
|
||||
e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
|
||||
/* end confdefs.h. */
|
||||
#include <unistd.h>
|
||||
int
|
||||
main (void)
|
||||
{
|
||||
void *x=freadlink
|
||||
;
|
||||
return 0;
|
||||
}
|
||||
_ACEOF
|
||||
if ac_fn_c_try_compile "$LINENO"
|
||||
then :
|
||||
ac_cv_func_freadlink=yes
|
||||
else case e in #(
|
||||
e) ac_cv_func_freadlink=no ;;
|
||||
esac
|
||||
fi
|
||||
rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_freadlink" >&5
|
||||
printf "%s\n" "$ac_cv_func_freadlink" >&6; }
|
||||
if test "x$ac_cv_func_freadlink" = xyes
|
||||
then :
|
||||
|
||||
printf "%s\n" "#define _Py_HAVE_FREADLINK 1" >>confdefs.h
|
||||
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for strerror_r" >&5
|
||||
printf %s "checking for strerror_r... " >&6; }
|
||||
if test ${ac_cv_func_strerror_r+y}
|
||||
|
||||
@@ -5584,6 +5584,7 @@ AC_CHECK_FUNCS([ \
|
||||
wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \
|
||||
])
|
||||
|
||||
PY_CHECK_FUNC_PRIVATE([freadlink], [@%:@include <unistd.h>])
|
||||
PY_CHECK_FUNC_PRIVATE([strerror_r], [@%:@include <string.h>])
|
||||
|
||||
|
||||
|
||||
@@ -2201,6 +2201,9 @@
|
||||
/* Defined if _Float16 C type is supported */
|
||||
#undef _Py_HAVE_FLOAT16
|
||||
|
||||
/* Define if you have the 'freadlink' function. */
|
||||
#undef _Py_HAVE_FREADLINK
|
||||
|
||||
/* Define if you have a working iconv() function. */
|
||||
#undef _Py_HAVE_ICONV
|
||||
|
||||
|
||||
Reference in new issue
Block a user