Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir_fd>`, and :ref:`not
following symlinks <follow_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

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,46 @@ 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', '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))

# 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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 41 additions & 44 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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=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
{
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
}

#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");
Expand All @@ -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);
Expand Down
Loading