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 = Element::parse(body).map_err(|_| Invalid)?;
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
120fn parse(body: &[u8], ns: &str, local: &str) -> Result<Element, Invalid> {
121 let root = Element::parse(body).map_err(|_| Invalid)?;
122 if Name::of(&root).is(ns, local) {
123 Ok(root)
124 } else {
125 Err(Invalid)
126 }
127}
128
129pub fn elements(e: &Element) -> impl Iterator<Item = &Element> {
130 e.children.iter().filter_map(XMLNode::as_element)
131}
132
133pub fn child<'a>(e: &'a Element, ns: &str, local: &str) -> Option<&'a Element> {
134 elements(e).find(|c| Name::of(c).is(ns, local))
135}
136
137/// The concatenated text content, trimmed.
138pub fn text(e: &Element) -> String {
139 e.get_text()
140 .map_or_else(String::new, |t| t.trim().to_string())
141}
142
143pub fn el(ns: &str, local: &str) -> Element {
144 let mut e = Element::new(local);
145 e.namespace = Some(ns.to_string());
146 e
147}
148
149pub fn with_text(mut e: Element, text: impl Into<String>) -> Element {
150 e.children.push(XMLNode::Text(text.into()));
151 e
152}
153
154pub fn with_children(mut e: Element, children: impl IntoIterator<Item = Element>) -> Element {
155 e.children
156 .extend(children.into_iter().map(XMLNode::Element));
157 e
158}
159
160pub fn with_attr(mut e: Element, name: &str, value: &str) -> Element {
161 e.attributes.insert(name.to_string(), value.to_string());
162 e
163}
164
165/// `<d:href>` elements.
166pub fn hrefs<'a>(hrefs: impl IntoIterator<Item = &'a str>) -> Vec<Element> {
167 hrefs
168 .into_iter()
169 .map(|h| with_text(el(DAV, "href"), h))
170 .collect()
171}
172
173/// One `<d:response>` of a multistatus.
174#[derive(Debug, Default)]
175pub struct Response {
176 pub href: String,
177 /// Status code and the properties that share it.
178 pub propstats: Vec<(u16, Vec<Element>)>,
179 /// The status of the whole resource, for a response without properties.
180 pub status: Option<u16>,
181 pub error: Option<Element>,
182}
183
184impl Response {
185 pub fn new(href: impl Into<String>) -> Self {
186 Response {
187 href: href.into(),
188 ..Default::default()
189 }
190 }
191
192 pub fn status(href: impl Into<String>, status: u16) -> Self {
193 Response {
194 href: href.into(),
195 status: Some(status),
196 ..Default::default()
197 }
198 }
199
200 /// Adds `prop` under `status`, next to the others with that status.
201 pub fn push(&mut self, status: u16, prop: Element) {
202 match self.propstats.iter_mut().find(|(s, _)| *s == status) {
203 Some((_, props)) => props.push(prop),
204 None => self.propstats.push((status, vec![prop])),
205 }
206 }
207}
208
209/// A `<d:multistatus>` body, or another root such as
210/// `<c:mkcalendar-response>` holding the same propstats.
211pub fn multistatus(root: &Name, responses: &[Response]) -> String {
212 multistatus_with(root, responses, None)
213}
214
215/// A multistatus with `tail`, such as a `<d:sync-token>`, after the
216/// responses.
217pub fn multistatus_with(root: &Name, responses: &[Response], tail: Option<Element>) -> String {
218 let body = responses.iter().map(|r| {
219 let mut children = vec![with_text(el(DAV, "href"), r.href.as_str())];
220 children.extend(
221 r.status
222 .map(|s| with_text(el(DAV, "status"), status_line(s))),
223 );
224 children.extend(
225 r.propstats
226 .iter()
227 .map(|(status, props)| propstat(*status, props)),
228 );
229 children.extend(
230 r.error
231 .iter()
232 .map(|e| with_children(el(DAV, "error"), [e.clone()])),
233 );
234 with_children(el(DAV, "response"), children)
235 });
236 let root = with_children(root.element(), body.chain(tail));
237 document(&root)
238}
239
240/// The propstats alone, for roots that hold them without `<d:response>`.
241pub fn propstat_document(root: &Name, propstats: &[(u16, Vec<Element>)]) -> String {
242 let root = with_children(
243 root.element(),
244 propstats.iter().map(|(s, p)| propstat(*s, p)),
245 );
246 document(&root)
247}
248
249fn propstat(status: u16, props: &[Element]) -> Element {
250 with_children(
251 el(DAV, "propstat"),
252 [
253 with_children(el(DAV, "prop"), props.iter().cloned()),
254 with_text(el(DAV, "status"), status_line(status)),
255 ],
256 )
257}
258
259/// A `<d:error>` body naming the failed precondition.
260pub fn error(condition: Element) -> String {
261 document(&with_children(el(DAV, "error"), [condition]))
262}
263
264pub fn status_line(code: u16) -> String {
265 let reason = match code {
266 200 => "OK",
267 201 => "Created",
268 403 => "Forbidden",
269 404 => "Not Found",
270 409 => "Conflict",
271 424 => "Failed Dependency",
272 507 => "Insufficient Storage",
273 _ => "",
274 };
275 format!("HTTP/1.1 {code} {reason}")
276}
277
278pub fn document(root: &Element) -> String {
279 let mut out = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
280 write(&mut out, root, true);
281 out
282}
283
284/// Known namespaces use the fixed prefixes. Any other element declares its
285/// namespace as the default on itself.
286fn write(out: &mut String, e: &Element, root: bool) {
287 let ns = e.namespace.as_deref().unwrap_or("");
288 let tag = match PREFIXES.iter().find(|(_, uri)| *uri == ns) {
289 Some((p, _)) => format!("{p}:{}", e.name),
290 None => e.name.clone(),
291 };
292 let _ = write!(out, "<{tag}");
293 if !tag.contains(':') {
294 let _ = write!(out, " xmlns=\"{}\"", escape(ns));
295 }
296 if root {
297 for (p, uri) in PREFIXES {
298 let _ = write!(out, " xmlns:{p}=\"{uri}\"");
299 }
300 }
301 let mut attrs: Vec<_> = e.attributes.iter().collect();
302 attrs.sort();
303 for (k, v) in attrs {
304 let _ = write!(out, " {k}=\"{}\"", escape(v));
305 }
306 if e.children.is_empty() {
307 out.push_str("/>");
308 return;
309 }
310 out.push('>');
311 for c in &e.children {
312 match c {
313 XMLNode::Element(c) => write(out, c, false),
314 XMLNode::Text(t) | XMLNode::CData(t) => out.push_str(&escape(t)),
315 _ => {}
316 }
317 }
318 let _ = write!(out, "</{tag}>");
319}
320
321fn escape(s: &str) -> String {
322 let mut out = String::with_capacity(s.len());
323 for c in s.chars() {
324 match c {
325 '&' => out.push_str("&amp;"),
326 '<' => out.push_str("&lt;"),
327 '>' => out.push_str("&gt;"),
328 '"' => out.push_str("&quot;"),
329 _ => out.push(c),
330 }
331 }
332 out
333}
334