Skip to main content

lib3mf_core/parser/
xml_parser.rs

1use crate::error::{Lib3mfError, Result};
2use lexical_core;
3use quick_xml::events::{BytesStart, Event};
4use quick_xml::reader::Reader;
5use std::borrow::Cow;
6use std::io::BufRead;
7
8/// A low-level XML parser wrapper providing event-based reading with a reusable buffer.
9pub struct XmlParser<R: BufRead> {
10    /// The underlying quick-xml reader.
11    pub reader: Reader<R>,
12    /// Reusable internal buffer for XML event parsing.
13    pub buf: Vec<u8>,
14}
15
16impl<R: BufRead> XmlParser<R> {
17    /// Creates a new `XmlParser` wrapping the given buffered reader.
18    pub fn new(reader: R) -> Self {
19        let mut reader = Reader::from_reader(reader);
20        reader.config_mut().trim_text(true);
21        reader.config_mut().expand_empty_elements = true;
22        Self {
23            reader,
24            buf: Vec::new(),
25        }
26    }
27
28    /// Reads the next XML event, clearing the internal buffer first.
29    pub fn read_next_event(&mut self) -> Result<Event<'_>> {
30        self.buf.clear();
31        self.reader
32            .read_event_into(&mut self.buf)
33            .map_err(|e| Lib3mfError::Validation(e.to_string()))
34    }
35
36    /// Reads and concatenates text content up to the closing tag, returning the accumulated string.
37    pub fn read_text_content(&mut self) -> Result<String> {
38        let mut text = String::new();
39        let mut depth = 0;
40
41        loop {
42            match self.read_next_event()? {
43                Event::Text(e) => text.push_str(&String::from_utf8_lossy(e.as_ref())),
44                Event::CData(e) => text.push_str(&String::from_utf8_lossy(e.into_inner().as_ref())),
45                Event::Start(_) => depth += 1,
46                Event::End(_) => {
47                    if depth > 0 {
48                        depth -= 1;
49                    } else {
50                        return Ok(text);
51                    }
52                }
53                Event::Eof => {
54                    return Err(Lib3mfError::Validation(
55                        "Unexpected EOF in text content".to_string(),
56                    ));
57                }
58                _ => {}
59            }
60        }
61    }
62
63    /// Skips all XML events until the closing tag matching `end` is consumed.
64    pub fn read_to_end(&mut self, end: &[u8]) -> Result<()> {
65        // read_to_end_into expects QName
66        self.reader
67            .read_to_end_into(quick_xml::name::QName(end), &mut self.buf)
68            .map_err(|e| Lib3mfError::Validation(e.to_string()))?;
69        Ok(())
70    }
71}
72
73// Helper functions for attribute parsing
74
75/// Returns the value of the named XML attribute as a string, or `None` if absent.
76pub fn get_attribute<'a>(e: &'a BytesStart, name: &[u8]) -> Option<Cow<'a, str>> {
77    e.try_get_attribute(name).ok().flatten().map(|a| {
78        a.unescape_value()
79            .unwrap_or_else(|_| String::from_utf8_lossy(&a.value).into_owned().into())
80    })
81}
82
83/// Returns the named attribute parsed as an `f32`, or an error if absent or invalid.
84pub fn get_attribute_f32(e: &BytesStart, name: &[u8]) -> Result<f32> {
85    let attr = e.try_get_attribute(name).ok().flatten().ok_or_else(|| {
86        Lib3mfError::Validation(format!(
87            "Missing attribute: {}",
88            String::from_utf8_lossy(name)
89        ))
90    })?;
91    lexical_core::parse::<f32>(attr.value.as_ref()).map_err(|_| {
92        Lib3mfError::Validation(format!(
93            "Invalid float for attribute {}: {}",
94            String::from_utf8_lossy(name),
95            String::from_utf8_lossy(&attr.value)
96        ))
97    })
98}
99
100/// Returns the named attribute parsed as a `u32`, or an error if absent or invalid.
101pub fn get_attribute_u32(e: &BytesStart, name: &[u8]) -> Result<u32> {
102    let attr = e.try_get_attribute(name).ok().flatten().ok_or_else(|| {
103        Lib3mfError::Validation(format!(
104            "Missing attribute: {}",
105            String::from_utf8_lossy(name)
106        ))
107    })?;
108    lexical_core::parse::<u32>(attr.value.as_ref()).map_err(|_| {
109        Lib3mfError::Validation(format!(
110            "Invalid integer for attribute {}: {}",
111            String::from_utf8_lossy(name),
112            String::from_utf8_lossy(&attr.value)
113        ))
114    })
115}
116
117/// Returns the named attribute parsed as a `u32`, or `None` if absent, or an error if invalid.
118pub fn get_attribute_u32_opt(e: &BytesStart, name: &[u8]) -> Result<Option<u32>> {
119    match e.try_get_attribute(name).ok().flatten() {
120        Some(attr) => lexical_core::parse::<u32>(attr.value.as_ref())
121            .map(Some)
122            .map_err(|_| {
123                Lib3mfError::Validation(format!(
124                    "Invalid integer: {}",
125                    String::from_utf8_lossy(&attr.value)
126                ))
127            }),
128        None => Ok(None),
129    }
130}
131
132/// Returns the `uuid` or `p:uuid` attribute parsed as a `Uuid`, or `None` if absent.
133pub fn get_attribute_uuid(e: &BytesStart) -> Result<Option<uuid::Uuid>> {
134    // Try "uuid" then "p:uuid"
135    let val = get_attribute(e, b"uuid").or_else(|| get_attribute(e, b"p:uuid"));
136
137    match val {
138        Some(s) => uuid::Uuid::parse_str(&s)
139            .map(Some)
140            .map_err(|_| Lib3mfError::Validation(format!("Invalid UUID: {}", s))),
141        None => Ok(None),
142    }
143}