This a test repository for the unpac monorepo tool
at opam/upstream/fpath 768 lines 28 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2015 The fpath programmers. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6open Astring 7 8(* Unsafe string and byte manipulations. If you don't believe the 9 author's invariants, replacing with safe versions makes everything 10 safe in the library. He won't be upset. *) 11 12let bytes_unsafe_set = Bytes.unsafe_set 13let string_unsafe_get = String.unsafe_get 14 15(* Errors *) 16 17let err_invalid_seg s = strf "%a: invalid segment" String.dump s 18let err_invalid_ext s = strf "%a: invalid extension" String.dump s 19 20(* A few useful constants *) 21 22let windows = Sys.os_type = "Win32" 23let dir_sep_char = if windows then '\\' else '/' 24let dir_sep = String.of_char dir_sep_char 25let dir_sep_sub = String.sub dir_sep 26let not_dir_sep c = c <> dir_sep_char 27 28let dot = "." 29let dot_dir = dot ^ dir_sep 30let dot_dir_sub = String.sub dot_dir 31let dotdot = ".." 32let dotdot_dir = dotdot ^ dir_sep 33let dotdot_dir_sub = String.sub dotdot_dir 34 35(* Platform specific preliminaties *) 36 37module Windows = struct 38 39 let is_unc_path p = String.is_prefix ~affix:"\\\\" p 40 let has_drive p = String.exists (Char.equal ':') p 41 let non_unc_path_start p = match String.find (Char.equal ':') p with 42 | None -> 0 43 | Some i -> i + 1 (* exists by construction *) 44 45 let parse_unc s = 46 (* parses an UNC path, the \\ prefix was already parsed, adds a root path 47 if there's only a volume, UNC paths are always absolute. *) 48 let p = String.sub ~start:2 s in 49 let not_bslash c = c <> '\\' in 50 let parse_seg p = String.Sub.span ~min:1 ~sat:not_bslash p in 51 let ensure_root r = Some (if String.Sub.is_empty r then (s ^ "\\") else s) 52 in 53 match parse_seg p with 54 | (seg1, _) when String.Sub.is_empty seg1 -> None (* \\ or \\\ *) 55 | (seg1, rest) -> 56 let seg1_len = String.Sub.length seg1 in 57 match String.Sub.get_head ~rev:true seg1 with 58 | '.' when seg1_len = 1 -> (* \\.\device\ *) 59 begin match parse_seg (String.Sub.tail rest) with 60 | (seg, _) when String.Sub.is_empty seg -> None 61 | (_, rest) -> ensure_root rest 62 end 63 | '?' when seg1_len = 1 -> 64 begin match parse_seg (String.Sub.tail rest) with 65 | (seg2, _) when String.Sub.is_empty seg2 -> None 66 | (seg2, rest) -> 67 if (String.Sub.get_head ~rev:true seg2 = ':') (* \\?\drive:\ *) 68 then (ensure_root rest) else 69 if not (String.Sub.equal_bytes seg2 (String.sub "UNC")) 70 then begin (* \\?\server\share\ *) 71 match parse_seg (String.Sub.tail rest) with 72 | (seg, _) when String.Sub.is_empty seg -> None 73 | (_, rest) -> ensure_root rest 74 end else begin (* \\?\UNC\server\share\ *) 75 match parse_seg (String.Sub.tail rest) with 76 | (seg, _) when String.Sub.is_empty seg -> None 77 | (_, rest) -> 78 match parse_seg (String.Sub.tail rest) with 79 | (seg, _) when String.Sub.is_empty seg -> None 80 | (_, rest) -> ensure_root rest 81 end 82 end 83 | _ -> (* \\server\share\ *) 84 begin match parse_seg (String.Sub.tail rest) with 85 | (seg, _) when String.Sub.is_empty seg -> None 86 | (_, rest) -> ensure_root rest 87 end 88 89 let sub_split_volume p = 90 (* splits a windows path into its volume (or drive) and actual file 91 path. When called the path in [p] is guaranteed to be non empty 92 and if [p] is an UNC path it is guaranteed to the be parseable by 93 parse_unc_windows. *) 94 let split_before i = String.sub p ~stop:i, String.sub p ~start:i in 95 if not (is_unc_path p) then 96 begin match String.find (Char.equal ':') p with 97 | None -> String.Sub.empty, String.sub p 98 | Some i -> split_before (i + 1) 99 end 100 else 101 let bslash ~start = match String.find ~start (Char.equal '\\') p with 102 | None -> assert false | Some i -> i 103 in 104 let i = bslash ~start:2 in 105 let j = bslash ~start:(i + 1) in 106 match p.[i-1] with 107 | '.' when i = 3 -> split_before j 108 | '?' when i = 3 -> 109 if p.[j-1] = ':' then split_before j else 110 if (String.Sub.equal_bytes 111 (String.sub p ~start:(i + 1) ~stop:j) 112 (String.sub "UNC")) 113 then split_before (bslash ~start:((bslash ~start:(j + 1)) + 1)) 114 else split_before (bslash ~start:(j + 1)) 115 | _ -> split_before j 116 117 let is_root p = 118 let _, path = sub_split_volume p in 119 String.Sub.length path = 1 && String.Sub.get path 0 = dir_sep_char 120end 121 122module Posix = struct 123 let has_volume p = String.is_prefix ~affix:"//" p 124 let is_root p = String.equal p dir_sep || String.equal p "//" 125end 126 127(* Segments *) 128 129let is_seg_windows s = 130 let valid c = c <> '\x00' && c <> dir_sep_char && c <> '/' in 131 String.for_all valid s 132 133let is_seg_posix s = 134 let valid c = c <> '\x00' && c <> dir_sep_char in 135 String.for_all valid s 136 137let is_seg = if windows then is_seg_windows else is_seg_posix 138 139let _split_last_seg p = String.Sub.span ~rev:true ~sat:not_dir_sep p 140let _sub_last_seg p = String.Sub.take ~rev:true ~sat:not_dir_sep p 141let _sub_last_non_empty_seg p = (* returns empty on roots though *) 142 let dir, last = _split_last_seg p in 143 match String.Sub.is_empty last with 144 | false -> last 145 | true -> _sub_last_seg (String.Sub.tail ~rev:true dir) 146 147let _split_last_non_empty_seg p = 148 let (dir, last_seg as r) = _split_last_seg p in 149 match String.Sub.is_empty last_seg with 150 | false -> r, true 151 | true -> _split_last_seg (String.Sub.tail ~rev:true dir), false 152 153let sub_last_seg_windows p = _sub_last_seg (snd (Windows.sub_split_volume p)) 154let sub_last_seg_posix p = _sub_last_seg (String.sub p) 155let sub_last_seg = if windows then sub_last_seg_windows else sub_last_seg_posix 156 157let sub_last_non_empty_seg_windows p = 158 _sub_last_non_empty_seg (snd (Windows.sub_split_volume p)) 159 160let sub_last_non_empty_seg_posix p = 161 _sub_last_non_empty_seg (String.sub p) 162 163let sub_last_non_empty_seg = 164 if windows then sub_last_non_empty_seg_windows else 165 sub_last_non_empty_seg_posix 166 167let is_rel_seg = function "." | ".." -> true | _ -> false 168 169let sub_is_rel_seg seg = match String.Sub.length seg with 170| 1 when String.Sub.get seg 0 = '.' -> true 171| 2 when String.Sub.get seg 0 = '.' && String.Sub.get seg 1 = '.' -> true 172| _ -> false 173 174let sub_is_dir_seg seg = match String.Sub.length seg with 175| 0 -> true 176| 1 when String.Sub.get seg 0 = '.' -> true 177| 2 when String.Sub.get seg 0 = '.' && String.Sub.get seg 1 = '.' -> true 178| _ -> false 179 180let segs_of_path p = String.cuts ~sep:dir_sep p 181let segs_to_path segs = String.concat ~sep:dir_sep segs 182 183(* File paths *) 184 185type t = string (* N.B. a path is never "" or something is wrooong. *) 186 187let err s = Error (`Msg (strf "%a: invalid path" String.dump s)) 188 189let validate_and_collapse_seps p = 190 (* collapse non-initial sequences of [dir_sep] to a single one and checks 191 no null byte *) 192 let max_idx = String.length p - 1 in 193 let rec with_buf b last_sep k i = (* k is the write index in b *) 194 if i > max_idx then Ok (Bytes.sub_string b 0 k) else 195 let c = string_unsafe_get p i in 196 if c = '\x00' then err p else 197 if c <> dir_sep_char 198 then (bytes_unsafe_set b k c; with_buf b false (k + 1) (i + 1)) else 199 if not last_sep 200 then (bytes_unsafe_set b k c; with_buf b true (k + 1) (i + 1)) else 201 with_buf b true k (i + 1) 202 in 203 let rec try_no_alloc last_sep i = 204 if i > max_idx then Ok p else 205 let c = string_unsafe_get p i in 206 if c = '\x00' then err p else 207 if c <> dir_sep_char then try_no_alloc false (i + 1) else 208 if not last_sep then try_no_alloc true (i + 1) else 209 let b = Bytes.of_string p in (* copy and overwrite starting from i *) 210 with_buf b true i (i + 1) 211 in 212 let start = (* Allow initial double sep for POSIX and UNC paths *) 213 if max_idx > 0 then (if p.[0] = dir_sep_char then 1 else 0) else 0 214 in 215 try_no_alloc false start 216 217let of_string_windows s = 218 if s = "" then err s else 219 let p = String.map (fun c -> if c = '/' then '\\' else c) s in 220 match validate_and_collapse_seps p with 221 | Error _ as e -> e 222 | Ok p as some -> 223 if Windows.is_unc_path p then 224 (match Windows.parse_unc p with None -> err s | Some p -> Ok p) 225 else 226 match String.find (Char.equal ':') p with 227 | None -> some 228 | Some i when i = String.length p - 1 -> err p (* path is empty *) 229 | Some _ -> Ok p 230 231let of_string_posix p = if p = "" then err p else validate_and_collapse_seps p 232let of_string = if windows then of_string_windows else of_string_posix 233 234let v s = match of_string s with 235| Ok p -> p 236| Error (`Msg m) -> invalid_arg m 237 238let add_seg p seg = 239 if not (is_seg seg) then invalid_arg (err_invalid_seg seg); 240 let sep = if p.[String.length p - 1] = dir_sep_char then "" else dir_sep in 241 String.concat ~sep [p; seg] 242 243let append_posix p0 p1 = 244 if p1.[0] = dir_sep_char (* absolute *) then p1 else 245 let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in 246 String.concat ~sep [p0; p1] 247 248let append_windows p0 p1 = 249 if Windows.is_unc_path p1 || Windows.has_drive p1 then p1 else 250 if p1.[0] = dir_sep_char then (* absolute *) p1 else 251 let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in 252 String.concat ~sep [p0; p1] 253 254let append = if windows then append_windows else append_posix 255 256let ( / ) = add_seg 257let ( // ) = append 258 259let split_volume_windows p = 260 let vol, path = Windows.sub_split_volume p in 261 String.Sub.to_string vol, String.Sub.to_string path 262 263let split_volume_posix p = 264 if Posix.has_volume p then dir_sep, String.with_range ~first:1 p else "", p 265 266let split_volume = if windows then split_volume_windows else split_volume_posix 267 268let segs_windows p = 269 let _, path = Windows.sub_split_volume p in 270 segs_of_path (String.Sub.to_string path) 271 272let segs_posix p = 273 let segs = segs_of_path p in 274 if Posix.has_volume p then List.tl segs else segs 275 276let segs = if windows then segs_windows else segs_posix 277 278(* File and directory paths *) 279 280let is_dir_path p = sub_is_dir_seg (sub_last_seg p) 281let is_file_path p = not (is_dir_path p) 282let to_dir_path p = add_seg p "" 283 284let filename p = match String.Sub.to_string (sub_last_seg p) with 285| "" | "." | ".." -> "" 286| filename -> filename 287 288(* Base and parent paths *) 289 290let sub_is_root p = String.Sub.length p = 1 && String.Sub.get p 0 = dir_sep_char 291 292let _split_base p = 293 let dir, last_seg = _split_last_seg p in 294 match String.Sub.is_empty dir with 295 | true -> (* single seg *) dot_dir_sub, String.Sub.to_string p 296 | false -> 297 match String.Sub.is_empty last_seg with 298 | false -> dir, String.Sub.to_string last_seg 299 | true -> 300 let dir_file = String.Sub.tail ~rev:true dir in 301 let dir, dir_last_seg = _split_last_seg dir_file in 302 match String.Sub.is_empty dir with 303 | true -> dot_dir_sub, String.Sub.to_string p 304 | false -> dir, String.Sub.(to_string (extend dir_last_seg)) 305 306let split_base_windows p = 307 let vol, path = Windows.sub_split_volume p in 308 if sub_is_root path then p, dot_dir else 309 let dir, b = _split_base path in 310 String.Sub.(base_string (append vol dir)), b 311 312let split_base_posix p = 313 if Posix.is_root p then p, dot_dir else 314 let dir, b = _split_base (String.sub p) in 315 String.Sub.to_string dir, b 316 317let split_base = if windows then split_base_windows else split_base_posix 318 319let base p = snd (split_base p) 320 321let _basename p = match String.Sub.to_string (_sub_last_non_empty_seg p) with 322| "." | ".." -> "" 323| basename -> basename 324 325let basename_windows p = 326 let vol, path = Windows.sub_split_volume p in 327 if sub_is_root path then "" else _basename path 328 329let basename_posix p = if Posix.is_root p then "" else _basename (String.sub p) 330let basename p = if windows then basename_windows p else basename_posix p 331 332let _parent p = 333 (* The parent algorithm is not very smart. It tries to preserve the 334 original path and avoids dealing with normalization. We simply 335 only keep everything before the last non-empty, non-relative, 336 path segment and if the resulting path is empty we return 337 "./". Otherwise if the last non-empty segment is "." or ".." we 338 simply postfix with "../" *) 339 let (dir, seg), is_last = _split_last_non_empty_seg p in 340 let dsep = if is_last then dir_sep_sub else String.Sub.empty in 341 if sub_is_rel_seg seg then [p; dsep; dotdot_dir_sub] else 342 if String.Sub.is_empty dir then [dot_dir_sub] else [dir] 343 344let parent_windows p = 345 let vol, path = Windows.sub_split_volume p in 346 if sub_is_root path then p else 347 String.Sub.(base_string @@ concat (vol :: _parent path)) 348 349let parent_posix p = 350 if Posix.is_root p then p else 351 String.Sub.(base_string @@ concat (_parent (String.sub p))) 352 353let parent = if windows then parent_windows else parent_posix 354 355(* Normalization *) 356 357let rem_empty_seg_windows p = 358 let vol, path = Windows.sub_split_volume p in 359 if sub_is_root path then p else 360 let max = String.Sub.stop_pos path - 1 in 361 if String.get p max <> dir_sep_char then p else 362 String.with_index_range p ~last:(max - 1) 363 364let rem_empty_seg_posix p = match String.length p with 365| 1 -> p 366| 2 -> 367 if p.[0] <> dir_sep_char && p.[1] = dir_sep_char 368 then String.of_char p.[0] 369 else p 370| len -> 371 let max = len - 1 in 372 if p.[max] <> dir_sep_char then p else 373 String.with_index_range p ~last:(max - 1) 374 375let rem_empty_seg = 376 if windows then rem_empty_seg_windows else rem_empty_seg_posix 377 378let normalize_rel_segs segs = (* result is non empty but may be [""] *) 379 let rec loop acc = function 380 | "." :: [] -> ("" :: acc) (* final "." remove but preserve directoryness. *) 381 | "." :: rest -> loop acc rest 382 | ".." :: rest -> 383 begin match acc with 384 | ".." :: _ | [] -> loop (".." :: acc) rest 385 | seg :: acc -> (* N.B. seg can't be "." *) 386 match rest with 387 | [] -> ("" :: acc) (* preserve directoryness *) 388 | rest -> loop acc rest 389 end 390 | seg :: rest -> loop (seg :: acc) rest 391 | [] -> 392 match acc with 393 | ".." :: _ -> ("" :: acc) (* normalize final .. to ../ *) 394 | [] -> [""] 395 | acc -> acc 396 in 397 List.rev (loop [] segs) 398 399let normalize_segs = function 400| "" :: segs -> (* absolute path *) 401 let rec rem_dotdots = function ".." :: ss -> rem_dotdots ss | ss -> ss in 402 "" :: (rem_dotdots @@ normalize_rel_segs segs) 403| segs -> 404 match normalize_rel_segs segs with 405 | [""] -> ["."; ""] 406 | segs -> segs 407 408let normalize_windows p = 409 let vol, path = Windows.sub_split_volume p in 410 let path = String.Sub.to_string path in 411 let path = segs_to_path @@ normalize_segs (segs_of_path path) in 412 String.Sub.(to_string (concat [vol; String.sub path])) 413 414let normalize_posix p = 415 let has_volume = Posix.has_volume p in 416 let segs = segs_of_path p in 417 let segs = normalize_segs @@ if has_volume then List.tl segs else segs in 418 let segs = if has_volume then "" :: segs else segs in 419 segs_to_path segs 420 421let normalize = if windows then normalize_windows else normalize_posix 422 423(* Prefixes *) 424 425let is_prefix prefix p = 426 if not (String.is_prefix ~affix:prefix p) then false else 427 (* Further check the prefix is segment-based. If [prefix] ends with a 428 dir_sep_char nothing more needs to be checked. If it doesn't we need 429 to check that [p]'s remaining suffix is either empty or 430 starts with a directory separator. *) 431 let suff_start = String.length prefix in 432 if prefix.[suff_start - 1] = dir_sep_char then true else 433 if suff_start = String.length p then (* suffix empty *) true else 434 p.[suff_start] = dir_sep_char 435 436let _prefix_last_index p0 p1 = (* last char index of segment-based prefix *) 437 let l0 = String.length p0 in 438 let l1 = String.length p1 in 439 let p0, p1, max = if l0 < l1 then p0, p1, l0 - 1 else p1, p0, l1 - 1 in 440 let rec loop last_dir_sep i p0 p1 = match i > max || p0.[i] <> p1.[i] with 441 | false -> 442 let last_dir_sep = if p0.[i] = dir_sep_char then i else last_dir_sep in 443 loop last_dir_sep (i + 1) p0 p1 444 | true -> 445 if i = 0 then None else 446 let last = i - 1 in 447 if last_dir_sep = last then Some last else 448 match last = max with 449 | true -> 450 if l1 = l0 then Some last else 451 if p1.[i] = dir_sep_char then Some last else 452 if last_dir_sep <> -1 then Some last_dir_sep else None 453 | false -> 454 if last_dir_sep <> -1 then Some last_dir_sep else None 455 in 456 loop (-1) 0 p0 p1 457 458let find_prefix_windows p0 p1 = match _prefix_last_index p0 p1 with 459| None -> None 460| Some i -> 461 let v0_len = String.Sub.length (fst (Windows.sub_split_volume p0)) in 462 let v1_len = String.Sub.length (fst (Windows.sub_split_volume p1)) in 463 let max_vlen = if v0_len > v1_len then v0_len else v1_len in 464 if i < max_vlen then None else Some (String.with_index_range p0 ~last:i) 465 466let find_prefix_posix p0 p1 = match _prefix_last_index p0 p1 with 467| None -> None 468| Some 0 when Posix.has_volume p0 || Posix.has_volume p1 -> None 469| Some i -> Some (String.with_index_range p0 ~last:i) 470 471let find_prefix = if windows then find_prefix_windows else find_prefix_posix 472 473let rem_prefix prefix p = match is_prefix prefix p with 474| false -> None 475| true -> 476 match String.length prefix with 477 | len when len = String.length p -> None 478 | len -> 479 let first = if p.[len] = dir_sep_char then len + 1 else len in 480 match String.with_index_range p ~first with 481 | "" -> Some dot_dir 482 | q -> Some q 483 484(* Roots and relativization *) 485 486let _relativize ~root p = 487 if String.equal root p 488 then Some (segs_to_path (if is_dir_path p then ["."; ""] else ["."])) else 489 let root = (* root is always interpreted as a directory *) 490 let root = normalize root in 491 if root.[String.length root - 1] = dir_sep_char then root else 492 root ^ dir_sep 493 in 494 let p = normalize p in 495 let rec walk root p = match root, p with 496 | (".." :: _, s :: _) when s <> ".." -> 497 (* [root] has too many up segments. Cannot walk down to express [p], 498 e.g. "../a" can't be expressed relative to "../../". *) 499 None 500 | (sr :: root, sp :: (_ :: _ as p)) when sr = sp -> 501 (* the next directory in [root] and [p] match and it's not the last 502 segment of [p], walk to next segment *) 503 walk root p 504 | [""], [""] -> 505 (* walk ends at the end of both path simultaneously, [p] is a 506 directory that matches exactly [root] interpreted as a directory. *) 507 Some (segs_to_path ["."; ""]) 508 | root, p -> 509 (* walk ends here, either the next directory is different in 510 [root] and [p] or it is equal but it is the last one for [p] 511 and different from [""] (i.e. [p] is a file path and prefix 512 of [root]). To get to the current position from the remaining 513 root we need to go up the number of non-empty segments that 514 remain in [root] (length root - 1). To get to the path [p] 515 from the current position we just use [p] so prepending 516 length root - 1 ".." segments to [p] tells us how to go from 517 the remaining root to [p]. *) 518 let segs = List.fold_left (fun acc _ -> dotdot :: acc) p (List.tl root) in 519 Some (segs_to_path segs) 520 in 521 match segs root, segs p with 522 | ("" :: _, s :: _) when s <> "" -> None (* absolute/relative mismatch *) 523 | (s :: _, "" :: _) when s <> "" -> None (* absolute/relative mismatch *) 524 | ["."; ""], p -> 525 (* p is relative and must be expressed w.r.t. "./", so it is itself. *) 526 Some (segs_to_path p) 527 | root, p -> 528 (* walk in the segments of root and p until a segment mismatches. 529 at that point express the remaining p relative to the remaining 530 root. Note that because of normalization both [root] and [p] may 531 only have initial .. segments and [root] by construction has a 532 final "" segment. *) 533 walk root p 534 535let relativize_windows ~root p = 536 let rvol, root = Windows.sub_split_volume root in 537 let pvol, p = Windows.sub_split_volume p in 538 if not (String.Sub.equal_bytes rvol pvol) then None else 539 let root = String.Sub.to_string root in 540 let p = String.Sub.to_string p in 541 _relativize ~root p 542 543let relativize_posix ~root p = _relativize ~root p 544 545let relativize = if windows then relativize_windows else relativize_posix 546 547let is_rooted ~root p = match relativize ~root p with 548| None -> false 549| Some r -> 550 not (String.equal dotdot r || String.is_prefix ~affix:dotdot_dir r || 551 (String.equal root p && not (is_dir_path p))) 552 553(* Predicates and comparison *) 554 555let is_rel_posix p = p.[0] <> dir_sep_char 556let is_rel_windows p = 557 if Windows.is_unc_path p then false else 558 p.[Windows.non_unc_path_start p] <> dir_sep_char 559 560let is_rel = if windows then is_rel_windows else is_rel_posix 561let is_abs p = not (is_rel p) 562let is_root = if windows then Windows.is_root else Posix.is_root 563 564let is_current_dir_posix ?(prefix = false) p = match prefix with 565| false -> String.equal dot p || String.equal dot_dir p 566| true -> String.equal dot p || String.is_prefix ~affix:dot_dir p 567 568let is_current_dir_windows ?(prefix = false) p = 569 if Windows.is_unc_path p then false else 570 let start = Windows.non_unc_path_start p in 571 match String.length p - start with 572 | 1 -> p.[start] = '.' 573 | n when n = 2 || prefix -> p.[start] = '.' && p.[start + 1] = dir_sep_char 574 | _ -> false 575 576let is_current_dir = 577 if windows then is_current_dir_windows else is_current_dir_posix 578 579let is_parent_dir_posix ?(prefix = false) p = match prefix with 580| false -> String.equal dotdot p || String.equal dotdot_dir p 581| true -> String.equal dotdot p || String.is_prefix ~affix:dotdot_dir p 582 583let is_parent_dir_windows ?(prefix = false) p = 584 if Windows.is_unc_path p then false else 585 let start = Windows.non_unc_path_start p in 586 match String.length p - start with 587 | 1 -> false 588 | 2 -> p.[start] = '.' && p.[start + 1] = '.' 589 | n when n = 3 || prefix -> 590 p.[start] = '.' && p.[start + 1] = '.' && p.[start + 2] = dir_sep_char 591 | _ -> false 592 593let is_parent_dir = 594 if windows then is_parent_dir_windows else is_parent_dir_posix 595 596let is_dotfile p = match basename p with | "" -> false | s -> s.[0] = '.' 597 598let equal = String.equal 599let compare = String.compare 600 601(* Conversions and pretty printing *) 602 603let to_string p = p 604let pp ppf p = Format.pp_print_string ppf (to_string p) 605let dump ppf p = String.dump ppf (to_string p) 606 607(* File extensions *) 608 609type ext = string 610 611let ext_sep_char = '.' 612let ext_sep = String.of_char ext_sep_char 613let ext_sep_sub = String.Sub.of_char ext_sep_char 614let eq_ext_sep c = c = ext_sep_char 615let neq_ext_sep c = c <> ext_sep_char 616 617let rec sub_multi_ext seg = 618 let first_not_sep = String.Sub.drop ~sat:eq_ext_sep seg in 619 String.Sub.drop ~sat:neq_ext_sep first_not_sep 620 621let sub_single_ext seg = 622 let name_dot, ext = String.Sub.span ~rev:true ~sat:neq_ext_sep seg in 623 if String.Sub.exists neq_ext_sep name_dot 624 then String.Sub.extend ~max:1 ~rev:true ext 625 else String.Sub.empty 626 627let sub_ext ?(multi = false) seg = 628 if multi then sub_multi_ext seg else sub_single_ext seg 629 630let sub_get_ext ?multi p = sub_ext ?multi (sub_last_non_empty_seg p) 631let get_ext ?multi p = String.Sub.to_string (sub_get_ext ?multi p) 632 633let has_ext e p = 634 let ext = sub_get_ext ~multi:true p in 635 if String.Sub.is_empty ext then false else 636 if not (String.(Sub.is_suffix ~affix:(sub e) ext)) then false else 637 if not (String.is_empty e) && e.[0] = ext_sep_char then true else 638 (* Check there's a dot before the suffix [e] in [ext] *) 639 let dot_index = String.Sub.length ext - String.length e - 1 in 640 String.Sub.get ext dot_index = ext_sep_char 641 642let mem_ext exts p = List.exists (fun ext -> has_ext ext p) exts 643 644let exists_ext ?(multi = false) p = 645 let ext = sub_get_ext ~multi p in 646 if multi then String.Sub.exists eq_ext_sep (String.Sub.tail ext) else 647 not (String.Sub.is_empty ext) 648 649let add_ext e p = 650 if String.is_empty e then p else 651 if not (is_seg e) then invalid_arg (err_invalid_ext e) else 652 let seg = sub_last_non_empty_seg p in 653 if sub_is_dir_seg seg then p else 654 let e_has_dot = e.[0] = ext_sep_char in 655 let maybe_dot = if e_has_dot then String.Sub.empty else ext_sep_sub in 656 let has_empty = p.[String.length p - 1] = dir_sep_char in 657 let maybe_empty = if has_empty then dir_sep_sub else String.Sub.empty in 658 let seg_end = String.Sub.stop_pos seg - 1 in 659 let prefix = String.sub_with_index_range ~last:seg_end p in 660 let path = [prefix; maybe_dot; String.sub e; maybe_empty] in 661 String.Sub.(base_string (concat path)) 662 663let _split_ext ?multi p = 664 let ext = sub_get_ext ?multi p in 665 if String.Sub.is_empty ext then p, ext else 666 let before_ext = String.Sub.start_pos ext - 1 in 667 if String.Sub.stop_pos ext = String.length p 668 then String.with_index_range p ~last:before_ext, ext else 669 let prefix = String.sub_with_index_range p ~last:before_ext in 670 String.Sub.(base_string (concat [prefix; dir_sep_sub])), ext 671 672let rem_ext ?multi p = fst (_split_ext ?multi p) 673let set_ext ?multi e p = add_ext e (rem_ext ?multi p) 674let split_ext ?multi p = 675 let p, ext = _split_ext ?multi p in 676 p, String.Sub.to_string ext 677 678let ( + ) p e = add_ext e p 679let ( -+ ) p e = set_ext e p 680 681(* Path sets and maps *) 682 683type path = t 684 685module Set = struct 686 include Set.Make (String) 687 688 let pp ?sep:(pp_sep = Format.pp_print_cut) pp_elt ppf ps = 689 let pp_elt elt is_first = 690 if is_first then () else pp_sep ppf (); 691 Format.fprintf ppf "%a" pp_elt elt; false 692 in 693 ignore (fold pp_elt ps true) 694 695 let dump_path = dump 696 let dump ppf ss = 697 let pp_elt elt is_first = 698 if is_first then () else Format.fprintf ppf "@ "; 699 Format.fprintf ppf "%a" dump_path elt; 700 false 701 in 702 Format.fprintf ppf "@[<1>{"; 703 ignore (fold pp_elt ss true); 704 Format.fprintf ppf "}@]"; 705 () 706 707 let err_empty () = invalid_arg "empty set" 708 let err_absent p ps = 709 invalid_arg (strf "%a not in set %a" dump_path p dump ps) 710 711 let get_min_elt ps = try min_elt ps with Not_found -> err_empty () 712 let min_elt ps = try Some (min_elt ps) with Not_found -> None 713 714 let get_max_elt ps = try max_elt ps with Not_found -> err_empty () 715 let max_elt ps = try Some (max_elt ps) with Not_found -> None 716 717 let get_any_elt ps = try choose ps with Not_found -> err_empty () 718 let choose ps = try Some (choose ps) with Not_found -> None 719 720 let get p ps = try find p ps with Not_found -> err_absent p ps 721 let find p ps = try Some (find p ps) with Not_found -> None 722 723 let of_list = List.fold_left (fun acc s -> add s acc) empty 724end 725 726module Map = struct 727 include Map.Make (String) 728 729 let err_empty () = invalid_arg "empty map" 730 let err_absent s = invalid_arg (strf "%s is not bound in map" s) 731 732 let get_min_binding m = try min_binding m with Not_found -> err_empty () 733 let min_binding m = try Some (min_binding m) with Not_found -> None 734 735 let get_max_binding m = try max_binding m with Not_found -> err_empty () 736 let max_binding m = try Some (max_binding m) with Not_found -> None 737 738 let get_any_binding m = try choose m with Not_found -> err_empty () 739 let choose m = try Some (choose m) with Not_found -> None 740 741 let get k s = try find k s with Not_found -> err_absent k 742 let find k m = try Some (find k m) with Not_found -> None 743 744 let dom m = fold (fun k _ acc -> Set.add k acc) m Set.empty 745 746 let of_list bs = List.fold_left (fun m (k,v) -> add k v m) empty bs 747 748 let pp ?sep:(pp_sep = Format.pp_print_cut) pp_binding ppf (m : 'a t) = 749 let pp_binding k v is_first = 750 if is_first then () else pp_sep ppf (); 751 pp_binding ppf (k, v); false 752 in 753 ignore (fold pp_binding m true) 754 755 let dump pp_v ppf m = 756 let pp_binding k v is_first = 757 if is_first then () else Format.fprintf ppf "@ "; 758 Format.fprintf ppf "@[<1>(@[%a@],@ @[%a@])@]" dump k pp_v v; 759 false 760 in 761 Format.fprintf ppf "@[<1>{"; 762 ignore (fold pp_binding m true); 763 Format.fprintf ppf "}@]"; 764 () 765end 766 767type set = Set.t 768type 'a map = 'a Map.t