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.
 
 
 
 

500 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,
);
};