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