///|
/// Reference resolution, as defined in
/// [Section 5 of RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-5).
///|
/// An error raised when resolving a URI/IRI reference.
pub(all) suberror ResolveError {
/// The base has a fragment.
BaseWithFragment
/// The base has no authority and its path is rootless, but the reference
/// is relative, is not empty and does not start with `'#'`.
InvalidReferenceAgainstOpaqueBase
/// An underflow occurred in path resolution. Raised only when
/// `allow_path_underflow` is `false`.
PathUnderflow
} derive(Eq, Debug)
///|
pub impl Show for ResolveError with fn output(self, logger) {
let msg = match self {
BaseWithFragment => "base should not have fragment"
InvalidReferenceAgainstOpaqueBase =>
"when base has a rootless path and no authority, reference should either have scheme, be empty or start with '#'"
PathUnderflow => "underflow occurred in path resolution"
}
logger.write_string(msg)
}
///|
/// The three kinds of path segment that matter to dot segment removal.
priv enum SegKind {
Dot
DoubleDot
NormalSeg
}
///|
/// Checks whether `s[start..start + 3]` is `"%2E"`, ignoring case.
fn is_pct2e(s : String, start : Int) -> Bool {
code_at(s, start) == '%' &&
code_at(s, start + 1) == '2' &&
(code_at(s, start + 2) | 0x20) == 'e'
}
///|
/// Classifies the segment `s[start..end]`, treating the percent-encoded forms
/// of `'.'` as dots.
fn classify_segment(s : String, start : Int, end : Int) -> SegKind {
match end - start {
1 => if code_at(s, start) == '.' { Dot } else { NormalSeg }
2 =>
if code_at(s, start) == '.' && code_at(s, start + 1) == '.' {
DoubleDot
} else {
NormalSeg
}
3 => if is_pct2e(s, start) { Dot } else { NormalSeg }
4 =>
if (code_at(s, start) == '.' && is_pct2e(s, start + 1)) ||
(code_at(s, start + 3) == '.' && is_pct2e(s, start)) {
DoubleDot
} else {
NormalSeg
}
6 =>
if is_pct2e(s, start) && is_pct2e(s, start + 3) {
DoubleDot
} else {
NormalSeg
}
_ => NormalSeg
}
}
///|
/// Removes the dot segments of an absolute path, optionally continuing with a
/// relative path merged onto it.
///
/// Returns the resulting path and whether an underflow occurred, i.e., whether
/// a `".."` segment tried to escape the root.
fn remove_dot_segments(abs : String, rel : String?) -> (String, Bool) {
// Each piece is a nonempty segment together with the `'/'` that follows it,
// if any. Only the last piece may lack a trailing `'/'`, and the first piece
// is always `"/"`, so popping a piece is exactly truncating the path to the
// previous segment boundary.
let pieces : Array[String] = []
let mut underflow = false
let parts = match rel {
Some(rel) => [abs, rel]
None => [abs]
}
for part in parts {
let len = part.length()
let mut start = 0
while start < len {
let mut end = start
while end < len && code_at(part, end) != '/' {
end += 1
}
match classify_segment(part, start, end) {
Dot => ()
DoubleDot =>
if pieces.length() <= 1 {
underflow = true
} else {
pieces.pop() |> ignore
}
// Append the segment and the following '/' if any.
NormalSeg =>
pieces.push(
slice(part, start, if end + 1 < len { end + 1 } else { len }),
)
}
if end == len {
break
}
// Skip '/'.
start = end + 1
}
}
let sb = StringBuilder::new(size_hint=abs.length())
for piece in pieces {
sb.write_string(piece)
}
(sb.to_string(), underflow)
}
///|
/// Resolves `r` against `base`, which must have a scheme.
fn resolve_ri(
base : Ri,
r : Ri,
allow_path_underflow : Bool,
) -> Ri raise ResolveError {
if base.fragment is Some(_) {
raise BaseWithFragment
}
if base.authority is None &&
!base.path.has_prefix("/") &&
r.scheme is None &&
!(r.text.is_empty() || code_at(r.text, 0) == '#') {
raise InvalidReferenceAgainstOpaqueBase
}
// The target components, per Section 5.2.2 of RFC 3986. `t_path_rel` is the
// relative path to be merged onto `t_path_abs`, if any.
let (t_scheme, t_authority, t_path_abs, t_path_rel, t_query) = if r.scheme
is Some(_) {
(r.scheme, r.authority, r.path, None, r.query)
} else if r.authority is Some(_) {
(base.scheme, r.authority, r.path, None, r.query)
} else if r.path.is_empty() {
(
base.scheme,
base.authority,
base.path,
None,
if r.query is Some(_) {
r.query
} else {
base.query
},
)
} else if r.path.has_prefix("/") {
(base.scheme, base.authority, r.path, None, r.query)
} else {
let base_path = if base.path.is_empty() { "/" } else { base.path }
// Make sure that swapping the order of resolution and normalization
// does not change the result.
let last_slash_idx = base_path.rev_find("/").unwrap()
let base_path_stripped = match
classify_segment(base_path, last_slash_idx + 1, base_path.length()) {
DoubleDot => base_path
_ => slice(base_path, 0, last_slash_idx + 1)
}
// Instead of merging the paths, remove dot segments incrementally.
(base.scheme, base.authority, base_path_stripped, Some(r.path), r.query)
}
let path = if t_path_abs.has_prefix("/") {
let (path, underflow) = remove_dot_segments(t_path_abs, t_path_rel)
if underflow && !allow_path_underflow {
raise PathUnderflow
}
path
} else {
t_path_abs
}
// Close the loophole in the original algorithm.
let path = if t_authority is None && path.has_prefix("//") {
"/." + path
} else {
path
}
ri(
scheme=t_scheme,
authority=t_authority,
path~,
query=t_query,
fragment=r.fragment,
)
}
///|
/// Resolves the URI reference against the given base URI and returns the
/// target URI.
///
/// The base URI must have no fragment. If it also has no authority and its
/// path is rootless, the reference must either have a scheme, be empty, or
/// start with `'#'`.
///
/// Setting `allow_path_underflow` to `false` deviates from RFC 3986 by
/// rejecting references whose `".."` segments escape the root of the base.
///
/// ```mbt check
/// test {
/// let base = @uri.Uri::parse("http://example.com/foo/bar")
/// inspect(
/// @uri.UriRef::parse("baz").resolve_against(base),
/// content="http://example.com/foo/baz",
/// )
/// inspect(
/// @uri.UriRef::parse("../baz").resolve_against(base),
/// content="http://example.com/baz",
/// )
/// inspect(
/// @uri.UriRef::parse("?baz").resolve_against(base),
/// content="http://example.com/foo/bar?baz",
/// )
/// }
/// ```
pub fn UriRef::resolve_against(
self : UriRef,
base : Uri,
allow_path_underflow? : Bool = true,
) -> Uri raise ResolveError {
uri_of_ri(resolve_ri(base.to_ri(), self.to_ri(), allow_path_underflow))
}
///|
/// Resolves the IRI reference against the given base IRI and returns the
/// target IRI. See [`UriRef::resolve_against`] for the exact behavior.
pub fn IriRef::resolve_against(
self : IriRef,
base : Iri,
allow_path_underflow? : Bool = true,
) -> Iri raise ResolveError {
iri_of_ri(resolve_ri(base.to_ri(), self.to_ri(), allow_path_underflow))
}