2019-10-16 14:55:49 +03:00
/*
* meli - addressbook module
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli .
*
* meli is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* meli is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with meli . If not , see < http ://www.gnu.org/licenses/>.
* /
/// Convert VCard strings to meli Cards (contacts).
2019-10-20 11:06:26 +03:00
use super ::* ;
2019-10-16 14:55:49 +03:00
use crate ::error ::{ MeliError , Result } ;
2019-11-27 01:37:27 +02:00
use crate ::parsec ::{ match_literal_anycase , one_or_more , peek , prefix , take_until , Parser } ;
2020-05-10 21:14:49 +03:00
use std ::collections ::HashMap ;
2019-11-27 01:37:27 +02:00
use std ::convert ::TryInto ;
2019-10-16 14:55:49 +03:00
/* Supported vcard versions */
2019-11-09 18:10:22 +02:00
pub trait VCardVersion : core ::fmt ::Debug { }
2019-10-16 14:55:49 +03:00
2019-11-27 01:37:27 +02:00
#[ derive(Debug) ]
pub struct VCardVersionUnknown ;
impl VCardVersion for VCardVersionUnknown { }
2019-10-16 14:55:49 +03:00
/// https://tools.ietf.org/html/rfc6350
2019-11-09 18:10:22 +02:00
#[ derive(Debug) ]
2019-10-16 14:55:49 +03:00
pub struct VCardVersion4 ;
impl VCardVersion for VCardVersion4 { }
/// https://tools.ietf.org/html/rfc2426
2019-11-09 18:10:22 +02:00
#[ derive(Debug) ]
2019-10-16 14:55:49 +03:00
pub struct VCardVersion3 ;
impl VCardVersion for VCardVersion3 { }
pub struct CardDeserializer ;
2020-07-05 15:28:55 +03:00
static HEADER : & str = " BEGIN:VCARD \r \n " ; //VERSION:4.0\r\n";
static FOOTER : & str = " END:VCARD \r \n " ;
2019-10-16 14:55:49 +03:00
2019-11-09 18:10:22 +02:00
#[ derive(Debug) ]
2019-10-16 14:55:49 +03:00
pub struct VCard < T : VCardVersion > (
2020-05-10 21:14:49 +03:00
HashMap < String , ContentLine > ,
2019-10-16 14:55:49 +03:00
std ::marker ::PhantomData < * const T > ,
) ;
impl < V : VCardVersion > VCard < V > {
pub fn new_v4 ( ) -> VCard < impl VCardVersion > {
VCard (
2020-05-10 21:14:49 +03:00
HashMap ::default ( ) ,
2019-10-16 14:55:49 +03:00
std ::marker ::PhantomData ::< * const VCardVersion4 > ,
)
}
}
#[ derive(Debug, Default, Clone) ]
pub struct ContentLine {
group : Option < String > ,
params : Vec < String > ,
value : String ,
}
impl CardDeserializer {
pub fn from_str ( mut input : & str ) -> Result < VCard < impl VCardVersion > > {
input = if ! input . starts_with ( HEADER ) | | ! input . ends_with ( FOOTER ) {
2019-10-20 11:14:29 +03:00
return Err ( MeliError ::new ( format! ( " Error while parsing vcard: input does not start or end with correct header and footer. input is: \n {:?} " , input ) ) ) ;
2019-10-16 14:55:49 +03:00
} else {
& input [ HEADER . len ( ) .. input . len ( ) - FOOTER . len ( ) ]
} ;
2020-05-10 21:14:49 +03:00
let mut ret = HashMap ::default ( ) ;
2019-10-16 14:55:49 +03:00
enum Stage {
Group ,
Name ,
Param ,
Value ,
}
let mut stage : Stage ;
for l in input . lines ( ) {
let mut el = ContentLine ::default ( ) ;
let mut value_start = 0 ;
let mut has_colon = false ;
stage = Stage ::Group ;
let mut name = String ::new ( ) ;
for i in 0 .. l . len ( ) {
let byte = l . as_bytes ( ) [ i ] ;
match ( byte , & stage ) {
( b '.' , Stage ::Group ) if l . as_bytes ( ) [ i ] ! = b '\\' = > {
el . group = Some ( l [ value_start .. i ] . to_string ( ) ) ;
value_start = i + 1 ;
stage = Stage ::Name ;
}
( b ';' , Stage ::Group ) = > {
name = l [ value_start .. i ] . to_string ( ) ;
value_start = i + 1 ;
stage = Stage ::Param ;
}
( b ';' , Stage ::Param ) = > {
el . params . push ( l [ value_start .. i ] . to_string ( ) ) ;
value_start = i + 1 ;
}
2019-11-27 01:37:27 +02:00
( b ';' , Stage ::Name ) = > {
name = l [ value_start .. i ] . to_string ( ) ;
value_start = i + 1 ;
stage = Stage ::Param ;
}
2019-10-16 14:55:49 +03:00
( b ':' , Stage ::Group ) | ( b ':' , Stage ::Name ) = > {
name = l [ value_start .. i ] . to_string ( ) ;
has_colon = true ;
value_start = i + 1 ;
stage = Stage ::Value ;
}
2019-11-27 01:37:27 +02:00
( b ':' , Stage ::Param ) if l . as_bytes ( ) [ i . saturating_sub ( 1 ) ] ! = b '\\' = > {
2019-10-16 14:55:49 +03:00
el . params . push ( l [ value_start .. i ] . to_string ( ) ) ;
has_colon = true ;
value_start = i + 1 ;
stage = Stage ::Value ;
}
_ = > { }
}
}
if ! has_colon {
return Err ( MeliError ::new ( format! (
" Error while parsing vcard: error at line {}, no colon. {:?} " ,
l , el
) ) ) ;
}
if name . is_empty ( ) {
return Err ( MeliError ::new ( format! (
" Error while parsing vcard: error at line {}, no name for content line. {:?} " ,
l , el
) ) ) ;
}
2019-11-27 01:37:27 +02:00
el . value = l [ value_start .. ] . replace ( " \\ : " , " : " ) ;
2019-10-16 14:55:49 +03:00
ret . insert ( name , el ) ;
}
Ok ( VCard ( ret , std ::marker ::PhantomData ::< * const VCardVersion4 > ) )
}
}
2019-11-27 01:37:27 +02:00
impl < V : VCardVersion > TryInto < Card > for VCard < V > {
2019-10-16 14:55:49 +03:00
type Error = crate ::error ::MeliError ;
fn try_into ( mut self ) -> crate ::error ::Result < Card > {
let mut card = Card ::new ( ) ;
2019-10-20 11:06:26 +03:00
card . set_id ( CardId ::Hash ( {
use std ::hash ::Hasher ;
let mut hasher = std ::collections ::hash_map ::DefaultHasher ::new ( ) ;
if let Some ( val ) = self . 0. get ( " FN " ) {
hasher . write ( val . value . as_bytes ( ) ) ;
}
if let Some ( val ) = self . 0. get ( " N " ) {
hasher . write ( val . value . as_bytes ( ) ) ;
}
if let Some ( val ) = self . 0. get ( " EMAIL " ) {
hasher . write ( val . value . as_bytes ( ) ) ;
}
hasher . finish ( )
} ) ) ;
2019-10-16 14:55:49 +03:00
if let Some ( val ) = self . 0. remove ( " FN " ) {
card . set_name ( val . value ) ;
} else {
return Err ( MeliError ::new ( " FN entry missing in VCard. " ) ) ;
}
if let Some ( val ) = self . 0. remove ( " NICKNAME " ) {
card . set_additionalname ( val . value ) ;
}
if let Some ( val ) = self . 0. remove ( " BDAY " ) {
/* 4.3.4. DATE-AND-OR-TIME
Either a DATE - TIME , a DATE , or a TIME value . To allow unambiguous
interpretation , a stand - alone TIME value is always preceded by a " T " .
Examples for " date-and-or-time " :
19961022 T140000
- - 1022 T1400
- - - 22 T14
19850412
1985 - 04
1985
- - 0412
- - - 12
T102200
T1022
T10
T - 2200
T - - 00
T102200Z
T102200 - 0800
* /
2021-01-05 21:33:44 +02:00
card . birthday = crate ::datetime ::timestamp_from_string ( val . value . as_str ( ) , " %Y%m%d \0 " )
2020-05-06 18:46:38 +03:00
. unwrap_or_default ( ) ;
2019-10-16 14:55:49 +03:00
}
if let Some ( val ) = self . 0. remove ( " EMAIL " ) {
card . set_email ( val . value ) ;
}
if let Some ( val ) = self . 0. remove ( " URL " ) {
card . set_url ( val . value ) ;
}
if let Some ( val ) = self . 0. remove ( " KEY " ) {
card . set_key ( val . value ) ;
}
for ( k , v ) in self . 0. into_iter ( ) {
2019-11-27 01:37:27 +02:00
if k . eq_ignore_ascii_case ( " VERSION " ) | | k . eq_ignore_ascii_case ( " N " ) {
continue ;
}
2019-10-16 14:55:49 +03:00
card . set_extra_property ( & k , v . value ) ;
}
Ok ( card )
}
}
2019-11-27 01:37:27 +02:00
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 )
2021-09-12 14:33:00 +03:00
. map ( | mut card | {
2019-11-27 01:37:27 +02:00
Card ::set_external_resource ( & mut card , true ) ;
is_any_valid = true ;
2021-09-12 14:33:00 +03:00
card
2019-11-27 01:37:27 +02:00
} ) ,
) ;
}
}
}
}
}
for c in & ret {
if c . is_err ( ) {
debug! ( & c ) ;
}
}
2021-09-12 14:33:00 +03:00
if is_any_valid {
2019-11-27 01:37:27 +02:00
ret . retain ( Result ::is_ok ) ;
}
2021-09-12 14:33:00 +03:00
ret . into_iter ( ) . collect ::< Result < Vec < Card > > > ( )
2019-11-27 01:37:27 +02:00
}
2019-10-16 14:55:49 +03:00
#[ test ]
fn test_card ( ) {
let j = " BEGIN:VCARD \r \n VERSION:4.0 \r \n N:Gump;Forrest;;Mr.; \r \n FN:Forrest Gump \r \n ORG:Bubba Gump Shrimp Co. \r \n TITLE:Shrimp Man \r \n PHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif \r \n TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212 \r \n TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212 \r \n ADR;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 \n ADR;TYPE=HOME;LABEL= \" 42 Plantation St. \\ nBaytown \\ , LA 30314 \\ nUnited States of America \" :;;42 Plantation St.;Baytown;LA;30314;United States of America \r \n EMAIL:forrestgump@example.com \r \n REV:20080424T195243Z \r \n x-qq:21588891 \r \n END:VCARD \r \n " ;
println! ( " results = {:#?} " , CardDeserializer ::from_str ( j ) . unwrap ( ) ) ;
}