diff --git a/lib/path/default.nix b/lib/path/default.nix index 3a871bc05283c..24a7f85affc1d 100644 --- a/lib/path/default.nix +++ b/lib/path/default.nix @@ -271,8 +271,57 @@ in /* No rec! Add dependencies on this file at the top. */ { second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"''; joinRelPath components; + /* + Split the filesystem root from a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path). + The result is an attribute set with these attributes: + - `root`: The filesystem root of the path, meaning that this directory has no parent directory. + - `subpath`: The [normalised subpath string](#function-library-lib.path.subpath.normalise) that when [appended](#function-library-lib.path.append) to `root` returns the original path. + + Laws: + - [Appending](#function-library-lib.path.append) the `root` and `subpath` gives the original path: + + p == + append + (splitRoot p).root + (splitRoot p).subpath + + - Trying to get the parent directory of `root` using [`readDir`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-readDir) returns `root` itself: + + dirOf (splitRoot p).root == (splitRoot p).root + + Type: + splitRoot :: Path -> { root :: Path, subpath :: String } + + Example: + splitRoot /foo/bar + => { root = /.; subpath = "./foo/bar"; } + + splitRoot /. + => { root = /.; subpath = "./."; } + + # Nix neutralises `..` path components for all path values automatically + splitRoot /foo/../bar + => { root = /.; subpath = "./bar"; } + + splitRoot "/foo/bar" + => + */ + splitRoot = path: + assert assertMsg + (isPath path) + "lib.path.splitRoot: Argument is of type ${typeOf path}, but a path was expected"; + let + deconstructed = deconstructPath path; + in { + root = deconstructed.root; + subpath = joinRelPath deconstructed.components; + }; + /* Whether a value is a valid subpath string. + A subpath string points to a specific file or directory within an absolute base directory. + It is a stricter form of a relative path that excludes `..` components, since those could escape the base directory. + - The value is a string - The string is not empty diff --git a/lib/path/tests/unit.nix b/lib/path/tests/unit.nix index 3e4b216f099ff..8bfb6f201219f 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) hasPrefix removePrefix append subpath; + inherit (lib.path) hasPrefix removePrefix append splitRoot subpath; cases = lib.runTests { # Test examples from the lib.path.append documentation @@ -74,6 +74,23 @@ let expected = "./foo"; }; + testSplitRootExample1 = { + expr = splitRoot /foo/bar; + expected = { root = /.; subpath = "./foo/bar"; }; + }; + testSplitRootExample2 = { + expr = splitRoot /.; + expected = { root = /.; subpath = "./."; }; + }; + testSplitRootExample3 = { + expr = splitRoot /foo/../bar; + expected = { root = /.; subpath = "./bar"; }; + }; + testSplitRootExample4 = { + expr = (builtins.tryEval (splitRoot "/foo/bar")).success; + expected = false; + }; + # Test examples from the lib.path.subpath.isValid documentation testSubpathIsValidExample1 = { expr = subpath.isValid null;