fix(xml): validate numeric references using the XML Char production

Validate XML numeric character references using the XML Char production rather than HTML recovery rules. Keep carriage returns and C1/noncharacter values which XML permits, and reject forbidden controls/surrogates. Parser regressions cover decimal and hexadecimal references and retain strict failures.
This commit is contained in:
ldm0
2026-10-10 03:44:51 +08:00
committed by Donough Liu
parent 658bc8fcb4
commit 2c8f19277b
3 changed files with 82 additions and 16 deletions
+69
View File
@@ -601,6 +601,75 @@ mod tests {
use moli_dom::native::{DomHost, NativeDom, NativeNodeId, Node, NodeType};
use url::Url;
#[test]
fn xml_numeric_references_follow_xml_chars_without_html_replacements() {
for unit in [
0x9, 0xa, 0xd, 0x20, 0x7f, 0x80, 0x85, 0x91, 0x9f, 0xd7ff, 0xe000, 0xfdd0, 0xfdef,
0xfffd, 0x10000, 0x1fffe, 0x1ffff, 0x10ffff,
] {
let expected = char::from_u32(unit).unwrap().to_string();
for reference in [format!("&#{unit};"), format!("&#x{unit:X};")] {
for quote in ['\'', '"'] {
let source = format!(
"<root xmlns:p='urn:test' p:v={quote}{reference}{quote}>{reference}</root>"
);
let dom = XmlParser.parse(
Url::parse("https://xml.test/character-reference.xml").unwrap(),
source.clone(),
);
assert!(
dom.parse_errors().is_empty(),
"{source}: {:?}",
dom.parse_errors()
);
let root = dom.document_element_node_id().unwrap();
let element = dom.node(root).and_then(Node::as_element).unwrap();
let attribute = element
.attributes()
.iter()
.find(|attribute| attribute.namespace() == "urn:test")
.unwrap();
assert_eq!(attribute.value(), expected, "{source}");
assert_eq!(
dom.text_content(root).as_deref(),
Some(expected.as_str()),
"{source}"
);
}
}
}
}
#[test]
fn xml_numeric_references_reject_values_outside_xml_char() {
for value in [
"0",
"1",
"8",
"11",
"12",
"14",
"31",
"55296",
"57343",
"65534",
"65535",
"1114112",
"999999999999999999999999999999999999",
] {
for source in [
format!("<root v='&#{value};'/>"),
format!("<root>&#{value};</root>"),
] {
let dom = XmlParser.parse(
Url::parse("https://xml.test/invalid-character-reference.xml").unwrap(),
source.clone(),
);
assert!(!dom.parse_errors().is_empty(), "{source}");
}
}
}
#[test]
fn xml_style_children_finish_at_close() {
for namespace in ["http://www.w3.org/1999/xhtml", "http://www.w3.org/2000/svg"] {
+2
View File
@@ -4,6 +4,8 @@ This directory contains the source of the crates.io `xml5ever` 0.39.0 release.
It is patched locally because Moli needs the tokenizer to preserve CDATA as a
distinct token, the XML tree builder to surface CDATA nodes to its sink, and
namespace declarations to remain available as DOM attributes after binding.
Numeric character references use the XML 1.0 Char production, preserving
referenced carriage returns and C1 values without HTML's Windows-1252 mapping.
The upstream source is licensed under MIT or Apache-2.0; see `LICENSE-MIT` and
`LICENSE-APACHE` in this directory.
+11 -16
View File
@@ -237,24 +237,19 @@ impl CharRefTokenizer {
}
fn finish_numeric<Sink: TokenSink>(&mut self, tokenizer: &XmlTokenizer<Sink>) -> Status {
fn conv(n: u32) -> char {
from_u32(n).expect("invalid char missed by error handling cases")
}
let (c, error) = match self.num {
n if (n > 0x10FFFF) || self.num_too_big => ('\u{fffd}', true),
0x00 | 0xD800..=0xDFFF => ('\u{fffd}', true),
0x80..=0x9F => match data::C1_REPLACEMENTS[(self.num - 0x80) as usize] {
Some(c) => (c, true),
None => (conv(self.num), true),
n if !self.num_too_big
&& matches!(
n,
0x9 | 0xa | 0xd | 0x20..=0xd7ff | 0xe000..=0xfffd | 0x10000..=0x10ffff
) =>
{
(
from_u32(n).expect("the XML Char production contains only Unicode scalars"),
false,
)
},
0x01..=0x08 | 0x0B | 0x0D..=0x1F | 0x7F | 0xFDD0..=0xFDEF => (conv(self.num), true),
n if (n & 0xFFFE) == 0xFFFE => (conv(n), true),
n => (conv(n), false),
_ => ('\u{fffd}', true),
};
if error {