render.rs
⎇
Raw
1//! The `calendar-data` and `address-data` a REPORT returns: the whole
2//! object, a part of it, or its recurrence set expanded (RFC 4791, 9.6;
3//! RFC 6352, 10.4).
4
5use std::collections::HashSet;
6
7use calcard::common::PartialDateTime;
8use calcard::icalendar::{
9 ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarParameter,
10 ICalendarParameterName, ICalendarParameterValue, ICalendarProperty, ICalendarValue,
11 ICalendarValueType,
12};
13use calcard::vcard::{VCard, VCardVersion};
14use chrono::{DateTime, Utc};
15use xmltree::Element;
16
17use crate::expand::expand;
18use crate::filter::{TimeRange, time_range};
19use crate::freebusy::period;
20use crate::report::Refused;
21use crate::xml::{CALDAV, CARDDAV, Name, elements};
22use crate::zone::{Zone, Zones};
23
24/// Instances one object may expand into in one response.
25const MAX_EXPANDED: usize = 10_000;
26
27#[derive(Debug, Clone, Default, PartialEq)]
28pub struct CalendarData {
29 /// `None` returns every component and property.
30 pub comp: Option<CompSelect>,
31 pub expand: Option<TimeRange>,
32 pub limit_recurrence: Option<TimeRange>,
33 pub limit_freebusy: Option<TimeRange>,
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub struct CompSelect {
38 /// Upper case.
39 pub name: String,
40 /// `None` for `allprop`.
41 pub props: Option<Vec<PropSelect>>,
42 /// `None` for `allcomp`.
43 pub comps: Option<Vec<CompSelect>>,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct PropSelect {
48 pub name: String,
49 pub novalue: bool,
50}
51
52#[derive(Debug, Clone, Default, PartialEq)]
53pub struct AddressData {
54 /// `None` for `allprop`.
55 pub props: Option<Vec<PropSelect>>,
56 pub version: Option<VCardVersion>,
57}
58
59/// The expansion would exceed [`MAX_EXPANDED`] instances.
60#[derive(Debug, PartialEq, Eq)]
61pub struct TooManyInstances;
62
63pub fn calendar_request(e: &Element) -> Result<CalendarData, Refused> {
64 let content_type = e.attributes.get("content-type").map(String::as_str);
65 let version = e.attributes.get("version").map(String::as_str);
66 if content_type.is_some_and(|t| t != "text/calendar") || version.is_some_and(|v| v != "2.0") {
67 return Err(Refused::Condition(Name::new(
68 CALDAV,
69 "supported-calendar-data",
70 )));
71 }
72 let mut d = CalendarData::default();
73 for c in elements(e).filter(|c| Name::of(c).ns == CALDAV) {
74 match c.name.as_str() {
75 "comp" => d.comp = Some(comp_select(c)?),
76 "expand" => d.expand = Some(time_range(c)?),
77 "limit-recurrence-set" => d.limit_recurrence = Some(time_range(c)?),
78 "limit-freebusy-set" => d.limit_freebusy = Some(time_range(c)?),
79 _ => {}
80 }
81 }
82 Ok(d)
83}
84
85fn comp_select(e: &Element) -> Result<CompSelect, Refused> {
86 let name = e.attributes.get("name").ok_or(Refused::Invalid)?;
87 let has = |local: &str| elements(e).any(|c| Name::of(c).is(CALDAV, local));
88 let props = (!has("allprop")).then(|| prop_selects(e, CALDAV));
89 let comps = match has("allcomp") {
90 true => None,
91 false => Some(
92 elements(e)
93 .filter(|c| Name::of(c).is(CALDAV, "comp"))
94 .map(comp_select)
95 .collect::<Result<_, _>>()?,
96 ),
97 };
98 Ok(CompSelect {
99 name: name.to_ascii_uppercase(),
100 props,
101 comps,
102 })
103}
104
105fn prop_selects(e: &Element, ns: &str) -> Vec<PropSelect> {
106 elements(e)
107 .filter(|c| Name::of(c).is(ns, "prop"))
108 .filter_map(|c| {
109 Some(PropSelect {
110 name: c.attributes.get("name")?.clone(),
111 novalue: c.attributes.get("novalue").map(String::as_str) == Some("yes"),
112 })
113 })
114 .collect()
115}
116
117pub fn address_request(e: &Element) -> Result<AddressData, Refused> {
118 let unsupported = || Refused::Condition(Name::new(CARDDAV, "supported-address-data"));
119 if e.attributes
120 .get("content-type")
121 .is_some_and(|t| t != "text/vcard")
122 {
123 return Err(unsupported());
124 }
125 let version = match e.attributes.get("version").map(String::as_str) {
126 None => None,
127 Some("3.0") => Some(VCardVersion::V3_0),
128 Some("4.0") => Some(VCardVersion::V4_0),
129 Some(_) => return Err(unsupported()),
130 };
131 let all = elements(e).any(|c| Name::of(c).is(CARDDAV, "allprop"));
132 let props = prop_selects(e, CARDDAV);
133 Ok(AddressData {
134 props: (!all && !props.is_empty()).then_some(props),
135 version,
136 })
137}
138
139/// The calendar-data of one object. The stored text is returned unchanged
140/// unless the request narrows or expands it.
141pub fn calendar_data(
142 raw: &str,
143 req: &CalendarData,
144 floating: &Zone,
145) -> Result<String, TooManyInstances> {
146 if *req == CalendarData::default() {
147 return Ok(raw.to_string());
148 }
149 let Ok(mut cal) = ICalendar::parse(raw) else {
150 return Ok(raw.to_string());
151 };
152 if let Some(r) = &req.expand {
153 cal = expanded(&cal, r, floating)?;
154 }
155 if let Some(r) = &req.limit_recurrence {
156 cal = limit_recurrence(&cal, r, floating);
157 }
158 if let Some(r) = &req.limit_freebusy {
159 limit_freebusy(&mut cal, r, floating);
160 }
161 if let Some(sel) = &req.comp {
162 let mut out = Vec::new();
163 copy(&cal, 0, Some(sel), &mut out);
164 cal = ICalendar { components: out };
165 }
166 Ok(cal.to_string())
167}
168
169/// One component per instance in `range`, in UTC, with a RECURRENCE-ID and
170/// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates.
171fn expanded(
172 cal: &ICalendar,
173 range: &TimeRange,
174 floating: &Zone,
175) -> Result<ICalendar, TooManyInstances> {
176 let exp = expand(cal, range.clone(), floating.clone());
177 if exp.truncated || exp.instances.len() > MAX_EXPANDED {
178 return Err(TooManyInstances);
179 }
180 let mut out = vec![root(cal)];
181 for x in exp.instances {
182 let src = &cal.components[x.component];
183 let date = src
184 .property(&ICalendarProperty::Dtstart)
185 .and_then(|e| e.values.first()?.as_partial_date_time())
186 .is_some_and(|p| p.hour.is_none());
187 let at = copy(cal, x.component, None, &mut out);
188 let c = &mut out[at as usize];
189 c.entries.retain(|e| {
190 !matches!(
191 e.name,
192 ICalendarProperty::Rrule
193 | ICalendarProperty::Rdate
194 | ICalendarProperty::Exdate
195 | ICalendarProperty::Exrule
196 | ICalendarProperty::Dtstart
197 | ICalendarProperty::Dtend
198 | ICalendarProperty::Due
199 | ICalendarProperty::RecurrenceId
200 )
201 });
202 let when = |name: ICalendarProperty, t: DateTime<Utc>| {
203 let (value, params) = if date {
204 let day = floating.to_local(t).and_utc().timestamp();
205 let param = ICalendarParameter::new(
206 ICalendarParameterName::Value,
207 ICalendarParameterValue::Value(ICalendarValueType::Date),
208 );
209 (PartialDateTime::from_date_timestamp(day), vec![param])
210 } else {
211 (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![])
212 };
213 ICalendarEntry {
214 name,
215 params,
216 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
217 }
218 };
219 c.entries.push(when(ICalendarProperty::Dtstart, x.start));
220 if src.has_property(&ICalendarProperty::Dtend) {
221 c.entries.push(when(ICalendarProperty::Dtend, x.end));
222 }
223 if src.has_property(&ICalendarProperty::Due) {
224 c.entries.push(when(ICalendarProperty::Due, x.end));
225 }
226 if let Some(rid) = x.recurrence_id {
227 c.entries.push(when(ICalendarProperty::RecurrenceId, rid));
228 }
229 out[0].component_ids.push(at);
230 }
231 Ok(ICalendar { components: out })
232}
233
234/// The masters, and only the overrides that affect `range` (RFC 4791,
235/// 9.6.6).
236fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar {
237 let exp = expand(cal, range.clone(), floating.clone());
238 let used: HashSet<usize> = exp.instances.iter().map(|x| x.component).collect();
239 let mut out = vec![root(cal)];
240 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
241 let c = &cal.components[i as usize];
242 let keep = match c.property(&ICalendarProperty::RecurrenceId) {
243 None => true,
244 Some(rid) => {
245 exp.truncated
246 || used.contains(&(i as usize))
247 || rid.parameter(&ICalendarParameterName::Range).is_some()
248 }
249 };
250 if keep {
251 let at = copy(cal, i as usize, None, &mut out);
252 out[0].component_ids.push(at);
253 }
254 }
255 ICalendar { components: out }
256}
257
258/// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7).
259fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) {
260 let zones = Zones::new(cal, floating.clone());
261 for c in cal
262 .components
263 .iter_mut()
264 .filter(|c| c.component_type == ICalendarComponentType::VFreebusy)
265 {
266 for e in c
267 .entries
268 .iter_mut()
269 .filter(|e| e.name == ICalendarProperty::Freebusy)
270 {
271 let tzid = e.tz_id().map(str::to_string);
272 e.values.retain(|v| {
273 period(&zones, tzid.as_deref(), v)
274 .is_some_and(|(s, e)| s < range.end && e > range.start)
275 });
276 }
277 c.entries
278 .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty());
279 }
280}
281
282/// The VCALENDAR with its properties and no components.
283fn root(cal: &ICalendar) -> ICalendarComponent {
284 ICalendarComponent {
285 component_type: ICalendarComponentType::VCalendar,
286 entries: cal
287 .components
288 .first()
289 .map(|c| c.entries.clone())
290 .unwrap_or_default(),
291 component_ids: Vec::new(),
292 }
293}
294
295/// Appends component `i` of `src` and its sub-components, narrowed by `sel`,
296/// to `out`. Returns the new index.
297fn copy(
298 src: &ICalendar,
299 i: usize,
300 sel: Option<&CompSelect>,
301 out: &mut Vec<ICalendarComponent>,
302) -> u32 {
303 let c = &src.components[i];
304 let entries = match sel.and_then(|s| s.props.as_ref()) {
305 None => c.entries.clone(),
306 Some(props) => c
307 .entries
308 .iter()
309 .filter_map(|e| {
310 let p = props
311 .iter()
312 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
313 let mut e = e.clone();
314 if p.novalue {
315 e.values.clear();
316 }
317 Some(e)
318 })
319 .collect(),
320 };
321 let at = out.len();
322 out.push(ICalendarComponent {
323 component_type: c.component_type.clone(),
324 entries,
325 component_ids: Vec::new(),
326 });
327 for &child in &c.component_ids {
328 let child = child as usize;
329 let child_sel = match sel.map(|s| &s.comps) {
330 None | Some(None) => None,
331 Some(Some(list)) => {
332 let name = src.components[child].component_type.as_str();
333 match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) {
334 Some(s) => Some(s),
335 None => continue,
336 }
337 }
338 };
339 let id = copy(src, child, child_sel, out);
340 out[at].component_ids.push(id);
341 }
342 at as u32
343}
344
345/// The address-data of one card. The stored text is returned unchanged
346/// unless the request narrows it or asks for another version.
347pub fn address_data(raw: &str, req: &AddressData) -> String {
348 if *req == AddressData::default() {
349 return raw.to_string();
350 }
351 let Ok(card) = VCard::parse(raw) else {
352 return raw.to_string();
353 };
354 let version = req.version.or(card.version()).unwrap_or_default();
355 if req.props.is_none() && Some(version) == card.version() {
356 return raw.to_string();
357 }
358 let entries = match &req.props {
359 None => card.entries,
360 Some(props) => card
361 .entries
362 .into_iter()
363 .filter_map(|mut e| {
364 let p = props
365 .iter()
366 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
367 if p.novalue {
368 e.values.clear();
369 }
370 Some(e)
371 })
372 .collect(),
373 };
374 let mut out = String::new();
375 let _ = VCard { entries }.write_to(&mut out, version);
376 out
377}
378