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 @@
'',
'1x?>', // invalid processing instruction target
'1x?>',
+ '', // 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> {