Skip to main content

vcard/tree/codec/
decode.rs

1//! # Decode (syntax to model)
2//!
3//! The read side of the structural bridge: project a raw syntax tree onto the
4//! decoded model. A [`VcardValueNode`] decodes its components, a
5//! [`VcardParamNode`] decodes into a [`VcardParam`], a [`VcardLine`] into a
6//! [`VcardProp`], and a [`VcardCst`] into a whole [`Vcard`].
7//!
8//! A property's value kind is resolved through its spec, not a name match:
9//! [`VcardLine::decode`] maps the name to a [`VcardPropKind`], asks the spec for
10//! the in-force value kind (version plus any declared `VALUE`), then routes to
11//! that kind's decoder. Value escapes are resolved by the sibling
12//! [`unescape`](crate::tree::codec::unescape) codec; content transfer encodings
13//! (`QUOTED-PRINTABLE`, `BASE64`) and `CHARSET` are left to the feature helpers.
14
15use 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    /// Decode the whole card into the semantic [`Vcard`] model.
49    pub fn decode(&self) -> Vcard<'_> {
50        let version = self.version();
51
52        // NOTE: VERSION is held as the card's indicator, not as a free
53        // property.
54        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    /// Decode the line into a typed property. A known property dispatches its
70    /// value through the spec (see `decode_value`); an unknown one keeps its
71    /// raw components so it round-trips.
72    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    /// Decode a known property's value through its spec: resolve the in-force
89    /// value kind from the card version and any declared `VALUE`, then run that
90    /// kind's decoder over the value node. Shared by the whole-card decode and
91    /// the version-specific lenses (`GEO`, the binary props).
92    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    /// The value kind named by this line's `VALUE` parameter, if any. Only the
103    /// declared kind selects the value type; `ENCODING` / `CHARSET` transform
104    /// the text and stay in the codec.
105    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    /// Whether the line declares the `QUOTED-PRINTABLE` encoding, as an
114    /// `ENCODING=` parameter or a bare token (the 2.1 short form).
115    #[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    /// The value of this line's `CHARSET` parameter, if any.
121    #[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
131/// Decode a value node as the given value kind, routing to that value type's
132/// [`VcardCodec`]. No version is needed: the one version-specific shape (the
133/// `GEO` pair separator) is resolved from the node's escaper inside its codec.
134fn 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    /// Decode the parameter into a typed parameter, dispatching on the name.
157    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    /// The parameter's first value, decoded (empty when there is none).
194    fn scalar(&self) -> Cow<'_, str> {
195        self.values
196            .first()
197            .map(|v| unescape(v.get()))
198            .unwrap_or(Cow::Borrowed(""))
199    }
200
201    /// The parameter's values, decoded.
202    fn list(&self) -> Vec<Cow<'_, str>> {
203        self.values.iter().map(|v| unescape(v.get())).collect()
204    }
205}
206
207/// Whether a parameter is `ENCODING=QUOTED-PRINTABLE` or the bare 2.1 token.
208#[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        // NOTE: BDAY defaults to a date, but VALUE=text forces the text
252        // reading.
253        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        // NOTE: A 2.1 PHOTO is inline base64 by default, but a plain URI when
263        // the line declares VALUE=uri (the old is_uri_reference path, now
264        // spec-derived).
265        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        // NOTE: 2.1: GEO is a comma pair; PHOTO is inline base64.
278        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        // NOTE: 3.0: GEO is a semicolon pair.
300        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        // NOTE: 4.0: GEO is a URI; its comma is literal, so it is not
312        // truncated.
313        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        // NOTE: The version-specific value shapes that decode() resolves must
327        // come back identically through the typed lens, not as a version-blind
328        // URI.
329        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        // NOTE: Core transforms no content: the `=XX` octets stay in the value
380        // and the ENCODING param is kept, so a consumer can decode via the
381        // `quoted-printable` feature helper.
382        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        // NOTE: 2.1: only `\;` is an escape; `\n` stays a literal backslash-n.
407        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        // NOTE: 3.0: `\n` is a newline.
416        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        // NOTE: 2.1 in-place edit escapes only `;`, leaving `,` literal.
425        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}