diff --git a/src/builder.rs b/src/builder.rs index 1bb3196..4a125fc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,8 +1,8 @@ //! Builder for creating vCards. //! use crate::{ - property::{DeliveryAddress, Gender, Kind, TextListProperty}, Date, DateTime, Uri, Vcard, + property::{DeliveryAddress, Gender, Kind, TextListProperty}, }; #[cfg(feature = "language-tags")] diff --git a/src/date_time.rs b/src/date_time.rs index cdcd92b..92aba7d 100644 --- a/src/date_time.rs +++ b/src/date_time.rs @@ -1,9 +1,9 @@ use crate::Error; use std::{fmt, str::FromStr}; -use time::{format_description::well_known::Rfc3339, OffsetDateTime}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; #[cfg(feature = "serde")] -use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay}; +use serde_with::{DeserializeFromStr, SerializeDisplay, serde_as}; /// Date and time that serializes to and from RFC3339. #[derive(Debug, Clone, PartialEq, Eq)] @@ -38,11 +38,7 @@ impl AsRef for DateTime { impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - self.0.format(&Rfc3339).map_err(|_| fmt::Error)? - ) + write!(f, "{}", self.0.format(&Rfc3339).map_err(|_| fmt::Error)?) } } diff --git a/src/helper.rs b/src/helper.rs index f93e744..b99932b 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,11 +1,11 @@ //! Utilities for parsing dates, times and primitive values. use std::fmt; use time::{ - format_description::{self, well_known::Iso8601}, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, + format_description::{self, well_known::Iso8601}, }; -use crate::{property::DateAndOrTime, DateTime, Error, Result}; +use crate::{DateTime, Error, Result, property::DateAndOrTime}; // UTC OFFSET @@ -155,9 +155,10 @@ pub fn parse_date(value: &str) -> Result { *val = "00"; } if let Some(val) = parts.get_mut(3) - && *val == "-" { - *val = "01"; - } + && *val == "-" + { + *val = "01"; + } let value = parts.join(""); do_parse_date(&value) @@ -279,11 +280,11 @@ pub(crate) fn format_date_time_list( /// Parse a timestamp. pub fn parse_timestamp(value: &str) -> Result { let offset_format = format_description::parse( - "[year][month][day]T[hour][minute][second][offset_hour sign:mandatory][offset_minute]", - )?; + "[year][month][day]T[hour][minute][second][offset_hour sign:mandatory][offset_minute]", + )?; let offset_format_hours = format_description::parse( - "[year][month][day]T[hour][minute][second][offset_hour sign:mandatory]", - )?; + "[year][month][day]T[hour][minute][second][offset_hour sign:mandatory]", + )?; let utc_format = format_description::parse( "[year][month][day]T[hour][minute][second]Z", )?; diff --git a/src/iter.rs b/src/iter.rs index d26c4c7..7e32c7f 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -1,7 +1,7 @@ //! Iterator for parsing vCards. use crate::{ - parser::{Token, VcardParser}, Error, Result, Vcard, + parser::{Token, VcardParser}, }; use std::ops::Range; diff --git a/src/parser.rs b/src/parser.rs index b0f412d..b6a0680 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -10,8 +10,8 @@ use language_tags::LanguageTag; use mime::Mime; use crate::{ - error::LexError, escape_control, helper::*, name::*, parameter::*, - property::*, unescape_value, Error, Result, Uri, Vcard, + Error, Result, Uri, Vcard, error::LexError, escape_control, helper::*, + name::*, parameter::*, property::*, unescape_value, }; type LexResult = std::result::Result; @@ -33,7 +33,9 @@ pub(crate) enum Token { #[token("GEO")] Geo, - #[regex("(?i:([a-z0-9-]+\\.)?(SOURCE|KIND|FN|N|NICKNAME|PHOTO|BDAY|ANNIVERSARY|GENDER|ADR|TEL|EMAIL|IMPP|LANG|TITLE|ROLE|LOGO|ORG|MEMBER|RELATED|CATEGORIES|NOTE|PRODID|REV|SOUND|UID|CLIENTPIDMAP|URL|KEY|FBURL|CALADRURI|CALURI|XML|VERSION|(X-[a-z0-9-]+)))")] + #[regex( + "(?i:([a-z0-9-]+\\.)?(SOURCE|KIND|FN|N|NICKNAME|PHOTO|BDAY|ANNIVERSARY|GENDER|ADR|TEL|EMAIL|IMPP|LANG|TITLE|ROLE|LOGO|ORG|MEMBER|RELATED|CATEGORIES|NOTE|PRODID|REV|SOUND|UID|CLIENTPIDMAP|URL|KEY|FBURL|CALADRURI|CALURI|XML|VERSION|(X-[a-z0-9-]+)))" + )] PropertyName, #[regex("(?i:x-[a-z0-9-]+)")] @@ -45,7 +47,9 @@ pub(crate) enum Token { #[token("\"")] DoubleQuote, - #[regex("(?i:LANGUAGE|VALUE|PREF|ALTID|PID|TYPE|MEDIATYPE|CALSCALE|SORT-AS|CHARSET|LABEL|ENCODING)")] + #[regex( + "(?i:LANGUAGE|VALUE|PREF|ALTID|PID|TYPE|MEDIATYPE|CALSCALE|SORT-AS|CHARSET|LABEL|ENCODING)" + )] ParameterKey, #[token("=")] @@ -170,9 +174,10 @@ impl<'s> VcardParser<'s> { )?; if let Err(e) = self.parse_property(lex, first, card) - && self.strict { - return Err(e); - } + && self.strict + { + return Err(e); + } } Ok(()) } @@ -282,7 +287,8 @@ impl<'s> VcardParser<'s> { if self.strict { let value: ValueType = value.parse()?; params.value = Some(value); - } else if let Ok(value) = value.parse::() + } else if let Ok(value) = + value.parse::() { params.value = Some(value); } @@ -399,7 +405,7 @@ impl<'s> VcardParser<'s> { _ => { return Err(Error::UnknownParameter( parameter_name.to_string(), - )) + )); } } } @@ -710,7 +716,7 @@ impl<'s> VcardParser<'s> { return Err(Error::UnsupportedValueType( value_type.to_string(), upper_name, - )) + )); } } } else { @@ -850,9 +856,10 @@ impl<'s> VcardParser<'s> { } CLIENTPIDMAP => { if let Some(params) = ¶meters - && params.pid.is_some() { - return Err(Error::ClientPidMapPidNotAllowed); - } + && params.pid.is_some() + { + return Err(Error::ClientPidMapPidNotAllowed); + } let value: ClientPidMap = value.as_ref().parse()?; card.client_pid_map.push(ClientPidMapProperty { diff --git a/src/property.rs b/src/property.rs index c8dfe26..7b39a64 100644 --- a/src/property.rs +++ b/src/property.rs @@ -13,13 +13,13 @@ use language_tags::LanguageTag; use serde::{Deserialize, Serialize}; #[cfg(feature = "serde")] -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::{DisplayFromStr, serde_as}; #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::{ - escape_value, + Date, DateTime, Error, Result, Uri, escape_value, helper::{ format_date, format_date_and_or_time_list, format_date_list, format_date_time, format_date_time_list, format_float_list, @@ -28,7 +28,6 @@ use crate::{ parse_date_time, parse_time, parse_utc_offset, }, parameter::Parameters, - Date, DateTime, Error, Result, Uri, }; const INDIVIDUAL: &str = "individual"; diff --git a/src/vcard.rs b/src/vcard.rs index 736561d..c7a68c2 100644 --- a/src/vcard.rs +++ b/src/vcard.rs @@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "zeroize")] use zeroize::{Zeroize, ZeroizeOnDrop}; -use base64::{engine::general_purpose, Engine}; +use base64::{Engine, engine::general_purpose}; -use crate::{iter, property::*, Error, Result}; +use crate::{Error, Result, iter, property::*}; /// The vCard type. #[derive(Debug, Default, Eq, PartialEq, Clone)] @@ -299,21 +299,20 @@ impl Vcard { for photo in self.photo.iter() { if let TextOrUriProperty::Text(prop) = photo && let Some(params) = &prop.parameters - && let (Some(types), Some(extensions)) = - (¶ms.types, ¶ms.extensions) - && let ( - Some(TypeParameter::Extension(value)), - Some((name, values)), - ) = (types.first(), extensions.first()) - && name.to_uppercase() == "ENCODING" - && values.first() == Some(&"b".to_string()) - && &value.to_uppercase() == "JPEG" - { - let encoded = &prop.value; - let buffer = general_purpose::STANDARD - .decode(encoded)?; - jpegs.push(buffer); - } + && let (Some(types), Some(extensions)) = + (¶ms.types, ¶ms.extensions) + && let ( + Some(TypeParameter::Extension(value)), + Some((name, values)), + ) = (types.first(), extensions.first()) + && name.to_uppercase() == "ENCODING" + && values.first() == Some(&"b".to_string()) + && &value.to_uppercase() == "JPEG" + { + let encoded = &prop.value; + let buffer = general_purpose::STANDARD.decode(encoded)?; + jpegs.push(buffer); + } } Ok(jpegs) } @@ -492,8 +491,11 @@ fn fold_line(line: String, wrap_at: usize) -> String { let mut folded_line = String::new(); for grapheme in UnicodeSegmentation::graphemes(&line[..], true) { length += grapheme.len(); - if length % wrap_at == 0 { + if length > wrap_at { folded_line.push_str("\r\n "); + // actual length of the next line + // including the leading space + length = 1 + grapheme.len(); } folded_line.push_str(grapheme); } diff --git a/tests/errors.rs b/tests/errors.rs index 6db30f7..1546afb 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -1,7 +1,7 @@ mod test_helpers; use anyhow::Result; -use vcard4::{helper::*, parameter::*, parse, property::ClientPidMap, Error}; +use vcard4::{Error, helper::*, parameter::*, parse, property::ClientPidMap}; #[test] fn error_empty() -> Result<()> { diff --git a/tests/general.rs b/tests/general.rs index 7fcfec4..ba0715f 100644 --- a/tests/general.rs +++ b/tests/general.rs @@ -62,19 +62,19 @@ END:VCARD"#; assert_round_trip(&card)?; /* - let input = r#"BEGIN:VCARD + let input = r#"BEGIN:VCARD VERSION:4.0 KIND:org FN:ABC Marketing ORG:ABC\, Inc.;North American Division;Marketing END:VCARD"#; - let mut vcards = parse(input)?; - assert_eq!(1, vcards.len()); - let card = vcards.remove(0); + let mut vcards = parse(input)?; + assert_eq!(1, vcards.len()); + let card = vcards.remove(0); - assert_eq!(Kind::Org, card.kind.as_ref().unwrap().value); - assert_round_trip(&card)?; - */ + assert_eq!(Kind::Org, card.kind.as_ref().unwrap().value); + assert_round_trip(&card)?; + */ Ok(()) } diff --git a/tests/identification.rs b/tests/identification.rs index 52ba990..327d10d 100644 --- a/tests/identification.rs +++ b/tests/identification.rs @@ -80,10 +80,12 @@ END:VCARD"#; } if let TextOrUriProperty::Uri(photo2) = card.photo.get(1).unwrap() { - assert!(photo2 - .value - .to_string() - .starts_with("data:image/jpeg;base64,")); + assert!( + photo2 + .value + .to_string() + .starts_with("data:image/jpeg;base64,") + ); assert!(photo2.value.to_string().ends_with("TeXN0")); } else { panic!("expecting URI property"); diff --git a/tests/organizational.rs b/tests/organizational.rs index 8881250..f7272ec 100644 --- a/tests/organizational.rs +++ b/tests/organizational.rs @@ -2,7 +2,7 @@ mod test_helpers; use anyhow::Result; use test_helpers::assert_round_trip; -use vcard4::{parameter::TypeParameter, parse, property::*, Uri}; +use vcard4::{Uri, parameter::TypeParameter, parse, property::*}; #[test] fn organizational_title() -> Result<()> { @@ -61,10 +61,12 @@ END:VCARD"#; &logo1.value.to_string() ); - assert!(logo2 - .value - .to_string() - .starts_with("data:image/jpeg;base64,")); + assert!( + logo2 + .value + .to_string() + .starts_with("data:image/jpeg;base64,") + ); assert!(logo2.value.to_string().ends_with("TeXN0")); assert_round_trip(&card)?; diff --git a/tests/parameters.rs b/tests/parameters.rs index 043f77a..d9718f8 100644 --- a/tests/parameters.rs +++ b/tests/parameters.rs @@ -3,6 +3,7 @@ mod test_helpers; use anyhow::Result; use vcard4::{ + Error, helper::parse_utc_offset, parameter::{ Pid, RelatedType, TelephoneType, TimeZoneParameter, TypeParameter, @@ -10,7 +11,6 @@ use vcard4::{ }, parse, parse_loose, property::{AnyProperty, TextOrUriProperty}, - Error, }; use test_helpers::{assert_language, assert_media_type, assert_round_trip}; diff --git a/tests/test_helpers.rs b/tests/test_helpers.rs index cabfaae..c1b2976 100644 --- a/tests/test_helpers.rs +++ b/tests/test_helpers.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use vcard4::{parameter::Parameters, parse, Vcard}; +use vcard4::{Vcard, parameter::Parameters, parse}; #[cfg(feature = "language-tags")] use language_tags::LanguageTag; diff --git a/tests/unicode_fold_line.rs b/tests/unicode_fold_line.rs new file mode 100644 index 0000000..3d3245e --- /dev/null +++ b/tests/unicode_fold_line.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use vcard4::Vcard; + +const EXPECTED: &str = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:12345678901234567890123456789012345678901234567890123456789012345678901\r\n ö123456789012345678901234567890123456789012345678901234567890123456789012\r\n 34567890123456789012345678901234567890123456789012345678901234567890123456\r\n 78901234567890123456789012345678901234567890123456789012345678901234567890\r\n 123456789012345678901234567890123456789012345678901234567890\r\nEND:VCARD\r\n"; + +#[test] +fn unicode_fold_line() -> Result<()> { + let line70 = "1234567890".repeat(7); + let line76 = format!("{line70}1ö{line70}{line70}{line70}{line70}"); + let mut card: Vcard = Default::default(); + card.formatted_name = vec![line76.into()]; + let result = card.to_string(); + assert_eq!(result, EXPECTED); + Ok(()) +}