mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
nixos/lib/utils: fix escapeSystemdPath for paths with "." segments
escapeSystemdPath relied on lib.strings.normalizePath, which only
collapses duplicate slashes and does not drop "." path components the
way systemd's path_simplify() does. A path like /mnt/./foo was thus
escaped to mnt-.-foo instead of mnt-foo, and systemd refused the
generated unit ("Where= setting doesn't match unit name").
Reimplement the simplification to match `systemd-escape --path` exactly:
drop "." and empty components, collapse a leading ".." run in an
absolute path to the root, and throw on any input systemd-escape
rejects (a non-leading "..", or a relative path that reduces to
nothing). Parity is checked against the systemd-escape binary over a
battery of edge cases and fuzzed inputs. While here, hoist the constant
escape table and partial applications out of the per-call lambda so
they are built once rather than on every call, making the function
~2.5x faster.
Resolves: https://github.com/NixOS/nixpkgs/issues/515270
Assisted-by: Claude:claude-opus-4-8
(cherry picked from commit 8f68c76d48)
This commit is contained in:
parent
bb6ef72304
commit
919ca6fd5a
2 changed files with 62 additions and 14 deletions
|
|
@ -14,6 +14,7 @@ let
|
|||
escapeShellArg
|
||||
filter
|
||||
flatten
|
||||
foldl'
|
||||
getName
|
||||
hasPrefix
|
||||
hasSuffix
|
||||
|
|
@ -31,13 +32,13 @@ let
|
|||
nameValuePair
|
||||
optionalString
|
||||
removePrefix
|
||||
removeSuffix
|
||||
replaceStrings
|
||||
splitString
|
||||
stringToCharacters
|
||||
types
|
||||
;
|
||||
|
||||
inherit (lib.strings) toJSON normalizePath escapeC;
|
||||
inherit (lib.strings) toJSON escapeC;
|
||||
in
|
||||
|
||||
let
|
||||
|
|
@ -98,26 +99,60 @@ let
|
|||
|| hasPrefix a'.mountPoint b'.mountPoint
|
||||
|| any (hasPrefix a'.mountPoint) b'.depends;
|
||||
|
||||
# Escape a path according to the systemd rules. FIXME: slow
|
||||
# Escape a path according to the systemd rules.
|
||||
# The rules are described in systemd.unit(5) as follows:
|
||||
# The escaping algorithm operates as follows: given a string, any "/" character is replaced by "-", and all other characters which are not ASCII alphanumerics, ":", "_" or "." are replaced by C-style "\x2d" escapes. In addition, "." is replaced with such a C-style escape when it would appear as the first character in the escaped string.
|
||||
# When the input qualifies as absolute file system path, this algorithm is extended slightly: the path to the root directory "/" is encoded as single dash "-". In addition, any leading, trailing or duplicate "/" characters are removed from the string before transformation. Example: /foo//bar/baz/ becomes "foo-bar-baz".
|
||||
escapeSystemdPath =
|
||||
s:
|
||||
let
|
||||
# These don't depend on the path being escaped, so build them once
|
||||
# rather than on every call.
|
||||
escapeChar = escapeC (stringToCharacters " !\"#$%&'()*+,;<=>?@[\\]^`{|}~-");
|
||||
escapeLeadingDot = escapeC [ "." ] ".";
|
||||
slashesToDashes = replaceStrings [ "/" ] [ "-" ];
|
||||
replacePrefix =
|
||||
p: r: s:
|
||||
(if (hasPrefix p s) then r + (removePrefix p s) else s);
|
||||
trim = s: removeSuffix "/" (removePrefix "/" s);
|
||||
normalizedPath = normalizePath s;
|
||||
(if hasPrefix p s then r + removePrefix p s else s);
|
||||
in
|
||||
replaceStrings [ "/" ] [ "-" ] (
|
||||
replacePrefix "." (escapeC [ "." ] ".") (
|
||||
escapeC (stringToCharacters " !\"#$%&'()*+,;<=>=@[\\]^`{|}~-") (
|
||||
if normalizedPath == "/" then normalizedPath else trim normalizedPath
|
||||
)
|
||||
)
|
||||
);
|
||||
s:
|
||||
let
|
||||
isAbsolute = hasPrefix "/" s;
|
||||
# path_simplify(): collapse duplicate slashes and drop "." components.
|
||||
rawComponents = filter (c: c != "" && c != ".") (splitString "/" s);
|
||||
# systemd accepts ".." only where it is redundant: a leading ".." in an
|
||||
# absolute path refers to the root's parent, i.e. the root itself, and is
|
||||
# dropped. Any other ".." cannot be resolved without the filesystem, so
|
||||
# the path is not normalized and systemd-escape errors on it.
|
||||
simplified =
|
||||
foldl'
|
||||
(
|
||||
acc: c:
|
||||
if c == ".." then
|
||||
# A leading ".." in an absolute path is the only redundant case.
|
||||
if isAbsolute && acc.components == [ ] then acc else acc // { normalized = false; }
|
||||
else
|
||||
acc // { components = acc.components ++ [ c ]; }
|
||||
)
|
||||
{
|
||||
components = [ ];
|
||||
normalized = true;
|
||||
}
|
||||
rawComponents;
|
||||
notNormalized = throw "escapeSystemdPath: ${s} is not a normalized path";
|
||||
simplifiedPath =
|
||||
if !simplified.normalized then
|
||||
notNormalized
|
||||
else if simplified.components != [ ] then
|
||||
concatStringsSep "/" simplified.components
|
||||
# The root directory, and - matching systemd-escape - the empty string.
|
||||
else if isAbsolute || s == "" then
|
||||
"/"
|
||||
# A relative path that reduces to nothing (e.g. "."), which has no
|
||||
# valid escaping.
|
||||
else
|
||||
notNormalized;
|
||||
in
|
||||
slashesToDashes (replacePrefix "." escapeLeadingDot (escapeChar simplifiedPath));
|
||||
|
||||
# Quotes an argument for use in Exec* service lines.
|
||||
# systemd accepts "-quoted strings with escape sequences, toJSON produces
|
||||
|
|
|
|||
|
|
@ -36,6 +36,19 @@ in
|
|||
assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ null ])).success;
|
||||
assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ false ])).success;
|
||||
assert !(builtins.tryEval (utils.escapeSystemdExecArgs [ (_: _) ])).success;
|
||||
# escapeSystemdPath simplifies the path like systemd-escape --path does:
|
||||
# "." components are dropped and duplicate/leading/trailing slashes removed.
|
||||
assert utils.escapeSystemdPath "/mnt/./foo" == "mnt-foo";
|
||||
assert utils.escapeSystemdPath "/foo//bar/baz/" == "foo-bar-baz";
|
||||
assert utils.escapeSystemdPath "/" == "-";
|
||||
assert utils.escapeSystemdPath "" == "-";
|
||||
assert utils.escapeSystemdPath "/.hidden/x" == "\\x2ehidden-x";
|
||||
assert utils.escapeSystemdPath "/foo?bar" == "foo\\x3fbar";
|
||||
# A leading ".." in an absolute path is the root's parent, i.e. the root.
|
||||
assert utils.escapeSystemdPath "/../foo" == "foo";
|
||||
# Non-normalized paths can't be escaped, matching systemd-escape.
|
||||
assert !(builtins.tryEval (utils.escapeSystemdPath "/mnt/../foo")).success;
|
||||
assert !(builtins.tryEval (utils.escapeSystemdPath ".")).success;
|
||||
{
|
||||
description = "Echo to the journal";
|
||||
serviceConfig.Type = "oneshot";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue