1use alloc::{borrow::Cow, vec::Vec};
16
17use crate::{
18 param::{VcardParam, VcardParamKind},
19 prop::{VcardProp, VcardPropKind, VcardPropName},
20 tree::{
21 codec::{VcardCodec, unescape::unescape},
22 cst::VcardCst,
23 line::VcardLine,
24 param::node::VcardParamNode,
25 prop::spec::prop_spec,
26 value::node::VcardValueNode,
27 },
28 value::{
29 VcardValue, VcardValueKind, VcardValueUnknown,
30 adr::VcardAdr,
31 binary::VcardBinary,
32 client_pid_map::VcardClientPidMap,
33 datetime::{VcardDateAndOrTime, VcardTimestamp},
34 gender::VcardGender,
35 geo::VcardGeo,
36 language::VcardLanguageTag,
37 n::VcardN,
38 org::VcardOrg,
39 text::{VcardText, VcardTextList},
40 uri::VcardUri,
41 utc_offset::VcardUtcOffset,
42 },
43 vcard::Vcard,
44 version::VcardVersion,
45};
46
47impl VcardCst<'_> {
48 pub fn decode(&self) -> Vcard<'_> {
50 let version = self.version();
51
52 let properties = self
55 .props
56 .iter()
57 .filter(|line| !line.name.get().eq_ignore_ascii_case("VERSION"))
58 .map(|line| line.decode(version))
59 .collect();
60
61 Vcard {
62 version,
63 properties,
64 }
65 }
66}
67
68impl VcardLine<'_> {
69 pub fn decode(&self, version: VcardVersion) -> VcardProp<'_> {
73 let name = self.name.get();
74 let params = self.params.iter().map(VcardParamNode::decode).collect();
75
76 let value = match name.parse::<VcardPropKind>() {
77 Ok(prop) => self.decode_value(prop, version),
78 Err(_) => VcardValue::Unknown(VcardValueUnknown::decode(&self.value)),
79 };
80
81 VcardProp {
82 name: VcardPropName::from(name),
83 params,
84 value,
85 }
86 }
87
88 pub(crate) fn decode_value(
93 &self,
94 prop: VcardPropKind,
95 version: VcardVersion,
96 ) -> VcardValue<'_> {
97 let declared = self.declared_value_kind();
98 let kind = (prop_spec(prop).value)(version, declared);
99 decode_value_kind(kind, &self.value)
100 }
101
102 fn declared_value_kind(&self) -> Option<VcardValueKind> {
106 self.params
107 .iter()
108 .find(|param| matches!(param.name.get().parse(), Ok(VcardParamKind::Value)))
109 .and_then(|param| param.values.first())
110 .and_then(|value| value.get().parse::<VcardValueKind>().ok())
111 }
112
113 #[cfg(feature = "quoted-printable")]
116 pub(crate) fn is_quoted_printable(&self) -> bool {
117 self.params.iter().any(param_is_quoted_printable)
118 }
119
120 #[cfg(feature = "encoding")]
122 pub(crate) fn charset_label(&self) -> Option<&str> {
123 self.params
124 .iter()
125 .find(|param| param.name.get().eq_ignore_ascii_case("CHARSET"))
126 .and_then(|param| param.values.first())
127 .map(|value| value.get())
128 }
129}
130
131fn decode_value_kind<'v>(kind: VcardValueKind, node: &'v VcardValueNode<'_>) -> VcardValue<'v> {
135 match kind {
136 VcardValueKind::Text => VcardValue::Text(VcardText::decode(node)),
137 VcardValueKind::TextList => VcardValue::TextList(VcardTextList::decode(node)),
138 VcardValueKind::Uri => VcardValue::Uri(VcardUri::decode(node)),
139 VcardValueKind::DateAndOrTime => {
140 VcardValue::DateAndOrTime(VcardDateAndOrTime::decode(node))
141 }
142 VcardValueKind::Timestamp => VcardValue::Timestamp(VcardTimestamp::decode(node)),
143 VcardValueKind::LanguageTag => VcardValue::LanguageTag(VcardLanguageTag::decode(node)),
144 VcardValueKind::UtcOffset => VcardValue::UtcOffset(VcardUtcOffset::decode(node)),
145 VcardValueKind::N => VcardValue::N(VcardN::decode(node)),
146 VcardValueKind::Adr => VcardValue::Adr(VcardAdr::decode(node)),
147 VcardValueKind::Gender => VcardValue::Gender(VcardGender::decode(node)),
148 VcardValueKind::Org => VcardValue::Org(VcardOrg::decode(node)),
149 VcardValueKind::ClientPidMap => VcardValue::ClientPidMap(VcardClientPidMap::decode(node)),
150 VcardValueKind::Geo => VcardValue::Geo(VcardGeo::decode(node)),
151 VcardValueKind::Binary => VcardValue::Binary(VcardBinary::decode(node)),
152 }
153}
154
155impl VcardParamNode<'_> {
156 pub fn decode(&self) -> VcardParam<'_> {
158 let Ok(kind) = self.name.get().parse::<VcardParamKind>() else {
159 return VcardParam::Unknown {
160 name: unescape(self.name.get()),
161 values: self.list(),
162 };
163 };
164
165 match kind {
166 VcardParamKind::Language => VcardParam::Language(self.scalar()),
167 VcardParamKind::Charset => VcardParam::Charset(self.scalar()),
168 VcardParamKind::Encoding => VcardParam::Encoding(self.scalar()),
169 VcardParamKind::Value => VcardParam::Value(self.scalar()),
170 VcardParamKind::Pref => VcardParam::Pref(self.scalar()),
171 VcardParamKind::AltId => VcardParam::AltId(self.scalar()),
172 VcardParamKind::Pid => VcardParam::Pid(self.list()),
173 VcardParamKind::Type => VcardParam::Type(self.list()),
174 VcardParamKind::MediaType => VcardParam::MediaType(self.scalar()),
175 VcardParamKind::CalScale => VcardParam::CalScale(self.scalar()),
176 VcardParamKind::SortAs => VcardParam::SortAs(self.list()),
177 VcardParamKind::Geo => VcardParam::Geo(self.scalar()),
178 VcardParamKind::Tz => VcardParam::Tz(self.scalar()),
179 VcardParamKind::Label => VcardParam::Label(self.scalar()),
180 VcardParamKind::Author => VcardParam::Author(self.scalar()),
181 VcardParamKind::AuthorName => VcardParam::AuthorName(self.scalar()),
182 VcardParamKind::Created => VcardParam::Created(self.scalar()),
183 VcardParamKind::Derived => VcardParam::Derived(self.scalar()),
184 VcardParamKind::Jsptr => VcardParam::Jsptr(self.scalar()),
185 VcardParamKind::Phonetic => VcardParam::Phonetic(self.scalar()),
186 VcardParamKind::PropId => VcardParam::PropId(self.scalar()),
187 VcardParamKind::Script => VcardParam::Script(self.scalar()),
188 VcardParamKind::ServiceType => VcardParam::ServiceType(self.scalar()),
189 VcardParamKind::Username => VcardParam::Username(self.scalar()),
190 }
191 }
192
193 fn scalar(&self) -> Cow<'_, str> {
195 self.values
196 .first()
197 .map(|v| unescape(v.get()))
198 .unwrap_or(Cow::Borrowed(""))
199 }
200
201 fn list(&self) -> Vec<Cow<'_, str>> {
203 self.values.iter().map(|v| unescape(v.get())).collect()
204 }
205}
206
207#[cfg(feature = "quoted-printable")]
209fn param_is_quoted_printable(param: &VcardParamNode<'_>) -> bool {
210 let name = param.name.get();
211
212 (name.eq_ignore_ascii_case("ENCODING")
213 && param
214 .values
215 .iter()
216 .any(|v| v.get().eq_ignore_ascii_case("QUOTED-PRINTABLE")))
217 || (param.values.is_empty() && name.eq_ignore_ascii_case("QUOTED-PRINTABLE"))
218}
219
220#[cfg(test)]
221mod tests {
222 use alloc::{borrow::Cow, string::ToString, vec};
223
224 use crate::{
225 param::VcardParam,
226 tree::{codec::VcardCodec, cst::VcardCst, value::node::VcardValueNode},
227 value::{
228 VcardValue, binary::VcardBinary, geo::VcardGeo, n::VcardN, text::VcardText,
229 uri::VcardUri,
230 },
231 };
232
233 #[test]
234 fn types_charset_and_encoding_params() {
235 let input = concat!(
236 "BEGIN:VCARD\r\n",
237 "VERSION:2.1\r\n",
238 "PHOTO;CHARSET=UTF-8;ENCODING=BASE64:Zm9v\r\n",
239 "END:VCARD\r\n",
240 );
241 let cst = VcardCst::parse(input).unwrap();
242 let card = cst.decode();
243 let params = &card.properties[0].params;
244
245 assert!(params.contains(&VcardParam::Charset(Cow::Borrowed("UTF-8"))));
246 assert!(params.contains(&VcardParam::Encoding(Cow::Borrowed("BASE64"))));
247 }
248
249 #[test]
250 fn the_value_param_selects_the_value_kind() {
251 let cst = VcardCst::parse(
254 "BEGIN:VCARD\r\nVERSION:4.0\r\nBDAY;VALUE=text:circa 1800\r\nEND:VCARD\r\n",
255 )
256 .unwrap();
257 assert_eq!(
258 cst.decode().properties[0].value,
259 VcardValue::Text(VcardText(Cow::Borrowed("circa 1800"))),
260 );
261
262 let cst = VcardCst::parse(
266 "BEGIN:VCARD\r\nVERSION:2.1\r\nPHOTO;VALUE=URI:http://x/p.png\r\nEND:VCARD\r\n",
267 )
268 .unwrap();
269 assert_eq!(
270 cst.decode().properties[0].value,
271 VcardValue::Uri(VcardUri(Cow::Borrowed("http://x/p.png"))),
272 );
273 }
274
275 #[test]
276 fn branches_geo_and_binary_on_version() {
277 let v21 = concat!(
279 "BEGIN:VCARD\r\n",
280 "VERSION:2.1\r\n",
281 "GEO:37.0,-122.0\r\n",
282 "PHOTO;ENCODING=BASE64:Zm9v\r\n",
283 "END:VCARD\r\n",
284 );
285 let cst = VcardCst::parse(v21).unwrap();
286 let card = cst.decode();
287 assert_eq!(
288 card.properties[0].value,
289 VcardValue::Geo(VcardGeo {
290 latitude: Cow::Borrowed("37.0"),
291 longitude: Cow::Borrowed("-122.0"),
292 }),
293 );
294 assert_eq!(
295 card.properties[1].value,
296 VcardValue::Binary(VcardBinary::Base64(Cow::Borrowed("Zm9v"))),
297 );
298
299 let v30 = "BEGIN:VCARD\r\nVERSION:3.0\r\nGEO:37.0;-122.0\r\nEND:VCARD\r\n";
301 let cst = VcardCst::parse(v30).unwrap();
302 let card = cst.decode();
303 assert_eq!(
304 card.properties[0].value,
305 VcardValue::Geo(VcardGeo {
306 latitude: Cow::Borrowed("37.0"),
307 longitude: Cow::Borrowed("-122.0"),
308 }),
309 );
310
311 let v40 = "BEGIN:VCARD\r\nVERSION:4.0\r\nGEO:geo:37.0,-122.0\r\nEND:VCARD\r\n";
314 let cst = VcardCst::parse(v40).unwrap();
315 let card = cst.decode();
316 assert_eq!(
317 card.properties[0].value,
318 VcardValue::Uri(VcardUri(Cow::Borrowed("geo:37.0,-122.0"))),
319 );
320 }
321
322 #[test]
323 fn geo_and_binary_lenses_agree_with_whole_card_decode() {
324 use crate::tree::prop::{geo::GEO, photo::PHOTO};
325
326 for input in [
330 concat!(
331 "BEGIN:VCARD\r\n",
332 "VERSION:2.1\r\n",
333 "GEO:37.0,-122.0\r\n",
334 "PHOTO;ENCODING=BASE64:Zm9v\r\n",
335 "END:VCARD\r\n",
336 ),
337 concat!(
338 "BEGIN:VCARD\r\n",
339 "VERSION:4.0\r\n",
340 "GEO:geo:37.0,-122.0\r\n",
341 "PHOTO:https://example.com/p.png\r\n",
342 "END:VCARD\r\n",
343 ),
344 ] {
345 let cst = VcardCst::parse(input).unwrap();
346 let card = cst.decode();
347
348 assert_eq!(cst.prop::<GEO>(), Some(card.properties[0].value.clone()));
349 assert_eq!(cst.prop::<PHOTO>(), Some(card.properties[1].value.clone()));
350 }
351 }
352
353 #[test]
354 fn types_legacy_text_properties_instead_of_unknown() {
355 let input = concat!(
356 "BEGIN:VCARD\r\n",
357 "VERSION:3.0\r\n",
358 "LABEL:123 Main St\r\n",
359 "NAME:Acme\r\n",
360 "END:VCARD\r\n",
361 );
362 let cst = VcardCst::parse(input).unwrap();
363 let card = cst.decode();
364
365 assert_eq!(
366 card.properties[0].value,
367 VcardValue::Text(VcardText(Cow::Borrowed("123 Main St"))),
368 );
369 assert_eq!(
370 card.properties[1].value,
371 VcardValue::Text(VcardText(Cow::Borrowed("Acme"))),
372 );
373 }
374
375 #[test]
376 fn keeps_a_quoted_printable_value_and_its_encoding_param_undecoded() {
377 use crate::param::VcardParam;
378
379 let input = concat!(
383 "BEGIN:VCARD\r\n",
384 "VERSION:2.1\r\n",
385 "NOTE;ENCODING=QUOTED-PRINTABLE:caf=C3=A9\r\n",
386 "END:VCARD\r\n",
387 );
388 let cst = VcardCst::parse(input).unwrap();
389 let card = cst.decode();
390 let prop = &card.properties[0];
391
392 assert_eq!(
393 prop.value,
394 VcardValue::Text(VcardText(Cow::Borrowed("caf=C3=A9"))),
395 );
396 assert!(
397 prop.params
398 .contains(&VcardParam::Encoding(Cow::Borrowed("QUOTED-PRINTABLE"))),
399 );
400 }
401
402 #[test]
403 fn applies_version_specific_escaping() {
404 use crate::tree::prop::note::NOTE;
405
406 let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:2.1\r\nNOTE:a\\nb\\;c\r\nEND:VCARD\r\n")
408 .unwrap();
409 let card = cst.decode();
410 assert_eq!(
411 card.properties[0].value,
412 VcardValue::Text(VcardText(Cow::Borrowed("a\\nb;c"))),
413 );
414
415 let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:3.0\r\nNOTE:a\\nb\\;c\r\nEND:VCARD\r\n")
417 .unwrap();
418 let card = cst.decode();
419 assert_eq!(
420 card.properties[0].value,
421 VcardValue::Text(VcardText(Cow::Borrowed("a\nb;c"))),
422 );
423
424 let mut card =
426 VcardCst::parse("BEGIN:VCARD\r\nVERSION:2.1\r\nNOTE:x\r\nEND:VCARD\r\n").unwrap();
427 card.prop_mut::<NOTE>().unwrap().set_text("a,b;c");
428 assert!(
429 card.to_string().contains("NOTE:a,b\\;c\r\n"),
430 "{}",
431 card.to_string(),
432 );
433 }
434
435 #[test]
436 fn decodes_components_into_scalar_and_list() {
437 let node = VcardValueNode::parse(b"a,b;c");
438 assert_eq!(
439 node.decode_at(0),
440 vec![Cow::Borrowed("a"), Cow::Borrowed("b")],
441 );
442 assert_eq!(node.decode_scalar_at(1), Cow::Borrowed("c"));
443 assert_eq!(node.decode_scalar_at(9), Cow::Borrowed(""));
444 }
445
446 #[test]
447 fn decodes_the_structured_n_value() {
448 let node = VcardValueNode::parse(b"Doe;John;;Dr.;");
449 let n = VcardN::decode(&node);
450 assert_eq!(n.family, vec![Cow::Borrowed("Doe")]);
451 assert_eq!(n.given, vec![Cow::Borrowed("John")]);
452 assert_eq!(n.suffixes, vec![Cow::Borrowed("")]);
453 }
454}