diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 54a5d3b98e8662..1e54cfec609bd2 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2338,6 +2338,7 @@ features: This function can support specifying *src_dir_fd* and/or *dst_dir_fd* to supply :ref:`paths relative to directory descriptors `, and :ref:`not following symlinks `. + The default value of *follow_symlinks* is ``False`` on Windows. .. audit-event:: os.link src,dst,src_dir_fd,dst_dir_fd os.link diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 4950af42cfe5a4..c9b37fcd8f6327 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5844,7 +5844,7 @@ def test_operator_module_has_signatures(self): self._test_module_has_signatures(operator) def test_os_module_has_signatures(self): - unsupported_signature = {'chmod', 'utime'} + unsupported_signature = {'chmod', 'link', 'utime'} unsupported_signature |= {name for name in ['get_terminal_size', 'posix_spawn', 'posix_spawnp', 'register_at_fork', 'startfile'] diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index c9cbe1541e733e..bb18e49c9e3f82 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1521,6 +1521,50 @@ def test_pidfd_open(self): self.assertEqual(cm.exception.errno, errno.EINVAL) os.close(os.pidfd_open(os.getpid(), 0)) + @unittest.skipUnless(hasattr(os, "link"), "test needs os.link()") + def test_link_follow_symlinks(self): + default_follow = sys.platform.startswith( + ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5')) + default_no_follow = sys.platform.startswith(('win32', 'linux')) + orig = os_helper.TESTFN + symlink = orig + 'symlink' + posix.symlink(orig, symlink) + self.addCleanup(os_helper.unlink, symlink) + + with self.subTest('no follow_symlinks'): + # no follow_symlinks -> platform depending + link = orig + 'link' + posix.link(symlink, link) + self.addCleanup(os_helper.unlink, link) + if os.link in os.supports_follow_symlinks or default_follow: + self.assertEqual(posix.lstat(link), posix.lstat(orig)) + elif default_no_follow: + self.assertEqual(posix.lstat(link), posix.lstat(symlink)) + + with self.subTest('follow_symlinks=False'): + # follow_symlinks=False -> duplicate the symlink itself + link = orig + 'link_nofollow' + try: + posix.link(symlink, link, follow_symlinks=False) + except NotImplementedError: + if os.link in os.supports_follow_symlinks or default_no_follow: + raise + else: + self.addCleanup(os_helper.unlink, link) + self.assertEqual(posix.lstat(link), posix.lstat(symlink)) + + with self.subTest('follow_symlinks=True'): + # follow_symlinks=True -> duplicate the target file + link = orig + 'link_following' + try: + posix.link(symlink, link, follow_symlinks=True) + except NotImplementedError: + if os.link in os.supports_follow_symlinks or default_follow: + raise + else: + self.addCleanup(os_helper.unlink, link) + self.assertEqual(posix.lstat(link), posix.lstat(orig)) + # tests for the posix *at functions follow class TestPosixDirFd(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst b/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst new file mode 100644 index 00000000000000..842e973b821744 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst @@ -0,0 +1,7 @@ +Fix :func:`os.link` on platforms (like Linux) where the +system :c:func:`!link` function does not follow symlinks. On Linux, +it now follows symlinks by default or if +``follow_symlinks=True`` is specified. On Windows, it now raises an error if +``follow_symlinks=True`` is passed. On macOS, it now raises an error if +``follow_symlinks=False`` is passed and the system :c:func:`!linkat` +function is not available at runtime. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 0125e247ee44d3..6b8cc3d07ab01c 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -1471,7 +1471,7 @@ os_getcwdb(PyObject *module, PyObject *Py_UNUSED(ignored)) PyDoc_STRVAR(os_link__doc__, "link($module, /, src, dst, *, src_dir_fd=None, dst_dir_fd=None,\n" -" follow_symlinks=True)\n" +" follow_symlinks=(os.name != \'nt\'))\n" "--\n" "\n" "Create a hard link to a file.\n" @@ -1530,7 +1530,7 @@ os_link(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwn path_t dst = PATH_T_INITIALIZE_P("link", "dst", 0, 0, 0, 0); int src_dir_fd = DEFAULT_DIR_FD; int dst_dir_fd = DEFAULT_DIR_FD; - int follow_symlinks = 1; + int follow_symlinks = -1; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -13398,4 +13398,4 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF #define OS__EMSCRIPTEN_DEBUGGER_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */ -/*[clinic end generated code: output=a5ca2541f2af5462 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=f7b5635e0b948be4 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 04c3b9e987aef8..964e988843110e 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -573,7 +573,11 @@ extern char *ctermid_r(char *); # define HAVE_FACCESSAT_RUNTIME 1 # define HAVE_FCHMODAT_RUNTIME 1 # define HAVE_FCHOWNAT_RUNTIME 1 +#ifdef __wasi__ +# define HAVE_LINKAT_RUNTIME 0 +# else # define HAVE_LINKAT_RUNTIME 1 +# endif # define HAVE_FDOPENDIR_RUNTIME 1 # define HAVE_MKDIRAT_RUNTIME 1 # define HAVE_RENAMEAT_RUNTIME 1 @@ -4346,7 +4350,7 @@ os.link * src_dir_fd : dir_fd = None dst_dir_fd : dir_fd = None - follow_symlinks: bool = True + follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = PLACEHOLDER Create a hard link to a file. @@ -4364,31 +4368,46 @@ src_dir_fd, dst_dir_fd, and follow_symlinks may not be implemented on your static PyObject * os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, int dst_dir_fd, int follow_symlinks) -/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/ +/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/ { #ifdef MS_WINDOWS BOOL result = FALSE; #else int result; #endif -#if defined(HAVE_LINKAT) - int linkat_unavailable = 0; -#endif -#ifndef HAVE_LINKAT - if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) { - argument_unavailable_error("link", "src_dir_fd and dst_dir_fd"); - return NULL; +#ifdef HAVE_LINKAT + if (HAVE_LINKAT_RUNTIME) { + if (follow_symlinks < 0) { + follow_symlinks = 1; + } } + else #endif - -#ifndef MS_WINDOWS - if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) { - PyErr_SetString(PyExc_NotImplementedError, - "link: src and dst must be the same type"); - return NULL; - } + { + if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) { + argument_unavailable_error("link", "src_dir_fd and dst_dir_fd"); + return NULL; + } +/* See issue 85527: link() on Linux works like linkat without AT_SYMLINK_FOLLOW, + but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */ +#if defined(MS_WINDOWS) || defined(__linux__) + if (follow_symlinks == 1) { + argument_unavailable_error("link", "follow_symlinks=True"); + return NULL; + } +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) || (defined(__sun) && defined(__SVR4)) + if (follow_symlinks == 0) { + argument_unavailable_error("link", "follow_symlinks=False"); + return NULL; + } +#else + if (follow_symlinks >= 0) { + argument_unavailable_error("link", "follow_symlinks"); + return NULL; + } #endif + } if (PySys_Audit("os.link", "OOii", src->object, dst->object, src_dir_fd == DEFAULT_DIR_FD ? -1 : src_dir_fd, @@ -4406,44 +4425,18 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #else Py_BEGIN_ALLOW_THREADS #ifdef HAVE_LINKAT - if ((src_dir_fd != DEFAULT_DIR_FD) || - (dst_dir_fd != DEFAULT_DIR_FD) || - (!follow_symlinks)) { - - if (HAVE_LINKAT_RUNTIME) { - - result = linkat(src_dir_fd, src->narrow, - dst_dir_fd, dst->narrow, - follow_symlinks ? AT_SYMLINK_FOLLOW : 0); - - } -#ifdef __APPLE__ - else { - if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) { - /* See issue 41355: This matches the behaviour of !HAVE_LINKAT */ - result = link(src->narrow, dst->narrow); - } else { - linkat_unavailable = 1; - } - } -#endif + if (HAVE_LINKAT_RUNTIME) { + result = linkat(src_dir_fd, src->narrow, + dst_dir_fd, dst->narrow, + follow_symlinks ? AT_SYMLINK_FOLLOW : 0); } else -#endif /* HAVE_LINKAT */ +#endif + { + /* linkat not available */ result = link(src->narrow, dst->narrow); - Py_END_ALLOW_THREADS - -#ifdef HAVE_LINKAT - if (linkat_unavailable) { - /* Either or both dir_fd arguments were specified */ - if (src_dir_fd != DEFAULT_DIR_FD) { - argument_unavailable_error("link", "src_dir_fd"); - } else { - argument_unavailable_error("link", "dst_dir_fd"); - } - return NULL; } -#endif + Py_END_ALLOW_THREADS if (result) return path_error2(src, dst); @@ -5935,12 +5928,6 @@ internal_rename(path_t *src, path_t *dst, int src_dir_fd, int dst_dir_fd, int is return path_error2(src, dst); #else - if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) { - PyErr_Format(PyExc_ValueError, - "%s: src and dst must be the same type", function_name); - return NULL; - } - Py_BEGIN_ALLOW_THREADS #ifdef HAVE_RENAMEAT if (dir_fd_specified) { @@ -10613,12 +10600,6 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst, #else - if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) { - PyErr_SetString(PyExc_ValueError, - "symlink: src and dst must be the same type"); - return NULL; - } - Py_BEGIN_ALLOW_THREADS #ifdef HAVE_SYMLINKAT if (dir_fd != DEFAULT_DIR_FD) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy