use crate::ConversionError;
pub const DEFAULT_MAX_DEPTH: usize = 512;
pub fn max_depth() -> usize {
static CAP: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CAP.get_or_init(|| {
docling_core::env::parse::<usize>("DOCLING_RS_MAX_XML_DEPTH")
.filter(|&n| n > 0)
.unwrap_or(DEFAULT_MAX_DEPTH)
})
}
pub fn nesting_depth(xml: &str, limit: usize) -> Option<usize> {
let b = xml.as_bytes();
let n = b.len();
let (mut depth, mut max) = (0usize, 0usize);
let mut i = 0;
let find = |from: usize, pat: &[u8]| -> usize {
let mut j = from;
while j + pat.len() <= n {
if &b[j..j + pat.len()] == pat {
return j + pat.len();
}
j += 1;
}
n
};
while i < n {
if b[i] != b'<' {
i += 1;
continue;
}
if b[i..].starts_with(b"<!--") {
i = find(i + 4, b"-->");
} else if b[i..].starts_with(b"<![CDATA[") {
i = find(i + 9, b"]]>");
} else if b[i..].starts_with(b"<?") {
i = find(i + 2, b"?>");
} else if b[i..].starts_with(b"<!") {
let mut j = i + 2;
let mut bracket = 0usize;
while j < n {
match b[j] {
b'[' => bracket += 1,
b']' => bracket = bracket.saturating_sub(1),
b'>' if bracket == 0 => break,
_ => {}
}
j += 1;
}
i = j + 1;
} else if b[i..].starts_with(b"</") {
depth = depth.saturating_sub(1);
i = find(i + 2, b">");
} else {
let mut j = i + 1;
let mut quote: Option<u8> = None;
let mut prev = b'<';
while j < n {
let c = b[j];
match quote {
Some(q) if c == q => quote = None,
Some(_) => {}
None if c == b'"' || c == b'\'' => quote = Some(c),
None if c == b'>' => break,
None => {}
}
prev = c;
j += 1;
}
if prev != b'/' {
depth += 1;
if depth > limit {
return None;
}
max = max.max(depth);
}
i = j + 1;
}
}
Some(max)
}
pub fn check(xml: &str, what: &str) -> Result<(), ConversionError> {
let limit = max_depth();
match nesting_depth(xml, limit) {
Some(_) => Ok(()),
None => Err(ConversionError::Parse(format!(
"{what}: XML nested deeper than {limit} elements (DOCLING_RS_MAX_XML_DEPTH)"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_elements_and_skips_non_element_markup() {
let xml = r#"<?xml version="1.0"?>
<!DOCTYPE a [ <!ENTITY e "<b><c><d>"> ]>
<!-- <x><y><z> -->
<a x="<q><r>" y='<s>'><![CDATA[<t><u><v>]]><b/><c><d>text</d></c></a>"#;
assert_eq!(nesting_depth(xml, 100), Some(3));
assert_eq!(nesting_depth("<a><b><c/></b></a>", 100), Some(2));
assert_eq!(nesting_depth("", 100), Some(0));
}
#[test]
fn stops_at_the_limit() {
let deep: String = "<t>".repeat(600) + &"</t>".repeat(600);
assert_eq!(nesting_depth(&deep, 512), None);
assert_eq!(nesting_depth(&deep, 600), Some(600));
assert!(check(&deep, "test").is_err());
assert!(check("<a><b/></a>", "test").is_ok());
}
}