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 // A repeated set keeps only the last value.
45 let body = br#"<d:propertyupdate xmlns:d="DAV:">
46 <d:set><d:prop><d:displayname>A</d:displayname></d:prop></d:set>
47 <d:set><d:prop><d:displayname>B</d:displayname></d:prop></d:set>
48 </d:propertyupdate>"#;
49 let u = xml::update(body).unwrap();
50 assert_eq!(u.set.len(), 1);
51 assert_eq!(xml::text(&u.set[0]), "B");
52
53 let body = br#"<c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
54 <d:set><d:prop><c:supported-calendar-component-set><c:comp name="VTODO"/></c:supported-calendar-component-set></d:prop></d:set>
55 </c:mkcalendar>"#;
56 let u = xml::update(body).unwrap();
57 assert!(Name::of(&u.set[0]).is(CALDAV, "supported-calendar-component-set"));
58 assert!(xml::update(b"").unwrap().set.is_empty());
59 assert!(xml::update(b"<d:other xmlns:d=\"DAV:\"/>").is_err());
60}
61
62#[test]
63fn multistatus_round_trips() {
64 let mut r = xml::Response::new("/a b/");
65 r.push(
66 200,
67 xml::with_text(xml::el(DAV, "displayname"), "x < y & \"z\""),
68 );
69 r.push(404, Name::new("urn:x", "odd").element());
70 r.push(
71 200,
72 xml::with_attr(xml::el(CALDAV, "comp"), "name", "VEVENT"),
73 );
74 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
75
76 let root = Element::parse(out.as_bytes()).unwrap();
77 assert!(Name::of(&root).is(DAV, "multistatus"));
78 let response = xml::child(&root, DAV, "response").unwrap();
79 assert_eq!(
80 xml::text(xml::child(response, DAV, "href").unwrap()),
81 "/a b/"
82 );
83 let stats: Vec<_> = xml::elements(response)
84 .filter(|e| e.name == "propstat")
85 .collect();
86 assert_eq!(stats.len(), 2);
87 let ok = xml::child(stats[0], DAV, "prop").unwrap();
88 assert_eq!(
89 xml::text(xml::child(ok, DAV, "displayname").unwrap()),
90 "x < y & \"z\""
91 );
92 assert_eq!(
93 xml::child(ok, CALDAV, "comp").unwrap().attributes["name"],
94 "VEVENT"
95 );
96 let missing = xml::child(stats[1], DAV, "prop").unwrap();
97 assert!(xml::child(missing, "urn:x", "odd").is_some());
98 assert_eq!(
99 xml::text(xml::child(stats[1], DAV, "status").unwrap()),
100 "HTTP/1.1 404 Not Found"
101 );
102}
103
104#[test]
105fn deep_bodies_and_control_characters() {
106 let deep = format!(
107 "<d:propfind xmlns:d=\"DAV:\"><d:prop>{}{}</d:prop></d:propfind>",
108 "<a>".repeat(20_000),
109 "</a>".repeat(20_000)
110 );
111 assert!(xml::propfind(deep.as_bytes()).is_err());
112 assert!(xml::update(deep.replace("propfind", "propertyupdate").as_bytes()).is_err());
113
114 let mut r = xml::Response::new("/a/");
115 r.push(200, xml::with_text(xml::el(DAV, "displayname"), "a\u{1}b"));
116 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
117 let root = Element::parse(out.as_bytes()).unwrap();
118 let response = xml::child(&root, DAV, "response").unwrap();
119 let prop = xml::child(xml::child(response, DAV, "propstat").unwrap(), DAV, "prop").unwrap();
120 assert_eq!(
121 xml::text(xml::child(prop, DAV, "displayname").unwrap()),
122 "a\u{fffd}b"
123 );
124}
125
126fn ics(body: &str) -> Vec<u8> {
127 format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\n{body}END:VCALENDAR\r\n")
128 .into_bytes()
129}
130
131const EVENT: &str = "BEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260101T100000Z\r\nEND:VEVENT\r\n";
132
133#[test]
134fn calendar_objects() {
135 let all = ["VEVENT", "VTODO"];
136 assert_eq!(
137 object::calendar(&ics(EVENT), &all),
138 Ok(CalendarObject {
139 uid: "a".into(),
140 component: "VEVENT"
141 })
142 );
143 let override_ = EVENT.replace("DTSTART", "RECURRENCE-ID:20260102T100000Z\r\nDTSTART");
144 assert!(object::calendar(&ics(&format!("{EVENT}{override_}")), &all).is_ok());
145 let todo = "BEGIN:VTODO\r\nUID:t\r\nDTSTAMP:20260101T000000Z\r\nEND:VTODO\r\n";
146 assert!(object::calendar(&ics(todo), &all).is_ok());
147
148 let cases = [
149 (
150 ics(&format!("METHOD:REQUEST\r\n{EVENT}")),
151 Invalid::CalendarResource,
152 ),
153 (
154 ics(&format!("{EVENT}{}", EVENT.replace("UID:a", "UID:b"))),
155 Invalid::CalendarResource,
156 ),
157 (
158 ics(&format!("{EVENT}{}", EVENT.replace("VEVENT", "VTODO"))),
159 Invalid::CalendarResource,
160 ),
161 (
162 ics(&EVENT.replace("UID:a\r\n", "")),
163 Invalid::CalendarResource,
164 ),
165 (ics(""), Invalid::CalendarResource),
166 (
167 ics(&EVENT.replace("VEVENT", "VJOURNAL")),
168 Invalid::CalendarComponent,
169 ),
170 (
171 ics(&EVENT.replace("DTSTART:20260101T100000Z\r\n", "")),
172 Invalid::CalendarData,
173 ),
174 (
175 ics("BEGIN:VTODO\r\nUID:t\r\nDURATION:PT1H\r\nEND:VTODO\r\n"),
176 Invalid::CalendarData,
177 ),
178 (b"not a calendar".to_vec(), Invalid::CalendarData),
179 (vec![0xff, 0xfe], Invalid::CalendarData),
180 ([ics(EVENT), ics(EVENT)].concat(), Invalid::CalendarResource),
181 (
182 ics(&EVENT.replace(
183 "END:VEVENT",
184 &format!(
185 "{}{}END:VEVENT",
186 "BEGIN:VALARM\r\n".repeat(5),
187 "END:VALARM\r\n".repeat(5)
188 ),
189 )),
190 Invalid::CalendarData,
191 ),
192 ];
193 for (body, want) in cases {
194 assert_eq!(
195 object::calendar(&body, &all),
196 Err(want),
197 "{}",
198 String::from_utf8_lossy(&body)
199 );
200 }
201}
202
203#[test]
204fn vcards() {
205 let card = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c1\r\nFN:Bob\r\nEND:VCARD\r\n";
206 assert_eq!(object::vcard(card.as_bytes()), Ok(Some("c1".into())));
207 let no_uid = card.replace("UID:c1\r\n", "");
208 assert_eq!(object::vcard(no_uid.as_bytes()), Ok(None));
209 assert_eq!(object::vcard(&ics(EVENT)), Err(Invalid::AddressData));
210 assert_eq!(
211 object::vcard(format!("{card}{card}").as_bytes()),
212 Err(Invalid::AddressData)
213 );
214 let cut = card.replace("END:VCARD\r\n", "");
215 assert_eq!(object::vcard(cut.as_bytes()), Err(Invalid::AddressData));
216}
217
218#[test]
219fn missing_dtstamp_is_inserted() {
220 let now = chrono::DateTime::from_timestamp(1_790_000_000, 0).unwrap();
221 let sent = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nDESCRIPTION:long\r\n line\r\n\
222 BEGIN:VALARM\r\nDTSTAMP:20260101T000000Z\r\nEND:VALARM\r\nEND:VEVENT\r\n\
223 BEGIN:VTODO\r\nDTSTAMP:20260101T000000Z\r\nUID:b\r\nEND:VTODO\r\nEND:VCALENDAR\r\n";
224 let got = object::with_dtstamp(sent.as_bytes(), now).unwrap();
225 // The VALARM's DTSTAMP does not count for the VEVENT around it.
226 let want = sent.replacen(
227 "BEGIN:VEVENT\r\n",
228 "BEGIN:VEVENT\r\nDTSTAMP:20260921T141320Z\r\n",
229 1,
230 );
231 assert_eq!(String::from_utf8(got).unwrap(), want);
232 assert_eq!(object::with_dtstamp(want.as_bytes(), now), None);
233 let lf = "BEGIN:VCALENDAR\nBEGIN:VJOURNAL\nUID:c\nEND:VJOURNAL\nEND:VCALENDAR\n";
234 let got = object::with_dtstamp(lf.as_bytes(), now).unwrap();
235 assert_eq!(
236 String::from_utf8(got).unwrap(),
237 lf.replace("VJOURNAL\nUID", "VJOURNAL\nDTSTAMP:20260921T141320Z\nUID")
238 );
239}
240
241#[test]
242fn too_many_rules_are_refused() {
243 let rules = |n: usize| "RRULE:FREQ=DAILY\r\n".repeat(n);
244 let event = |n| EVENT.replace("END:VEVENT", &format!("{}END:VEVENT", rules(n)));
245 assert!(object::calendar(&ics(&event(4)), &["VEVENT"]).is_ok());
246 assert_eq!(
247 object::calendar(&ics(&event(5)), &["VEVENT"]),
248 Err(Invalid::CalendarResource)
249 );
250 let zone = |n| {
251 format!(
252 "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",
253 rules(n)
254 )
255 };
256 assert!(object::calendar(&ics(&format!("{}{EVENT}", zone(50))), &["VEVENT"]).is_ok());
257 assert_eq!(
258 object::calendar(&ics(&format!("{}{EVENT}", zone(51))), &["VEVENT"]),
259 Err(Invalid::CalendarResource)
260 );
261 // The cap is per zone, not per object.
262 let zones = (0..3)
263 .map(|i| zone(50).replace("TZID:X", &format!("TZID:X{i}")))
264 .collect::<String>();
265 assert!(object::calendar(&ics(&format!("{zones}{EVENT}")), &["VEVENT"]).is_ok());
266 // But zones and their rules are capped in total.
267 let zones = |n: usize, rules: usize| {
268 (0..n)
269 .map(|i| zone(rules).replace("TZID:X", &format!("TZID:X{i}")))
270 .collect::<String>()
271 };
272 assert_eq!(
273 object::calendar(&ics(&format!("{}{EVENT}", zones(51, 0))), &["VEVENT"]),
274 Err(Invalid::CalendarResource)
275 );
276 assert_eq!(
277 object::calendar(&ics(&format!("{}{EVENT}", zones(11, 50))), &["VEVENT"]),
278 Err(Invalid::CalendarResource)
279 );
280 // An observance outside any zone counts too.
281 let stray = zone(501)
282 .replace("BEGIN:VTIMEZONE\r\nTZID:X\r\n", "")
283 .replace("END:VTIMEZONE\r\n", "");
284 assert_eq!(
285 object::calendar(&ics(&format!("{stray}{EVENT}")), &["VEVENT"]),
286 Err(Invalid::CalendarResource)
287 );
288}
289
290#[test]
291fn huge_objects_are_refused() {
292 let attendees = |n: usize| {
293 let lines: String = (0..n)
294 .map(|i| format!("ATTENDEE:mailto:u{i}@x\r\n"))
295 .collect();
296 EVENT.replace("END:VEVENT", &format!("{lines}END:VEVENT"))
297 };
298 assert!(object::calendar(&ics(&attendees(2000)), &["VEVENT"]).is_ok());
299 assert_eq!(
300 object::calendar(&ics(&attendees(2001)), &["VEVENT"]),
301 Err(Invalid::CalendarResource)
302 );
303 let alarm = "BEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT5M\r\nEND:VALARM\r\n";
304 let alarms = EVENT.replace("END:VEVENT", &format!("{}END:VEVENT", alarm.repeat(4000)));
305 assert_eq!(
306 object::calendar(&ics(&alarms), &["VEVENT"]),
307 Err(Invalid::CalendarResource)
308 );
309 let card = |n: usize| {
310 let notes = "NOTE:x\r\n".repeat(n);
311 format!("BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\n{notes}END:VCARD\r\n")
312 };
313 assert!(object::vcard(card(9000).as_bytes()).is_ok());
314 assert_eq!(
315 object::vcard(card(10_000).as_bytes()),
316 Err(Invalid::AddressData)
317 );
318}
319
320#[test]
321fn a_rule_with_a_huge_count_is_refused() {
322 let with = |rule: &str| {
323 let event = EVENT.replace("END:VEVENT", &format!("{rule}\r\nEND:VEVENT"));
324 object::calendar(&ics(&event), &["VEVENT"])
325 };
326 assert!(with("RRULE:FREQ=DAILY;COUNT=100000").is_ok());
327 assert_eq!(
328 with("RRULE:FREQ=DAILY;COUNT=100001"),
329 Err(Invalid::CalendarResource)
330 );
331 assert!(with("RRULE:FREQ=MINUTELY;COUNT=10000").is_ok());
332 assert_eq!(
333 with("EXRULE:FREQ=SECONDLY;COUNT=10001"),
334 Err(Invalid::CalendarResource)
335 );
336}
337
338#[test]
339fn one_master_per_object_and_no_blank_uid() {
340 let master2 = EVENT.replace("T100000Z\r\nEND", "T110000Z\r\nEND");
341 assert_eq!(
342 object::calendar(&ics(&format!("{EVENT}{master2}")), &["VEVENT"]),
343 Err(Invalid::CalendarResource)
344 );
345 let blank = EVENT.replace("UID:a", "UID: ");
346 assert_eq!(
347 object::calendar(&ics(&blank), &["VEVENT"]),
348 Err(Invalid::CalendarResource)
349 );
350 let card = b"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\nUID: \r\nEND:VCARD\r\n";
351 assert_eq!(object::vcard(card), Ok(None));
352}
353
354#[test]
355fn a_cut_or_nested_object_is_refused() {
356 let full = ics(EVENT);
357 let cut = &full[..full.len() - "END:VCALENDAR\r\n".len()];
358 assert_eq!(
359 object::calendar(cut, &["VEVENT"]),
360 Err(Invalid::CalendarData)
361 );
362 // END:VCALENDAR would close the open VEVENT for the parser.
363 let open = EVENT.replace("END:VEVENT\r\n", "");
364 assert_eq!(
365 object::calendar(&ics(&open), &["VEVENT"]),
366 Err(Invalid::CalendarData)
367 );
368 let inner = EVENT.replace("UID:a", "UID:b");
369 let nested = EVENT.replace("END:VEVENT", &format!("{inner}END:VEVENT"));
370 assert_eq!(
371 object::calendar(&ics(&nested), &["VEVENT"]),
372 Err(Invalid::CalendarResource)
373 );
374}
375