···2-------------------------------------------------------------------------------
3v%%VERSION%%
45-Fpath is an OCaml module for handling file system paths on POSIX and
6-Windows operating systems. Fpath processes paths without accessing the
7-file system and is independent from any system library.
89Fpath depends on [Astring][astring] and is distributed under the ISC
10license.
···2-------------------------------------------------------------------------------
3v%%VERSION%%
45+Fpath is an OCaml module for handling file system paths with POSIX or
6+Windows conventions. Fpath processes paths without accessing the file
7+system and is independent from any system library.
89Fpath depends on [Astring][astring] and is distributed under the ISC
10license.
+230-266
src/fpath.ml
···25let dir_sep_char = if windows then '\\' else '/'
26let dir_sep = String.of_char dir_sep_char
27let dir_sep_sub = String.sub dir_sep
02829let dot = "."
30let dot_sub = String.sub dot
···35let dotdot_dir = dotdot ^ dir_sep
36let dotdot_dir_sub = String.sub dotdot_dir
3738-(* Structural preliminaries *)
3940-let validate_and_collapse_seps p =
41- (* collapse non-initial sequences of [dir_sep] to a single one and checks
42- no null byte *)
43- let max_idx = String.length p - 1 in
44- let rec with_buf b last_sep k i = (* k is the write index in b *)
45- if i > max_idx then Some (Bytes.sub_string b 0 k) else
46- let c = string_unsafe_get p i in
47- if c = '\x00' then None else
48- if c <> dir_sep_char
49- then (bytes_unsafe_set b k c; with_buf b false (k + 1) (i + 1)) else
50- if not last_sep
51- then (bytes_unsafe_set b k c; with_buf b true (k + 1) (i + 1)) else
52- with_buf b true k (i + 1)
53- in
54- let rec try_no_alloc last_sep i =
55- if i > max_idx then Some p else
56- let c = string_unsafe_get p i in
57- if c = '\x00' then None else
58- if c <> dir_sep_char then try_no_alloc false (i + 1) else
59- if not last_sep then try_no_alloc true (i + 1) else
60- let b = Bytes.of_string p in (* copy and overwrite starting from i *)
61- with_buf b true i (i + 1)
62- in
63- let start = (* Allow initial double sep *)
64- if max_idx > 0 then (if p.[0] = dir_sep_char then 1 else 0) else 0
65- in
66- try_no_alloc false start
6768-let is_unc_path_windows p = String.is_prefix "\\\\" p
69-let windows_non_unc_path_start_index p =
70- match String.find (Char.equal ':') p with
71 | None -> 0
72 | Some i -> i + 1 (* exists by construction *)
7374-let parse_unc_windows s =
75- (* parses an UNC path, the \\ prefix was already parsed, adds a root path
76- if there's only a volume, UNC paths are always absolute. *)
77- let p = String.sub ~start:2 s in
78- let not_bslash c = c <> '\\' in
79- let parse_seg p = String.Sub.span ~min:1 ~sat:not_bslash p in
80- let ensure_root r = Some (if String.Sub.is_empty r then (s ^ "\\") else s) in
81- match parse_seg p with
82- | (seg1, _) when String.Sub.is_empty seg1 -> None (* \\ or \\\ *)
83- | (seg1, rest) ->
84- let seg1_len = String.Sub.length seg1 in
85- match String.Sub.get_head ~rev:true seg1 with
86- | '.' when seg1_len = 1 -> (* \\.\device\ *)
87- begin match parse_seg (String.Sub.tail rest) with
88- | (seg, _) when String.Sub.is_empty seg -> None
89- | (_, rest) -> ensure_root rest
90- end
91- | '?' when seg1_len = 1 ->
92- begin match parse_seg (String.Sub.tail rest) with
93- | (seg2, _) when String.Sub.is_empty seg2 -> None
94- | (seg2, rest) ->
95- if (String.Sub.get_head ~rev:true seg2 = ':') (* \\?\drive:\ *)
96- then (ensure_root rest) else
97- if not (String.Sub.equal_bytes seg2 (String.sub "UNC"))
98- then begin (* \\?\server\share\ *)
99- match parse_seg (String.Sub.tail rest) with
100- | (seg, _) when String.Sub.is_empty seg -> None
101- | (_, rest) -> ensure_root rest
102- end else begin (* \\?\UNC\server\share\ *)
103- match parse_seg (String.Sub.tail rest) with
104- | (seg, _) when String.Sub.is_empty seg -> None
105- | (_, rest) ->
106- match parse_seg (String.Sub.tail rest) with
107- | (seg, _) when String.Sub.is_empty seg -> None
108- | (_, rest) -> ensure_root rest
109- end
110- end
111- | _ -> (* \\server\share\ *)
112- begin match parse_seg (String.Sub.tail rest) with
113- | (seg, _) when String.Sub.is_empty seg -> None
114- | (_, rest) -> ensure_root rest
115- end
0116117-let sub_split_volume_windows p =
118- (* splits a windows path into its volume (or drive) and actual file
119- path. When called the path in [p] is guaranteed to be non empty
120- and if [p] is an UNC path it is guaranteed to the be parseable by
121- parse_unc_windows. *)
122- let split_before i = String.sub p ~stop:i, String.sub p ~start:i in
123- if not (is_unc_path_windows p) then
124- begin match String.find (Char.equal ':') p with
125- | None -> String.Sub.empty, String.sub p
126- | Some i -> split_before (i + 1)
127- end
128- else
129- let bslash ~start = match String.find ~start (Char.equal '\\') p with
130- | None -> assert false | Some i -> i
131- in
132- let i = bslash ~start:2 in
133- let j = bslash ~start:(i + 1) in
134- match p.[i-1] with
135- | '.' when i = 3 -> split_before j
136- | '?' when i = 3 ->
137- if p.[j-1] = ':' then split_before j else
138- if (String.Sub.equal_bytes
139- (String.sub p ~start:(i + 1) ~stop:j)
140- (String.sub "UNC"))
141- then split_before (bslash ~start:((bslash ~start:(j + 1)) + 1))
142 else split_before (bslash ~start:(j + 1))
143- | _ -> split_before j
144145-let is_root_posix p = String.equal p dir_sep || String.equal p "//"
146-let is_root_windows p =
147- let _, p = sub_split_volume_windows p in
148- String.Sub.equal_bytes dir_sep_sub p
00000149150(* Segments *)
151···159160let is_seg = if windows then is_seg_windows else is_seg_posix
161162-let not_dir_sep c = c <> dir_sep_char
163-164let _split_last_seg p = String.Sub.span ~rev:true ~sat:not_dir_sep p
165let _sub_last_seg p = String.Sub.take ~rev:true ~sat:not_dir_sep p
166-let sub_last_seg_windows p = _sub_last_seg (snd (sub_split_volume_windows p))
167-let sub_last_seg_posix p = _sub_last_seg (String.sub p)
168-let sub_last_seg = if windows then sub_last_seg_windows else sub_last_seg_posix
169-170-let _sub_last_non_empty_seg p = (* result is empty only roots *)
171 let dir, last = _split_last_seg p in
172 match String.Sub.is_empty last with
173 | false -> last
174 | true -> _sub_last_seg (String.Sub.tail ~rev:true dir)
1750000000000176let sub_last_non_empty_seg_windows p =
177- _sub_last_non_empty_seg (snd (sub_split_volume_windows p))
178179let sub_last_non_empty_seg_posix p =
180 _sub_last_non_empty_seg (String.sub p)
···183 if windows then sub_last_non_empty_seg_windows else
184 sub_last_non_empty_seg_posix
185186-let _split_last_non_empty_seg p =
187- let (dir, last_seg as r) = _split_last_seg p in
188- match String.Sub.is_empty last_seg with
189- | false -> r, true
190- | true -> _split_last_seg (String.Sub.tail ~rev:true dir), false
191-192-let seg_is_rel = function "." | ".." -> true | _ -> false
193-let sub_seg_is_rel seg =
194 String.Sub.(equal_bytes dot_sub seg || equal_bytes dotdot_sub seg)
195000196(* File paths *)
197198type t = string (* N.B. a path is never "" or something is wrooong. *)
0000000000000000000000000000199200let of_string_windows p =
201 if p = "" then None else
···203 match validate_and_collapse_seps p with
204 | None -> None
205 | Some p as some ->
206- if is_unc_path_windows p then parse_unc_windows p else
207 match String.find (Char.equal ':') p with
208 | None -> some
209- | Some i -> if i = String.length p - 1 then None else (Some p)
0210211let of_string_posix p = if p = "" then None else validate_and_collapse_seps p
212let of_string = if windows then of_string_windows else of_string_posix
···218let add_seg p seg =
219 if not (is_seg seg) then invalid_arg (err_invalid_seg seg);
220 let sep = if p.[String.length p - 1] = dir_sep_char then "" else dir_sep in
221- String.concat [p; sep; seg]
222223let append_posix p0 p1 =
224 if p1.[0] = dir_sep_char (* absolute *) then p1 else
225 let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in
226- String.concat [p0; sep; p1]
227228let append_windows p0 p1 =
229- if is_unc_path_windows p1 then p1 else
230- match String.find (Char.equal ':') p1 with
231- | Some _ (* drive *) -> p1
232- | None ->
233- if p1.[0] = dir_sep_char then (* absolute *) p1 else
234- let sep =
235- if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep
236- in
237- String.concat [p0; sep; p1]
238239let append = if windows then append_windows else append_posix
240···242let ( // ) = append
243244let split_volume_windows p =
245- let vol, path = sub_split_volume_windows p in
246 String.Sub.to_string vol, String.Sub.to_string path
247248let split_volume_posix p =
249- if String.is_prefix "//" p then dir_sep, String.with_range ~first:1 p else
250- "", p
251252let split_volume = if windows then split_volume_windows else split_volume_posix
253254let segs_windows p =
255- let _, path = sub_split_volume_windows p in
256- let path = String.Sub.to_string path in
257- String.cuts ~sep:dir_sep path
258259let segs_posix p =
260- let segs = String.cuts ~sep:dir_sep p in
261- if String.is_prefix "//" p then List.tl segs else segs
262263let segs = if windows then segs_windows else segs_posix
264···279280(* Base and parent paths *)
281282-let split_base_windows p =
283- let vol, path = sub_split_volume_windows p in
284- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p, dot_dir else
285- let dir, last_seg = _split_last_seg path in
286 match String.Sub.is_empty dir with
287- | true -> (* single seg *)
288- String.Sub.base_string (String.Sub.append vol dot_dir_sub),
289- String.Sub.to_string path
290 | false ->
291 match String.Sub.is_empty last_seg with
292- | false ->
293- String.Sub.base_string (String.Sub.append vol dir),
294- String.Sub.to_string last_seg
295 | true ->
296 let dir_file = String.Sub.tail ~rev:true dir in
297 let dir, dir_last_seg = _split_last_seg dir_file in
298 match String.Sub.is_empty dir with
299- | true ->
300- String.Sub.base_string (String.Sub.append vol dot_dir_sub),
301- String.Sub.to_string path
302- | false ->
303- String.Sub.base_string (String.Sub.append vol dir),
304- String.Sub.to_string (String.Sub.extend dir_last_seg)
00305306let split_base_posix p =
307- if is_root_posix p then p, dot_dir else
308- let dir, last_seg = _split_last_seg (String.sub p) in
309- match String.Sub.is_empty dir with
310- | true -> (* single seg *) dot_dir, p
311- | false ->
312- match String.Sub.is_empty last_seg with
313- | false -> String.Sub.to_string dir, String.Sub.to_string last_seg
314- | true ->
315- let dir_file = String.Sub.tail ~rev:true dir in
316- let dir, dir_last_seg = _split_last_seg dir_file in
317- match String.Sub.is_empty dir with
318- | true -> dot_dir, p
319- | false ->
320- String.Sub.to_string dir,
321- String.Sub.to_string (String.Sub.extend dir_last_seg)
322323let split_base = if windows then split_base_windows else split_base_posix
324325let base p = snd (split_base p)
326327-let basename_windows p =
328- let vol, path = sub_split_volume_windows p in
329- if String.Sub.equal_bytes dir_sep_sub path then (* root *) "" else
330- let basename =
331- let dir, last_seg = _split_last_seg path in
332- match String.Sub.is_empty dir with
333- | true -> (* single seg *) String.Sub.to_string path
334- | false ->
335- match String.Sub.is_empty last_seg with
336- | false -> String.Sub.to_string last_seg
337- | true ->
338- let dir_file = String.Sub.tail ~rev:true dir in
339- let _, dir_last_seg = _split_last_seg dir_file in
340- String.Sub.to_string dir_last_seg
341- in
342- match basename with "." | ".." -> "" | basename -> basename
343344-let basename_posix p =
345- if p = dir_sep || p = "//" then (* root *) "" else
346- let basename =
347- let dir, last_seg = _split_last_seg (String.sub p) in
348- match String.Sub.is_empty dir with
349- | true -> (* single seg *) p
350- | false ->
351- match String.Sub.is_empty last_seg with
352- | false -> String.Sub.to_string last_seg
353- | true ->
354- let dir_file = String.Sub.tail ~rev:true dir in
355- let _, dir_last_seg = _split_last_seg dir_file in
356- String.Sub.to_string dir_last_seg
357- in
358- match basename with "." | ".." -> "" | basename -> basename
3590360let basename p = if windows then basename_windows p else basename_posix p
361362-(* The parent algorithm is not very smart. It tries not to preserve
363- the original path and avoids dealing with normalization. We simply
364- remove everyting (i.e. a potential "") after the last non-empty,
365- non-relative, path segment and if the resulting path is empty we
366- return "./". If the last non-empty segment is "." or ".." we then
367- simply postfix "../" *)
368-369let _parent p =
000000370 let (dir, seg), is_last = _split_last_non_empty_seg p in
371 let dsep = if is_last then dir_sep_sub else String.Sub.empty in
372- match String.Sub.is_empty dir with
373- | true ->
374- if sub_seg_is_rel seg then [p; dsep; dotdot_dir_sub] else [dot_dir_sub]
375- | false ->
376- if sub_seg_is_rel seg then [p; dsep; dotdot_dir_sub] else [dir]
377378let parent_windows p =
379- let vol, path = sub_split_volume_windows p in
380- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p else
381 String.Sub.(base_string @@ concat (vol :: _parent path))
382383let parent_posix p =
384- if is_root_posix p then p else
385 String.Sub.(base_string @@ concat (_parent (String.sub p)))
386387let parent = if windows then parent_windows else parent_posix
···389(* Normalization *)
390391let rem_empty_seg_windows p =
392- let vol, path = sub_split_volume_windows p in
393- if String.Sub.equal_bytes dir_sep_sub path then (* root *) p else
394- let dir, last_seg = _split_last_seg path in
395- if not (String.Sub.is_empty last_seg) then p else
396- let p = String.Sub.tail ~rev:true dir in
397- String.Sub.(base_string @@ concat [vol; p])
398399let rem_empty_seg_posix p = match String.length p with
400| 1 -> p
···410let rem_empty_seg =
411 if windows then rem_empty_seg_windows else rem_empty_seg_posix
412413-let normalize_rel_segs_rev segs =
414 let rec loop acc = function
415 | "." :: [] -> ("" :: acc) (* final "." remove but preserve directoryness. *)
416 | "." :: rest -> loop acc rest
···426 | [] ->
427 match acc with
428 | ".." :: _ -> ("" :: acc) (* normalize final .. to ../ *)
0429 | acc -> acc
430 in
431- loop [] segs
432433let normalize_segs = function
434| "" :: segs -> (* absolute path *)
435- let rec rem_dotdots = function
436- | ".." :: segs -> rem_dotdots segs
437- | [] -> [""]
438- | segs -> segs
439- in
440- "" :: (rem_dotdots (List.rev (normalize_rel_segs_rev segs)))
441| segs ->
442- match List.rev (normalize_rel_segs_rev segs) with
443- | [] | [""] -> ["."; ""]
444 | segs -> segs
445446let normalize_windows p =
447- let vol, path = sub_split_volume_windows p in
448 let path = String.Sub.to_string path in
449- let segs = normalize_segs (String.cuts ~sep:dir_sep path) in
450- let path = String.concat ~sep:dir_sep segs in
451 String.Sub.(to_string (concat [vol; String.sub path]))
452453let normalize_posix p =
454- let segs = String.cuts ~sep:dir_sep p in
455- let has_volume = String.is_prefix "//" p in
456- let segs = normalize_segs (if has_volume then List.tl segs else segs) in
457 let segs = if has_volume then "" :: segs else segs in
458- String.concat ~sep:dir_sep segs
459460let normalize = if windows then normalize_windows else normalize_posix
461···469 starts with a directory separator. *)
470 let suff_start = String.length prefix in
471 if prefix.[suff_start - 1] = dir_sep_char then true else
472- if suff_start = String.length p then true else
473 p.[suff_start] = dir_sep_char
474475-let seg_prefix_last_index p0 p1 =
476- (* Warning doesn't care about volumes *)
477 let l0 = String.length p0 in
478 let l1 = String.length p1 in
479 let p0, p1, max = if l0 < l1 then p0, p1, l0 - 1 else p1, p0, l1 - 1 in
···495 in
496 loop (-1) 0 p0 p1
497498-let find_prefix_windows p0 p1 = match seg_prefix_last_index p0 p1 with
499| None -> None
500| Some i ->
501- let v0_len = String.Sub.length (fst (sub_split_volume_windows p0)) in
502- let v1_len = String.Sub.length (fst (sub_split_volume_windows p1)) in
503- let vmax = if v0_len > v1_len then v0_len else v1_len in
504- if i < vmax then None else
505- Some (String.with_index_range p0 ~last:i)
506507-let find_prefix_posix p0 p1 = match seg_prefix_last_index p0 p1 with
508| None -> None
509-| Some 0 when String.is_prefix "//" p0 || String.is_prefix "//" p1 -> None
510| Some i -> Some (String.with_index_range p0 ~last:i)
511512let find_prefix = if windows then find_prefix_windows else find_prefix_posix
···529 let prooted = normalize (append root p) in
530 if is_prefix nroot prooted then Some prooted else None
531532-let relativize ~root p =
533 let root = (* root is always interpreted as a directory *)
534 let root = normalize root in
535 if root.[String.length root - 1] = dir_sep_char then root else
···548 | [""], [""] ->
549 (* walk ends at the end of both path simultaneously, [p] is a
550 directory that matches exactly [root] expressed as a directory. *)
551- Some ["."; ""]
552 | root, p ->
553 (* walk ends here, either the next directory is different in
554 [root] and [p] or it is equal but it is the last one for [p]
···559 from the current position we just use [p] so prepending
560 length root - 1 .. segments to [p] tells us how to go from
561 the remaining root to [p]. *)
562- Some (List.fold_left (fun acc _ -> dotdot :: acc) p (List.tl root))
0563 in
564 match segs root, segs p with
565 | ("" :: _, s :: _)
···568 None
569 | ["."; ""], p ->
570 (* p is relative and expressed w.r.t. "./", so it is itself. *)
571- Some p
572 | root, p ->
573 (* walk in the segments of root and p until a segment mismatches.
574 at that point express the remaining p relative to the remaining
···577 final "" segment. *)
578 walk root p
579580-let relativize ~root p = match relativize ~root p with
581-| None -> None
582-| Some segs -> Some (String.concat ~sep:dir_sep segs)
00000000583584(* Predicates and comparison *)
585586let is_rel_posix p = p.[0] <> dir_sep_char
587let is_rel_windows p =
588- if is_unc_path_windows p then false else
589- p.[windows_non_unc_path_start_index p] <> dir_sep_char
590591let is_rel = if windows then is_rel_windows else is_rel_posix
592let is_abs p = not (is_rel p)
593-let is_root = if windows then is_root_windows else is_root_posix
594595let is_current_dir_posix p = String.equal p dot || String.equal p dot_dir
596let is_current_dir_windows p =
597- if is_unc_path_windows p then false else
598- let start = windows_non_unc_path_start_index p in
599 match String.length p - start with
600 | 1 -> p.[start] = '.'
601 | 2 -> p.[start] = '.' && p.[start + 1] = dir_sep_char
···639 if multi then multi_ext_sub seg else single_ext_sub seg
640641let get_ext ?multi p =
642- String.Sub.to_string (ext_sub ?multi (sub_last_seg p))
643644let has_ext e p =
645- let seg = String.Sub.drop ~sat:eq_ext_sep (sub_last_seg p) in
646 if not (String.Sub.is_suffix (String.sub e) seg) then false else
647 if not (String.is_empty e) && e.[0] = ext_sep_char then true else
648 (* check there's a dot before the suffix in [seg] *)
···651 String.Sub.get seg dot_index = ext_sep_char
652653let ext_exists ?(multi = false) p =
654- let ext = ext_sub ~multi (sub_last_seg p) in
655 if not multi then not (String.Sub.is_empty ext) else
656 if String.Sub.is_empty ext then false else
657 match String.Sub.find ~rev:true eq_ext_sep ext with (* find another dot *)
···25let dir_sep_char = if windows then '\\' else '/'
26let dir_sep = String.of_char dir_sep_char
27let dir_sep_sub = String.sub dir_sep
28+let not_dir_sep c = c <> dir_sep_char
2930let dot = "."
31let dot_sub = String.sub dot
···36let dotdot_dir = dotdot ^ dir_sep
37let dotdot_dir_sub = String.sub dotdot_dir
3839+(* Platform specific preliminaties *)
4041+module Windows = struct
000000000000000000000000004243+ let is_unc_path p = String.is_prefix "\\\\" p
44+ let has_drive p = String.exists (Char.equal ':') p
45+ let non_unc_path_start p = match String.find (Char.equal ':') p with
46 | None -> 0
47 | Some i -> i + 1 (* exists by construction *)
4849+ let parse_unc s =
50+ (* parses an UNC path, the \\ prefix was already parsed, adds a root path
51+ if there's only a volume, UNC paths are always absolute. *)
52+ let p = String.sub ~start:2 s in
53+ let not_bslash c = c <> '\\' in
54+ let parse_seg p = String.Sub.span ~min:1 ~sat:not_bslash p in
55+ let ensure_root r = Some (if String.Sub.is_empty r then (s ^ "\\") else s)
56+ in
57+ match parse_seg p with
58+ | (seg1, _) when String.Sub.is_empty seg1 -> None (* \\ or \\\ *)
59+ | (seg1, rest) ->
60+ let seg1_len = String.Sub.length seg1 in
61+ match String.Sub.get_head ~rev:true seg1 with
62+ | '.' when seg1_len = 1 -> (* \\.\device\ *)
63+ begin match parse_seg (String.Sub.tail rest) with
64+ | (seg, _) when String.Sub.is_empty seg -> None
65+ | (_, rest) -> ensure_root rest
66+ end
67+ | '?' when seg1_len = 1 ->
68+ begin match parse_seg (String.Sub.tail rest) with
69+ | (seg2, _) when String.Sub.is_empty seg2 -> None
70+ | (seg2, rest) ->
71+ if (String.Sub.get_head ~rev:true seg2 = ':') (* \\?\drive:\ *)
72+ then (ensure_root rest) else
73+ if not (String.Sub.equal_bytes seg2 (String.sub "UNC"))
74+ then begin (* \\?\server\share\ *)
75+ match parse_seg (String.Sub.tail rest) with
76+ | (seg, _) when String.Sub.is_empty seg -> None
77+ | (_, rest) -> ensure_root rest
78+ end else begin (* \\?\UNC\server\share\ *)
79+ match parse_seg (String.Sub.tail rest) with
80+ | (seg, _) when String.Sub.is_empty seg -> None
81+ | (_, rest) ->
82+ match parse_seg (String.Sub.tail rest) with
83+ | (seg, _) when String.Sub.is_empty seg -> None
84+ | (_, rest) -> ensure_root rest
85+ end
86+ end
87+ | _ -> (* \\server\share\ *)
88+ begin match parse_seg (String.Sub.tail rest) with
89+ | (seg, _) when String.Sub.is_empty seg -> None
90+ | (_, rest) -> ensure_root rest
91+ end
9293+ let sub_split_volume p =
94+ (* splits a windows path into its volume (or drive) and actual file
95+ path. When called the path in [p] is guaranteed to be non empty
96+ and if [p] is an UNC path it is guaranteed to the be parseable by
97+ parse_unc_windows. *)
98+ let split_before i = String.sub p ~stop:i, String.sub p ~start:i in
99+ if not (is_unc_path p) then
100+ begin match String.find (Char.equal ':') p with
101+ | None -> String.Sub.empty, String.sub p
102+ | Some i -> split_before (i + 1)
103+ end
104+ else
105+ let bslash ~start = match String.find ~start (Char.equal '\\') p with
106+ | None -> assert false | Some i -> i
107+ in
108+ let i = bslash ~start:2 in
109+ let j = bslash ~start:(i + 1) in
110+ match p.[i-1] with
111+ | '.' when i = 3 -> split_before j
112+ | '?' when i = 3 ->
113+ if p.[j-1] = ':' then split_before j else
114+ if (String.Sub.equal_bytes
115+ (String.sub p ~start:(i + 1) ~stop:j)
116+ (String.sub "UNC"))
117+ then split_before (bslash ~start:((bslash ~start:(j + 1)) + 1))
118 else split_before (bslash ~start:(j + 1))
119+ | _ -> split_before j
120121+ let is_root p =
122+ let _, p = sub_split_volume p in
123+ String.Sub.get_head p = dir_sep_char
124+end
125+126+module Posix = struct
127+ let has_volume p = String.is_prefix "//" p
128+ let is_root p = String.equal p dir_sep || String.equal p "//"
129+end
130131(* Segments *)
132···140141let is_seg = if windows then is_seg_windows else is_seg_posix
14200143let _split_last_seg p = String.Sub.span ~rev:true ~sat:not_dir_sep p
144let _sub_last_seg p = String.Sub.take ~rev:true ~sat:not_dir_sep p
145+let _sub_last_non_empty_seg p = (* returns empty on roots though *)
0000146 let dir, last = _split_last_seg p in
147 match String.Sub.is_empty last with
148 | false -> last
149 | true -> _sub_last_seg (String.Sub.tail ~rev:true dir)
150151+let _split_last_non_empty_seg p =
152+ let (dir, last_seg as r) = _split_last_seg p in
153+ match String.Sub.is_empty last_seg with
154+ | false -> r, true
155+ | true -> _split_last_seg (String.Sub.tail ~rev:true dir), false
156+157+let sub_last_seg_windows p = _sub_last_seg (snd (Windows.sub_split_volume p))
158+let sub_last_seg_posix p = _sub_last_seg (String.sub p)
159+let sub_last_seg = if windows then sub_last_seg_windows else sub_last_seg_posix
160+161let sub_last_non_empty_seg_windows p =
162+ _sub_last_non_empty_seg (snd (Windows.sub_split_volume p))
163164let sub_last_non_empty_seg_posix p =
165 _sub_last_non_empty_seg (String.sub p)
···168 if windows then sub_last_non_empty_seg_windows else
169 sub_last_non_empty_seg_posix
170171+let is_rel_seg = function "." | ".." -> true | _ -> false
172+let sub_is_rel_seg seg =
000000173 String.Sub.(equal_bytes dot_sub seg || equal_bytes dotdot_sub seg)
174175+let segs_of_path p = String.cuts ~sep:dir_sep p
176+let segs_to_path segs = String.concat ~sep:dir_sep segs
177+178(* File paths *)
179180type t = string (* N.B. a path is never "" or something is wrooong. *)
181+182+let validate_and_collapse_seps p =
183+ (* collapse non-initial sequences of [dir_sep] to a single one and checks
184+ no null byte *)
185+ let max_idx = String.length p - 1 in
186+ let rec with_buf b last_sep k i = (* k is the write index in b *)
187+ if i > max_idx then Some (Bytes.sub_string b 0 k) else
188+ let c = string_unsafe_get p i in
189+ if c = '\x00' then None else
190+ if c <> dir_sep_char
191+ then (bytes_unsafe_set b k c; with_buf b false (k + 1) (i + 1)) else
192+ if not last_sep
193+ then (bytes_unsafe_set b k c; with_buf b true (k + 1) (i + 1)) else
194+ with_buf b true k (i + 1)
195+ in
196+ let rec try_no_alloc last_sep i =
197+ if i > max_idx then Some p else
198+ let c = string_unsafe_get p i in
199+ if c = '\x00' then None else
200+ if c <> dir_sep_char then try_no_alloc false (i + 1) else
201+ if not last_sep then try_no_alloc true (i + 1) else
202+ let b = Bytes.of_string p in (* copy and overwrite starting from i *)
203+ with_buf b true i (i + 1)
204+ in
205+ let start = (* Allow initial double sep for POSIX and UNC paths *)
206+ if max_idx > 0 then (if p.[0] = dir_sep_char then 1 else 0) else 0
207+ in
208+ try_no_alloc false start
209210let of_string_windows p =
211 if p = "" then None else
···213 match validate_and_collapse_seps p with
214 | None -> None
215 | Some p as some ->
216+ if Windows.is_unc_path p then Windows.parse_unc p else
217 match String.find (Char.equal ':') p with
218 | None -> some
219+ | Some i when i = String.length p - 1 -> None (* path is empty *)
220+ | Some _ -> Some p
221222let of_string_posix p = if p = "" then None else validate_and_collapse_seps p
223let of_string = if windows then of_string_windows else of_string_posix
···229let add_seg p seg =
230 if not (is_seg seg) then invalid_arg (err_invalid_seg seg);
231 let sep = if p.[String.length p - 1] = dir_sep_char then "" else dir_sep in
232+ String.concat ~sep [p; seg]
233234let append_posix p0 p1 =
235 if p1.[0] = dir_sep_char (* absolute *) then p1 else
236 let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in
237+ String.concat ~sep [p0; p1]
238239let append_windows p0 p1 =
240+ if Windows.is_unc_path p1 || Windows.has_drive p1 then p1 else
241+ if p1.[0] = dir_sep_char then (* absolute *) p1 else
242+ let sep = if p0.[String.length p0 - 1] = dir_sep_char then "" else dir_sep in
243+ String.concat ~sep [p0; p1]
00000244245let append = if windows then append_windows else append_posix
246···248let ( // ) = append
249250let split_volume_windows p =
251+ let vol, path = Windows.sub_split_volume p in
252 String.Sub.to_string vol, String.Sub.to_string path
253254let split_volume_posix p =
255+ if Posix.has_volume p then dir_sep, String.with_range ~first:1 p else "", p
0256257let split_volume = if windows then split_volume_windows else split_volume_posix
258259let segs_windows p =
260+ let _, path = Windows.sub_split_volume p in
261+ segs_of_path (String.Sub.to_string path)
0262263let segs_posix p =
264+ let segs = segs_of_path p in
265+ if Posix.has_volume p then List.tl segs else segs
266267let segs = if windows then segs_windows else segs_posix
268···283284(* Base and parent paths *)
285286+let sub_is_root p = String.Sub.get_head p = dir_sep_char
287+288+let _split_base p =
289+ let dir, last_seg = _split_last_seg p in
290 match String.Sub.is_empty dir with
291+ | true -> (* single seg *) dot_dir_sub, String.Sub.to_string p
00292 | false ->
293 match String.Sub.is_empty last_seg with
294+ | false -> dir, String.Sub.to_string last_seg
00295 | true ->
296 let dir_file = String.Sub.tail ~rev:true dir in
297 let dir, dir_last_seg = _split_last_seg dir_file in
298 match String.Sub.is_empty dir with
299+ | true -> dot_dir_sub, String.Sub.to_string p
300+ | false -> dir, String.Sub.(to_string (extend dir_last_seg))
301+302+let split_base_windows p =
303+ let vol, path = Windows.sub_split_volume p in
304+ if sub_is_root path then p, dot_dir else
305+ let dir, b = _split_base path in
306+ String.Sub.(base_string (append vol dir)), b
307308let split_base_posix p =
309+ if Posix.is_root p then p, dot_dir else
310+ let dir, b = _split_base (String.sub p) in
311+ String.Sub.to_string dir, b
000000000000312313let split_base = if windows then split_base_windows else split_base_posix
314315let base p = snd (split_base p)
316317+let _basename p = match String.Sub.to_string (_sub_last_non_empty_seg p) with
318+| "." | ".." -> ""
319+| basename -> basename
0000000000000320321+let basename_windows p =
322+ let vol, path = Windows.sub_split_volume p in
323+ if sub_is_root path then "" else _basename path
000000000000324325+let basename_posix p = if Posix.is_root p then "" else _basename (String.sub p)
326let basename p = if windows then basename_windows p else basename_posix p
3270000000328let _parent p =
329+ (* The parent algorithm is not very smart. It tries to preserve the
330+ original path and avoids dealing with normalization. We simply
331+ only keep everything before the last non-empty, non-relative,
332+ path segment and if the resulting path is empty we return
333+ "./". Otherwise if the last non-empty segment is "." or ".." we
334+ simply postfix with "../" *)
335 let (dir, seg), is_last = _split_last_non_empty_seg p in
336 let dsep = if is_last then dir_sep_sub else String.Sub.empty in
337+ if sub_is_rel_seg seg then [p; dsep; dotdot_dir_sub] else
338+ if String.Sub.is_empty dir then [dot_dir_sub] else [dir]
000339340let parent_windows p =
341+ let vol, path = Windows.sub_split_volume p in
342+ if sub_is_root path then p else
343 String.Sub.(base_string @@ concat (vol :: _parent path))
344345let parent_posix p =
346+ if Posix.is_root p then p else
347 String.Sub.(base_string @@ concat (_parent (String.sub p)))
348349let parent = if windows then parent_windows else parent_posix
···351(* Normalization *)
352353let rem_empty_seg_windows p =
354+ let vol, path = Windows.sub_split_volume p in
355+ if sub_is_root path then p else
356+ let max = String.Sub.length path - 1 in
357+ if String.Sub.get path max <> dir_sep_char then p else
358+ String.with_index_range p ~last:(max - 1)
0359360let rem_empty_seg_posix p = match String.length p with
361| 1 -> p
···371let rem_empty_seg =
372 if windows then rem_empty_seg_windows else rem_empty_seg_posix
373374+let normalize_rel_segs segs = (* result is non empty but may be [""] *)
375 let rec loop acc = function
376 | "." :: [] -> ("" :: acc) (* final "." remove but preserve directoryness. *)
377 | "." :: rest -> loop acc rest
···387 | [] ->
388 match acc with
389 | ".." :: _ -> ("" :: acc) (* normalize final .. to ../ *)
390+ | [] -> [""]
391 | acc -> acc
392 in
393+ List.rev (loop [] segs)
394395let normalize_segs = function
396| "" :: segs -> (* absolute path *)
397+ let rec rem_dotdots = function ".." :: ss -> rem_dotdots ss | ss -> ss in
398+ "" :: (rem_dotdots @@ normalize_rel_segs segs)
0000399| segs ->
400+ match normalize_rel_segs segs with
401+ | [""] -> ["."; ""]
402 | segs -> segs
403404let normalize_windows p =
405+ let vol, path = Windows.sub_split_volume p in
406 let path = String.Sub.to_string path in
407+ let path = segs_to_path @@ normalize_segs (segs_of_path path) in
0408 String.Sub.(to_string (concat [vol; String.sub path]))
409410let normalize_posix p =
411+ let has_volume = Posix.has_volume p in
412+ let segs = segs_of_path p in
413+ let segs = normalize_segs @@ if has_volume then List.tl segs else segs in
414 let segs = if has_volume then "" :: segs else segs in
415+ segs_to_path segs
416417let normalize = if windows then normalize_windows else normalize_posix
418···426 starts with a directory separator. *)
427 let suff_start = String.length prefix in
428 if prefix.[suff_start - 1] = dir_sep_char then true else
429+ if suff_start = String.length p then (* suffix empty *) true else
430 p.[suff_start] = dir_sep_char
431432+let _prefix_last_index p0 p1 = (* last char index of segment-based prefix *)
0433 let l0 = String.length p0 in
434 let l1 = String.length p1 in
435 let p0, p1, max = if l0 < l1 then p0, p1, l0 - 1 else p1, p0, l1 - 1 in
···451 in
452 loop (-1) 0 p0 p1
453454+let find_prefix_windows p0 p1 = match _prefix_last_index p0 p1 with
455| None -> None
456| Some i ->
457+ let v0_len = String.Sub.length (fst (Windows.sub_split_volume p0)) in
458+ let v1_len = String.Sub.length (fst (Windows.sub_split_volume p1)) in
459+ let max_vlen = if v0_len > v1_len then v0_len else v1_len in
460+ if i < max_vlen then None else Some (String.with_index_range p0 ~last:i)
0461462+let find_prefix_posix p0 p1 = match _prefix_last_index p0 p1 with
463| None -> None
464+| Some 0 when Posix.has_volume p0 || Posix.has_volume p1 -> None
465| Some i -> Some (String.with_index_range p0 ~last:i)
466467let find_prefix = if windows then find_prefix_windows else find_prefix_posix
···484 let prooted = normalize (append root p) in
485 if is_prefix nroot prooted then Some prooted else None
486487+let _relativize ~root p =
488 let root = (* root is always interpreted as a directory *)
489 let root = normalize root in
490 if root.[String.length root - 1] = dir_sep_char then root else
···503 | [""], [""] ->
504 (* walk ends at the end of both path simultaneously, [p] is a
505 directory that matches exactly [root] expressed as a directory. *)
506+ Some (segs_to_path ["."; ""])
507 | root, p ->
508 (* walk ends here, either the next directory is different in
509 [root] and [p] or it is equal but it is the last one for [p]
···514 from the current position we just use [p] so prepending
515 length root - 1 .. segments to [p] tells us how to go from
516 the remaining root to [p]. *)
517+ let segs = List.fold_left (fun acc _ -> dotdot :: acc) p (List.tl root) in
518+ Some (segs_to_path segs)
519 in
520 match segs root, segs p with
521 | ("" :: _, s :: _)
···524 None
525 | ["."; ""], p ->
526 (* p is relative and expressed w.r.t. "./", so it is itself. *)
527+ Some (segs_to_path p)
528 | root, p ->
529 (* walk in the segments of root and p until a segment mismatches.
530 at that point express the remaining p relative to the remaining
···533 final "" segment. *)
534 walk root p
535536+let relativize_windows ~root p =
537+ let rvol, root = Windows.sub_split_volume root in
538+ let pvol, p = Windows.sub_split_volume p in
539+ if not (String.Sub.equal_bytes rvol pvol) then None else
540+ let root = String.Sub.to_string root in
541+ let p = String.Sub.to_string p in
542+ _relativize ~root p
543+544+let relativize_posix ~root p = _relativize ~root p
545+546+let relativize = if windows then relativize_windows else relativize_posix
547548(* Predicates and comparison *)
549550let is_rel_posix p = p.[0] <> dir_sep_char
551let is_rel_windows p =
552+ if Windows.is_unc_path p then false else
553+ p.[Windows.non_unc_path_start p] <> dir_sep_char
554555let is_rel = if windows then is_rel_windows else is_rel_posix
556let is_abs p = not (is_rel p)
557+let is_root = if windows then Windows.is_root else Posix.is_root
558559let is_current_dir_posix p = String.equal p dot || String.equal p dot_dir
560let is_current_dir_windows p =
561+ if Windows.is_unc_path p then false else
562+ let start = Windows.non_unc_path_start p in
563 match String.length p - start with
564 | 1 -> p.[start] = '.'
565 | 2 -> p.[start] = '.' && p.[start + 1] = dir_sep_char
···603 if multi then multi_ext_sub seg else single_ext_sub seg
604605let get_ext ?multi p =
606+ String.Sub.to_string (ext_sub ?multi (sub_last_non_empty_seg p))
607608let has_ext e p =
609+ let seg = String.Sub.drop ~sat:eq_ext_sep (sub_last_non_empty_seg p) in
610 if not (String.Sub.is_suffix (String.sub e) seg) then false else
611 if not (String.is_empty e) && e.[0] = ext_sep_char then true else
612 (* check there's a dot before the suffix in [seg] *)
···615 String.Sub.get seg dot_index = ext_sep_char
616617let ext_exists ?(multi = false) p =
618+ let ext = ext_sub ~multi (sub_last_non_empty_seg p) in
619 if not multi then not (String.Sub.is_empty ext) else
620 if String.Sub.is_empty ext then false else
621 match String.Sub.find ~rev:true eq_ext_sep ext with (* find another dot *)
+90-87
src/fpath.mli
···19 distinguishes {e directory paths}
20 (["a/b/"]) from {e file paths} (["a/b"]).}}
2100000022 Consult a few {{!tips}important tips}.
2324 {b Note.} [Fpath] processes paths without accessing the file system.
···34 ["/"] on POSIX and ["\\"] on Windows. *)
3536val is_seg : string -> bool
37-(** [is_seg s] is [true] iff [s] does not contain {!dir_sep} or a
38- [0x00] byte. *)
3940val is_rel_seg : string -> bool
41(** [is_rel_seg s] is true iff [s] is a relative segment, that is
···50(** [v s] is the string [s] as path.
5152 @raise Invalid_argument if [s] is not a {{!of_string}valid path}. Use
53- {!of_string} to deal with foreign input and errors. *)
5455val add_seg : t -> string -> t
56-(** [add_seg p seg] adds [seg] at the end of [p]. If [seg] is [""]
57- it is only added if [p] has no final empty segment. {{!ex_add_seg}Examples}.
5859 @raise Invalid_argument if {!is_seg}[ seg] is [false]. *)
60···66 {ul
67 {- If [p'] is absolute or has a non-empty {{!split_volume}volume} then
68 [p'] is returned.}
69- {- Otherwise appends [p'] to [p] using a {!dir_sep} if needed.}}
70 {{!ex_append}Examples}. *)
7172val ( // ) : t -> t -> t
···105 {- [equal p (v @@ (fst @@ split_volume p) ^ (String.concat ~sep:dir_sep
106 (segs p)))]}} *)
107108-(** {1:filedir File and directory paths} *)
0000109110val is_dir_path : t -> bool
111-(** [is_dir_path p] is [true] iff [p] represents a directory. This means
112- that [p]'s last segment is either [""], ["."] or [".."].
0113 {{!ex_is_dir_path}Examples}. *)
114115val is_file_path : t -> bool
116(** [is_file_path p] is [true] iff [p] represents a file. This is the
117 negation of {!is_dir_path}. This means that [p]'s last segment is
118- neither empty nor ["."], nor [".."]. {{!ex_is_file_path}Examples}. *)
00119120val to_dir_path : t -> t
121(** [to_dir_path p] is {!add_seg}[ p ""] it ensure that the result
122- represents a {{!is_dir_path}directory} (and, if converted to
123- a string, that it will end with a {!dir_sep}).
124 {{!ex_to_dir_path}Examples}. *)
125126val filename : t -> string
127(** [filename p] is the file name of [p]. This is the last segment of
128 [p] if [p] is a {{!is_file_path}file path} and the empty string
129- otherwise. See also {!basename}. {{!ex_filename}Examples}. *)
00130131(** {1:parentbase Base and parent paths} *)
132···141 {{!is_root}root path} there are no such segments and [b]
142 is ["./"].}
143 {- [d] is a {{!is_dir_path}directory} such that [d // b]
144- represents the same path as [p] (they may however differ
145- syntactically when converted to a string).}}
146 {{!ex_split_base}Examples}.
147148 {b Note.} {{!normalize}Normalizing} [p] before using the function
149- ensures that [b] will be a {{!is_rel_seg}relative segment} iff [p] cannot
150 be named (like in ["."], ["../../"], ["/"], etc.). *)
151152val base : t -> t
153(** [base p] is [snd (split_base p)]. *)
154155val basename : t -> string
156-(** [basename p] is [p]'s last non-empty empty segment if
157- {{!is_rel_seg}non-relative} or the empty string otherwise. The
158- latter occurs on {{!is_root}root paths} and on paths whose last
159- non-empty segment is relative. See also {!filename} and
160 {!base}. {{!ex_basename}Examples}.
161162 {b Note.} {{!normalize}Normalizing} [p] before using the function
···174(** {1:norm Normalization} *)
175176val rem_empty_seg : t -> t
177-(** [rem_empty_seg p] removes the empty segment of [p] if it
178- exists and [p] is not a {{!is_root}root path}. This ensure that if
179- [p] is converted to a string it will not have a trailing
180- {!dir_sep} unless [p] is a root path. Note that this may affect
181- [p]'s {{!is_dir_path}directoryness}.
182- {{!ex_rem_empty_seg}Examples}. *)
183184val normalize : t -> t
185(** [normalize p] is a path that represents the same path as [p],
···197198(** {1:prefix Prefixes}
199200- {b Warning.} The {{!is_prefix}prefix property} between paths does
201- not entail directory containement in general, as it is, by
202- definition, a syntactic test. For example [is_prefix (v "..") (v
203- "../..")] is [true], but the second path is not contained in the
204- first one or [is_prefix (v "..") (v ".")] is [false]. However, on
205- {{!normalize}normalized}, {{!is_abs}absolute} paths, the prefix relation
206- does entail directory containement. See also {!rooted}. *)
000207208val is_prefix : t -> t -> bool
209(** [is_prefix prefix p] is [true] if [prefix] is a prefix of
···235 prefix [prefix] and preserves [p]'s
236 {{!is_dir_path}directoryness}. This means that [q] is a always
237 {{!is_rel}relative} and that the path [prefix // q] and [p] represent the
238- same paths (they may however differ syntactically when
239- converted to a string).}}
240 {{!ex_rem_prefix}Examples}. *)
241242(** {1 Roots and relativization} *)
···246 {ul
247 {- [Some q] if there exists a {{!is_relative}relative} path [q] such
248 that [root // q] and [p] represent the same paths,
249- {{!is_dir_path}directoryness} included (they may however differ
250- syntactically when converted to a string).}
251 {- [None] otherwise}}
252253 {{!ex_relativize}Examples.} *)
254255-(*
256-257-val is_rooted : root:t -> t -> bool
258-(** [is_rooted root p] is [true] iff [p] is equal or contained in the
259- directory represented by [root] (if [root] is a {{!is_file_path}file path},
260- the path {!to_dir_path}[ root] is used instead).
261- {{!ex_is_rooted}Examples.} *)
262-263-val rooted_append : ?normalized:bool -> root:t -> t -> t option
264-(** [rooted_append ~root p] {{!appends}appends} [p] to [root] and
265- returns a result iff [is_rooted root (append root t)] is [true].
266- If [normalized] is [true] the result is normalized.
267- {{!ex_rooted_append}Examples.} *)
268-*)
269-270-(*
271-272-273- the path [p] is contained in path [root].
274- {ul
275- {- [None] if [prefix]
276- [is_prefix (normalize root) (normalize @@ append root p) = false].}
277- {- [Some (normalize @@ append root p)] otherwise.}}
278- In other words it ensures that an absolute path [p] or a relative
279- path [p] expressed w.r.t. [root] expresses a path that is
280- within the [root] file hierarchy. {{!ex_rooted}Examples}. *)
281-282(** {1:predicates Predicates and comparison} *)
283284val is_rel : t -> bool
···317*)
318319val is_dotfile : t -> bool
320-(** [is_dotfile p] is [true] iff [p]'s last non-empty segment is not
321- ["."] or [".."] and starts with a ['.']. {{!ex_is_dotfile}Examples}.
322323 {b Warning.} By definition this is a syntactic test. For example it will
324 return [false] on [".ssh/."]. {{!normalize}Normalizing} the
···345(** [of_string s] is the string [s] as a path. [None] is returned if
346 {ul
347 {- [s] or the path following the {{!split_volume}volume} is empty ([""]),
348- expect on Windows UNC paths, see below.}
349 {- [s] has null byte (['\x00']).}
350 {- On Windows, [s] is an invalid UNC path (e.g. ["\\\\"] or ["\\\\a"])}}
351 The following transformations are performed on the string:
···373 character. If there is no such occurence in the segment, the
374 extension is empty. With these definitions, ["."], [".."],
375 ["..."] and dot files like [".ocamlinit"] or ["..ocamlinit"] have
376- no extension, but [".emacs.d"] and ["..emacs.d"] do have one. *)
000000377378type ext = string
379(** The type for file extensions. *)
380381val get_ext : ?multi:bool -> t -> ext
382-(** [get_ext p] is [p]'s last non-empty segment file extension or the empty
383- string if there is no extension. If [multi] is [true] (defaults to
384- [false]), returns the multiple file extension. {{!ex_get_ext}Examples}. *)
0385386val has_ext : ext -> t -> bool
387(** [has_ext e p] is [true] iff [ext p = e || ext ~multi:true p = e].
···389 the test. {{!ex_has_ext}Examples}. *)
390391val ext_exists : ?multi:bool -> t -> bool
392-(** [ext_exists ~multi p] is [true] iff [p]'s last segment has an
393- extension. If [multi] is [true] (default to [false]) returns
394- [true] iff [p] has {e more than one} extension.
395 {{!ex_ext_exists}Examples}. *)
396397val add_ext : ext -> t -> t
···556 a path}. This usually means that we don't care whether the path
557 is a {{!is_file_path}file path} (e.g. ["a"]) or a
558 {{!is_dir_path}directory path} (e.g. ["a/"]).}
559- {- Windows accepts both ['\\'] and ['/'] as directory
560- separator. However [Fpath] on Windows converts ['/'] to ['\\'] on
561- the fly. Therefore you should either use ['/'] for defining
562 constant paths you inject with {!v} or better, construct them
563- directly with {!(/)}. {!to_string} will convert these paths
564- to strings using the platform's specific directory
565- separator {!dir_sep}.}
566 {- Avoid platform specific {{!split_volume}volumes} or hard-coding file
567 hierarchy conventions in your constants.}
568 {- Do not assume there is a single root path and that it is
569- [/]. On Windows each {{!split_volume}volume} can have a root path.
570- Use {!is_root} to detect root paths.}
571 {- Do not use {!to_string} to construct URIs, {!to_string} uses
572 {!dir_sep} to separate segments, on Windows this is ['\\'] which
573- is not what URIs expect. Access the path segments directly
574- with {!segs}, note that you will need to percent encode these.}}
575576 {1:ex Examples}
577···926927 {2:ex_get_ext {!get_ext}}
928 {ul
0929 {- [get_ext (v "/a/b") = ""]}
930- {- [get_ext (v "a/.") = ""]}
931- {- [get_ext (v "a/..") = ""]}
932 {- [get_ext (v "a/.ocamlinit") = ""]}
0933 {- [get_ext (v "/a/b.") = "."]}
934 {- [get_ext (v "/a/b.mli") = ".mli"]}
935 {- [get_ext (v "a.tar.gz") = ".gz"]}
936 {- [get_ext (v "a/.emacs.d") = ".d"]}
0937 {- [get_ext ~multi:true (v "/a/b.mli") = ".mli"]}
938 {- [get_ext ~multi:true (v "a.tar.gz") = ".tar.gz"]}
939- {- [get_ext ~multi:true (v "a/.emacs.d") = ".d"]}}
0940941 {2:ex_has_ext {!has_ext}}
942 {ul
943 {- [has_ext ".mli" (v "a/b.mli") = true]}
944 {- [has_ext "mli" (v "a/b.mli") = true]}
0945 {- [has_ext "mli" (v "a/bmli") = false]}
946 {- [has_ext ".tar.gz" (v "a/f.tar.gz") = true]}
947 {- [has_ext "tar.gz" (v "a/f.tar.gz") = true]}
···953 {- [ext_exists (v "a/f.") = true]}
954 {- [ext_exists (v "a/f.gz") = true]}
955 {- [ext_exists (v "a/f.tar.gz") = true]}
0956 {- [ext_exists (v ".emacs.d") = true]}
0957 {- [ext_exists ~multi:true (v "a/f.gz") = false]}
958 {- [ext_exists ~multi:true (v "a/f.tar.gz") = true]}
959 {- [ext_exists ~multi:true (v ".emacs.d") = false]}}
···19 distinguishes {e directory paths}
20 (["a/b/"]) from {e file paths} (["a/b"]).}}
2122+ The path segments ["."] and [".."] are {{!is_rel_seg}{e relative
23+ path segments}} that respectively denote the current and parent
24+ directory. The {{!basename}{e basename}} of a path is its last
25+ non-empty segment if it is not a relative path segment and the empty
26+ string otherwise.
27+28 Consult a few {{!tips}important tips}.
2930 {b Note.} [Fpath] processes paths without accessing the file system.
···40 ["/"] on POSIX and ["\\"] on Windows. *)
4142val is_seg : string -> bool
43+(** [is_seg s] is [true] iff [s] does not contain {!dir_sep} or ['/'] or
44+ a [0x00] byte. *)
4546val is_rel_seg : string -> bool
47(** [is_rel_seg s] is true iff [s] is a relative segment, that is
···56(** [v s] is the string [s] as path.
5758 @raise Invalid_argument if [s] is not a {{!of_string}valid path}. Use
59+ {!of_string} to deal with untrusted input and errors. *)
6061val add_seg : t -> string -> t
62+(** [add_seg p seg] adds [seg] at the end of [p] if [p]'s last segment
63+ is non-empty and replaces it otherwise. {{!ex_add_seg}Examples}.
6465 @raise Invalid_argument if {!is_seg}[ seg] is [false]. *)
66···72 {ul
73 {- If [p'] is absolute or has a non-empty {{!split_volume}volume} then
74 [p'] is returned.}
75+ {- Otherwise appends [p'] segments to [p] using {!add_seg}.}}
76 {{!ex_append}Examples}. *)
7778val ( // ) : t -> t -> t
···111 {- [equal p (v @@ (fst @@ split_volume p) ^ (String.concat ~sep:dir_sep
112 (segs p)))]}} *)
113114+(** {1:filedir File and directory paths}
115+116+ {b Note.} The following properties are derived from the syntactic
117+ semantics of paths which can be different from the one a file
118+ system attributes to them. *)
119120val is_dir_path : t -> bool
121+(** [is_dir_path p] is [true] iff [p] represents a directory. This
122+ means that [p]'s last segment is either [""], ["."] or [".."].
123+ The property is invariant with respect to {{!normalize}normalization}.
124 {{!ex_is_dir_path}Examples}. *)
125126val is_file_path : t -> bool
127(** [is_file_path p] is [true] iff [p] represents a file. This is the
128 negation of {!is_dir_path}. This means that [p]'s last segment is
129+ neither empty nor ["."], nor [".."]. The property is invariant
130+ with respect to {{!normalize}normalization}.
131+ {{!ex_is_file_path}Examples}. *)
132133val to_dir_path : t -> t
134(** [to_dir_path p] is {!add_seg}[ p ""] it ensure that the result
135+ represents a {{!is_dir_path}directory} and, if converted to a
136+ string, that it ends with a {!dir_sep}.
137 {{!ex_to_dir_path}Examples}. *)
138139val filename : t -> string
140(** [filename p] is the file name of [p]. This is the last segment of
141 [p] if [p] is a {{!is_file_path}file path} and the empty string
142+ otherwise. The result is invariant with respect to
143+ {{!normalize}normalization}. See also
144+ {!basename}. {{!ex_filename}Examples}. *)
145146(** {1:parentbase Base and parent paths} *)
147···156 {{!is_root}root path} there are no such segments and [b]
157 is ["./"].}
158 {- [d] is a {{!is_dir_path}directory} such that [d // b]
159+ represents the same path as [p]. They may however differ
160+ syntactically when converted to a string.}}
161 {{!ex_split_base}Examples}.
162163 {b Note.} {{!normalize}Normalizing} [p] before using the function
164+ ensures that [b] is a {{!is_rel_seg}relative segment} iff [p] cannot
165 be named (like in ["."], ["../../"], ["/"], etc.). *)
166167val base : t -> t
168(** [base p] is [snd (split_base p)]. *)
169170val basename : t -> string
171+(** [basename p] is [p]'s last non-empty segment if non-relative or
172+ the empty string otherwise. The latter occurs only on {{!is_root}root
173+ paths} and on paths whose last non-empty segment is a
174+ {{!is_rel_seg}relative segment}. See also {!filename} and
175 {!base}. {{!ex_basename}Examples}.
176177 {b Note.} {{!normalize}Normalizing} [p] before using the function
···189(** {1:norm Normalization} *)
190191val rem_empty_seg : t -> t
192+(** [rem_empty_seg p] removes the empty segment of [p] if it exists
193+ and [p] is not a {{!is_root}root path}. This ensure that if [p] is
194+ converted to a string it will not have a trailing {!dir_sep}
195+ unless [p] is a root path. Note that this may affect [p]'s
196+ {{!is_dir_path}directoryness}. {{!ex_rem_empty_seg}Examples}. *)
0197198val normalize : t -> t
199(** [normalize p] is a path that represents the same path as [p],
···211212(** {1:prefix Prefixes}
213214+ {b Warning.} The syntactic {{!is_prefix}prefix relation} between
215+ paths does not, in general, entail directory containement. The following
216+ examples show this:
217+{[
218+is_prefix (v "..") (v "../..") = true
219+is_prefix (v "..") (v ".") = false
220+]}
221+ However, on {{!normalize}normalized}, {{!is_abs}absolute} paths,
222+ the prefix relation does entail directory containement. See also
223+ {!is_rooted}. *)
224225val is_prefix : t -> t -> bool
226(** [is_prefix prefix p] is [true] if [prefix] is a prefix of
···252 prefix [prefix] and preserves [p]'s
253 {{!is_dir_path}directoryness}. This means that [q] is a always
254 {{!is_rel}relative} and that the path [prefix // q] and [p] represent the
255+ same paths. They may however differ syntactically when
256+ converted to a string.}}
257 {{!ex_rem_prefix}Examples}. *)
258259(** {1 Roots and relativization} *)
···263 {ul
264 {- [Some q] if there exists a {{!is_relative}relative} path [q] such
265 that [root // q] and [p] represent the same paths,
266+ {{!is_dir_path}directoryness} included. They may however differ
267+ syntactically when converted to a string.}
268 {- [None] otherwise}}
269270 {{!ex_relativize}Examples.} *)
271000000000000000000000000000272(** {1:predicates Predicates and comparison} *)
273274val is_rel : t -> bool
···307*)
308309val is_dotfile : t -> bool
310+(** [is_dotfile p] is [true] iff [p]'s {{!basename}basename} is non
311+ empty and starts with a ['.'].
312313 {b Warning.} By definition this is a syntactic test. For example it will
314 return [false] on [".ssh/."]. {{!normalize}Normalizing} the
···335(** [of_string s] is the string [s] as a path. [None] is returned if
336 {ul
337 {- [s] or the path following the {{!split_volume}volume} is empty ([""]),
338+ except on Windows UNC paths, see below.}
339 {- [s] has null byte (['\x00']).}
340 {- On Windows, [s] is an invalid UNC path (e.g. ["\\\\"] or ["\\\\a"])}}
341 The following transformations are performed on the string:
···363 character. If there is no such occurence in the segment, the
364 extension is empty. With these definitions, ["."], [".."],
365 ["..."] and dot files like [".ocamlinit"] or ["..ocamlinit"] have
366+ no extension, but [".emacs.d"] and ["..emacs.d"] do have one.
367+368+ {b Warning.} The following functions act on paths whose
369+ {{!basename}basename} is non empty and do nothing otherwise.
370+ {{!normalize}Normalizing} [p] before using the functions ensures
371+ that the functions do nothing iff [p] cannot be named (like in
372+ ["."], ["../../"], ["/"], etc.). *)
373374type ext = string
375(** The type for file extensions. *)
376377val get_ext : ?multi:bool -> t -> ext
378+(** [get_ext p] is [p]'s {{!basename}basename} file extension or the
379+ empty string if there is no extension. If [multi] is [true]
380+ (defaults to [false]), returns the multiple file
381+ extension. {{!ex_get_ext}Examples}. *)
382383val has_ext : ext -> t -> bool
384(** [has_ext e p] is [true] iff [ext p = e || ext ~multi:true p = e].
···386 the test. {{!ex_has_ext}Examples}. *)
387388val ext_exists : ?multi:bool -> t -> bool
389+(** [ext_exists ~multi p] is [true] iff [p]'s last non-empty
390+ segment has an extension. If [multi] is [true] (default to [false])
391+ returns [true] iff [p] has {e more than one} extension.
392 {{!ex_ext_exists}Examples}. *)
393394val add_ext : ext -> t -> t
···553 a path}. This usually means that we don't care whether the path
554 is a {{!is_file_path}file path} (e.g. ["a"]) or a
555 {{!is_dir_path}directory path} (e.g. ["a/"]).}
556+ {- Windows accepts both ['\\'] and ['/'] as directory separator.
557+ However [Fpath] on Windows converts ['/'] to ['\\'] on the
558+ fly. Therefore you should either use ['/'] for defining
559 constant paths you inject with {!v} or better, construct them
560+ directly with {!(/)}. {!to_string} then converts paths to strings
561+ using the platform's specific directory separator {!dir_sep}.}
0562 {- Avoid platform specific {{!split_volume}volumes} or hard-coding file
563 hierarchy conventions in your constants.}
564 {- Do not assume there is a single root path and that it is
565+ ["/"]. On Windows each {{!split_volume}volume} can have a root path.
566+ Use {!is_root} on {{!normalize}normalized} paths to detect roots.}
567 {- Do not use {!to_string} to construct URIs, {!to_string} uses
568 {!dir_sep} to separate segments, on Windows this is ['\\'] which
569+ is not what URIs expect. Access path segments directly
570+ with {!segs}; note that you will need to percent encode these.}}
571572 {1:ex Examples}
573···922923 {2:ex_get_ext {!get_ext}}
924 {ul
925+ {- [get_ext (v "/") = ""]}
926 {- [get_ext (v "/a/b") = ""]}
927+ {- [get_ext (v "a.mli/.") = ""]}
928+ {- [get_ext (v "a.mli/..") = ""]}
929 {- [get_ext (v "a/.ocamlinit") = ""]}
930+ {- [get_ext (v "a/.ocamlinit/") = ""]}
931 {- [get_ext (v "/a/b.") = "."]}
932 {- [get_ext (v "/a/b.mli") = ".mli"]}
933 {- [get_ext (v "a.tar.gz") = ".gz"]}
934 {- [get_ext (v "a/.emacs.d") = ".d"]}
935+ {- [get_ext (v "a/.emacs.d/") = ".d"]}
936 {- [get_ext ~multi:true (v "/a/b.mli") = ".mli"]}
937 {- [get_ext ~multi:true (v "a.tar.gz") = ".tar.gz"]}
938+ {- [get_ext ~multi:true (v "a/.emacs.d") = ".d"]}
939+ {- [get_ext ~multi:true (v "a/.emacs.d/") = ".d/"]}}
940941 {2:ex_has_ext {!has_ext}}
942 {ul
943 {- [has_ext ".mli" (v "a/b.mli") = true]}
944 {- [has_ext "mli" (v "a/b.mli") = true]}
945+ {- [has_ext "mli" (v "a/b.mli/") = true]}
946 {- [has_ext "mli" (v "a/bmli") = false]}
947 {- [has_ext ".tar.gz" (v "a/f.tar.gz") = true]}
948 {- [has_ext "tar.gz" (v "a/f.tar.gz") = true]}
···954 {- [ext_exists (v "a/f.") = true]}
955 {- [ext_exists (v "a/f.gz") = true]}
956 {- [ext_exists (v "a/f.tar.gz") = true]}
957+ {- [ext_exists (v "a/f.tar.gz/") = true]}
958 {- [ext_exists (v ".emacs.d") = true]}
959+ {- [ext_exists (v ".emacs.d/") = true]}
960 {- [ext_exists ~multi:true (v "a/f.gz") = false]}
961 {- [ext_exists ~multi:true (v "a/f.tar.gz") = true]}
962 {- [ext_exists ~multi:true (v ".emacs.d") = false]}}
+33-3
test/test_path.ml
···507 eqp (Fpath.normalize @@ v "a/..b") (v "a/..b");
508 eqp (Fpath.normalize @@ v "./a") (v "a");
509 eqp (Fpath.normalize @@ v "../a") (v "../a");
0510 eqp (Fpath.normalize @@ v "../../a") (v "../../a");
511 eqp (Fpath.normalize @@ v "./a/..") (v "./");
512 eqp (Fpath.normalize @@ v "/a/b/./..") (v "/a/");
···729 relativize (v "../a") (v "../../b") (Some (v "../../b"));
730 relativize (v "a") (v "../../b") (Some (v "../../../b"));
731 relativize (v "a/c") (v "../../b") (Some (v "../../../../b"));
000000732 ()
733734let is_abs_rel = test "Fpath.is_abs_rel" @@ fun () ->
···828 ()
829830let get_ext = test "Fpath.get_ext" @@ fun () ->
0831 eq_str (Fpath.get_ext @@ v ".") "";
832 eq_str (Fpath.get_ext @@ v "..") "";
833 eq_str (Fpath.get_ext @@ v "...") "";
···839 eq_str (Fpath.get_ext @@ v ".a...") ".";
840 eq_str (Fpath.get_ext @@ v ".a....") ".";
841 eq_str (Fpath.get_ext @@ v "a/...") "";
842- eq_str (Fpath.get_ext @@ v "a/.") "";
843- eq_str (Fpath.get_ext @@ v "a/..") "";
844 eq_str (Fpath.get_ext @@ v "a/.a") "";
845 eq_str (Fpath.get_ext @@ v "a/..b") "";
846 eq_str (Fpath.get_ext @@ v "a/..b.a") ".a";
0847 eq_str (Fpath.get_ext @@ v "a/..b..ac") ".ac";
0848 eq_str (Fpath.get_ext @@ v "/a/b") "";
849 eq_str (Fpath.get_ext @@ v "/a/b.") ".";
0850 eq_str (Fpath.get_ext @@ v "a/.ocamlinit") "";
0851 eq_str (Fpath.get_ext @@ v "a/.emacs.d") ".d";
0852 eq_str (Fpath.get_ext @@ v "/a/b.mli") ".mli";
0853 eq_str (Fpath.get_ext @@ v "a.tar.gz") ".gz";
0854 eq_str (Fpath.get_ext @@ v "./a.") ".";
0855 eq_str (Fpath.get_ext @@ v "./a..") ".";
0856 eq_str (Fpath.get_ext @@ v "./.a.") ".";
857- eq_str (Fpath.get_ext @@ v "./.a..") ".";
858 eq_str (Fpath.get_ext ~multi:true @@ v ".") "";
859 eq_str (Fpath.get_ext ~multi:true @@ v "..") "";
860 eq_str (Fpath.get_ext ~multi:true @@ v "...") "";
···862 eq_str (Fpath.get_ext ~multi:true @@ v ".....") "";
863 eq_str (Fpath.get_ext ~multi:true @@ v ".a") "";
864 eq_str (Fpath.get_ext ~multi:true @@ v ".a.") ".";
0865 eq_str (Fpath.get_ext ~multi:true @@ v ".a..") "..";
866 eq_str (Fpath.get_ext ~multi:true @@ v ".a...") "...";
867 eq_str (Fpath.get_ext ~multi:true @@ v ".a....") "....";
···870 eq_str (Fpath.get_ext ~multi:true @@ v "a/..") "";
871 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b") "";
872 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b.a") ".a";
0873 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b..ac") "..ac";
0874 eq_str (Fpath.get_ext ~multi:true @@ v "a/.emacs.d") ".d";
0875 eq_str (Fpath.get_ext ~multi:true @@ v "/a/b.mli") ".mli";
0876 eq_str (Fpath.get_ext ~multi:true @@ v "a.tar.gz") ".tar.gz";
0877 eq_str (Fpath.get_ext ~multi:true @@ v "./a.") ".";
0878 eq_str (Fpath.get_ext ~multi:true @@ v "./a..") "..";
0879 eq_str (Fpath.get_ext ~multi:true @@ v "./.a.") ".";
0880 eq_str (Fpath.get_ext ~multi:true @@ v "./.a..") "..";
0881 ()
882883let has_ext = test "Fpath.has_ext" @@ fun () ->
···905 eq_bool (Fpath.has_ext "..." @@ v "....") false;
906 eq_bool (Fpath.has_ext "..." @@ v ".a...") true;
907 eq_bool (Fpath.has_ext ".mli" @@ v "a/b.mli") true;
0908 eq_bool (Fpath.has_ext "mli" @@ v "a/b.mli") true;
909 eq_bool (Fpath.has_ext "mli" @@ v "a/bmli") false;
910 eq_bool (Fpath.has_ext "mli" @@ v "a/.mli") false;
911 eq_bool (Fpath.has_ext ".tar.gz" @@ v "a/f.tar.gz") true;
912 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/f.tar.gz") true;
0913 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/ftar.gz") false;
914 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/tar.gz") false;
915 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/.tar.gz") false;
916 eq_bool (Fpath.has_ext ".tar" @@ v "a/f.tar.gz") false;
917 eq_bool (Fpath.has_ext ".ocamlinit" @@ v ".ocamlinit") false;
0918 eq_bool (Fpath.has_ext ".ocamlinit" @@ v "..ocamlinit") false;
919 eq_bool (Fpath.has_ext "..ocamlinit" @@ v "...ocamlinit") false;
920 eq_bool (Fpath.has_ext "..ocamlinit" @@ v ".a..ocamlinit") true;
···507 eqp (Fpath.normalize @@ v "a/..b") (v "a/..b");
508 eqp (Fpath.normalize @@ v "./a") (v "a");
509 eqp (Fpath.normalize @@ v "../a") (v "../a");
510+ eqp (Fpath.normalize @@ v "a/..") (v "./");
511 eqp (Fpath.normalize @@ v "../../a") (v "../../a");
512 eqp (Fpath.normalize @@ v "./a/..") (v "./");
513 eqp (Fpath.normalize @@ v "/a/b/./..") (v "/a/");
···730 relativize (v "../a") (v "../../b") (Some (v "../../b"));
731 relativize (v "a") (v "../../b") (Some (v "../../../b"));
732 relativize (v "a/c") (v "../../b") (Some (v "../../../../b"));
733+ if windows then begin
734+ relativize (v "C:a\\c") (v "C:..\\..\\b") (Some (v "..\\..\\..\\..\\b"));
735+ relativize (v "C:a\\c") (v "..\\..\\b") None;
736+ relativize (v "\\\\?\\UNC\\server\\share\\a\\b\\c")
737+ (v "\\\\?\\UNC\\server\\share\\d\\e\\f") (Some (v "../../../d/e/f"));
738+ end;
739 ()
740741let is_abs_rel = test "Fpath.is_abs_rel" @@ fun () ->
···835 ()
836837let get_ext = test "Fpath.get_ext" @@ fun () ->
838+ eq_str (Fpath.get_ext @@ v "/") "";
839 eq_str (Fpath.get_ext @@ v ".") "";
840 eq_str (Fpath.get_ext @@ v "..") "";
841 eq_str (Fpath.get_ext @@ v "...") "";
···847 eq_str (Fpath.get_ext @@ v ".a...") ".";
848 eq_str (Fpath.get_ext @@ v ".a....") ".";
849 eq_str (Fpath.get_ext @@ v "a/...") "";
850+ eq_str (Fpath.get_ext @@ v "a.mli/.") "";
851+ eq_str (Fpath.get_ext @@ v "a.mli/..") "";
852 eq_str (Fpath.get_ext @@ v "a/.a") "";
853 eq_str (Fpath.get_ext @@ v "a/..b") "";
854 eq_str (Fpath.get_ext @@ v "a/..b.a") ".a";
855+ eq_str (Fpath.get_ext @@ v "a/..b.a/") ".a";
856 eq_str (Fpath.get_ext @@ v "a/..b..ac") ".ac";
857+ eq_str (Fpath.get_ext @@ v "a/..b..ac/") ".ac";
858 eq_str (Fpath.get_ext @@ v "/a/b") "";
859 eq_str (Fpath.get_ext @@ v "/a/b.") ".";
860+ eq_str (Fpath.get_ext @@ v "/a/b./") ".";
861 eq_str (Fpath.get_ext @@ v "a/.ocamlinit") "";
862+ eq_str (Fpath.get_ext @@ v "a/.ocamlinit/") "";
863 eq_str (Fpath.get_ext @@ v "a/.emacs.d") ".d";
864+ eq_str (Fpath.get_ext @@ v "a/.emacs.d/") ".d";
865 eq_str (Fpath.get_ext @@ v "/a/b.mli") ".mli";
866+ eq_str (Fpath.get_ext @@ v "/a/b.mli/") ".mli";
867 eq_str (Fpath.get_ext @@ v "a.tar.gz") ".gz";
868+ eq_str (Fpath.get_ext @@ v "a.tar.gz/") ".gz";
869 eq_str (Fpath.get_ext @@ v "./a.") ".";
870+ eq_str (Fpath.get_ext @@ v "./a./") ".";
871 eq_str (Fpath.get_ext @@ v "./a..") ".";
872+ eq_str (Fpath.get_ext @@ v "./a../") ".";
873 eq_str (Fpath.get_ext @@ v "./.a.") ".";
874+ eq_str (Fpath.get_ext @@ v "./.a../") ".";
875 eq_str (Fpath.get_ext ~multi:true @@ v ".") "";
876 eq_str (Fpath.get_ext ~multi:true @@ v "..") "";
877 eq_str (Fpath.get_ext ~multi:true @@ v "...") "";
···879 eq_str (Fpath.get_ext ~multi:true @@ v ".....") "";
880 eq_str (Fpath.get_ext ~multi:true @@ v ".a") "";
881 eq_str (Fpath.get_ext ~multi:true @@ v ".a.") ".";
882+ eq_str (Fpath.get_ext ~multi:true @@ v ".a./") ".";
883 eq_str (Fpath.get_ext ~multi:true @@ v ".a..") "..";
884 eq_str (Fpath.get_ext ~multi:true @@ v ".a...") "...";
885 eq_str (Fpath.get_ext ~multi:true @@ v ".a....") "....";
···888 eq_str (Fpath.get_ext ~multi:true @@ v "a/..") "";
889 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b") "";
890 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b.a") ".a";
891+ eq_str (Fpath.get_ext ~multi:true @@ v "a/..b.a/") ".a";
892 eq_str (Fpath.get_ext ~multi:true @@ v "a/..b..ac") "..ac";
893+ eq_str (Fpath.get_ext ~multi:true @@ v "a/..b..ac/") "..ac";
894 eq_str (Fpath.get_ext ~multi:true @@ v "a/.emacs.d") ".d";
895+ eq_str (Fpath.get_ext ~multi:true @@ v "a/.emacs.d/") ".d";
896 eq_str (Fpath.get_ext ~multi:true @@ v "/a/b.mli") ".mli";
897+ eq_str (Fpath.get_ext ~multi:true @@ v "/a/b.mli/") ".mli";
898 eq_str (Fpath.get_ext ~multi:true @@ v "a.tar.gz") ".tar.gz";
899+ eq_str (Fpath.get_ext ~multi:true @@ v "a.tar.gz/") ".tar.gz";
900 eq_str (Fpath.get_ext ~multi:true @@ v "./a.") ".";
901+ eq_str (Fpath.get_ext ~multi:true @@ v "./a./") ".";
902 eq_str (Fpath.get_ext ~multi:true @@ v "./a..") "..";
903+ eq_str (Fpath.get_ext ~multi:true @@ v "./a../") "..";
904 eq_str (Fpath.get_ext ~multi:true @@ v "./.a.") ".";
905+ eq_str (Fpath.get_ext ~multi:true @@ v "./.a./") ".";
906 eq_str (Fpath.get_ext ~multi:true @@ v "./.a..") "..";
907+ eq_str (Fpath.get_ext ~multi:true @@ v "./.a../") "..";
908 ()
909910let has_ext = test "Fpath.has_ext" @@ fun () ->
···932 eq_bool (Fpath.has_ext "..." @@ v "....") false;
933 eq_bool (Fpath.has_ext "..." @@ v ".a...") true;
934 eq_bool (Fpath.has_ext ".mli" @@ v "a/b.mli") true;
935+ eq_bool (Fpath.has_ext ".mli" @@ v "a/b.mli/") true;
936 eq_bool (Fpath.has_ext "mli" @@ v "a/b.mli") true;
937 eq_bool (Fpath.has_ext "mli" @@ v "a/bmli") false;
938 eq_bool (Fpath.has_ext "mli" @@ v "a/.mli") false;
939 eq_bool (Fpath.has_ext ".tar.gz" @@ v "a/f.tar.gz") true;
940 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/f.tar.gz") true;
941+ eq_bool (Fpath.has_ext "tar.gz" @@ v "a/f.tar.gz/") true;
942 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/ftar.gz") false;
943 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/tar.gz") false;
944 eq_bool (Fpath.has_ext "tar.gz" @@ v "a/.tar.gz") false;
945 eq_bool (Fpath.has_ext ".tar" @@ v "a/f.tar.gz") false;
946 eq_bool (Fpath.has_ext ".ocamlinit" @@ v ".ocamlinit") false;
947+ eq_bool (Fpath.has_ext ".ocamlinit/" @@ v ".ocamlinit") false;
948 eq_bool (Fpath.has_ext ".ocamlinit" @@ v "..ocamlinit") false;
949 eq_bool (Fpath.has_ext "..ocamlinit" @@ v "...ocamlinit") false;
950 eq_bool (Fpath.has_ext "..ocamlinit" @@ v ".a..ocamlinit") true;