aster_forge_xml/
syntax.rs1use crate::{Error, XmlSafetyError};
4
5pub(crate) const XML_NAMESPACE_URI: &str = "http://www.w3.org/XML/1998/namespace";
6pub(crate) const XMLNS_NAMESPACE_URI: &str = "http://www.w3.org/2000/xmlns/";
7
8pub(crate) fn validate_qualified_name(name: &str) -> Result<(Option<&str>, &str), Error> {
9 let (prefix, local) = split_qualified_name(name);
10 if !valid_name(local)
11 || prefix.is_some_and(|prefix| !valid_name(prefix))
12 || name.matches(':').count() > 1
13 {
14 return Err(XmlSafetyError::Malformed.into());
15 }
16 Ok((prefix, local))
17}
18
19pub(crate) fn split_qualified_name(name: &str) -> (Option<&str>, &str) {
20 match name.split_once(':') {
21 Some((prefix, local)) => (Some(prefix), local),
22 None => (None, name),
23 }
24}
25
26#[must_use]
31pub fn is_valid_xml_local_name(name: &str) -> bool {
32 let mut characters = name.chars();
33 characters.next().is_some_and(is_name_start) && characters.all(is_name_char)
34}
35
36pub(crate) use is_valid_xml_local_name as valid_name;
37
38fn is_name_start(character: char) -> bool {
39 matches!(
40 character,
41 'A'..='Z'
42 | '_'
43 | 'a'..='z'
44 | '\u{00C0}'..='\u{00D6}'
45 | '\u{00D8}'..='\u{00F6}'
46 | '\u{00F8}'..='\u{02FF}'
47 | '\u{0370}'..='\u{037D}'
48 | '\u{037F}'..='\u{1FFF}'
49 | '\u{200C}'..='\u{200D}'
50 | '\u{2070}'..='\u{218F}'
51 | '\u{2C00}'..='\u{2FEF}'
52 | '\u{3001}'..='\u{D7FF}'
53 | '\u{F900}'..='\u{FDCF}'
54 | '\u{FDF0}'..='\u{FFFD}'
55 | '\u{10000}'..='\u{EFFFF}'
56 )
57}
58
59fn is_name_char(character: char) -> bool {
60 is_name_start(character)
61 || character.is_ascii_digit()
62 || matches!(character, '-' | '.' | '\u{B7}')
63 || ('\u{300}'..='\u{36F}').contains(&character)
64 || ('\u{203F}'..='\u{2040}').contains(&character)
65}
66
67pub(crate) fn validate_namespace_binding(prefix: &str, uri: &str) -> Result<(), Error> {
68 if prefix == "xmlns"
69 || uri == XMLNS_NAMESPACE_URI
70 || (prefix == "xml" && uri != XML_NAMESPACE_URI)
71 || (prefix != "xml" && uri == XML_NAMESPACE_URI)
72 || (!prefix.is_empty() && uri.is_empty())
73 || !is_valid_xml_namespace_name(uri)
74 {
75 Err(XmlSafetyError::Malformed.into())
76 } else {
77 Ok(())
78 }
79}
80
81#[must_use]
86pub fn is_valid_xml_namespace_name(uri: &str) -> bool {
87 if uri.chars().any(|character| {
88 character.is_control()
89 || character.is_whitespace()
90 || matches!(
91 character,
92 '<' | '>' | '"' | '{' | '}' | '|' | '\\' | '^' | '`'
93 )
94 }) {
95 return false;
96 }
97
98 let mut bytes = uri.as_bytes().iter();
99 while let Some(byte) = bytes.next() {
100 if *byte != b'%' {
101 continue;
102 }
103 let (Some(first), Some(second)) = (bytes.next(), bytes.next()) else {
104 return false;
105 };
106 if !first.is_ascii_hexdigit() || !second.is_ascii_hexdigit() {
107 return false;
108 }
109 }
110 true
111}
112
113pub(crate) fn map_quick_xml_error(error: quick_xml::Error) -> Error {
114 match error {
115 quick_xml::Error::Encoding(_) => XmlSafetyError::InvalidEncoding.into(),
116 error => Error::InvalidXml(error.to_string()),
117 }
118}
119
120pub(crate) fn map_quick_xml_error_at(
124 error: quick_xml::Error,
125 error_position: usize,
126 input: &[u8],
127 reject_doctype: bool,
128) -> Error {
129 if reject_doctype
130 && matches!(
131 error,
132 quick_xml::Error::Syntax(quick_xml::errors::SyntaxError::InvalidBangMarkup)
133 )
134 && input
135 .get(error_position..)
136 .is_some_and(|input| input.starts_with(b"<!ENTITY"))
137 {
138 XmlSafetyError::ExternalEntity.into()
139 } else {
140 map_quick_xml_error(error)
141 }
142}