xml.rs
⎇
Raw
1//! WebDAV XML: request bodies into typed values, and response bodies out.
2
3use std::fmt::Write as _;
4
5use xmltree::{Element, XMLNode};
6
7pub const DAV: &str = "DAV:";
8pub const CALDAV: &str = "urn:ietf:params:xml:ns:caldav";
9pub const CARDDAV: &str = "urn:ietf:params:xml:ns:carddav";
10pub const CALSERVER: &str = "http://calendarserver.org/ns/";
11pub const APPLE: &str = "http://apple.com/ns/ical/";
12
13/// Prefixes declared once on every response root.
14const PREFIXES: [(&str, &str); 5] = [
15 ("d", DAV),
16 ("c", CALDAV),
17 ("card", CARDDAV),
18 ("cs", CALSERVER),
19 ("ical", APPLE),
20];
21
22/// A property or element name.
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct Name {
25 pub ns: String,
26 pub local: String,
27}
28
29impl Name {
30 pub fn new(ns: &str, local: &str) -> Self {
31 Name {
32 ns: ns.to_string(),
33 local: local.to_string(),
34 }
35 }
36
37 pub fn of(e: &Element) -> Self {
38 Name {
39 ns: e.namespace.clone().unwrap_or_default(),
40 local: e.name.clone(),
41 }
42 }
43
44 pub fn is(&self, ns: &str, local: &str) -> bool {
45 self.ns == ns && self.local == local
46 }
47
48 /// An element with this name, to be filled with a value.
49 pub fn element(&self) -> Element {
50 el(&self.ns, &self.local)
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Invalid;
56
57#[derive(Debug, Clone, PartialEq)]
58pub enum Propfind {
59 /// With the names an `include` element adds.
60 AllProp(Vec<Name>),
61 PropName,
62 Prop(Vec<Name>),
63}
64
65/// A PROPFIND body. An empty body means `allprop`.
66pub fn propfind(body: &[u8]) -> Result<Propfind, Invalid> {
67 if body.iter().all(u8::is_ascii_whitespace) {
68 return Ok(Propfind::AllProp(Vec::new()));
69 }
70 let root = parse(body, DAV, "propfind")?;
71 let names = |e: &Element| elements(e).map(Name::of).collect();
72 for e in elements(&root) {
73 match (e.namespace.as_deref(), e.name.as_str()) {
74 (Some(DAV), "prop") => return Ok(Propfind::Prop(names(e))),
75 (Some(DAV), "propname") => return Ok(Propfind::PropName),
76 (Some(DAV), "allprop") => {
77 let include = child(&root, DAV, "include").map_or_else(Vec::new, names);
78 return Ok(Propfind::AllProp(include));
79 }
80 _ => {}
81 }
82 }
83 Err(Invalid)
84}
85
86/// Properties to set and remove, from a PROPPATCH, MKCALENDAR or extended
87/// MKCOL body. An empty body sets nothing.
88#[derive(Debug, Default)]
89pub struct Update {
90 /// Property elements with their values.
91 pub set: Vec<Element>,
92 pub remove: Vec<Name>,
93}
94
95pub fn update(body: &[u8]) -> Result<Update, Invalid> {
96 let mut out = Update::default();
97 if body.iter().all(u8::is_ascii_whitespace) {
98 return Ok(out);
99 }
100 let root = tree(body)?;
101 let expected = [
102 (DAV, "propertyupdate"),
103 (CALDAV, "mkcalendar"),
104 (DAV, "mkcol"),
105 ];
106 if !expected.iter().any(|(ns, n)| Name::of(&root).is(ns, n)) {
107 return Err(Invalid);
108 }
109 // RFC 4918 applies the instructions in document order, so a later one
110 // on the same property replaces an earlier one.
111 for op in elements(&root) {
112 let props = child(op, DAV, "prop").into_iter().flat_map(elements);
113 match (op.namespace.as_deref(), op.name.as_str()) {
114 (Some(DAV), "set") => {
115 for p in props {
116 let name = Name::of(p);
117 out.remove.retain(|n| *n != name);
118 out.set.retain(|q| Name::of(q) != name);
119 out.set.push(p.clone());
120 }
121 }
122 (Some(DAV), "remove") => {
123 for name in props.map(Name::of) {
124 out.set.retain(|p| Name::of(p) != name);
125 out.remove.retain(|n| *n != name);
126 out.remove.push(name);
127 }
128 }
129 _ => {}
130 }
131 }
132 Ok(out)
133}
134
135/// Nesting allowed in a request body. Building and dropping the tree
136/// recurse once per level, so a deep body would overflow the stack.
137const MAX_DEPTH: usize = 64;
138
139/// Whether `body` nests elements deeper than [`MAX_DEPTH`]. Malformed XML is
140/// left to the parser.
141pub fn too_deep(body: &[u8]) -> bool {
142 let mut depth = 0;
143 for e in xml::reader::EventReader::new(body) {
144 match e {
145 Ok(xml::reader::XmlEvent::StartElement { .. }) if depth == MAX_DEPTH => return true,
146 Ok(xml::reader::XmlEvent::StartElement { .. }) => depth += 1,
147 Ok(xml::reader::XmlEvent::EndElement { .. }) => depth -= 1,
148 Ok(_) => {}
149 Err(_) => return false,
150 }
151 }
152 false
153}
154
155/// A request body as a tree, refused when nested deeper than [`MAX_DEPTH`].
156pub(crate) fn tree(body: &[u8]) -> Result<Element, Invalid> {
157 if too_deep(body) {
158 return Err(Invalid);
159 }
160 Element::parse(body).map_err(|_| Invalid)
161}
162
163fn parse(body: &[u8], ns: &str, local: &str) -> Result<Element, Invalid> {
164 let root = tree(body)?;
165 if Name::of(&root).is(ns, local) {
166 Ok(root)
167 } else {
168 Err(Invalid)
169 }
170}
171
172pub fn elements(e: &Element) -> impl Iterator<Item = &Element> {
173 e.children.iter().filter_map(XMLNode::as_element)
174}
175
176pub fn child<'a>(e: &'a Element, ns: &str, local: &str) -> Option<&'a Element> {
177 elements(e).find(|c| Name::of(c).is(ns, local))
178}
179
180/// The concatenated text content, trimmed.
181pub fn text(e: &Element) -> String {
182 e.get_text()
183 .map_or_else(String::new, |t| t.trim().to_string())
184}
185
186pub fn el(ns: &str, local: &str) -> Element {
187 let mut e = Element::new(local);
188 e.namespace = Some(ns.to_string());
189 e
190}
191
192pub fn with_text(mut e: Element, text: impl Into<String>) -> Element {
193 e.children.push(XMLNode::Text(text.into()));
194 e
195}
196
197pub fn with_children(mut e: Element, children: impl IntoIterator<Item = Element>) -> Element {
198 e.children
199 .extend(children.into_iter().map(XMLNode::Element));
200 e
201}
202
203pub fn with_attr(mut e: Element, name: &str, value: &str) -> Element {
204 e.attributes.insert(name.to_string(), value.to_string());
205 e
206}
207
208/// `<d:href>` elements.
209pub fn hrefs<'a>(hrefs: impl IntoIterator<Item = &'a str>) -> Vec<Element> {
210 hrefs
211 .into_iter()
212 .map(|h| with_text(el(DAV, "href"), h))
213 .collect()
214}
215
216/// One `<d:response>` of a multistatus.
217#[derive(Debug, Default)]
218pub struct Response {
219 pub href: String,
220 /// Status code and the properties that share it.
221 pub propstats: Vec<(u16, Vec<Element>)>,
222 /// The status of the whole resource, for a response without properties.
223 pub status: Option<u16>,
224 pub error: Option<Element>,
225}
226
227impl Response {
228 pub fn new(href: impl Into<String>) -> Self {
229 Response {
230 href: href.into(),
231 ..Default::default()
232 }
233 }
234
235 pub fn status(href: impl Into<String>, status: u16) -> Self {
236 Response {
237 href: href.into(),
238 status: Some(status),
239 ..Default::default()
240 }
241 }
242
243 /// Adds `prop` under `status`, next to the others with that status.
244 pub fn push(&mut self, status: u16, prop: Element) {
245 match self.propstats.iter_mut().find(|(s, _)| *s == status) {
246 Some((_, props)) => props.push(prop),
247 None => self.propstats.push((status, vec![prop])),
248 }
249 }
250}
251
252/// A `<d:multistatus>` body, or another root such as
253/// `<c:mkcalendar-response>` holding the same propstats.
254pub fn multistatus(root: &Name, responses: &[Response]) -> String {
255 multistatus_with(root, responses, None)
256}
257
258/// A multistatus with `tail`, such as a `<d:sync-token>`, after the
259/// responses.
260pub fn multistatus_with(root: &Name, responses: &[Response], tail: Option<Element>) -> String {
261 let body = responses.iter().map(|r| {
262 let mut children = vec![with_text(el(DAV, "href"), r.href.as_str())];
263 children.extend(
264 r.status
265 .map(|s| with_text(el(DAV, "status"), status_line(s))),
266 );
267 children.extend(
268 r.propstats
269 .iter()
270 .map(|(status, props)| propstat(*status, props)),
271 );
272 children.extend(
273 r.error
274 .iter()
275 .map(|e| with_children(el(DAV, "error"), [e.clone()])),
276 );
277 with_children(el(DAV, "response"), children)
278 });
279 let root = with_children(root.element(), body.chain(tail));
280 document(&root)
281}
282
283/// The propstats alone, for roots that hold them without `<d:response>`.
284pub fn propstat_document(root: &Name, propstats: &[(u16, Vec<Element>)]) -> String {
285 let root = with_children(
286 root.element(),
287 propstats.iter().map(|(s, p)| propstat(*s, p)),
288 );
289 document(&root)
290}
291
292fn propstat(status: u16, props: &[Element]) -> Element {
293 with_children(
294 el(DAV, "propstat"),
295 [
296 with_children(el(DAV, "prop"), props.iter().cloned()),
297 with_text(el(DAV, "status"), status_line(status)),
298 ],
299 )
300}
301
302/// A `<d:error>` body naming the failed precondition.
303pub fn error(condition: Element) -> String {
304 document(&with_children(el(DAV, "error"), [condition]))
305}
306
307pub fn status_line(code: u16) -> String {
308 let reason = match code {
309 200 => "OK",
310 201 => "Created",
311 204 => "No Content",
312 207 => "Multi-Status",
313 307 => "Temporary Redirect",
314 400 => "Bad Request",
315 401 => "Unauthorized",
316 403 => "Forbidden",
317 404 => "Not Found",
318 405 => "Method Not Allowed",
319 409 => "Conflict",
320 412 => "Precondition Failed",
321 413 => "Payload Too Large",
322 415 => "Unsupported Media Type",
323 423 => "Locked",
324 424 => "Failed Dependency",
325 500 => "Internal Server Error",
326 502 => "Bad Gateway",
327 503 => "Service Unavailable",
328 507 => "Insufficient Storage",
329 _ => "",
330 };
331 format!("HTTP/1.1 {code} {reason}").trim_end().to_owned()
332}
333
334pub fn document(root: &Element) -> String {
335 let mut out = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
336 write(&mut out, root, true);
337 out
338}
339
340/// Known namespaces use the fixed prefixes. Any other element declares its
341/// namespace as the default on itself.
342fn write(out: &mut String, e: &Element, root: bool) {
343 let ns = e.namespace.as_deref().unwrap_or("");
344 let tag = match PREFIXES.iter().find(|(_, uri)| *uri == ns) {
345 Some((p, _)) => format!("{p}:{}", e.name),
346 None => e.name.clone(),
347 };
348 let _ = write!(out, "<{tag}");
349 if !tag.contains(':') {
350 let _ = write!(out, " xmlns=\"{}\"", escape(ns));
351 }
352 if root {
353 for (p, uri) in PREFIXES {
354 let _ = write!(out, " xmlns:{p}=\"{uri}\"");
355 }
356 }
357 let mut attrs: Vec<_> = e.attributes.iter().collect();
358 attrs.sort();
359 for (k, v) in attrs {
360 let _ = write!(out, " {k}=\"{}\"", escape(v));
361 }
362 if e.children.is_empty() {
363 out.push_str("/>");
364 return;
365 }
366 out.push('>');
367 for c in &e.children {
368 match c {
369 XMLNode::Element(c) => write(out, c, false),
370 XMLNode::Text(t) | XMLNode::CData(t) => out.push_str(&escape(t)),
371 _ => {}
372 }
373 }
374 let _ = write!(out, "</{tag}>");
375}
376
377fn escape(s: &str) -> String {
378 let mut out = String::with_capacity(s.len());
379 for c in s.chars() {
380 match c {
381 '&' => out.push_str("&amp;"),
382 '<' => out.push_str("&lt;"),
383 '>' => out.push_str("&gt;"),
384 '"' => out.push_str("&quot;"),
385 // A raw CR reaches the client as LF: XML parsers normalize line
386 // ends. iCalendar and vCard data need their CRLF.
387 '\r' => out.push_str("&#13;"),
388 // XML 1.0 forbids these even as character references.
389 '\u{0}'..='\u{8}'
390 | '\u{b}'
391 | '\u{c}'
392 | '\u{e}'..='\u{1f}'
393 | '\u{fffe}'
394 | '\u{ffff}' => out.push('\u{fffd}'),
395 _ => out.push(c),
396 }
397 }
398 out
399}
400