at master 24 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 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}