Merge pull request #515 from mnaza/fix/domparser-xml-504

fix(js): surface a parsererror for malformed XML in DOMParser
This commit is contained in:
SGavrl
2026-08-07 16:52:28 +00:00
committed by GitHub
2 changed files with 88 additions and 1 deletions

View File

@@ -25,7 +25,7 @@
'_isSpecialScheme', '_applyDocQueryEncoding', '_anchorBase',
'_elemHrefURL', '_setElemHrefPart', '_pad', '_daysInMonth',
'_isoWeek1Monday', '_inputParseNumber', '_inputFormatNumber',
'_htmlAttrName', '_convertNodes', '_parseHTMLFragment', '_elementClassFor', '_wrap', '_wrapEl',
'_htmlAttrName', '_convertNodes', '_parseHTMLFragment', '_xmlWellFormed', '_elementClassFor', '_wrap', '_wrapEl',
'_resolveUrl', '_registerIframe', '_base64ToUint8Array',
'_bodyToUint8Array', '_arrayBufferFromBytes',
'_installWasmStreamingFallback', '_urlParseOp', '_urlSetOp',
@@ -5472,6 +5472,56 @@ if (typeof URLSearchParams === "undefined") globalThis.URLSearchParams = class U
// input into a detached `<html>` element and wrap it so the common Document
// API surface (body / head / documentElement / querySelector* / getElementById /
// getElementsByTagName / getElementsByClassName / title / cloneNode) works.
// Conservative XML well-formedness check. obscura has no XML parser, so this
// only decides whether to surface a <parsererror> (it does not build an XML
// tree). It flags clear structural errors — mismatched or unclosed tags,
// multiple/no root elements, unterminated comment/CDATA/PI — and defaults to
// "well-formed" whenever the scan is ambiguous, so valid XML is never falsely
// flagged. Quoted attribute regions, comments, CDATA, PIs and the doctype are
// skipped; a literal '<' in text (invalid in XML) reads as a bad tag.
function _xmlWellFormed(src) {
const s = String(src);
const stack = [];
let rootsClosed = 0; // top-level elements fully closed (or self-closed)
let i = 0;
const n = s.length;
while (i < n) {
const lt = s.indexOf('<', i);
if (lt === -1) break;
i = lt;
if (s.startsWith('<!--', i)) { const e = s.indexOf('-->', i + 4); if (e === -1) return false; i = e + 3; continue; }
if (s.startsWith('<![CDATA[', i)) { const e = s.indexOf(']]>', i + 9); if (e === -1) return false; i = e + 3; continue; }
if (s.startsWith('<?', i)) { const e = s.indexOf('?>', i + 2); if (e === -1) return false; i = e + 2; continue; }
if (s.startsWith('<!', i)) { const e = s.indexOf('>', i + 2); if (e === -1) return false; i = e + 1; continue; }
// A start/end/self-closing tag: find its '>' while skipping quoted regions.
let j = i + 1, quote = null;
while (j < n) {
const c = s[j];
if (quote) { if (c === quote) quote = null; }
else if (c === '"' || c === "'") quote = c;
else if (c === '>') break;
j++;
}
if (j >= n) return false; // unterminated tag
const inner = s.slice(i + 1, j).trim();
i = j + 1;
if (!inner) return false;
if (inner[0] === '/') {
const name = inner.slice(1).trim().split(/\s/)[0];
if (stack.length === 0 || stack[stack.length - 1] !== name) return false;
stack.pop();
if (stack.length === 0) rootsClosed++;
} else if (inner[inner.length - 1] === '/') {
if (stack.length === 0) rootsClosed++;
} else {
const name = inner.split(/\s/)[0];
if (!name) return false;
stack.push(name);
}
}
return stack.length === 0 && rootsClosed === 1;
}
globalThis.DOMParser = class DOMParser {
parseFromString(source, mimeType) {
const html = String(source ?? "");
@@ -5483,6 +5533,15 @@ globalThis.DOMParser = class DOMParser {
// children, which is what callers want.
try { root.innerHTML = html; } catch (e) { /* leave empty on parse error */ }
// For XML mime types, surface a <parsererror> on clearly-malformed input so
// error-detection code (doc.querySelector('parsererror')) works, matching
// Chrome. obscura has no XML parser, so the tree stays HTML-parsed.
if (isXml && !_xmlWellFormed(html)) {
try {
root.innerHTML = '<parsererror xmlns="http://www.w3.org/1999/xhtml">This page contains the following errors:<div>error while parsing XML</div></parsererror>';
} catch (e) { /* ignore */ }
}
// Helper: depth-first walk to find an element by predicate.
const walk = (node, pred) => {
if (!node) return null;

View File

@@ -1712,6 +1712,34 @@ mod tests {
);
}
#[test]
fn dom_parser_flags_malformed_xml_with_parsererror() {
let mut rt = setup_runtime("<html><body></body></html>");
let has_err = rt
.evaluate("(function(){var d=new DOMParser().parseFromString('<a><b></a>','application/xml'); return d.querySelector('parsererror') ? true : false;})()")
.unwrap();
assert_eq!(has_err, serde_json::json!(true));
}
#[test]
fn dom_parser_accepts_well_formed_xml() {
let mut rt = setup_runtime("<html><body></body></html>");
let ok = rt
.evaluate("(function(){var d=new DOMParser().parseFromString('<root><child>x</child></root>','application/xml'); return d.querySelector('parsererror') ? 'ERR' : 'OK';})()")
.unwrap();
assert_eq!(ok, serde_json::json!("OK"));
}
#[test]
fn dom_parser_html_never_gets_parsererror() {
// HTML parsing is tolerant and must never synthesize a parsererror.
let mut rt = setup_runtime("<html><body></body></html>");
let ok = rt
.evaluate("(function(){var d=new DOMParser().parseFromString('<div><p>hi</a>','text/html'); return d.querySelector('parsererror') ? 'ERR' : 'OK';})()")
.unwrap();
assert_eq!(ok, serde_json::json!("OK"));
}
#[test]
fn test_document_title() {
let mut rt = setup_runtime("<html><head><title>Test</title></head><body></body></html>");