/** * Copyright 2004-present Facebook. All Rights Reserved. * * @emails oncall+ads_front_end_infra */; [@warning "-27-39-32-34"]; let sep = "/"; let homeChar = "~"; type absolute; type relative; type upDirs = int; /* int 0 implies ./ and 1 implies ../ etc */ /** * We might eventually want to allow extending this with many * reference points. */ type relFrom = | Home | Any; type base('kind) = /* Optional drive name */ | Abs(option(string)): base(absolute) | Rel(relFrom, upDirs): base(relative); /** * Internal representation of paths. The list of strings represents all * subdirectories after the base (in reverse order - head of the list is the * rightmost segment of the path). */ type t('kind) = (base('kind), list(string)); type firstClass = | Absolute(t(absolute)) | Relative(t(relative)); type opaqueBase = | Base(base('exists)): opaqueBase; type opaqueT = (opaqueBase, list(string)); let drive = name => (Abs(Some(name)), []); let root = (Abs(None), []); let home = (Rel(Home, 0), []); let dot = (Rel(Any, 0), []); let hasParentDir = ((Abs(_), lst): t(absolute)) => lst !== []; let rec revSegmentsAreInside = (~ofSegments, l) => switch (ofSegments, l) { | ([], [_, ..._]) => true | ([], []) => true | ([_, ..._], []) => false | ([hd, ...tl], [hd2, ...tl2]) => String.equal(hd, hd2) && revSegmentsAreInside(~ofSegments=tl, tl2) }; let segmentsAreInside = (~ofSegments, l) => revSegmentsAreInside(~ofSegments=List.rev(ofSegments), List.rev(l)); let isDescendent: type kind. (~ofPath: t(kind), t(kind)) => bool = (~ofPath, p) => switch (ofPath, p) { | ((Abs(dr1), l1), (Abs(dr2), l2)) => switch (dr1, dr2) { | (None, None) => segmentsAreInside(~ofSegments=l1, l2) | (Some(d1), Some(d2)) => String.equal(d1, d2) && segmentsAreInside(~ofSegments=l1, l2) | (Some(_), None) | (None, Some(_)) => false } | ((Rel(Any, d1), l1), (Rel(Any, d2), l2)) => d1 === d1 && segmentsAreInside(~ofSegments=l1, l2) | ((Rel(Home, d1), l1), (Rel(Home, d2), l2)) => d1 === d1 && segmentsAreInside(~ofSegments=l1, l2) | ((Rel(Any, _), _), (Rel(Home, _), _)) => false | ((Rel(Home, _), _), (Rel(Any, _), _)) => false }; let toString: type kind. t(kind) => string = path => switch (path) { | (Abs(l), lst) => let lbl = switch (l) { | None => "" | Some(txt) => txt }; lbl ++ "/" ++ (lst |> List.rev |> String.concat(sep)); | (Rel(w, i), lst) => let init = switch (w) { | Any => "." ++ sep | Home => "~" ++ sep }; let rest = lst |> List.rev |> List.append(Array.to_list(Array.init(i, i => ".."))) |> String.concat(sep); init ++ rest; }; /** * Expose this under the name `toDebugString` and accept any kind of path. * The name is to warn people about using this for relative paths. This may * print paths like `"."` and `"~"`, which is not very meaningful. */ let toDebugString = toString; type token = | SLASH | DOT | TILDE | DOTDOT | DRIVE(string) | TXT(string); let makeToken = s => switch (s) { | "~" => TILDE | "." => DOT | ".." => DOTDOT | s when String.length(s) >= 2 && s.[String.length(s) - 1] === ':' => DRIVE(s) | s => TXT(s) }; /* * Splits on slashes, but being intelligent about escaped slashes. */ let lex = s => { let s = String.trim(s); let len = String.length(s); let revTokens = {contents: []}; /* j is what you are all caught up to */ let j = {contents: (-1)}; let prevEsc = {contents: false}; for (i in 0 to len - 1) { let ch = String.unsafe_get(s, i); if (ch === '/' && !prevEsc.contents) { if (j.contents !== i - 1) { let tok = makeToken(String.sub(s, j.contents + 1, i - j.contents - 1)); revTokens.contents = [tok, ...revTokens.contents]; }; revTokens.contents = [SLASH, ...revTokens.contents]; j.contents = i; }; prevEsc.contents = ch === '\\' && !prevEsc.contents; }; let rev = j.contents === len - 1 ? revTokens.contents : [ makeToken(String.sub(s, j.contents + 1, len - 1 - j.contents)), ...revTokens.contents, ]; List.rev(rev); }; let parseFirstToken = token => switch (token) { | SLASH => (Base(Abs(None)), []) | DOT => (Base(Rel(Any, 0)), []) | TILDE => (Base(Rel(Home, 0)), []) | DOTDOT => (Base(Rel(Any, 1)), []) | DRIVE(l) => (Base(Abs(Some(l))), []) | TXT(s) => (Base(Rel(Any, 0)), [s]) }; let rec parseNextToken: type kind. (t(kind), token) => t(kind) = (path, nextToken) => switch (path, nextToken) { | (path, SLASH) => path | (path, DOT) => path | ((base, subs), TILDE) => (base, [homeChar, ...subs]) | ((base, subs), DRIVE(l)) => (base, [l, ...subs]) | ((base, subs), TXT(s)) => (base, [s, ...subs]) | ((base, [hd, ...tl]), DOTDOT) => (base, tl) | ((Rel(Any, r), []), DOTDOT) => (Rel(Any, r + 1), []) | ((Rel(Home, r), []), DOTDOT) => (Rel(Home, r + 1), []) | ((Abs(_), []), DOTDOT) => path }; let parseFirstTokenAbsolute = token => switch (token) { | SLASH => Some((Abs(None), [])) | DRIVE(l) => Some((Abs(Some(l)), [])) | TXT(_) | DOT | TILDE | DOTDOT => None }; let parseFirstTokenRelative = token => switch (token) { | DOT => Some((Rel(Any, 0), [])) | TILDE => Some((Rel(Home, 0), [])) | DOTDOT => Some((Rel(Any, 1), [])) | TXT(s) => Some((Rel(Any, 0), [s])) | SLASH => None | DRIVE(l) => None }; let absolute = s => switch (lex(s)) { /* Cannot pass empty string for absolute path */ | [] => None | [hd, ...tl] => switch (parseFirstTokenAbsolute(hd)) { | None => None | Some(initAbsPath) => Some(List.fold_left(parseNextToken, initAbsPath, tl)) } }; let absoluteExn = s => switch (lex(s)) { /* Cannot pass empty string for absolute path */ | [] => raise(Invalid_argument("Empty path is not a valid absolute path.")) | [hd, ...tl] => switch (parseFirstTokenAbsolute(hd)) { | None => raise( Invalid_argument("First token in path " ++ s ++ " is not absolute."), ) | Some(initAbsPath) => List.fold_left(parseNextToken, initAbsPath, tl) } }; let relative = s => { let (tok, tl) = switch (lex(s)) { | [] => (DOT, []) | [hd, ...tl] => (hd, tl) }; switch (parseFirstTokenRelative(tok)) { | None => None | Some(initRelPath) => Some(List.fold_left(parseNextToken, initRelPath, tl)) }; }; let relativeExn = s => switch (lex(s)) { /* Cannot pass empty string for absolute path */ | [] => dot | [hd, ...tl] => switch (parseFirstTokenRelative(hd)) { | None => raise( Invalid_argument("First token in path " ++ s ++ " not relative."), ) | Some(initRelPath) => List.fold_left(parseNextToken, initRelPath, tl) } }; /** * Relates two positive integers to zero and eachother. */ type ord = | /** 0 === i === j */ Zeros | /** 0 === i < j */ ZeroPositive | /** i > 0 === j */ PositiveZero | /** 0 < i && 0 < j */ Positives; /** * Using `ord` allows us to retain exhaustiveness pattern matching checks that * would normally be lost when adding `when i < j` guards to matches. It's * very likely inlined so there's no performance hit. Annotate as int so that * it isn't inferred to be polymorphic. */ let ord = (i: int, j: int) => i === 0 && j === 0 ? Zeros : i === 0 ? ZeroPositive : j === 0 ? PositiveZero : Positives; let rec repeat = (soFar, i, s) => i === 0 ? soFar : repeat(soFar ++ s, i - 1, s); /* * relativize(a/rest1..., a/rest2...) == relativize(rest1..., rest2...) * relativize(../rest1..., ../rest2...) == relativize(rest1..., res2...) * relativize(a/rest1..., b/rest2...) == [...len(1)]/b/rest2 * relativize(../a/rest1..., b/rest2...) == raise * relativize(a/rest1..., ../b/rest2...) == [...len(1)]../b/rest2 * * "upDirs" is the number of ../ the path is assumed to have. The segments * `s1`/`s2`, are in the path order from left to right, unlike `Path.t` which * usually stores them in reverse order. Relativizing paths is one place where * it's more convenient to have them in the left to right segment order. */ let rec relativizeDepth = ((upDirs1, s1), (upDirs2, s2)) => switch (ord(upDirs1, upDirs2), s1, s2) { | (Zeros, [hd1, ...tl1], [hd2, ...tl2]) => if (String.compare(hd1, hd2) === 0) { relativizeDepth((0, tl1), (0, tl2)); } else { (List.length(s1), s2); } | (Zeros, [], []) => (0, []) | (Zeros, [], [hd2, ...tl2] as s2) => (upDirs2, s2) | (Zeros, [hd1, ...tl1] as s1, []) => (List.length(s1), []) | (Positives, _, _) => relativizeDepth((upDirs1 - 1, s1), (upDirs2 - 1, s2)) | (ZeroPositive, _, _) => (List.length(s1) + upDirs2, s2) | (PositiveZero, _, _) => raise( Invalid_argument( "Cannot relativize paths source='" ++ repeat("", upDirs1, "../") ++ String.concat(sep, s1) ++ "' dest='" ++ repeat("", upDirs2, "../") ++ String.concat(sep, s2), ), ) }; let raiseDriveMismatch = (p1, p2) => raise( Invalid_argument( "Cannot relativize paths with different drives or relative roots " ++ toString(p1) ++ " and " ++ toString(p2), ), ); let relativizeExn: type k. (~source: t(k), ~dest: t(k)) => t(relative) = (~source, ~dest) => { let (depth, segs) = switch (source, dest) { | ((Abs(d1), s1), (Abs(d2), s2)) => switch (d1, d2) { | (None, None) => relativizeDepth((0, List.rev(s1)), (0, List.rev(s2))) | (Some(_), None) => raiseDriveMismatch(source, dest) | (None, Some(_)) => raiseDriveMismatch(source, dest) | (Some(d1), Some(d2)) => String.compare(d1, d2) !== 0 ? raiseDriveMismatch(source, dest) : relativizeDepth((0, List.rev(s1)), (0, List.rev(s2))) } | ((Rel(w1, r1), s1), (Rel(w2, r2), s2)) => w1 === w2 ? relativizeDepth((r1, List.rev(s1)), (r2, List.rev(s2))) : raiseDriveMismatch(source, dest) }; (Rel(Any, depth), List.rev(segs)); }; let relativize: type k. (~source: t(k), ~dest: t(k)) => result(t(relative), exn) = (~source, ~dest) => try (Ok(relativizeExn(~source, ~dest))) { | Invalid_argument(_) as e => Error(e) }; let rec segEq = (l1, l2) => switch (l1, l2) { | ([], []) => true | ([], [_, ..._]) => false | ([_, ..._], []) => false | ([hd1, ...tl1], [hd2, ...tl2]) => String.compare(hd1, hd2) === 0 && segEq(tl1, tl2) }; let eq: type k1 k2. (t(k1), t(k2)) => bool = (p1, p2) => switch (p1, p2) { | ((Abs(_), s1), (Rel(_), s2)) => false | ((Rel(_), s1), (Abs(_), s2)) => false | ((Abs(d1), s1), (Abs(d2), s2)) => switch (d1, d2) { | (Some(_), None) | (None, Some(_)) => false | (None, None) => segEq(s1, s2) | (Some(d1), Some(d2)) => String.compare(d1, d2) === 0 && segEq(s1, s2) } | ((Rel(w1, r1), s1), (Rel(w2, r2), s2)) => w1 === w2 && r1 === r2 && segEq(s1, s2) }; let absoluteEq = eq; let relativeEq = eq; let testForPath = s => switch (absolute(s)) { | Some(abs) => Some(Absolute(abs)) | None => switch (relative(s)) { | Some(r) => Some(Relative(r)) | None => None } }; let firstClass: type k. t(k) => firstClass = p => switch (p) { | (Abs(d), s) => Absolute((Abs(d), s)) | (Rel(w, r), s) => Relative((Rel(w, r), s)) }; let testForPathExn = s => switch (testForPath(s)) { | Some(res) => res | None => raise(Invalid_argument("Path neither absolute nor relative.")) }; let continue = (s, path) => List.fold_left(parseNextToken, path, lex(s)); let rec join: type k1 k2. (t(k1), t(k2)) => t(k1) = (p1, p2) => switch (p1, p2) { | ((Rel(w, r1), []), (Rel(Any, r2), s2)) => (Rel(w, r1 + r2), s2) | ((Rel(w, r1), [s1hd, ...s1tl] as s1), (Rel(Any, r2), s2)) => r2 > 0 ? join((Rel(w, r1), s1tl), (Rel(Any, r2 - 1), s2)) : (Rel(w, r1), List.append(s2, s1)) | ((b1, s1), (Rel(Home, r2), s2)) => join((b1, [homeChar, ...List.append(s2, s1)]), (Rel(Any, r2), s2)) | ((b1, s1), (Abs(Some(ll)), s2)) => ( b1, [ll, ...List.append(s2, s1)], ) | ((b1, s1), (Abs(None), s2)) => (b1, List.append(s2, s1)) | ((Abs(_) as d, []), (Rel(Any, r2), s2)) => (d, s2) | ((Abs(_) as d, [s1hd, ...s1tl] as s1), (Rel(Any, r2), s2)) => r2 > 0 ? join((d, s1tl), (Rel(Any, r2 - 1), s2)) : (d, List.append(s2, s1)) }; let rec dirName: type k1. t(k1) => t(k1) = p1 => switch (p1) { | (Rel(w, r1), []) => (Rel(w, r1 + 1), []) | (Rel(w, r1), [s1hd, ...s1tl]) => (Rel(w, r1), s1tl) | (Abs(_) as d, []) => (d, []) | (Abs(_) as d, [s1hd, ...s1tl]) => (d, s1tl) }; let rec baseName: type k1. t(k1) => option(string) = p1 => switch (p1) { | (Rel(w, r1), []) => None | (Rel(w, r1), [s1hd, ...s1tl]) => Some(s1hd) | (Abs(_), []) => None | (Abs(_), [s1hd, ...s1tl]) => Some(s1hd) }; let sub: type k1. (string, t(k1)) => t(k1) = (name, path) => continue(name, path); /** * Append functions always follow their "natural" left/right ordering, * regardless of t-first/last. * * The following pairs are equivalent but note that `append` is always safe. * * Path.append(Path.root, "foo"); * Option.getUnsafe(Path.absolute("/foo")); * * Path.append(Path.root, "foo/bar"); * Option.getUnsafe(Path.absolute("/foo/bar")); * * Path.append(Path.drive("C"), "foo/bar"); * Option.getUnsafe(Path.absolute("C:/foo/bar")); * * Path.append(Path.dot, "foo"); * Option.getUnsafe(Path.relative("./foo")); */ let append: type k1. (t(k1), string) => t(k1) = (path, name) => continue(name, path); module At = { let (/) = append; /** * Applies `dirName` to the first argument, then passes the result to * `append` with the second. * * let result = root / "foo" / "bar" /../ "baz"; * * Would result in * * "/foo/baz" */ let (/../) = (dir, s) => append(dirName(dir), s); let (/../../) = (dir, s) => append(dirName(dirName(dir)), s); let (/../../../) = (dir, s) => append(dirName(dirName(dirName(dir))), s); let (/../../../../) = (dir, s) => append(dirName(dirName(dirName(dirName(dir)))), s); let (/../../../../../) = (dir, s) => append(dirName(dirName(dirName(dirName(dirName(dir))))), s); let (/../../../../../../) = (dir, s) => append( dirName(dirName(dirName(dirName(dirName(dirName(dir)))))), s, ); };