protocol.rs
⎇
Raw
1//! XML request parsing, response building, and PUT body validation.
2
3use pimdav::object::{self, CalendarObject, Invalid};
4use pimdav::xml::{self, 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 let u = xml::update(body).unwrap();
39 assert_eq!(u.set.len(), 2);
40 assert_eq!(xml::text(&u.set[0]), "Work");
41 assert_eq!(u.remove, vec![Name::new(DAV, "displayname")]);
42
43 let body = br#"<c:mkcalendar xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
44 <d:set><d:prop><c:supported-calendar-component-set><c:comp name="VTODO"/></c:supported-calendar-component-set></d:prop></d:set>
45 </c:mkcalendar>"#;
46 let u = xml::update(body).unwrap();
47 assert!(Name::of(&u.set[0]).is(CALDAV, "supported-calendar-component-set"));
48 assert!(xml::update(b"").unwrap().set.is_empty());
49 assert!(xml::update(b"<d:other xmlns:d=\"DAV:\"/>").is_err());
50}
51
52#[test]
53fn multistatus_round_trips() {
54 let mut r = xml::Response::new("/a b/");
55 r.push(
56 200,
57 xml::with_text(xml::el(DAV, "displayname"), "x < y & \"z\""),
58 );
59 r.push(404, Name::new("urn:x", "odd").element());
60 r.push(
61 200,
62 xml::with_attr(xml::el(CALDAV, "comp"), "name", "VEVENT"),
63 );
64 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
65
66 let root = Element::parse(out.as_bytes()).unwrap();
67 assert!(Name::of(&root).is(DAV, "multistatus"));
68 let response = xml::child(&root, DAV, "response").unwrap();
69 assert_eq!(
70 xml::text(xml::child(response, DAV, "href").unwrap()),
71 "/a b/"
72 );
73 let stats: Vec<_> = xml::elements(response)
74 .filter(|e| e.name == "propstat")
75 .collect();
76 assert_eq!(stats.len(), 2);
77 let ok = xml::child(stats[0], DAV, "prop").unwrap();
78 assert_eq!(
79 xml::text(xml::child(ok, DAV, "displayname").unwrap()),
80 "x < y & \"z\""
81 );
82 assert_eq!(
83 xml::child(ok, CALDAV, "comp").unwrap().attributes["name"],
84 "VEVENT"
85 );
86 let missing = xml::child(stats[1], DAV, "prop").unwrap();
87 assert!(xml::child(missing, "urn:x", "odd").is_some());
88 assert_eq!(
89 xml::text(xml::child(stats[1], DAV, "status").unwrap()),
90 "HTTP/1.1 404 Not Found"
91 );
92}
93
94#[test]
95fn deep_bodies_and_control_characters() {
96 let deep = format!(
97 "<d:propfind xmlns:d=\"DAV:\"><d:prop>{}{}</d:prop></d:propfind>",
98 "<a>".repeat(20_000),
99 "</a>".repeat(20_000)
100 );
101 assert!(xml::propfind(deep.as_bytes()).is_err());
102 assert!(xml::update(deep.replace("propfind", "propertyupdate").as_bytes()).is_err());
103
104 let mut r = xml::Response::new("/a/");
105 r.push(200, xml::with_text(xml::el(DAV, "displayname"), "a\u{1}b"));
106 let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]);
107 let root = Element::parse(out.as_bytes()).unwrap();
108 let response = xml::child(&root, DAV, "response").unwrap();
109 let prop = xml::child(xml::child(response, DAV, "propstat").unwrap(), DAV, "prop").unwrap();
110 assert_eq!(
111 xml::text(xml::child(prop, DAV, "displayname").unwrap()),
112 "a\u{fffd}b"
113 );
114}
115
116fn ics(body: &str) -> Vec<u8> {
117 format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\n{body}END:VCALENDAR\r\n")
118 .into_bytes()
119}
120
121const EVENT: &str = "BEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260101T100000Z\r\nEND:VEVENT\r\n";
122
123#[test]
124fn calendar_objects() {
125 let all = ["VEVENT", "VTODO"];
126 assert_eq!(
127 object::calendar(&ics(EVENT), &all),
128 Ok(CalendarObject {
129 uid: "a".into(),
130 component: "VEVENT"
131 })
132 );
133 let override_ = EVENT.replace("DTSTART", "RECURRENCE-ID:20260102T100000Z\r\nDTSTART");
134 assert!(object::calendar(&ics(&format!("{EVENT}{override_}")), &all).is_ok());
135 let todo = "BEGIN:VTODO\r\nUID:t\r\nDTSTAMP:20260101T000000Z\r\nEND:VTODO\r\n";
136 assert!(object::calendar(&ics(todo), &all).is_ok());
137
138 let cases = [
139 (
140 ics(&format!("METHOD:REQUEST\r\n{EVENT}")),
141 Invalid::CalendarResource,
142 ),
143 (
144 ics(&format!("{EVENT}{}", EVENT.replace("UID:a", "UID:b"))),
145 Invalid::CalendarResource,
146 ),
147 (
148 ics(&format!("{EVENT}{}", EVENT.replace("VEVENT", "VTODO"))),
149 Invalid::CalendarResource,
150 ),
151 (
152 ics(&EVENT.replace("UID:a\r\n", "")),
153 Invalid::CalendarResource,
154 ),
155 (ics(""), Invalid::CalendarResource),
156 (
157 ics(&EVENT.replace("VEVENT", "VJOURNAL")),
158 Invalid::CalendarComponent,
159 ),
160 (
161 ics(&EVENT.replace("DTSTART:20260101T100000Z\r\n", "")),
162 Invalid::CalendarData,
163 ),
164 (
165 ics("BEGIN:VTODO\r\nUID:t\r\nDURATION:PT1H\r\nEND:VTODO\r\n"),
166 Invalid::CalendarData,
167 ),
168 (b"not a calendar".to_vec(), Invalid::CalendarData),
169 (vec![0xff, 0xfe], Invalid::CalendarData),
170 ([ics(EVENT), ics(EVENT)].concat(), Invalid::CalendarResource),
171 (
172 ics(&EVENT.replace(
173 "END:VEVENT",
174 &format!(
175 "{}{}END:VEVENT",
176 "BEGIN:VALARM\r\n".repeat(5),
177 "END:VALARM\r\n".repeat(5)
178 ),
179 )),
180 Invalid::CalendarData,
181 ),
182 ];
183 for (body, want) in cases {
184 assert_eq!(
185 object::calendar(&body, &all),
186 Err(want),
187 "{}",
188 String::from_utf8_lossy(&body)
189 );
190 }
191}
192
193#[test]
194fn vcards() {
195 let card = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c1\r\nFN:Bob\r\nEND:VCARD\r\n";
196 assert_eq!(object::vcard(card.as_bytes()), Ok(Some("c1".into())));
197 let no_uid = card.replace("UID:c1\r\n", "");
198 assert_eq!(object::vcard(no_uid.as_bytes()), Ok(None));
199 assert_eq!(object::vcard(&ics(EVENT)), Err(Invalid::AddressData));
200 assert_eq!(
201 object::vcard(format!("{card}{card}").as_bytes()),
202 Err(Invalid::AddressData)
203 );
204}
205
206#[test]
207fn missing_dtstamp_is_inserted() {
208 let now = chrono::DateTime::from_timestamp(1_790_000_000, 0).unwrap();
209 let sent = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nDESCRIPTION:long\r\n line\r\n\
210 BEGIN:VALARM\r\nDTSTAMP:20260101T000000Z\r\nEND:VALARM\r\nEND:VEVENT\r\n\
211 BEGIN:VTODO\r\nDTSTAMP:20260101T000000Z\r\nUID:b\r\nEND:VTODO\r\nEND:VCALENDAR\r\n";
212 let got = object::with_dtstamp(sent.as_bytes(), now).unwrap();
213 // The VALARM's DTSTAMP does not count for the VEVENT around it.
214 let want = sent.replacen(
215 "BEGIN:VEVENT\r\n",
216 "BEGIN:VEVENT\r\nDTSTAMP:20260921T141320Z\r\n",
217 1,
218 );
219 assert_eq!(String::from_utf8(got).unwrap(), want);
220 assert_eq!(object::with_dtstamp(want.as_bytes(), now), None);
221 let lf = "BEGIN:VCALENDAR\nBEGIN:VJOURNAL\nUID:c\nEND:VJOURNAL\nEND:VCALENDAR\n";
222 let got = object::with_dtstamp(lf.as_bytes(), now).unwrap();
223 assert_eq!(
224 String::from_utf8(got).unwrap(),
225 lf.replace("VJOURNAL\nUID", "VJOURNAL\nDTSTAMP:20260921T141320Z\nUID")
226 );
227}
228
229#[test]
230fn too_many_rules_are_refused() {
231 let rules = |n: usize| "RRULE:FREQ=DAILY\r\n".repeat(n);
232 let event = |n| EVENT.replace("END:VEVENT", &format!("{}END:VEVENT", rules(n)));
233 assert!(object::calendar(&ics(&event(4)), &["VEVENT"]).is_ok());
234 assert_eq!(
235 object::calendar(&ics(&event(5)), &["VEVENT"]),
236 Err(Invalid::CalendarResource)
237 );
238 let zone = |n| {
239 format!(
240 "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",
241 rules(n)
242 )
243 };
244 assert!(object::calendar(&ics(&format!("{}{EVENT}", zone(50))), &["VEVENT"]).is_ok());
245 assert_eq!(
246 object::calendar(&ics(&format!("{}{EVENT}", zone(51))), &["VEVENT"]),
247 Err(Invalid::CalendarResource)
248 );
249}
250