object.rs
⎇
Raw
1//! Validation of the calendar and address objects clients PUT.
2
3use calcard::icalendar::{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, or missing a property RFC 5545 requires.
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 if too_deep(&cal) {
62 return Err(Invalid::CalendarData);
63 }
64 let mut found: Option<CalendarObject> = None;
65 for c in root
66 .component_ids
67 .iter()
68 .filter_map(|&id| cal.components.get(id as usize))
69 {
70 let component = match c.component_type {
71 ICalendarComponentType::VTimezone => continue,
72 ICalendarComponentType::VEvent => "VEVENT",
73 ICalendarComponentType::VTodo => "VTODO",
74 ICalendarComponentType::VJournal => "VJOURNAL",
75 _ => return Err(Invalid::CalendarComponent),
76 };
77 // RFC 5545 requires DTSTART on a VEVENT without METHOD, and on a
78 // VTODO with DURATION.
79 let needs_start = match component {
80 "VEVENT" => true,
81 "VTODO" => c.has_property(&ICalendarProperty::Duration),
82 _ => false,
83 };
84 if needs_start && !c.has_property(&ICalendarProperty::Dtstart) {
85 return Err(Invalid::CalendarData);
86 }
87 let uid = c.uid().ok_or(Invalid::CalendarResource)?;
88 match &found {
89 Some(f) if f.uid != uid || f.component != component => {
90 return Err(Invalid::CalendarResource);
91 }
92 Some(_) => {}
93 None => {
94 found = Some(CalendarObject {
95 uid: uid.to_string(),
96 component,
97 })
98 }
99 }
100 }
101 let found = found.ok_or(Invalid::CalendarResource)?;
102 if !supported.contains(&found.component) {
103 return Err(Invalid::CalendarComponent);
104 }
105 Ok(found)
106}
107
108/// Levels below VCALENDAR, as in VEVENT > PARTICIPANT > VLOCATION, with
109/// room to spare. Scheduling and rendering recurse once per level.
110const MAX_NESTING: usize = 4;
111
112fn too_deep(cal: &ICalendar) -> bool {
113 let mut stack = vec![(0, 0)];
114 while let Some((i, depth)) = stack.pop() {
115 if depth > MAX_NESTING {
116 return true;
117 }
118 let ids = cal.components.get(i).map_or(&[][..], |c| &c.component_ids);
119 stack.extend(
120 ids.iter()
121 .map(|&id| id as usize)
122 .filter(|&id| id > i)
123 .map(|id| (id, depth + 1)),
124 );
125 }
126 false
127}
128
129/// Checks an address object resource (RFC 6352, 5.1) and returns its UID. A
130/// card without one is accepted: several clients omit it.
131pub fn vcard(body: &[u8]) -> Result<Option<String>, Invalid> {
132 let text = std::str::from_utf8(body).map_err(|_| Invalid::AddressData)?;
133 let mut parser = Parser::new(text);
134 let Entry::VCard(card) = parser.entry() else {
135 return Err(Invalid::AddressData);
136 };
137 if !matches!(parser.entry(), Entry::Eof) {
138 return Err(Invalid::AddressData);
139 }
140 Ok(card.uid().map(str::to_string))
141}
142
143/// `data` with `DTSTAMP:<now>` inserted after the BEGIN line of each VEVENT,
144/// VTODO, VJOURNAL and VFREEBUSY that lacks it (RFC 5545 requires it).
145/// Inserts text instead of re-serializing, so every other byte stays.
146/// `None` if nothing was missing.
147pub fn with_dtstamp(data: &[u8], now: DateTime<Utc>) -> Option<Vec<u8>> {
148 const STAMPED: [&[u8]; 4] = [b"VEVENT", b"VTODO", b"VJOURNAL", b"VFREEBUSY"];
149 let lines: Vec<&[u8]> = data.split_inclusive(|&b| b == b'\n').collect();
150 // (component, index of its BEGIN line, has DTSTAMP)
151 let mut open: Vec<(&[u8], usize, bool)> = Vec::new();
152 let mut missing = Vec::new();
153 for (i, line) in lines.iter().enumerate() {
154 if line.first().is_some_and(|b| *b == b' ' || *b == b'\t') {
155 continue;
156 }
157 let line = line.trim_ascii_end();
158 let name_end = line
159 .iter()
160 .position(|b| *b == b':' || *b == b';')
161 .unwrap_or(line.len());
162 let (name, value) = (
163 &line[..name_end],
164 line.get(name_end + 1..).unwrap_or_default(),
165 );
166 if name.eq_ignore_ascii_case(b"BEGIN") {
167 open.push((value, i, false));
168 } else if name.eq_ignore_ascii_case(b"END") {
169 if let Some((comp, begin, false)) = open.pop()
170 && STAMPED.iter().any(|s| comp.eq_ignore_ascii_case(s))
171 {
172 missing.push(begin);
173 }
174 } else if name.eq_ignore_ascii_case(b"DTSTAMP")
175 && let Some(top) = open.last_mut()
176 {
177 top.2 = true;
178 }
179 }
180 if missing.is_empty() {
181 return None;
182 }
183 let stamp = now.format("DTSTAMP:%Y%m%dT%H%M%SZ").to_string();
184 let mut out = Vec::with_capacity(data.len() + missing.len() * 28);
185 for (i, line) in lines.iter().enumerate() {
186 out.extend_from_slice(line);
187 if missing.contains(&i) {
188 let lf_only = line.ends_with(b"\n") && !line.ends_with(b"\r\n");
189 let eol: &[u8] = if lf_only { b"\n" } else { b"\r\n" };
190 if !line.ends_with(b"\n") {
191 out.extend_from_slice(eol);
192 }
193 out.extend_from_slice(stamp.as_bytes());
194 out.extend_from_slice(eol);
195 }
196 }
197 Some(out)
198}
199