Skip to main content

nextcloud_dav/xml/
response.rs

1//! Reading values out of a multistatus body.
2//!
3//! # What this reader is
4//!
5//! A targeted scanner: it finds an element by its exact qualified name and
6//! yields the slice between its tags. Namespace prefixes are compared
7//! literally and never resolved, which is sound for Nextcloud's WebDAV
8//! answers because the server uses the same `d`/`oc`/`nc` prefixes this crate
9//! writes.
10//!
11//! # What it deliberately is not
12//!
13//! - It does not expand namespace declarations, so a server answering with a
14//!   different prefix (`<D:href>`) is not understood. The caller sees a
15//!   missing element, never a wrong value.
16//! - It does not validate well-formedness. It does skip comments, CDATA
17//!   sections and processing instructions, and it respects quoted attribute
18//!   values, so a `<` inside those cannot start a false match.
19//! - It decodes only the five XML entities plus numeric character references
20//!   (see [`escape`](super::escape)).
21//!
22//! Every lookup returns `Option`, so an unreadable property is omitted rather
23//! than guessed at.
24
25use super::scanner;
26use super::{Element, MissingElement};
27
28/// First element with `name`.
29pub fn first_element<'a>(xml: &'a str, name: &'a str) -> Option<Element<'a>> {
30    elements(xml, name).into_iter().next()
31}
32
33/// Every element with `name`, in document order.
34///
35/// A match nested inside another element of a different name is still
36/// returned; see the module docs for why strict nesting is not attempted.
37pub fn elements<'a>(xml: &'a str, name: &'a str) -> Vec<Element<'a>> {
38    let mut found = Vec::new();
39    let mut cursor = 0;
40    while let Some(span) = scanner::find(xml, cursor, name) {
41        found.push(Element::new(name, &xml[span.inner]));
42        cursor = span.resume;
43    }
44    found
45}
46
47/// Whether any element with `name` is present.
48pub fn has_element(xml: &str, name: &str) -> bool {
49    scanner::find(xml, 0, name).is_some()
50}
51
52/// Text of the first element with `name`, if it is present.
53pub fn text_of(xml: &str, name: &str) -> Option<String> {
54    first_element(xml, name).map(|element| element.text())
55}
56
57/// Requires `name` to be present, reporting it by name when absent.
58pub fn require_element<'a>(xml: &'a str, name: &'a str) -> Result<Element<'a>, MissingElement> {
59    first_element(xml, name).ok_or_else(|| MissingElement::new(name))
60}
61
62#[cfg(test)]
63mod tests {
64    use super::{elements, has_element, require_element, text_of};
65
66    const BODY: &str = "<d:multistatus><d:response><d:href>/a&amp;b</d:href>\
67        <d:propstat><d:prop><oc:fileid>7</oc:fileid></d:prop></d:propstat>\
68        </d:response></d:multistatus>";
69
70    #[test]
71    fn reads_element_text_with_entities_decoded() {
72        assert_eq!(text_of(BODY, "d:href").as_deref(), Some("/a&b"));
73    }
74
75    #[test]
76    fn lists_every_matching_element_in_order() {
77        let xml = "<a><b>1</b><b>2</b></a>";
78        let texts: Vec<String> = elements(xml, "b").iter().map(|e| e.text()).collect();
79        assert_eq!(texts, ["1", "2"]);
80    }
81
82    #[test]
83    fn absent_elements_are_reported_not_invented() {
84        assert!(!has_element(BODY, "d:collection"));
85        assert!(text_of(BODY, "d:collection").is_none());
86    }
87
88    #[test]
89    fn require_element_names_the_missing_tag() {
90        assert!(require_element(BODY, "d:propstat").is_ok());
91        let error = require_element(BODY, "d:collection").expect_err("collection is absent");
92        assert_eq!(error.name(), "d:collection");
93    }
94}