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