fix(domparser): discard trees recovered after prologue errors

This commit is contained in:
ldm0
2026-09-09 06:44:47 +08:00
parent 8f3d04077f
commit 2a1fbf2cbc
2 changed files with 82 additions and 1 deletions
+53 -1
View File
@@ -37,6 +37,13 @@ impl XmlPrepassError {
}
}
fn before_document_element(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
discard_partial_tree: true,
}
}
fn namespace(detail: impl Into<String>) -> Self {
Self {
detail: detail.into(),
@@ -159,10 +166,12 @@ fn xml_element_stack_error(xml: &str) -> Option<XmlPrepassError> {
let mut pending_declared_prefixes = Vec::new();
let mut open_elements = Vec::new();
let mut active_prefixes = HashMap::from([("xml".to_owned(), 1usize)]);
let mut saw_document_element = false;
for token in XmlTokenizer::from(xml) {
match token {
Ok(XmlTokenizerToken::ElementStart { prefix, local, .. }) => {
saw_document_element = true;
let prefix = prefix.as_str();
if !prefix.is_empty() {
pending_used_prefixes.push(prefix.to_owned());
@@ -243,7 +252,17 @@ fn xml_element_stack_error(xml: &str) -> Option<XmlPrepassError> {
}
},
Ok(_) => {}
Err(error) => return Some(XmlPrepassError::tree(error.to_string())),
Err(error) => {
let detail = error.to_string();
return Some(if saw_document_element {
XmlPrepassError::tree(detail)
} else {
// xml5ever can recover a malformed prologue or doctype and
// then expose nodes parsed after the error. DOMParser error
// documents must not retain that recovered document tree.
XmlPrepassError::before_document_element(detail)
});
}
}
}
@@ -801,6 +820,39 @@ mod tests {
);
}
#[test]
fn xml_parser_discards_recovered_tree_after_doctype_error() {
let url = Url::parse("https://example.test/doctype.xml").unwrap();
let invalid = XmlParser.parse(
url.clone(),
concat!(
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\">",
"<html><div id=\"test\"/></html>"
)
.to_owned(),
);
assert!(!invalid.parse_errors().is_empty());
assert!(
invalid
.child_ids(invalid.document_node_id())
.all(|child| !invalid.node(child).is_some_and(Node::is_element))
);
for system_id in ["", "x"] {
let valid = XmlParser.parse(
url.clone(),
format!(
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"{system_id}\"><html/>"
),
);
assert!(
valid.parse_errors().is_empty(),
"system ID `{system_id}` should be accepted: {:?}",
valid.parse_errors()
);
}
}
#[test]
fn xml_parser_preserves_namespace_declarations_as_dom_attributes() {
let dom = XmlParser.parse(
@@ -1764,6 +1764,35 @@ fn dom_parser_xml_errors_preserve_the_partial_document_root() {
assert_eq!(result, "catalog|1|true|http://www.w3.org/1999/xhtml|item|2");
}
#[test]
fn dom_parser_rejects_public_doctype_without_system_literal() {
let mut vm = new_storage_test_vm("https://dom-parser-doctype.test/");
let result = vm
.eval(
r#"
(() => {
const parser = new DOMParser();
const prefix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"';
const suffix = '><html><div id="test"/></html>';
const invalid = parser.parseFromString(prefix + suffix, 'application/xhtml+xml');
const emptySystemId = parser.parseFromString(prefix + ' ""' + suffix, 'application/xhtml+xml');
const quotedSystemId = parser.parseFromString(prefix + ' "x"' + suffix, 'application/xhtml+xml');
return [
invalid.getElementById('test') === null,
invalid.getElementsByTagName('parsererror').length === 1,
emptySystemId.getElementById('test') !== null,
quotedSystemId.getElementById('test') !== null
].join('|');
})()
"#,
)
.expect("DOMParser doctype system ID validation should evaluate");
assert_eq!(result, "true|true|true|true");
}
#[test]
fn xhtml_element_interface_survives_move_through_xml_document() {
let mut vm = new_storage_test_vm("https://xhtml-xml-document-move.test/");