Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at litex 351 lines 10 kB view raw
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}