1# Functions for working with path values.
2# See ./README.md for internal docs
3{ lib }:
4let
5
6 inherit (builtins)
7 isString
8 isPath
9 split
10 match
11 typeOf
12 storeDir
13 ;
14
15 inherit (lib.lists)
16 length
17 head
18 last
19 genList
20 elemAt
21 all
22 concatMap
23 foldl'
24 take
25 drop
26 ;
27
28 listHasPrefix = lib.lists.hasPrefix;
29
30 inherit (lib.strings)
31 concatStringsSep
32 substring
33 ;
34
35 inherit (lib.asserts)
36 assertMsg
37 ;
38
39 inherit (lib.path.subpath)
40 isValid
41 ;
42
43 # Return the reason why a subpath is invalid, or `null` if it's valid
44 subpathInvalidReason =
45 value:
46 if !isString value then
47 "The given value is of type ${builtins.typeOf value}, but a string was expected"
48 else if value == "" then
49 "The given string is empty"
50 else if substring 0 1 value == "/" then
51 "The given string \"${value}\" starts with a `/`, representing an absolute path"
52 # We don't support ".." components, see ./path.md#parent-directory
53 else if match "(.*/)?\\.\\.(/.*)?" value != null then
54 "The given string \"${value}\" contains a `..` component, which is not allowed in subpaths"
55 else
56 null;
57
58 # Split and normalise a relative path string into its components.
59 # Error for ".." components and doesn't include "." components
60 splitRelPath =
61 path:
62 let
63 # Split the string into its parts using regex for efficiency. This regex
64 # matches patterns like "/", "/./", "/././", with arbitrarily many "/"s
65 # together. These are the main special cases:
66 # - Leading "./" gets split into a leading "." part
67 # - Trailing "/." or "/" get split into a trailing "." or ""
68 # part respectively
69 #
70 # These are the only cases where "." and "" parts can occur
71 parts = split "/+(\\./+)*" path;
72
73 # `split` creates a list of 2 * k + 1 elements, containing the k +
74 # 1 parts, interleaved with k matches where k is the number of
75 # (non-overlapping) matches. This calculation here gets the number of parts
76 # back from the list length
77 # floor( (2 * k + 1) / 2 ) + 1 == floor( k + 1/2 ) + 1 == k + 1
78 partCount = length parts / 2 + 1;
79
80 # To assemble the final list of components we want to:
81 # - Skip a potential leading ".", normalising "./foo" to "foo"
82 # - Skip a potential trailing "." or "", normalising "foo/" and "foo/." to
83 # "foo". See ./path.md#trailing-slashes
84 skipStart = if head parts == "." then 1 else 0;
85 skipEnd = if last parts == "." || last parts == "" then 1 else 0;
86
87 # We can now know the length of the result by removing the number of
88 # skipped parts from the total number
89 componentCount = partCount - skipEnd - skipStart;
90
91 in
92 # Special case of a single "." path component. Such a case leaves a
93 # componentCount of -1 due to the skipStart/skipEnd not verifying that
94 # they don't refer to the same character
95 if path == "." then
96 [ ]
97
98 # Generate the result list directly. This is more efficient than a
99 # combination of `filter`, `init` and `tail`, because here we don't
100 # allocate any intermediate lists
101 else
102 genList (
103 index:
104 # To get to the element we need to add the number of parts we skip and
105 # multiply by two due to the interleaved layout of `parts`
106 elemAt parts ((skipStart + index) * 2)
107 ) componentCount;
108
109 # Join relative path components together
110 joinRelPath =
111 components:
112 # Always return relative paths with `./` as a prefix (./path.md#leading-dots-for-relative-paths)
113 "./"
114 +
115 # An empty string is not a valid relative path, so we need to return a `.` when we have no components
116 (if components == [ ] then "." else concatStringsSep "/" components);
117
118 # Type: Path -> { root :: Path, components :: [ String ] }
119 #
120 # Deconstruct a path value type into:
121 # - root: The filesystem root of the path, generally `/`
122 # - components: All the path's components
123 #
124 # This is similar to `splitString "/" (toString path)` but safer
125 # because it can distinguish different filesystem roots
126 deconstructPath =
127 let
128 recurse =
129 components: base:
130 # If the parent of a path is the path itself, then it's a filesystem root
131 if base == dirOf base then
132 {
133 root = base;
134 inherit components;
135 }
136 else
137 recurse ([ (baseNameOf base) ] ++ components) (dirOf base);
138 in
139 recurse [ ];
140
141 # The components of the store directory, typically [ "nix" "store" ]
142 storeDirComponents = splitRelPath ("./" + storeDir);
143 # The number of store directory components, typically 2
144 storeDirLength = length storeDirComponents;
145
146 # Type: [ String ] -> Bool
147 #
148 # Whether path components have a store path as a prefix, according to
149 # https://nixos.org/manual/nix/stable/store/store-path.html#store-path.
150 componentsHaveStorePathPrefix =
151 components:
152 # path starts with the store directory (typically /nix/store)
153 listHasPrefix storeDirComponents components
154 # is not the store directory itself, meaning there's at least one extra component
155 && storeDirComponents != components
156 # and the first component after the store directory has the expected format.
157 # NOTE: We could change the hash regex to be [0-9a-df-np-sv-z],
158 # because these are the actual ASCII characters used by Nix's base32 implementation,
159 # but this is not fully specified, so let's tie this too much to the currently implemented concept of store paths.
160 # Similar reasoning applies to the validity of the name part.
161 # We care more about discerning store path-ness on realistic values. Making it airtight would be fragile and slow.
162 && match ".{32}-.+" (elemAt components storeDirLength) != null;
163
164in
165# No rec! Add dependencies on this file at the top.
166{
167
168 /*
169 Append a subpath string to a path.
170
171 Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
172 More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"),
173 and that the second argument is a [valid subpath string](#function-library-lib.path.subpath.isValid).
174
175 Laws:
176
177 - Not influenced by subpath [normalisation](#function-library-lib.path.subpath.normalise):
178
179 append p s == append p (subpath.normalise s)
180
181 Type:
182 append :: Path -> String -> Path
183
184 Example:
185 append /foo "bar/baz"
186 => /foo/bar/baz
187
188 # subpaths don't need to be normalised
189 append /foo "./bar//baz/./"
190 => /foo/bar/baz
191
192 # can append to root directory
193 append /. "foo/bar"
194 => /foo/bar
195
196 # first argument needs to be a path value type
197 append "/foo" "bar"
198 => <error>
199
200 # second argument needs to be a valid subpath string
201 append /foo /bar
202 => <error>
203 append /foo ""
204 => <error>
205 append /foo "/bar"
206 => <error>
207 append /foo "../bar"
208 => <error>
209 */
210 append =
211 # The absolute path to append to
212 path:
213 # The subpath string to append
214 subpath:
215 assert assertMsg (isPath path)
216 ''lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected'';
217 assert assertMsg (isValid subpath) ''
218 lib.path.append: Second argument is not a valid subpath string:
219 ${subpathInvalidReason subpath}'';
220 path + ("/" + subpath);
221
222 /*
223 Whether the first path is a component-wise prefix of the second path.
224
225 Laws:
226
227 - `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`.
228
229 - `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.
230
231 Type:
232 hasPrefix :: Path -> Path -> Bool
233
234 Example:
235 hasPrefix /foo /foo/bar
236 => true
237 hasPrefix /foo /foo
238 => true
239 hasPrefix /foo/bar /foo
240 => false
241 hasPrefix /. /foo
242 => true
243 */
244 hasPrefix =
245 path1:
246 assert assertMsg (isPath path1)
247 "lib.path.hasPrefix: First argument is of type ${typeOf path1}, but a path was expected";
248 let
249 path1Deconstructed = deconstructPath path1;
250 in
251 path2:
252 assert assertMsg (isPath path2)
253 "lib.path.hasPrefix: Second argument is of type ${typeOf path2}, but a path was expected";
254 let
255 path2Deconstructed = deconstructPath path2;
256 in
257 assert assertMsg (path1Deconstructed.root == path2Deconstructed.root) ''
258 lib.path.hasPrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
259 first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
260 second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
261 take (length path1Deconstructed.components) path2Deconstructed.components
262 == path1Deconstructed.components;
263
264 /*
265 Remove the first path as a component-wise prefix from the second path.
266 The result is a [normalised subpath string](#function-library-lib.path.subpath.normalise).
267
268 Laws:
269
270 - Inverts [`append`](#function-library-lib.path.append) for [normalised subpath string](#function-library-lib.path.subpath.normalise):
271
272 removePrefix p (append p s) == subpath.normalise s
273
274 Type:
275 removePrefix :: Path -> Path -> String
276
277 Example:
278 removePrefix /foo /foo/bar/baz
279 => "./bar/baz"
280 removePrefix /foo /foo
281 => "./."
282 removePrefix /foo/bar /foo
283 => <error>
284 removePrefix /. /foo
285 => "./foo"
286 */
287 removePrefix =
288 path1:
289 assert assertMsg (isPath path1)
290 "lib.path.removePrefix: First argument is of type ${typeOf path1}, but a path was expected.";
291 let
292 path1Deconstructed = deconstructPath path1;
293 path1Length = length path1Deconstructed.components;
294 in
295 path2:
296 assert assertMsg (isPath path2)
297 "lib.path.removePrefix: Second argument is of type ${typeOf path2}, but a path was expected.";
298 let
299 path2Deconstructed = deconstructPath path2;
300 success = take path1Length path2Deconstructed.components == path1Deconstructed.components;
301 components =
302 if success then
303 drop path1Length path2Deconstructed.components
304 else
305 throw ''lib.path.removePrefix: The first path argument "${toString path1}" is not a component-wise prefix of the second path argument "${toString path2}".'';
306 in
307 assert assertMsg (path1Deconstructed.root == path2Deconstructed.root) ''
308 lib.path.removePrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
309 first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
310 second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
311 joinRelPath components;
312
313 /*
314 Split the filesystem root from a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path).
315 The result is an attribute set with these attributes:
316 - `root`: The filesystem root of the path, meaning that this directory has no parent directory.
317 - `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.
318
319 Laws:
320 - [Appending](#function-library-lib.path.append) the `root` and `subpath` gives the original path:
321
322 p ==
323 append
324 (splitRoot p).root
325 (splitRoot p).subpath
326
327 - Trying to get the parent directory of `root` using [`readDir`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-readDir) returns `root` itself:
328
329 dirOf (splitRoot p).root == (splitRoot p).root
330
331 Type:
332 splitRoot :: Path -> { root :: Path, subpath :: String }
333
334 Example:
335 splitRoot /foo/bar
336 => { root = /.; subpath = "./foo/bar"; }
337
338 splitRoot /.
339 => { root = /.; subpath = "./."; }
340
341 # Nix neutralises `..` path components for all path values automatically
342 splitRoot /foo/../bar
343 => { root = /.; subpath = "./bar"; }
344
345 splitRoot "/foo/bar"
346 => <error>
347 */
348 splitRoot =
349 # The path to split the root off of
350 path:
351 assert assertMsg (isPath path)
352 "lib.path.splitRoot: Argument is of type ${typeOf path}, but a path was expected";
353 let
354 deconstructed = deconstructPath path;
355 in
356 {
357 root = deconstructed.root;
358 subpath = joinRelPath deconstructed.components;
359 };
360
361 /*
362 Whether a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path)
363 has a [store path](https://nixos.org/manual/nix/stable/store/store-path.html#store-path)
364 as a prefix.
365
366 :::{.note}
367 As with all functions of this `lib.path` library, it does not work on paths in strings,
368 which is how you'd typically get store paths.
369
370 Instead, this function only handles path values themselves,
371 which occur when Nix files in the store use relative path expressions.
372 :::
373
374 Type:
375 hasStorePathPrefix :: Path -> Bool
376
377 Example:
378 # Subpaths of derivation outputs have a store path as a prefix
379 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo/bar/baz
380 => true
381
382 # The store directory itself is not a store path
383 hasStorePathPrefix /nix/store
384 => false
385
386 # Derivation outputs are store paths themselves
387 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo
388 => true
389
390 # Paths outside the Nix store don't have a store path prefix
391 hasStorePathPrefix /home/user
392 => false
393
394 # Not all paths under the Nix store are store paths
395 hasStorePathPrefix /nix/store/.links/10gg8k3rmbw8p7gszarbk7qyd9jwxhcfq9i6s5i0qikx8alkk4hq
396 => false
397
398 # Store derivations are also store paths themselves
399 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo.drv
400 => true
401 */
402 hasStorePathPrefix =
403 path:
404 let
405 deconstructed = deconstructPath path;
406 in
407 assert assertMsg (isPath path)
408 "lib.path.hasStorePathPrefix: Argument is of type ${typeOf path}, but a path was expected";
409 assert assertMsg
410 # This function likely breaks or needs adjustment if used with other filesystem roots, if they ever get implemented.
411 # 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.
412 # See also https://github.com/NixOS/nix/pull/6530#discussion_r1422843117
413 (deconstructed.root == /. && toString deconstructed.root == "/")
414 "lib.path.hasStorePathPrefix: Argument has a filesystem root (${toString deconstructed.root}) that's not /, which is currently not supported.";
415 componentsHaveStorePathPrefix deconstructed.components;
416
417 /*
418 Whether a value is a valid subpath string.
419
420 A subpath string points to a specific file or directory within an absolute base directory.
421 It is a stricter form of a relative path that excludes `..` components, since those could escape the base directory.
422
423 - The value is a string.
424
425 - The string is not empty.
426
427 - The string doesn't start with a `/`.
428
429 - The string doesn't contain any `..` path components.
430
431 Type:
432 subpath.isValid :: String -> Bool
433
434 Example:
435 # Not a string
436 subpath.isValid null
437 => false
438
439 # Empty string
440 subpath.isValid ""
441 => false
442
443 # Absolute path
444 subpath.isValid "/foo"
445 => false
446
447 # Contains a `..` path component
448 subpath.isValid "../foo"
449 => false
450
451 # Valid subpath
452 subpath.isValid "foo/bar"
453 => true
454
455 # Doesn't need to be normalised
456 subpath.isValid "./foo//bar/"
457 => true
458 */
459 subpath.isValid =
460 # The value to check
461 value: subpathInvalidReason value == null;
462
463 /*
464 Join subpath strings together using `/`, returning a normalised subpath string.
465
466 Like `concatStringsSep "/"` but safer, specifically:
467
468 - All elements must be [valid subpath strings](#function-library-lib.path.subpath.isValid).
469
470 - The result gets [normalised](#function-library-lib.path.subpath.normalise).
471
472 - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`.
473
474 Laws:
475
476 - Associativity:
477
478 subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ]
479
480 - Identity - `"./."` is the neutral element for normalised paths:
481
482 subpath.join [ ] == "./."
483 subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
484 subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p
485
486 - Normalisation - the result is [normalised](#function-library-lib.path.subpath.normalise):
487
488 subpath.join ps == subpath.normalise (subpath.join ps)
489
490 - For non-empty lists, the implementation is equivalent to [normalising](#function-library-lib.path.subpath.normalise) the result of `concatStringsSep "/"`.
491 Note that the above laws can be derived from this one:
492
493 ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)
494
495 Type:
496 subpath.join :: [ String ] -> String
497
498 Example:
499 subpath.join [ "foo" "bar/baz" ]
500 => "./foo/bar/baz"
501
502 # normalise the result
503 subpath.join [ "./foo" "." "bar//./baz/" ]
504 => "./foo/bar/baz"
505
506 # passing an empty list results in the current directory
507 subpath.join [ ]
508 => "./."
509
510 # elements must be valid subpath strings
511 subpath.join [ /foo ]
512 => <error>
513 subpath.join [ "" ]
514 => <error>
515 subpath.join [ "/foo" ]
516 => <error>
517 subpath.join [ "../foo" ]
518 => <error>
519 */
520 subpath.join =
521 # The list of subpaths to join together
522 subpaths:
523 # Fast in case all paths are valid
524 if all isValid subpaths then
525 joinRelPath (concatMap splitRelPath subpaths)
526 else
527 # Otherwise we take our time to gather more info for a better error message
528 # Strictly go through each path, throwing on the first invalid one
529 # Tracks the list index in the fold accumulator
530 foldl' (
531 i: path:
532 if isValid path then
533 i + 1
534 else
535 throw ''
536 lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string:
537 ${subpathInvalidReason path}''
538 ) 0 subpaths;
539
540 /*
541 Split [a subpath](#function-library-lib.path.subpath.isValid) into its path component strings.
542 Throw an error if the subpath isn't valid.
543 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).
544
545 Laws:
546
547 - 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):
548
549 subpath.join (subpath.components s) == subpath.normalise s
550
551 Type:
552 subpath.components :: String -> [ String ]
553
554 Example:
555 subpath.components "."
556 => [ ]
557
558 subpath.components "./foo//bar/./baz/"
559 => [ "foo" "bar" "baz" ]
560
561 subpath.components "/foo"
562 => <error>
563 */
564 subpath.components =
565 # The subpath string to split into components
566 subpath:
567 assert assertMsg (isValid subpath) ''
568 lib.path.subpath.components: Argument is not a valid subpath string:
569 ${subpathInvalidReason subpath}'';
570 splitRelPath subpath;
571
572 /*
573 Normalise a subpath. Throw an error if the subpath isn't [valid](#function-library-lib.path.subpath.isValid).
574
575 - Limit repeating `/` to a single one.
576
577 - Remove redundant `.` components.
578
579 - Remove trailing `/` and `/.`.
580
581 - Add leading `./`.
582
583 Laws:
584
585 - Idempotency - normalising multiple times gives the same result:
586
587 subpath.normalise (subpath.normalise p) == subpath.normalise p
588
589 - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
590
591 subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
592
593 - Don't change the result when [appended](#function-library-lib.path.append) to a Nix path value:
594
595 append base p == append base (subpath.normalise p)
596
597 - Don't change the path according to `realpath`:
598
599 $(realpath ${p}) == $(realpath ${subpath.normalise p})
600
601 - Only error on [invalid subpaths](#function-library-lib.path.subpath.isValid):
602
603 builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
604
605 Type:
606 subpath.normalise :: String -> String
607
608 Example:
609 # limit repeating `/` to a single one
610 subpath.normalise "foo//bar"
611 => "./foo/bar"
612
613 # remove redundant `.` components
614 subpath.normalise "foo/./bar"
615 => "./foo/bar"
616
617 # add leading `./`
618 subpath.normalise "foo/bar"
619 => "./foo/bar"
620
621 # remove trailing `/`
622 subpath.normalise "foo/bar/"
623 => "./foo/bar"
624
625 # remove trailing `/.`
626 subpath.normalise "foo/bar/."
627 => "./foo/bar"
628
629 # Return the current directory as `./.`
630 subpath.normalise "."
631 => "./."
632
633 # error on `..` path components
634 subpath.normalise "foo/../bar"
635 => <error>
636
637 # error on empty string
638 subpath.normalise ""
639 => <error>
640
641 # error on absolute path
642 subpath.normalise "/foo"
643 => <error>
644 */
645 subpath.normalise =
646 # The subpath string to normalise
647 subpath:
648 assert assertMsg (isValid subpath) ''
649 lib.path.subpath.normalise: Argument is not a valid subpath string:
650 ${subpathInvalidReason subpath}'';
651 joinRelPath (splitRelPath subpath);
652
653}