summaryrefslogtreecommitdiffstats
path: root/lib/path/default.nix
diff options
context:
space:
mode:
authorSilvan Mosberger <silvan.mosberger@tweag.io>2022-12-23 21:07:30 +0100
committerSilvan Mosberger <silvan.mosberger@tweag.io>2023-01-03 13:21:03 +0100
commit63dd6d20db8ac4b5576b48a58fe434319791a7d7 (patch)
treec71013a5372f5ba3fde90f45e3f1f7ee9356ea3e /lib/path/default.nix
parent98fbcf17888872f5ebdf9fb6247266929f4308db (diff)
lib.path.subpath.normalise: init
Diffstat (limited to 'lib/path/default.nix')
-rw-r--r--lib/path/default.nix143
1 files changed, 143 insertions, 0 deletions
diff --git a/lib/path/default.nix b/lib/path/default.nix
index 59f670dfed0f..96a9244407bf 100644
--- a/lib/path/default.nix
+++ b/lib/path/default.nix
@@ -4,10 +4,20 @@ let
inherit (builtins)
isString
+ split
match
;
+ inherit (lib.lists)
+ length
+ head
+ last
+ genList
+ elemAt
+ ;
+
inherit (lib.strings)
+ concatStringsSep
substring
;
@@ -28,6 +38,60 @@ let
"The given string \"${value}\" contains a `..` component, which is not allowed in subpaths"
else null;
+ # Split and normalise a relative path string into its components.
+ # Error for ".." components and doesn't include "." components
+ splitRelPath = path:
+ let
+ # Split the string into its parts using regex for efficiency. This regex
+ # matches patterns like "/", "/./", "/././", with arbitrarily many "/"s
+ # together. These are the main special cases:
+ # - Leading "./" gets split into a leading "." part
+ # - Trailing "/." or "/" get split into a trailing "." or ""
+ # part respectively
+ #
+ # These are the only cases where "." and "" parts can occur
+ parts = split "/+(\\./+)*" path;
+
+ # `split` creates a list of 2 * k + 1 elements, containing the k +
+ # 1 parts, interleaved with k matches where k is the number of
+ # (non-overlapping) matches. This calculation here gets the number of parts
+ # back from the list length
+ # floor( (2 * k + 1) / 2 ) + 1 == floor( k + 1/2 ) + 1 == k + 1
+ partCount = length parts / 2 + 1;
+
+ # To assemble the final list of components we want to:
+ # - Skip a potential leading ".", normalising "./foo" to "foo"
+ # - Skip a potential trailing "." or "", normalising "foo/" and "foo/." to
+ # "foo". See ./path.md#trailing-slashes
+ skipStart = if head parts == "." then 1 else 0;
+ skipEnd = if last parts == "." || last parts == "" then 1 else 0;
+
+ # We can now know the length of the result by removing the number of
+ # skipped parts from the total number
+ componentCount = partCount - skipEnd - skipStart;
+
+ in
+ # Special case of a single "." path component. Such a case leaves a
+ # componentCount of -1 due to the skipStart/skipEnd not verifying that
+ # they don't refer to the same character
+ if path == "." then []
+
+ # Generate the result list directly. This is more efficient than a
+ # combination of `filter`, `init` and `tail`, because here we don't
+ # allocate any intermediate lists
+ else genList (index:
+ # To get to the element we need to add the number of parts we skip and
+ # multiply by two due to the interleaved layout of `parts`
+ elemAt parts ((skipStart + index) * 2)
+ ) componentCount;
+
+ # Join relative path components together
+ joinRelPath = components:
+ # Always return relative paths with `./` as a prefix (./path.md#leading-dots-for-relative-paths)
+ "./" +
+ # 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);
+
in /* No rec! Add dependencies on this file at the top. */ {
@@ -72,4 +136,83 @@ in /* No rec! Add dependencies on this file at the top. */ {
subpath.isValid = value:
subpathInvalidReason value == null;
+
+ /* Normalise a subpath. Throw an error if the subpath isn't valid, see
+ `lib.path.subpath.isValid`
+
+ - Limit repeating `/` to a single one
+
+ - Remove redundant `.` components
+
+ - Remove trailing `/` and `/.`
+
+ - Add leading `./`
+
+ Laws:
+
+ - (Idempotency) Normalising multiple times gives the same result:
+
+ subpath.normalise (subpath.normalise p) == subpath.normalise p
+
+ - (Uniqueness) There's only a single normalisation for the paths that lead to the same file system node:
+
+ subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
+
+ - Don't change the result when appended to a Nix path value:
+
+ base + ("/" + p) == base + ("/" + subpath.normalise p)
+
+ - Don't change the path according to `realpath`:
+
+ $(realpath ${p}) == $(realpath ${subpath.normalise p})
+
+ - Only error on invalid subpaths:
+
+ builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
+
+ Type:
+ subpath.normalise :: String -> String
+
+ Example:
+ # limit repeating `/` to a single one
+ subpath.normalise "foo//bar"
+ => "./foo/bar"
+
+ # remove redundant `.` components
+ subpath.normalise "foo/./bar"
+ => "./foo/bar"
+
+ # add leading `./`
+ subpath.normalise "foo/bar"
+ => "./foo/bar"
+
+ # remove trailing `/`
+ subpath.normalise "foo/bar/"
+ => "./foo/bar"
+
+ # remove trailing `/.`
+ subpath.normalise "foo/bar/."
+ => "./foo/bar"
+
+ # Return the current directory as `./.`
+ subpath.normalise "."
+ => "./."
+
+ # error on `..` path components
+ subpath.normalise "foo/../bar"
+ => <error>
+
+ # error on empty string
+ subpath.normalise ""
+ => <error>
+
+ # error on absolute path
+ subpath.normalise "/foo"
+ => <error>
+ */
+ subpath.normalise = path:
+ assert assertMsg (subpathInvalidReason path == null)
+ "lib.path.subpath.normalise: Argument is not a valid subpath string: ${subpathInvalidReason path}";
+ joinRelPath (splitRelPath path);
+
}