From 1ab4473cddcf8c95956b22c3e6d5c9720a62925e Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 12 Nov 2026 19:59:04 +0000 Subject: populate file: check the .rsync-filter files that really apply rsync reads .rsync-filter files in the directories above a source as well, and their rules apply to the transfer. The check for files that rsync 3.5.0 and later can't read only starts if the source itself contains one, so a subdirectory of a repository with a filter file at its top gets synced without it, silently. The check also starts for filter files above the source. The check's dry run is the transfer it stands for, as far as filtering goes: - It gets the source's exclude and filters, so it doesn't enter directories the transfer skips. - It doesn't run for transfers that apply no .rsync-filter files, i.e. uploads of flakes and patches. - A filter file the user may not read anyway doesn't count: rsync fails on its own when it gets to transfer that file, and --insecure-links changes nothing about it. - rsync prints bytes it takes for unprintable as \#ooo, which depends on the locale. File names get decoded before checking that they exist, so files below, e.g., directories with umlauts in their names don't pass for missing in the C locale. - If rsync doesn't tell which filter files it reads at all, populate warns that it can't tell either. The README mentions that --insecure-links also affects the way to the target path on local targets, and that only rsync 3.5.0 ignores filter files below directories that may be entered but not read. Phases of scenarios can set environment variables for populate. New file scenarios sync a subdirectory whose filter file is above it, and a source with a filter file below a directory with an umlaut in its name, in the C locale. --- tests/fixtures.sh | 10 ++++++++++ tests/scenarios.nix | 45 ++++++++++++++++++++++++++++++++++++++++++--- tests/vm.nix | 2 +- 3 files changed, 53 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/fixtures.sh b/tests/fixtures.sh index d9d8b2b..4e794db 100755 --- a/tests/fixtures.sh +++ b/tests/fixtures.sh @@ -461,3 +461,13 @@ echo keep > /fixtures/filtered-parent/sub/keep echo garbage > /fixtures/filtered-parent/sub/garbage ln -s filtered-parent /fixtures/filtered-parent-link chown -h nobody /fixtures/filtered-parent-link + +# filtered-name, filtered-name-link: like filtered, but the filter file is in +# a subdirectory whose name has a byte that rsync prints escaped in the C +# locale. +mkdir -p /fixtures/filtered-name/bär +printf -- '- garbage\n' > /fixtures/filtered-name/bär/.rsync-filter +echo keep > /fixtures/filtered-name/bär/keep +echo garbage > /fixtures/filtered-name/bär/garbage +ln -s filtered-name /fixtures/filtered-name-link +chown -h nobody /fixtures/filtered-name-link diff --git a/tests/scenarios.nix b/tests/scenarios.nix index 7743482..2fb6609 100644 --- a/tests/scenarios.nix +++ b/tests/scenarios.nix @@ -96,10 +96,11 @@ let # pass for failing for an unrelated reason, e.g. a misspelt option. # targetPath deploys to another path than the scenario's target # directory, and controlTargetPath is the control for a path that makes - # evaluation fail. + # evaluation fail. env sets environment variables for populate, as + # arguments of env. phase = { sources, backup ? false, prune ? false, deploy ? null, - targetPath ? path, + targetPath ? path, env ? "", expect ? "success", control ? null, controlTargetPath ? null, check ? "", mutate ? null, checkDeployer ? null, mutateDeployer ? null, }: let @@ -134,6 +135,7 @@ let null else { populate = toString (command sources targetPath); + inherit env; expect = if expect == "success" then null else expect.fail; # No phase gets around checking the canaries. check = script (check + "\ncanary_check\n"); @@ -337,7 +339,44 @@ assert !(krops.isLocalTarget (targets.remote "/")); ''} ''; })); - }) [ null true false ]) targetKinds; + }) [ null true false ]) targetKinds + ++ concatMap (targetKind: let + affected = lib.versionAtLeast rsyncVersion "3.5.0"; + unreadable = name: { path, file, dir ? "x", env ? "" }: scenario { + name = "file-${targetKind}-insecureLinks${name}"; + inherit targetKind; + prepare = '' + mkdir -p "$T"/${dir} + echo old > "$T"/${dir}/garbage + ''; + phases = twice ({ + sources.x.file = { inherit path; }; + inherit env; + } // (if affected then { + expect.fail = "can't read ${file}, so it would sync ${path}/ without it."; + } else { + check = '' + expect_eq keep "$(cat "$T"/${dir}/keep)" keep + test ! -e "$T"/${dir}/garbage || fail "garbage not excluded" + ''; + })); + }; + in [ + # The filter file is in the directory above what gets synced, where it + # applies as well. + (unreadable "Parent" { + path = "/fixtures/filtered-parent-link/sub"; + file = "/fixtures/filtered-parent-link/.rsync-filter"; + }) + # The filter file is in a directory whose name rsync prints escaped + # in the C locale. + (unreadable "Name" { + path = "/fixtures/filtered-name-link"; + file = "/fixtures/filtered-name-link/bär/.rsync-filter"; + dir = "x/bär"; + env = "LC_ALL=C"; + }) + ]) targetKinds; flake = matrix "flake" { kind = [ "git" "path" "subdir" "remote" ]; diff --git a/tests/vm.nix b/tests/vm.nix index 5eb4dbb..014897a 100644 --- a/tests/vm.nix +++ b/tests/vm.nix @@ -152,7 +152,7 @@ in pkgs.testers.runNixOSTest { for i, phase in enumerate(scenario["phases"]): # A hang fails the scenario, not the whole test. status, out = deployer.execute( - f"HOME=/root timeout 600 {phase['populate']} 2>&1", + f"env HOME=/root {phase['env']} timeout 600 {phase['populate']} 2>&1", timeout=900, ) if phase["expect"] is None: -- cgit v1.3.1