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 # alternatively match content‐addressed derivations, which _currently_ do
164 # not have a store directory prefix.
165 # This is a workaround for https://github.com/NixOS/nix/issues/12361 which
166 # was needed during the experimental phase of ca-derivations and should be
167 # removed once the issue has been resolved.
168 || components != [ ] && match "[0-9a-z]{52}" (head components) != null;
169
170in
171# No rec! Add dependencies on this file at the top.
172{
173
174 /**
175 Append a subpath string to a path.
176
177 Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
178 More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"),
179 and that the second argument is a [valid subpath string](#function-library-lib.path.subpath.isValid).
180
181 Laws:
182
183 - Not influenced by subpath [normalisation](#function-library-lib.path.subpath.normalise):
184
185 append p s == append p (subpath.normalise s)
186
187 # Inputs
188
189 `path`
190
191 : The absolute path to append to
192
193 `subpath`
194
195 : The subpath string to append
196
197 # Type
198
199 ```
200 append :: Path -> String -> Path
201 ```
202
203 # Examples
204 :::{.example}
205 ## `append` usage example
206
207 ```nix
208 append /foo "bar/baz"
209 => /foo/bar/baz
210
211 # subpaths don't need to be normalised
212 append /foo "./bar//baz/./"
213 => /foo/bar/baz
214
215 # can append to root directory
216 append /. "foo/bar"
217 => /foo/bar
218
219 # first argument needs to be a path value type
220 append "/foo" "bar"
221 => <error>
222
223 # second argument needs to be a valid subpath string
224 append /foo /bar
225 => <error>
226 append /foo ""
227 => <error>
228 append /foo "/bar"
229 => <error>
230 append /foo "../bar"
231 => <error>
232 ```
233
234 :::
235 */
236 append =
237 # The absolute path to append to
238 path:
239 # The subpath string to append
240 subpath:
241 assert assertMsg (isPath path)
242 ''lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected'';
243 assert assertMsg (isValid subpath) ''
244 lib.path.append: Second argument is not a valid subpath string:
245 ${subpathInvalidReason subpath}'';
246 path + ("/" + subpath);
247
248 /**
249 Whether the first path is a component-wise prefix of the second path.
250
251 Laws:
252
253 - `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`.
254
255 - `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.
256
257 # Inputs
258
259 `path1`
260
261 : 1\. Function argument
262
263 # Type
264
265 ```
266 hasPrefix :: Path -> Path -> Bool
267 ```
268
269 # Examples
270 :::{.example}
271 ## `hasPrefix` usage example
272
273 ```nix
274 hasPrefix /foo /foo/bar
275 => true
276 hasPrefix /foo /foo
277 => true
278 hasPrefix /foo/bar /foo
279 => false
280 hasPrefix /. /foo
281 => true
282 ```
283
284 :::
285 */
286 hasPrefix =
287 path1:
288 assert assertMsg (isPath path1)
289 "lib.path.hasPrefix: First argument is of type ${typeOf path1}, but a path was expected";
290 let
291 path1Deconstructed = deconstructPath path1;
292 in
293 path2:
294 assert assertMsg (isPath path2)
295 "lib.path.hasPrefix: Second argument is of type ${typeOf path2}, but a path was expected";
296 let
297 path2Deconstructed = deconstructPath path2;
298 in
299 assert assertMsg (path1Deconstructed.root == path2Deconstructed.root) ''
300 lib.path.hasPrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
301 first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
302 second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
303 take (length path1Deconstructed.components) path2Deconstructed.components
304 == path1Deconstructed.components;
305
306 /**
307 Remove the first path as a component-wise prefix from the second path.
308 The result is a [normalised subpath string](#function-library-lib.path.subpath.normalise).
309
310 Laws:
311
312 - Inverts [`append`](#function-library-lib.path.append) for [normalised subpath string](#function-library-lib.path.subpath.normalise):
313
314 removePrefix p (append p s) == subpath.normalise s
315
316 # Inputs
317
318 `path1`
319
320 : 1\. Function argument
321
322 # Type
323
324 ```
325 removePrefix :: Path -> Path -> String
326 ```
327
328 # Examples
329 :::{.example}
330 ## `removePrefix` usage example
331
332 ```nix
333 removePrefix /foo /foo/bar/baz
334 => "./bar/baz"
335 removePrefix /foo /foo
336 => "./."
337 removePrefix /foo/bar /foo
338 => <error>
339 removePrefix /. /foo
340 => "./foo"
341 ```
342
343 :::
344 */
345 removePrefix =
346 path1:
347 assert assertMsg (isPath path1)
348 "lib.path.removePrefix: First argument is of type ${typeOf path1}, but a path was expected.";
349 let
350 path1Deconstructed = deconstructPath path1;
351 path1Length = length path1Deconstructed.components;
352 in
353 path2:
354 assert assertMsg (isPath path2)
355 "lib.path.removePrefix: Second argument is of type ${typeOf path2}, but a path was expected.";
356 let
357 path2Deconstructed = deconstructPath path2;
358 success = take path1Length path2Deconstructed.components == path1Deconstructed.components;
359 components =
360 if success then
361 drop path1Length path2Deconstructed.components
362 else
363 throw ''lib.path.removePrefix: The first path argument "${toString path1}" is not a component-wise prefix of the second path argument "${toString path2}".'';
364 in
365 assert assertMsg (path1Deconstructed.root == path2Deconstructed.root) ''
366 lib.path.removePrefix: Filesystem roots must be the same for both paths, but paths with different roots were given:
367 first argument: "${toString path1}" with root "${toString path1Deconstructed.root}"
368 second argument: "${toString path2}" with root "${toString path2Deconstructed.root}"'';
369 joinRelPath components;
370
371 /**
372 Split the filesystem root from a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path).
373 The result is an attribute set with these attributes:
374 - `root`: The filesystem root of the path, meaning that this directory has no parent directory.
375 - `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.
376
377 Laws:
378 - [Appending](#function-library-lib.path.append) the `root` and `subpath` gives the original path:
379
380 p ==
381 append
382 (splitRoot p).root
383 (splitRoot p).subpath
384
385 - Trying to get the parent directory of `root` using [`dirOf`](https://nixos.org/manual/nix/stable/language/builtins.html#builtins-dirOf) returns `root` itself:
386
387 dirOf (splitRoot p).root == (splitRoot p).root
388
389 # Inputs
390
391 `path`
392
393 : The path to split the root off of
394
395 # Type
396
397 ```
398 splitRoot :: Path -> { root :: Path, subpath :: String }
399 ```
400
401 # Examples
402 :::{.example}
403 ## `splitRoot` usage example
404
405 ```nix
406 splitRoot /foo/bar
407 => { root = /.; subpath = "./foo/bar"; }
408
409 splitRoot /.
410 => { root = /.; subpath = "./."; }
411
412 # Nix neutralises `..` path components for all path values automatically
413 splitRoot /foo/../bar
414 => { root = /.; subpath = "./bar"; }
415
416 splitRoot "/foo/bar"
417 => <error>
418 ```
419
420 :::
421 */
422 splitRoot =
423 # The path to split the root off of
424 path:
425 assert assertMsg (isPath path)
426 "lib.path.splitRoot: Argument is of type ${typeOf path}, but a path was expected";
427 let
428 deconstructed = deconstructPath path;
429 in
430 {
431 root = deconstructed.root;
432 subpath = joinRelPath deconstructed.components;
433 };
434
435 /**
436 Whether a [path](https://nixos.org/manual/nix/stable/language/values.html#type-path)
437 has a [store path](https://nixos.org/manual/nix/stable/store/store-path.html#store-path)
438 as a prefix.
439
440 :::{.note}
441 As with all functions of this `lib.path` library, it does not work on paths in strings,
442 which is how you'd typically get store paths.
443
444 Instead, this function only handles path values themselves,
445 which occur when Nix files in the store use relative path expressions.
446 :::
447
448 # Inputs
449
450 `path`
451
452 : 1\. Function argument
453
454 # Type
455
456 ```
457 hasStorePathPrefix :: Path -> Bool
458 ```
459
460 # Examples
461 :::{.example}
462 ## `hasStorePathPrefix` usage example
463
464 ```nix
465 # Subpaths of derivation outputs have a store path as a prefix
466 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo/bar/baz
467 => true
468
469 # The store directory itself is not a store path
470 hasStorePathPrefix /nix/store
471 => false
472
473 # Derivation outputs are store paths themselves
474 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo
475 => true
476
477 # Paths outside the Nix store don't have a store path prefix
478 hasStorePathPrefix /home/user
479 => false
480
481 # Not all paths under the Nix store are store paths
482 hasStorePathPrefix /nix/store/.links/10gg8k3rmbw8p7gszarbk7qyd9jwxhcfq9i6s5i0qikx8alkk4hq
483 => false
484
485 # Store derivations are also store paths themselves
486 hasStorePathPrefix /nix/store/nvl9ic0pj1fpyln3zaqrf4cclbqdfn1j-foo.drv
487 => true
488 ```
489
490 :::
491 */
492 hasStorePathPrefix =
493 path:
494 let
495 deconstructed = deconstructPath path;
496 in
497 assert assertMsg (isPath path)
498 "lib.path.hasStorePathPrefix: Argument is of type ${typeOf path}, but a path was expected";
499 assert assertMsg
500 # This function likely breaks or needs adjustment if used with other filesystem roots, if they ever get implemented.
501 # 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.
502 # See also https://github.com/NixOS/nix/pull/6530#discussion_r1422843117
503 (deconstructed.root == /. && toString deconstructed.root == "/")
504 "lib.path.hasStorePathPrefix: Argument has a filesystem root (${toString deconstructed.root}) that's not /, which is currently not supported.";
505 componentsHaveStorePathPrefix deconstructed.components;
506
507 /**
508 Whether a value is a valid subpath string.
509
510 A subpath string points to a specific file or directory within an absolute base directory.
511 It is a stricter form of a relative path that excludes `..` components, since those could escape the base directory.
512
513 - The value is a string.
514
515 - The string is not empty.
516
517 - The string doesn't start with a `/`.
518
519 - The string doesn't contain any `..` path components.
520
521 # Inputs
522
523 `value`
524
525 : The value to check
526
527 # Type
528
529 ```
530 subpath.isValid :: String -> Bool
531 ```
532
533 # Examples
534 :::{.example}
535 ## `subpath.isValid` usage example
536
537 ```nix
538 # Not a string
539 subpath.isValid null
540 => false
541
542 # Empty string
543 subpath.isValid ""
544 => false
545
546 # Absolute path
547 subpath.isValid "/foo"
548 => false
549
550 # Contains a `..` path component
551 subpath.isValid "../foo"
552 => false
553
554 # Valid subpath
555 subpath.isValid "foo/bar"
556 => true
557
558 # Doesn't need to be normalised
559 subpath.isValid "./foo//bar/"
560 => true
561 ```
562
563 :::
564 */
565 subpath.isValid =
566 # The value to check
567 value: subpathInvalidReason value == null;
568
569 /**
570 Join subpath strings together using `/`, returning a normalised subpath string.
571
572 Like `concatStringsSep "/"` but safer, specifically:
573
574 - All elements must be [valid subpath strings](#function-library-lib.path.subpath.isValid).
575
576 - The result gets [normalised](#function-library-lib.path.subpath.normalise).
577
578 - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`.
579
580 Laws:
581
582 - Associativity:
583
584 subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ]
585
586 - Identity - `"./."` is the neutral element for normalised paths:
587
588 subpath.join [ ] == "./."
589 subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
590 subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p
591
592 - Normalisation - the result is [normalised](#function-library-lib.path.subpath.normalise):
593
594 subpath.join ps == subpath.normalise (subpath.join ps)
595
596 - For non-empty lists, the implementation is equivalent to [normalising](#function-library-lib.path.subpath.normalise) the result of `concatStringsSep "/"`.
597 Note that the above laws can be derived from this one:
598
599 ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)
600
601 # Inputs
602
603 `subpaths`
604
605 : The list of subpaths to join together
606
607 # Type
608
609 ```
610 subpath.join :: [ String ] -> String
611 ```
612
613 # Examples
614 :::{.example}
615 ## `subpath.join` usage example
616
617 ```nix
618 subpath.join [ "foo" "bar/baz" ]
619 => "./foo/bar/baz"
620
621 # normalise the result
622 subpath.join [ "./foo" "." "bar//./baz/" ]
623 => "./foo/bar/baz"
624
625 # passing an empty list results in the current directory
626 subpath.join [ ]
627 => "./."
628
629 # elements must be valid subpath strings
630 subpath.join [ /foo ]
631 => <error>
632 subpath.join [ "" ]
633 => <error>
634 subpath.join [ "/foo" ]
635 => <error>
636 subpath.join [ "../foo" ]
637 => <error>
638 ```
639
640 :::
641 */
642 subpath.join =
643 # The list of subpaths to join together
644 subpaths:
645 # Fast in case all paths are valid
646 if all isValid subpaths then
647 joinRelPath (concatMap splitRelPath subpaths)
648 else
649 # Otherwise we take our time to gather more info for a better error message
650 # Strictly go through each path, throwing on the first invalid one
651 # Tracks the list index in the fold accumulator
652 foldl' (
653 i: path:
654 if isValid path then
655 i + 1
656 else
657 throw ''
658 lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string:
659 ${subpathInvalidReason path}''
660 ) 0 subpaths;
661
662 /**
663 Split [a subpath](#function-library-lib.path.subpath.isValid) into its path component strings.
664 Throw an error if the subpath isn't valid.
665 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).
666
667 Laws:
668
669 - 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):
670
671 subpath.join (subpath.components s) == subpath.normalise s
672
673 # Inputs
674
675 `subpath`
676
677 : The subpath string to split into components
678
679 # Type
680
681 ```
682 subpath.components :: String -> [ String ]
683 ```
684
685 # Examples
686 :::{.example}
687 ## `subpath.components` usage example
688
689 ```nix
690 subpath.components "."
691 => [ ]
692
693 subpath.components "./foo//bar/./baz/"
694 => [ "foo" "bar" "baz" ]
695
696 subpath.components "/foo"
697 => <error>
698 ```
699
700 :::
701 */
702 subpath.components =
703 # The subpath string to split into components
704 subpath:
705 assert assertMsg (isValid subpath) ''
706 lib.path.subpath.components: Argument is not a valid subpath string:
707 ${subpathInvalidReason subpath}'';
708 splitRelPath subpath;
709
710 /**
711 Normalise a subpath. Throw an error if the subpath isn't [valid](#function-library-lib.path.subpath.isValid).
712
713 - Limit repeating `/` to a single one.
714
715 - Remove redundant `.` components.
716
717 - Remove trailing `/` and `/.`.
718
719 - Add leading `./`.
720
721 Laws:
722
723 - Idempotency - normalising multiple times gives the same result:
724
725 subpath.normalise (subpath.normalise p) == subpath.normalise p
726
727 - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
728
729 subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
730
731 - Don't change the result when [appended](#function-library-lib.path.append) to a Nix path value:
732
733 append base p == append base (subpath.normalise p)
734
735 - Don't change the path according to `realpath`:
736
737 $(realpath ${p}) == $(realpath ${subpath.normalise p})
738
739 - Only error on [invalid subpaths](#function-library-lib.path.subpath.isValid):
740
741 builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
742
743 # Inputs
744
745 `subpath`
746
747 : The subpath string to normalise
748
749 # Type
750
751 ```
752 subpath.normalise :: String -> String
753 ```
754
755 # Examples
756 :::{.example}
757 ## `subpath.normalise` usage example
758
759 ```nix
760 # limit repeating `/` to a single one
761 subpath.normalise "foo//bar"
762 => "./foo/bar"
763
764 # remove redundant `.` components
765 subpath.normalise "foo/./bar"
766 => "./foo/bar"
767
768 # add leading `./`
769 subpath.normalise "foo/bar"
770 => "./foo/bar"
771
772 # remove trailing `/`
773 subpath.normalise "foo/bar/"
774 => "./foo/bar"
775
776 # remove trailing `/.`
777 subpath.normalise "foo/bar/."
778 => "./foo/bar"
779
780 # Return the current directory as `./.`
781 subpath.normalise "."
782 => "./."
783
784 # error on `..` path components
785 subpath.normalise "foo/../bar"
786 => <error>
787
788 # error on empty string
789 subpath.normalise ""
790 => <error>
791
792 # error on absolute path
793 subpath.normalise "/foo"
794 => <error>
795 ```
796
797 :::
798 */
799 subpath.normalise =
800 # The subpath string to normalise
801 subpath:
802 assert assertMsg (isValid subpath) ''
803 lib.path.subpath.normalise: Argument is not a valid subpath string:
804 ${subpathInvalidReason subpath}'';
805 joinRelPath (splitRelPath subpath);
806
807}