//! 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: &RiReferenceStr, ) -> (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 . pub(super) fn resolve( reference: &RiReferenceStr, base: &RiAbsoluteStr, ) -> RiString { 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::::try_from(s).expect("resolution result must be a valid IRI") } /// Merges the two paths. /// /// See . 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 . 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 , /// 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 . 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 }