1# Functions for working with paths, see ./path.md
2{ lib }:
3let
4
5 inherit (builtins)
6 isString
7 isPath
8 split
9 match
10 ;
11
12 inherit (lib.lists)
13 length
14 head
15 last
16 genList
17 elemAt
18 all
19 concatMap
20 foldl'
21 ;
22
23 inherit (lib.strings)
24 concatStringsSep
25 substring
26 ;
27
28 inherit (lib.asserts)
29 assertMsg
30 ;
31
32 inherit (lib.path.subpath)
33 isValid
34 ;
35
36 # Return the reason why a subpath is invalid, or `null` if it's valid
37 subpathInvalidReason = value:
38 if ! isString value then
39 "The given value is of type ${builtins.typeOf value}, but a string was expected"
40 else if value == "" then
41 "The given string is empty"
42 else if substring 0 1 value == "/" then
43 "The given string \"${value}\" starts with a `/`, representing an absolute path"
44 # We don't support ".." components, see ./path.md#parent-directory
45 else if match "(.*/)?\\.\\.(/.*)?" value != null then
46 "The given string \"${value}\" contains a `..` component, which is not allowed in subpaths"
47 else null;
48
49 # Split and normalise a relative path string into its components.
50 # Error for ".." components and doesn't include "." components
51 splitRelPath = path:
52 let
53 # Split the string into its parts using regex for efficiency. This regex
54 # matches patterns like "/", "/./", "/././", with arbitrarily many "/"s
55 # together. These are the main special cases:
56 # - Leading "./" gets split into a leading "." part
57 # - Trailing "/." or "/" get split into a trailing "." or ""
58 # part respectively
59 #
60 # These are the only cases where "." and "" parts can occur
61 parts = split "/+(\\./+)*" path;
62
63 # `split` creates a list of 2 * k + 1 elements, containing the k +
64 # 1 parts, interleaved with k matches where k is the number of
65 # (non-overlapping) matches. This calculation here gets the number of parts
66 # back from the list length
67 # floor( (2 * k + 1) / 2 ) + 1 == floor( k + 1/2 ) + 1 == k + 1
68 partCount = length parts / 2 + 1;
69
70 # To assemble the final list of components we want to:
71 # - Skip a potential leading ".", normalising "./foo" to "foo"
72 # - Skip a potential trailing "." or "", normalising "foo/" and "foo/." to
73 # "foo". See ./path.md#trailing-slashes
74 skipStart = if head parts == "." then 1 else 0;
75 skipEnd = if last parts == "." || last parts == "" then 1 else 0;
76
77 # We can now know the length of the result by removing the number of
78 # skipped parts from the total number
79 componentCount = partCount - skipEnd - skipStart;
80
81 in
82 # Special case of a single "." path component. Such a case leaves a
83 # componentCount of -1 due to the skipStart/skipEnd not verifying that
84 # they don't refer to the same character
85 if path == "." then []
86
87 # Generate the result list directly. This is more efficient than a
88 # combination of `filter`, `init` and `tail`, because here we don't
89 # allocate any intermediate lists
90 else genList (index:
91 # To get to the element we need to add the number of parts we skip and
92 # multiply by two due to the interleaved layout of `parts`
93 elemAt parts ((skipStart + index) * 2)
94 ) componentCount;
95
96 # Join relative path components together
97 joinRelPath = components:
98 # Always return relative paths with `./` as a prefix (./path.md#leading-dots-for-relative-paths)
99 "./" +
100 # An empty string is not a valid relative path, so we need to return a `.` when we have no components
101 (if components == [] then "." else concatStringsSep "/" components);
102
103in /* No rec! Add dependencies on this file at the top. */ {
104
105 /* Append a subpath string to a path.
106
107 Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
108 More specifically, it checks that the first argument is a [path value type](https://nixos.org/manual/nix/stable/language/values.html#type-path"),
109 and that the second argument is a valid subpath string (see `lib.path.subpath.isValid`).
110
111 Type:
112 append :: Path -> String -> Path
113
114 Example:
115 append /foo "bar/baz"
116 => /foo/bar/baz
117
118 # subpaths don't need to be normalised
119 append /foo "./bar//baz/./"
120 => /foo/bar/baz
121
122 # can append to root directory
123 append /. "foo/bar"
124 => /foo/bar
125
126 # first argument needs to be a path value type
127 append "/foo" "bar"
128 => <error>
129
130 # second argument needs to be a valid subpath string
131 append /foo /bar
132 => <error>
133 append /foo ""
134 => <error>
135 append /foo "/bar"
136 => <error>
137 append /foo "../bar"
138 => <error>
139 */
140 append =
141 # The absolute path to append to
142 path:
143 # The subpath string to append
144 subpath:
145 assert assertMsg (isPath path) ''
146 lib.path.append: The first argument is of type ${builtins.typeOf path}, but a path was expected'';
147 assert assertMsg (isValid subpath) ''
148 lib.path.append: Second argument is not a valid subpath string:
149 ${subpathInvalidReason subpath}'';
150 path + ("/" + subpath);
151
152 /* Whether a value is a valid subpath string.
153
154 - The value is a string
155
156 - The string is not empty
157
158 - The string doesn't start with a `/`
159
160 - The string doesn't contain any `..` path components
161
162 Type:
163 subpath.isValid :: String -> Bool
164
165 Example:
166 # Not a string
167 subpath.isValid null
168 => false
169
170 # Empty string
171 subpath.isValid ""
172 => false
173
174 # Absolute path
175 subpath.isValid "/foo"
176 => false
177
178 # Contains a `..` path component
179 subpath.isValid "../foo"
180 => false
181
182 # Valid subpath
183 subpath.isValid "foo/bar"
184 => true
185
186 # Doesn't need to be normalised
187 subpath.isValid "./foo//bar/"
188 => true
189 */
190 subpath.isValid =
191 # The value to check
192 value:
193 subpathInvalidReason value == null;
194
195
196 /* Join subpath strings together using `/`, returning a normalised subpath string.
197
198 Like `concatStringsSep "/"` but safer, specifically:
199
200 - All elements must be valid subpath strings, see `lib.path.subpath.isValid`
201
202 - The result gets normalised, see `lib.path.subpath.normalise`
203
204 - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`
205
206 Laws:
207
208 - Associativity:
209
210 subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ]
211
212 - Identity - `"./."` is the neutral element for normalised paths:
213
214 subpath.join [ ] == "./."
215 subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
216 subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p
217
218 - Normalisation - the result is normalised according to `lib.path.subpath.normalise`:
219
220 subpath.join ps == subpath.normalise (subpath.join ps)
221
222 - For non-empty lists, the implementation is equivalent to normalising the result of `concatStringsSep "/"`.
223 Note that the above laws can be derived from this one.
224
225 ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)
226
227 Type:
228 subpath.join :: [ String ] -> String
229
230 Example:
231 subpath.join [ "foo" "bar/baz" ]
232 => "./foo/bar/baz"
233
234 # normalise the result
235 subpath.join [ "./foo" "." "bar//./baz/" ]
236 => "./foo/bar/baz"
237
238 # passing an empty list results in the current directory
239 subpath.join [ ]
240 => "./."
241
242 # elements must be valid subpath strings
243 subpath.join [ /foo ]
244 => <error>
245 subpath.join [ "" ]
246 => <error>
247 subpath.join [ "/foo" ]
248 => <error>
249 subpath.join [ "../foo" ]
250 => <error>
251 */
252 subpath.join =
253 # The list of subpaths to join together
254 subpaths:
255 # Fast in case all paths are valid
256 if all isValid subpaths
257 then joinRelPath (concatMap splitRelPath subpaths)
258 else
259 # Otherwise we take our time to gather more info for a better error message
260 # Strictly go through each path, throwing on the first invalid one
261 # Tracks the list index in the fold accumulator
262 foldl' (i: path:
263 if isValid path
264 then i + 1
265 else throw ''
266 lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string:
267 ${subpathInvalidReason path}''
268 ) 0 subpaths;
269
270 /* Normalise a subpath. Throw an error if the subpath isn't valid, see
271 `lib.path.subpath.isValid`
272
273 - Limit repeating `/` to a single one
274
275 - Remove redundant `.` components
276
277 - Remove trailing `/` and `/.`
278
279 - Add leading `./`
280
281 Laws:
282
283 - Idempotency - normalising multiple times gives the same result:
284
285 subpath.normalise (subpath.normalise p) == subpath.normalise p
286
287 - Uniqueness - there's only a single normalisation for the paths that lead to the same file system node:
288
289 subpath.normalise p != subpath.normalise q -> $(realpath ${p}) != $(realpath ${q})
290
291 - Don't change the result when appended to a Nix path value:
292
293 base + ("/" + p) == base + ("/" + subpath.normalise p)
294
295 - Don't change the path according to `realpath`:
296
297 $(realpath ${p}) == $(realpath ${subpath.normalise p})
298
299 - Only error on invalid subpaths:
300
301 builtins.tryEval (subpath.normalise p)).success == subpath.isValid p
302
303 Type:
304 subpath.normalise :: String -> String
305
306 Example:
307 # limit repeating `/` to a single one
308 subpath.normalise "foo//bar"
309 => "./foo/bar"
310
311 # remove redundant `.` components
312 subpath.normalise "foo/./bar"
313 => "./foo/bar"
314
315 # add leading `./`
316 subpath.normalise "foo/bar"
317 => "./foo/bar"
318
319 # remove trailing `/`
320 subpath.normalise "foo/bar/"
321 => "./foo/bar"
322
323 # remove trailing `/.`
324 subpath.normalise "foo/bar/."
325 => "./foo/bar"
326
327 # Return the current directory as `./.`
328 subpath.normalise "."
329 => "./."
330
331 # error on `..` path components
332 subpath.normalise "foo/../bar"
333 => <error>
334
335 # error on empty string
336 subpath.normalise ""
337 => <error>
338
339 # error on absolute path
340 subpath.normalise "/foo"
341 => <error>
342 */
343 subpath.normalise =
344 # The subpath string to normalise
345 subpath:
346 assert assertMsg (isValid subpath) ''
347 lib.path.subpath.normalise: Argument is not a valid subpath string:
348 ${subpathInvalidReason subpath}'';
349 joinRelPath (splitRelPath subpath);
350
351}