xml.rs
| 1 | //! WebDAV XML: request bodies into typed values, and response bodies out. |
| 2 | |
| 3 | use std::fmt::Write as _; |
| 4 | |
| 5 | use xmltree::{Element, XMLNode}; |
| 6 | |
| 7 | pub const DAV: &str = "DAV:"; |
| 8 | pub const CALDAV: &str = "urn:ietf:params:xml:ns:caldav"; |
| 9 | pub const CARDDAV: &str = "urn:ietf:params:xml:ns:carddav"; |
| 10 | pub const CALSERVER: &str = "http://calendarserver.org/ns/"; |
| 11 | pub const APPLE: &str = "http://apple.com/ns/ical/"; |
| 12 | |
| 13 | /// Prefixes declared once on every response root. |
| 14 | const 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)] |
| 24 | pub struct Name { |
| 25 | pub ns: String, |
| 26 | pub local: String, |
| 27 | } |
| 28 | |
| 29 | impl 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)] |
| 55 | pub struct Invalid; |
| 56 | |
| 57 | #[derive(Debug, PartialEq)] |
| 58 | pub 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`. |
| 66 | pub 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)] |
| 89 | pub struct Update { |
| 90 | /// Property elements with their values. |
| 91 | pub set: Vec<Element>, |
| 92 | pub remove: Vec<Name>, |
| 93 | } |
| 94 | |
| 95 | pub 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 | |
| 120 | fn 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 | |
| 129 | pub fn elements(e: &Element) -> impl Iterator<Item = &Element> { |
| 130 | e.children.iter().filter_map(XMLNode::as_element) |
| 131 | } |
| 132 | |
| 133 | pub 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. |
| 138 | pub fn text(e: &Element) -> String { |
| 139 | e.get_text() |
| 140 | .map_or_else(String::new, |t| t.trim().to_string()) |
| 141 | } |
| 142 | |
| 143 | pub 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 | |
| 149 | pub fn with_text(mut e: Element, text: impl Into<String>) -> Element { |
| 150 | e.children.push(XMLNode::Text(text.into())); |
| 151 | e |
| 152 | } |
| 153 | |
| 154 | pub 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 | |
| 160 | pub 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. |
| 166 | pub 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)] |
| 175 | pub struct Response { |
| 176 | pub href: String, |
| 177 | /// Status code and the properties that share it. |
| 178 | pub propstats: Vec<(u16, Vec<Element>)>, |
| 179 | } |
| 180 | |
| 181 | impl 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. |
| 200 | pub 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>`. |
| 215 | pub 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 | |
| 223 | fn 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. |
| 234 | pub fn error(condition: Element) -> String { |
| 235 | document(&with_children(el(DAV, "error"), [condition])) |
| 236 | } |
| 237 | |
| 238 | pub 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 | |
| 252 | pub 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. |
| 260 | fn 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 | |
| 295 | fn 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("&"), |
| 300 | '<' => out.push_str("<"), |
| 301 | '>' => out.push_str(">"), |
| 302 | '"' => out.push_str("""), |
| 303 | _ => out.push(c), |
| 304 | } |
| 305 | } |
| 306 | out |
| 307 | } |
| 308 |