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::{HashMap, 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, stamp};
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 // RFC 6352, 10.4: without a version, 3.0.
126 let version = match e.attributes.get("version").map(String::as_str) {
127 None | 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, and how many instances an `expand`
140/// produced. The stored text is returned unchanged unless the request
141/// narrows or expands it.
142pub fn calendar_data(
143 raw: &str,
144 req: &CalendarData,
145 floating: &Zone,
146) -> Result<(String, usize), TooManyInstances> {
147 if *req == CalendarData::default() {
148 return Ok((raw.to_string(), 0));
149 }
150 let Ok(mut cal) = ICalendar::parse(raw) else {
151 return Ok((raw.to_string(), 0));
152 };
153 let mut instances = 0;
154 if let Some(r) = &req.expand {
155 (cal, instances) = expanded(&cal, r, floating)?;
156 }
157 if let Some(r) = &req.limit_recurrence {
158 cal = limit_recurrence(&cal, r, floating);
159 }
160 if let Some(r) = &req.limit_freebusy {
161 limit_freebusy(&mut cal, r, floating);
162 }
163 if let Some(sel) = &req.comp {
164 let mut out = Vec::new();
165 copy(&cal, 0, Some(sel), &mut out);
166 cal = ICalendar { components: out };
167 }
168 Ok((cal.to_string(), instances))
169}
170
171/// One component per instance in `range`, in UTC, with a RECURRENCE-ID and
172/// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates.
173fn expanded(
174 cal: &ICalendar,
175 range: &TimeRange,
176 floating: &Zone,
177) -> Result<(ICalendar, usize), TooManyInstances> {
178 let exp = expand(cal, range.clone(), floating.clone());
179 let count = exp.instances.len();
180 if exp.truncated || count > MAX_EXPANDED {
181 return Err(TooManyInstances);
182 }
183 let mut out = vec![root(cal)];
184 for x in exp.instances {
185 let src = &cal.components[x.component];
186 let date = src
187 .property(&ICalendarProperty::Dtstart)
188 .and_then(|e| e.values.first()?.as_partial_date_time())
189 .is_some_and(|p| p.hour.is_none());
190 let at = copy(cal, x.component, None, &mut out);
191 let c = &mut out[at as usize];
192 c.entries.retain(|e| {
193 !matches!(
194 e.name,
195 ICalendarProperty::Rrule
196 | ICalendarProperty::Rdate
197 | ICalendarProperty::Exdate
198 | ICalendarProperty::Exrule
199 | ICalendarProperty::Dtstart
200 | ICalendarProperty::Dtend
201 | ICalendarProperty::Due
202 | ICalendarProperty::RecurrenceId
203 )
204 });
205 let when = |name: ICalendarProperty, t: DateTime<Utc>| {
206 let (value, params) = if date {
207 let day = floating.to_local(t).and_utc().timestamp();
208 let param = ICalendarParameter::new(
209 ICalendarParameterName::Value,
210 ICalendarParameterValue::Value(ICalendarValueType::Date),
211 );
212 (PartialDateTime::from_date_timestamp(day), vec![param])
213 } else {
214 (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![])
215 };
216 ICalendarEntry {
217 name,
218 params,
219 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
220 }
221 };
222 c.entries.push(when(ICalendarProperty::Dtstart, x.start));
223 if src.has_property(&ICalendarProperty::Dtend) {
224 c.entries.push(when(ICalendarProperty::Dtend, x.end));
225 }
226 if src.has_property(&ICalendarProperty::Due) {
227 c.entries.push(when(ICalendarProperty::Due, x.end));
228 }
229 if let Some(rid) = x.recurrence_id {
230 c.entries.push(when(ICalendarProperty::RecurrenceId, rid));
231 }
232 out[0].component_ids.push(at);
233 }
234 // A task or journal without DTSTART has no instances to expand. Its
235 // times still go to UTC: the answer holds no VTIMEZONE.
236 let zones = Zones::new(cal, floating.clone());
237 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
238 let c = &cal.components[i as usize];
239 if c.component_type != ICalendarComponentType::VTimezone
240 && !c.has_property(&ICalendarProperty::Dtstart)
241 {
242 let at = copy(cal, i as usize, None, &mut out);
243 for e in &mut out[at as usize].entries {
244 let tzid = e.tz_id().map(str::to_string);
245 for v in &mut e.values {
246 if let ICalendarValue::PartialDateTime(p) = v
247 && p.hour.is_some()
248 && let Some(s) = stamp(&zones, p, tzid.as_deref())
249 {
250 **p = PartialDateTime::from_utc_timestamp(s.utc().timestamp());
251 }
252 }
253 e.params.retain(|p| p.name != ICalendarParameterName::Tzid);
254 }
255 out[0].component_ids.push(at);
256 }
257 }
258 Ok((ICalendar { components: out }, count))
259}
260
261/// The masters, and only the overrides that affect `range` (RFC 4791,
262/// 9.6.6).
263fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar {
264 let exp = expand(cal, range.clone(), floating.clone());
265 let used: HashSet<usize> = exp.instances.iter().map(|x| x.component).collect();
266 let mut out = vec![root(cal)];
267 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
268 let c = &cal.components[i as usize];
269 let keep = match c.property(&ICalendarProperty::RecurrenceId) {
270 None => true,
271 Some(rid) => {
272 exp.truncated
273 || used.contains(&(i as usize))
274 || rid.parameter(&ICalendarParameterName::Range).is_some()
275 }
276 };
277 if keep {
278 let at = copy(cal, i as usize, None, &mut out);
279 out[0].component_ids.push(at);
280 }
281 }
282 ICalendar { components: out }
283}
284
285/// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7).
286fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) {
287 let zones = Zones::new(cal, floating.clone());
288 for c in cal
289 .components
290 .iter_mut()
291 .filter(|c| c.component_type == ICalendarComponentType::VFreebusy)
292 {
293 for e in c
294 .entries
295 .iter_mut()
296 .filter(|e| e.name == ICalendarProperty::Freebusy)
297 {
298 let tzid = e.tz_id().map(str::to_string);
299 e.values.retain(|v| {
300 period(&zones, tzid.as_deref(), v)
301 .is_some_and(|(s, e)| s < range.end && e > range.start)
302 });
303 }
304 c.entries
305 .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty());
306 }
307}
308
309/// The VCALENDAR with its properties and no components.
310fn root(cal: &ICalendar) -> ICalendarComponent {
311 ICalendarComponent {
312 component_type: ICalendarComponentType::VCalendar,
313 entries: cal
314 .components
315 .first()
316 .map(|c| c.entries.clone())
317 .unwrap_or_default(),
318 component_ids: Vec::new(),
319 }
320}
321
322/// Appends component `i` of `src` and its sub-components, narrowed by `sel`,
323/// to `out`. Returns the new index.
324fn copy(
325 src: &ICalendar,
326 i: usize,
327 sel: Option<&CompSelect>,
328 out: &mut Vec<ICalendarComponent>,
329) -> u32 {
330 let c = &src.components[i];
331 let entries = match sel.and_then(|s| s.props.as_ref()) {
332 None => c.entries.clone(),
333 Some(props) => c
334 .entries
335 .iter()
336 .filter_map(|e| {
337 let p = props
338 .iter()
339 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
340 let mut e = e.clone();
341 if p.novalue {
342 e.values.clear();
343 }
344 Some(e)
345 })
346 .collect(),
347 };
348 let at = out.len();
349 out.push(ICalendarComponent {
350 component_type: c.component_type.clone(),
351 entries,
352 component_ids: Vec::new(),
353 });
354 for &child in &c.component_ids {
355 let child = child as usize;
356 let child_sel = match sel.map(|s| &s.comps) {
357 None | Some(None) => None,
358 Some(Some(list)) => {
359 let name = src.components[child].component_type.as_str();
360 match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) {
361 Some(s) => Some(s),
362 None => continue,
363 }
364 }
365 };
366 let id = copy(src, child, child_sel, out);
367 out[at].component_ids.push(id);
368 }
369 at as u32
370}
371
372/// The address-data of one card. The stored text is returned unchanged
373/// unless the request narrows it or asks for another version.
374pub fn address_data(raw: &str, req: &AddressData) -> String {
375 if *req == AddressData::default() {
376 return raw.to_string();
377 }
378 let Ok(card) = VCard::parse(raw) else {
379 return raw.to_string();
380 };
381 let version = req.version.or(card.version()).unwrap_or_default();
382 if req.props.is_none() && Some(version) == card.version() {
383 return raw.to_string();
384 }
385 // calcard's 4.0 writer drops a `pref` type, so map before parsing.
386 let card = match version {
387 VCardVersion::V4_0 => VCard::parse(apple_forms(raw, version)).unwrap_or(card),
388 _ => card,
389 };
390 let entries = match &req.props {
391 None => card.entries,
392 Some(props) => card
393 .entries
394 .into_iter()
395 .filter_map(|mut e| {
396 let p = props
397 .iter()
398 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
399 if p.novalue {
400 e.values.clear();
401 }
402 Some(e)
403 })
404 .collect(),
405 };
406 let mut out = String::new();
407 let _ = VCard { entries }.write_to(&mut out, version);
408 match version {
409 VCardVersion::V4_0 => raw_data_uris(&out),
410 _ => apple_forms(&out, version),
411 }
412}
413
414/// calcard's 4.0 writer escapes the comma of a `data:` URI as TEXT, which
415/// breaks the URI. Binary values have no backslash to keep.
416fn raw_data_uris(text: &str) -> String {
417 let eol = if text.contains("\r\n") { "\r\n" } else { "\n" };
418 let mut out = String::with_capacity(text.len());
419 for raw in crate::text::logical_lines(text) {
420 let line = crate::text::unfold(raw);
421 let start = crate::text::value_start(&line);
422 let name = crate::text::name(&line);
423 let name = name.rsplit('.').next().unwrap_or_default();
424 let uri = ["PHOTO", "LOGO", "SOUND", "KEY"].contains(&name)
425 && line[start..]
426 .get(..5)
427 .is_some_and(|v| v.eq_ignore_ascii_case("data:"));
428 match uri && line.contains("\\,") {
429 true => out.push_str(&crate::text::fold(&line.replace("\\,", ","), eol)),
430 false => out.push_str(raw),
431 }
432 }
433 out
434}
435
436/// The version a GET asks for in its `Accept` header: 4.0 when named,
437/// else 3.0, which RFC 6352 makes the default and Apple Contacts needs.
438pub fn accepted_version(accept: Option<&str>) -> VCardVersion {
439 match accept.is_some_and(|a| a.contains("version=4.0")) {
440 true => VCardVersion::V4_0,
441 false => VCardVersion::V3_0,
442 }
443}
444
445/// vCard 4.0 groups and companies in the vCard 3.0 extensions Apple
446/// Contacts reads, and back: `KIND:group` and `MEMBER` as
447/// `X-ADDRESSBOOKSERVER-KIND` and `X-ADDRESSBOOKSERVER-MEMBER`, `KIND:org`
448/// as `X-ABSHOWAS:COMPANY`, and `PREF=1` as `TYPE=pref`. sabre/vobject
449/// converts the same way.
450fn apple_forms(text: &str, version: VCardVersion) -> String {
451 let v3 = version == VCardVersion::V3_0;
452 let eol = if text.contains("\r\n") { "\r\n" } else { "\n" };
453 let mut out = String::with_capacity(text.len());
454 // vCard 3.0 has no ranks: the best ranked line of a name becomes the pref.
455 let mut best: HashMap<String, u32> = HashMap::new();
456 if v3 {
457 for raw in crate::text::logical_lines(text) {
458 let line = crate::text::unfold(raw);
459 let name = crate::text::name(&line);
460 let name = name.rsplit('.').next().unwrap_or_default().to_string();
461 for p in crate::text::param_parts(&line) {
462 if let Some(n) = p
463 .split_once('=')
464 .filter(|(k, _)| k.eq_ignore_ascii_case("PREF"))
465 .and_then(|(_, v)| v.trim().parse::<u32>().ok())
466 {
467 let b = best.entry(name.clone()).or_insert(n);
468 *b = (*b).min(n);
469 }
470 }
471 }
472 }
473 for raw in crate::text::logical_lines(text) {
474 let line = crate::text::unfold(raw);
475 let start = crate::text::value_start(&line);
476 let head = crate::text::head(&line);
477 let head_name = head.split(';').next().unwrap_or_default();
478 let (group, name) = match head_name.rsplit_once('.') {
479 Some((g, n)) => (&head_name[..=g.len()], n.to_ascii_uppercase()),
480 None => ("", head_name.to_ascii_uppercase()),
481 };
482 let value = &line[start..];
483 let lower = value.trim().to_ascii_lowercase();
484 let renamed = match (v3, name.as_str(), lower.as_str()) {
485 (true, "KIND", "group") => Some(("X-ADDRESSBOOKSERVER-KIND", "group")),
486 (true, "KIND", "org") => Some(("X-ABSHOWAS", "COMPANY")),
487 (true, "KIND", "individual") => continue,
488 (true, "MEMBER", _) => Some(("X-ADDRESSBOOKSERVER-MEMBER", value)),
489 (false, "X-ADDRESSBOOKSERVER-KIND", "group") => Some(("KIND", "group")),
490 (false, "X-ABSHOWAS", "company") => Some(("KIND", "org")),
491 (false, "X-ADDRESSBOOKSERVER-MEMBER", _) => Some(("MEMBER", value)),
492 _ => None,
493 };
494 let params = pref_params(
495 crate::text::param_parts(&line),
496 v3,
497 best.get(&name).copied().unwrap_or(1),
498 );
499 let (name, value) = renamed.unwrap_or((&name, value));
500 let mut new = format!("{group}{name}");
501 for p in &params {
502 new.push(';');
503 new.push_str(p);
504 }
505 new.push(':');
506 new.push_str(value);
507 if new == line {
508 out.push_str(raw);
509 } else {
510 out.push_str(&crate::text::fold(&new, eol));
511 }
512 }
513 out
514}
515
516/// The lowest `PREF` as a `pref` type for vCard 3.0, and back for 4.0.
517fn pref_params(params: Vec<&str>, v3: bool, lowest: u32) -> Vec<String> {
518 let mut out = Vec::with_capacity(params.len());
519 let mut pref = false;
520 for p in params {
521 let (key, value) = p.split_once('=').unwrap_or((p, ""));
522 match (v3, key.to_ascii_uppercase().as_str()) {
523 (true, "PREF") => pref |= value.trim().parse() == Ok(lowest),
524 (false, "TYPE") => {
525 let (prefs, rest): (Vec<&str>, Vec<&str>) = value
526 .trim_matches('"')
527 .split(',')
528 .partition(|t| t.eq_ignore_ascii_case("pref"));
529 pref |= !prefs.is_empty();
530 if !rest.is_empty() {
531 out.push(format!("{key}={}", rest.join(",")));
532 }
533 }
534 _ => out.push(p.to_string()),
535 }
536 }
537 if pref {
538 out.push(if v3 { "TYPE=pref" } else { "PREF=1" }.to_string());
539 }
540 out
541}
542