diff --git a/moli-parser/src/xml.rs b/moli-parser/src/xml.rs
index f12ad4bae1..baf7494287 100644
--- a/moli-parser/src/xml.rs
+++ b/moli-parser/src/xml.rs
@@ -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!("{unit:X};")] {
+ for quote in ['\'', '"'] {
+ let source = format!(
+ "{reference}"
+ );
+ 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!(""),
+ format!("{value};"),
+ ] {
+ 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"] {
diff --git a/vendor/xml5ever-0.39.0/MOLI_VENDORING.md b/vendor/xml5ever-0.39.0/MOLI_VENDORING.md
index a68a2c197d..0e69e8cc77 100644
--- a/vendor/xml5ever-0.39.0/MOLI_VENDORING.md
+++ b/vendor/xml5ever-0.39.0/MOLI_VENDORING.md
@@ -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.
diff --git a/vendor/xml5ever-0.39.0/src/tokenizer/char_ref/mod.rs b/vendor/xml5ever-0.39.0/src/tokenizer/char_ref/mod.rs
index 4728a76b66..5d1bd1063e 100644
--- a/vendor/xml5ever-0.39.0/src/tokenizer/char_ref/mod.rs
+++ b/vendor/xml5ever-0.39.0/src/tokenizer/char_ref/mod.rs
@@ -237,24 +237,19 @@ impl CharRefTokenizer {
}
fn finish_numeric(&mut self, tokenizer: &XmlTokenizer) -> 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 {