forked from mirrors/nixpkgs
lib.path.subpath.normalise: init
This commit is contained in:
parent
98fbcf1788
commit
63dd6d20db
|
@ -4,10 +4,20 @@ let
|
||||||
|
|
||||||
inherit (builtins)
|
inherit (builtins)
|
||||||
isString
|
isString
|
||||||
|
split
|
||||||
match
|
match
|
||||||
;
|
;
|
||||||
|
|
||||||
|
inherit (lib.lists)
|
||||||
|
length
|
||||||
|
head
|
||||||
|
last
|
||||||
|
genList
|
||||||
|
elemAt
|
||||||
|
;
|
||||||
|
|
||||||
inherit (lib.strings)
|
inherit (lib.strings)
|
||||||
|
concatStringsSep
|
||||||
substring
|
substring
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -28,6 +38,60 @@ let
|
||||||
"The given string \"${value}\" contains a `..` component, which is not allowed in subpaths"
|
"The given string \"${value}\" contains a `..` component, which is not allowed in subpaths"
|
||||||
else null;
|
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. */ {
|
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:
|
subpath.isValid = value:
|
||||||
subpathInvalidReason value == null;
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,55 @@ let
|
||||||
expr = subpath.isValid "foo/..../bar";
|
expr = subpath.isValid "foo/..../bar";
|
||||||
expected = true;
|
expected = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
testSubpathNormaliseExample1 = {
|
||||||
|
expr = subpath.normalise "foo//bar";
|
||||||
|
expected = "./foo/bar";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample2 = {
|
||||||
|
expr = subpath.normalise "foo/./bar";
|
||||||
|
expected = "./foo/bar";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample3 = {
|
||||||
|
expr = subpath.normalise "foo/bar";
|
||||||
|
expected = "./foo/bar";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample4 = {
|
||||||
|
expr = subpath.normalise "foo/bar/";
|
||||||
|
expected = "./foo/bar";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample5 = {
|
||||||
|
expr = subpath.normalise "foo/bar/.";
|
||||||
|
expected = "./foo/bar";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample6 = {
|
||||||
|
expr = subpath.normalise ".";
|
||||||
|
expected = "./.";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample7 = {
|
||||||
|
expr = (builtins.tryEval (subpath.normalise "foo/../bar")).success;
|
||||||
|
expected = false;
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample8 = {
|
||||||
|
expr = (builtins.tryEval (subpath.normalise "")).success;
|
||||||
|
expected = false;
|
||||||
|
};
|
||||||
|
testSubpathNormaliseExample9 = {
|
||||||
|
expr = (builtins.tryEval (subpath.normalise "/foo")).success;
|
||||||
|
expected = false;
|
||||||
|
};
|
||||||
|
testSubpathNormaliseIsValidDots = {
|
||||||
|
expr = subpath.normalise "./foo/.bar/.../baz...qux";
|
||||||
|
expected = "./foo/.bar/.../baz...qux";
|
||||||
|
};
|
||||||
|
testSubpathNormaliseWrongType = {
|
||||||
|
expr = (builtins.tryEval (subpath.normalise null)).success;
|
||||||
|
expected = false;
|
||||||
|
};
|
||||||
|
testSubpathNormaliseTwoDots = {
|
||||||
|
expr = (builtins.tryEval (subpath.normalise "..")).success;
|
||||||
|
expected = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
if cases == [] then "Unit tests successful"
|
if cases == [] then "Unit tests successful"
|
||||||
|
|
Loading…
Reference in a new issue