Skip to main content

aster_forge_webdav/
xml_response.rs

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