You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
14 KiB
504 lines
14 KiB
/** |
|
* 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; |
|
|
|
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 === d2 && segmentsAreInside(~ofSegments=l1, l2) |
|
| ((Rel(Home, d1), l1), (Rel(Home, d2), l2)) => |
|
d1 === d2 && 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, _ => ".."))) |
|
|> 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 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 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 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, |
|
); |
|
};
|
|
|