use std::cell::Cell;
use crate::xml::dom::Node;
use crate::xml::dom::storage::Storage as DomStorage;
use super::C14nError;
use super::NodeVisibility;
#[cfg(any(feature = "xmldsig", test))]
use super::scratch::Native;
use super::scratch::{List, Scratch, Text, TextBuilder};
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
pub(crate) struct XmlBaseResolutionBudget {
remaining_bytes: Cell<usize>,
max_bytes: usize,
max_components: usize,
}
impl Default for XmlBaseResolutionBudget {
fn default() -> Self {
Self::with_limits(
crate::hard_limits::XML_BASE_COMPONENT_CEILING,
crate::hard_limits::XML_BASE_RESOLUTION_BYTE_CEILING,
)
}
}
impl XmlBaseResolutionBudget {
pub(crate) fn with_limits(max_components: usize, max_bytes: usize) -> Self {
Self {
remaining_bytes: Cell::new(max_bytes),
max_bytes,
max_components,
}
}
fn check_components(&self, actual: usize) -> Result<(), XmlBaseResolutionError> {
if actual > self.max_components {
return Err(XmlBaseResolutionError::Components {
maximum: self.max_components,
actual,
});
}
Ok(())
}
fn charge_bytes(&self, bytes: usize) -> Result<(), XmlBaseResolutionError> {
let remaining = self.remaining_bytes.get();
let Some(next) = remaining.checked_sub(bytes) else {
self.remaining_bytes.set(0);
return Err(XmlBaseResolutionError::Bytes {
maximum: self.max_bytes,
actual: self
.max_bytes
.saturating_add(bytes.saturating_sub(remaining)),
});
};
self.remaining_bytes.set(next);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub(crate) enum XmlBaseResolutionError {
#[error("XML Base resolution exceeds maximum of {maximum} inherited components: got {actual}")]
Components { maximum: usize, actual: usize },
#[error("XML Base resolution exceeds cumulative maximum of {maximum} bytes: got {actual}")]
Bytes { maximum: usize, actual: usize },
}
#[cfg(test)]
pub(crate) fn compute_effective_xml_base(
start: Node<'_, '_>,
visibility: Option<&dyn NodeVisibility>,
) -> Option<String> {
compute_effective_xml_base_with_budget(start, visibility, &XmlBaseResolutionBudget::default())
.expect("test XML Base fixtures stay within implementation ceilings")
}
#[cfg(test)]
pub(crate) fn compute_effective_xml_base_with_budget(
start: Node<'_, '_>,
visibility: Option<&dyn NodeVisibility>,
budget: &XmlBaseResolutionBudget,
) -> Result<Option<String>, XmlBaseResolutionError> {
compute_effective_xml_base_in(start, visibility, budget, &Native).map_err(native_policy_error)
}
pub(super) fn compute_effective_xml_base_in<S: Scratch, D: DomStorage>(
start: Node<'_, '_, D>,
visibility: Option<&dyn NodeVisibility<D>>,
budget: &XmlBaseResolutionBudget,
scratch: &S,
) -> Result<Option<S::Text>, C14nError> {
let mut bases = scratch.values();
let mut current = Some(start);
while let Some(node) = current {
scratch.charge(1)?;
if node.is_element() {
for attribute in node.attributes() {
scratch.charge(attribute.name().len().saturating_add(1))?;
}
let base = xml_base_value(node);
if let Some(set) = visibility
&& preserves_xml_base_context(node, set)?
{
break;
}
if let Some(base) = base {
let component_count = bases.len().saturating_add(1);
budget
.check_components(component_count)
.map_err(super::serialize::map_xml_base_error)?;
budget
.charge_bytes(base.len())
.map_err(super::serialize::map_xml_base_error)?;
bases.push(base)?;
}
}
current = node.parent();
}
let Some(first) = bases.pop() else {
return Ok(None);
};
budget
.charge_bytes(first.len())
.map_err(super::serialize::map_xml_base_error)?;
let mut effective = scratch.text(&[first])?;
for relative in bases.iter().rev() {
effective = resolve_uri_with_budget_in(effective.view()?, relative, budget, scratch)?;
}
Ok(Some(effective))
}
pub(super) fn resolve_uri_with_budget_in<S: Scratch>(
base: &str,
reference: &str,
budget: &XmlBaseResolutionBudget,
scratch: &S,
) -> Result<S::Text, C14nError> {
let input_bytes = base
.len()
.checked_add(reference.len())
.and_then(|bytes| bytes.checked_add(1))
.ok_or_else(|| {
super::serialize::map_xml_base_error(XmlBaseResolutionError::Bytes {
maximum: budget.max_bytes,
actual: usize::MAX,
})
})?;
budget
.charge_bytes(input_bytes)
.map_err(super::serialize::map_xml_base_error)?;
let resolved = resolve_uri_in(base, reference, scratch)?;
budget
.charge_bytes(resolved.view()?.len())
.map_err(super::serialize::map_xml_base_error)?;
Ok(resolved)
}
#[cfg(any(feature = "xmldsig", test))]
fn native_policy_error(error: C14nError) -> XmlBaseResolutionError {
match error {
C14nError::XmlBaseComponentsTooLarge { max, actual } => {
XmlBaseResolutionError::Components {
maximum: max,
actual,
}
}
C14nError::XmlBaseResolutionTooLarge { max_bytes, actual } => {
XmlBaseResolutionError::Bytes {
maximum: max_bytes,
actual,
}
}
_ => unreachable!("native scratch has no fallible resource domain"),
}
}
#[cfg(feature = "xmldsig")]
pub(crate) fn resolve_uri_from_node_with_budget(
origin: Node<'_, '_>,
reference: &str,
budget: &XmlBaseResolutionBudget,
) -> Result<String, XmlBaseResolutionError> {
resolve_uri_from_node_with_document_base_with_budget(origin, reference, None, budget)
}
#[cfg(feature = "xmldsig")]
pub(crate) fn resolve_uri_from_node_with_document_base_with_budget(
origin: Node<'_, '_>,
reference: &str,
document_base: Option<&str>,
budget: &XmlBaseResolutionBudget,
) -> Result<String, XmlBaseResolutionError> {
resolve_uri_from_node_in(origin, reference, document_base, budget, &Native)
.map_err(native_policy_error)
}
#[cfg(feature = "xmldsig")]
pub(crate) fn resolve_uri_from_node_in<S: Scratch, D: DomStorage>(
origin: Node<'_, '_, D>,
reference: &str,
document_base: Option<&str>,
budget: &XmlBaseResolutionBudget,
scratch: &S,
) -> Result<S::Text, C14nError> {
scratch.charge(reference.len())?;
if has_scheme(reference) {
return resolve_uri_with_budget_in("", reference, budget, scratch);
}
let inherited_base = compute_effective_xml_base_in(origin, None, budget, scratch)?;
match (document_base, inherited_base) {
(Some(document_base), Some(xml_base)) => {
let base =
resolve_uri_with_budget_in(document_base, xml_base.view()?, budget, scratch)?;
resolve_uri_with_budget_in(base.view()?, reference, budget, scratch)
}
(Some(document_base), None) => {
resolve_uri_with_budget_in(document_base, reference, budget, scratch)
}
(None, Some(xml_base)) => {
resolve_uri_with_budget_in(xml_base.view()?, reference, budget, scratch)
}
(None, None) => resolve_uri_with_budget_in("", reference, budget, scratch),
}
}
pub(super) fn preserves_xml_base_context<D: DomStorage>(
node: Node<'_, '_, D>,
visibility: &dyn NodeVisibility<D>,
) -> Result<bool, C14nError> {
Ok(visibility.contains_node(node)?
&& xml_base_value(node).is_some()
&& visibility.contains_attribute(node, Some(XML_NS), "base")?)
}
fn xml_base_value<'a, D: DomStorage>(node: Node<'a, '_, D>) -> Option<&'a str> {
for attr in node.attributes() {
if attr.namespace() == Some(XML_NS) && attr.name() == "base" {
let value = attr.value();
if value.is_empty() {
return None;
}
return Some(value);
}
}
None
}
#[cfg(test)]
pub(crate) fn resolve_uri(base: &str, reference: &str) -> String {
resolve_uri_in(base, reference, &Native).expect("native URI storage is unmetered")
}
fn resolve_uri_in<S: Scratch>(
base: &str,
reference: &str,
scratch: &S,
) -> Result<S::Text, C14nError> {
if reference.is_empty() {
return scratch.text(&[base]);
}
scratch.charge(reference.len().saturating_mul(2))?;
if has_scheme(reference) {
scratch.charge(reference.len())?;
let (absolute, suffix) = split_path_suffix(reference);
scratch.charge(absolute.len().saturating_mul(6))?;
let parts = parse_base(absolute).expect("has_scheme accepted the absolute reference");
let path = remove_dot_segments_in(parts.path, false, scratch)?;
return recompose_in(parts.scheme, parts.authority, path.view()?, suffix, scratch);
}
if reference.starts_with('?') {
scratch.charge(base.len().saturating_mul(2))?;
return scratch.text(&[strip_query_fragment(base), reference]);
}
if reference.starts_with('#') {
scratch.charge(base.len())?;
return scratch.text(&[base.split('#').next().unwrap_or(base), reference]);
}
scratch.charge(base.len().saturating_mul(6))?;
let base_parts = match parse_base(base) {
Some(parts) => parts,
None => {
scratch.charge(reference.len().saturating_mul(2))?;
let (ref_path, ref_suffix) = split_path_suffix(reference);
if let Some((authority, path)) = parse_network_path(ref_path) {
let path = remove_dot_segments_in(path, true, scratch)?;
return scratch.text(&["//", authority, path.view()?, ref_suffix]);
}
scratch.charge(base.len().saturating_mul(2))?;
let (base_path_with_authority, _) = split_path_suffix(base);
let network_base = parse_network_path(base_path_with_authority);
if ref_path.starts_with('/') {
let path = remove_dot_segments_in(ref_path, true, scratch)?;
return match network_base {
Some((authority, _)) => {
scratch.text(&["//", authority, path.view()?, ref_suffix])
}
None => scratch.text(&[path.view()?, ref_suffix]),
};
}
let (base_path, authority) = match network_base {
Some((authority, path)) => (path, Some(authority)),
None => (base_path_with_authority, None),
};
let merged = merge_paths_in(base_path, ref_path, authority.is_some(), scratch)?;
let cleaned = remove_dot_segments_in(merged.view()?, true, scratch)?;
return match authority {
Some(authority) => scratch.text(&["//", authority, cleaned.view()?, ref_suffix]),
None => scratch.text(&[cleaned.view()?, ref_suffix]),
};
}
};
let scheme = base_parts.scheme;
let authority = base_parts.authority;
let base_path = base_parts.path;
scratch.charge(reference.len())?;
let (ref_path, ref_suffix) = split_path_suffix(reference);
if let Some(rest) = ref_path.strip_prefix("//") {
scratch.charge(rest.len().saturating_mul(3))?;
let mut auth_end = rest.len();
for ch in ['/', '?', '#'] {
if let Some(pos) = rest.find(ch)
&& pos < auth_end
{
auth_end = pos;
}
}
let new_authority = &rest[..auth_end];
let new_path = remove_dot_segments_in(&rest[auth_end..], true, scratch)?;
return recompose_in(
scheme,
Some(new_authority),
new_path.view()?,
ref_suffix,
scratch,
);
}
if ref_path.starts_with('/') {
let cleaned = remove_dot_segments_in(ref_path, true, scratch)?;
return recompose_in(scheme, authority, cleaned.view()?, ref_suffix, scratch);
}
scratch.charge(base_path.len().saturating_mul(2))?;
let clean_base_path = strip_query_fragment(base_path);
let merged = merge_paths_in(clean_base_path, ref_path, authority.is_some(), scratch)?;
let cleaned = remove_dot_segments_in(merged.view()?, true, scratch)?;
recompose_in(scheme, authority, cleaned.view()?, ref_suffix, scratch)
}
fn has_scheme(uri: &str) -> bool {
if let Some(colon_pos) = uri.find(':') {
colon_pos > 0
&& uri.as_bytes()[0].is_ascii_alphabetic()
&& uri[..colon_pos]
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.')
} else {
false
}
}
struct BaseParts<'a> {
scheme: &'a str,
authority: Option<&'a str>,
path: &'a str,
}
fn parse_base(base: &str) -> Option<BaseParts<'_>> {
if !has_scheme(base) {
return None;
}
let scheme_end = base.find(':')?;
let scheme = &base[..scheme_end];
let mut rest = &base[scheme_end + 1..];
let mut authority = None;
if rest.starts_with("//") {
rest = &rest[2..];
let mut auth_end = rest.len();
for ch in ['/', '?', '#'] {
if let Some(pos) = rest.find(ch)
&& pos < auth_end
{
auth_end = pos;
}
}
authority = Some(&rest[..auth_end]);
rest = &rest[auth_end..];
}
Some(BaseParts {
scheme,
authority,
path: rest,
})
}
fn parse_network_path(reference: &str) -> Option<(&str, &str)> {
let rest = reference.strip_prefix("//")?;
let authority_end = rest.find('/').unwrap_or(rest.len());
Some((&rest[..authority_end], &rest[authority_end..]))
}
fn recompose_in<S: Scratch>(
scheme: &str,
authority: Option<&str>,
path: &str,
suffix: &str,
scratch: &S,
) -> Result<S::Text, C14nError> {
match authority {
Some(auth) => scratch.text(&[scheme, "://", auth, path, suffix]),
None => scratch.text(&[scheme, ":", path, suffix]),
}
}
fn split_path_suffix(reference: &str) -> (&str, &str) {
let split_at = reference
.char_indices()
.skip(1)
.find_map(|(index, ch)| matches!(ch, '?' | '#').then_some(index))
.unwrap_or(reference.len());
(&reference[..split_at], &reference[split_at..])
}
fn strip_query_fragment(s: &str) -> &str {
let end = s
.find('?')
.unwrap_or(s.len())
.min(s.find('#').unwrap_or(s.len()));
&s[..end]
}
#[cfg(test)]
fn merge_paths(base_path: &str, reference: &str, base_has_authority: bool) -> String {
merge_paths_in(base_path, reference, base_has_authority, &Native).expect("native storage")
}
fn merge_paths_in<S: Scratch>(
base_path: &str,
reference: &str,
base_has_authority: bool,
scratch: &S,
) -> Result<S::Text, C14nError> {
if base_has_authority && base_path.is_empty() {
scratch.text(&["/", reference])
} else {
scratch.charge(base_path.len())?;
match base_path.rfind('/') {
Some(pos) => scratch.text(&[&base_path[..=pos], reference]),
None => scratch.text(&[reference]),
}
}
}
#[cfg(test)]
mod merge_tests {
use super::*;
#[test]
fn non_hierarchical_base_does_not_add_slash() {
assert_eq!(merge_paths("foo:bar", "baz", false), "baz");
}
}
#[cfg(test)]
fn remove_dot_segments(path: &str) -> String {
remove_dot_segments_in(path, true, &Native).expect("native storage")
}
fn remove_dot_segments_in<S: Scratch>(
path: &str,
preserve_unmatched_parents: bool,
scratch: &S,
) -> Result<S::Text, C14nError> {
scratch.charge(path.len())?;
let is_absolute = path.starts_with('/');
let mut segments = scratch.values();
for segment in path.split('/') {
scratch.charge(4)?;
match segment {
"." => {
}
".." => {
let root_segments = usize::from(is_absolute);
let can_pop =
segments.len() > root_segments && !matches!(segments.last(), Some(&".."));
if can_pop {
segments.pop();
} else if !is_absolute && preserve_unmatched_parents {
segments.push("..")?;
}
}
s => segments.push(s)?,
}
}
scratch.charge(segments.len())?;
let capacity = segments
.iter()
.try_fold(0usize, |n, segment| {
n.checked_add(segment.len()).and_then(|n| n.checked_add(1))
})
.ok_or(C14nError::OutputLimitExceeded {
max_bytes: usize::MAX,
})?;
let mut result = scratch.builder();
result.reserve(capacity)?;
for (index, segment) in segments.iter().enumerate() {
if index != 0 {
result.append("/")?;
}
result.append(segment)?;
}
if (path.ends_with("/.") || path.ends_with("/..")) && result.last_byte() != Some(b'/') {
result.append("/")?;
}
result.finish()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::xml::dom::Document;
#[test]
fn resolve_absolute_reference() {
assert_eq!(
resolve_uri("http://a.com/b", "http://other.com/c"),
"http://other.com/c"
);
}
#[test]
fn resolve_absolute_reference_removes_dot_segments() {
assert_eq!(
resolve_uri(
"https://base.example/ignored/",
"https://example.test/a/../data.bin?version=1#payload"
),
"https://example.test/data.bin?version=1#payload"
);
}
#[test]
fn resolve_absolute_reference_consumes_interior_empty_segment() {
assert_eq!(
resolve_uri(
"https://base.example/ignored/",
"https://example.test/a//../b"
),
"https://example.test/a/b"
);
}
#[test]
fn resolve_empty_reference() {
assert_eq!(resolve_uri("http://a.com/b/c", ""), "http://a.com/b/c");
}
#[test]
fn resolve_authority_override() {
assert_eq!(
resolve_uri("http://a.com/b", "//other.com/c"),
"http://other.com/c"
);
}
#[test]
fn resolve_absolute_path() {
assert_eq!(resolve_uri("http://a.com/b/c", "/d/e"), "http://a.com/d/e");
}
#[test]
fn resolve_relative_path_simple() {
assert_eq!(
resolve_uri("http://example.com/a/b/", "c/d"),
"http://example.com/a/b/c/d"
);
}
#[test]
fn resolve_relative_path_sibling() {
assert_eq!(
resolve_uri("http://example.com/a/b", "c"),
"http://example.com/a/c"
);
}
#[test]
fn resolve_relative_path_parent() {
assert_eq!(
resolve_uri("http://example.com/a/b/c", "../d"),
"http://example.com/a/d"
);
}
#[test]
fn resolve_relative_path_double_parent() {
assert_eq!(
resolve_uri("http://example.com/a/b/c/", "../../d"),
"http://example.com/a/d"
);
}
#[test]
fn resolve_root_base_with_relative() {
assert_eq!(
resolve_uri("http://example.com/", "sub/"),
"http://example.com/sub/"
);
}
#[test]
fn resolve_dot_current_dir() {
assert_eq!(
resolve_uri("http://example.com/a/b/", "./c"),
"http://example.com/a/b/c"
);
}
#[test]
fn resolve_schemeless_base_merges_paths() {
assert_eq!(resolve_uri("sub/dir/", "file.xml"), "sub/dir/file.xml");
assert_eq!(resolve_uri("a/b/", "../c"), "a/c");
assert_eq!(resolve_uri("a/b", "c"), "a/c");
}
#[test]
fn resolve_absolute_path_normalizes_against_schemeless_base() {
assert_eq!(resolve_uri("a/b", "/x/../data.bin"), "/data.bin");
}
#[test]
fn resolve_network_path_normalizes_against_schemeless_base() {
assert_eq!(
resolve_uri("a/b", "//cdn.example/x/../data.bin?version=1"),
"//cdn.example/data.bin?version=1"
);
}
#[test]
fn resolve_against_network_path_base_preserves_authority() {
assert_eq!(
resolve_uri("//cdn.example/a/b/", "/x/../data.bin?version=1"),
"//cdn.example/data.bin?version=1"
);
assert_eq!(
resolve_uri("//cdn.example/a/b/", "../data.bin"),
"//cdn.example/a/data.bin"
);
assert_eq!(
resolve_uri("//cdn.example/a/b?old#fragment", "?new"),
"//cdn.example/a/b?new"
);
assert_eq!(
resolve_uri("//cdn.example/a/b?old#fragment", "#new"),
"//cdn.example/a/b?old#new"
);
assert_eq!(
resolve_uri("//cdn.example/a/b/", "//other.example/x/../data.bin"),
"//other.example/data.bin"
);
}
#[test]
fn resolve_pathless_schemeless_base_preserves_relative_reference() {
assert_eq!(resolve_uri("?old", "data.bin"), "data.bin");
}
#[test]
fn resolve_query_and_fragment_against_schemeless_base() {
assert_eq!(resolve_uri("a/b?old#frag", "?new"), "a/b?new");
assert_eq!(resolve_uri("a/b?old#frag", "#new"), "a/b?old#new");
}
#[test]
fn resolve_urn_reference() {
assert_eq!(
resolve_uri("http://example.com/a", "urn:foo:bar"),
"urn:foo:bar"
);
}
#[test]
fn resolve_rootless_absolute_uri_removes_leading_dot_segments() {
assert_eq!(
resolve_uri("https://example.test/base", "urn:../payload?version=1"),
"urn:payload?version=1"
);
assert_eq!(
resolve_uri("https://example.test/base", "urn:./payload"),
"urn:payload"
);
}
#[test]
fn resolve_parent_beyond_root() {
assert_eq!(
resolve_uri("http://example.com/a", "../../b"),
"http://example.com/b"
);
}
#[test]
fn resolve_file_scheme_no_authority() {
assert_eq!(
resolve_uri("file:///home/user/doc.xml", "sub/file.xml"),
"file:///home/user/sub/file.xml"
);
}
#[test]
fn resolve_base_with_query_fragment() {
assert_eq!(
resolve_uri("http://example.com/a/b?q=1#f", "c"),
"http://example.com/a/c"
);
}
#[test]
fn resolve_reference_with_query() {
assert_eq!(
resolve_uri("http://example.com/a/b", "c?x=1"),
"http://example.com/a/c?x=1"
);
}
#[test]
fn resolve_unicode_reference_with_query_uses_utf8_boundaries() {
assert_eq!(
resolve_uri("https://example.test/base/", "é?x"),
"https://example.test/base/é?x"
);
}
#[test]
fn resolve_reference_with_fragment() {
assert_eq!(
resolve_uri("http://example.com/a/b", "c#frag"),
"http://example.com/a/c#frag"
);
}
#[test]
fn resolve_reference_with_query_and_fragment() {
assert_eq!(
resolve_uri("http://example.com/a/b", "c?q=1#f"),
"http://example.com/a/c?q=1#f"
);
}
#[test]
fn resolve_absolute_path_with_query() {
assert_eq!(
resolve_uri("http://example.com/a", "/b/c?q"),
"http://example.com/b/c?q"
);
}
#[test]
fn empty_xml_base_treated_as_absent() {
let xml = r#"<root xml:base=""><child xml:base="http://ex.com/"/></root>"#;
let doc = Document::parse(xml).unwrap();
let root = doc.root_element();
assert_eq!(xml_base_value(root), None);
let child = root.first_element_child().unwrap();
assert_eq!(xml_base_value(child), Some("http://ex.com/"));
}
#[test]
fn remove_dots_simple() {
assert_eq!(remove_dot_segments("/a/b/c"), "/a/b/c");
}
#[test]
fn remove_single_dot() {
assert_eq!(remove_dot_segments("/a/./b"), "/a/b");
}
#[test]
fn remove_double_dot() {
assert_eq!(remove_dot_segments("/a/b/../c"), "/a/c");
}
#[test]
fn remove_dots_trailing_slash() {
assert_eq!(remove_dot_segments("/a/b/.."), "/a/");
}
#[test]
fn remove_dots_at_root() {
assert_eq!(remove_dot_segments("/../a"), "/a");
}
#[test]
fn remove_dots_relative_leading_dotdot() {
assert_eq!(remove_dot_segments("../../a"), "../../a");
assert_eq!(remove_dot_segments("foo/../../bar"), "../bar");
}
#[test]
fn scheme_detection() {
assert!(has_scheme("http://example.com"));
assert!(has_scheme("https://x"));
assert!(has_scheme("urn:foo:bar"));
assert!(has_scheme("ftp://a"));
assert!(!has_scheme("/a/b"));
assert!(!has_scheme("a/b"));
assert!(!has_scheme(""));
assert!(!has_scheme("://bad"));
}
#[test]
fn effective_base_single_ancestor() {
let xml = r#"<root xml:base="http://example.com/"><child/></root>"#;
let doc = Document::parse(xml).unwrap();
let child = doc.root_element().first_element_child().unwrap();
let base = compute_effective_xml_base(child.parent().unwrap(), None);
assert_eq!(base.as_deref(), Some("http://example.com/"));
}
#[test]
fn effective_base_chain_resolved() {
let xml = r#"<a xml:base="http://example.com/"><b xml:base="sub/"><c/></b></a>"#;
let doc = Document::parse(xml).unwrap();
let a = doc.root_element();
let b = a.first_element_child().unwrap();
let c = b.first_element_child().unwrap();
let base = compute_effective_xml_base(c.parent().unwrap(), None);
assert_eq!(base.as_deref(), Some("http://example.com/sub/"));
}
#[test]
fn effective_base_three_levels() {
let xml =
r#"<a xml:base="http://ex.com/"><b xml:base="x/"><c xml:base="y/"><d/></c></b></a>"#;
let doc = Document::parse(xml).unwrap();
let a = doc.root_element();
let b = a.first_element_child().unwrap();
let c = b.first_element_child().unwrap();
let d = c.first_element_child().unwrap();
let base = compute_effective_xml_base(d.parent().unwrap(), None);
assert_eq!(base.as_deref(), Some("http://ex.com/x/y/"));
}
#[test]
fn effective_base_none_when_no_xml_base() {
let xml = r#"<root><child/></root>"#;
let doc = Document::parse(xml).unwrap();
let child = doc.root_element().first_element_child().unwrap();
assert_eq!(
compute_effective_xml_base(child.parent().unwrap(), None),
None
);
}
#[test]
fn effective_base_with_dotdot() {
let xml = r#"<a xml:base="http://example.com/a/b/"><b xml:base="../c/"><d/></b></a>"#;
let doc = Document::parse(xml).unwrap();
let a = doc.root_element();
let b = a.first_element_child().unwrap();
let d = b.first_element_child().unwrap();
let base = compute_effective_xml_base(d.parent().unwrap(), None);
assert_eq!(base.as_deref(), Some("http://example.com/a/c/"));
}
}