pub const MAX_NESTING_DEPTH: usize = 10_000;
const VOID_ELEMENTS: [&str; 14] = [
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
"track", "wbr",
];
pub fn max_nesting_depth(html: &str) -> usize {
let bytes = html.as_bytes();
let mut depth: isize = 0;
let mut max: isize = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'<' {
i += 1;
continue;
}
let Some(next) = bytes.get(i + 1) else { break };
if bytes[i..].starts_with(b"<!--") {
i = match find_slice(bytes, i + 4, b"-->") {
Some(end) => end + 3,
None => break,
};
continue;
}
if matches!(next, b'!' | b'?') {
i = match find_byte(bytes, i, b'>') {
Some(end) => end + 1,
None => break,
};
continue;
}
let closing = *next == b'/';
let name_start = if closing { i + 2 } else { i + 1 };
let mut name_end = name_start;
while name_end < bytes.len() && bytes[name_end].is_ascii_alphanumeric() {
name_end += 1;
}
if name_end == name_start {
i += 1;
continue;
}
let Some(tag_end) = find_byte(bytes, name_end, b'>') else {
break;
};
let name = html[name_start..name_end].to_ascii_lowercase();
let self_closing = tag_end > 0 && bytes[tag_end - 1] == b'/';
if closing {
depth -= 1;
} else if !self_closing && !VOID_ELEMENTS.contains(&name.as_str()) {
depth += 1;
max = max.max(depth);
}
i = tag_end + 1;
}
max.max(0) as usize
}
pub fn too_deeply_nested(html: &str) -> Option<usize> {
let depth = max_nesting_depth(html);
(depth > MAX_NESTING_DEPTH).then_some(depth)
}
fn find_byte(bytes: &[u8], from: usize, needle: u8) -> Option<usize> {
bytes[from..]
.iter()
.position(|b| *b == needle)
.map(|p| p + from)
}
fn find_slice(bytes: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
if from >= bytes.len() {
return None;
}
bytes[from..]
.windows(needle.len())
.position(|w| w == needle)
.map(|p| p + from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ordinary_documents_are_shallow() {
let html = "<html><body><div><section><article><p>Hi <b>there</b></p>\
</article></section></div></body></html>";
assert!(max_nesting_depth(html) < 10, "{}", max_nesting_depth(html));
assert_eq!(too_deeply_nested(html), None);
}
#[test]
fn void_and_self_closing_tags_do_not_nest() {
let html = "<div><br><img src=x><hr><input><meta charset=utf-8><span/></div>";
assert_eq!(max_nesting_depth(html), 1);
}
#[test]
fn comments_and_doctype_are_ignored() {
let html = "<!DOCTYPE html><!-- <div><div><div> --><p>x</p>";
assert_eq!(max_nesting_depth(html), 1);
}
#[test]
fn depth_is_counted() {
let n = 500;
let html = format!("{}<p>x</p>{}", "<div>".repeat(n), "</div>".repeat(n));
assert_eq!(max_nesting_depth(&html), n + 1);
}
#[test]
fn pathological_nesting_is_refused() {
let n = MAX_NESTING_DEPTH + 1;
let html = format!("{}text{}", "<div>".repeat(n), "</div>".repeat(n));
assert_eq!(too_deeply_nested(&html), Some(n));
}
}