diff --git a/src/browser/frame/parse.zig b/src/browser/frame/parse.zig index c4251e3a8..946fb0978 100644 --- a/src/browser/frame/parse.zig +++ b/src/browser/frame/parse.zig @@ -123,13 +123,5 @@ pub fn xmlDocument(frame: *Frame, xml: []const u8) !?*Document.XMLDocument { if (parser.err != null or parser.xml_error or doc_node.firstChild() == null) { return null; } - - // If first node is a `ProcessingInstruction` (e.g. the - // declaration), skip it. - const first_child = doc_node.firstChild().?; - if (first_child.getNodeType() == 7) { - _ = try doc_node.removeChild(first_child, frame); - } - return doc; } diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 759f55f30..2a67c0062 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -521,6 +521,11 @@ '�', '', // invalid processing instruction target '', + '', // only 1.x versions are accepted + '', + '', + '', + '', // version is required ]) { testing.expectEqual(bad + ' -> error', bad + (isError(p.parseFromString(bad, 'text/xml')) ? ' -> error' : ' -> ok')); } @@ -529,6 +534,8 @@ for (const good of [ '', '\n\n', + "", + '', '\uFEFF', ']]>', '', @@ -548,6 +555,12 @@ testing.expectEqual('svg', svg.doctype.name); testing.expectEqual('-//W3C//DTD SVG 1.1//EN', svg.doctype.publicId); testing.expectEqual('svg', svg.documentElement.localName); + + // the XML declaration creates no node, a leading processing instruction does + const decl = p.parseFromString('', 'text/xml'); + testing.expectEqual(2, decl.childNodes.length); + testing.expectEqual('xml-stylesheet', decl.firstChild.target); + testing.expectEqual('xml-stylesheet', p.parseFromString('', 'text/xml').firstChild.target); } diff --git a/src/rust/html5ever/lib.rs b/src/rust/html5ever/lib.rs index ef6209495..a0ee8df1c 100644 --- a/src/rust/html5ever/lib.rs +++ b/src/rust/html5ever/lib.rs @@ -874,6 +874,17 @@ impl<'arena> xml5ever::tokenizer::TokenSink for UnclosedTagSink<'arena> { .parse_error(std::borrow::Cow::Borrowed("Unclosed element at EOF")); } } + // The XML declaration isn't a processing instruction, so no node + // is created for it. xml5ever doesn't validate it. + Token::ProcessingInstruction(pi) if &*pi.target == "xml" => { + if !is_valid_xml_declaration(&pi.data) { + use xml5ever::tree_builder::TreeSink; + self.tb + .sink + .parse_error(std::borrow::Cow::Borrowed("Invalid XML declaration")); + } + return xml5ever::tokenizer::ProcessResult::Continue; + } _ => {} } self.tb.process_token(token) @@ -884,6 +895,30 @@ impl<'arena> xml5ever::tokenizer::TokenSink for UnclosedTagSink<'arena> { } } +// The declaration must start with `version="1.x"`: browsers accept any 1.x +// (XML 1.0 5th edition's VersionNum is `1.[0-9]+`) and reject everything else. +fn is_valid_xml_declaration(data: &str) -> bool { + fn trim(s: &str) -> &str { + s.trim_start_matches([' ', '\t', '\r', '\n']) + } + let Some(rest) = trim(data).strip_prefix("version").map(trim) else { + return false; + }; + let Some(rest) = rest.strip_prefix('=').map(trim) else { + return false; + }; + let Some(quote) = rest.chars().next().filter(|c| *c == '"' || *c == '\'') else { + return false; + }; + let Some((version, _)) = rest[1..].split_once(quote) else { + return false; + }; + match version.strip_prefix("1.") { + Some(minor) => !minor.is_empty() && minor.bytes().all(|b| b.is_ascii_digit()), + None => false, + } +} + // xml5ever::driver::XmlParser, minus the tree-builder-typed tokenizer so the // UnclosedTagSink can sit in between. struct XmlDocumentParser<'arena> {