use crate::common::content_line::{parse_content_line, split_lines, unfold};
use crate::error::{Error, Result};
use crate::vcard::contact::Contact;
pub(crate) fn parse_vcards(input: &str) -> Result<Vec<Contact>> {
let unfolded = unfold(input);
let lines = split_lines(&unfolded);
let mut contacts = Vec::new();
let mut idx = 0;
while idx < lines.len() {
let prop = parse_content_line(lines[idx], idx + 1)?;
idx += 1;
if prop.name == "BEGIN" && prop.value.to_uppercase() == "VCARD" {
let (contact, new_idx) = parse_single_vcard(&lines, idx)?;
contacts.push(contact);
idx = new_idx;
}
}
Ok(contacts)
}
fn parse_single_vcard(lines: &[&str], start: usize) -> Result<(Contact, usize)> {
let mut idx = start;
let mut props = Vec::new();
while idx < lines.len() {
let prop = parse_content_line(lines[idx], idx + 1)?;
idx += 1;
if prop.name == "END" && prop.value.to_uppercase() == "VCARD" {
let contact = Contact::from_properties(props)?;
return Ok((contact, idx));
}
props.push(prop);
}
Err(Error::UnclosedComponent("VCARD".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_vcard() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Jane Doe\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].get_full_name(), Some("Jane Doe"));
}
#[test]
fn parse_full_vcard() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
UID:urn:uuid:12345\r\n\
FN:Jane Doe\r\n\
N:Doe;Jane;;Dr.;\r\n\
EMAIL;TYPE=WORK:jane@example.com\r\n\
EMAIL;TYPE=HOME:jane.doe@personal.com\r\n\
TEL;TYPE=WORK,VOICE:+1-555-0123\r\n\
ORG:Acme Corp\r\n\
TITLE:Software Engineer\r\n\
ADR;TYPE=WORK:;;123 Main St;Anytown;CA;90210;USA\r\n\
NOTE:A note about Jane\r\n\
URL:https://jane.example.com\r\n\
BDAY:19900115\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(contacts.len(), 1);
let c = &contacts[0];
assert_eq!(c.get_uid(), Some("urn:uuid:12345"));
assert_eq!(c.get_full_name(), Some("Jane Doe"));
assert_eq!(c.get_emails().len(), 2);
assert_eq!(c.get_email(), Some("jane@example.com"));
assert_eq!(c.get_phone(), Some("+1-555-0123"));
assert_eq!(c.get_organization(), Some("Acme Corp"));
assert_eq!(c.get_title(), Some("Software Engineer"));
assert_eq!(c.get_addresses().len(), 1);
assert_eq!(c.get_addresses()[0].address.city, "Anytown");
assert_eq!(c.get_note(), Some("A note about Jane"));
assert_eq!(c.get_birthday(), Some("19900115"));
let name = c.get_name().unwrap();
assert_eq!(name.family, "Doe");
assert_eq!(name.given, "Jane");
assert_eq!(name.prefix, "Dr.");
}
#[test]
fn parse_multiple_vcards() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Alice\r\n\
END:VCARD\r\n\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Bob\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(contacts.len(), 2);
assert_eq!(contacts[0].get_full_name(), Some("Alice"));
assert_eq!(contacts[1].get_full_name(), Some("Bob"));
}
#[test]
fn parse_vcard_with_folded_lines() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Jane Doe\r\n\
NOTE:This is a very long note that has been folded across \r\n multiple lines in the file\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(
contacts[0].get_note(),
Some("This is a very long note that has been folded across multiple lines in the file")
);
}
#[test]
fn parse_vcard_escaped_text() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Jane Doe\r\n\
NOTE:Line 1\\nLine 2\\nLine 3\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(contacts[0].get_note(), Some("Line 1\nLine 2\nLine 3"));
}
#[test]
fn parse_vcard_unknown_properties_preserved() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Jane\r\n\
X-CUSTOM:custom-value\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
assert!(
contacts[0]
.get_extra_properties()
.iter()
.any(|p| p.name == "X-CUSTOM" && p.value == "custom-value")
);
}
#[test]
fn error_unclosed_vcard() {
let input = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Jane\r\n";
let result = parse_vcards(input);
assert!(result.is_err());
}
#[test]
fn parse_bare_lf() {
let input = "BEGIN:VCARD\nVERSION:4.0\nFN:Jane Doe\nEND:VCARD\n";
let contacts = parse_vcards(input).unwrap();
assert_eq!(contacts.len(), 1);
}
#[test]
fn parse_typed_phone() {
let input = "\
BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
FN:Test\r\n\
TEL;TYPE=WORK,VOICE:+1-555-0123\r\n\
TEL;TYPE=HOME:+1-555-0456\r\n\
END:VCARD\r\n";
let contacts = parse_vcards(input).unwrap();
let phones = contacts[0].get_phones();
assert_eq!(phones.len(), 2);
assert_eq!(phones[0].types, vec!["WORK", "VOICE"]);
assert_eq!(phones[1].types, vec!["HOME"]);
}
}