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/// Validate the format-neutral lexical rules required by strict OOXML readers.
37pub 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
343/// Relationships namespace.
344pub const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
345
346/// Markup Compatibility namespace.
347pub const MC_NS: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
348
349/// Return the local portion of a possibly prefixed XML name.
350pub 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
357/// Check whether an XML name has the expected local portion.
358pub fn matches_local_name(name: &[u8], expected: &[u8]) -> bool {
359    local_name(name) == expected
360}
361
362/// Return a named attribute value, matching with or without a prefix.
363pub 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
371/// Return non-`vt` prefixed namespace declarations needed by raw XML children.
372pub(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=\"UT&#70;-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}