use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum VCardError {
#[error("Invalid vCard: {0}")]
InvalidFormat(String),
#[error("Missing BEGIN:VCARD or END:VCARD")]
MissingBoundary,
#[error("Unsupported encoding: {0}")]
UnsupportedEncoding(String),
#[error("IO error: {0}")]
Io(String),
}
type Result<T> = std::result::Result<T, VCardError>;
#[derive(Debug, Clone, Default)]
pub struct VCardParamMap {
inner: HashMap<String, Vec<String>>,
}
impl VCardParamMap {
pub fn new() -> Self {
Self::default()
}
pub fn add_param(&mut self, name: &str, value: &str) {
let key = name.to_uppercase();
self.inner.entry(key).or_default().push(value.to_string());
}
pub fn set_param(&mut self, name: &str, value: &str) {
let key = name.to_uppercase();
self.inner.insert(key, vec![value.to_string()]);
}
pub fn remove_param(&mut self, name: &str) {
let key = name.to_uppercase();
self.inner.remove(&key);
}
pub fn has_param(&self, name: &str) -> bool {
let key = name.to_uppercase();
self.inner.contains_key(&key)
}
pub fn get_param(&self, name: &str) -> Option<&str> {
let key = name.to_uppercase();
self.inner.get(&key).and_then(|v| v.first()).map(|s| s.as_str())
}
pub fn get_all_params(&self, name: &str) -> Vec<&str> {
let key = name.to_uppercase();
self.inner.get(&key).map(|v| v.iter().map(|s| s.as_str()).collect()).unwrap_or_default()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
self.inner.iter()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct VCardProperty {
group: String,
name: String,
values: Vec<String>,
params: VCardParamMap,
}
impl VCardProperty {
pub fn new(name: &str, value: &str) -> Self {
Self {
group: String::new(),
name: name.to_uppercase(),
values: vec![value.to_string()],
params: VCardParamMap::new(),
}
}
pub fn new_multi(name: &str, values: Vec<String>) -> Self {
Self {
group: String::new(),
name: name.to_uppercase(),
values,
params: VCardParamMap::new(),
}
}
pub fn group(&self) -> &str { &self.group }
pub fn name(&self) -> &str { &self.name }
pub fn values(&self) -> &[String] { &self.values }
pub fn params(&self) -> &VCardParamMap { &self.params }
pub fn params_mut(&mut self) -> &mut VCardParamMap { &mut self.params }
pub fn set_group(&mut self, group: &str) { self.group = group.to_string(); }
pub fn set_name(&mut self, name: &str) { self.name = name.to_uppercase(); }
pub fn set_values(&mut self, values: Vec<String>) { self.values = values; }
pub fn combined_value(&self) -> String {
self.values.join(";")
}
}
#[derive(Debug, Clone, Default)]
pub struct VCard {
version: String, properties: Vec<VCardProperty>,
}
impl VCard {
pub fn new(version: &str) -> Self {
Self {
version: version.to_string(),
properties: Vec::new(),
}
}
pub fn new_v2_1() -> Self { Self::new("2.1") }
pub fn new_v3_0() -> Self { Self::new("3.0") }
pub fn new_v4_0() -> Self { Self::new("4.0") }
pub fn version(&self) -> &str { &self.version }
pub fn set_version(&mut self, version: &str) { self.version = version.to_string(); }
pub fn add_property(&mut self, prop: VCardProperty) {
self.properties.push(prop);
}
pub fn remove_property(&mut self, name: &str) {
let name = name.to_uppercase();
self.properties.retain(|p| p.name != name);
}
pub fn has_property(&self, name: &str) -> bool {
let name = name.to_uppercase();
self.properties.iter().any(|p| p.name == name)
}
pub fn get_properties(&self, name: &str) -> Vec<&VCardProperty> {
let name = name.to_uppercase();
self.properties.iter().filter(|p| p.name == name).collect()
}
pub fn get_first_property(&self, name: &str) -> Option<&VCardProperty> {
let name = name.to_uppercase();
self.properties.iter().find(|p| p.name == name)
}
pub fn get_formatted_name(&self) -> Option<&str> {
self.get_first_property("FN").and_then(|p| p.values.first()).map(|s| s.as_str())
}
pub fn get_first_name(&self) -> Option<&str> {
self.get_first_property("N").and_then(|p| p.values.get(1)).map(|s| s.as_str())
}
pub fn get_last_name(&self) -> Option<&str> {
self.get_first_property("N").and_then(|p| p.values.first()).map(|s| s.as_str())
}
pub fn get_phone_numbers(&self) -> Vec<&str> {
self.get_properties("TEL").iter().filter_map(|p| p.values.first()).map(|s| s.as_str()).collect()
}
pub fn get_emails(&self) -> Vec<&str> {
self.get_properties("EMAIL").iter().filter_map(|p| p.values.first()).map(|s| s.as_str()).collect()
}
pub fn to_string(&self) -> Result<String> {
let mut out = String::new();
out.push_str("BEGIN:VCARD\r\n");
out.push_str(&format!("VERSION:{}\r\n", self.version));
for prop in &self.properties {
if !prop.group.is_empty() {
out.push_str(&prop.group);
out.push('.');
}
out.push_str(&prop.name);
if !prop.params.is_empty() {
for (name, values) in prop.params.iter() {
for val in values {
out.push(';');
out.push_str(name);
out.push('=');
if val.contains(';') || val.contains(',') {
out.push('"');
out.push_str(val);
out.push('"');
} else {
out.push_str(val);
}
}
}
}
out.push(':');
for (i, val) in prop.values.iter().enumerate() {
if i > 0 { out.push(';'); }
for c in val.chars() {
if c == '\\' { out.push_str("\\\\"); }
else if c == ';' { out.push_str("\\;"); }
else { out.push(c); }
}
}
out.push_str("\r\n");
}
out.push_str("END:VCARD\r\n");
Ok(out)
}
}
pub fn parse_vcard(vcf: &str) -> Result<VCard> {
let lines = fold_lines(vcf);
let mut inside = false;
let mut card = None;
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
if trimmed == "BEGIN:VCARD" {
inside = true;
card = Some(VCard::default());
continue;
}
if trimmed == "END:VCARD" {
if let Some(c) = card.take() {
return Ok(c);
} else {
return Err(VCardError::MissingBoundary);
}
}
if inside {
let card_mut = card.as_mut().unwrap();
if let Some(prop) = parse_property(trimmed)? {
if prop.name == "VERSION" {
if let Some(ver) = prop.values.first() {
card_mut.set_version(ver);
}
} else {
card_mut.add_property(prop);
}
}
}
}
Err(VCardError::MissingBoundary)
}
fn fold_lines(vcf: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let lines: Vec<&str> = vcf.lines().map(|l| l.trim_end_matches('\r')).collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if current.is_empty() {
current = line.to_string();
i += 1;
continue;
}
if current.ends_with('=') {
current.pop();
current.push_str(line);
i += 1;
continue;
}
if i < lines.len() && (lines[i].starts_with(' ') || lines[i].starts_with('\t')) {
current.push_str(&lines[i][1..]);
i += 1;
continue;
}
result.push(current);
current = String::new();
}
if !current.is_empty() {
result.push(current);
}
result
}
fn parse_property(line: &str) -> Result<Option<VCardProperty>> {
let colon_pos = find_colon_unescaped(line);
let colon_pos = colon_pos.ok_or_else(|| VCardError::InvalidFormat(format!("Missing ':' in line: {}", line)))?;
let head = &line[..colon_pos];
let value_part = &line[colon_pos+1..];
let (group, rest) = if let Some(dot) = head.find('.') {
(head[..dot].to_string(), &head[dot+1..])
} else {
(String::new(), head)
};
let (name, param_str) = if let Some(semi) = rest.find(';') {
(rest[..semi].to_uppercase(), &rest[semi+1..])
} else {
(rest.to_uppercase(), "")
};
let mut params = VCardParamMap::new();
if !param_str.is_empty() {
parse_params(param_str, &mut params)?;
}
let mut values = split_unescaped(value_part, ';');
let encoding = params.get_param("ENCODING").unwrap_or("");
if encoding == "QUOTED-PRINTABLE" {
for v in &mut values {
*v = decode_quoted_printable(v);
}
}
Ok(Some(VCardProperty {
group,
name,
values,
params,
}))
}
fn find_colon_unescaped(s: &str) -> Option<usize> {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' {
i += 2; continue;
}
if chars[i] == ':' {
return Some(i);
}
i += 1;
}
None
}
fn split_unescaped(s: &str, sep: char) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
current.push(next);
}
} else if c == sep {
result.push(current);
current = String::new();
} else {
current.push(c);
}
}
result.push(current);
result
}
fn parse_params(param_str: &str, params: &mut VCardParamMap) -> Result<()> {
for part in param_str.split(';') {
if let Some(eq) = part.find('=') {
let name = &part[..eq];
let value = &part[eq+1..];
if name == "TYPE" && value.contains(',') {
for t in value.split(',') {
params.add_param(name, t);
}
} else {
params.add_param(name, value);
}
} else {
params.add_param(part, "");
}
}
Ok(())
}
fn decode_quoted_printable(s: &str) -> String {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'=' && i + 2 < bytes.len() {
let hex = &bytes[i+1..i+3];
if let Ok(byte) = u8::from_str_radix(&String::from_utf8_lossy(hex), 16) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).to_string()
}