//! Reference implementation based on RFC 3986 section 5.
#![cfg(feature = "alloc")]
extern crate alloc;
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::String;
use iri_string::spec::Spec;
use iri_string::types::{RiAbsoluteStr, RiReferenceStr, RiString};
fn to_major_components<S: Spec>(
s: &RiReferenceStr<S>,
) -> (Option<&str>, Option<&str>, &str, Option<&str>, Option<&str>) {
(
s.scheme_str(),
s.authority_str(),
s.path_str(),
s.query().map(|s| s.as_str()),
s.fragment().map(|s| s.as_str()),
)
}
/// Resolves the relative IRI.
///
/// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.2.2>.
pub(super) fn resolve<S: Spec>(
reference: &RiReferenceStr<S>,
base: &RiAbsoluteStr<S>,
) -> RiString<S> {
let (r_scheme, r_authority, r_path, r_query, r_fragment) = to_major_components(reference);
let (b_scheme, b_authority, b_path, b_query, _) = to_major_components(base.as_ref());
let t_scheme: &str;
let t_authority: Option<&str>;
let t_path: String;
let t_query: Option<&str>;
if let Some(r_scheme) = r_scheme {
t_scheme = r_scheme;
t_authority = r_authority;
t_path = remove_dot_segments(r_path.into());
t_query = r_query;
} else {
if r_authority.is_some() {
t_authority = r_authority;
t_path = remove_dot_segments(r_path.into());
t_query = r_query;
} else {
if r_path.is_empty() {
t_path = b_path.into();
if r_query.is_some() {
t_query = r_query;
} else {
t_query = b_query;
}
} else {
if r_path.starts_with('/') {
t_path = remove_dot_segments(r_path.into());
} else {
t_path = remove_dot_segments(merge(b_path, r_path, b_authority.is_some()));
}
t_query = r_query;
}
t_authority = b_authority;
}
t_scheme = b_scheme.expect("non-relative IRI must have a scheme");
}
let t_fragment: Option<&str> = r_fragment;
let s = recompose(t_scheme, t_authority, &t_path, t_query, t_fragment);
RiString::<S>::try_from(s).expect("resolution result must be a valid IRI")
}
/// Merges the two paths.
///
/// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.2.3>.
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_path_end = base_path.rfind('/').map_or(0, |s| s + 1);
format!("{}{}", &base_path[..base_path_end], ref_path)
}
}
/// Removes dot segments from the path.
///
/// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.2.4>.
fn remove_dot_segments(mut input: String) -> String {
let mut output = String::new();
while !input.is_empty() {
if input.starts_with("../") {
// 2A.
input.drain(..3);
} else if input.starts_with("./") {
// 2A.
input.drain(..2);
} else if input.starts_with("/./") {
// 2B.
input.replace_range(..3, "/");
} else if input == "/." {
// 2B.
input.replace_range(..2, "/");
} else if input.starts_with("/../") {
// 2C.
input.replace_range(..4, "/");
remove_last_segment_and_preceding_slash(&mut output);
} else if input == "/.." {
// 2C.
input.replace_range(..3, "/");
remove_last_segment_and_preceding_slash(&mut output);
} else if input == "." {
// 2D.
input.drain(..1);
} else if input == ".." {
// 2D.
input.drain(..2);
} else {
// 2E.
let first_seg_end = if let Some(after_slash) = input.strip_prefix('/') {
// `+1` is the length of the initial slash.
after_slash
.find('/')
.map_or_else(|| input.len(), |pos| pos + 1)
} else {
input.find('/').unwrap_or(input.len())
};
output.extend(input.drain(..first_seg_end));
}
}
output
}
/// Removes the last path segment and the preceding slash if any.
///
/// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.2.4>,
/// step 2C.
fn remove_last_segment_and_preceding_slash(output: &mut String) {
match output.rfind('/') {
Some(slash_pos) => {
output.drain(slash_pos..);
}
None => output.clear(),
}
}
/// Recomposes the components.
///
/// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-5.3>.
fn recompose(
scheme: &str,
authority: Option<&str>,
path: &str,
query: Option<&str>,
fragment: Option<&str>,
) -> String {
let mut result = String::new();
result.push_str(scheme);
result.push(':');
if let Some(authority) = authority {
result.push_str("//");
result.push_str(authority);
}
result.push_str(path);
if let Some(query) = query {
result.push('?');
result.push_str(query);
}
if let Some(fragment) = fragment {
result.push('#');
result.push_str(fragment);
}
result
}