1use std::collections::HashSet;
4
5use quick_xml::XmlVersion;
6use quick_xml::errors::{Error as QuickXmlError, IllFormedError, SyntaxError};
7use quick_xml::events::{BytesDecl, BytesRef, BytesStart, Event};
8use quick_xml::name::{Namespace, ResolveResult};
9use quick_xml::reader::NsReader;
10
11use crate::error::Result;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub enum XmlLexicalError {
16 InvalidUtf8,
18 InvalidDeclaration(String),
20 ForbiddenLiteralCharacter,
22 InvalidName(String),
24 InvalidNamespace(String),
26 DuplicateExpandedAttribute,
28 InvalidReference(String),
30 InvalidProcessingInstruction(String),
32 InvalidComment(String),
34}
35
36pub fn validate_strict_xml_1_0(xml: &[u8]) -> std::result::Result<(), XmlLexicalError> {
38 let text = std::str::from_utf8(xml).map_err(|_| XmlLexicalError::InvalidUtf8)?;
39 if !text.chars().all(xml_1_0_character_is_valid) {
40 return Err(XmlLexicalError::ForbiddenLiteralCharacter);
41 }
42
43 let mut reader = NsReader::from_reader(xml);
44 reader.config_mut().trim_text(false);
45 reader.config_mut().check_comments = true;
46 let mut buffer = Vec::new();
47 loop {
48 let (_, event) = reader
49 .read_resolved_event_into(&mut buffer)
50 .map_err(classify_reader_error)?;
51 let event = event.into_owned();
52 validate_lexical_event(&reader, &event)?;
53 if matches!(event, Event::Eof) {
54 return Ok(());
55 }
56 buffer.clear();
57 }
58}
59
60fn classify_reader_error(error: QuickXmlError) -> XmlLexicalError {
61 let message = error.to_string();
62 match error {
63 QuickXmlError::Syntax(SyntaxError::UnclosedComment)
64 | QuickXmlError::IllFormed(IllFormedError::DoubleHyphenInComment) => {
65 XmlLexicalError::InvalidComment(message)
66 }
67 QuickXmlError::Syntax(SyntaxError::UnclosedPI) => {
68 XmlLexicalError::InvalidProcessingInstruction(message)
69 }
70 QuickXmlError::Syntax(SyntaxError::UnclosedXmlDecl)
71 | QuickXmlError::IllFormed(IllFormedError::MissingDeclVersion(_))
72 | QuickXmlError::IllFormed(IllFormedError::UnknownVersion) => {
73 XmlLexicalError::InvalidDeclaration(message)
74 }
75 QuickXmlError::IllFormed(IllFormedError::UnclosedReference) | QuickXmlError::Escape(_) => {
76 XmlLexicalError::InvalidReference(message)
77 }
78 QuickXmlError::Namespace(_) => XmlLexicalError::InvalidNamespace(message),
79 _ => XmlLexicalError::InvalidName(message),
80 }
81}
82
83fn validate_lexical_event(
84 reader: &NsReader<&[u8]>,
85 event: &Event<'_>,
86) -> std::result::Result<(), XmlLexicalError> {
87 match event {
88 Event::Decl(declaration) => validate_xml_declaration(declaration),
89 Event::Start(element) | Event::Empty(element) => validate_element(reader, element),
90 Event::End(element) => {
91 let name = element.name();
92 let prefix = validate_xml_qname(name.as_ref())?;
93 validate_bound_prefix(&reader.resolver().resolve_element(name).0, prefix).map(|_| ())
94 }
95 Event::GeneralRef(reference) => validate_reference(reference).map(|_| ()),
96 Event::PI(instruction) => validate_processing_instruction(instruction.target()),
97 _ => Ok(()),
98 }
99}
100
101fn validate_xml_declaration(
102 declaration: &BytesDecl<'_>,
103) -> std::result::Result<(), XmlLexicalError> {
104 if declaration
105 .xml_version()
106 .map_err(|error| XmlLexicalError::InvalidDeclaration(error.to_string()))?
107 != XmlVersion::Explicit1_0
108 {
109 return Err(XmlLexicalError::InvalidDeclaration(
110 "version must be 1.0".to_owned(),
111 ));
112 }
113 let content = std::str::from_utf8(declaration.as_ref())
114 .map_err(|error| XmlLexicalError::InvalidDeclaration(error.to_string()))?;
115 let start = BytesStart::from_content(content, 3);
116 let attributes = start
117 .attributes()
118 .collect::<std::result::Result<Vec<_>, _>>()
119 .map_err(|error| XmlLexicalError::InvalidDeclaration(error.to_string()))?;
120 if attributes.first().map(|attribute| attribute.key.as_ref()) != Some(b"version".as_ref()) {
121 return Err(XmlLexicalError::InvalidDeclaration(
122 "must begin with version".to_owned(),
123 ));
124 }
125 let mut encoding_seen = false;
126 let mut standalone_seen = false;
127 for (index, attribute) in attributes.iter().enumerate() {
128 let value = std::str::from_utf8(attribute.value.as_ref())
129 .map_err(|error| XmlLexicalError::InvalidDeclaration(error.to_string()))?;
130 match attribute.key.as_ref() {
131 b"version" if index == 0 && value == "1.0" => {}
132 b"encoding" if !encoding_seen && !standalone_seen && encoding_name_is_valid(value) => {
133 encoding_seen = true;
134 }
135 b"standalone" if !standalone_seen && matches!(value, "yes" | "no") => {
136 standalone_seen = true;
137 }
138 _ => {
139 return Err(XmlLexicalError::InvalidDeclaration(
140 "attributes are invalid, duplicated, or out of order".to_owned(),
141 ));
142 }
143 }
144 }
145 Ok(())
146}
147
148fn encoding_name_is_valid(value: &str) -> bool {
149 let mut bytes = value.bytes();
150 bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic())
151 && bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
152}
153
154fn validate_element(
155 reader: &NsReader<&[u8]>,
156 element: &BytesStart<'_>,
157) -> std::result::Result<(), XmlLexicalError> {
158 let element_name = element.name();
159 let prefix = validate_xml_qname(element_name.as_ref())?;
160 if prefix == Some(b"xmlns".as_slice()) {
161 return Err(XmlLexicalError::InvalidNamespace(
162 "element uses the reserved xmlns prefix".to_owned(),
163 ));
164 }
165 validate_bound_prefix(&reader.resolver().resolve_element(element_name).0, prefix)?;
166
167 let mut expanded_names = HashSet::new();
168 for attribute in element.attributes() {
169 let attribute =
170 attribute.map_err(|error| XmlLexicalError::InvalidName(error.to_string()))?;
171 let name = attribute.key.as_ref();
172 let prefix = validate_xml_qname(name)?;
173 if attribute.value.contains(&b'<') {
174 return Err(XmlLexicalError::InvalidReference(
175 "attribute contains a literal less-than sign".to_owned(),
176 ));
177 }
178 let value = attribute
179 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
180 .map_err(|error| XmlLexicalError::InvalidReference(error.to_string()))?;
181 if !value.chars().all(xml_1_0_character_is_valid) {
182 return Err(XmlLexicalError::ForbiddenLiteralCharacter);
183 }
184 if name == b"xmlns" {
185 validate_namespace_declaration(None, value.as_bytes())?;
186 continue;
187 }
188 if prefix == Some(b"xmlns".as_slice()) {
189 validate_namespace_declaration(Some(local_name(name)), value.as_bytes())?;
190 continue;
191 }
192 let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
193 let resolved = validate_bound_prefix(&namespace, prefix)?;
194 if !expanded_names.insert((resolved, local.as_ref().to_vec())) {
195 return Err(XmlLexicalError::DuplicateExpandedAttribute);
196 }
197 }
198 Ok(())
199}
200
201fn validate_namespace_declaration(
202 prefix: Option<&[u8]>,
203 namespace: &[u8],
204) -> std::result::Result<(), XmlLexicalError> {
205 const XML_NS: &[u8] = b"http://www.w3.org/XML/1998/namespace";
206 const XMLNS_NS: &[u8] = b"http://www.w3.org/2000/xmlns/";
207 let valid = match prefix {
208 None => namespace != XML_NS && namespace != XMLNS_NS,
209 Some(b"xml") => namespace == XML_NS,
210 Some(b"xmlns") => false,
211 Some(_) => !namespace.is_empty() && namespace != XML_NS && namespace != XMLNS_NS,
212 };
213 if valid {
214 Ok(())
215 } else {
216 Err(XmlLexicalError::InvalidNamespace(
217 "invalid namespace declaration".to_owned(),
218 ))
219 }
220}
221
222fn validate_bound_prefix(
223 namespace: &ResolveResult<'_>,
224 prefix: Option<&[u8]>,
225) -> std::result::Result<Option<Vec<u8>>, XmlLexicalError> {
226 match namespace {
227 ResolveResult::Bound(Namespace(namespace)) => Ok(Some(namespace.to_vec())),
228 ResolveResult::Unbound if prefix.is_none() => Ok(None),
229 ResolveResult::Unbound => Err(XmlLexicalError::InvalidNamespace(format!(
230 "unbound namespace prefix {}",
231 String::from_utf8_lossy(prefix.unwrap_or_default())
232 ))),
233 ResolveResult::Unknown(prefix) => Err(XmlLexicalError::InvalidNamespace(format!(
234 "unbound namespace prefix {}",
235 String::from_utf8_lossy(prefix)
236 ))),
237 }
238}
239
240fn validate_xml_qname(name: &[u8]) -> std::result::Result<Option<&[u8]>, XmlLexicalError> {
241 let name = std::str::from_utf8(name)
242 .map_err(|error| XmlLexicalError::InvalidName(error.to_string()))?;
243 let mut parts = name.split(':');
244 let first = parts.next().unwrap_or_default();
245 let second = parts.next();
246 if !xml_ncname_is_valid(first)
247 || second.is_some_and(|local| !xml_ncname_is_valid(local))
248 || parts.next().is_some()
249 {
250 return Err(XmlLexicalError::InvalidName(format!(
251 "qualified name {name}"
252 )));
253 }
254 Ok(second.map(|_| first.as_bytes()))
255}
256
257fn validate_xml_name(name: &[u8]) -> std::result::Result<(), XmlLexicalError> {
258 let name = std::str::from_utf8(name)
259 .map_err(|error| XmlLexicalError::InvalidName(error.to_string()))?;
260 let mut characters = name.chars();
261 if characters
262 .next()
263 .is_some_and(|character| character == ':' || xml_ncname_start_character(character))
264 && characters.all(|character| character == ':' || xml_ncname_character(character))
265 {
266 Ok(())
267 } else {
268 Err(XmlLexicalError::InvalidName(format!("name {name}")))
269 }
270}
271
272fn xml_ncname_is_valid(value: &str) -> bool {
273 let mut characters = value.chars();
274 characters.next().is_some_and(xml_ncname_start_character)
275 && characters.all(xml_ncname_character)
276}
277
278fn xml_ncname_start_character(character: char) -> bool {
279 matches!(
280 character,
281 'A'..='Z' | '_' | 'a'..='z' | '\u{00C0}'..='\u{00D6}' | '\u{00D8}'..='\u{00F6}'
282 | '\u{00F8}'..='\u{02FF}' | '\u{0370}'..='\u{037D}' | '\u{037F}'..='\u{1FFF}'
283 | '\u{200C}'..='\u{200D}' | '\u{2070}'..='\u{218F}' | '\u{2C00}'..='\u{2FEF}'
284 | '\u{3001}'..='\u{D7FF}' | '\u{F900}'..='\u{FDCF}' | '\u{FDF0}'..='\u{FFFD}'
285 | '\u{10000}'..='\u{EFFFF}'
286 )
287}
288
289fn xml_ncname_character(character: char) -> bool {
290 xml_ncname_start_character(character)
291 || matches!(character, '-' | '.' | '0'..='9' | '\u{00B7}' | '\u{0300}'..='\u{036F}' | '\u{203F}'..='\u{2040}')
292}
293
294fn xml_1_0_character_is_valid(character: char) -> bool {
295 matches!(character, '\t' | '\n' | '\r')
296 || ('\u{20}'..='\u{D7FF}').contains(&character)
297 || ('\u{E000}'..='\u{FFFD}').contains(&character)
298 || ('\u{10000}'..='\u{10FFFF}').contains(&character)
299}
300
301fn validate_reference(reference: &BytesRef<'_>) -> std::result::Result<char, XmlLexicalError> {
302 if let Some(character) = reference
303 .resolve_char_ref()
304 .map_err(|error| XmlLexicalError::InvalidReference(error.to_string()))?
305 {
306 if xml_1_0_character_is_valid(character) {
307 return Ok(character);
308 }
309 return Err(XmlLexicalError::InvalidReference(
310 "character reference is not legal in XML 1.0".to_owned(),
311 ));
312 }
313 let name = reference
314 .decode()
315 .map_err(|error| XmlLexicalError::InvalidReference(error.to_string()))?;
316 match name.as_ref() {
317 "amp" => Ok('&'),
318 "lt" => Ok('<'),
319 "gt" => Ok('>'),
320 "apos" => Ok('\''),
321 "quot" => Ok('"'),
322 _ => Err(XmlLexicalError::InvalidReference(format!(
323 "undeclared entity reference &{name};"
324 ))),
325 }
326}
327
328fn validate_processing_instruction(target: &[u8]) -> std::result::Result<(), XmlLexicalError> {
329 validate_xml_name(target).map_err(|error| match error {
330 XmlLexicalError::InvalidName(message) => {
331 XmlLexicalError::InvalidProcessingInstruction(message)
332 }
333 other => XmlLexicalError::InvalidProcessingInstruction(format!("{other:?}")),
334 })?;
335 if target.eq_ignore_ascii_case(b"xml") {
336 return Err(XmlLexicalError::InvalidProcessingInstruction(
337 "reserved XML target".to_owned(),
338 ));
339 }
340 Ok(())
341}
342
343pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
345
346pub const MC_NS: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
348
349pub fn local_name(name: &[u8]) -> &[u8] {
351 match name.iter().position(|&byte| byte == b':') {
352 Some(pos) => &name[pos + 1..],
353 None => name,
354 }
355}
356
357pub fn matches_local_name(name: &[u8], expected: &[u8]) -> bool {
359 local_name(name) == expected
360}
361
362pub fn get_attr(element: &BytesStart<'_>, name: &[u8]) -> Option<String> {
364 element
365 .attributes()
366 .flatten()
367 .find(|attr| matches_local_name(attr.key.as_ref(), name))
368 .and_then(|attr| std::str::from_utf8(&attr.value).ok().map(str::to_owned))
369}
370
371pub(crate) fn extra_namespace_declarations(
373 element: &BytesStart<'_>,
374) -> Result<Vec<(String, String)>> {
375 let mut declarations = Vec::new();
376 for attribute in element.attributes() {
377 let attribute = attribute?;
378 let key = attribute.key.as_ref();
379 if key.starts_with(b"xmlns:") && key != b"xmlns:vt" {
380 let name = std::str::from_utf8(key)?.to_owned();
381 let value = attribute
382 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
383 .into_owned();
384 declarations.push((name, value));
385 }
386 }
387 Ok(declarations)
388}
389
390#[cfg(test)]
391mod tests {
392 use quick_xml::Reader;
393 use quick_xml::events::Event;
394
395 use super::*;
396
397 #[test]
398 fn local_names_match_with_or_without_a_prefix() {
399 assert_eq!(local_name(b"w:document"), b"document");
400 assert_eq!(local_name(b"document"), b"document");
401 assert!(matches_local_name(b"p:sld", b"sld"));
402 assert!(!matches_local_name(b"p:sld", b"slide"));
403 }
404
405 #[test]
406 fn attributes_match_with_or_without_a_prefix() {
407 let mut reader = Reader::from_str(r#"<item r:id="rId7" plain="value"/>"#);
408 let mut buf = Vec::new();
409 let Event::Empty(element) = reader.read_event_into(&mut buf).unwrap() else {
410 panic!("expected empty element");
411 };
412
413 assert_eq!(get_attr(&element, b"id").as_deref(), Some("rId7"));
414 assert_eq!(get_attr(&element, b"plain").as_deref(), Some("value"));
415 assert_eq!(get_attr(&element, b"missing"), None);
416 }
417
418 #[test]
419 fn strict_xml_1_0_validator_rejects_every_shared_lexical_class() {
420 let malformed = [
421 (&b"\xff"[..], XmlLexicalError::InvalidUtf8),
422 (
423 &b"<?xml encoding=\"UTF-8\"?><root/>"[..],
424 XmlLexicalError::InvalidDeclaration(String::new()),
425 ),
426 (
427 &b"<?xml version=\"1.0\" encoding=\"UTF-8\"?><root/>"[..],
428 XmlLexicalError::InvalidDeclaration(String::new()),
429 ),
430 (
431 &b"<root>\x01</root>"[..],
432 XmlLexicalError::ForbiddenLiteralCharacter,
433 ),
434 (
435 &b"<1root/>"[..],
436 XmlLexicalError::InvalidName(String::new()),
437 ),
438 (
439 &b"<p:root/>"[..],
440 XmlLexicalError::InvalidNamespace(String::new()),
441 ),
442 (
443 &b"<root xmlns:a=\"urn:same\" xmlns:b=\"urn:same\" a:id=\"1\" b:id=\"2\"/>"[..],
444 XmlLexicalError::DuplicateExpandedAttribute,
445 ),
446 (
447 &b"<root>&undefined;</root>"[..],
448 XmlLexicalError::InvalidReference(String::new()),
449 ),
450 (
451 &b"<?XML value?><root/>"[..],
452 XmlLexicalError::InvalidProcessingInstruction(String::new()),
453 ),
454 (
455 &b"<root><!--bad--comment--></root>"[..],
456 XmlLexicalError::InvalidComment(String::new()),
457 ),
458 ];
459
460 for (xml, expected) in malformed {
461 let actual = validate_strict_xml_1_0(xml);
462 assert!(
463 actual.as_ref().is_err_and(
464 |error| std::mem::discriminant(error) == std::mem::discriminant(&expected)
465 ),
466 "{xml:?}: {actual:?}"
467 );
468 }
469 }
470}