Merge branch 'feat/parity-ownership' into feat/rsync-parity
# Conflicts: # src/shared/config.h # tests/test_config.c
This commit is contained in:
@@ -115,7 +115,9 @@ static void build_canonical_frame(void) {
|
||||
if (cfg->usermap) {
|
||||
cfg->usermap_count = 1;
|
||||
cfg->usermap[0].from = MAP_FROM;
|
||||
cfg->usermap[0].from_hi = MAP_FROM;
|
||||
cfg->usermap[0].to = MAP_TO;
|
||||
cfg->usermap[0].to_name = NULL;
|
||||
}
|
||||
if (!cfg->send_directory || !cfg->receive_root_directory || !cfg->usermap) {
|
||||
config_delete(cfg);
|
||||
|
||||
@@ -5009,9 +5009,11 @@ class TestIdentityMapping:
|
||||
clean_dir(dest)
|
||||
with open(os.path.join(source, "f.txt"), "wb") as f:
|
||||
f.write(b"mapped")
|
||||
# #294: --chown cannot be mixed with --usermap/--groupmap on the same
|
||||
# side, so the maps travel together and --chown is exercised separately.
|
||||
result, _ = run_client(
|
||||
source, dest,
|
||||
flags=["--preserve", "--usermap=@1000:@1001", "--groupmap=@100:@101", "--chown=@2000:@2001"],
|
||||
flags=["--preserve", "--usermap=@1000:@1001", "--groupmap=@100:@101"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
@@ -5019,8 +5021,14 @@ class TestIdentityMapping:
|
||||
with open(os.path.join(received, "f.txt"), "rb") as f:
|
||||
assert f.read() == b"mapped"
|
||||
|
||||
result, _ = run_client(source, dest, flags=["--preserve", "--chown=@2000:@2001"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"chown exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="only root can change ownership")
|
||||
def test_numeric_ids_applies_ownership_as_root(self, shared_server):
|
||||
def test_numeric_ids_alone_does_not_apply_ownership_as_root(self, shared_server):
|
||||
# #286.1: --numeric-ids is a mapping modifier, not an ownership request.
|
||||
source = os.path.join(TEST_DATA_DIR, "identity_root_source")
|
||||
dest = os.path.join(TEST_DATA_DIR, "identity_root_dest")
|
||||
clean_dir(source)
|
||||
@@ -5038,8 +5046,8 @@ class TestIdentityMapping:
|
||||
dst_file = os.path.join(received, "f.txt")
|
||||
assert os.path.exists(dst_file)
|
||||
st = os.stat(dst_file)
|
||||
assert st.st_uid == 12345 and st.st_gid == 12346, \
|
||||
f"owner not applied: uid={st.st_uid} gid={st.st_gid}"
|
||||
assert st.st_uid != 12345, \
|
||||
f"--numeric-ids alone must not chown: uid={st.st_uid} gid={st.st_gid}"
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="only root can change ownership")
|
||||
def test_chown_overrides_ownership_as_root(self, shared_server):
|
||||
@@ -5126,21 +5134,21 @@ class TestSuperPrivilege:
|
||||
f"--super alone must not apply ownership (uid={st.st_uid} gid={st.st_gid})"
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="only root can change ownership")
|
||||
def test_super_with_numeric_ids_applies_ownership_as_root(self, shared_server):
|
||||
"""Control: an explicit identity policy is what enables ownership, so
|
||||
--numeric-ids --super still applies the raw ids as root (the very
|
||||
ownership --no-super suppresses)."""
|
||||
def test_super_with_owner_numeric_ids_applies_ownership_as_root(self, shared_server):
|
||||
"""Control: an explicit ownership request is what enables ownership, so
|
||||
-a --numeric-ids --super applies the raw ids as root (the very ownership
|
||||
--no-super suppresses). --numeric-ids itself is only the modifier."""
|
||||
source, dest = self._seed("supernumeric")
|
||||
os.chown(os.path.join(source, "f.txt"), 12345, 12346)
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--preserve", "--numeric-ids", "--super"],
|
||||
flags=["-a", "--numeric-ids", "--super"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
st = os.stat(os.path.join(received, "f.txt"))
|
||||
assert (st.st_uid, st.st_gid) == (12345, 12346), \
|
||||
f"--numeric-ids --super should apply raw ids: uid={st.st_uid} gid={st.st_gid}"
|
||||
f"-a --numeric-ids --super should apply raw ids: uid={st.st_uid} gid={st.st_gid}"
|
||||
|
||||
@pytest.mark.ci
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="only root can change ownership")
|
||||
@@ -6063,6 +6071,79 @@ class TestExtendedAttributes:
|
||||
assert len(fields) == 5
|
||||
assert fields[0] == str(uid), f"reserved uid field {fields[0]} != source uid {uid}"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_fake_super_records_resolved_chown_without_real_chown(self, shared_server):
|
||||
"""#294: --fake-super must NOT real-chown the recorded owner; it records
|
||||
the RESOLVED ownership (here a --chown mapping) in the reserved xattr."""
|
||||
source, dest = self._source_and_dest("fakesuper_chown")
|
||||
f = os.path.join(source, "data.txt")
|
||||
with open(f, "wb") as fh:
|
||||
fh.write(b"fake-super chown\n")
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--fake-super", "--chown=@33333:@44444"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fake-super --chown sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
dst = os.path.join(get_dest_received_dir(dest, source), "data.txt")
|
||||
record = os.getxattr(dst, "user.fastsync.stat").decode().split(":")
|
||||
assert record[0] == "33333", f"recorded owner {record[0]} != resolved 33333"
|
||||
assert record[1] == "44444", f"recorded group {record[1]} != resolved 44444"
|
||||
st = os.stat(dst)
|
||||
assert st.st_uid != 33333, "--fake-super must not real-chown the recorded owner"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_directory_xattrs_preserved(self, shared_server):
|
||||
"""#286.3: -aX must preserve user.* xattrs on DIRECTORIES, not just files."""
|
||||
source, dest = self._source_and_dest("dirxattr")
|
||||
os.makedirs(os.path.join(source, "sub"))
|
||||
if not _xattr_supported(source):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(source, "user.rootdir", b"r")
|
||||
os.setxattr(os.path.join(source, "sub"), "user.subdir", b"s")
|
||||
with open(os.path.join(source, "sub", "f.txt"), "wb") as fh:
|
||||
fh.write(b"x\n")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-aX"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-aX dir sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(received, "user.rootdir") == b"r"
|
||||
assert os.getxattr(os.path.join(received, "sub"), "user.subdir") == b"s"
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_directory_default_acl_preserved(self, shared_server):
|
||||
"""#286.3: -aA must preserve a directory's default POSIX ACL (the
|
||||
system.posix_acl_default xattr), which regular-file ACLs do not cover."""
|
||||
source, dest = self._source_and_dest("diracl")
|
||||
sub = os.path.join(source, "sub")
|
||||
os.makedirs(sub)
|
||||
# A child is needed because FastSync deliberately does not materialize
|
||||
# empty directories; the implicit parent is created by the child write.
|
||||
with open(os.path.join(sub, "f.txt"), "wb") as fh:
|
||||
fh.write(b"acl dir\n")
|
||||
if not _xattr_supported(sub):
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
if shutil.which("setfacl") is None:
|
||||
pytest.skip("setfacl is not available")
|
||||
acl = subprocess.run(["setfacl", "-m", "d:u::rwx,d:g::rx,d:o::---", sub],
|
||||
capture_output=True, text=True)
|
||||
if acl.returncode != 0:
|
||||
pytest.skip(f"cannot set a default ACL: {acl.stderr.strip()}")
|
||||
try:
|
||||
before = os.getxattr(sub, "system.posix_acl_default")
|
||||
except OSError as e:
|
||||
pytest.skip(f"no default ACL xattr: {e}")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-aA"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-aA dir sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert os.getxattr(os.path.join(received, "sub"),
|
||||
"system.posix_acl_default") == before
|
||||
|
||||
|
||||
class TestConnectivityClientOptions:
|
||||
"""Phase 5 connectivity launch options (--outbuf, --blocking-io).
|
||||
@@ -6488,8 +6569,8 @@ class TestCopyAs:
|
||||
@pytest.mark.ci
|
||||
@pytest.mark.skipif(os.geteuid() != 0, reason="requires a root receiver to chown")
|
||||
def test_root_copy_as_with_fake_super_keeps_target_owner(self, shared_server):
|
||||
"""--fake-super must not let the recorded source owner override the
|
||||
--copy-as forced owner (copy-as is authoritative)."""
|
||||
"""#294: --fake-super records the RESOLVED copy-as ownership without
|
||||
real-chowning; the recorded source owner can never override copy-as."""
|
||||
source = os.path.join(TEST_DATA_DIR, "copyas_fakesuper_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "copyas_fakesuper_dst")
|
||||
clean_dir(source)
|
||||
@@ -6497,6 +6578,8 @@ class TestCopyAs:
|
||||
src_file = os.path.join(source, "mixed.txt")
|
||||
with open(src_file, "wb") as fh:
|
||||
fh.write(b"copy-as wins over fake-super\n")
|
||||
if not _xattr_supported(src_file):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.chown(src_file, 12345, 12346)
|
||||
|
||||
result, _ = run_client(source, dest,
|
||||
@@ -6507,7 +6590,13 @@ class TestCopyAs:
|
||||
f"{(result.stderr or result.stdout)[:400]}"
|
||||
)
|
||||
received = get_dest_received_dir(dest, source)
|
||||
st = os.lstat(os.path.join(received, "mixed.txt"))
|
||||
assert (st.st_uid, st.st_gid) == (65534, 65534), (
|
||||
f"--fake-super overrode --copy-as: uid={st.st_uid} gid={st.st_gid}"
|
||||
dst = os.path.join(received, "mixed.txt")
|
||||
record = os.getxattr(dst, "user.fastsync.stat").decode().split(":")
|
||||
assert (record[0], record[1]) == ("65534", "65534"), (
|
||||
f"fake-super must record the resolved copy-as ownership: {record[:2]}"
|
||||
)
|
||||
st = os.lstat(dst)
|
||||
assert (st.st_uid, st.st_gid) != (12345, 12346), (
|
||||
f"--fake-super must not real-chown the recorded source owner: "
|
||||
f"uid={st.st_uid} gid={st.st_gid}"
|
||||
)
|
||||
|
||||
@@ -340,16 +340,85 @@ class TestOwnershipRoot:
|
||||
assert (st.st_uid, st.st_gid) == (33333, 44444), \
|
||||
f"--chown must override -o, got uid={st.st_uid} gid={st.st_gid}"
|
||||
|
||||
def test_fake_super_o_does_not_change_group(self, shared_server):
|
||||
# --fake-super replays the recorded source stat; with only -o requested
|
||||
# it must apply the owner but leave the group untouched (MAJOR 1).
|
||||
def test_fake_super_o_does_not_real_chown(self, shared_server):
|
||||
# #294: --fake-super only RECORDS ownership; it must never real-chown the
|
||||
# recorded source owner (that defeats the point of the flag). With -o the
|
||||
# resolved owner is parked in the reserved xattr and the on-disk owner is
|
||||
# left as the receiver's.
|
||||
source, dest = self._seed_owned("fake_o", 12345, 54321)
|
||||
result, _ = run_client(source, dest, flags=["--fake-super", "-o"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"--fake-super -o failed: {(result.stderr or '')[:300]}"
|
||||
dst = _received(dest, source, "f.txt")
|
||||
st = os.stat(dst)
|
||||
assert st.st_uid != 12345, \
|
||||
f"--fake-super -o must NOT real-chown the source owner, got uid={st.st_uid}"
|
||||
record = os.getxattr(dst, "user.fastsync.stat").decode()
|
||||
fields = record.split(":")
|
||||
assert fields[0] == "12345", \
|
||||
f"--fake-super must record the resolved owner, got {fields[0]}"
|
||||
|
||||
def test_o_applies_directory_owner(self, shared_server):
|
||||
"""#286.2: -o must apply the source owner to DIRECTORIES too (the
|
||||
deferred directory-metadata application now runs the identity path)."""
|
||||
source = os.path.join(TEST_DATA_DIR, "root_dir_o_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "root_dir_o_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
os.makedirs(os.path.join(source, "sub", "deep"))
|
||||
with open(os.path.join(source, "sub", "deep", "f.txt"), "wb") as fh:
|
||||
fh.write(b"dir owner\n")
|
||||
os.chown(os.path.join(source, "sub"), 12345, 12346)
|
||||
os.chown(os.path.join(source, "sub", "deep"), 23456, 34567)
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-o", "-t"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"-o dir failed: {(result.stderr or '')[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
sub = os.stat(os.path.join(received, "sub"))
|
||||
deep = os.stat(os.path.join(received, "sub", "deep"))
|
||||
assert sub.st_uid == 12345, f"dir 'sub' owner not applied: {sub.st_uid}"
|
||||
assert deep.st_uid == 23456, f"dir 'sub/deep' owner not applied: {deep.st_uid}"
|
||||
# -o alone must not change the group.
|
||||
assert sub.st_gid != 12346
|
||||
|
||||
def test_a_applies_directory_owner_and_group(self, shared_server):
|
||||
source = os.path.join(TEST_DATA_DIR, "root_dir_a_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "root_dir_a_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
os.makedirs(os.path.join(source, "sub"))
|
||||
with open(os.path.join(source, "sub", "f.txt"), "wb") as fh:
|
||||
fh.write(b"dir owner group\n")
|
||||
os.chown(os.path.join(source, "sub"), 12345, 54321)
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-a"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"-a dir failed: {(result.stderr or '')[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
st = os.stat(os.path.join(received, "sub"))
|
||||
assert (st.st_uid, st.st_gid) == (12345, 54321), \
|
||||
f"-a must apply dir owner+group, got uid={st.st_uid} gid={st.st_gid}"
|
||||
|
||||
def test_numeric_ids_alone_does_not_chown(self, shared_server):
|
||||
"""#286.1: --numeric-ids is a mapping modifier, not an ownership request.
|
||||
`-t --numeric-ids` must leave the receiver's ownership untouched."""
|
||||
source, dest = self._seed_owned("num_only", 12345, 54321)
|
||||
result, _ = run_client(source, dest, flags=["-t", "--numeric-ids"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-t --numeric-ids failed: {(result.stderr or '')[:300]}"
|
||||
st = os.stat(_received(dest, source, "f.txt"))
|
||||
assert st.st_uid == 12345, f"--fake-super -o must apply the owner, got uid={st.st_uid}"
|
||||
assert st.st_gid != 54321, "--fake-super -o must not change the group"
|
||||
assert st.st_uid != 12345, \
|
||||
f"--numeric-ids alone must not chown, got uid={st.st_uid}"
|
||||
|
||||
def test_numeric_ids_with_o_uses_raw_id(self, shared_server):
|
||||
source, dest = self._seed_owned("num_o", 12345, 54321)
|
||||
result, _ = run_client(source, dest, flags=["-o", "-t", "--numeric-ids"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-o --numeric-ids failed: {(result.stderr or '')[:300]}"
|
||||
st = os.stat(_received(dest, source, "f.txt"))
|
||||
assert st.st_uid == 12345, \
|
||||
f"-o --numeric-ids must apply the raw id, got uid={st.st_uid}"
|
||||
|
||||
|
||||
class TestPreserveFeatureMatrix:
|
||||
|
||||
+91
-1
@@ -2864,6 +2864,92 @@ static void test_parse_args_usermap_name_resolution() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* #294: rsync map FROM forms -- inclusive numeric ranges, '*' (any), and the
|
||||
* empty token (ids with no name on the sender). A TO name is transmitted as a
|
||||
* NAME for the receiver to resolve (rsync resolves TO names on the receiving
|
||||
* side), not resolved against the client's database. */
|
||||
static void test_parse_args_usermap_rsync_forms() {
|
||||
int positional_args[2];
|
||||
|
||||
Config* cfg = config_create();
|
||||
int positional_count = 0;
|
||||
char* argv[] = {"fastsync", "--usermap=0-99:nobody", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->usermap_count, 1);
|
||||
EXPECT_EQ_INT(cfg->usermap[0].from, 0);
|
||||
EXPECT_EQ_INT(cfg->usermap[0].from_hi, 99);
|
||||
EXPECT_TRUE(cfg->preserve_owner);
|
||||
config_delete(cfg);
|
||||
|
||||
/* Empty FROM => IDENTITY_MATCH_UNNAMED. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv2[] = {"fastsync", "--usermap=:@0", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv2, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->usermap_count, 1);
|
||||
EXPECT_EQ_INT(cfg->usermap[0].from, IDENTITY_MATCH_UNNAMED);
|
||||
EXPECT_EQ_INT(cfg->usermap[0].from_hi, IDENTITY_MATCH_UNNAMED);
|
||||
config_delete(cfg);
|
||||
|
||||
/* '*' FROM => IDENTITY_MATCH_ANY. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv3[] = {"fastsync", "--groupmap=*:@0", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->groupmap[0].from, IDENTITY_MATCH_ANY);
|
||||
EXPECT_EQ_INT(cfg->groupmap[0].from_hi, IDENTITY_MATCH_ANY);
|
||||
config_delete(cfg);
|
||||
|
||||
/* A TO name is kept as a receiver-resolved name, NOT resolved locally. */
|
||||
cfg = config_create();
|
||||
positional_count = 0;
|
||||
char* argv4[] = {"fastsync", "--usermap=0:nobody", "/src", "/dst"};
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv4, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->usermap_count, 1);
|
||||
EXPECT_NOT_NULL(cfg->usermap[0].to_name);
|
||||
if (cfg->usermap[0].to_name)
|
||||
EXPECT_EQ_STR(cfg->usermap[0].to_name, "nobody");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* #294: rsync refuses to mix --chown with --usermap/--groupmap on the same
|
||||
* side (either order). --chown=USER conflicts with a prior --usermap;
|
||||
* --chown=:GROUP conflicts with a prior --groupmap; the opposite side is fine. */
|
||||
static void test_parse_args_identity_map_chown_conflict() {
|
||||
int positional_args[2];
|
||||
struct {
|
||||
const char* a;
|
||||
const char* b;
|
||||
} bad[] = {
|
||||
{"--usermap=0:1", "--chown=2:3"}, {"--chown=2:3", "--usermap=0:1"},
|
||||
{"--chown=2", "--usermap=0:1"}, {"--chown=2:3", "--groupmap=0:1"},
|
||||
{"--groupmap=0:1", "--chown=2:3"}, {"--chown=:3", "--groupmap=0:1"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", (char*)bad[i].a, (char*)bad[i].b, "/src", "/dst"};
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* The opposite-side combinations rsync allows must still parse. */
|
||||
struct {
|
||||
const char* a;
|
||||
const char* b;
|
||||
} ok[] = {
|
||||
{"--chown=2", "--groupmap=0:1"},
|
||||
{"--chown=:3", "--usermap=0:1"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(ok) / sizeof(ok[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", (char*)ok[i].a, (char*)ok[i].b, "/src", "/dst"};
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/* --chown parses USER:GROUP / USER / :GROUP, numeric ids, and '*'. */
|
||||
static void test_parse_args_chown() {
|
||||
Config* cfg = config_create();
|
||||
@@ -2981,8 +3067,10 @@ static void test_parse_args_rejects_malformed_identity() {
|
||||
const char* val;
|
||||
} bad[] = {
|
||||
{"--usermap", "@1000"},
|
||||
{"--usermap", ":1000"},
|
||||
{"--usermap", "definitely_not_a_real_user_zzz:@1"},
|
||||
{"--usermap", "0-"},
|
||||
{"--usermap", "5-2:@1"},
|
||||
{"--usermap", "roo*:@1"},
|
||||
{"--groupmap", "@1"},
|
||||
{"--groupmap", "no_such_group_qqq:x"},
|
||||
{"--chown", "a:b:c"},
|
||||
@@ -3962,6 +4050,8 @@ void test_client_cli() {
|
||||
test_parse_args_usermap();
|
||||
test_parse_args_groupmap();
|
||||
test_parse_args_usermap_name_resolution();
|
||||
test_parse_args_usermap_rsync_forms();
|
||||
test_parse_args_identity_map_chown_conflict();
|
||||
test_parse_args_chown();
|
||||
test_parse_args_copy_as();
|
||||
test_parse_args_rejects_malformed_identity();
|
||||
|
||||
+36
-12
@@ -1345,13 +1345,19 @@ static void test_config_identity_wire_roundtrip() {
|
||||
send_cfg->usermap_count = 2;
|
||||
send_cfg->usermap = calloc(2, sizeof(IdentityMap));
|
||||
send_cfg->usermap[0].from = IDENTITY_MATCH_ANY;
|
||||
send_cfg->usermap[0].from_hi = IDENTITY_MATCH_ANY;
|
||||
send_cfg->usermap[0].to = 65534;
|
||||
send_cfg->usermap[0].to_name = NULL;
|
||||
send_cfg->usermap[1].from = 1000;
|
||||
send_cfg->usermap[1].from_hi = 1000;
|
||||
send_cfg->usermap[1].to = 1000;
|
||||
send_cfg->usermap[1].to_name = NULL;
|
||||
send_cfg->groupmap_count = 1;
|
||||
send_cfg->groupmap = calloc(1, sizeof(IdentityMap));
|
||||
send_cfg->groupmap[0].from = 0;
|
||||
send_cfg->groupmap[0].from_hi = 0;
|
||||
send_cfg->groupmap[0].to = IDENTITY_CURRENT;
|
||||
send_cfg->groupmap[0].to_name = str_dup("root");
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||
@@ -1367,9 +1373,12 @@ static void test_config_identity_wire_roundtrip() {
|
||||
ok = recv->numeric_ids && recv->chown_uid_set && recv->chown_uid == 1001 &&
|
||||
recv->chown_gid_set && recv->chown_gid == IDENTITY_CURRENT && recv->usermap_count == 2 &&
|
||||
recv->groupmap_count == 1 && recv->usermap[0].from == IDENTITY_MATCH_ANY &&
|
||||
recv->usermap[0].to == 65534 && recv->usermap[1].from == 1000 &&
|
||||
recv->usermap[1].to == 1000 && recv->groupmap[0].from == 0 &&
|
||||
recv->groupmap[0].to == IDENTITY_CURRENT;
|
||||
recv->usermap[0].from_hi == IDENTITY_MATCH_ANY && recv->usermap[0].to == 65534 &&
|
||||
recv->usermap[0].to_name == NULL && recv->usermap[1].from == 1000 &&
|
||||
recv->usermap[1].from_hi == 1000 && recv->usermap[1].to == 1000 &&
|
||||
recv->groupmap[0].from == 0 && recv->groupmap[0].from_hi == 0 &&
|
||||
recv->groupmap[0].to == IDENTITY_CURRENT && recv->groupmap[0].to_name != NULL &&
|
||||
strcmp(recv->groupmap[0].to_name, "root") == 0;
|
||||
}
|
||||
config_delete(recv);
|
||||
close(p[0]);
|
||||
@@ -1399,7 +1408,8 @@ static void test_config_receive_rejects_invalid_identity() {
|
||||
c->receive_root_directory = str_dup("/dst");
|
||||
c->usermap_count = 1;
|
||||
c->usermap = calloc(1, sizeof(IdentityMap));
|
||||
c->usermap[0].from = -2; /* below IDENTITY_MATCH_ANY */
|
||||
c->usermap[0].from = -3; /* below IDENTITY_MATCH_UNNAMED */
|
||||
c->usermap[0].from_hi = -3;
|
||||
c->usermap[0].to = 0;
|
||||
EXPECT_FALSE(roundtrip_config_ok(c));
|
||||
config_delete(c);
|
||||
@@ -2101,8 +2111,10 @@ static void test_identity_explicit_ownership_requested() {
|
||||
}
|
||||
|
||||
/* P7 Wave E hardening (A3): --super no longer implies raw numeric-id
|
||||
preservation, so it must never enable ownership application on its own; an
|
||||
explicit identity flag is required. */
|
||||
preservation, so it must never enable ownership application on its own.
|
||||
#286: --numeric-ids is a mapping MODIFIER only and is likewise inert on its
|
||||
own; a real ownership request (-o/-g or an explicit identity flag) is
|
||||
required to activate chown. */
|
||||
static void test_super_does_not_imply_numeric() {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
@@ -2112,6 +2124,9 @@ static void test_super_does_not_imply_numeric() {
|
||||
EXPECT_FALSE(identity_active_enabled());
|
||||
c->numeric_ids = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_FALSE(identity_active_enabled()); /* mapping modifier only */
|
||||
c->preserve_owner = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(identity_active_enabled());
|
||||
identity_clear_active();
|
||||
config_delete(c);
|
||||
@@ -2635,13 +2650,19 @@ static void golden_config_populate(Config* c) {
|
||||
c->usermap_count = 2;
|
||||
c->usermap = calloc(2, sizeof(IdentityMap));
|
||||
c->usermap[0].from = IDENTITY_MATCH_ANY;
|
||||
c->usermap[0].from_hi = IDENTITY_MATCH_ANY;
|
||||
c->usermap[0].to = 1000;
|
||||
c->usermap[0].to_name = NULL;
|
||||
c->usermap[1].from = 5;
|
||||
c->usermap[1].from_hi = 9;
|
||||
c->usermap[1].to = 6;
|
||||
c->usermap[1].to_name = NULL;
|
||||
c->groupmap_count = 1;
|
||||
c->groupmap = calloc(1, sizeof(IdentityMap));
|
||||
c->groupmap[0].from = 7;
|
||||
c->groupmap[0].from_hi = 7;
|
||||
c->groupmap[0].to = 8;
|
||||
c->groupmap[0].to_name = str_dup("root");
|
||||
c->preserve_atimes = true;
|
||||
c->preserve_crtimes = false;
|
||||
c->omit_dir_times = true;
|
||||
@@ -2668,11 +2689,11 @@ static void golden_config_populate(Config* c) {
|
||||
/* The pinned golden frame (protocol 2.23.0). The values below are the only
|
||||
* thing that ties the generated table to the historical wire format; update
|
||||
* them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.23.0
|
||||
* delete-semantics wave keeps the config-frame LAYOUT unchanged, but the
|
||||
* embedded version string moves to "2.23.0", so the byte-exact hash changes
|
||||
* while the length stays 653. */
|
||||
#define GOLDEN_WIRE_LEN 653
|
||||
#define GOLDEN_WIRE_HASH 3267254725292157519ULL
|
||||
* rsync-parity wave changes the config-frame layout (map-entry range + TO name,
|
||||
* plus other wire changes landing in this version); the byte-exact hash is
|
||||
* recomputed for the merged layout. */
|
||||
#define GOLDEN_WIRE_LEN 693
|
||||
#define GOLDEN_WIRE_HASH 6341115972171444885ULL
|
||||
|
||||
static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) {
|
||||
unsigned long long h = 1469598103934665603ULL;
|
||||
@@ -2815,7 +2836,10 @@ static void test_config_wire_golden_receive() {
|
||||
ok = ok && recv->super_mode == SUPER_MODE_ON;
|
||||
ok = ok && recv->chown_uid == 1234 && recv->chown_gid == 5678;
|
||||
ok = ok && recv->usermap_count == 2 && recv->usermap[0].from == IDENTITY_MATCH_ANY &&
|
||||
recv->usermap[0].to == 1000 && recv->usermap[1].from == 5 && recv->usermap[1].to == 6;
|
||||
recv->usermap[0].from_hi == IDENTITY_MATCH_ANY && recv->usermap[0].to == 1000 &&
|
||||
recv->usermap[1].from == 5 && recv->usermap[1].from_hi == 9 && recv->usermap[1].to == 6;
|
||||
ok = ok && recv->groupmap_count == 1 && recv->groupmap[0].from == 7 &&
|
||||
recv->groupmap[0].to_name != NULL && strcmp(recv->groupmap[0].to_name, "root") == 0;
|
||||
ok = ok && recv->basis_count == 2 && recv->basis_dirs[0].type == BASIS_DEST_COMPARE &&
|
||||
recv->basis_dirs[1].type == BASIS_DEST_LINK;
|
||||
ok = ok && recv->module != NULL && strcmp(recv->module, "goldenmod") == 0;
|
||||
|
||||
+15
-4
@@ -1659,9 +1659,19 @@ static void test_dir_time_list() {
|
||||
dir_time_list_init(&list);
|
||||
EXPECT_EQ_INT((int)list.count, 0);
|
||||
FileMetadata metadata = {.mtime_sec = 1000000000, .mtime_nsec = 0};
|
||||
EXPECT_TRUE(dir_time_list_add(&list, "sub", &metadata));
|
||||
EXPECT_TRUE(dir_time_list_add(&list, "sub", &metadata));
|
||||
EXPECT_TRUE(dir_time_list_add(&list, "sub", &metadata, NULL));
|
||||
/* A captured xattr block is deep-copied into the list. */
|
||||
FileXattrList* xl = xattr_list_new();
|
||||
EXPECT_NOT_NULL(xl);
|
||||
EXPECT_TRUE(xattr_list_append(xl, "user.dir", "v", 1));
|
||||
EXPECT_TRUE(dir_time_list_add(&list, "sub", &metadata, xl));
|
||||
xattr_list_free(xl); /* the list owns its own copy now */
|
||||
EXPECT_EQ_INT((int)list.count, 2);
|
||||
EXPECT_NOT_NULL(list.xattrs);
|
||||
EXPECT_NOT_NULL(list.xattrs[1]);
|
||||
EXPECT_EQ_INT(list.xattrs[1]->count, 1);
|
||||
EXPECT_EQ_STR(list.xattrs[1]->items[0].name, "user.dir");
|
||||
EXPECT_NULL(list.xattrs[0]);
|
||||
|
||||
Config* cfg = config_create();
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
@@ -1677,6 +1687,7 @@ static void test_dir_time_list() {
|
||||
EXPECT_EQ_INT((int)list.count, 0);
|
||||
EXPECT_NULL(list.paths);
|
||||
EXPECT_NULL(list.entries);
|
||||
EXPECT_NULL(list.xattrs);
|
||||
|
||||
rmdir(sub);
|
||||
rmdir(root);
|
||||
@@ -1701,14 +1712,14 @@ static void test_dir_time_list_cap() {
|
||||
for (size_t i = 0; i < MAX_DIR_TIME_ENTRIES + 1 && !rejected; i++) {
|
||||
size_t before_count = list.count;
|
||||
size_t before_bytes = list.bytes;
|
||||
if (!dir_time_list_add(&list, path, &metadata)) {
|
||||
if (!dir_time_list_add(&list, path, &metadata, NULL)) {
|
||||
rejected = true;
|
||||
/* The rejected add must not have partially mutated the list. */
|
||||
EXPECT_TRUE(list.count == before_count);
|
||||
EXPECT_TRUE(list.bytes == before_bytes);
|
||||
} else {
|
||||
EXPECT_TRUE(list.count == before_count + 1);
|
||||
EXPECT_TRUE(list.bytes == before_bytes + path_len + sizeof(FileMetadata) + sizeof(char*));
|
||||
EXPECT_TRUE(list.bytes == before_bytes + path_len + sizeof(FileMetadata) + 2 * sizeof(char*));
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(rejected);
|
||||
|
||||
@@ -379,7 +379,9 @@ static void test_fuzz_config_receive_huge_map_count() {
|
||||
}
|
||||
c->usermap_count = 1;
|
||||
c->usermap[0].from = sentinel_from;
|
||||
c->usermap[0].from_hi = sentinel_from;
|
||||
c->usermap[0].to = sentinel_to;
|
||||
c->usermap[0].to_name = NULL;
|
||||
|
||||
unsigned char* frame = NULL;
|
||||
size_t len = 0;
|
||||
@@ -390,9 +392,12 @@ static void test_fuzz_config_receive_huge_map_count() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char pattern[8];
|
||||
/* One wire entry is [from][from_hi][to][to_name]; search the fixed-width
|
||||
prefix (the to_name length-prefixed string follows). */
|
||||
unsigned char pattern[12];
|
||||
memcpy(pattern, &sentinel_from, sizeof(sentinel_from));
|
||||
memcpy(pattern + sizeof(sentinel_from), &sentinel_to, sizeof(sentinel_to));
|
||||
memcpy(pattern + sizeof(sentinel_from), &sentinel_from, sizeof(sentinel_from));
|
||||
memcpy(pattern + 2 * sizeof(sentinel_from), &sentinel_to, sizeof(sentinel_to));
|
||||
size_t entry_off = find_bytes(frame, len, pattern, sizeof(pattern));
|
||||
if (entry_off == SIZE_MAX || entry_off < sizeof(int32_t)) {
|
||||
free(frame);
|
||||
|
||||
+103
-99
@@ -400,14 +400,12 @@ static void test_fake_super_restore() {
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* --fake-super owner replay must honor the super gate and copy-as authority:
|
||||
--no-super suppresses the recorded-source-owner chown even for root, and an
|
||||
active --copy-as keeps its forced owner (the recorded source owner must never
|
||||
override it). Root-gated: only root can observe a chown actually landing. */
|
||||
static void test_fake_super_owner_gate() {
|
||||
if (geteuid() != 0)
|
||||
return; /* non-root cannot observe ownership changes; skip silently */
|
||||
const char* path = "test_fake_super_owner_gate.txt";
|
||||
/* --fake-super must NEVER perform a real chown: fake_super_restore_fd applies
|
||||
* only mode/mtime and leaves the entry's uid/gid exactly as they were, even
|
||||
* when an explicit ownership policy is active and super_mode permits it. This
|
||||
* is observable unprivileged (the file's owner is simply unchanged). */
|
||||
static void test_fake_super_no_real_chown() {
|
||||
const char* path = "test_fake_super_nochown.txt";
|
||||
unlink(path);
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0)
|
||||
@@ -416,63 +414,34 @@ static void test_fake_super_owner_gate() {
|
||||
if (has_xattr)
|
||||
removexattr(path, "user.fastsync.xprobe");
|
||||
if (!has_xattr) {
|
||||
close(fd);
|
||||
unlink(path);
|
||||
return; /* filesystem without xattr support */
|
||||
}
|
||||
if (fchown(fd, 0, 0) != 0) {
|
||||
close(fd);
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
struct stat before;
|
||||
EXPECT_EQ_INT(fstat(fd, &before), 0);
|
||||
fake_super_store_fd(fd, 12345, 12346, 0755, 1700000000, 0);
|
||||
|
||||
Config* c = config_create();
|
||||
FileAttrPolicy policy = {true, true, false, false};
|
||||
EXPECT_NOT_NULL(c);
|
||||
|
||||
/* An explicit ownership policy is required before fake-super replay may
|
||||
chown; --fake-super alone only records the source owner (A2). */
|
||||
c->numeric_ids = true;
|
||||
|
||||
/* --no-super: the owner leg is skipped even as root. */
|
||||
c->super_mode = SUPER_MODE_OFF;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 0);
|
||||
|
||||
/* AUTO with an identity policy: the recorded source owner is applied. */
|
||||
c->super_mode = SUPER_MODE_AUTO;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 12345);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 12346);
|
||||
|
||||
/* --super / --fake-super with NO explicit identity flag must NOT apply a
|
||||
client-chosen owner: super_mode alone never enables ownership. */
|
||||
EXPECT_EQ_INT(fchown(fd, 0, 0), 0);
|
||||
c->numeric_ids = false;
|
||||
/* The strongest ownership request available plus permitted super mode. */
|
||||
c->preserve_owner = true;
|
||||
c->preserve_group = true;
|
||||
c->chown_uid_set = true;
|
||||
c->chown_uid = 12345;
|
||||
c->chown_gid_set = true;
|
||||
c->chown_gid = 12346;
|
||||
c->super_mode = SUPER_MODE_ON;
|
||||
c->fake_super = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 0);
|
||||
|
||||
/* Active --copy-as is authoritative: the recorded source owner must not
|
||||
override it, even with AUTO/ON. */
|
||||
c->copy_as_set = true;
|
||||
c->copy_as_uid = 777;
|
||||
c->copy_as_gid = 778;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 0);
|
||||
struct stat after;
|
||||
EXPECT_EQ_INT(fstat(fd, &after), 0);
|
||||
EXPECT_EQ_INT((int)after.st_uid, (int)before.st_uid);
|
||||
EXPECT_EQ_INT((int)after.st_gid, (int)before.st_gid);
|
||||
/* Mode is still replayed (policy-gated). */
|
||||
EXPECT_EQ_INT((int)(after.st_mode & 0777), 0755);
|
||||
|
||||
identity_clear_active();
|
||||
config_delete(c);
|
||||
@@ -480,64 +449,99 @@ static void test_fake_super_owner_gate() {
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* MAJOR 1: the --fake-super owner replay must honor the per-side -o/-g split.
|
||||
* With only -o (preserve_owner) requested the recorded GROUP must be left
|
||||
* untouched, and with only -g (preserve_group) the recorded OWNER must be left
|
||||
* untouched. Root-gated: only root can observe a chown actually landing. */
|
||||
static void test_fake_super_owner_group_split() {
|
||||
if (geteuid() != 0)
|
||||
return; /* non-root cannot observe ownership changes; skip silently */
|
||||
const char* path = "test_fake_super_owner_group_split.txt";
|
||||
unlink(path);
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0)
|
||||
return;
|
||||
bool has_xattr = setxattr(path, "user.fastsync.xprobe", "p", 1, 0) == 0;
|
||||
if (has_xattr)
|
||||
removexattr(path, "user.fastsync.xprobe");
|
||||
if (!has_xattr) {
|
||||
close(fd);
|
||||
unlink(path);
|
||||
return; /* filesystem without xattr support */
|
||||
}
|
||||
if (fchown(fd, 0, 0) != 0) {
|
||||
close(fd);
|
||||
unlink(path);
|
||||
return;
|
||||
}
|
||||
fake_super_store_fd(fd, 12345, 12346, 0755, 1700000000, 0);
|
||||
/* identity_resolve_storage_ids() is what --fake-super RECORDS: the resolved
|
||||
* mapping for a requested side, and the source's own id for a side never
|
||||
* requested. Also pins the #286 rule that --numeric-ids alone never activates
|
||||
* ownership (it is only a mapping modifier). */
|
||||
static void test_fake_super_storage_resolution() {
|
||||
uint32_t uid = 0, gid = 0;
|
||||
|
||||
/* --numeric-ids alone is INERT: no ownership request, storage unchanged. */
|
||||
Config* c = config_create();
|
||||
FileAttrPolicy policy = {true, true, false, false};
|
||||
EXPECT_NOT_NULL(c);
|
||||
struct stat st;
|
||||
c->numeric_ids = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_FALSE(identity_active_enabled());
|
||||
EXPECT_FALSE(identity_owner_requested());
|
||||
EXPECT_FALSE(identity_group_requested());
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 12345);
|
||||
EXPECT_EQ_INT((int)gid, 6789);
|
||||
config_delete(c);
|
||||
|
||||
/* -o only: the owner is applied, the group stays at its current value (0). */
|
||||
/* --fake-super with no ownership request records the raw source ids. */
|
||||
c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->fake_super = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 12345);
|
||||
EXPECT_EQ_INT((int)gid, 6789);
|
||||
|
||||
/* -o + --numeric-ids: raw owner, un-requested group stays the source gid. */
|
||||
c->preserve_owner = true;
|
||||
c->preserve_group = false;
|
||||
c->numeric_ids = true;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 12345);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 0);
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 12345);
|
||||
EXPECT_EQ_INT((int)gid, 6789);
|
||||
|
||||
/* -g only: the group is applied, the owner stays at its current value (0). */
|
||||
EXPECT_EQ_INT(fchown(fd, 0, 0), 0);
|
||||
c->preserve_owner = false;
|
||||
c->preserve_group = true;
|
||||
/* --chown overrides both sides. */
|
||||
c->chown_uid_set = true;
|
||||
c->chown_uid = 777;
|
||||
c->chown_gid_set = true;
|
||||
c->chown_gid = 778;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
EXPECT_TRUE(fake_super_restore_fd(fd, policy));
|
||||
EXPECT_EQ_INT(fstat(fd, &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_uid, 0);
|
||||
EXPECT_EQ_INT((int)st.st_gid, 12346);
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 777);
|
||||
EXPECT_EQ_INT((int)gid, 778);
|
||||
|
||||
/* A usermap match beats --chown on the owner side only. */
|
||||
c->usermap_count = 1;
|
||||
c->usermap = calloc(1, sizeof(IdentityMap));
|
||||
EXPECT_NOT_NULL(c->usermap);
|
||||
c->usermap[0].from = IDENTITY_MATCH_ANY;
|
||||
c->usermap[0].to = 999;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 999);
|
||||
EXPECT_EQ_INT((int)gid, 778);
|
||||
|
||||
/* --copy-as is authoritative for both sides. */
|
||||
c->copy_as_set = true;
|
||||
c->copy_as_uid = 111;
|
||||
c->copy_as_gid = 222;
|
||||
EXPECT_TRUE(identity_set_active(c));
|
||||
identity_resolve_storage_ids(12345, 6789, &uid, &gid);
|
||||
EXPECT_EQ_INT((int)uid, 111);
|
||||
EXPECT_EQ_INT((int)gid, 222);
|
||||
|
||||
identity_clear_active();
|
||||
config_delete(c);
|
||||
close(fd);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* xattr_list_clone deep-copies names/values (used by the deferred directory
|
||||
* metadata accumulator), so the clone stays valid after the original is freed. */
|
||||
static void test_xattr_list_clone() {
|
||||
EXPECT_NULL(xattr_list_clone(NULL));
|
||||
FileXattrList* list = xattr_list_new();
|
||||
EXPECT_NOT_NULL(list);
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.a", "1", 1));
|
||||
EXPECT_TRUE(xattr_list_append(list, "user.b", "22", 2));
|
||||
FileXattrList* clone = xattr_list_clone(list);
|
||||
EXPECT_NOT_NULL(clone);
|
||||
EXPECT_EQ_INT(clone->count, 2);
|
||||
EXPECT_EQ_STR(clone->items[0].name, "user.a");
|
||||
EXPECT_EQ_INT((int)clone->items[1].value_len, 2);
|
||||
EXPECT_TRUE(memcmp(clone->items[1].value, "22", 2) == 0);
|
||||
EXPECT_TRUE(clone->items[0].name != list->items[0].name);
|
||||
xattr_list_free(list);
|
||||
EXPECT_EQ_STR(clone->items[0].name, "user.a");
|
||||
xattr_list_free(clone);
|
||||
}
|
||||
|
||||
void test_xattr() {
|
||||
test_xattr_list_clone();
|
||||
test_xattr_wire_roundtrip();
|
||||
test_xattr_reject_privileged_namespace();
|
||||
test_xattr_reject_oversized_value();
|
||||
@@ -547,6 +551,6 @@ void test_xattr() {
|
||||
test_xattr_receive_drops_acl_without_preserve_acls();
|
||||
test_link_copy_fallback_preserves_xattrs();
|
||||
test_fake_super_restore();
|
||||
test_fake_super_owner_gate();
|
||||
test_fake_super_owner_group_split();
|
||||
test_fake_super_no_real_chown();
|
||||
test_fake_super_storage_resolution();
|
||||
}
|
||||
Reference in New Issue
Block a user