melib/vcard: add parser for vcard files
parent
9a516e0663
commit
689327651f
|
@ -23,11 +23,17 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chrono::TimeZone;
|
use crate::chrono::TimeZone;
|
||||||
use crate::error::{MeliError, Result};
|
use crate::error::{MeliError, Result};
|
||||||
|
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
/* Supported vcard versions */
|
/* Supported vcard versions */
|
||||||
pub trait VCardVersion: core::fmt::Debug {}
|
pub trait VCardVersion: core::fmt::Debug {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VCardVersionUnknown;
|
||||||
|
impl VCardVersion for VCardVersionUnknown {}
|
||||||
|
|
||||||
/// https://tools.ietf.org/html/rfc6350
|
/// https://tools.ietf.org/html/rfc6350
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VCardVersion4;
|
pub struct VCardVersion4;
|
||||||
|
@ -40,7 +46,7 @@ impl VCardVersion for VCardVersion3 {}
|
||||||
|
|
||||||
pub struct CardDeserializer;
|
pub struct CardDeserializer;
|
||||||
|
|
||||||
static HEADER: &'static str = "BEGIN:VCARD\r\nVERSION:4.0\r\n";
|
static HEADER: &'static str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
|
||||||
static FOOTER: &'static str = "END:VCARD\r\n";
|
static FOOTER: &'static str = "END:VCARD\r\n";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -106,13 +112,18 @@ impl CardDeserializer {
|
||||||
el.params.push(l[value_start..i].to_string());
|
el.params.push(l[value_start..i].to_string());
|
||||||
value_start = i + 1;
|
value_start = i + 1;
|
||||||
}
|
}
|
||||||
|
(b';', Stage::Name) => {
|
||||||
|
name = l[value_start..i].to_string();
|
||||||
|
value_start = i + 1;
|
||||||
|
stage = Stage::Param;
|
||||||
|
}
|
||||||
(b':', Stage::Group) | (b':', Stage::Name) => {
|
(b':', Stage::Group) | (b':', Stage::Name) => {
|
||||||
name = l[value_start..i].to_string();
|
name = l[value_start..i].to_string();
|
||||||
has_colon = true;
|
has_colon = true;
|
||||||
value_start = i + 1;
|
value_start = i + 1;
|
||||||
stage = Stage::Value;
|
stage = Stage::Value;
|
||||||
}
|
}
|
||||||
(b':', Stage::Param) if l.as_bytes()[i] != b'\\' => {
|
(b':', Stage::Param) if l.as_bytes()[i.saturating_sub(1)] != b'\\' => {
|
||||||
el.params.push(l[value_start..i].to_string());
|
el.params.push(l[value_start..i].to_string());
|
||||||
has_colon = true;
|
has_colon = true;
|
||||||
value_start = i + 1;
|
value_start = i + 1;
|
||||||
|
@ -121,7 +132,6 @@ impl CardDeserializer {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
el.value = l[value_start..].to_string();
|
|
||||||
if !has_colon {
|
if !has_colon {
|
||||||
return Err(MeliError::new(format!(
|
return Err(MeliError::new(format!(
|
||||||
"Error while parsing vcard: error at line {}, no colon. {:?}",
|
"Error while parsing vcard: error at line {}, no colon. {:?}",
|
||||||
|
@ -134,13 +144,14 @@ impl CardDeserializer {
|
||||||
l, el
|
l, el
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
el.value = l[value_start..].replace("\\:", ":");
|
||||||
ret.insert(name, el);
|
ret.insert(name, el);
|
||||||
}
|
}
|
||||||
Ok(VCard(ret, std::marker::PhantomData::<*const VCardVersion4>))
|
Ok(VCard(ret, std::marker::PhantomData::<*const VCardVersion4>))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: VCardVersion> std::convert::TryInto<Card> for VCard<V> {
|
impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
||||||
type Error = crate::error::MeliError;
|
type Error = crate::error::MeliError;
|
||||||
|
|
||||||
fn try_into(mut self) -> crate::error::Result<Card> {
|
fn try_into(mut self) -> crate::error::Result<Card> {
|
||||||
|
@ -203,6 +214,9 @@ impl<V: VCardVersion> std::convert::TryInto<Card> for VCard<V> {
|
||||||
card.set_key(val.value);
|
card.set_key(val.value);
|
||||||
}
|
}
|
||||||
for (k, v) in self.0.into_iter() {
|
for (k, v) in self.0.into_iter() {
|
||||||
|
if k.eq_ignore_ascii_case("VERSION") || k.eq_ignore_ascii_case("N") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
card.set_extra_property(&k, v.value);
|
card.set_extra_property(&k, v.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +224,80 @@ impl<V: VCardVersion> std::convert::TryInto<Card> for VCard<V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_card<'a>() -> impl Parser<'a, Vec<&'a str>> {
|
||||||
|
move |input| {
|
||||||
|
one_or_more(prefix(
|
||||||
|
peek(match_literal_anycase(HEADER)),
|
||||||
|
take_until(match_literal_anycase(FOOTER)),
|
||||||
|
))
|
||||||
|
.parse(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_cards() {
|
||||||
|
/*
|
||||||
|
let mut contents = String::with_capacity(256);
|
||||||
|
let p = &std::path::Path::new("/tmp/contacts.vcf");
|
||||||
|
use std::io::Read;
|
||||||
|
contents.clear();
|
||||||
|
std::fs::File::open(&p)
|
||||||
|
.unwrap()
|
||||||
|
.read_to_string(&mut contents)
|
||||||
|
.unwrap();
|
||||||
|
for s in parse_card().parse(contents.as_str()).unwrap().1 {
|
||||||
|
println!("");
|
||||||
|
println!("{}", s);
|
||||||
|
println!("{:?}", CardDeserializer::from_str(s));
|
||||||
|
println!("");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
||||||
|
let vcf_dir = std::fs::read_dir(p);
|
||||||
|
let mut ret: Vec<Result<_>> = Vec::new();
|
||||||
|
let mut is_any_valid = false;
|
||||||
|
if vcf_dir.is_ok() {
|
||||||
|
let mut contents = String::with_capacity(256);
|
||||||
|
for f in vcf_dir? {
|
||||||
|
if f.is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let f = f?.path();
|
||||||
|
if f.is_file() {
|
||||||
|
use std::io::Read;
|
||||||
|
contents.clear();
|
||||||
|
std::fs::File::open(&f)?.read_to_string(&mut contents)?;
|
||||||
|
if let Ok((_, c)) = parse_card().parse(contents.as_str()) {
|
||||||
|
for s in c {
|
||||||
|
ret.push(
|
||||||
|
CardDeserializer::from_str(s)
|
||||||
|
.and_then(TryInto::try_into)
|
||||||
|
.and_then(|mut card| {
|
||||||
|
Card::set_external_resource(&mut card, true);
|
||||||
|
is_any_valid = true;
|
||||||
|
Ok(card)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c in &ret {
|
||||||
|
if c.is_err() {
|
||||||
|
debug!(&c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !is_any_valid {
|
||||||
|
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||||
|
} else {
|
||||||
|
ret.retain(Result::is_ok);
|
||||||
|
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_card() {
|
fn test_card() {
|
||||||
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
||||||
|
|
|
@ -309,3 +309,37 @@ where
|
||||||
Err(_) => Ok((input, None)),
|
Err(_) => Ok((input, None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn peek<'a, P, A>(parser: P) -> impl Parser<'a, A>
|
||||||
|
where
|
||||||
|
P: Parser<'a, A>,
|
||||||
|
{
|
||||||
|
move |input| match parser.parse(input) {
|
||||||
|
Ok((_, result)) => Ok((input, result)),
|
||||||
|
e @ Err(_) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_until<'a, A, P>(end: P) -> impl Parser<'a, &'a str>
|
||||||
|
where
|
||||||
|
P: Parser<'a, A>,
|
||||||
|
{
|
||||||
|
move |input: &'a str| {
|
||||||
|
let mut offset = 0;
|
||||||
|
while !input[offset..].is_empty() {
|
||||||
|
if let Ok((rest, _)) = end.parse(&input[offset..]) {
|
||||||
|
return Ok((
|
||||||
|
rest,
|
||||||
|
&input[..(offset + input[offset..].len() - rest.len())],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
while offset != input.len() {
|
||||||
|
offset += 1;
|
||||||
|
if input.is_char_boundary(offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((&input[offset..], input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ mod tests {
|
||||||
let mut stack = StackVec::from_iter(0..4 * STACK_VEC_CAPACITY);
|
let mut stack = StackVec::from_iter(0..4 * STACK_VEC_CAPACITY);
|
||||||
let mut ctr = 0;
|
let mut ctr = 0;
|
||||||
assert!(stack.iter().all(|&x| {
|
assert!(stack.iter().all(|&x| {
|
||||||
let ret = (x == ctr);
|
let ret = x == ctr;
|
||||||
ctr += 1;
|
ctr += 1;
|
||||||
ret
|
ret
|
||||||
}));
|
}));
|
||||||
|
@ -234,7 +234,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
ctr = 0;
|
ctr = 0;
|
||||||
assert!(stack.iter().all(|&x| {
|
assert!(stack.iter().all(|&x| {
|
||||||
let ret = (x == ctr);
|
let ret = x == ctr;
|
||||||
ctr += 1;
|
ctr += 1;
|
||||||
ret
|
ret
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue