at 23.11-beta 18 kB view raw
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}