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/>.
* /
2022-09-26 18:04:53 +03:00
//! # vCard format
//!
//! This module implements the standards:
//!
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
2023-04-30 19:39:41 +03:00
use std ::{ collections ::HashMap , convert ::TryInto } ;
2019-10-20 11:06:26 +03:00
use super ::* ;
2023-04-30 19:39:41 +03:00
use crate ::{
2024-02-04 14:50:20 +02:00
error ::{ Error , ErrorKind , Result } ,
2023-06-18 22:28:40 +03:00
utils ::parsec ::{ match_literal_anycase , one_or_more , peek , prefix , take_until , Parser } ,
2023-04-30 19:39:41 +03:00
} ;
2019-10-16 14:55:49 +03:00
/* Supported vcard versions */
2023-08-11 13:16:47 +03:00
pub trait VCardVersion : std ::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 { }
2022-09-26 18:04:53 +03:00
/// Version 4 <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 { }
2022-09-26 18:04:53 +03:00
/// <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 ;
2021-10-24 14:31:22 +03:00
const HEADER_CRLF : & str = " BEGIN:VCARD \r \n " ; //VERSION:4.0\r\n";
const FOOTER_CRLF : & str = " END:VCARD \r \n " ;
const HEADER_LF : & str = " BEGIN:VCARD \n " ; //VERSION:4.0\n";
const FOOTER_LF : & str = " END:VCARD \n " ;
const HEADER : & str = " BEGIN:VCARD " ; //VERSION:4.0";
const FOOTER : & str = " END:VCARD " ;
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 > ,
)
}
}
2023-12-09 18:47:16 +02:00
#[ derive(Clone, Debug, Default) ]
2019-10-16 14:55:49 +03:00
pub struct ContentLine {
group : Option < String > ,
params : Vec < String > ,
value : String ,
}
impl CardDeserializer {
2023-07-01 16:34:06 +03:00
pub fn try_from_str ( mut input : & str ) -> Result < VCard < impl VCardVersion > > {
2024-02-04 14:50:20 +02:00
input = if ( ! input . starts_with ( HEADER_CRLF )
| | ( ! input . ends_with ( FOOTER_CRLF ) & & ! input . ends_with ( FOOTER ) ) )
& & ( ! input . starts_with ( HEADER_LF )
| | ( ! input . ends_with ( FOOTER_LF ) & & ! input . ends_with ( FOOTER ) ) )
2021-10-24 14:31:22 +03:00
{
2023-04-30 19:39:41 +03:00
return Err ( Error ::new ( format! (
" Error while parsing vcard: input does not start or end with correct header and \
2024-02-04 14:50:20 +02:00
footer : { :? } " ,
2023-04-30 19:39:41 +03:00
input
2024-02-04 14:50:20 +02:00
) )
. set_kind ( ErrorKind ::ValueError )
. set_details (
" vcard file entries are expected to start with a `BEGIN:VCARD` line and end with \
a ` END :VCARD ` line . " ,
) ) ;
2021-10-24 14:31:22 +03:00
} else if input . starts_with ( HEADER_CRLF ) {
& input [ HEADER_CRLF . len ( ) .. input . len ( ) - FOOTER_CRLF . len ( ) ]
2019-10-16 14:55:49 +03:00
} else {
2021-10-24 14:31:22 +03:00
& input [ HEADER_LF . len ( ) .. input . len ( ) - FOOTER_LF . len ( ) ]
2019-10-16 14:55:49 +03:00
} ;
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 {
2022-12-08 22:20:05 +02:00
return Err ( Error ::new ( format! (
2019-10-16 14:55:49 +03:00
" Error while parsing vcard: error at line {}, no colon. {:?} " ,
l , el
2024-02-04 14:50:20 +02:00
) )
. set_kind ( ErrorKind ::ValueError ) ) ;
2019-10-16 14:55:49 +03:00
}
if name . is_empty ( ) {
2022-12-08 22:20:05 +02:00
return Err ( Error ::new ( format! (
2019-10-16 14:55:49 +03:00
" Error while parsing vcard: error at line {}, no name for content line. {:?} " ,
l , el
2024-02-04 14:50:20 +02:00
) )
. set_kind ( ErrorKind ::ValueError ) ) ;
2019-10-16 14:55:49 +03:00
}
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 > {
2022-12-08 22:20:05 +02:00
type Error = crate ::error ::Error ;
2019-10-16 14:55:49 +03:00
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 ( {
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 {
2024-02-04 14:50:20 +02:00
return Err ( Error ::new ( " FN entry missing in VCard. " ) . set_kind ( ErrorKind ::ValueError ) ) ;
2019-10-16 14:55:49 +03:00
}
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
* /
2023-06-18 22:28:40 +03:00
card . birthday =
crate ::utils ::datetime ::timestamp_from_string ( val . value . as_str ( ) , " %Y%m%d \0 " )
. 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 ) ;
2023-07-01 16:34:06 +03:00
println! ( " {:?} " , CardDeserializer ::try_from_str ( s ) ) ;
2019-11-27 01:37:27 +02:00
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 ) ? ;
2021-10-24 14:17:49 +03:00
match parse_card ( ) . parse ( contents . as_str ( ) ) {
Ok ( ( _ , c ) ) = > {
for s in c {
ret . push (
2023-07-01 16:34:06 +03:00
CardDeserializer ::try_from_str ( s )
2021-10-24 14:17:49 +03:00
. and_then ( TryInto ::try_into )
. map ( | mut card | {
Card ::set_external_resource ( & mut card , true ) ;
is_any_valid = true ;
card
} ) ,
) ;
}
}
Err ( err ) = > {
2023-05-01 16:22:35 +03:00
log ::warn! ( " Could not parse vcard from {}: {} " , f . display ( ) , err ) ;
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 " ;
2023-07-01 16:34:06 +03:00
println! (
" results = {:#?} " ,
CardDeserializer ::try_from_str ( j ) . unwrap ( )
) ;
2021-10-24 14:31:22 +03:00
let j = " BEGIN:VCARD \n VERSION:4.0 \n N:Gump;Forrest;;Mr.; \n FN:Forrest Gump \n ORG:Bubba Gump Shrimp Co. \n TITLE:Shrimp Man \n PHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif \n TEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212 \n TEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212 \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 \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 \n EMAIL:forrestgump@example.com \n REV:20080424T195243Z \n x-qq:21588891 \n END:VCARD \n " ;
2023-07-01 16:34:06 +03:00
println! (
" results = {:#?} " ,
CardDeserializer ::try_from_str ( j ) . unwrap ( )
) ;
2019-10-16 14:55:49 +03:00
}