From a8f488aa7267510e953979175d8d9045120d0291 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 17:35:52 +0300 Subject: [PATCH 01/11] gh-81793: Always call linkat() from os.link(), if available This fixes os.link() on platforms (like Linux and OpenIndiana) where the system link() function does not follow symlinks. * On Linux and OpenIndiana, it now follows symlinks by default and if follow_symlinks=True is specified. * On Windows, it now raises error if follow_symlinks=True is passed. * On macOS, it now raises error if follow_symlinks=False is passed and the system linkat() function is not available at runtime. * On other platforms, it now raises error if follow_symlinks is passed with a value that does not match the system link() function behavior if if the behavior is not known. Co-authored-by: Joachim Henke <37883863+jo-he@users.noreply.github.com> Co-authored-by: Thomas Kluyver --- Doc/library/os.rst | 1 + Lib/test/test_posix.py | 39 +++++++++ ...5-04-14-17-24-50.gh-issue-81793.OhRTTT.rst | 7 ++ Modules/clinic/posixmodule.c.h | 4 +- Modules/posixmodule.c | 87 +++++++++---------- 5 files changed, 91 insertions(+), 47 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst 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_posix.py b/Lib/test/test_posix.py index c9cbe1541e733e..6e077eecea2aed 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1521,6 +1521,45 @@ 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): + orig = os_helper.TESTFN + symlink = orig + 'symlink' + posix.symlink(orig, symlink) + self.addCleanup(os_helper.unlink, symlink) + + link = orig + 'link' + posix.link(symlink, link) + self.addCleanup(os_helper.unlink, link) + default_follow = sys.platform.startswith(('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly')) + default_no_follow = sys.platform.startswith(('win32', 'linux', 'sunos5')) + 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)) + + # follow_symlinks=False -> duplicate the symlink itself + link_nofollow = orig + 'link_nofollow' + try: + posix.link(symlink, link_nofollow, 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_nofollow) + self.assertEqual(posix.lstat(link_nofollow), posix.lstat(symlink)) + + # follow_symlinks=True -> duplicate the target file + link_following = orig + 'link_following' + try: + posix.link(symlink, link_following, follow_symlinks=True) + except NotImplementedError: + if os.link in os.supports_follow_symlinks or default_follow: + raise + else: + self.addCleanup(os_helper.unlink, link_following) + self.assertEqual(posix.lstat(link_following), 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..15fe8ba9e9a902 --- /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 and OpenIndiana) where the +system :c:finc:`!link` function does not follow symlinks. On Linux and +OpenIndiana, it now follows symlinks by default and if +``follow_symlinks=True`` is specified. On Windows, it now raises error if +``follow_symlinks=True`` is passed. On macOS, it now raises error if +``follow_symlinks=False`` is passed and the system :c:finc:`!linkat` +function is not available at runtime. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index d03f68ab8fb9aa..2c0f4d9401e2ee 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -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=35dd8edb53b50537 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=27f684c2f99c83c6 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b7300def8dc75f..f18b785de6d3af 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -427,7 +427,7 @@ extern char *ctermid_r(char *); /* Something to implement in autoconf, not present in autoconf 2.69 */ # define HAVE_STRUCT_STAT_ST_FSTYPE 1 #endif - +#undef HAVE_LINKAT // --- Apple __builtin_available() macros ----------------------------------- @@ -4323,7 +4323,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. @@ -4341,25 +4341,48 @@ 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=f6a681a558380a15]*/ { #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 + { + 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 41355: 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__) || (defined(__sun) && defined(__SVR4)) + if (follow_symlinks == 1) { + argument_unavailable_error("link", "follow_symlinks=True"); + return NULL; + } +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) + 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 + } -#ifndef MS_WINDOWS +#ifdef MS_WINDOWS if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) { PyErr_SetString(PyExc_NotImplementedError, "link: src and dst must be the same type"); @@ -4383,44 +4406,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); From 4ba224e4e1c5c385b416173218a6bbe1eef6bc7f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 18:05:48 +0300 Subject: [PATCH 02/11] Fix typo and regenerate clinic files. --- .../Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst | 4 ++-- Modules/clinic/posixmodule.c.h | 4 ++-- Modules/posixmodule.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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 index 15fe8ba9e9a902..f0246b181bdd2e 100644 --- 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 @@ -1,7 +1,7 @@ Fix :func:`os.link` on platforms (like Linux and OpenIndiana) where the -system :c:finc:`!link` function does not follow symlinks. On Linux and +system :c:func:`!link` function does not follow symlinks. On Linux and OpenIndiana, it now follows symlinks by default and if ``follow_symlinks=True`` is specified. On Windows, it now raises error if ``follow_symlinks=True`` is passed. On macOS, it now raises error if -``follow_symlinks=False`` is passed and the system :c:finc:`!linkat` +``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 2c0f4d9401e2ee..0d970a6a050f5c 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" @@ -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=27f684c2f99c83c6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=12932e9cd3f9afb9 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index f18b785de6d3af..70c235278015c4 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4341,7 +4341,7 @@ 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=f6a681a558380a15]*/ +/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/ { #ifdef MS_WINDOWS BOOL result = FALSE; From 5881b2a0eb6ba177468b750bf8b2026c89743d36 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 18:51:54 +0300 Subject: [PATCH 03/11] Fix OpenIndiana. --- Lib/test/test_posix.py | 5 +++-- .../Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst | 6 +++--- Modules/posixmodule.c | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 6e077eecea2aed..7e093d1c850d81 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1531,8 +1531,9 @@ def test_link_follow_symlinks(self): link = orig + 'link' posix.link(symlink, link) self.addCleanup(os_helper.unlink, link) - default_follow = sys.platform.startswith(('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly')) - default_no_follow = sys.platform.startswith(('win32', 'linux', 'sunos5')) + default_follow = sys.platform.startswith( + ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5')) + default_no_follow = sys.platform.startswith(('win32', 'linux')) if os.link in os.supports_follow_symlinks or default_follow: self.assertEqual(posix.lstat(link), posix.lstat(orig)) elif default_no_follow: 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 index f0246b181bdd2e..933c4b63181d12 100644 --- 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 @@ -1,6 +1,6 @@ -Fix :func:`os.link` on platforms (like Linux and OpenIndiana) where the -system :c:func:`!link` function does not follow symlinks. On Linux and -OpenIndiana, it now follows symlinks by default and if +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 and if ``follow_symlinks=True`` is specified. On Windows, it now raises error if ``follow_symlinks=True`` is passed. On macOS, it now raises error if ``follow_symlinks=False`` is passed and the system :c:func:`!linkat` diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 70c235278015c4..39c5369671b9f8 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4364,12 +4364,12 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, } /* See issue 41355: 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__) || (defined(__sun) && defined(__SVR4)) +#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__) +#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; From d2f233b9450724238aadbbf1126872d1b9209acc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 19:05:12 +0300 Subject: [PATCH 04/11] Fix test_inspect. --- Lib/test/test_inspect/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index daae990458d708..05f97337b0fa69 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5799,7 +5799,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'] From 95f55e3192cde90fe34ce05fe0fcae9246a323ec Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 19:08:12 +0300 Subject: [PATCH 05/11] Enable linkat(). --- Modules/posixmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 39c5369671b9f8..a6e18ffd8e1026 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -427,7 +427,7 @@ extern char *ctermid_r(char *); /* Something to implement in autoconf, not present in autoconf 2.69 */ # define HAVE_STRUCT_STAT_ST_FSTYPE 1 #endif -#undef HAVE_LINKAT + // --- Apple __builtin_available() macros ----------------------------------- From 2eeb438c1289ad5962b68ed21ec1b1f8724b0661 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Apr 2025 20:49:29 +0300 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- .../Library/2025-04-14-17-24-50.gh-issue-81793.OhRTTT.rst | 6 +++--- Modules/posixmodule.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 933c4b63181d12..842e973b821744 100644 --- 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 @@ -1,7 +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 and if -``follow_symlinks=True`` is specified. On Windows, it now raises error if -``follow_symlinks=True`` is passed. On macOS, it now raises error if +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/posixmodule.c b/Modules/posixmodule.c index a6e18ffd8e1026..3512e6de5e9ece 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4362,7 +4362,7 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, argument_unavailable_error("link", "src_dir_fd and dst_dir_fd"); return NULL; } -/* See issue 41355: link() on Linux works like linkat without AT_SYMLINK_FOLLOW, +/* 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) { From b4be5269d76ecf29069eb66114a8ffbc183b0b53 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 15 Apr 2025 10:03:21 +0300 Subject: [PATCH 07/11] Use subTest(). --- Lib/test/test_posix.py | 64 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 7e093d1c850d81..bb18e49c9e3f82 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1523,43 +1523,47 @@ def test_pidfd_open(self): @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) - link = orig + 'link' - posix.link(symlink, link) - self.addCleanup(os_helper.unlink, link) - default_follow = sys.platform.startswith( - ('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5')) - default_no_follow = sys.platform.startswith(('win32', 'linux')) - 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('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)) - # follow_symlinks=False -> duplicate the symlink itself - link_nofollow = orig + 'link_nofollow' - try: - posix.link(symlink, link_nofollow, 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_nofollow) - self.assertEqual(posix.lstat(link_nofollow), 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)) - # follow_symlinks=True -> duplicate the target file - link_following = orig + 'link_following' - try: - posix.link(symlink, link_following, follow_symlinks=True) - except NotImplementedError: - if os.link in os.supports_follow_symlinks or default_follow: - raise - else: - self.addCleanup(os_helper.unlink, link_following) - self.assertEqual(posix.lstat(link_following), posix.lstat(orig)) + 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 From 80128e381b97ebccfe9c36d120afff0c528a9d40 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 15 Apr 2025 12:02:15 +0300 Subject: [PATCH 08/11] Disable linkat() on WASI. --- Modules/posixmodule.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 3512e6de5e9ece..909ecd502d9eef 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 From a45dfd9e18e44d43ef1bae00c0e2f2293c3dbe3f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 15 Apr 2025 18:18:53 +0300 Subject: [PATCH 09/11] Test how link() actually works on WASI. --- Lib/test/test_posix.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index bb18e49c9e3f82..3015e579243ad8 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1540,6 +1540,9 @@ def test_link_follow_symlinks(self): self.assertEqual(posix.lstat(link), posix.lstat(orig)) elif default_no_follow: self.assertEqual(posix.lstat(link), posix.lstat(symlink)) + if sys.platform.startswith('wasi'): + self.assertEqual(posix.lstat(link), posix.lstat(orig)) + self.assertEqual(posix.lstat(link), posix.lstat(symlink)) with self.subTest('follow_symlinks=False'): # follow_symlinks=False -> duplicate the symlink itself From 557f1ee014ee27f6f7da7288bc1b365cbee2d6a5 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 15 Apr 2025 18:47:05 +0300 Subject: [PATCH 10/11] Revert "Test how link() actually works on WASI." This reverts commit a45dfd9e18e44d43ef1bae00c0e2f2293c3dbe3f. --- Lib/test/test_posix.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 3015e579243ad8..bb18e49c9e3f82 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1540,9 +1540,6 @@ def test_link_follow_symlinks(self): self.assertEqual(posix.lstat(link), posix.lstat(orig)) elif default_no_follow: self.assertEqual(posix.lstat(link), posix.lstat(symlink)) - if sys.platform.startswith('wasi'): - self.assertEqual(posix.lstat(link), posix.lstat(orig)) - self.assertEqual(posix.lstat(link), posix.lstat(symlink)) with self.subTest('follow_symlinks=False'): # follow_symlinks=False -> duplicate the symlink itself From 2c436fbe85f398efb3921ab2a8c6d9a44f722148 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Apr 2025 10:35:11 +0300 Subject: [PATCH 11/11] Remove redundant checks for same type. --- Modules/posixmodule.c | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 909ecd502d9eef..0a0597ebc317a5 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4386,14 +4386,6 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #endif } -#ifdef 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; - } -#endif - if (PySys_Audit("os.link", "OOii", src->object, dst->object, src_dir_fd == DEFAULT_DIR_FD ? -1 : src_dir_fd, dst_dir_fd == DEFAULT_DIR_FD ? -1 : dst_dir_fd) < 0) { @@ -5913,12 +5905,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) { @@ -10591,12 +10577,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) {