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, original, 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/// Bytes one object may expand into in one response.
28const MAX_EXPANDED_BYTES: usize = 16 * 1024 * 1024;
29
30#[derive(Debug, Clone, Default, PartialEq)]
31pub struct CalendarData {
32 /// `None` returns every component and property.
33 pub comp: Option<CompSelect>,
34 pub expand: Option<TimeRange>,
35 pub limit_recurrence: Option<TimeRange>,
36 pub limit_freebusy: Option<TimeRange>,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct CompSelect {
41 /// Upper case.
42 pub name: String,
43 /// `None` for `allprop`.
44 pub props: Option<Vec<PropSelect>>,
45 /// `None` for `allcomp`.
46 pub comps: Option<Vec<CompSelect>>,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub struct PropSelect {
51 pub name: String,
52 pub novalue: bool,
53}
54
55#[derive(Debug, Clone, Default, PartialEq)]
56pub struct AddressData {
57 /// `None` for `allprop`.
58 pub props: Option<Vec<PropSelect>>,
59 pub version: Option<VCardVersion>,
60}
61
62/// The expansion would exceed [`MAX_EXPANDED`] instances or
63/// [`MAX_EXPANDED_BYTES`].
64#[derive(Debug, PartialEq, Eq)]
65pub struct TooManyInstances;
66
67pub fn calendar_request(e: &Element) -> Result<CalendarData, Refused> {
68 let content_type = e.attributes.get("content-type").map(String::as_str);
69 let version = e.attributes.get("version").map(String::as_str);
70 if content_type.is_some_and(|t| t != "text/calendar") || version.is_some_and(|v| v != "2.0") {
71 return Err(Refused::Condition(Name::new(
72 CALDAV,
73 "supported-calendar-data",
74 )));
75 }
76 let mut d = CalendarData::default();
77 for c in elements(e).filter(|c| Name::of(c).ns == CALDAV) {
78 match c.name.as_str() {
79 "comp" => d.comp = Some(comp_select(c)?),
80 "expand" => d.expand = Some(time_range(c)?),
81 "limit-recurrence-set" => d.limit_recurrence = Some(time_range(c)?),
82 "limit-freebusy-set" => d.limit_freebusy = Some(time_range(c)?),
83 _ => {}
84 }
85 }
86 Ok(d)
87}
88
89fn comp_select(e: &Element) -> Result<CompSelect, Refused> {
90 let name = e.attributes.get("name").ok_or(Refused::Invalid)?;
91 let has = |local: &str| elements(e).any(|c| Name::of(c).is(CALDAV, local));
92 // Clients send an empty `<comp name="VTIMEZONE"/>` for the whole zone.
93 let all = !["allprop", "prop", "allcomp", "comp"].into_iter().any(has);
94 let props = (!all && !has("allprop")).then(|| prop_selects(e, CALDAV));
95 let comps = match all || has("allcomp") {
96 true => None,
97 false => Some(
98 elements(e)
99 .filter(|c| Name::of(c).is(CALDAV, "comp"))
100 .map(comp_select)
101 .collect::<Result<_, _>>()?,
102 ),
103 };
104 Ok(CompSelect {
105 name: name.to_ascii_uppercase(),
106 props,
107 comps,
108 })
109}
110
111fn prop_selects(e: &Element, ns: &str) -> Vec<PropSelect> {
112 elements(e)
113 .filter(|c| Name::of(c).is(ns, "prop"))
114 .filter_map(|c| {
115 Some(PropSelect {
116 name: c.attributes.get("name")?.clone(),
117 novalue: c.attributes.get("novalue").map(String::as_str) == Some("yes"),
118 })
119 })
120 .collect()
121}
122
123pub fn address_request(e: &Element) -> Result<AddressData, Refused> {
124 let unsupported = || Refused::Condition(Name::new(CARDDAV, "supported-address-data"));
125 if e.attributes
126 .get("content-type")
127 .is_some_and(|t| t != "text/vcard")
128 {
129 return Err(unsupported());
130 }
131 // RFC 6352, 10.4: without a version, 3.0.
132 let version = match e.attributes.get("version").map(String::as_str) {
133 None | Some("3.0") => Some(VCardVersion::V3_0),
134 Some("4.0") => Some(VCardVersion::V4_0),
135 Some(_) => return Err(unsupported()),
136 };
137 let all = elements(e).any(|c| Name::of(c).is(CARDDAV, "allprop"));
138 let props = prop_selects(e, CARDDAV);
139 Ok(AddressData {
140 props: (!all && !props.is_empty()).then_some(props),
141 version,
142 })
143}
144
145/// The calendar-data of one object, and how many instances an `expand`
146/// produced. The stored text is returned unchanged unless the request
147/// narrows or expands it.
148pub fn calendar_data(
149 raw: &str,
150 req: &CalendarData,
151 floating: &Zone,
152) -> Result<(String, usize), TooManyInstances> {
153 if *req == CalendarData::default() {
154 return Ok((raw.to_string(), 0));
155 }
156 let Ok(mut cal) = ICalendar::parse(raw) else {
157 return Ok((raw.to_string(), 0));
158 };
159 let mut instances = 0;
160 if let Some(r) = &req.expand {
161 (cal, instances) = expanded(&cal, r, floating, req.comp.as_ref())?;
162 }
163 if let Some(r) = &req.limit_recurrence {
164 cal = limit_recurrence(&cal, r, floating);
165 }
166 if let Some(r) = &req.limit_freebusy {
167 limit_freebusy(&mut cal, r, floating);
168 }
169 if let Some(sel) = &req.comp {
170 let mut out = Vec::new();
171 copy(&cal, 0, Some(sel), &mut out);
172 cal = ICalendar { components: out };
173 }
174 Ok((cal.to_string(), instances))
175}
176
177/// One component per instance in `range`, in UTC, with a RECURRENCE-ID and
178/// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates.
179fn expanded(
180 cal: &ICalendar,
181 range: &TimeRange,
182 floating: &Zone,
183 sel: Option<&CompSelect>,
184) -> Result<(ICalendar, usize), TooManyInstances> {
185 let exp = expand(cal, range.clone(), floating.clone());
186 let count = exp.instances.len();
187 if exp.truncated || count > MAX_EXPANDED {
188 return Err(TooManyInstances);
189 }
190 // Each instance copies its component, so many attendees times many
191 // instances can be huge before anything is written.
192 let mut sizes = HashMap::new();
193 let mut bytes = 0;
194 for x in &exp.instances {
195 // Measured as written: with the requested selection.
196 bytes += *sizes.entry(x.component).or_insert_with(|| {
197 let mut one = vec![root(cal)];
198 let at = copy(cal, x.component, None, &mut one);
199 one[0].component_ids.push(at);
200 let mut one = ICalendar { components: one };
201 if let Some(sel) = sel {
202 let mut picked = Vec::new();
203 copy(&one, 0, Some(sel), &mut picked);
204 one = ICalendar { components: picked };
205 }
206 one.to_string().len()
207 });
208 if bytes > MAX_EXPANDED_BYTES {
209 return Err(TooManyInstances);
210 }
211 }
212 let zones = Zones::new(cal, floating.clone());
213 let mut out = vec![root(cal)];
214 for x in exp.instances {
215 let src = &cal.components[x.component];
216 let date = src
217 .property(&ICalendarProperty::Dtstart)
218 .and_then(|e| e.values.first()?.as_partial_date_time())
219 .is_some_and(|p| p.hour.is_none());
220 let at = copy(cal, x.component, None, &mut out);
221 let c = &mut out[at as usize];
222 c.entries.retain(|e| {
223 !matches!(
224 e.name,
225 ICalendarProperty::Rrule
226 | ICalendarProperty::Rdate
227 | ICalendarProperty::Exdate
228 | ICalendarProperty::Exrule
229 | ICalendarProperty::Dtstart
230 | ICalendarProperty::Dtend
231 | ICalendarProperty::Due
232 | ICalendarProperty::RecurrenceId
233 )
234 });
235 in_utc(&mut c.entries, &zones);
236 let when = |name: ICalendarProperty, t: DateTime<Utc>| {
237 let (value, params) = if date {
238 let day = floating.to_local(t).and_utc().timestamp();
239 let param = ICalendarParameter::new(
240 ICalendarParameterName::Value,
241 ICalendarParameterValue::Value(ICalendarValueType::Date),
242 );
243 (PartialDateTime::from_date_timestamp(day), vec![param])
244 } else {
245 (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![])
246 };
247 ICalendarEntry {
248 name,
249 params,
250 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
251 }
252 };
253 c.entries.push(when(ICalendarProperty::Dtstart, x.start));
254 if src.has_property(&ICalendarProperty::Dtend) {
255 c.entries.push(when(ICalendarProperty::Dtend, x.end));
256 }
257 if src.has_property(&ICalendarProperty::Due) {
258 c.entries.push(when(ICalendarProperty::Due, x.end));
259 }
260 if let Some(rid) = x.recurrence_id {
261 c.entries.push(when(ICalendarProperty::RecurrenceId, rid));
262 }
263 out[0].component_ids.push(at);
264 }
265 // A task or journal without DTSTART has no instances to expand.
266 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
267 let c = &cal.components[i as usize];
268 if c.component_type != ICalendarComponentType::VTimezone
269 && !c.has_property(&ICalendarProperty::Dtstart)
270 {
271 let at = copy(cal, i as usize, None, &mut out);
272 in_utc(&mut out[at as usize].entries, &zones);
273 out[0].component_ids.push(at);
274 }
275 }
276 Ok((ICalendar { components: out }, count))
277}
278
279/// Date-times in UTC: an expanded answer holds no VTIMEZONE.
280fn in_utc(entries: &mut [ICalendarEntry], zones: &Zones) {
281 for e in entries {
282 let tzid = e.tz_id().map(str::to_string);
283 for v in &mut e.values {
284 if let ICalendarValue::PartialDateTime(p) = v
285 && p.hour.is_some()
286 && let Some(s) = stamp(zones, p, tzid.as_deref())
287 {
288 **p = PartialDateTime::from_utc_timestamp(s.utc().timestamp());
289 }
290 }
291 e.params.retain(|p| p.name != ICalendarParameterName::Tzid);
292 }
293}
294
295/// The masters, and only the overrides that affect `range` (RFC 4791,
296/// 9.6.6).
297fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar {
298 let exp = expand(cal, range.clone(), floating.clone());
299 let used: HashSet<usize> = exp.instances.iter().map(|x| x.component).collect();
300 let zones = Zones::new(cal, floating.clone());
301 // An override moved out of the range still removes an instance in it.
302 let master = cal.components.iter().position(|c| {
303 matches!(
304 c.component_type,
305 ICalendarComponentType::VEvent
306 | ICalendarComponentType::VTodo
307 | ICalendarComponentType::VJournal
308 ) && !c.has_property(&ICalendarProperty::RecurrenceId)
309 });
310 let replaces_in_range = |rid: &ICalendarEntry| {
311 let Some(s) = rid
312 .values
313 .first()
314 .and_then(|v| v.as_partial_date_time())
315 .and_then(|p| stamp(&zones, p, rid.tz_id()))
316 else {
317 return false;
318 };
319 let (start, end) = master
320 .and_then(|m| original(cal, &zones, m, &s))
321 .unwrap_or((s.utc(), s.utc()));
322 match start == end {
323 true => range.contains(&start),
324 false => start < range.end && end > range.start,
325 }
326 };
327 let mut out = vec![root(cal)];
328 for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) {
329 let c = &cal.components[i as usize];
330 let keep = match c.property(&ICalendarProperty::RecurrenceId) {
331 None => true,
332 Some(rid) => {
333 exp.truncated
334 || used.contains(&(i as usize))
335 || replaces_in_range(rid)
336 || rid.parameter(&ICalendarParameterName::Range).is_some()
337 }
338 };
339 if keep {
340 let at = copy(cal, i as usize, None, &mut out);
341 out[0].component_ids.push(at);
342 }
343 }
344 ICalendar { components: out }
345}
346
347/// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7).
348fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) {
349 let zones = Zones::new(cal, floating.clone());
350 for c in cal
351 .components
352 .iter_mut()
353 .filter(|c| c.component_type == ICalendarComponentType::VFreebusy)
354 {
355 for e in c
356 .entries
357 .iter_mut()
358 .filter(|e| e.name == ICalendarProperty::Freebusy)
359 {
360 let tzid = e.tz_id().map(str::to_string);
361 e.values.retain(|v| {
362 period(&zones, tzid.as_deref(), v)
363 .is_some_and(|(s, e)| s < range.end && e > range.start)
364 });
365 }
366 c.entries
367 .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty());
368 }
369}
370
371/// The VCALENDAR with its properties and no components.
372fn root(cal: &ICalendar) -> ICalendarComponent {
373 ICalendarComponent {
374 component_type: ICalendarComponentType::VCalendar,
375 entries: cal
376 .components
377 .first()
378 .map(|c| c.entries.clone())
379 .unwrap_or_default(),
380 component_ids: Vec::new(),
381 }
382}
383
384/// Appends component `i` of `src` and its sub-components, narrowed by `sel`,
385/// to `out`. Returns the new index.
386fn copy(
387 src: &ICalendar,
388 i: usize,
389 sel: Option<&CompSelect>,
390 out: &mut Vec<ICalendarComponent>,
391) -> u32 {
392 let c = &src.components[i];
393 let entries = match sel.and_then(|s| s.props.as_ref()) {
394 None => c.entries.clone(),
395 Some(props) => c
396 .entries
397 .iter()
398 .filter_map(|e| {
399 let p = props
400 .iter()
401 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
402 let mut e = e.clone();
403 if p.novalue {
404 e.values.clear();
405 }
406 Some(e)
407 })
408 .collect(),
409 };
410 let at = out.len();
411 out.push(ICalendarComponent {
412 component_type: c.component_type.clone(),
413 entries,
414 component_ids: Vec::new(),
415 });
416 for &child in &c.component_ids {
417 let child = child as usize;
418 let child_sel = match sel.map(|s| &s.comps) {
419 None | Some(None) => None,
420 Some(Some(list)) => {
421 let name = src.components[child].component_type.as_str();
422 match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) {
423 Some(s) => Some(s),
424 None => continue,
425 }
426 }
427 };
428 let id = copy(src, child, child_sel, out);
429 out[at].component_ids.push(id);
430 }
431 at as u32
432}
433
434/// The address-data of one card. The stored text is returned unchanged
435/// unless the request narrows it or asks for another version.
436pub fn address_data(raw: &str, req: &AddressData) -> String {
437 if *req == AddressData::default() {
438 return raw.to_string();
439 }
440 let Ok(card) = VCard::parse(raw) else {
441 return raw.to_string();
442 };
443 let version = req.version.or(card.version()).unwrap_or_default();
444 if req.props.is_none() && Some(version) == card.version() {
445 return raw.to_string();
446 }
447 // calcard's 4.0 writer drops a `pref` type, so map before parsing.
448 let card = match version {
449 VCardVersion::V4_0 => VCard::parse(apple_forms(raw, version)).unwrap_or(card),
450 _ => card,
451 };
452 let entries = match &req.props {
453 None => card.entries,
454 Some(props) => card
455 .entries
456 .into_iter()
457 .filter_map(|mut e| {
458 let p = props
459 .iter()
460 .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?;
461 if p.novalue {
462 e.values.clear();
463 }
464 Some(e)
465 })
466 .collect(),
467 };
468 let mut out = String::new();
469 let _ = VCard { entries }.write_to(&mut out, version);
470 match version {
471 VCardVersion::V4_0 => raw_data_uris(&out),
472 _ => apple_forms(&out, version),
473 }
474}
475
476/// calcard's 4.0 writer escapes the comma of a `data:` URI as TEXT, which
477/// breaks the URI. Binary values have no backslash to keep. A vCard 3 PHOTO
478/// without TYPE becomes `data:;base64,`, so the media type comes from the
479/// first bytes.
480fn raw_data_uris(text: &str) -> String {
481 let eol = if text.contains("\r\n") { "\r\n" } else { "\n" };
482 let mut out = String::with_capacity(text.len());
483 for raw in crate::text::logical_lines(text) {
484 let line = crate::text::unfold(raw);
485 let start = crate::text::value_start(&line);
486 let name = crate::text::name(&line);
487 let name = name.rsplit('.').next().unwrap_or_default();
488 let uri = ["PHOTO", "LOGO", "SOUND", "KEY"].contains(&name)
489 && line[start..]
490 .get(..5)
491 .is_some_and(|v| v.eq_ignore_ascii_case("data:"));
492 if !uri {
493 out.push_str(raw);
494 continue;
495 }
496 let line = line.replace("\\,", ",");
497 let (head, value) = line.split_at(crate::text::value_start(&line));
498 const BARE: &str = "data:;base64,";
499 let data = value
500 .get(..BARE.len())
501 .filter(|p| p.eq_ignore_ascii_case(BARE))
502 .map(|_| &value[BARE.len()..]);
503 let sniffed = data.and_then(|d| match d {
504 d if d.starts_with("iVBORw0KGgo") => Some("image/png"),
505 d if d.starts_with("/9j/") => Some("image/jpeg"),
506 d if d.starts_with("R0lGOD") => Some("image/gif"),
507 d if d.starts_with("UklG") && d.get(12..16) == Some("RUJQ") => Some("image/webp"),
508 _ => None,
509 });
510 let line = match (sniffed, data) {
511 (Some(t), Some(d)) => format!("{head}data:{t};base64,{d}"),
512 _ => line.clone(),
513 };
514 out.push_str(&crate::text::fold(&line, eol));
515 }
516 out
517}
518
519/// The version a GET asks for in its `Accept` header: 4.0 when named,
520/// else 3.0, which RFC 6352 makes the default and Apple Contacts needs.
521pub fn accepted_version(accept: Option<&str>) -> VCardVersion {
522 match accept.is_some_and(|a| a.contains("version=4.0")) {
523 true => VCardVersion::V4_0,
524 false => VCardVersion::V3_0,
525 }
526}
527
528/// vCard 4.0 groups and companies in the vCard 3.0 extensions Apple
529/// Contacts reads, and back: `KIND:group` and `MEMBER` as
530/// `X-ADDRESSBOOKSERVER-KIND` and `X-ADDRESSBOOKSERVER-MEMBER`, `KIND:org`
531/// as `X-ABSHOWAS:COMPANY`, and `PREF=1` as `TYPE=pref`. sabre/vobject
532/// converts the same way.
533fn apple_forms(text: &str, version: VCardVersion) -> String {
534 let v3 = version == VCardVersion::V3_0;
535 let eol = if text.contains("\r\n") { "\r\n" } else { "\n" };
536 let mut out = String::with_capacity(text.len());
537 // vCard 3.0 has no ranks: the best ranked line of a name becomes the pref.
538 let mut best: HashMap<String, u32> = HashMap::new();
539 if v3 {
540 for raw in crate::text::logical_lines(text) {
541 let line = crate::text::unfold(raw);
542 let name = crate::text::name(&line);
543 let name = name.rsplit('.').next().unwrap_or_default().to_string();
544 for p in crate::text::param_parts(&line) {
545 if let Some(n) = p
546 .split_once('=')
547 .filter(|(k, _)| k.eq_ignore_ascii_case("PREF"))
548 .and_then(|(_, v)| v.trim().parse::<u32>().ok())
549 {
550 let b = best.entry(name.clone()).or_insert(n);
551 *b = (*b).min(n);
552 }
553 }
554 }
555 }
556 for raw in crate::text::logical_lines(text) {
557 let line = crate::text::unfold(raw);
558 let start = crate::text::value_start(&line);
559 let head = crate::text::head(&line);
560 let head_name = head.split(';').next().unwrap_or_default();
561 let (group, name) = match head_name.rsplit_once('.') {
562 Some((g, n)) => (&head_name[..=g.len()], n.to_ascii_uppercase()),
563 None => ("", head_name.to_ascii_uppercase()),
564 };
565 let value = &line[start..];
566 let lower = value.trim().to_ascii_lowercase();
567 let renamed = match (v3, name.as_str(), lower.as_str()) {
568 (true, "KIND", "group") => Some(("X-ADDRESSBOOKSERVER-KIND", "group")),
569 (true, "KIND", "org") => Some(("X-ABSHOWAS", "COMPANY")),
570 (true, "KIND", "individual") => continue,
571 (true, "MEMBER", _) => Some(("X-ADDRESSBOOKSERVER-MEMBER", value)),
572 (false, "X-ADDRESSBOOKSERVER-KIND", "group") => Some(("KIND", "group")),
573 (false, "X-ABSHOWAS", "company") => Some(("KIND", "org")),
574 (false, "X-ADDRESSBOOKSERVER-MEMBER", _) => Some(("MEMBER", value)),
575 _ => None,
576 };
577 let params = pref_params(
578 crate::text::param_parts(&line),
579 v3,
580 best.get(&name).copied().unwrap_or(1),
581 );
582 let (name, value) = renamed.unwrap_or((&name, value));
583 let (params, value) = plain_forms(v3, &line, name, params, value);
584 let mut new = format!("{group}{name}");
585 for p in &params {
586 new.push(';');
587 new.push_str(p);
588 }
589 new.push(':');
590 new.push_str(&value);
591 if new == line {
592 out.push_str(raw);
593 } else {
594 out.push_str(&crate::text::fold(&new, eol));
595 }
596 }
597 out
598}
599
600/// vCard 4.0 values vCard 3.0 readers reject, and back: a `tel:` URI as the
601/// bare number, and a birthday without year as Apple writes it, with
602/// `X-APPLE-OMIT-YEAR`.
603fn plain_forms(
604 v3: bool,
605 line: &str,
606 name: &str,
607 params: Vec<String>,
608 value: &str,
609) -> (Vec<String>, String) {
610 let key = |p: &str, k: &str| {
611 p.split_once('=')
612 .is_some_and(|(pk, _)| pk.trim().eq_ignore_ascii_case(k))
613 };
614 let param = |k: &str| crate::text::param(line, k);
615 let digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
616 match (v3, name) {
617 (true, "TEL") => {
618 let uri = param("VALUE").is_some_and(|v| v.eq_ignore_ascii_case("uri"));
619 match value.get(..4).filter(|s| s.eq_ignore_ascii_case("tel:")) {
620 Some(_) if uri => (
621 params.into_iter().filter(|p| !key(p, "VALUE")).collect(),
622 value[4..].to_string(),
623 ),
624 _ => (params, value.to_string()),
625 }
626 }
627 (true, "BDAY") => {
628 let md = value.trim().strip_prefix("--").map(|v| v.replace('-', ""));
629 match md {
630 Some(md) if md.len() == 4 && digits(&md) => {
631 let mut params = params;
632 params.push("X-APPLE-OMIT-YEAR=1604".to_string());
633 (params, format!("1604-{}-{}", &md[..2], &md[2..]))
634 }
635 _ => (params, value.to_string()),
636 }
637 }
638 (false, "BDAY") => {
639 let year = param("X-APPLE-OMIT-YEAR");
640 let date = value
641 .trim()
642 .split(['T', 't'])
643 .next()
644 .unwrap_or_default()
645 .replace('-', "");
646 match year {
647 Some(y) if date.len() == 8 && digits(&date) && date[..4] == y => (
648 params
649 .into_iter()
650 .filter(|p| !key(p, "X-APPLE-OMIT-YEAR"))
651 .collect(),
652 format!("--{}", &date[4..]),
653 ),
654 _ => (params, value.to_string()),
655 }
656 }
657 _ => (params, value.to_string()),
658 }
659}
660
661/// The lowest `PREF` as a `pref` type for vCard 3.0, and back for 4.0.
662fn pref_params(params: Vec<&str>, v3: bool, lowest: u32) -> Vec<String> {
663 let mut out = Vec::with_capacity(params.len());
664 let mut pref = false;
665 for p in params {
666 let (key, value) = p.split_once('=').unwrap_or((p, ""));
667 match (v3, key.to_ascii_uppercase().as_str()) {
668 (true, "PREF") => pref |= value.trim().parse() == Ok(lowest),
669 (false, "TYPE") => {
670 let (prefs, rest): (Vec<&str>, Vec<&str>) = value
671 .trim_matches('"')
672 .split(',')
673 .partition(|t| t.eq_ignore_ascii_case("pref"));
674 pref |= !prefs.is_empty();
675 if !rest.is_empty() {
676 out.push(format!("{key}={}", rest.join(",")));
677 }
678 }
679 _ => out.push(p.to_string()),
680 }
681 }
682 if pref {
683 out.push(if v3 { "TYPE=pref" } else { "PREF=1" }.to_string());
684 }
685 out
686}
687