use url::Url;
use broadcast_common::url_ref;
const SYNTHETIC_SCHEME: &str = "transmux-relative";
const SYNTHETIC_BASE: &str = "transmux-relative:///";
pub fn first_forbidden_char(s: &str) -> Option<char> {
url_ref::first_forbidden_char(s).or_else(|| s.chars().find(|c| c.is_whitespace()))
}
pub fn resolve(base: Option<&Url>, reference: &str) -> Option<String> {
resolve_chain(base, &[], reference)
}
pub fn resolve_chain(base: Option<&Url>, chain: &[String], reference: &str) -> Option<String> {
let entries: Vec<&str> = chain
.iter()
.map(|b| b.trim())
.filter(|b| !b.is_empty())
.collect();
if first_forbidden_char(reference).is_some()
|| entries.iter().any(|b| first_forbidden_char(b).is_some())
|| entries
.iter()
.chain(core::iter::once(&reference))
.any(|r| names_synthetic_scheme(r))
{
return None;
}
let mut current = match base {
Some(b) => b.clone(),
None => Url::parse(SYNTHETIC_BASE).ok()?,
};
let mut absolute_path = false;
for entry in entries.iter().copied().chain(core::iter::once(reference)) {
if is_absolute_path_reference(entry) {
absolute_path = true;
}
current = url_ref::resolve(¤t, entry).ok()?;
}
Some(render(¤t, absolute_path))
}
fn is_absolute_path_reference(r: &str) -> bool {
r.starts_with('/') && !r.starts_with("//")
}
fn names_synthetic_scheme(r: &str) -> bool {
r.len() > SYNTHETIC_SCHEME.len()
&& r.as_bytes()[..SYNTHETIC_SCHEME.len()].eq_ignore_ascii_case(SYNTHETIC_SCHEME.as_bytes())
&& r.as_bytes()[SYNTHETIC_SCHEME.len()] == b':'
}
pub fn render(url: &Url, absolute_path: bool) -> String {
let text = url.as_str();
let Some(rest) = text
.strip_prefix(SYNTHETIC_SCHEME)
.and_then(|r| r.strip_prefix(':'))
else {
return text.to_owned();
};
let Some(after_slashes) = rest.strip_prefix("//") else {
return rest.to_owned();
};
if !after_slashes.starts_with('/') {
return rest.to_owned(); }
if absolute_path {
after_slashes.to_owned()
} else {
after_slashes[1..].to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn synthetic_base_parses() {
assert!(Url::parse(SYNTHETIC_BASE).is_ok());
}
#[test]
fn render_cases() {
let r = |s: &str, abs: bool| render(&Url::parse(s).unwrap(), abs);
assert_eq!(
r("transmux-relative:///a/b.m4s?q=1#f", false),
"a/b.m4s?q=1#f"
);
assert_eq!(r("transmux-relative:///x/y", true), "/x/y");
assert_eq!(r("transmux-relative://h/x", false), "//h/x");
assert_eq!(r("https://h/a/b", false), "https://h/a/b");
}
}