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!("&#x{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 {