diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index c9cbe1541e733e..7fc81c49975ce4 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -1521,6 +1521,27 @@ 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") and os.link in os.supports_follow_symlinks, + "test needs follow_symlinks support in os.link()" + ) + def test_link_follow_symlinks(self): + symlink_fn = os_helper.TESTFN + 'symlink' + link_following = os_helper.TESTFN + 'link_following' + link_nofollow = os_helper.TESTFN + 'link_nofollow' + posix.symlink(os_helper.TESTFN, symlink_fn) + self.addCleanup(os_helper.unlink, symlink_fn) + + # follow_symlinks=False -> duplicate the symlink itself + posix.link(symlink_fn, link_nofollow, follow_symlinks=False) + self.addCleanup(os_helper.unlink, link_nofollow) + self.assertEqual(posix.lstat(link_nofollow), posix.lstat(symlink_fn)) + + # follow_symlinks=True -> duplicate the target file + posix.link(symlink_fn, link_following, follow_symlinks=True) + self.addCleanup(os_helper.unlink, link_following) + self.assertEqual(posix.lstat(link_following), posix.lstat(os_helper.TESTFN)) + # tests for the posix *at functions follow class TestPosixDirFd(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2019-07-19-09-57-18.bpo-37612.yjjxx-.rst b/Misc/NEWS.d/next/Library/2019-07-19-09-57-18.bpo-37612.yjjxx-.rst new file mode 100644 index 00000000000000..85d2775110ac9e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-07-19-09-57-18.bpo-37612.yjjxx-.rst @@ -0,0 +1 @@ +fix os.link() on platforms (like Linux) where the system link() function does not follow symlinks \ No newline at end of file diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b7300def8dc75f..99e5d6f382ab36 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4383,31 +4383,41 @@ 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); - - } + 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; - } + else { + if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD && follow_symlinks) { + /* See issue 41355: This matches the behaviour of !HAVE_LINKAT */ + result = link(src->narrow, dst->narrow); + } else { + linkat_unavailable = 1; } + } #endif + +#else /* linkat not available */ +/* See issue 41355: link() on Linux works like linkat without AT_SYMLINK_FOLLOW, + but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */ +#ifdef __APPLE__ + if (!follow_symlinks) { + PyErr_SetString(PyExc_NotImplementedError, + "link: follow_symlinks=False unavailable on this platform"); + return NULL; + } else { +#else + if (follow_symlinks) { + PyErr_SetString(PyExc_NotImplementedError, + "link: follow_symlinks=True unavailable on this platform"); + return NULL; + } else { +#endif /* __APPLE__ */ + result = link(src->narrow, dst->narrow); } - else #endif /* HAVE_LINKAT */ - result = link(src->narrow, dst->narrow); Py_END_ALLOW_THREADS #ifdef HAVE_LINKAT