push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

240
lib/path/README.md Normal file
View File

@@ -0,0 +1,240 @@
# Path library
This document explains why the `lib.path` library is designed the way it is.
The purpose of this library is to process [filesystem paths].
It does not read files from the filesystem.
It exists to support the native Nix [path value type] with extra functionality.
[filesystem paths]: https://en.m.wikipedia.org/wiki/Path_(computing)
[path value type]: https://nixos.org/manual/nix/stable/language/values.html#type-path
As an extension of the path value type, it inherits the same intended use cases and limitations:
- Only use paths to access files at evaluation time, such as the local project source.
- Paths cannot point to derivations, so they are unfit to represent dependencies.
- A path implicitly imports the referenced files into the Nix store when interpolated to a string.
Therefore paths are not suitable to access files at build- or run-time, as you risk importing the path from the evaluation system instead.
Overall, this library works with two types of paths:
- Absolute paths are represented with the Nix [path value type].
Nix automatically normalises these paths.
- Subpaths are represented with the [string value type] since path value types don't support relative paths.
This library normalises these paths as safely as possible.
Absolute paths in strings are not supported.
A subpath refers to a specific file or directory within an absolute base directory.
It is a stricter form of a relative path, notably [without support for `..` components][parents] since those could escape the base directory.
[string value type]: https://nixos.org/manual/nix/stable/language/values.html#type-string
This library is designed to be as safe and intuitive as possible, throwing errors when operations are attempted that would produce surprising results, and giving the expected result otherwise.
This library is designed to work well as a dependency for the `lib.filesystem` and `lib.sources` library components.
Contrary to these library components, `lib.path` does not read any paths from the filesystem.
This library makes only these assumptions about paths and no others:
- `dirOf path` returns the path to the parent directory of `path`, unless `path` is the filesystem root, in which case `path` is returned.
- There can be multiple filesystem roots: `p == dirOf p` and `q == dirOf q` does not imply `p == q`.
- While there's only a single filesystem root in stable Nix, the [lazy trees feature](https://github.com/NixOS/nix/pull/6530) introduces [additional filesystem roots](https://github.com/NixOS/nix/pull/6530#discussion_r1041442173).
- `path + ("/" + string)` returns the path to the `string` subdirectory in `path`.
- If `string` contains no `/` characters, then `dirOf (path + ("/" + string)) == path`.
- If `string` contains no `/` characters, then `baseNameOf (path + ("/" + string)) == string`.
- `path1 == path2` returns `true` only if `path1` points to the same filesystem path as `path2`.
Notably we do not make the assumption that we can turn paths into strings using `toString path`.
## Design decisions
Each subsection here contains a decision along with arguments and counter-arguments for (+) and against (-) that decision.
### Leading dots for relative paths
[leading-dots]: #leading-dots-for-relative-paths
Observing: Since subpaths are a form of relative paths, they can have a leading `./` to indicate it being a relative path, this is generally not necessary for tools though.
Considering: Paths should be as explicit, consistent and unambiguous as possible.
Decision: Returned subpaths should always have a leading `./`.
<details>
<summary>Arguments</summary>
- (+) In shells, just running `foo` as a command wouldn't execute the file `foo`, whereas `./foo` would execute the file.
In contrast, `foo/bar` does execute that file without the need for `./`.
This can lead to confusion about when a `./` needs to be prefixed.
If a `./` is always included, this becomes a non-issue.
This effectively then means that paths don't overlap with command names.
- (+) Prepending with `./` makes the subpaths always valid as relative Nix path expressions.
- (+) Using paths in command line arguments could give problems if not escaped properly, e.g. if a path was `--version`.
This is not a problem with `./--version`.
This effectively then means that paths don't overlap with GNU-style command line options.
- (-) `./` is not required to resolve relative paths, resolution always has an implicit `./` as prefix.
- (-) It's less noisy without the `./`, e.g. in error messages.
- (+) But similarly, it could be confusing whether something was even a path.
e.g. `foo` could be anything, but `./foo` is more clearly a path.
- (+) Makes it more uniform with absolute paths (those always start with `/`).
- (-) That is not relevant for practical purposes.
- (+) `find` also outputs results with `./`.
- (-) But only if you give it an argument of `.`.
If you give it the argument `some-directory`, it won't prefix that.
- (-) `realpath --relative-to` doesn't prefix relative paths with `./`.
- (+) There is no need to return the same result as `realpath`.
</details>
### Representation of the current directory
[curdir]: #representation-of-the-current-directory
Observing: The subpath that produces the base directory can be represented with `.` or `./` or `./.`.
Considering: Paths should be as consistent and unambiguous as possible.
Decision: It should be `./.`.
<details>
<summary>Arguments</summary>
- (+) `./` would be inconsistent with [the decision to not persist trailing slashes][trailing-slashes].
- (-) `.` is how `realpath` normalises paths.
- (+) `.` can be interpreted as a shell command (it's a builtin for sourcing files in `bash` and `zsh`).
- (+) `.` would be the only path without a `/`.
It could not be used as a Nix path expression, since those require at least one `/` to be parsed as such.
- (-) `./.` is rather long.
- (-) We don't require users to type this though, as it's only output by the library.
As inputs all three variants are supported for subpaths (and we can't do anything about absolute paths)
- (-) `builtins.dirOf "foo" == "."`, so `.` would be consistent with that.
- (+) `./.` is consistent with the [decision to have leading `./`][leading-dots].
- (+) `./.` is a valid Nix path expression, although this property does not hold for every relative path or subpath.
</details>
### Subpath representation
[relrepr]: #subpath-representation
Observing: Subpaths such as `foo/bar` can be represented in various ways:
- string: `"foo/bar"`
- list with all the components: `[ "foo" "bar" ]`
- attribute set: `{ type = "relative-path"; components = [ "foo" "bar" ]; }`
Considering: Paths should be as safe to use as possible.
We should generate string outputs in the library and not encourage users to do that themselves.
Decision: Paths are represented as strings.
<details>
<summary>Arguments</summary>
- (+) It's simpler for the users of the library.
One doesn't have to convert a path a string before it can be used.
- (+) Naively converting the list representation to a string with `concatStringsSep "/"` would break for `[]`, requiring library users to be more careful.
- (+) It doesn't encourage people to do their own path processing and instead use the library.
With a list representation it would seem easy to just use `lib.lists.init` to get the parent directory, but then it breaks for `.`, which would be represented as `[ ]`.
- (+) `+` is convenient and doesn't work on lists and attribute sets.
- (-) Shouldn't use `+` anyways, we export safer functions for path manipulation.
</details>
### Parent directory
[parents]: #parent-directory
Observing: Relative paths can have `..` components, which refer to the parent directory.
Considering: Paths should be as safe and unambiguous as possible.
Decision: `..` path components in string paths are not supported, neither as inputs nor as outputs.
Hence, string paths are called subpaths, rather than relative paths.
<details>
<summary>Arguments</summary>
- (+) If we wanted relative paths to behave according to the "physical" interpretation (as a directory tree with relations between nodes), it would require resolving symlinks, since e.g. `foo/..` would not be the same as `.` if `foo` is a symlink.
- (-) The "logical" interpretation is also valid (treating paths as a sequence of names), and is used by some software.
It is simpler, and not using symlinks at all is safer.
- (+) Mixing both models can lead to surprises.
- (+) We can't resolve symlinks without filesystem access.
- (+) Nix also doesn't support reading symlinks at evaluation time.
- (-) We could just not handle such cases, e.g. `equals "foo" "foo/bar/.. == false`.
The paths are different, we don't need to check whether the paths point to the same thing.
- (+) Assume we said `relativeTo /foo /bar == "../bar"`.
If this is used like `/bar/../foo` in the end, and `bar` turns out to be a symlink to somewhere else, this won't be accurate.
- (-) We could decide to not support such ambiguous operations, or mark them as such, e.g. the normal `relativeTo` will error on such a case, but there could be `extendedRelativeTo` supporting that.
- (-) `..` are a part of paths, a path library should therefore support it.
- (+) If we can convincingly argue that all such use cases are better done e.g. with runtime tools, the library not supporting it can nudge people towards using those.
- (-) We could allow "..", but only in the prefix.
- (+) Then we'd have to throw an error for doing `append /some/path "../foo"`, making it non-composable.
- (+) The same is for returning paths with `..`: `relativeTo /foo /bar => "../bar"` would produce a non-composable path.
- (+) We argue that `..` is not needed at the Nix evaluation level, since we'd always start evaluation from the project root and don't go up from there.
- (+) `..` is supported in Nix paths, turning them into absolute paths.
- (-) This is ambiguous in the presence of symlinks.
- (+) If you need `..` for building or runtime, you can use build-/run-time tooling to create those (e.g. `realpath` with `--relative-to`), or use absolute paths instead.
This also gives you the ability to correctly handle symlinks.
</details>
### Trailing slashes
[trailing-slashes]: #trailing-slashes
Observing: Subpaths can contain trailing slashes, like `foo/`, indicating that the path points to a directory and not a file.
Considering: Paths should be as consistent as possible, there should only be a single normalisation for the same path.
Decision: All functions remove trailing slashes in their results.
<details>
<summary>Arguments</summary>
- (+) It allows normalisations to be unique, in that there's only a single normalisation for the same path.
If trailing slashes were preserved, both `foo/bar` and `foo/bar/` would be valid but different normalisations for the same path.
- Comparison to other frameworks to figure out the least surprising behavior:
- (+) Nix itself doesn't support trailing slashes when parsing and doesn't preserve them when appending paths.
- (-) [Rust's std::path](https://doc.rust-lang.org/std/path/index.html) does preserve them during [construction](https://doc.rust-lang.org/std/path/struct.Path.html#method.new).
- (+) Doesn't preserve them when returning individual [components](https://doc.rust-lang.org/std/path/struct.Path.html#method.components).
- (+) Doesn't preserve them when [canonicalizing](https://doc.rust-lang.org/std/path/struct.Path.html#method.canonicalize).
- (+) [Python 3's pathlib](https://docs.python.org/3/library/pathlib.html#module-pathlib) doesn't preserve them during [construction](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath).
- Notably it represents the individual components as a list internally.
- (-) [Haskell's filepath](https://hackage.haskell.org/package/filepath-1.4.100.0) has [explicit support](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html#g:6) for handling trailing slashes.
- (-) Does preserve them for [normalisation](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html#v:normalise).
- (-) [NodeJS's Path library](https://nodejs.org/api/path.html) preserves trailing slashes for [normalisation](https://nodejs.org/api/path.html#pathnormalizepath).
- (+) For [parsing a path](https://nodejs.org/api/path.html#pathparsepath) into its significant elements, trailing slashes are not preserved.
- (+) Nix's builtin function `dirOf` gives an unexpected result for paths with trailing slashes: `dirOf "foo/bar/" == "foo/bar"`.
Inconsistently, `baseNameOf` works correctly though: `baseNameOf "foo/bar/" == "bar"`.
- (-) We are writing a path library to improve handling of paths though, so we shouldn't use these functions and discourage their use.
- (-) Unexpected result when normalising intermediate paths, like `relative.normalise ("foo" + "/") + "bar" == "foobar"`.
- (+) This is not a practical use case though.
- (+) Don't use `+` to append paths, this library has a `join` function for that.
- (-) Users might use `+` out of habit though.
- (+) The `realpath` command also removes trailing slashes.
- (+) Even with a trailing slash, the path is the same, it's only an indication that it's a directory.
</details>
### Prefer returning subpaths over components
[subpath-preference]: #prefer-returning-subpaths-over-components
Observing: Functions could return subpaths or lists of path component strings.
Considering: Subpaths are used as inputs for some functions.
Using them for outputs, too, makes the library more consistent and composable.
Decision: Subpaths should be preferred over list of path component strings.
<details>
<summary>Arguments</summary>
- (+) It is consistent with functions accepting subpaths, making the library more composable
- (-) It is less efficient when the components are needed, because after creating the normalised subpath string, it will have to be parsed into components again
- (+) If necessary, we can still make it faster by adding builtins to Nix
- (+) Alternatively if necessary, versions of these functions that return components could later still be introduced.
- (+) It makes the path library simpler because there's only two types (paths and subpaths).
Only `lib.path.subpath.components` can be used to get a list of components.
And once we have a list of component strings, `lib.lists` and `lib.strings` can be used to operate on them.
For completeness, `lib.path.subpath.join` allows converting the list of components back to a subpath.
</details>
## Other implementations and references
- [Rust](https://doc.rust-lang.org/std/path/struct.Path.html)
- [Python](https://docs.python.org/3/library/pathlib.html)
- [Haskell](https://hackage.haskell.org/package/filepath-1.4.100.0/docs/System-FilePath.html)
- [Nodejs](https://nodejs.org/api/path.html)
- [POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/nframe.html)

807
lib/path/default.nix Normal file
View File

@@ -0,0 +1,807 @@
# Functions for working with path values.
# See ./README.md for internal docs
{ lib }:
let
inherit (builtins)
isString
isPath
split
match
typeOf
storeDir
;
inherit (lib.lists)
length
head
last
genList
elemAt
all
concatMap
foldl'
take
drop
;
listHasPrefix = lib.lists.hasPrefix;
inherit (lib.strings)
concatStringsSep
substring
;
inherit (lib.asserts)
assertMsg
;
inherit (lib.path.subpath)
isValid
;
# Returns the reason why a subpath is invalid, or `null` if it's valid
subpathInvalidReason =
value:
if !isString value then
"The given value is of type ${builtins.typeOf value}, but a string was expected"
else if value == "" then
"The given string is empty"
else if substring 0 1 value == "/" then
"The given string \"${value}\" starts with a `/`, representing an absolute path"
# We don't support ".." components, see ./path.md#parent-directory
else if match "(.*/)?\\.\\.(/.*)?" value != null then
"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);
# 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 [ ];
# The components of the store directory, typically [ "nix" "store" ]
storeDirComponents = splitRelPath ("./" + storeDir);
# The number of store directory components, typically 2
storeDirLength = length storeDirComponents;
# Type: [ String ] -> Bool
#
# Whether path components have a store path as a prefix, according to
# https://nixos.org/manual/nix/stable/store/store-path.html#store-path.
componentsHaveStorePathPrefix =
components:
# path starts with the store directory (typically /nix/store)
listHasPrefix storeDirComponents components
# is not the store directory itself, meaning there's at least one extra component
&& storeDirComponents != components
# and the first component after the store directory has the expected format.
# NOTE: We could change the hash regex to be [0-9a-df-np-sv-z],
# because these are the actual ASCII characters used by Nix's base32 implementation,
# but this is not fully specified, so let's tie this too much to the currently implemented concept of store paths.
# Similar reasoning applies to the validity of the name part.
# We care more about discerning store path-ness on realistic values. Making it airtight would be fragile and slow.
&& match ".{32}-.+" (elemAt components storeDirLength) != null
# alternatively match contentaddressed derivations, which _currently_ do
# not have a store directory prefix.
# This is a workaround for https://github.com/NixOS/nix/issues/12361 which
# was needed during the experimental phase of ca-derivations and should be
# removed once the issue has been resolved.
|| components != [ ] && match "[0-9a-z]{52}" (head components) != null;
in
# No rec! Add dependencies on this file at the top.
{
/**
Append a subpath string to a path.
Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
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](#function-library-lib.path.subpath.isValid).
Laws:
- Not influenced by subpath [normalisation](#function-library-lib.path.subpath.normalise):
append p s == append p (subpath.normalise s)
# Inputs
`path`
: The absolute path to append to
`subpath`
: The subpath string to append
# Type
```
append :: Path -> String -> Path
```
# Examples
:::{.example}
## `append` usage example
```nix
append /foo "bar/baz"
=> /foo/bar/baz
# subpaths don't need to be normalised
append /foo "./bar//baz/./"
=> /foo/bar/baz
# can append to root directory
append /. "foo/bar"
=> /foo/bar
# first argument needs to be a path value type
append "/foo" "bar"
=> <error>
# second argument needs to be a valid subpath string
append /foo /bar
=> <error>
append /foo ""
=> <error>
append /foo "/bar"
=> <error>
append /foo "../bar"
=> <error>
```
:::
*/
append =
# The absolute path to append to
path:
# The subpath string to append
subpath:
assert assertMsg (isPath path)
''lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected'';
assert assertMsg (isValid subpath) ''
lib.path.append: Second argument is not a valid subpath string:
${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`](#function-library-lib.path.append) for some [subpath](#function-library-lib.path.subpath.isValid) `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.
# Inputs
`path1`
: 1\. Function argument
# Type
```
hasPrefix :: Path -> Path -> Bool
```
# Examples
:::{.example}
## `hasPrefix` usage example
```nix
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;
/**
Remove the first path as a component-wise prefix from the second path.
The result is a [normalised subpath string](#function-library-lib.path.subpath.normalise).
Laws:
- Inverts [`append`](#function-library-lib.path.append) for [normalised subpath string](#function-library-lib.path.subpath.normalise):
removePrefix p (append p s) == subpath.normalise s
# Inputs
`path1`
: 1\. Function argument
# Type
```
removePrefix :: Path -> Path -> String
```
# Examples
:::{.example}
## `removePrefix` usage example
```nix
removePrefix /foo /foo/bar/baz
=> "./bar/baz"
removePrefix /foo /foo
=> "./."
removePrefix /foo/bar /foo
=> <error>
removePrefix /. /foo
=> "./foo"
```
:::
*/
removePrefix =
path1:
assert assertMsg (isPath path1)
"lib.path.removePrefix: First argument is of type ${typeOf path1}, but a path was expected.";
let
path1Deconstructed = deconstructPath path1;
path1Length = length path1Deconstructed.components;
in
path2:
assert assertMsg (isPath path2)
"lib.path.removePrefix: Second argument is of type ${typeOf path2}, but a path was expected.";
let
path2Deconstructed = deconstructPath path2;
success = take path1Length path2Deconstructed.components == path1Deconstructed.components;
components =
if success then
drop path1Length path2Deconstructed.components
else
throw ''lib.path.removePrefix: The first path argument "${toString path1}" is not a component-wise prefix of the second path argument "${toString path2}".'';
in
assert assertMsg (path1Deconstructed.root == path2Deconstructed.root) ''
lib.path.removePrefix: 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}"'';
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 [`dirOf`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-dirOf) returns `root` itself:
dirOf (splitRoot p).root == (splitRoot p).root
# Inputs
`path`
: The path to split the root off of
# Type
```
splitRoot :: Path -> { root :: Path, subpath :: String }
```
# Examples
:::{.example}
## `splitRoot` usage example
```nix
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"
=> <error>
```
:::
*/
splitRoot =
# The path to split the root off of
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 [path](https://nixos.org/manual/nix/stable/language/values.html#type-path)
has a [store path](https://nixos.org/manual/nix/stable/store/store-path.html#store-path)
as a prefix.
:::{.note}
As with all functions of this `lib.path` library, it does not work on paths in strings,
which is how you'd typically get store paths.
Instead, this function only handles path values themselves,
which occur when Nix files in the store use relative path expressions.
:::
# Inputs
`path`
: 1\. Function argument
# Type
```
hasStorePathPrefix :: Path -> Bool
```
# Examples
:::{.example}
## `hasStorePathPrefix` usage example
```nix
# Subpaths of derivation outputs have a store path as a prefix
hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo/bar/baz
=> true
# The store directory itself is not a store path
hasStorePathPrefix /nix/store
=> false
# Derivation outputs are store paths themselves
hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo
=> true
# Paths outside the Nix store don't have a store path prefix
hasStorePathPrefix /home/user
=> false
# Not all paths under the Nix store are store paths
hasStorePathPrefix /nix/store/.links/10gg8k3rmbw8p7gszarbk7qyd9jwxhcfq9i6s5i0qikx8alkk4hq
=> false
# Store derivations are also store paths themselves
hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo.drv
=> true
```
:::
*/
hasStorePathPrefix =
path:
let
deconstructed = deconstructPath path;
in
assert assertMsg (isPath path)
"lib.path.hasStorePathPrefix: Argument is of type ${typeOf path}, but a path was expected";
assert assertMsg
# This function likely breaks or needs adjustment if used with other filesystem roots, if they ever get implemented.
# Let's try to error nicely in such a case, though it's unclear how an implementation would work even and whether this could be detected.
# See also https://github.com/NixOS/nix/pull/6530#discussion_r1422843117
(deconstructed.root == /. && toString deconstructed.root == "/")
"lib.path.hasStorePathPrefix: Argument has a filesystem root (${toString deconstructed.root}) that's not /, which is currently not supported.";
componentsHaveStorePathPrefix 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.
- The string doesn't start with a `/`.
- The string doesn't contain any `..` path components.
# Inputs
`value`
: The value to check
# Type
```
subpath.isValid :: String -> Bool
```
# Examples
:::{.example}
## `subpath.isValid` usage example
```nix
# Not a string
subpath.isValid null
=> false
# Empty string
subpath.isValid ""
=> false
# Absolute path
subpath.isValid "/foo"
=> false
# Contains a `..` path component
subpath.isValid "../foo"
=> false
# Valid subpath
subpath.isValid "foo/bar"
=> true
# Doesn't need to be normalised
subpath.isValid "./foo//bar/"
=> true
```
:::
*/
subpath.isValid =
# The value to check
value: subpathInvalidReason value == null;
/**
Join subpath strings together using `/`, returning a normalised subpath string.
Like `concatStringsSep "/"` but safer, specifically:
- All elements must be [valid subpath strings](#function-library-lib.path.subpath.isValid).
- The result gets [normalised](#function-library-lib.path.subpath.normalise).
- The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`.
Laws:
- Associativity:
subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ]
- Identity - `"./."` is the neutral element for normalised paths:
subpath.join [ ] == "./."
subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p
- Normalisation - the result is [normalised](#function-library-lib.path.subpath.normalise):
subpath.join ps == subpath.normalise (subpath.join ps)
- For non-empty lists, the implementation is equivalent to [normalising](#function-library-lib.path.subpath.normalise) the result of `concatStringsSep "/"`.
Note that the above laws can be derived from this one:
ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)
# Inputs
`subpaths`
: The list of subpaths to join together
# Type
```
subpath.join :: [ String ] -> String
```
# Examples
:::{.example}
## `subpath.join` usage example
```nix
subpath.join [ "foo" "bar/baz" ]
=> "./foo/bar/baz"
# normalise the result
subpath.join [ "./foo" "." "bar//./baz/" ]
=> "./foo/bar/baz"
# passing an empty list results in the current directory
subpath.join [ ]
=> "./."
# elements must be valid subpath strings
subpath.join [ /foo ]
=> <error>
subpath.join [ "" ]
=> <error>
subpath.join [ "/foo" ]
=> <error>
subpath.join [ "../foo" ]
=> <error>
```
:::
*/
subpath.join =
# The list of subpaths to join together
subpaths:
# Fast in case all paths are valid
if all isValid subpaths then
joinRelPath (concatMap splitRelPath subpaths)
else
# Otherwise we take our time to gather more info for a better error message
# Strictly go through each path, throwing on the first invalid one
# Tracks the list index in the fold accumulator
foldl' (
i: path:
if isValid path then
i + 1
else
throw ''
lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string:
${subpathInvalidReason path}''
) 0 subpaths;
/**
Split [a subpath](#function-library-lib.path.subpath.isValid) into its path component strings.
Throw an error if the subpath isn't valid.
Note that the returned path components are also [valid subpath strings](#function-library-lib.path.subpath.isValid), though they are intentionally not [normalised](#function-library-lib.path.subpath.normalise).
Laws:
- Splitting a subpath into components and [joining](#function-library-lib.path.subpath.join) the components gives the same subpath but [normalised](#function-library-lib.path.subpath.normalise):
subpath.join (subpath.components s) == subpath.normalise s
# Inputs
`subpath`
: The subpath string to split into components
# Type
```
subpath.components :: String -> [ String ]
```
# Examples
:::{.example}
## `subpath.components` usage example
```nix
subpath.components "."
=> [ ]
subpath.components "./foo//bar/./baz/"
=> [ "foo" "bar" "baz" ]
subpath.components "/foo"
=> <error>
```
:::
*/
subpath.components =
# The subpath string to split into components
subpath:
assert assertMsg (isValid subpath) ''
lib.path.subpath.components: Argument is not a valid subpath string:
${subpathInvalidReason subpath}'';
splitRelPath subpath;
/**
Normalise a subpath. Throw an error if the subpath isn't [valid](#function-library-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](#function-library-lib.path.append) to a Nix path value:
append base p == append base (subpath.normalise p)
- Don't change the path according to `realpath`:
$(realpath ${p}) == $(realpath ${subpath.normalise p})
- Only error on [invalid subpaths](#function-library-lib.path.subpath.isValid):
builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
# Inputs
`subpath`
: The subpath string to normalise
# Type
```
subpath.normalise :: String -> String
```
# Examples
:::{.example}
## `subpath.normalise` usage example
```nix
# 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 =
# The subpath string to normalise
subpath:
assert assertMsg (isValid subpath) ''
lib.path.subpath.normalise: Argument is not a valid subpath string:
${subpathInvalidReason subpath}'';
joinRelPath (splitRelPath subpath);
}

View File

@@ -0,0 +1,48 @@
{
nixpkgs ? ../../..,
system ? builtins.currentSystem,
pkgs ? import nixpkgs {
config = { };
overlays = [ ];
inherit system;
},
nixVersions ? import ../../tests/nix-for-tests.nix { inherit pkgs; },
libpath ? ../..,
# Random seed
seed ? null,
}:
pkgs.runCommand "lib-path-tests"
{
nativeBuildInputs = [
nixVersions.stable
]
++ (with pkgs; [
jq
bc
]);
}
''
# Needed to make Nix evaluation work
export TEST_ROOT=$(pwd)/test-tmp
export NIX_BUILD_HOOK=
export NIX_CONF_DIR=$TEST_ROOT/etc
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
export NIX_STATE_DIR=$TEST_ROOT/var/nix
export NIX_STORE_DIR=$TEST_ROOT/store
export PAGER=cat
cp -r ${libpath} lib
export TEST_LIB=$PWD/lib
echo "Running unit tests lib/path/tests/unit.nix"
nix-instantiate --eval --show-trace \
--argstr libpath "$TEST_LIB" \
lib/path/tests/unit.nix
echo "Running property tests lib/path/tests/prop.sh"
bash lib/path/tests/prop.sh ${toString seed}
touch $out
''

View File

@@ -0,0 +1,64 @@
# Generate random path-like strings, separated by null characters.
#
# Invocation:
#
# awk -f ./generate.awk -v <variable>=<value> | tr '\0' '\n'
#
# Customizable variables (all default to 0):
# - seed: Deterministic random seed to use for generation
# - count: Number of paths to generate
# - extradotweight: Give extra weight to dots being generated
# - extraslashweight: Give extra weight to slashes being generated
# - extranullweight: Give extra weight to null being generated, making paths shorter
BEGIN {
# Random seed, passed explicitly for reproducibility
srand(seed)
# Don't include special characters below 32
minascii = 32
# Don't include DEL at 128
maxascii = 127
upperascii = maxascii - minascii
# add extra weight for ., in addition to the one weight from the ascii range
upperdot = upperascii + extradotweight
# add extra weight for /, in addition to the one weight from the ascii range
upperslash = upperdot + extraslashweight
# add extra weight for null, indicating the end of the string
# Must be at least 1 to have strings end at all
total = upperslash + 1 + extranullweight
# new=1 indicates that it's a new string
new=1
while (count > 0) {
# Random integer between [0, total)
value = int(rand() * total)
if (value < upperascii) {
# Ascii range
printf("%c", value + minascii)
new=0
} else if (value < upperdot) {
# Dot range
printf "."
new=0
} else if (value < upperslash) {
# If it's the start of a new path, only generate a / in 10% of cases
# This is always an invalid subpath, which is not a very interesting case
if (new && rand() > 0.1) continue
printf "/"
} else {
# Do not generate empty strings
if (new) continue
printf "\x00"
count--
new=1
}
}
}

60
lib/path/tests/prop.nix Normal file
View File

@@ -0,0 +1,60 @@
# Given a list of path-like strings, check some properties of the path library
# using those paths and return a list of attribute sets of the following form:
#
# { <string> = <lib.path.subpath.normalise string>; }
#
# If `normalise` fails to evaluate, the attribute value is set to `""`.
# If not, the resulting value is normalised again and an appropriate attribute set added to the output list.
{
# The path to the nixpkgs lib to use
libpath,
# A flat directory containing files with randomly-generated
# path-like values
dir,
}:
let
lib = import libpath;
# read each file into a string
strings = map (name: builtins.readFile (dir + "/${name}")) (
builtins.attrNames (builtins.readDir dir)
);
inherit (lib.path.subpath) normalise isValid;
inherit (lib.asserts) assertMsg;
normaliseAndCheck =
str:
let
originalValid = isValid str;
tryOnce = builtins.tryEval (normalise str);
tryTwice = builtins.tryEval (normalise tryOnce.value);
absConcatOrig = /. + ("/" + str);
absConcatNormalised = /. + ("/" + tryOnce.value);
in
# Check the lib.path.subpath.normalise property to only error on invalid subpaths
assert assertMsg (
originalValid -> tryOnce.success
) "Even though string \"${str}\" is valid as a subpath, the normalisation for it failed";
assert assertMsg (
!originalValid -> !tryOnce.success
) "Even though string \"${str}\" is invalid as a subpath, the normalisation for it succeeded";
# Check normalisation idempotency
assert assertMsg (
originalValid -> tryTwice.success
) "For valid subpath \"${str}\", the normalisation \"${tryOnce.value}\" was not a valid subpath";
assert assertMsg (originalValid -> tryOnce.value == tryTwice.value)
"For valid subpath \"${str}\", normalising it once gives \"${tryOnce.value}\" but normalising it twice gives a different result: \"${tryTwice.value}\"";
# Check that normalisation doesn't change a string when appended to an absolute Nix path value
assert assertMsg (originalValid -> absConcatOrig == absConcatNormalised)
"For valid subpath \"${str}\", appending to an absolute Nix path value gives \"${absConcatOrig}\", but appending the normalised result \"${tryOnce.value}\" gives a different value \"${absConcatNormalised}\"";
# Return an empty string when failed
if tryOnce.success then tryOnce.value else "";
in
lib.genAttrs strings normaliseAndCheck

182
lib/path/tests/prop.sh Executable file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env bash
# Property tests for lib/path/default.nix
# It generates random path-like strings and runs the functions on
# them, checking that the expected laws of the functions hold
# Run:
# [nixpkgs]$ lib/path/tests/prop.sh
# or:
# [nixpkgs]$ nix-build lib/tests/release.nix
set -euo pipefail
shopt -s inherit_errexit
# https://stackoverflow.com/a/246128
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if test -z "${TEST_LIB:-}"; then
TEST_LIB=$SCRIPT_DIR/../..
fi
tmp="$(mktemp -d)"
clean_up() {
rm -rf "$tmp"
}
trap clean_up EXIT
mkdir -p "$tmp/work"
cd "$tmp/work"
# Defaulting to a random seed but the first argument can override this
seed=${1:-$RANDOM}
echo >&2 "Using seed $seed, use \`lib/path/tests/prop.sh $seed\` to reproduce this result"
# The number of random paths to generate. This specific number was chosen to
# be fast enough while still generating enough variety to detect bugs.
count=500
debug=0
# debug=1 # print some extra info
# debug=2 # print generated values
# Fine tuning parameters to balance the number of generated invalid paths
# to the variance in generated paths.
extradotweight=64 # Larger value: more dots
extraslashweight=64 # Larger value: more slashes
extranullweight=16 # Larger value: shorter strings
die() {
echo >&2 "test case failed: " "$@"
exit 1
}
if [[ "$debug" -ge 1 ]]; then
echo >&2 "Generating $count random path-like strings"
fi
# Read stream of null-terminated strings entry-by-entry into bash,
# write it to a file and the `strings` array.
declare -a strings=()
mkdir -p "$tmp/strings"
while IFS= read -r -d $'\0' str; do
printf "%s" "$str" > "$tmp/strings/${#strings[@]}"
strings+=("$str")
done < <(awk \
-f "$SCRIPT_DIR"/generate.awk \
-v seed="$seed" \
-v count="$count" \
-v extradotweight="$extradotweight" \
-v extraslashweight="$extraslashweight" \
-v extranullweight="$extranullweight")
if [[ "$debug" -ge 1 ]]; then
echo >&2 "Trying to normalise the generated path-like strings with Nix"
fi
# Precalculate all normalisations with a single Nix call. Calling Nix for each
# string individually would take way too long
nix-instantiate --eval --strict --json --show-trace \
--argstr libpath "$TEST_LIB" \
--argstr dir "$tmp/strings" \
"$SCRIPT_DIR"/prop.nix \
>"$tmp/result.json"
# Uses some jq magic to turn the resulting attribute set into an associative
# bash array assignment
declare -A normalised_result="($(jq '
to_entries
| map("[\(.key | @sh)]=\(.value | @sh)")
| join(" \n")' -r < "$tmp/result.json"))"
# Looks up a normalisation result for a string
# Checks that the normalisation is only failing iff it's an invalid subpath
# For valid subpaths, returns 0 and prints the normalisation result
# For invalid subpaths, returns 1
normalise() {
local str=$1
# Uses the same check for validity as in the library implementation
if [[ "$str" == "" || "$str" == /* || "$str" =~ ^(.*/)?\.\.(/.*)?$ ]]; then
valid=
else
valid=1
fi
normalised=${normalised_result[$str]}
# An empty string indicates failure, this is encoded in ./prop.nix
if [[ -n "$normalised" ]]; then
if [[ -n "$valid" ]]; then
echo "$normalised"
else
die "For invalid subpath \"$str\", lib.path.subpath.normalise returned this result: \"$normalised\""
fi
else
if [[ -n "$valid" ]]; then
die "For valid subpath \"$str\", lib.path.subpath.normalise failed"
else
if [[ "$debug" -ge 2 ]]; then
echo >&2 "String \"$str\" is not a valid subpath"
fi
# Invalid and it correctly failed, we let the caller continue if they catch the exit code
return 1
fi
fi
}
# Intermediate result populated by test_idempotency_realpath
# and used in test_normalise_uniqueness
#
# Contains a mapping from a normalised subpath to the realpath result it represents
declare -A norm_to_real
test_idempotency_realpath() {
if [[ "$debug" -ge 1 ]]; then
echo >&2 "Checking idempotency of each result and making sure the realpath result isn't changed"
fi
# Count invalid subpaths to display stats
invalid=0
for str in "${strings[@]}"; do
if ! result=$(normalise "$str"); then
((invalid++)) || true
continue
fi
# Check the law that it doesn't change the result of a realpath
mkdir -p -- "$str" "$result"
real_orig=$(realpath -- "$str")
real_norm=$(realpath -- "$result")
if [[ "$real_orig" != "$real_norm" ]]; then
die "realpath of the original string \"$str\" (\"$real_orig\") is not the same as realpath of the normalisation \"$result\" (\"$real_norm\")"
fi
if [[ "$debug" -ge 2 ]]; then
echo >&2 "String \"$str\" gets normalised to \"$result\" and file path \"$real_orig\""
fi
norm_to_real["$result"]="$real_orig"
done
if [[ "$debug" -ge 1 ]]; then
echo >&2 "$(bc <<< "scale=1; 100 / $count * $invalid")% of the total $count generated strings were invalid subpath strings, and were therefore ignored"
fi
}
test_normalise_uniqueness() {
if [[ "$debug" -ge 1 ]]; then
echo >&2 "Checking for the uniqueness law"
fi
for norm_p in "${!norm_to_real[@]}"; do
real_p=${norm_to_real["$norm_p"]}
for norm_q in "${!norm_to_real[@]}"; do
real_q=${norm_to_real["$norm_q"]}
# Checks normalisation uniqueness law for each pair of values
if [[ "$norm_p" != "$norm_q" && "$real_p" == "$real_q" ]]; then
die "Normalisations \"$norm_p\" and \"$norm_q\" are different, but the realpath of them is the same: \"$real_p\""
fi
done
done
}
test_idempotency_realpath
test_normalise_uniqueness
echo >&2 tests ok

332
lib/path/tests/unit.nix Normal file
View File

@@ -0,0 +1,332 @@
# Unit tests for lib.path functions. Use `nix-build` in this directory to
# run these
{ libpath }:
let
lib = import libpath;
inherit (lib.path)
hasPrefix
removePrefix
append
splitRoot
hasStorePathPrefix
subpath
;
# This is not allowed generally, but we're in the tests here, so we'll allow ourselves.
storeDirPath = /. + builtins.storeDir;
cases = lib.runTests {
# Test examples from the lib.path.append documentation
testAppendExample1 = {
expr = append /foo "bar/baz";
expected = /foo/bar/baz;
};
testAppendExample2 = {
expr = append /foo "./bar//baz/./";
expected = /foo/bar/baz;
};
testAppendExample3 = {
expr = append /. "foo/bar";
expected = /foo/bar;
};
testAppendExample4 = {
expr = (builtins.tryEval (append "/foo" "bar")).success;
expected = false;
};
testAppendExample5 = {
expr = (builtins.tryEval (append /foo /bar)).success;
expected = false;
};
testAppendExample6 = {
expr = (builtins.tryEval (append /foo "")).success;
expected = false;
};
testAppendExample7 = {
expr = (builtins.tryEval (append /foo "/bar")).success;
expected = false;
};
testAppendExample8 = {
expr = (builtins.tryEval (append /foo "../bar")).success;
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;
};
testRemovePrefixExample1 = {
expr = removePrefix /foo /foo/bar/baz;
expected = "./bar/baz";
};
testRemovePrefixExample2 = {
expr = removePrefix /foo /foo;
expected = "./.";
};
testRemovePrefixExample3 = {
expr = (builtins.tryEval (removePrefix /foo/bar /foo)).success;
expected = false;
};
testRemovePrefixExample4 = {
expr = removePrefix /. /foo;
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;
};
# Root path (empty path components list)
testHasStorePathPrefixRoot = {
expr = hasStorePathPrefix /.;
expected = false;
};
testHasStorePathPrefixExample1 = {
expr = hasStorePathPrefix (storeDirPath + "/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo/bar/baz");
expected = true;
};
testHasStorePathPrefixExample2 = {
expr = hasStorePathPrefix storeDirPath;
expected = false;
};
testHasStorePathPrefixExample3 = {
expr = hasStorePathPrefix (storeDirPath + "/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo");
expected = true;
};
testHasStorePathPrefixExample4 = {
expr = hasStorePathPrefix /home/user;
expected = false;
};
testHasStorePathPrefixExample5 = {
expr = hasStorePathPrefix (
storeDirPath + "/.links/10gg8k3rmbw8p7gszarbk7qyd9jwxhcfq9i6s5i0qikx8alkk4hq"
);
expected = false;
};
testHasStorePathPrefixExample6 = {
expr = hasStorePathPrefix (storeDirPath + "/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo.drv");
expected = true;
};
# Test paths for contentaddressed derivations
testHasStorePathPrefixExample7 = {
expr = hasStorePathPrefix (/. + "/1121rp0gvr1qya7hvy925g5kjwg66acz6sn1ra1hca09f1z5dsab");
expected = true;
};
testHasStorePathPrefixExample8 = {
expr = hasStorePathPrefix (/. + "/1121rp0gvr1qya7hvy925g5kjwg66acz6sn1ra1hca09f1z5dsab/foo/bar");
expected = true;
};
# Test examples from the lib.path.subpath.isValid documentation
testSubpathIsValidExample1 = {
expr = subpath.isValid null;
expected = false;
};
testSubpathIsValidExample2 = {
expr = subpath.isValid "";
expected = false;
};
testSubpathIsValidExample3 = {
expr = subpath.isValid "/foo";
expected = false;
};
testSubpathIsValidExample4 = {
expr = subpath.isValid "../foo";
expected = false;
};
testSubpathIsValidExample5 = {
expr = subpath.isValid "foo/bar";
expected = true;
};
testSubpathIsValidExample6 = {
expr = subpath.isValid "./foo//bar/";
expected = true;
};
# Some extra tests
testSubpathIsValidTwoDotsEnd = {
expr = subpath.isValid "foo/..";
expected = false;
};
testSubpathIsValidTwoDotsMiddle = {
expr = subpath.isValid "foo/../bar";
expected = false;
};
testSubpathIsValidTwoDotsPrefix = {
expr = subpath.isValid "..foo";
expected = true;
};
testSubpathIsValidTwoDotsSuffix = {
expr = subpath.isValid "foo..";
expected = true;
};
testSubpathIsValidTwoDotsPrefixComponent = {
expr = subpath.isValid "foo/..bar/baz";
expected = true;
};
testSubpathIsValidTwoDotsSuffixComponent = {
expr = subpath.isValid "foo/bar../baz";
expected = true;
};
testSubpathIsValidThreeDots = {
expr = subpath.isValid "...";
expected = true;
};
testSubpathIsValidFourDots = {
expr = subpath.isValid "....";
expected = true;
};
testSubpathIsValidThreeDotsComponent = {
expr = subpath.isValid "foo/.../bar";
expected = true;
};
testSubpathIsValidFourDotsComponent = {
expr = subpath.isValid "foo/..../bar";
expected = true;
};
# Test examples from the lib.path.subpath.join documentation
testSubpathJoinExample1 = {
expr = subpath.join [
"foo"
"bar/baz"
];
expected = "./foo/bar/baz";
};
testSubpathJoinExample2 = {
expr = subpath.join [
"./foo"
"."
"bar//./baz/"
];
expected = "./foo/bar/baz";
};
testSubpathJoinExample3 = {
expr = subpath.join [ ];
expected = "./.";
};
testSubpathJoinExample4 = {
expr = (builtins.tryEval (subpath.join [ /foo ])).success;
expected = false;
};
testSubpathJoinExample5 = {
expr = (builtins.tryEval (subpath.join [ "" ])).success;
expected = false;
};
testSubpathJoinExample6 = {
expr = (builtins.tryEval (subpath.join [ "/foo" ])).success;
expected = false;
};
testSubpathJoinExample7 = {
expr = (builtins.tryEval (subpath.join [ "../foo" ])).success;
expected = false;
};
# Test examples from the lib.path.subpath.normalise documentation
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;
};
# Some extra tests
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;
};
testSubpathComponentsExample1 = {
expr = subpath.components ".";
expected = [ ];
};
testSubpathComponentsExample2 = {
expr = subpath.components "./foo//bar/./baz/";
expected = [
"foo"
"bar"
"baz"
];
};
testSubpathComponentsExample3 = {
expr = (builtins.tryEval (subpath.components "/foo")).success;
expected = false;
};
};
in
if cases == [ ] then
"Unit tests successful"
else
throw "Path unit tests failed: ${lib.generators.toPretty { } cases}"