From 866f75e5b9579a4c4f6391b68c2a31633ab13e30 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Wed, 5 Apr 2023 20:27:46 +0200 Subject: [PATCH 1/2] lib.path.append: Add a law With removePrefix introduced in a future commit this law can then be used to derive removePrefix p (append p s) == subpath.normalise s => (wrap with append) append p (removePrefix p (append p s)) == append p (subpath.normalise s) => (append is not influenced by subpath normalisation) append p (removePrefix p (append p s)) == append p s => (substitute q = append p s) append p (removePrefix p q) == q Not included in the docs because it's not that important, just shows that the first statement is more general than the second one (because this derivation doesn't work the other way) --- lib/path/default.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/path/default.nix b/lib/path/default.nix index a4a08668ae62..442060c18643 100644 --- a/lib/path/default.nix +++ b/lib/path/default.nix @@ -108,6 +108,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 From 592213ad3f4de2cf3a0f13ab8271122e5e9f9822 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Wed, 5 Apr 2023 20:31:50 +0200 Subject: [PATCH 2/2] lib.path.hasPrefix: init --- lib/path/default.nix | 63 +++++++++++++++++++++++++++++++++++++++++ lib/path/tests/unit.nix | 19 ++++++++++++- lib/strings.nix | 3 +- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/path/default.nix b/lib/path/default.nix index 442060c18643..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. @@ -155,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.