use core::convert::TryFrom;
use alloc::{format, string::String};
use crate::{
parser::RiReferenceComponents,
spec::Spec,
types::{RiAbsoluteStr, RiReferenceStr, RiString},
};
pub fn resolve<S: Spec>(
reference: impl AsRef<RiReferenceStr<S>>,
base: impl AsRef<RiAbsoluteStr<S>>,
is_strict: bool,
) -> RiString<S> {
resolve_impl(reference.as_ref(), base.as_ref(), is_strict)
}
fn resolve_impl<S: Spec>(
reference: &RiReferenceStr<S>,
base: &RiAbsoluteStr<S>,
is_strict: bool,
) -> RiString<S> {
let r = RiReferenceComponents::<S>::from(reference);
let b = RiReferenceComponents::<S>::from(base.as_ref());
let b_scheme = b
.scheme
.expect("Should never fail: `RiAbsoluteStr` should have `scheme` part");
if let Some(r_scheme) = r.scheme {
if is_strict || r_scheme != b_scheme {
let path_new = remove_dot_segments(r.path);
return recompose_components(r_scheme, r.authority, &path_new, r.query, r.fragment);
}
}
if let Some(r_authority) = r.authority {
let path_new = remove_dot_segments(r.path);
return recompose_components(b_scheme, Some(r_authority), &path_new, r.query, r.fragment);
}
if r.path.is_empty() {
return recompose_components(
b_scheme,
b.authority,
b.path,
r.query.or(b.query),
r.fragment,
);
}
let path_new = if r.path.starts_with('/') {
remove_dot_segments(r.path)
} else {
remove_dot_segments(&merge(b.path, r.path, b.authority.is_some()))
};
recompose_components(b_scheme, b.authority, &path_new, r.query, r.fragment)
}
fn recompose_components<S: Spec>(
scheme: &str,
authority: Option<&str>,
path: &str,
query: Option<&str>,
fragment: Option<&str>,
) -> RiString<S> {
let mut s = String::from(scheme);
s.push(':');
if let Some(authority) = authority {
s.push_str("//");
s.push_str(authority);
}
s.push_str(path);
if let Some(query) = query {
s.push('?');
s.push_str(query);
}
if let Some(fragment) = fragment {
s.push('#');
s.push_str(fragment);
}
RiString::<S>::try_from(s).unwrap_or_else(|e| {
panic!(
"Should never happen: if panicked, `resolve()` has a bug: {}",
e
)
})
}
fn remove_dot_segments(mut input: &str) -> String {
let mut output = String::new();
while !input.is_empty() {
input = remove_dot_segments_step(input, &mut output);
assert!(input.is_empty() || input.as_bytes()[0] == b'/');
}
output
}
fn remove_dot_segments_step<'a>(input: &'a str, output: &'_ mut String) -> &'a str {
if input.starts_with("../") {
&input[3..]
} else if input.starts_with("./") || input.starts_with("/./") {
&input[2..]
} else if input == "/." {
&input[0..1]
} else if input.starts_with("/../") {
match output.rfind('/') {
Some(slash_pos) => output.truncate(slash_pos),
None => output.clear(),
}
assert!(!output.ends_with('/'));
&input[3..]
} else if input == "/.." {
match output.rfind('/') {
Some(slash_pos) => output.truncate(slash_pos),
None => output.clear(),
}
assert!(!output.ends_with('/'));
"/"
} else if input == "." || input == ".." {
""
} else {
let (first_seg, rest) = match input.find('/').and_then(|i| {
if i == 0 {
input[1..].find('/').map(|i| i + 1)
} else {
Some(i)
}
}) {
Some(i) => (&input[..i], &input[i..]),
None => (input, ""),
};
output.push_str(first_seg);
rest
}
}
fn merge(base_path: &str, ref_path: &str, base_authority_defined: bool) -> String {
if base_authority_defined && base_path.is_empty() {
format!("/{}", ref_path)
} else {
let base_dir = &base_path[..base_path.rfind('/').map(|i| i + 1).unwrap_or(0)];
format!("{}{}", base_dir, ref_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{IriAbsoluteStr, IriReferenceStr};
#[test]
fn test_remove_dot_segments() {
const TEST_CASES: &[&[(&str, &str)]] = &[
&[
("", "/a/b/c/./../../g"),
("/a", "/b/c/./../../g"),
("/a/b", "/c/./../../g"),
("/a/b/c", "/./../../g"),
("/a/b/c", "/../../g"),
("/a/b", "/../g"),
("/a", "/g"),
("/a/g", ""),
],
&[
("", "mid/content=5/../6"),
("mid", "/content=5/../6"),
("mid/content=5", "/../6"),
("mid", "/6"),
("mid/6", ""),
],
];
for steps in TEST_CASES {
for steps in steps.windows(2) {
let (out_prev, in_prev) = steps[0];
let (out_expected, in_expected) = steps[1];
let mut out_got = String::from(out_prev);
let in_got = remove_dot_segments_step(in_prev, &mut out_got);
assert_eq!(
(out_got.as_ref(), in_got),
(out_expected, in_expected),
"out_prev = {:?}, in_prev = {:?}",
out_prev,
in_prev
);
}
assert_eq!(remove_dot_segments(steps[0].1), steps[steps.len() - 1].0);
}
}
#[test]
fn test_reference_resolution() {
const STRICT_TEST_CASES: &[(&str, &[(&str, &str)])] = &[
(
"http://a/b/c/d;p?q",
&[
("g:h", "g:h"),
("g", "http://a/b/c/g"),
("./g", "http://a/b/c/g"),
("g/", "http://a/b/c/g/"),
("/g", "http://a/g"),
("//g", "http://g"),
("?y", "http://a/b/c/d;p?y"),
("g?y", "http://a/b/c/g?y"),
("#s", "http://a/b/c/d;p?q#s"),
("g#s", "http://a/b/c/g#s"),
("g?y#s", "http://a/b/c/g?y#s"),
(";x", "http://a/b/c/;x"),
("g;x", "http://a/b/c/g;x"),
("g;x?y#s", "http://a/b/c/g;x?y#s"),
("", "http://a/b/c/d;p?q"),
(".", "http://a/b/c/"),
("./", "http://a/b/c/"),
("..", "http://a/b/"),
("../", "http://a/b/"),
("../g", "http://a/b/g"),
("../..", "http://a/"),
("../../", "http://a/"),
("../../g", "http://a/g"),
],
),
(
"http://a/b/c/d;p?q",
&[
("../../../g", "http://a/g"),
("../../../../g", "http://a/g"),
("/./g", "http://a/g"),
("/../g", "http://a/g"),
("g.", "http://a/b/c/g."),
(".g", "http://a/b/c/.g"),
("g..", "http://a/b/c/g.."),
("..g", "http://a/b/c/..g"),
("./../g", "http://a/b/g"),
("./g/.", "http://a/b/c/g/"),
("g/./h", "http://a/b/c/g/h"),
("g/../h", "http://a/b/c/h"),
("g;x=1/./y", "http://a/b/c/g;x=1/y"),
("g;x=1/../y", "http://a/b/c/y"),
("g?y/./x", "http://a/b/c/g?y/./x"),
("g?y/../x", "http://a/b/c/g?y/../x"),
("g#s/./x", "http://a/b/c/g#s/./x"),
("g#s/../x", "http://a/b/c/g#s/../x"),
("http:g", "http:g"),
],
),
];
for (base, pairs) in STRICT_TEST_CASES {
let base = <&IriAbsoluteStr>::try_from(*base)
.expect("Invalid testcase: base IRI should be absolute IRI");
for (input, expected) in *pairs {
let input = <&IriReferenceStr>::try_from(*input)
.expect("Invalid testcase: `input` should be IRI reference");
let got = resolve(input, base, true);
assert_eq!(
AsRef::<str>::as_ref(&got),
*expected,
"base = {:?}, input = {:?}",
base,
input
);
}
}
{
let base = <&IriAbsoluteStr>::try_from("http://a/b/c/d;p?q")
.expect("Invalid testcase: base IRI should be absolute IRI");
let input = <&IriReferenceStr>::try_from("http:g")
.expect("Invalid testcase: `input` should be IRI reference");
let got = resolve(input, base, false);
assert_eq!(AsRef::<str>::as_ref(&got), "http://a/b/c/g");
}
}
}