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. A [`IcalValueNode`] decodes its components, a
5//! [`IcalParamNode`] decodes into a [`IcalParam`], a [`IcalLine`] decodes into
6//! a [`IcalProp`], and a [`IcalCst`] decodes into a whole [`Ical`]
7//! (recursively, walking every nested component).
8//!
9//! A property's value kind is resolved through its spec, not a name match:
10//! [`IcalLine::decode`] maps the name to a [`IcalPropKind`], asks the spec for
11//! the in-force value kind (version plus any declared `VALUE`), then routes to
12//! that kind's decoder. The parameter name dispatch is the match in
13//! [`IcalParamNode::decode`].
14
15use alloc::{borrow::Cow, vec::Vec};
16
17use crate::{
18    component::{IcalComponent, IcalComponentName},
19    ical::Ical,
20    param::{IcalParam, IcalParamKind},
21    prop::{IcalProp, IcalPropKind, IcalPropName},
22    tree::{
23        codec::{Codec, unescape::unescape},
24        cst::{IcalCst, IcalItem},
25        line::IcalLine,
26        param::IcalParamNode,
27        prop::prop_spec,
28        value::IcalValueNode,
29    },
30    value::{
31        IcalUnknownValue, IcalValue, IcalValueKind,
32        binary::IcalBinary,
33        boolean::IcalBoolean,
34        cal_address::IcalCalAddress,
35        datetime::{IcalDate, IcalDateTime, IcalDateTimeList, IcalTime},
36        duration::IcalDuration,
37        float::IcalFloat,
38        geo::IcalGeo,
39        integer::IcalInteger,
40        period::IcalPeriod,
41        recur::IcalRecur,
42        request_status::IcalRequestStatus,
43        text::{IcalText, IcalTextList},
44        uri::IcalUri,
45        utc_offset::IcalUtcOffset,
46    },
47    version::IcalVersion,
48};
49
50impl IcalCst<'_> {
51    /// Decode the whole calendar into the semantic [`Ical`] model. `VERSION` is
52    /// held as the calendar's indicator, not as a free property.
53    pub fn decode(&self) -> Ical<'_> {
54        let version = self.version();
55        let mut props = Vec::new();
56        let mut components = Vec::new();
57
58        for item in &self.items {
59            match item {
60                IcalItem::Prop(line) if line.name.get().eq_ignore_ascii_case("VERSION") => {}
61                IcalItem::Prop(line) => props.push(line.decode(version)),
62                IcalItem::Component(child) => components.push(child.decode_component(version)),
63                // NOTE: An opaque line carried no structure to decode.
64                IcalItem::Opaque(_) => {}
65            }
66        }
67
68        Ical {
69            version,
70            props,
71            components,
72        }
73    }
74
75    /// Decode a nested component into the recursive [`IcalComponent`] model.
76    fn decode_component(&self, version: IcalVersion) -> IcalComponent<'_> {
77        let mut props = Vec::new();
78        let mut components = Vec::new();
79
80        for item in &self.items {
81            match item {
82                IcalItem::Prop(line) => props.push(line.decode(version)),
83                IcalItem::Component(child) => components.push(child.decode_component(version)),
84                // NOTE: An opaque line carried no structure to decode.
85                IcalItem::Opaque(_) => {}
86            }
87        }
88
89        IcalComponent {
90            name: IcalComponentName::from(self.component_name()),
91            props,
92            components,
93        }
94    }
95}
96
97impl IcalLine<'_> {
98    /// Decode the line into a typed property. A known property dispatches its
99    /// value through the spec (see `decode_value`); an unknown one keeps its
100    /// raw components so it round-trips.
101    pub fn decode(&self, version: IcalVersion) -> IcalProp<'_> {
102        let name = self.name.get();
103        let params = self.params.iter().map(IcalParamNode::decode).collect();
104
105        let value = match name.parse::<IcalPropKind>() {
106            Ok(prop) => self.decode_value(prop, version),
107            // NOTE: A name outside the vocabulary has no spec to consult, but a
108            // line that declares its own VALUE has said what to read it as
109            // (RFC 5545 3.2.20), and that holds for an X- name as much as for a
110            // registered one.
111            Err(_) => match self.declared_value_kind() {
112                Some(kind) => decode_value_kind(kind, &self.value),
113                None => IcalValue::Unknown(IcalUnknownValue::decode(&self.value)),
114            },
115        };
116
117        IcalProp {
118            name: IcalPropName::from(name),
119            params,
120            value,
121        }
122    }
123
124    /// Decode a known property's value through its spec: resolve the in-force
125    /// value kind from the calendar version and any declared `VALUE`, then run
126    /// that kind's decoder over the value node.
127    pub(crate) fn decode_value(&self, prop: IcalPropKind, version: IcalVersion) -> IcalValue<'_> {
128        let declared = self.declared_value_kind();
129        let kind = (prop_spec(prop).value)(version, declared);
130        decode_value_kind(kind, &self.value)
131    }
132
133    /// The value kind named by this line's `VALUE` parameter, if any.
134    fn declared_value_kind(&self) -> Option<IcalValueKind> {
135        self.params
136            .iter()
137            .find(|param| matches!(param.name.get().parse(), Ok(IcalParamKind::Value)))
138            .and_then(|param| param.values.first())
139            .and_then(|value| value.get().parse::<IcalValueKind>().ok())
140    }
141
142    /// Whether the line declares the `QUOTED-PRINTABLE` encoding, as an
143    /// `ENCODING=` parameter or a bare token (the 1.0 short form).
144    #[cfg(feature = "quoted-printable")]
145    pub(crate) fn is_quoted_printable(&self) -> bool {
146        self.params.iter().any(param_is_quoted_printable)
147    }
148
149    /// The value of this line's `CHARSET` parameter, if any.
150    #[cfg(feature = "encoding")]
151    pub(crate) fn charset_label(&self) -> Option<&str> {
152        self.params
153            .iter()
154            .find(|param| param.name.get().eq_ignore_ascii_case("CHARSET"))
155            .and_then(|param| param.values.first())
156            .map(|value| value.get())
157    }
158}
159
160/// Decode a value node as the given value kind, routing to that value type's
161/// [`Codec`].
162fn decode_value_kind<'v>(kind: IcalValueKind, node: &'v IcalValueNode<'_>) -> IcalValue<'v> {
163    match kind {
164        IcalValueKind::Binary => IcalValue::Binary(IcalBinary::decode(node)),
165        IcalValueKind::Boolean => IcalValue::Boolean(IcalBoolean::decode(node)),
166        IcalValueKind::CalAddress => IcalValue::CalAddress(IcalCalAddress::decode(node)),
167        IcalValueKind::Date => IcalValue::Date(IcalDate::decode(node)),
168        IcalValueKind::DateTime => IcalValue::DateTime(IcalDateTime::decode(node)),
169        IcalValueKind::DateTimeList => IcalValue::DateTimeList(IcalDateTimeList::decode(node)),
170        IcalValueKind::Duration => IcalValue::Duration(IcalDuration::decode(node)),
171        IcalValueKind::Float => IcalValue::Float(IcalFloat::decode(node)),
172        IcalValueKind::Geo => IcalValue::Geo(IcalGeo::decode(node)),
173        IcalValueKind::Integer => IcalValue::Integer(IcalInteger::decode(node)),
174        IcalValueKind::Period => IcalValue::Period(IcalPeriod::decode(node)),
175        IcalValueKind::Recur => IcalValue::Recur(IcalRecur::decode(node)),
176        IcalValueKind::RequestStatus => IcalValue::RequestStatus(IcalRequestStatus::decode(node)),
177        IcalValueKind::Text => IcalValue::Text(IcalText::decode(node)),
178        IcalValueKind::TextList => IcalValue::TextList(IcalTextList::decode(node)),
179        IcalValueKind::Time => IcalValue::Time(IcalTime::decode(node)),
180        IcalValueKind::Uri => IcalValue::Uri(IcalUri::decode(node)),
181        IcalValueKind::UtcOffset => IcalValue::UtcOffset(IcalUtcOffset::decode(node)),
182    }
183}
184
185impl IcalParamNode<'_> {
186    /// Decode the parameter into a typed parameter, dispatching on the name.
187    pub fn decode(&self) -> IcalParam<'_> {
188        let Ok(kind) = self.name.get().parse::<IcalParamKind>() else {
189            return IcalParam::Unknown {
190                name: unescape(self.name.get()),
191                values: self.list(),
192            };
193        };
194
195        match kind {
196            IcalParamKind::AltRep => IcalParam::AltRep(self.scalar()),
197            IcalParamKind::Cn => IcalParam::Cn(self.scalar()),
198            IcalParamKind::CuType => IcalParam::CuType(self.scalar()),
199            IcalParamKind::DelegatedFrom => IcalParam::DelegatedFrom(self.list()),
200            IcalParamKind::DelegatedTo => IcalParam::DelegatedTo(self.list()),
201            IcalParamKind::Dir => IcalParam::Dir(self.scalar()),
202            IcalParamKind::Encoding => IcalParam::Encoding(self.scalar()),
203            IcalParamKind::FmtType => IcalParam::FmtType(self.scalar()),
204            IcalParamKind::FbType => IcalParam::FbType(self.scalar()),
205            IcalParamKind::Language => IcalParam::Language(self.scalar()),
206            IcalParamKind::Member => IcalParam::Member(self.list()),
207            IcalParamKind::PartStat => IcalParam::PartStat(self.scalar()),
208            IcalParamKind::Range => IcalParam::Range(self.scalar()),
209            IcalParamKind::Related => IcalParam::Related(self.scalar()),
210            IcalParamKind::RelType => IcalParam::RelType(self.scalar()),
211            IcalParamKind::Role => IcalParam::Role(self.scalar()),
212            IcalParamKind::Rsvp => IcalParam::Rsvp(self.scalar()),
213            IcalParamKind::SentBy => IcalParam::SentBy(self.scalar()),
214            IcalParamKind::TzId => IcalParam::TzId(self.scalar()),
215            IcalParamKind::Value => IcalParam::Value(self.scalar()),
216            IcalParamKind::Display => IcalParam::Display(self.scalar()),
217            IcalParamKind::Email => IcalParam::Email(self.scalar()),
218            IcalParamKind::Feature => IcalParam::Feature(self.list()),
219            IcalParamKind::Label => IcalParam::Label(self.scalar()),
220            IcalParamKind::Order => IcalParam::Order(self.scalar()),
221            IcalParamKind::Schema => IcalParam::Schema(self.scalar()),
222            IcalParamKind::Derived => IcalParam::Derived(self.scalar()),
223            IcalParamKind::ScheduleAgent => IcalParam::ScheduleAgent(self.scalar()),
224            IcalParamKind::ScheduleForceSend => IcalParam::ScheduleForceSend(self.scalar()),
225            IcalParamKind::ScheduleStatus => IcalParam::ScheduleStatus(self.scalar()),
226            IcalParamKind::LinkRel => IcalParam::LinkRel(self.scalar()),
227            IcalParamKind::Gap => IcalParam::Gap(self.scalar()),
228            IcalParamKind::Charset => IcalParam::Charset(self.scalar()),
229        }
230    }
231
232    /// The parameter's first value, decoded (empty when there is none).
233    fn scalar(&self) -> Cow<'_, str> {
234        self.values
235            .first()
236            .map(|v| unescape(v.get()))
237            .unwrap_or(Cow::Borrowed(""))
238    }
239
240    /// The parameter's values, decoded.
241    fn list(&self) -> Vec<Cow<'_, str>> {
242        self.values.iter().map(|v| unescape(v.get())).collect()
243    }
244}
245
246/// Whether a parameter is `ENCODING=QUOTED-PRINTABLE` or the bare 1.0 token.
247#[cfg(feature = "quoted-printable")]
248fn param_is_quoted_printable(param: &IcalParamNode<'_>) -> bool {
249    let name = param.name.get();
250
251    (name.eq_ignore_ascii_case("ENCODING")
252        && param
253            .values
254            .iter()
255            .any(|v| v.get().eq_ignore_ascii_case("QUOTED-PRINTABLE")))
256        || (param.values.is_empty() && name.eq_ignore_ascii_case("QUOTED-PRINTABLE"))
257}
258
259#[cfg(test)]
260mod tests {
261    use alloc::borrow::Cow;
262
263    use crate::{
264        tree::cst::IcalCst,
265        value::{IcalValue, datetime::IcalDateTime, text::IcalText},
266    };
267
268    #[test]
269    fn decodes_a_calendar_with_a_nested_event() {
270        let input = concat!(
271            "BEGIN:VCALENDAR\r\n",
272            "VERSION:2.0\r\n",
273            "PRODID:-//x//EN\r\n",
274            "BEGIN:VEVENT\r\n",
275            "UID:1\r\n",
276            "DTSTART:20260101T120000Z\r\n",
277            "SUMMARY:Lunch\r\n",
278            "END:VEVENT\r\n",
279            "END:VCALENDAR\r\n",
280        );
281        let cst = IcalCst::parse(input).unwrap();
282        let cal = cst.decode();
283
284        // NOTE: VERSION is the indicator; PRODID is the only calendar-level
285        // prop.
286        assert_eq!(cal.version, crate::version::IcalVersion::V2_0);
287        assert_eq!(cal.props.len(), 1);
288        assert_eq!(&*cal.props[0].name, "PRODID");
289        assert_eq!(cal.components.len(), 1);
290
291        let event = &cal.components[0];
292        assert_eq!(&*event.name, "VEVENT");
293        assert_eq!(event.props.len(), 3);
294        assert_eq!(
295            event.props[1].value,
296            IcalValue::DateTime(IcalDateTime(Cow::Borrowed("20260101T120000Z"))),
297        );
298        assert_eq!(
299            event.props[2].value,
300            IcalValue::Text(IcalText(Cow::Borrowed("Lunch"))),
301        );
302    }
303
304    #[test]
305    fn an_unknown_property_round_trips_as_unknown() {
306        let input = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nX-WR-CALNAME:Work\r\nEND:VCALENDAR\r\n";
307        let cst = IcalCst::parse(input).unwrap();
308        let cal = cst.decode();
309        assert!(matches!(cal.props[0].value, IcalValue::Unknown(_)));
310        assert_eq!(&*cal.props[0].name, "X-WR-CALNAME");
311    }
312}