object.rs
⎇
Raw
1//! Validation of the calendar and address objects clients PUT.
2
3use calcard::icalendar::{ICalendarComponentType, ICalendarProperty};
4use calcard::{Entry, Parser};
5use chrono::{DateTime, Utc};
6use xmltree::Element;
7
8use crate::xml::{CALDAV, CARDDAV, el};
9
10/// Why a PUT body is refused, as the precondition the RFCs name.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Invalid {
13 /// Not parseable as iCalendar.
14 CalendarData,
15 /// Parseable, but not one CalDAV object: several UIDs, mixed component
16 /// types, a METHOD, or no component at all.
17 CalendarResource,
18 /// A component type the collection does not take.
19 CalendarComponent,
20 /// Not parseable as one vCard.
21 AddressData,
22}
23
24impl Invalid {
25 pub fn condition(self) -> Element {
26 match self {
27 Invalid::CalendarData => el(CALDAV, "valid-calendar-data"),
28 Invalid::CalendarResource => el(CALDAV, "valid-calendar-object-resource"),
29 Invalid::CalendarComponent => el(CALDAV, "supported-calendar-component"),
30 Invalid::AddressData => el(CARDDAV, "valid-address-data"),
31 }
32 }
33}
34
35/// What the store needs to know about a valid calendar object.
36#[derive(Debug, PartialEq, Eq)]
37pub struct CalendarObject {
38 pub uid: String,
39 /// `VEVENT`, `VTODO` or `VJOURNAL`.
40 pub component: &'static str,
41}
42
43/// Checks a calendar object resource (RFC 4791, 4.1). `supported` lists the
44/// component types the collection takes.
45pub fn calendar(body: &[u8], supported: &[&str]) -> Result<CalendarObject, Invalid> {
46 let text = std::str::from_utf8(body).map_err(|_| Invalid::CalendarData)?;
47 let mut parser = Parser::new(text);
48 let Entry::ICalendar(cal) = parser.entry() else {
49 return Err(Invalid::CalendarData);
50 };
51 if !matches!(parser.entry(), Entry::Eof) {
52 return Err(Invalid::CalendarResource);
53 }
54 let root = cal.components.first().ok_or(Invalid::CalendarData)?;
55 if root.component_type != ICalendarComponentType::VCalendar {
56 return Err(Invalid::CalendarData);
57 }
58 if root.has_property(&ICalendarProperty::Method) {
59 return Err(Invalid::CalendarResource);
60 }
61 let mut found: Option<CalendarObject> = None;
62 for c in root
63 .component_ids
64 .iter()
65 .filter_map(|&id| cal.components.get(id as usize))
66 {
67 let component = match c.component_type {
68 ICalendarComponentType::VTimezone => continue,
69 ICalendarComponentType::VEvent => "VEVENT",
70 ICalendarComponentType::VTodo => "VTODO",
71 ICalendarComponentType::VJournal => "VJOURNAL",
72 _ => return Err(Invalid::CalendarComponent),
73 };
74 let uid = c.uid().ok_or(Invalid::CalendarResource)?;
75 match &found {
76 Some(f) if f.uid != uid || f.component != component => {
77 return Err(Invalid::CalendarResource);
78 }
79 Some(_) => {}
80 None => {
81 found = Some(CalendarObject {
82 uid: uid.to_string(),
83 component,
84 })
85 }
86 }
87 }
88 let found = found.ok_or(Invalid::CalendarResource)?;
89 if !supported.contains(&found.component) {
90 return Err(Invalid::CalendarComponent);
91 }
92 Ok(found)
93}
94
95/// Checks an address object resource (RFC 6352, 5.1) and returns its UID. A
96/// card without one is accepted: several clients omit it.
97pub fn vcard(body: &[u8]) -> Result<Option<String>, Invalid> {
98 let text = std::str::from_utf8(body).map_err(|_| Invalid::AddressData)?;
99 let mut parser = Parser::new(text);
100 let Entry::VCard(card) = parser.entry() else {
101 return Err(Invalid::AddressData);
102 };
103 if !matches!(parser.entry(), Entry::Eof) {
104 return Err(Invalid::AddressData);
105 }
106 Ok(card.uid().map(str::to_string))
107}
108
109/// `data` with `DTSTAMP:<now>` inserted after the BEGIN line of each VEVENT,
110/// VTODO, VJOURNAL and VFREEBUSY that lacks it (RFC 5545 requires it).
111/// Inserts text instead of re-serializing, so every other byte stays.
112/// `None` if nothing was missing.
113pub fn with_dtstamp(data: &[u8], now: DateTime<Utc>) -> Option<Vec<u8>> {
114 const STAMPED: [&[u8]; 4] = [b"VEVENT", b"VTODO", b"VJOURNAL", b"VFREEBUSY"];
115 let lines: Vec<&[u8]> = data.split_inclusive(|&b| b == b'\n').collect();
116 // (component, index of its BEGIN line, has DTSTAMP)
117 let mut open: Vec<(&[u8], usize, bool)> = Vec::new();
118 let mut missing = Vec::new();
119 for (i, line) in lines.iter().enumerate() {
120 if line.first().is_some_and(|b| *b == b' ' || *b == b'\t') {
121 continue;
122 }
123 let line = line.trim_ascii_end();
124 let name_end = line
125 .iter()
126 .position(|b| *b == b':' || *b == b';')
127 .unwrap_or(line.len());
128 let (name, value) = (
129 &line[..name_end],
130 line.get(name_end + 1..).unwrap_or_default(),
131 );
132 if name.eq_ignore_ascii_case(b"BEGIN") {
133 open.push((value, i, false));
134 } else if name.eq_ignore_ascii_case(b"END") {
135 if let Some((comp, begin, false)) = open.pop()
136 && STAMPED.iter().any(|s| comp.eq_ignore_ascii_case(s))
137 {
138 missing.push(begin);
139 }
140 } else if name.eq_ignore_ascii_case(b"DTSTAMP")
141 && let Some(top) = open.last_mut()
142 {
143 top.2 = true;
144 }
145 }
146 if missing.is_empty() {
147 return None;
148 }
149 let stamp = now.format("DTSTAMP:%Y%m%dT%H%M%SZ").to_string();
150 let mut out = Vec::with_capacity(data.len() + missing.len() * 28);
151 for (i, line) in lines.iter().enumerate() {
152 out.extend_from_slice(line);
153 if missing.contains(&i) {
154 let lf_only = line.ends_with(b"\n") && !line.ends_with(b"\r\n");
155 let eol: &[u8] = if lf_only { b"\n" } else { b"\r\n" };
156 if !line.ends_with(b"\n") {
157 out.extend_from_slice(eol);
158 }
159 out.extend_from_slice(stamp.as_bytes());
160 out.extend_from_slice(eol);
161 }
162 }
163 Some(out)
164}
165