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