Skip to main content

ical/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.
5//!
6//! A [`IcalValueNode`] decodes its components, a [`IcalParamNode`] decodes
7//! into an [`IcalParam`], an [`IcalLine`] decodes into an [`IcalProp`], and an
8//! [`IcalCst`] decodes into a whole [`Ical`], recursively, walking every
9//! nested component.
10//!
11//! A property's value kind is resolved through its spec, not a name match:
12//! [`IcalLine::decode`] maps the name to an [`IcalPropKind`], asks the spec
13//! for the in-force value kind (version plus any declared `VALUE`), then
14//! routes to that kind's decoder.
15//!
16//! The parameter name dispatch is the match in [`IcalParamNode::decode`].
17
18use alloc::{borrow::Cow, vec::Vec};
19
20use crate::{
21    component::{IcalComponent, IcalComponentName},
22    ical::Ical,
23    param::{IcalParam, IcalParamKind},
24    prop::{IcalProp, IcalPropKind, IcalPropName, spec::prop_spec},
25    tree::{
26        codec::{Codec, unescape::unescape_param},
27        cst::{IcalCst, IcalItem},
28        line::IcalLine,
29        param::node::IcalParamNode,
30        value::node::IcalValueNode,
31    },
32    value::{
33        IcalUnknownValue, IcalValue, IcalValueKind,
34        binary::IcalBinary,
35        boolean::IcalBoolean,
36        cal_address::IcalCalAddress,
37        datetime::{IcalDate, IcalDateTime, IcalDateTimeList, IcalTime},
38        duration::IcalDuration,
39        float::IcalFloat,
40        geo::IcalGeo,
41        integer::IcalInteger,
42        period::IcalPeriod,
43        recur::IcalRecur,
44        request_status::IcalRequestStatus,
45        text::{IcalText, IcalTextList},
46        uri::IcalUri,
47        utc_offset::IcalUtcOffset,
48    },
49    version::IcalVersion,
50};
51
52impl IcalCst<'_> {
53    /// Decode the whole calendar into the semantic [`Ical`] model. `VERSION` is
54    /// held as the calendar's indicator, not as a free property.
55    pub fn decode(&self) -> Ical<'_> {
56        let version = self.version();
57        let mut props = Vec::new();
58        let mut components = Vec::new();
59
60        for item in &self.items {
61            match item {
62                IcalItem::Prop(line) if line.name.get().eq_ignore_ascii_case("VERSION") => {}
63                IcalItem::Prop(line) => props.push(line.decode(version)),
64                IcalItem::Component(child) => components.push(child.decode_component(version)),
65                // NOTE: An opaque line carried no structure to decode.
66                IcalItem::Opaque(_) => {}
67            }
68        }
69
70        Ical {
71            version,
72            props,
73            components,
74        }
75    }
76
77    /// Decode a nested component into the recursive [`IcalComponent`] model.
78    fn decode_component(&self, version: IcalVersion) -> IcalComponent<'_> {
79        let mut props = Vec::new();
80        let mut components = Vec::new();
81
82        for item in &self.items {
83            match item {
84                IcalItem::Prop(line) => props.push(line.decode(version)),
85                IcalItem::Component(child) => components.push(child.decode_component(version)),
86                // NOTE: An opaque line carried no structure to decode.
87                IcalItem::Opaque(_) => {}
88            }
89        }
90
91        IcalComponent {
92            name: IcalComponentName::from(self.component_name()),
93            props,
94            components,
95        }
96    }
97}
98
99impl IcalLine<'_> {
100    /// Decode the line into a typed property. A known property dispatches its
101    /// value through the spec (see `decode_value`); an unknown one keeps its
102    /// raw components so it round-trips.
103    pub fn decode(&self, version: IcalVersion) -> IcalProp<'_> {
104        let name = self.name.get();
105        let params = self.params.iter().map(IcalParamNode::decode).collect();
106
107        let value = match name.parse::<IcalPropKind>() {
108            Ok(prop) => self.decode_value(prop, version),
109            // NOTE: A name outside the vocabulary has no spec to consult, but a
110            // line that declares its own VALUE has said what to read it as
111            // (RFC 5545 3.2.20), and that holds for an X- name as much as for a
112            // registered one.
113            Err(_) => match self.declared_value_kind() {
114                Some(kind) => decode_value_kind(kind, &self.value),
115                None => IcalValue::Unknown(IcalUnknownValue::decode(&self.value)),
116            },
117        };
118
119        IcalProp {
120            name: IcalPropName::from(name),
121            params,
122            value,
123        }
124    }
125
126    /// Decode a known property's value through its spec: resolve the in-force
127    /// value kind from the calendar version and any declared `VALUE`, then run
128    /// that kind's decoder over the value node.
129    pub(crate) fn decode_value(&self, prop: IcalPropKind, version: IcalVersion) -> IcalValue<'_> {
130        let declared = self.declared_value_kind();
131        let kind = (prop_spec(prop).value)(version, declared);
132        decode_value_kind(kind, &self.value)
133    }
134
135    /// The value kind named by this line's `VALUE` parameter, if any.
136    fn declared_value_kind(&self) -> Option<IcalValueKind> {
137        self.params
138            .iter()
139            .find(|param| matches!(param.name.get().parse(), Ok(IcalParamKind::Value)))
140            .and_then(|param| param.values.first())
141            .and_then(|value| value.get().parse::<IcalValueKind>().ok())
142    }
143
144    /// Whether the line declares the `QUOTED-PRINTABLE` encoding, as an
145    /// `ENCODING=` parameter or a bare token (the 1.0 short form).
146    #[cfg(feature = "quoted-printable")]
147    pub(crate) fn is_quoted_printable(&self) -> bool {
148        self.params.iter().any(param_is_quoted_printable)
149    }
150
151    /// The value of this line's `CHARSET` parameter, if any.
152    #[cfg(feature = "encoding")]
153    pub(crate) fn charset_label(&self) -> Option<&str> {
154        self.params
155            .iter()
156            .find(|param| param.name.get().eq_ignore_ascii_case("CHARSET"))
157            .and_then(|param| param.values.first())
158            .map(|value| value.get())
159    }
160}
161
162/// Decode a value node as the given value kind, routing to that value type's
163/// [`Codec`].
164fn decode_value_kind<'v>(kind: IcalValueKind, node: &'v IcalValueNode<'_>) -> IcalValue<'v> {
165    match kind {
166        IcalValueKind::Binary => IcalValue::Binary(IcalBinary::decode(node)),
167        IcalValueKind::Boolean => IcalValue::Boolean(IcalBoolean::decode(node)),
168        IcalValueKind::CalAddress => IcalValue::CalAddress(IcalCalAddress::decode(node)),
169        IcalValueKind::Date => IcalValue::Date(IcalDate::decode(node)),
170        IcalValueKind::DateTime => IcalValue::DateTime(IcalDateTime::decode(node)),
171        IcalValueKind::DateTimeList => IcalValue::DateTimeList(IcalDateTimeList::decode(node)),
172        IcalValueKind::Duration => IcalValue::Duration(IcalDuration::decode(node)),
173        IcalValueKind::Float => IcalValue::Float(IcalFloat::decode(node)),
174        IcalValueKind::Geo => IcalValue::Geo(IcalGeo::decode(node)),
175        IcalValueKind::Integer => IcalValue::Integer(IcalInteger::decode(node)),
176        IcalValueKind::Period => IcalValue::Period(IcalPeriod::decode(node)),
177        IcalValueKind::Recur => IcalValue::Recur(IcalRecur::decode(node)),
178        IcalValueKind::RequestStatus => IcalValue::RequestStatus(IcalRequestStatus::decode(node)),
179        IcalValueKind::Text => IcalValue::Text(IcalText::decode(node)),
180        IcalValueKind::TextList => IcalValue::TextList(IcalTextList::decode(node)),
181        IcalValueKind::Time => IcalValue::Time(IcalTime::decode(node)),
182        IcalValueKind::Uri => IcalValue::Uri(IcalUri::decode(node)),
183        IcalValueKind::UtcOffset => IcalValue::UtcOffset(IcalUtcOffset::decode(node)),
184    }
185}
186
187impl IcalParamNode<'_> {
188    /// Decode the parameter into a typed parameter, dispatching on the name.
189    pub fn decode(&self) -> IcalParam<'_> {
190        let Ok(kind) = self.name.get().parse::<IcalParamKind>() else {
191            return IcalParam::Unknown {
192                // NOTE: a parameter name is a token (RFC 5545 3.2), with no
193                // encoding of any kind to resolve.
194                name: Cow::Borrowed(self.name.get()),
195                values: self.list(),
196            };
197        };
198
199        match kind {
200            IcalParamKind::AltRep => IcalParam::AltRep(self.scalar()),
201            IcalParamKind::Cn => IcalParam::Cn(self.scalar()),
202            IcalParamKind::CuType => IcalParam::CuType(self.scalar()),
203            IcalParamKind::DelegatedFrom => IcalParam::DelegatedFrom(self.list()),
204            IcalParamKind::DelegatedTo => IcalParam::DelegatedTo(self.list()),
205            IcalParamKind::Dir => IcalParam::Dir(self.scalar()),
206            IcalParamKind::Encoding => IcalParam::Encoding(self.scalar()),
207            IcalParamKind::FmtType => IcalParam::FmtType(self.scalar()),
208            IcalParamKind::FbType => IcalParam::FbType(self.scalar()),
209            IcalParamKind::Language => IcalParam::Language(self.scalar()),
210            IcalParamKind::Member => IcalParam::Member(self.list()),
211            IcalParamKind::PartStat => IcalParam::PartStat(self.scalar()),
212            IcalParamKind::Range => IcalParam::Range(self.scalar()),
213            IcalParamKind::Related => IcalParam::Related(self.scalar()),
214            IcalParamKind::RelType => IcalParam::RelType(self.scalar()),
215            IcalParamKind::Role => IcalParam::Role(self.scalar()),
216            IcalParamKind::Rsvp => IcalParam::Rsvp(self.scalar()),
217            IcalParamKind::SentBy => IcalParam::SentBy(self.scalar()),
218            IcalParamKind::TzId => IcalParam::TzId(self.scalar()),
219            IcalParamKind::Value => IcalParam::Value(self.scalar()),
220            IcalParamKind::Display => IcalParam::Display(self.scalar()),
221            IcalParamKind::Email => IcalParam::Email(self.scalar()),
222            IcalParamKind::Feature => IcalParam::Feature(self.list()),
223            IcalParamKind::Label => IcalParam::Label(self.scalar()),
224            IcalParamKind::Order => IcalParam::Order(self.scalar()),
225            IcalParamKind::Schema => IcalParam::Schema(self.scalar()),
226            IcalParamKind::Derived => IcalParam::Derived(self.scalar()),
227            IcalParamKind::ScheduleAgent => IcalParam::ScheduleAgent(self.scalar()),
228            IcalParamKind::ScheduleForceSend => IcalParam::ScheduleForceSend(self.scalar()),
229            IcalParamKind::ScheduleStatus => IcalParam::ScheduleStatus(self.scalar()),
230            IcalParamKind::LinkRel => IcalParam::LinkRel(self.scalar()),
231            IcalParamKind::Gap => IcalParam::Gap(self.scalar()),
232            IcalParamKind::Charset => IcalParam::Charset(self.scalar()),
233        }
234    }
235
236    /// The parameter's first value, decoded by the RFC 6868 rules (empty when
237    /// there is none).
238    fn scalar(&self) -> Cow<'_, str> {
239        self.values
240            .first()
241            .map(|v| unescape_param(v.get(), self.escaper))
242            .unwrap_or(Cow::Borrowed(""))
243    }
244
245    /// The parameter's values, decoded by the RFC 6868 rules.
246    fn list(&self) -> Vec<Cow<'_, str>> {
247        self.values
248            .iter()
249            .map(|v| unescape_param(v.get(), self.escaper))
250            .collect()
251    }
252}
253
254/// Whether a parameter is `ENCODING=QUOTED-PRINTABLE` or the bare 1.0 token.
255#[cfg(feature = "quoted-printable")]
256fn param_is_quoted_printable(param: &IcalParamNode<'_>) -> bool {
257    let name = param.name.get();
258
259    (name.eq_ignore_ascii_case("ENCODING")
260        && param
261            .values
262            .iter()
263            .any(|v| v.get().eq_ignore_ascii_case("QUOTED-PRINTABLE")))
264        || (param.values.is_empty() && name.eq_ignore_ascii_case("QUOTED-PRINTABLE"))
265}
266
267#[cfg(test)]
268mod tests {
269    use alloc::{borrow::Cow, vec};
270
271    use crate::{
272        param::IcalParam,
273        tree::{
274            codec::Codec, cst::IcalCst, param::node::IcalParamNode, value::node::IcalValueNode,
275        },
276        value::{
277            IcalValue,
278            binary::IcalBinary,
279            boolean::IcalBoolean,
280            cal_address::IcalCalAddress,
281            datetime::{IcalDate, IcalDateTime, IcalDateTimeList, IcalTime},
282            duration::IcalDuration,
283            float::IcalFloat,
284            integer::IcalInteger,
285            period::IcalPeriod,
286            recur::IcalRecur,
287            request_status::IcalRequestStatus,
288            text::{IcalText, IcalTextList},
289            uri::IcalUri,
290            utc_offset::IcalUtcOffset,
291        },
292        version::IcalVersion,
293    };
294
295    #[test]
296    fn decodes_a_calendar_with_a_nested_event() {
297        let input = concat!(
298            "BEGIN:VCALENDAR\r\n",
299            "VERSION:2.0\r\n",
300            "PRODID:-//x//EN\r\n",
301            "BEGIN:VEVENT\r\n",
302            "UID:1\r\n",
303            "DTSTART:20260101T120000Z\r\n",
304            "SUMMARY:Lunch\r\n",
305            "END:VEVENT\r\n",
306            "END:VCALENDAR\r\n",
307        );
308        let cst = IcalCst::parse(input).unwrap();
309        let cal = cst.decode();
310
311        assert_eq!(cal.version, IcalVersion::V2_0);
312        assert_eq!(cal.props.len(), 1);
313        assert_eq!(&*cal.props[0].name, "PRODID");
314        assert_eq!(cal.components.len(), 1);
315
316        let event = &cal.components[0];
317        assert_eq!(&*event.name, "VEVENT");
318        assert_eq!(event.props.len(), 3);
319        assert_eq!(
320            event.props[1].value,
321            IcalValue::DateTime(IcalDateTime(Cow::Borrowed("20260101T120000Z"))),
322        );
323        assert_eq!(
324            event.props[2].value,
325            IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))),
326        );
327    }
328
329    #[test]
330    fn an_unknown_property_round_trips_as_unknown() {
331        let input = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nX-WR-CALNAME:Work\r\nEND:VCALENDAR\r\n";
332        let cst = IcalCst::parse(input).unwrap();
333        let cal = cst.decode();
334        assert!(matches!(cal.props[0].value, IcalValue::Unknown(_)));
335        assert_eq!(&*cal.props[0].name, "X-WR-CALNAME");
336    }
337
338    /// A kind with no `;`-structure of its own is decoded whole.
339    ///
340    /// RFC 5545 3.3.11 has a text value escape a semicolon it means literally
341    /// and 3.3.13 gives a URI no structure, so an unescaped `;` is content in
342    /// either, and reading component by component dropped everything past it.
343    #[test]
344    fn decodes_every_unstructured_kind_whole() {
345        let node = IcalValueNode::parse(b"a;b,c");
346
347        assert_eq!(IcalText::decode(&node).0, "a;b,c");
348        assert_eq!(IcalUri::decode(&node).0, "a;b,c");
349        assert_eq!(IcalCalAddress::decode(&node).0, "a;b,c");
350        assert_eq!(IcalPeriod::decode(&node).0, "a;b,c");
351        assert_eq!(IcalRecur::decode(&node).0, "a;b,c");
352        assert_eq!(
353            IcalBinary::decode(&node),
354            IcalBinary::Base64(Cow::Borrowed("a;b,c")),
355        );
356        assert_eq!(IcalBoolean::decode(&node).0, "a;b,c");
357        assert_eq!(IcalDate::decode(&node).0, "a;b,c");
358        assert_eq!(IcalDateTime::decode(&node).0, "a;b,c");
359        assert_eq!(IcalTime::decode(&node).0, "a;b,c");
360        assert_eq!(IcalDuration::decode(&node).0, "a;b,c");
361        assert_eq!(IcalFloat::decode(&node).0, "a;b,c");
362        assert_eq!(IcalInteger::decode(&node).0, "a;b,c");
363        assert_eq!(IcalUtcOffset::decode(&node).0, "a;b,c");
364
365        // NOTE: A list value owns its commas and nothing else, so only they
366        // separate.
367        assert_eq!(
368            IcalTextList::decode(&node).0,
369            vec![Cow::Borrowed("a;b"), Cow::Borrowed("c")],
370        );
371        assert_eq!(
372            IcalDateTimeList::decode(&node).0,
373            vec![Cow::Borrowed("a;b"), Cow::Borrowed("c")],
374        );
375    }
376
377    /// A structured value's component keeps the commas inside it.
378    ///
379    /// A `REQUEST-STATUS` description is a text, where a comma separates
380    /// nothing, so reading only a component's first comma-piece truncated the
381    /// status a caller reads.
382    #[test]
383    fn decodes_a_structured_component_past_its_first_comma() {
384        let node = IcalValueNode::parse(br"2.0;Success\, welcome;rcpt,two");
385        let status = IcalRequestStatus::decode(&node);
386
387        assert_eq!(status.code, "2.0");
388        assert_eq!(status.description, "Success, welcome");
389        assert_eq!(status.extra, "rcpt,two");
390    }
391
392    #[test]
393    fn decodes_the_rfc_6868_parameter_sequences() {
394        // NOTE: RFC 6868 section 3.1 spells the three characters a parameter
395        // value cannot carry raw.
396        let node = IcalParamNode::parse("CN=a^nb^^c^'d");
397
398        assert_eq!(node.decode(), IcalParam::Cn(Cow::Borrowed("a\nb^c\"d")));
399    }
400
401    #[test]
402    fn keeps_an_unknown_caret_sequence_in_a_parameter() {
403        // NOTE: RFC 6868 section 3.1 forbids reading any other caret sequence
404        // as an error, so the caret and what follows stay literal, and so does
405        // a trailing one.
406        let node = IcalParamNode::parse("CN=a^xb^");
407
408        assert_eq!(node.decode(), IcalParam::Cn(Cow::Borrowed("a^xb^")));
409    }
410
411    #[test]
412    fn keeps_a_backslash_in_a_parameter() {
413        // NOTE: RFC 6868 section 3.2 forbids backslash escaping in a parameter
414        // value, so a Windows path keeps its separators.
415        let node = IcalParamNode::parse(r"X-PATH=C:\temp\note.txt");
416
417        assert_eq!(
418            node.decode(),
419            IcalParam::Unknown {
420                name: Cow::Borrowed("X-PATH"),
421                values: vec![Cow::Borrowed(r"C:\temp\note.txt")],
422            },
423        );
424    }
425}