Skip to main content

oxml_core/
xml.rs

1//! Shared OOXML namespace and attribute helpers.
2
3use 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/// A strict XML 1.0 lexical validation failure.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub enum XmlLexicalError {
16    /// The input is not UTF-8.
17    InvalidUtf8,
18    /// The XML declaration does not satisfy the XML 1.0 grammar.
19    InvalidDeclaration(String),
20    /// The input contains a literal character forbidden by XML 1.0.
21    ForbiddenLiteralCharacter,
22    /// An element, attribute, or processing instruction name is invalid.
23    InvalidName(String),
24    /// A namespace declaration or qualified-name binding is invalid.
25    InvalidNamespace(String),
26    /// Two attributes resolve to the same namespace and local name.
27    DuplicateExpandedAttribute,
28    /// An entity or character reference is invalid.
29    InvalidReference(String),
30    /// A processing instruction target is invalid or reserved.
31    InvalidProcessingInstruction(String),
32    /// A comment is not lexically valid.
33    InvalidComment(String),
34}
35
36/// Refuses `value` when it holds a character XML 1.0 cannot carry, not even
37/// escaped, naming the character and its one-based position, as python-docx
38/// and python-pptx refuse such a string. `what` names the value in the message.
39pub 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
54/// Validate the format-neutral lexical rules required by strict OOXML readers.
55pub 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
312/// Whether XML 1.0 can carry `character`, the `Char` production.
313pub 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
362/// Relationships namespace.
363pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
364
365/// Markup Compatibility namespace.
366pub const MC_NS: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
367
368/// Return the local portion of a possibly prefixed XML name.
369pub 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
376/// Check whether an XML name has the expected local portion.
377pub fn matches_local_name(name: &[u8], expected: &[u8]) -> bool {
378    local_name(name) == expected
379}
380
381/// Return a named attribute value, matching with or without a prefix.
382pub 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
390/// Return non-`vt` prefixed namespace declarations needed by raw XML children.
391pub(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=\"UT&#70;-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}