aster_forge_webdav/
xml_response.rs

1//! Product-neutral `WebDAV` XML response grammar.
2
3use std::time::Duration;
4
5use crate::{DavRequestedProperty, DavXmlElement, DavXmlNode, encode_href};
6
7/// One `<D:propstat>` group in a multistatus response.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DavPropStat {
10    /// HTTP status applying to every property in this group.
11    pub status: u16,
12    /// Ordered property elements.
13    pub properties: Vec<DavXmlElement>,
14}
15
16/// `WebDAV` precondition or postcondition carried inside `<D:error>`.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum DavErrorCondition {
19    /// RFC 4918 `no-external-entities` precondition.
20    NoExternalEntities,
21    /// RFC 4918 `lock-token-submitted` precondition.
22    LockTokenSubmitted {
23        /// Encoded resource href whose token must be submitted.
24        href: String,
25    },
26    /// RFC 4918 `lock-token-matches-request-uri` precondition.
27    LockTokenMatchesRequestUri,
28    /// RFC 4918 `propfind-finite-depth` precondition.
29    PropfindFiniteDepth,
30    /// RFC 3253 checked-in content mutation precondition.
31    CannotModifyVersionControlledContent,
32    /// RFC 3253 checked-in dead-property mutation precondition.
33    CannotModifyVersionControlledProperty,
34    /// RFC 3253 immutable version mutation precondition.
35    CannotModifyVersion,
36    /// RFC 3253 immutable version rename precondition.
37    CannotRenameVersion,
38    /// RFC 3253 optional immutable version delete precondition.
39    NoVersionDelete,
40}
41
42/// One `<D:response>` entry in a multistatus response.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct DavMultiStatusItem {
45    /// Encoded resource href.
46    pub href: String,
47    /// Resource-wide status, used by COPY/MOVE/DELETE failures.
48    pub status: Option<u16>,
49    /// Property status groups, used by PROPFIND and PROPPATCH.
50    pub propstats: Vec<DavPropStat>,
51    /// Optional `WebDAV` condition accompanying the resource status.
52    pub error: Option<DavErrorCondition>,
53}
54
55impl DavMultiStatusItem {
56    /// Creates a property response entry.
57    #[must_use]
58    pub fn properties(href: impl Into<String>, propstats: Vec<DavPropStat>) -> Self {
59        Self {
60            href: href.into(),
61            status: None,
62            propstats,
63            error: None,
64        }
65    }
66
67    /// Creates a resource-wide status response entry.
68    #[must_use]
69    pub fn status(href: impl Into<String>, status: u16) -> Self {
70        Self {
71            href: href.into(),
72            status: Some(status),
73            propstats: Vec::new(),
74            error: None,
75        }
76    }
77
78    /// Attaches a `WebDAV` error condition.
79    #[must_use]
80    pub fn with_error(mut self, error: DavErrorCondition) -> Self {
81        self.error = Some(error);
82        self
83    }
84}
85
86/// Protocol-visible lock values used to render `lockdiscovery`.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct DavLockXml {
89    /// Raw lock token; the XML writer percent-encodes it for the href.
90    pub token: String,
91    /// Optional validated owner element.
92    pub owner: Option<DavXmlElement>,
93    /// Negotiated timeout, or `None` for `Infinite`.
94    pub timeout: Option<Duration>,
95    /// Whether the lock is shared rather than exclusive.
96    pub shared: bool,
97    /// Whether the lock depth is infinity rather than zero.
98    pub deep: bool,
99    /// Encoded href of the lock root.
100    pub root_href: String,
101}
102
103/// Creates a `DAV:` element using the conventional `D` prefix.
104#[must_use]
105pub fn dav_element(local_name: &str) -> DavXmlElement {
106    DavXmlElement::dav(local_name)
107}
108
109/// Creates a `DAV:` element containing one text node.
110#[must_use]
111pub fn dav_text_element(local_name: &str, text: impl Into<String>) -> DavXmlElement {
112    let mut element = dav_element(local_name);
113    element.children.push(DavXmlNode::Text(text.into()));
114    element
115}
116
117/// Creates an empty property element using its expanded name and preferred prefix.
118#[must_use]
119pub fn dav_property_name_element(name: &DavRequestedProperty) -> DavXmlElement {
120    property_element(name, None)
121}
122
123/// Creates a property element containing one text node.
124#[must_use]
125pub fn dav_property_text_element(
126    name: &DavRequestedProperty,
127    text: impl Into<String>,
128) -> DavXmlElement {
129    property_element(name, Some(DavXmlNode::Text(text.into())))
130}
131
132/// Creates a property element containing one child element.
133#[must_use]
134pub fn dav_property_child_element(
135    name: &DavRequestedProperty,
136    child: DavXmlElement,
137) -> DavXmlElement {
138    property_element(name, Some(DavXmlNode::Element(child)))
139}
140
141/// Reconstructs a persisted dead property under the requested lexical `QName`.
142///
143/// Matching validated XML contributes its attributes and children. Legacy malformed or
144/// mismatched values are emitted as escaped text instead of response markup.
145#[must_use]
146pub fn dav_dead_property_element(
147    stored_name: &DavRequestedProperty,
148    requested_name: Option<&DavRequestedProperty>,
149    stored_xml: Option<&[u8]>,
150) -> DavXmlElement {
151    let output_name = requested_name.unwrap_or(stored_name);
152    let mut output = property_element(output_name, None);
153    let Some(stored_xml) = stored_xml.filter(|xml| !xml.is_empty()) else {
154        return output;
155    };
156    if let Ok(stored) = DavXmlElement::parse(stored_xml)
157        && stored.name == stored_name.name
158        && stored.namespace == stored_name.namespace
159    {
160        for (key, value) in stored.attributes {
161            if key.starts_with("xmlns") {
162                continue;
163            }
164            let key = if key == "lang" { "xml:lang" } else { &key };
165            output.attributes.entry(key.to_owned()).or_insert(value);
166        }
167        output.children = stored.children;
168    } else {
169        output.children.push(DavXmlNode::Text(
170            String::from_utf8_lossy(stored_xml).into_owned(),
171        ));
172    }
173    output
174}
175
176/// Creates a complete `<D:error>` document.
177#[must_use]
178pub fn dav_error_element(condition: &DavErrorCondition) -> DavXmlElement {
179    let mut error = dav_element("error");
180    declare_dav_namespace(&mut error);
181    error
182        .children
183        .push(DavXmlNode::Element(error_condition_element(condition)));
184    error
185}
186
187/// Creates the RFC 4918 `supportedlock` property value.
188#[must_use]
189pub fn dav_supported_lock_element() -> DavXmlElement {
190    let mut supported = dav_element("supportedlock");
191    for scope in ["exclusive", "shared"] {
192        let mut entry = dav_element("lockentry");
193        let mut lockscope = dav_element("lockscope");
194        lockscope
195            .children
196            .push(DavXmlNode::Element(dav_element(scope)));
197        entry.children.push(DavXmlNode::Element(lockscope));
198        let mut locktype = dav_element("locktype");
199        locktype
200            .children
201            .push(DavXmlNode::Element(dav_element("write")));
202        entry.children.push(DavXmlNode::Element(locktype));
203        supported.children.push(DavXmlNode::Element(entry));
204    }
205    supported
206}
207
208/// Creates the RFC 4918 `lockdiscovery` property value.
209#[must_use]
210pub fn dav_lock_discovery_element(locks: &[DavLockXml]) -> DavXmlElement {
211    let mut discovery = dav_element("lockdiscovery");
212    discovery.children.extend(
213        locks
214            .iter()
215            .map(active_lock_element)
216            .map(DavXmlNode::Element),
217    );
218    discovery
219}
220
221/// Creates a complete LOCK response `<D:prop>` document.
222#[must_use]
223pub fn dav_lock_response_element(locks: &[DavLockXml]) -> DavXmlElement {
224    let mut prop = dav_element("prop");
225    declare_dav_namespace(&mut prop);
226    prop.children
227        .push(DavXmlNode::Element(dav_lock_discovery_element(locks)));
228    prop
229}
230
231fn active_lock_element(lock: &DavLockXml) -> DavXmlElement {
232    let mut active = dav_element("activelock");
233    let mut lockscope = dav_element("lockscope");
234    lockscope
235        .children
236        .push(DavXmlNode::Element(dav_element(if lock.shared {
237            "shared"
238        } else {
239            "exclusive"
240        })));
241    active.children.push(DavXmlNode::Element(lockscope));
242
243    let mut locktype = dav_element("locktype");
244    locktype
245        .children
246        .push(DavXmlNode::Element(dav_element("write")));
247    active.children.push(DavXmlNode::Element(locktype));
248    active.children.push(DavXmlNode::Element(dav_text_element(
249        "depth",
250        if lock.deep { "Infinity" } else { "0" },
251    )));
252    if let Some(owner) = &lock.owner {
253        active.children.push(DavXmlNode::Element(owner.clone()));
254    }
255    active.children.push(DavXmlNode::Element(dav_text_element(
256        "timeout",
257        lock.timeout.map_or_else(
258            || "Infinite".to_owned(),
259            |timeout| format!("Second-{}", timeout.as_secs()),
260        ),
261    )));
262
263    let mut token = dav_element("locktoken");
264    token.children.push(DavXmlNode::Element(dav_text_element(
265        "href",
266        encode_href(&lock.token),
267    )));
268    active.children.push(DavXmlNode::Element(token));
269
270    let mut lockroot = dav_element("lockroot");
271    lockroot.children.push(DavXmlNode::Element(dav_text_element(
272        "href",
273        lock.root_href.clone(),
274    )));
275    active.children.push(DavXmlNode::Element(lockroot));
276    active
277}
278
279fn error_condition_element(condition: &DavErrorCondition) -> DavXmlElement {
280    let (name, href) = error_condition_parts(condition);
281    let mut element = dav_element(name);
282    if let Some(href) = href {
283        element
284            .children
285            .push(DavXmlNode::Element(dav_text_element("href", href)));
286    }
287    element
288}
289
290pub(crate) fn error_condition_parts(condition: &DavErrorCondition) -> (&'static str, Option<&str>) {
291    match condition {
292        DavErrorCondition::NoExternalEntities => ("no-external-entities", None),
293        DavErrorCondition::LockTokenSubmitted { href } => {
294            ("lock-token-submitted", Some(href.as_str()))
295        }
296        DavErrorCondition::LockTokenMatchesRequestUri => ("lock-token-matches-request-uri", None),
297        DavErrorCondition::PropfindFiniteDepth => ("propfind-finite-depth", None),
298        DavErrorCondition::CannotModifyVersionControlledContent => {
299            ("cannot-modify-version-controlled-content", None)
300        }
301        DavErrorCondition::CannotModifyVersionControlledProperty => {
302            ("cannot-modify-version-controlled-property", None)
303        }
304        DavErrorCondition::CannotModifyVersion => ("cannot-modify-version", None),
305        DavErrorCondition::CannotRenameVersion => ("cannot-rename-version", None),
306        DavErrorCondition::NoVersionDelete => ("no-version-delete", None),
307    }
308}
309
310fn declare_dav_namespace(element: &mut DavXmlElement) {
311    element
312        .attributes
313        .insert("xmlns:D".to_owned(), "DAV:".to_owned());
314}
315
316fn property_element(name: &DavRequestedProperty, child: Option<DavXmlNode>) -> DavXmlElement {
317    let prefix = name
318        .prefix
319        .as_deref()
320        .filter(|prefix| !matches!(*prefix, "xml" | "xmlns"))
321        .unwrap_or_else(|| default_property_prefix(name.namespace.as_deref()));
322    let tag = if name.namespace.is_some() {
323        format!("{prefix}:{}", name.name)
324    } else {
325        name.name.clone()
326    };
327    let mut element = DavXmlElement::new(&tag);
328    element.namespace.clone_from(&name.namespace);
329    if let Some(namespace) = &name.namespace
330        && (namespace != "DAV:" || prefix != "D")
331    {
332        element
333            .attributes
334            .insert(format!("xmlns:{prefix}"), namespace.clone());
335    }
336    if let Some(child) = child {
337        element.children.push(child);
338    }
339    element
340}
341
342fn default_property_prefix(namespace: Option<&str>) -> &str {
343    match namespace {
344        Some("DAV:") => "D",
345        Some(_) => "A",
346        None => "",
347    }
348}