1use 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::node::IcalParamNode,
27 prop::prop_spec,
28 value::node::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 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 IcalItem::Opaque(_) => {}
65 }
66 }
67
68 Ical {
69 version,
70 props,
71 components,
72 }
73 }
74
75 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 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 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 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 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 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 #[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 #[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
160fn 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 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 fn scalar(&self) -> Cow<'_, str> {
234 self.values
235 .first()
236 .map(|v| unescape(v.get()))
237 .unwrap_or(Cow::Borrowed(""))
238 }
239
240 fn list(&self) -> Vec<Cow<'_, str>> {
242 self.values.iter().map(|v| unescape(v.get())).collect()
243 }
244}
245
246#[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 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}