use formats::vcard::Contact;
use quick_xml::{events::Event, Reader};
use thiserror::Error;
use crate::PbapError;
#[derive(Debug, Error)]
pub enum CardListingError {
#[error("XML error: {0}")]
Parse(#[from] quick_xml::Error),
#[error("attribute error: {0}")]
Attr(#[from] quick_xml::events::attributes::AttrError),
#[error("non-UTF-8 attribute value")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("card element missing required handle attribute")]
MissingHandle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardEntry {
handle: String,
name: Option<String>,
}
impl CardEntry {
pub(crate) const fn new(handle: String, name: Option<String>) -> Self {
Self { handle, name }
}
#[must_use]
pub fn handle(&self) -> &str {
&self.handle
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
pub(crate) fn parse_card_listing(xml: &[u8]) -> Result<Vec<CardEntry>, CardListingError> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut entries = Vec::new();
let mut buf = Vec::with_capacity(64);
loop {
match reader.read_event_into(&mut buf)? {
Event::Empty(e) | Event::Start(e) if e.name().as_ref() == b"card" => {
let mut handle: Option<String> = None;
let mut name: Option<String> = None;
for attr in e.attributes() {
let a = attr?;
match a.key.as_ref() {
b"handle" => handle = Some(String::from_utf8(a.value.into_owned())?),
b"name" => name = Some(String::from_utf8(a.value.into_owned())?),
_ => {}
}
}
entries.push(CardEntry::new(handle.ok_or(CardListingError::MissingHandle)?, name));
}
Event::Eof => break,
_ => {}
}
buf.clear();
}
Ok(entries)
}
#[must_use]
pub fn normalize_number(s: &str) -> String {
let mut d = String::with_capacity(s.len());
d.extend(s.chars().filter(|c| c.is_ascii_digit() || *c == '+'));
if d.starts_with('1') && d.len() == 11 {
d.insert(0, '+');
} else if d.len() == 10 {
d.insert_str(0, "+1");
}
d
}
pub(crate) fn parse_contacts(body: &[u8]) -> Result<Vec<Contact>, PbapError> {
let text = std::str::from_utf8(body).map_err(|_| PbapError::InvalidEncoding)?;
Ok(text
.split_inclusive("END:VCARD\r\n")
.filter(|block| block.contains("BEGIN:VCARD"))
.filter_map(|block| Contact::from_vcard_str(block).ok())
.collect())
}