diff --git a/lib/path/default.nix b/lib/path/default.nix index a4a08668ae62..936e9b030253 100644 --- a/lib/path/default.nix +++ b/lib/path/default.nix @@ -7,6 +7,7 @@ let isPath split match + typeOf ; inherit (lib.lists) @@ -18,6 +19,7 @@ let all concatMap foldl' + take ; inherit (lib.strings) @@ -100,6 +102,22 @@ let # An empty string is not a valid relative path, so we need to return a `.` when we have no components (if components == [] then "." else concatStringsSep "/" components); + # Type: Path -> { root :: Path, components :: [ String ] } + # + # Deconstruct a path value type into: + # - root: The filesystem root of the path, generally `/` + # - components: All the path's components + # + # This is similar to `splitString "/" (toString path)` but safer + # because it can distinguish different filesystem roots + deconstructPath = + let + recurse = components: base: + # If the parent of a path is the path itself, then it's a filesystem root + if base == dirOf base then { root = base; inherit components; } + else recurse ([ (baseNameOf base) ] ++ components) (dirOf base); + in recurse []; + in /* No rec! Add dependencies on this file at the top. */ { /* Append a subpath string to a path. @@ -108,6 +126,12 @@ in /* No rec! Add dependencies on this file at the top. */ { More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"), and that the second argument is a valid subpath string (see `lib.path.subpath.isValid`). + Laws: + + - Not influenced by subpath normalisation + + append p s == append p (subpath.normalise s) + Type: append :: Path -> String -> Path @@ -149,6 +173,51 @@ in /* No rec! Add dependencies on this file at the top. */ { ${subpathInvalidReason subpath}''; path + ("/" + subpath); + /* + Whether the first path is a component-wise prefix of the second path. + + Laws: + + - `hasPrefix p q` is only true if `q == append p s` for some subpath `s`. + + - `hasPrefix` is a [non-strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Non-strict_partial_order) over the set of all path values + + Type: + hasPrefix :: Path -> Path -> Bool + + Example: + hasPrefix /foo /foo/bar + => true + hasPrefix /foo /foo + => true + hasPrefix /foo/bar /foo + => false + hasPrefix /. /foo + => true + */ + hasPrefix = + path1: + assert assertMsg + (isPath path1) + "lib.path.hasPrefix: First argument is of type ${typeOf path1}, but a path was expected"; + let + path1Deconstructed = deconstructPath path1; + in + path2: + assert assertMsg + (isPath path2) + "lib.path.hasPrefix: Second argument is of type ${typeOf path2}, but a path was expected"; + let + path2Deconstructed = deconstructPath path2; + in + assert assertMsg + (path1Deconstructed.root == path2Deconstructed.root) '' + lib.path.hasPrefix: Filesystem roots must be the same for both paths, but paths with different roots were given: + first argument: "${toString path1}" with root "${toString path1Deconstructed.root}" + second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"''; + take (length path1Deconstructed.components) path2Deconstructed.components == path1Deconstructed.components; + + /* Whether a value is a valid subpath string. - The value is a string diff --git a/lib/path/tests/unit.nix b/lib/path/tests/unit.nix index 61c4ab4d6f2e..9c5b752cf64a 100644 --- a/lib/path/tests/unit.nix +++ b/lib/path/tests/unit.nix @@ -3,7 +3,7 @@ { libpath }: let lib = import libpath; - inherit (lib.path) append subpath; + inherit (lib.path) hasPrefix append subpath; cases = lib.runTests { # Test examples from the lib.path.append documentation @@ -40,6 +40,23 @@ let expected = false; }; + testHasPrefixExample1 = { + expr = hasPrefix /foo /foo/bar; + expected = true; + }; + testHasPrefixExample2 = { + expr = hasPrefix /foo /foo; + expected = true; + }; + testHasPrefixExample3 = { + expr = hasPrefix /foo/bar /foo; + expected = false; + }; + testHasPrefixExample4 = { + expr = hasPrefix /. /foo; + expected = true; + }; + # Test examples from the lib.path.subpath.isValid documentation testSubpathIsValidExample1 = { expr = subpath.isValid null; diff --git a/lib/strings.nix b/lib/strings.nix index e875520c6858..bb07f40d7a55 100644 --- a/lib/strings.nix +++ b/lib/strings.nix @@ -264,7 +264,8 @@ rec { lib.strings.hasPrefix: The first argument (${toString pref}) is a path value, but only strings are supported. There is almost certainly a bug in the calling code, since this function always returns `false` in such a case. This function also copies the path to the Nix store, which may not be what you want. - This behavior is deprecated and will throw an error in the future.'' + This behavior is deprecated and will throw an error in the future. + You might want to use `lib.path.hasPrefix` instead, which correctly supports paths.'' (substring 0 (stringLength pref) str == pref); /* Determine whether a string has given suffix.