1use 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 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 IcalItem::Opaque(_) => {}
67 }
68 }
69
70 Ical {
71 version,
72 props,
73 components,
74 }
75 }
76
77 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 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 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 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 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 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 #[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 #[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
162fn 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 pub fn decode(&self) -> IcalParam<'_> {
190 let Ok(kind) = self.name.get().parse::<IcalParamKind>() else {
191 return IcalParam::Unknown {
192 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 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 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#[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 #[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 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 #[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 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 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 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}