protocol.rs
⎇
Raw
1//! XML request parsing, response building, and PUT body validation.
2
3use pimdav::object::{self, CalendarObject, Invalid};
4use pimdav::xml::{self, APPLE, CALDAV, DAV, Name, Propfind};
5use xmltree::Element;
6
7#[test]
8fn propfind_bodies() {
9 assert_eq!(xml::propfind(b""), Ok(Propfind::AllProp(vec![])));
10 let body = br#"<?xml version="1.0"?>
11 <d:propfind xmlns:d="DAV:" xmlns:x="urn:x">
12 <d:prop><d:getetag/><x:odd/></d:prop>
13 </d:propfind>"#;
14 assert_eq!(
15 xml::propfind(body),
16 Ok(Propfind::Prop(vec![
17 Name::new(DAV, "getetag"),
18 Name::new("urn:x", "odd")
19 ]))
20 );
21 let body = br#"<propfind xmlns="DAV:"><allprop/><include><getetag/></include></propfind>"#;
22 assert_eq!(
23 xml::propfind(body),
24 Ok(Propfind::AllProp(vec![Name::new(DAV, "getetag")]))
25 );
26 let body = br#"<propfind xmlns="DAV:"><propname/></propfind>"#;
27 assert_eq!(xml::propfind(body), Ok(Propfind::PropName));
28 assert!(xml::propfind(b"<nope/>").is_err());
29 assert!(xml::propfind(b"<propfind").is_err());
30}
31
32#[test]
33fn update_bodies() {
34 let body = br#"<d:propertyupdate xmlns:d="DAV:" xmlns:i="http://apple.com/ns/ical/">
35 <d:set><d:prop><d:displayname>Work</d:displayname><i:calendar-color>#ff0000</i:calendar-color></d:prop></d:set>
36 <d:remove><d:prop><d:displayname/></d:prop></d:remove>
37 </d:propertyupdate>"#;
38 // The later remove replaces the set of the same property.
39 let u = xml::update(body).unwrap();
40 assert_eq!(u.set.len(), 1);
41 assert!(Name::of(&u.set[0]).is(APPLE, "calendar-color"));
42 assert_eq!(u.remove, vec![Name::new(DAV, "displayname")]);
43
44 let body = br#"<c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
45 <d:set><d:prop><c:supported-calendar-component-set><c:comp name="VTODO"/></c:supported-calendar-component-set></d:prop></d:set>
46 </c:mkcalendar>"#;
47 let u = xml::update(body).unwrap();
48 assert!(Name::of(&u.set[0]).is(CALDAV, "supported-calendar-component-set"));
49 assert!(xml::update(b"").unwrap().set.is_empty());
50 assert!(xml::update(b"<d:other xmlns:d=\"DAV:\"/>").is_err());
51}
52
53#[test]
54fn multistatus_round_trips() {
55 let mut r = xml::Response::new("/a b/");
56 r.push(
57 200,
58 xml::with_text(xml::el(DAV, "displayname"), "x < y & \"z\""),
59 );
60 r.push(404, Name::new("urn:x", "odd").element());
61 r.push(
62 200,
63 xml::with_attr(xml::el(CALDAV, "comp"), "name", "VEVENT"),
64 );
65 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
66
67 let root = Element::parse(out.as_bytes()).unwrap();
68 assert!(Name::of(&root).is(DAV, "multistatus"));
69 let response = xml::child(&root, DAV, "response").unwrap();
70 assert_eq!(
71 xml::text(xml::child(response, DAV, "href").unwrap()),
72 "/a b/"
73 );
74 let stats: Vec<_> = xml::elements(response)
75 .filter(|e| e.name == "propstat")
76 .collect();
77 assert_eq!(stats.len(), 2);
78 let ok = xml::child(stats[0], DAV, "prop").unwrap();
79 assert_eq!(
80 xml::text(xml::child(ok, DAV, "displayname").unwrap()),
81 "x < y & \"z\""
82 );
83 assert_eq!(
84 xml::child(ok, CALDAV, "comp").unwrap().attributes["name"],
85 "VEVENT"
86 );
87 let missing = xml::child(stats[1], DAV, "prop").unwrap();
88 assert!(xml::child(missing, "urn:x", "odd").is_some());
89 assert_eq!(
90 xml::text(xml::child(stats[1], DAV, "status").unwrap()),
91 "HTTP/1.1 404 Not Found"
92 );
93}
94
95#[test]
96fn deep_bodies_and_control_characters() {
97 let deep = format!(
98 "<d:propfind xmlns:d=\"DAV:\"><d:prop>{}{}</d:prop></d:propfind>",
99 "<a>".repeat(20_000),
100 "</a>".repeat(20_000)
101 );
102 assert!(xml::propfind(deep.as_bytes()).is_err());
103 assert!(xml::update(deep.replace("propfind", "propertyupdate").as_bytes()).is_err());
104
105 let mut r = xml::Response::new("/a/");
106 r.push(200, xml::with_text(xml::el(DAV, "displayname"), "a\u{1}b"));
107 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
108 let root = Element::parse(out.as_bytes()).unwrap();
109 let response = xml::child(&root, DAV, "response").unwrap();
110 let prop = xml::child(xml::child(response, DAV, "propstat").unwrap(), DAV, "prop").unwrap();
111 assert_eq!(
112 xml::text(xml::child(prop, DAV, "displayname").unwrap()),
113 "a\u{fffd}b"
114 );
115}
116
117fn ics(body: &str) -> Vec<u8> {
118 format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\n{body}END:VCALENDAR\r\n")
119 .into_bytes()
120}
121
122const EVENT: &str = "BEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260101T100000Z\r\nEND:VEVENT\r\n";
123
124#[test]
125fn calendar_objects() {
126 let all = ["VEVENT", "VTODO"];
127 assert_eq!(
128 object::calendar(&ics(EVENT), &all),
129 Ok(CalendarObject {
130 uid: "a".into(),
131 component: "VEVENT"
132 })
133 );
134 let override_ = EVENT.replace("DTSTART", "RECURRENCE-ID:20260102T100000Z\r\nDTSTART");
135 assert!(object::calendar(&ics(&format!("{EVENT}{override_}")), &all).is_ok());
136 let todo = "BEGIN:VTODO\r\nUID:t\r\nDTSTAMP:20260101T000000Z\r\nEND:VTODO\r\n";
137 assert!(object::calendar(&ics(todo), &all).is_ok());
138
139 let cases = [
140 (
141 ics(&format!("METHOD:REQUEST\r\n{EVENT}")),
142 Invalid::CalendarResource,
143 ),
144 (
145 ics(&format!("{EVENT}{}", EVENT.replace("UID:a", "UID:b"))),
146 Invalid::CalendarResource,
147 ),
148 (
149 ics(&format!("{EVENT}{}", EVENT.replace("VEVENT", "VTODO"))),
150 Invalid::CalendarResource,
151 ),
152 (
153 ics(&EVENT.replace("UID:a\r\n", "")),
154 Invalid::CalendarResource,
155 ),
156 (ics(""), Invalid::CalendarResource),
157 (
158 ics(&EVENT.replace("VEVENT", "VJOURNAL")),
159 Invalid::CalendarComponent,
160 ),
161 (
162 ics(&EVENT.replace("DTSTART:20260101T100000Z\r\n", "")),
163 Invalid::CalendarData,
164 ),
165 (
166 ics("BEGIN:VTODO\r\nUID:t\r\nDURATION:PT1H\r\nEND:VTODO\r\n"),
167 Invalid::CalendarData,
168 ),
169 (b"not a calendar".to_vec(), Invalid::CalendarData),
170 (vec![0xff, 0xfe], Invalid::CalendarData),
171 ([ics(EVENT), ics(EVENT)].concat(), Invalid::CalendarResource),
172 (
173 ics(&EVENT.replace(
174 "END:VEVENT",
175 &format!(
176 "{}{}END:VEVENT",
177 "BEGIN:VALARM\r\n".repeat(5),
178 "END:VALARM\r\n".repeat(5)
179 ),
180 )),
181 Invalid::CalendarData,
182 ),
183 ];
184 for (body, want) in cases {
185 assert_eq!(
186 object::calendar(&body, &all),
187 Err(want),
188 "{}",
189 String::from_utf8_lossy(&body)
190 );
191 }
192}
193
194#[test]
195fn vcards() {
196 let card = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c1\r\nFN:Bob\r\nEND:VCARD\r\n";
197 assert_eq!(object::vcard(card.as_bytes()), Ok(Some("c1".into())));
198 let no_uid = card.replace("UID:c1\r\n", "");
199 assert_eq!(object::vcard(no_uid.as_bytes()), Ok(None));
200 assert_eq!(object::vcard(&ics(EVENT)), Err(Invalid::AddressData));
201 assert_eq!(
202 object::vcard(format!("{card}{card}").as_bytes()),
203 Err(Invalid::AddressData)
204 );
205 let cut = card.replace("END:VCARD\r\n", "");
206 assert_eq!(object::vcard(cut.as_bytes()), Err(Invalid::AddressData));
207}
208
209#[test]
210fn missing_dtstamp_is_inserted() {
211 let now = chrono::DateTime::from_timestamp(1_790_000_000, 0).unwrap();
212 let sent = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nDESCRIPTION:long\r\n line\r\n\
213 BEGIN:VALARM\r\nDTSTAMP:20260101T000000Z\r\nEND:VALARM\r\nEND:VEVENT\r\n\
214 BEGIN:VTODO\r\nDTSTAMP:20260101T000000Z\r\nUID:b\r\nEND:VTODO\r\nEND:VCALENDAR\r\n";
215 let got = object::with_dtstamp(sent.as_bytes(), now).unwrap();
216 // The VALARM's DTSTAMP does not count for the VEVENT around it.
217 let want = sent.replacen(
218 "BEGIN:VEVENT\r\n",
219 "BEGIN:VEVENT\r\nDTSTAMP:20260921T141320Z\r\n",
220 1,
221 );
222 assert_eq!(String::from_utf8(got).unwrap(), want);
223 assert_eq!(object::with_dtstamp(want.as_bytes(), now), None);
224 let lf = "BEGIN:VCALENDAR\nBEGIN:VJOURNAL\nUID:c\nEND:VJOURNAL\nEND:VCALENDAR\n";
225 let got = object::with_dtstamp(lf.as_bytes(), now).unwrap();
226 assert_eq!(
227 String::from_utf8(got).unwrap(),
228 lf.replace("VJOURNAL\nUID", "VJOURNAL\nDTSTAMP:20260921T141320Z\nUID")
229 );
230}
231
232#[test]
233fn too_many_rules_are_refused() {
234 let rules = |n: usize| "RRULE:FREQ=DAILY\r\n".repeat(n);
235 let event = |n| EVENT.replace("END:VEVENT", &format!("{}END:VEVENT", rules(n)));
236 assert!(object::calendar(&ics(&event(4)), &["VEVENT"]).is_ok());
237 assert_eq!(
238 object::calendar(&ics(&event(5)), &["VEVENT"]),
239 Err(Invalid::CalendarResource)
240 );
241 let zone = |n| {
242 format!(
243 "BEGIN:VTIMEZONE\r\nTZID:X\r\nBEGIN:STANDARD\r\nDTSTART:19701025T030000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\n{}END:STANDARD\r\nEND:VTIMEZONE\r\n",
244 rules(n)
245 )
246 };
247 assert!(object::calendar(&ics(&format!("{}{EVENT}", zone(50))), &["VEVENT"]).is_ok());
248 assert_eq!(
249 object::calendar(&ics(&format!("{}{EVENT}", zone(51))), &["VEVENT"]),
250 Err(Invalid::CalendarResource)
251 );
252}
253
254#[test]
255fn a_rule_with_a_huge_count_is_refused() {
256 let with = |rule: &str| {
257 let event = EVENT.replace("END:VEVENT", &format!("{rule}\r\nEND:VEVENT"));
258 object::calendar(&ics(&event), &["VEVENT"])
259 };
260 assert!(with("RRULE:FREQ=DAILY;COUNT=100000").is_ok());
261 assert_eq!(
262 with("RRULE:FREQ=DAILY;COUNT=100001"),
263 Err(Invalid::CalendarResource)
264 );
265 assert!(with("RRULE:FREQ=MINUTELY;COUNT=10000").is_ok());
266 assert_eq!(
267 with("EXRULE:FREQ=SECONDLY;COUNT=10001"),
268 Err(Invalid::CalendarResource)
269 );
270}
271
272#[test]
273fn one_master_per_object_and_no_blank_uid() {
274 let master2 = EVENT.replace("T100000Z\r\nEND", "T110000Z\r\nEND");
275 assert_eq!(
276 object::calendar(&ics(&format!("{EVENT}{master2}")), &["VEVENT"]),
277 Err(Invalid::CalendarResource)
278 );
279 let blank = EVENT.replace("UID:a", "UID: ");
280 assert_eq!(
281 object::calendar(&ics(&blank), &["VEVENT"]),
282 Err(Invalid::CalendarResource)
283 );
284 let card = b"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\nUID: \r\nEND:VCARD\r\n";
285 assert_eq!(object::vcard(card), Ok(None));
286}
287
288#[test]
289fn a_cut_or_nested_object_is_refused() {
290 let full = ics(EVENT);
291 let cut = &full[..full.len() - "END:VCALENDAR\r\n".len()];
292 assert_eq!(
293 object::calendar(cut, &["VEVENT"]),
294 Err(Invalid::CalendarData)
295 );
296 // END:VCALENDAR would close the open VEVENT for the parser.
297 let open = EVENT.replace("END:VEVENT\r\n", "");
298 assert_eq!(
299 object::calendar(&ics(&open), &["VEVENT"]),
300 Err(Invalid::CalendarData)
301 );
302 let inner = EVENT.replace("UID:a", "UID:b");
303 let nested = EVENT.replace("END:VEVENT", &format!("{inner}END:VEVENT"));
304 assert_eq!(
305 object::calendar(&ics(&nested), &["VEVENT"]),
306 Err(Invalid::CalendarResource)
307 );
308}
309