about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Hensing <roberth@users.noreply.github.com>2023-06-21 13:36:25 +0200
committerGitHub <noreply@github.com>2023-06-21 13:36:25 +0200
commit43be38bd783d957475c714a07c63a32fbe25a5fc (patch)
tree2740db82ef4ff0d2d50f549a8b0255f0b2e63742
parent4b9dcab76aa3710ad184f85b75f6ce9df91924a9 (diff)
parent592213ad3f4de2cf3a0f13ab8271122e5e9f9822 (diff)
downloadnixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar.gz
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar.bz2
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar.lz
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar.xz
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.tar.zst
nixlib-43be38bd783d957475c714a07c63a32fbe25a5fc.zip
Merge pull request #237610 from tweag/lib.path.hasPrefix
`lib.path.hasPrefix`: init
-rw-r--r--lib/path/default.nix69
-rw-r--r--lib/path/tests/unit.nix19
-rw-r--r--lib/strings.nix3
3 files changed, 89 insertions, 2 deletions
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.