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