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.push(p.clone());
119 }
120 }
121 (Some(DAV), "remove") => {
122 for name in props.map(Name::of) {
123 out.set.retain(|p| Name::of(p) != name);
124 out.remove.push(name);
125 }
126 }
127 _ => {}
128 }
129 }
130 Ok(out)
131}
132
133/// Nesting allowed in a request body. Building and dropping the tree
134/// recurse once per level, so a deep body would overflow the stack.
135const MAX_DEPTH: usize = 64;
136
137/// Whether `body` nests elements deeper than [`MAX_DEPTH`]. Malformed XML is
138/// left to the parser.
139pub fn too_deep(body: &[u8]) -> bool {
140 let mut depth = 0;
141 for e in xml::reader::EventReader::new(body) {
142 match e {
143 Ok(xml::reader::XmlEvent::StartElement { .. }) if depth == MAX_DEPTH => return true,
144 Ok(xml::reader::XmlEvent::StartElement { .. }) => depth += 1,
145 Ok(xml::reader::XmlEvent::EndElement { .. }) => depth -= 1,
146 Ok(_) => {}
147 Err(_) => return false,
148 }
149 }
150 false
151}
152
153/// A request body as a tree, refused when nested deeper than [`MAX_DEPTH`].
154pub(crate) fn tree(body: &[u8]) -> Result<Element, Invalid> {
155 if too_deep(body) {
156 return Err(Invalid);
157 }
158 Element::parse(body).map_err(|_| Invalid)
159}
160
161fn parse(body: &[u8], ns: &str, local: &str) -> Result<Element, Invalid> {
162 let root = tree(body)?;
163 if Name::of(&root).is(ns, local) {
164 Ok(root)
165 } else {
166 Err(Invalid)
167 }
168}
169
170pub fn elements(e: &Element) -> impl Iterator<Item = &Element> {
171 e.children.iter().filter_map(XMLNode::as_element)
172}
173
174pub fn child<'a>(e: &'a Element, ns: &str, local: &str) -> Option<&'a Element> {
175 elements(e).find(|c| Name::of(c).is(ns, local))
176}
177
178/// The concatenated text content, trimmed.
179pub fn text(e: &Element) -> String {
180 e.get_text()
181 .map_or_else(String::new, |t| t.trim().to_string())
182}
183
184pub fn el(ns: &str, local: &str) -> Element {
185 let mut e = Element::new(local);
186 e.namespace = Some(ns.to_string());
187 e
188}
189
190pub fn with_text(mut e: Element, text: impl Into<String>) -> Element {
191 e.children.push(XMLNode::Text(text.into()));
192 e
193}
194
195pub fn with_children(mut e: Element, children: impl IntoIterator<Item = Element>) -> Element {
196 e.children
197 .extend(children.into_iter().map(XMLNode::Element));
198 e
199}
200
201pub fn with_attr(mut e: Element, name: &str, value: &str) -> Element {
202 e.attributes.insert(name.to_string(), value.to_string());
203 e
204}
205
206/// `<d:href>` elements.
207pub fn hrefs<'a>(hrefs: impl IntoIterator<Item = &'a str>) -> Vec<Element> {
208 hrefs
209 .into_iter()
210 .map(|h| with_text(el(DAV, "href"), h))
211 .collect()
212}
213
214/// One `<d:response>` of a multistatus.
215#[derive(Debug, Default)]
216pub struct Response {
217 pub href: String,
218 /// Status code and the properties that share it.
219 pub propstats: Vec<(u16, Vec<Element>)>,
220 /// The status of the whole resource, for a response without properties.
221 pub status: Option<u16>,
222 pub error: Option<Element>,
223}
224
225impl Response {
226 pub fn new(href: impl Into<String>) -> Self {
227 Response {
228 href: href.into(),
229 ..Default::default()
230 }
231 }
232
233 pub fn status(href: impl Into<String>, status: u16) -> Self {
234 Response {
235 href: href.into(),
236 status: Some(status),
237 ..Default::default()
238 }
239 }
240
241 /// Adds `prop` under `status`, next to the others with that status.
242 pub fn push(&mut self, status: u16, prop: Element) {
243 match self.propstats.iter_mut().find(|(s, _)| *s == status) {
244 Some((_, props)) => props.push(prop),
245 None => self.propstats.push((status, vec![prop])),
246 }
247 }
248}
249
250/// A `<d:multistatus>` body, or another root such as
251/// `<c:mkcalendar-response>` holding the same propstats.
252pub fn multistatus(root: &Name, responses: &[Response]) -> String {
253 multistatus_with(root, responses, None)
254}
255
256/// A multistatus with `tail`, such as a `<d:sync-token>`, after the
257/// responses.
258pub fn multistatus_with(root: &Name, responses: &[Response], tail: Option<Element>) -> String {
259 let body = responses.iter().map(|r| {
260 let mut children = vec![with_text(el(DAV, "href"), r.href.as_str())];
261 children.extend(
262 r.status
263 .map(|s| with_text(el(DAV, "status"), status_line(s))),
264 );
265 children.extend(
266 r.propstats
267 .iter()
268 .map(|(status, props)| propstat(*status, props)),
269 );
270 children.extend(
271 r.error
272 .iter()
273 .map(|e| with_children(el(DAV, "error"), [e.clone()])),
274 );
275 with_children(el(DAV, "response"), children)
276 });
277 let root = with_children(root.element(), body.chain(tail));
278 document(&root)
279}
280
281/// The propstats alone, for roots that hold them without `<d:response>`.
282pub fn propstat_document(root: &Name, propstats: &[(u16, Vec<Element>)]) -> String {
283 let root = with_children(
284 root.element(),
285 propstats.iter().map(|(s, p)| propstat(*s, p)),
286 );
287 document(&root)
288}
289
290fn propstat(status: u16, props: &[Element]) -> Element {
291 with_children(
292 el(DAV, "propstat"),
293 [
294 with_children(el(DAV, "prop"), props.iter().cloned()),
295 with_text(el(DAV, "status"), status_line(status)),
296 ],
297 )
298}
299
300/// A `<d:error>` body naming the failed precondition.
301pub fn error(condition: Element) -> String {
302 document(&with_children(el(DAV, "error"), [condition]))
303}
304
305pub fn status_line(code: u16) -> String {
306 let reason = match code {
307 200 => "OK",
308 201 => "Created",
309 204 => "No Content",
310 207 => "Multi-Status",
311 307 => "Temporary Redirect",
312 400 => "Bad Request",
313 401 => "Unauthorized",
314 403 => "Forbidden",
315 404 => "Not Found",
316 405 => "Method Not Allowed",
317 409 => "Conflict",
318 412 => "Precondition Failed",
319 413 => "Payload Too Large",
320 415 => "Unsupported Media Type",
321 423 => "Locked",
322 424 => "Failed Dependency",
323 500 => "Internal Server Error",
324 502 => "Bad Gateway",
325 503 => "Service Unavailable",
326 507 => "Insufficient Storage",
327 _ => "",
328 };
329 format!("HTTP/1.1 {code} {reason}").trim_end().to_owned()
330}
331
332pub fn document(root: &Element) -> String {
333 let mut out = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
334 write(&mut out, root, true);
335 out
336}
337
338/// Known namespaces use the fixed prefixes. Any other element declares its
339/// namespace as the default on itself.
340fn write(out: &mut String, e: &Element, root: bool) {
341 let ns = e.namespace.as_deref().unwrap_or("");
342 let tag = match PREFIXES.iter().find(|(_, uri)| *uri == ns) {
343 Some((p, _)) => format!("{p}:{}", e.name),
344 None => e.name.clone(),
345 };
346 let _ = write!(out, "<{tag}");
347 if !tag.contains(':') {
348 let _ = write!(out, " xmlns=\"{}\"", escape(ns));
349 }
350 if root {
351 for (p, uri) in PREFIXES {
352 let _ = write!(out, " xmlns:{p}=\"{uri}\"");
353 }
354 }
355 let mut attrs: Vec<_> = e.attributes.iter().collect();
356 attrs.sort();
357 for (k, v) in attrs {
358 let _ = write!(out, " {k}=\"{}\"", escape(v));
359 }
360 if e.children.is_empty() {
361 out.push_str("/>");
362 return;
363 }
364 out.push('>');
365 for c in &e.children {
366 match c {
367 XMLNode::Element(c) => write(out, c, false),
368 XMLNode::Text(t) | XMLNode::CData(t) => out.push_str(&escape(t)),
369 _ => {}
370 }
371 }
372 let _ = write!(out, "</{tag}>");
373}
374
375fn escape(s: &str) -> String {
376 let mut out = String::with_capacity(s.len());
377 for c in s.chars() {
378 match c {
379 '&' => out.push_str("&amp;"),
380 '<' => out.push_str("&lt;"),
381 '>' => out.push_str("&gt;"),
382 '"' => out.push_str("&quot;"),
383 // A raw CR reaches the client as LF: XML parsers normalize line
384 // ends. iCalendar and vCard data need their CRLF.
385 '\r' => out.push_str("&#13;"),
386 // XML 1.0 forbids these even as character references.
387 '\u{0}'..='\u{8}'
388 | '\u{b}'
389 | '\u{c}'
390 | '\u{e}'..='\u{1f}'
391 | '\u{fffe}'
392 | '\u{ffff}' => out.push('\u{fffd}'),
393 _ => out.push(c),
394 }
395 }
396 out
397}
398