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, 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}
180
181impl Response {
182 pub fn new(href: impl Into<String>) -> Self {
183 Response {
184 href: href.into(),
185 propstats: Vec::new(),
186 }
187 }
188
189 /// Adds `prop` under `status`, next to the others with that status.
190 pub fn push(&mut self, status: u16, prop: Element) {
191 match self.propstats.iter_mut().find(|(s, _)| *s == status) {
192 Some((_, props)) => props.push(prop),
193 None => self.propstats.push((status, vec![prop])),
194 }
195 }
196}
197
198/// A `<d:multistatus>` body, or another root such as
199/// `<c:mkcalendar-response>` holding the same propstats.
200pub fn multistatus(root: &Name, responses: &[Response]) -> String {
201 let body = responses.iter().map(|r| {
202 let mut children = vec![with_text(el(DAV, "href"), r.href.as_str())];
203 children.extend(
204 r.propstats
205 .iter()
206 .map(|(status, props)| propstat(*status, props)),
207 );
208 with_children(el(DAV, "response"), children)
209 });
210 let root = with_children(root.element(), body);
211 document(&root)
212}
213
214/// The propstats alone, for roots that hold them without `<d:response>`.
215pub fn propstat_document(root: &Name, propstats: &[(u16, Vec<Element>)]) -> String {
216 let root = with_children(
217 root.element(),
218 propstats.iter().map(|(s, p)| propstat(*s, p)),
219 );
220 document(&root)
221}
222
223fn propstat(status: u16, props: &[Element]) -> Element {
224 with_children(
225 el(DAV, "propstat"),
226 [
227 with_children(el(DAV, "prop"), props.iter().cloned()),
228 with_text(el(DAV, "status"), status_line(status)),
229 ],
230 )
231}
232
233/// A `<d:error>` body naming the failed precondition.
234pub fn error(condition: Element) -> String {
235 document(&with_children(el(DAV, "error"), [condition]))
236}
237
238pub fn status_line(code: u16) -> String {
239 let reason = match code {
240 200 => "OK",
241 201 => "Created",
242 403 => "Forbidden",
243 404 => "Not Found",
244 409 => "Conflict",
245 424 => "Failed Dependency",
246 507 => "Insufficient Storage",
247 _ => "",
248 };
249 format!("HTTP/1.1 {code} {reason}")
250}
251
252pub fn document(root: &Element) -> String {
253 let mut out = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
254 write(&mut out, root, true);
255 out
256}
257
258/// Known namespaces use the fixed prefixes. Any other element declares its
259/// namespace as the default on itself.
260fn write(out: &mut String, e: &Element, root: bool) {
261 let ns = e.namespace.as_deref().unwrap_or("");
262 let tag = match PREFIXES.iter().find(|(_, uri)| *uri == ns) {
263 Some((p, _)) => format!("{p}:{}", e.name),
264 None => e.name.clone(),
265 };
266 let _ = write!(out, "<{tag}");
267 if !tag.contains(':') {
268 let _ = write!(out, " xmlns=\"{}\"", escape(ns));
269 }
270 if root {
271 for (p, uri) in PREFIXES {
272 let _ = write!(out, " xmlns:{p}=\"{uri}\"");
273 }
274 }
275 let mut attrs: Vec<_> = e.attributes.iter().collect();
276 attrs.sort();
277 for (k, v) in attrs {
278 let _ = write!(out, " {k}=\"{}\"", escape(v));
279 }
280 if e.children.is_empty() {
281 out.push_str("/>");
282 return;
283 }
284 out.push('>');
285 for c in &e.children {
286 match c {
287 XMLNode::Element(c) => write(out, c, false),
288 XMLNode::Text(t) | XMLNode::CData(t) => out.push_str(&escape(t)),
289 _ => {}
290 }
291 }
292 let _ = write!(out, "</{tag}>");
293}
294
295fn escape(s: &str) -> String {
296 let mut out = String::with_capacity(s.len());
297 for c in s.chars() {
298 match c {
299 '&' => out.push_str("&amp;"),
300 '<' => out.push_str("&lt;"),
301 '>' => out.push_str("&gt;"),
302 '"' => out.push_str("&quot;"),
303 _ => out.push(c),
304 }
305 }
306 out
307}
308