feat(xattr): carry and apply symlink xattrs (protocol 2.29.0)

This commit is contained in:
2026-09-23 00:34:39 +02:00
parent 6db9827b87
commit 492ce0ce89
16 changed files with 392 additions and 27 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ from common import ( # noqa: E402
verify_transfer,
)
PROTOCOL_VERSION = b"2.28.0"
PROTOCOL_VERSION = b"2.29.0"
STATUS_MANIFEST = 5
STATUS_OK = 0
+50
View File
@@ -6637,6 +6637,56 @@ class TestExtendedAttributes:
received = get_dest_received_dir(dest, source)
assert os.getxattr(os.path.join(received, "data.txt"), "user.k") == b"v"
@pytest.mark.ci
def test_symlink_own_xattrs_never_referent(self, shared_server):
"""Protocol 2.29.0: a symlink's STATUS_SYMLINK frame carries a trailing
xattr block captured with llistxattr/lgetxattr (no follow) and applied
with lsetxattr on the link itself. Linux's VFS refuses to associate
xattrs with a symlink at all, so the portable guarantee asserted here is
the no-follow one: a referent that carries user.* must NOT have those
attributes appear on the destination symlink entry (the old
path-following capture would have copied the referent's attrs onto the
link). On a platform/filesystem that does support symlink xattrs the
full round-trip of the link's own attribute is asserted too."""
source, dest = self._source_and_dest("symlink_xattr")
target = os.path.join(source, "target.txt")
with open(target, "wb") as fh:
fh.write(b"referent payload\n")
if not _xattr_supported(target):
pytest.skip("filesystem does not support user xattrs")
os.setxattr(target, "user.referent-only", b"referent-value")
link = os.path.join(source, "link")
os.symlink("target.txt", link)
link_xattr_supported = False
try:
os.setxattr(link, "user.link-own", b"link-value", follow_symlinks=False)
link_xattr_supported = os.getxattr(
link, "user.link-own", follow_symlinks=False
) == b"link-value"
except (OSError, AttributeError, NotImplementedError):
link_xattr_supported = False
result, _ = run_client(source, dest, flags=["-aX"], port=shared_server.port)
assert result.returncode == 0, \
f"-aX symlink sync failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(dest, source)
dst_link = os.path.join(received, "link")
assert os.path.islink(dst_link), "destination link entry is not a symlink"
assert os.readlink(dst_link) == "target.txt"
# The no-follow guarantee: the referent's attribute must never leak onto
# the symlink entry.
link_names = os.listxattr(dst_link, follow_symlinks=False)
assert "user.referent-only" not in link_names, (
"the destination symlink captured its REFERENT's xattr "
"(path-following capture bug)"
)
if link_xattr_supported:
assert os.getxattr(
dst_link, "user.link-own", follow_symlinks=False
) == b"link-value", "the symlink's own xattr did not round-trip"
@pytest.mark.ci
def test_acls_via_posix_acl_xattr(self, shared_server):
source, dest = self._source_and_dest("acl")
+2 -2
View File
@@ -133,14 +133,14 @@ def _seed_protocol_source(source):
class TestProtocol:
@pytest.mark.ci
def test_protocol_current_version_accepted(self, shared_server):
"""--protocol=2.28.0 (the current PROTOCOL_VERSION) is accepted and the
"""--protocol=2.29.0 (the current PROTOCOL_VERSION) is accepted and the
transfer completes normally."""
source = os.path.join(TEST_DATA_DIR, "proto_ok_src")
dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst")
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
_seed_protocol_source(source)
result, _ = run_client(source, dest, flags=["--protocol=2.28.0"],
result, _ = run_client(source, dest, flags=["--protocol=2.29.0"],
port=shared_server.port)
assert result.returncode == 0, \
f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}"