Skip to content

Commit 3748c32

Browse files
committed
testsuite: added a test for symlinks to the same dir
when a symlink is to the same directory as the source then it can be considered unsafe if it goes via a path outside the directory. This came up on the mailing list, added a test to make the case clear
1 parent 907505c commit 3748c32

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
# Absolute symlink that *resolves inside* the copied tree, under --safe-links.
3+
#
4+
# This is the case that surprises users: a symlink and its referent live in
5+
# the same source directory, so the link "obviously" stays inside the transfer
6+
# -- yet --safe-links still drops it. The reason is that rsync classifies a
7+
# link's safety from the *literal text* of its target, never by resolving it.
8+
# An absolute target (one starting with '/') is unconditionally "unsafe",
9+
# regardless of where it actually points. See unsafe_symlink() in util1.c
10+
# ("all absolute and null symlinks are unsafe") and the SYMBOLIC LINKS section
11+
# of the man page ("considered unsafe if they are absolute symlinks").
12+
#
13+
# The same link written as a *relative* path is safe and survives, which is
14+
# the recommended fix.
15+
16+
import os
17+
18+
from rsyncfns import (
19+
TMPDIR, is_a_link, run_rsync, test_fail,
20+
)
21+
22+
23+
def assert_symlink(path, target):
24+
if not is_a_link(path):
25+
test_fail(f"File {path} is not a symlink")
26+
actual = os.readlink(path)
27+
if actual != target:
28+
test_fail(f"symlink {path} target is {actual!r}, expected {target!r}")
29+
30+
31+
def assert_notexist(path):
32+
# os.path.exists() follows the link, so a dropped link reads as "missing";
33+
# islink() catches a link that was copied verbatim but left dangling.
34+
if os.path.exists(path) or os.path.islink(path):
35+
test_fail(f"File {path} unexpectedly exists")
36+
37+
38+
def assert_regular_file(path):
39+
if is_a_link(path):
40+
test_fail(f"File {path} is a symlink, expected a regular file")
41+
if not os.path.isfile(path):
42+
test_fail(f"File {path} is not a regular file")
43+
44+
45+
os.chdir(TMPDIR)
46+
47+
os.mkdir("from")
48+
with open("from/linked_file", "w") as f:
49+
f.write("payload\n")
50+
51+
# Both links point at the very same in-tree file; only the spelling differs.
52+
abs_target = os.path.abspath("from/linked_file")
53+
os.symlink(abs_target, "from/abs_link") # absolute -> always "unsafe"
54+
os.symlink("linked_file", "from/rel_link") # relative, same dir -> "safe"
55+
56+
# Sanity: the absolute link really does resolve to the in-tree file.
57+
if os.path.realpath("from/abs_link") != os.path.realpath("from/linked_file"):
58+
test_fail("test setup: abs_link does not resolve to linked_file")
59+
60+
# --- 1. Baseline: plain -a (no --safe-links) keeps the absolute link as-is. --
61+
print("baseline: -a without --safe-links preserves the absolute symlink")
62+
run_rsync('-a', 'from/', 'to-plain')
63+
assert_symlink("to-plain/abs_link", abs_target)
64+
assert_symlink("to-plain/rel_link", "linked_file")
65+
66+
# --- 2. --safe-links drops the absolute link though it resolves in-tree. -----
67+
print("--safe-links drops the in-tree-resolving absolute symlink")
68+
proc = run_rsync('-av', '--safe-links', 'from/', 'to-safe',
69+
capture_output=True)
70+
out = proc.stdout + proc.stderr
71+
if 'ignoring unsafe symlink' not in out:
72+
test_fail(f"expected 'ignoring unsafe symlink' message, got:\n{out}")
73+
74+
# The absolute link is omitted entirely -- NOT replaced by its target file.
75+
assert_notexist("to-safe/abs_link")
76+
# The relative link to the same file survives untouched.
77+
assert_symlink("to-safe/rel_link", "linked_file")
78+
# The referent itself is still copied normally.
79+
assert_regular_file("to-safe/linked_file")
80+
81+
# --- 3. The fix paths. -------------------------------------------------------
82+
# --copy-unsafe-links turns the unsafe (absolute) link into a real file copy.
83+
print("--copy-unsafe-links materialises the absolute link as a file")
84+
run_rsync('-a', '--copy-unsafe-links', 'from/', 'to-copy')
85+
assert_regular_file("to-copy/abs_link")
86+
assert_symlink("to-copy/rel_link", "linked_file")

0 commit comments

Comments
 (0)