Skip to main content

imsg_formats/
xml.rs

1//! MAP XML structures: folder listing parser. Use [`FolderListing::parse`].
2
3use quick_xml::{events::Event, Reader};
4use thiserror::Error;
5
6/// MAP XML parsing errors — quick-xml parse, attribute decode, and folder name UTF-8 validation.
7#[derive(Debug, Error)]
8pub enum XmlError {
9    /// Malformed XML or unrecognised structure.
10    #[error("XML error: {0}")]
11    Parse(#[from] quick_xml::Error),
12    /// Invalid or malformed attribute encoding.
13    #[error("attribute error: {0}")]
14    Attr(#[from] quick_xml::events::attributes::AttrError),
15    /// Folder name attribute is not valid UTF-8.
16    #[error("non-UTF-8 folder name")]
17    Utf8(#[from] std::string::FromUtf8Error),
18}
19
20/// A single MAP folder from a `GetFolderListing` response.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct FolderEntry {
23    name: String,
24}
25
26impl FolderEntry {
27    pub(crate) const fn new(name: String) -> Self {
28        Self { name }
29    }
30
31    /// Folder name decoded from the XML `name` attribute, e.g. `inbox`.
32    #[must_use]
33    pub fn name(&self) -> &str {
34        &self.name
35    }
36}
37
38/// Parsed body of a MAP `GetFolderListing` OBEX response.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct FolderListing {
41    folders: Vec<FolderEntry>,
42}
43
44impl FolderListing {
45    /// Folders in document order.
46    #[must_use]
47    pub fn folders(&self) -> &[FolderEntry] {
48        &self.folders
49    }
50
51    /// Parses a `<folder-listing>` XML document from raw bytes.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`XmlError`] on malformed XML, undecodable attributes, or non-UTF-8 folder names.
56    pub fn parse(xml: &[u8]) -> Result<Self, XmlError> {
57        let mut reader = Reader::from_reader(xml);
58        reader.config_mut().trim_text(true);
59        let mut folders = Vec::new();
60        let mut buf = Vec::new();
61        loop {
62            match reader.read_event_into(&mut buf)? {
63                Event::Empty(e) | Event::Start(e) if e.name().as_ref() == b"folder" => {
64                    for attr in e.attributes() {
65                        let a = attr?;
66                        if a.key.as_ref() == b"name" {
67                            folders.push(FolderEntry::new(
68                                String::from_utf8(a.value.into_owned()).map_err(XmlError::Utf8)?,
69                            ));
70                        }
71                    }
72                }
73                Event::Eof => break,
74                _ => {}
75            }
76            buf.clear();
77        }
78        Ok(Self { folders })
79    }
80}