aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-12 20:25:15 +0000
committertv <tv@krebsco.de>2026-11-13 03:24:27 +0100
commit1880ceb3544de8cf194163394c8291b60924a2e6 (patch)
treeb932631e5ded3a8a2bf99d8f0e9fb738b9b29453 /tests
parent08500cfcbb91cda09c6790b916ba97c333cf2df9 (diff)
populate: cope with unusual names and paths
Names and paths with characters that mean something to shells, to find, or in URLs get handled as what they are, or refused: - Pruning, and removing state that sources don't need anymore, list entries with find instead of a loop reading names, which strips blanks at the ends of names and splits names at newlines. So an entry named "trail " gets pruned instead of reported as pruned and left. - Pruning doesn't make entries writable before removing them, which would change the mode of files elsewhere that are hard links to files therein. - Source names are portable file names: letters, digits, and ._-, not starting with -. nixpkgs' writers accept nothing else in the names of the scripts populating the sources, which contain the source names, and fail with an assertion that doesn't mention the source. - Paths get percent-encoded where they go into URLs for Nix, which refuses blanks there and takes ? and # for delimiters. - rsync gets the target path protected from the target's shell (--protect-args), which expands wildcards in it otherwise: a file source delivered to /var/src/a?c ends up in /var/src/abc if that exists. A safety scenario deploys every kind of state to a target path with a blank, wildcards, #, and %. An edge case prunes entries with blanks at the ends of their names, with a newline, and with wildcards, and the scenario pruning links also prunes hard links to a read-only file outside, and lists what's left in the C locale, where ls sorts .populate first. The scenario with unusual source names also draws inputs from a source named self.
Diffstat (limited to 'tests')
-rw-r--r--tests/scenarios.nix65
1 files changed, 58 insertions, 7 deletions
diff --git a/tests/scenarios.nix b/tests/scenarios.nix
index 3113586..849d3e3 100644
--- a/tests/scenarios.nix
+++ b/tests/scenarios.nix
@@ -1321,6 +1321,20 @@ assert !(krops.isLocalTarget (targets.remote "/"));
}
];
}))
+ # Pruning entries whose names a shell's read would strip or split.
+ (s "prune-names" {
+ prepare = ''
+ mkdir -p "$T/ lead" "$T/trail " "$T/$(printf 'new\nline')" "$T/axb" "$T/[b]"
+ '';
+ phases = [{
+ sources = alt;
+ prune = true;
+ check = ''
+ # In the C locale, ls sorts by bytes, so . comes first.
+ expect_eq entries "$(LC_ALL=C ls -A "$T" | tr '\n' ' ')" '.populate alt '
+ '';
+ }];
+ })
# A signal ending populate leaves neither the ssh master nor the
# temporary directory.
(s "interrupt" {
@@ -1414,14 +1428,23 @@ assert !(krops.isLocalTarget (targets.remote "/"));
}
];
})
- (modes "source-name" ({ draw, flake, ... }: {
+ # Source names that aren't input names, or that Nix has another use
+ # for, both of sources inputs get drawn from and of sources drawing them.
+ (modes "source-name" ({ draw, flake, ... }: let
+ x = from: { path = "/fixtures/filesrc"; } // draw { dep.follows = from; };
+ in {
phases = twice {
sources = {
"alt-1.0".file = "/fixtures/alt";
- x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "alt-1.0"; };
+ self.file = "/fixtures/alt";
+ x.file = x "alt-1.0";
+ y.file = x "self";
+ "a.b_c-d".file = x "self";
};
check = ''
- expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub
+ expect_eq x "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub
+ expect_eq y "$(nix_eval_online ${flake "y"}#msg)" top:alt,sub
+ expect_eq a.b_c-d "$(nix_eval_online ${flake "a.b_c-d"}#msg)" top:alt,sub
'';
};
}))
@@ -1661,13 +1684,16 @@ assert !(krops.isLocalTarget (targets.remote "/"));
])
# Symlinks to outside among what populate removes: leftovers of
# interrupted runs, state of sources that are gone, and, when pruning,
- # entries of the target directory. They get removed as links.
+ # entries of the target directory. They get removed as links. So do
+ # hard links to a read-only file outside, whose mode stays.
(s "leftovers" {
isolate = true;
prepare = ''
- mkdir -p "$T"
+ mkdir -p "$T"/hard
ln -s ${outside}/dir "$T"/other
ln -s ${outside}/repo "$T"/repo
+ ln ${outside}/dir/sub/file "$T"/hard/link
+ ln ${outside}/dir/sub/file "$T"/hardlink
'';
phases = [
{
@@ -1697,13 +1723,38 @@ assert !(krops.isLocalTarget (targets.remote "/"));
sources = stateful;
prune = true;
check = ''
- for name in other repo .populate/flake-inputs/gone .populate/flake-uploads; do
+ for name in other repo hard hardlink .populate/flake-inputs/gone .populate/flake-uploads; do
test ! -L "$T"/$name && test ! -e "$T"/$name || fail "$name not pruned"
done
'';
}
];
})
+ # A target path with characters that mean something to shells and in
+ # URLs.
+ (s "odd-target" (let
+ targetPath = "/var/krops/odd-target-${targetKind}/a b*c?d#e%f";
+ in {
+ isolate = true;
+ phases = twice {
+ sources = stateful // {
+ g.git = { url = gitUrl; ref = gitHead; };
+ p.pipe = "/fixtures/pipe";
+ };
+ inherit targetPath;
+ prune = true;
+ check = ''
+ D=${lib.escapeShellArg targetPath}
+ test -e "$D"/alt/flake.nix || fail "alt missing"
+ expect_eq HEAD "$(git -C "$D"/g rev-parse HEAD)" ${gitHead}
+ expect_eq p "$(cat "$D"/p)" piped
+ expect_eq entries "$(ls "$T")" 'a b*c?d#e%f'
+ gc
+ expect_eq greeting "$(nix_eval "$(readlink -f "$D"/x)"#greeting)" hello+alt
+ expect_eq msg "$(nix_eval "$(readlink -f "$D"/y)"#msg)" top:dep+inner,sub
+ '';
+ };
+ }))
# The target directory itself may be a symlink: what it points to is the
# target directory then.
(s "linked-target" {
@@ -1757,7 +1808,7 @@ assert !(krops.isLocalTarget (targets.remote "/"));
expect = "eval-error";
control = { ok.file = "/fixtures/alt"; };
})
- [ "" "." ".." ".populate" "a/b" "../x" "/etc" "a\nb" ];
+ [ "" "." ".." ".populate" "a/b" "../x" "/etc" "a\nb" "a b" "a*b" "a?b" "a+b" "a=b" "-a" ];
})
# Target paths that aren't absolute, that are the root directory, or
# that resolve to another directory than they name.