Skip to main content

aster_forge_webdav/
property.rs

1//! WebDAV property selection and propstat composition.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::time::SystemTime;
5
6use http::StatusCode;
7
8use crate::response::{xml_document_response, xml_request_error_response};
9use crate::{
10    DavErrorCondition, DavMultiStatusItem, DavPropStat, DavPropfindRequest, DavRequestedProperty,
11    DavResponse, DavXmlElement, DavXmlError, dav_error_element, dav_multistatus_element,
12    dav_property_name_element,
13};
14
15type PropertyKey = (String, Option<String>);
16
17/// Formats a DAV `creationdate` value as RFC 3339 UTC text.
18#[must_use]
19pub fn format_creation_date(time: SystemTime) -> String {
20    chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339()
21}
22
23/// Atomic PROPPATCH execution decision and per-property protocol statuses.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct DavProppatchAtomicPlan {
26    /// Whether the product adapter should persist every requested property mutation.
27    pub apply: bool,
28    /// Status assigned to each property in request order.
29    pub statuses: Vec<StatusCode>,
30}
31
32/// Selects the RFC 4918 atomic PROPPATCH statuses from product-owned protection decisions.
33///
34/// When any property is protected, that property receives `403` and every otherwise valid
35/// property receives `424`. If none are protected, every property receives `200` and the adapter
36/// may apply the entire transaction.
37pub fn plan_atomic_proppatch(protected: impl IntoIterator<Item = bool>) -> DavProppatchAtomicPlan {
38    let protected = protected.into_iter().collect::<Vec<_>>();
39    let has_protected = protected.iter().any(|protected| *protected);
40    let statuses = protected
41        .into_iter()
42        .map(|protected| {
43            if has_protected {
44                if protected {
45                    StatusCode::FORBIDDEN
46                } else {
47                    StatusCode::FAILED_DEPENDENCY
48                }
49            } else {
50                StatusCode::OK
51            }
52        })
53        .collect();
54    DavProppatchAtomicPlan {
55        apply: !has_protected,
56        statuses,
57    }
58}
59
60/// Builds one PROPFIND multistatus item from a product-supplied property catalog.
61///
62/// `available` contains every property exposed by `allprop`/`propname`. `resolve` supplies a value
63/// for the requested QName and may intentionally return `None` for unavailable or hidden values.
64pub fn build_propfind_item<E, F>(
65    href: String,
66    request: &DavPropfindRequest,
67    available: &[DavRequestedProperty],
68    mut resolve: F,
69) -> Result<DavMultiStatusItem, E>
70where
71    F: FnMut(&DavRequestedProperty) -> Result<Option<DavXmlElement>, E>,
72{
73    let groups = match request {
74        DavPropfindRequest::AllProp { include } => {
75            let mut keys = available.iter().map(property_key).collect::<BTreeSet<_>>();
76            let mut ok = resolve_available(available, &mut resolve)?;
77            let extra = include
78                .iter()
79                .filter(|property| keys.insert(property_key(property)))
80                .cloned()
81                .collect::<Vec<_>>();
82            let (mut included, missing) = resolve_requested(&extra, &mut resolve)?;
83            ok.append(&mut included);
84            propstat_groups(ok, missing)
85        }
86        DavPropfindRequest::PropName => vec![DavPropStat {
87            status: 200,
88            properties: available.iter().map(dav_property_name_element).collect(),
89        }],
90        DavPropfindRequest::Prop(requested) => {
91            let (ok, missing) = resolve_requested(requested, &mut resolve)?;
92            propstat_groups(ok, missing)
93        }
94    };
95    Ok(DavMultiStatusItem::properties(href, groups))
96}
97
98/// Builds one PROPPATCH multistatus item by grouping property outcomes by status.
99pub fn build_proppatch_item(
100    href: String,
101    outcomes: impl IntoIterator<Item = (u16, DavXmlElement)>,
102) -> DavMultiStatusItem {
103    let mut groups = BTreeMap::<u16, Vec<DavXmlElement>>::new();
104    for (status, property) in outcomes {
105        groups.entry(status).or_default().push(property);
106    }
107    DavMultiStatusItem::properties(
108        href,
109        groups
110            .into_iter()
111            .map(|(status, properties)| DavPropStat { status, properties })
112            .collect(),
113    )
114}
115
116/// Builds the 207 XML response for PROPFIND or PROPPATCH items.
117pub fn property_multistatus_response(
118    items: Vec<DavMultiStatusItem>,
119) -> Result<DavResponse, DavXmlError> {
120    xml_document_response(StatusCode::MULTI_STATUS, dav_multistatus_element(items))
121}
122
123/// Maps PROPFIND XML failures to their protocol response.
124pub fn propfind_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
125    xml_request_error_response(error, "Invalid PROPFIND body")
126}
127
128/// Maps PROPPATCH XML failures to their protocol response.
129pub fn proppatch_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
130    xml_request_error_response(error, "Invalid PROPPATCH body")
131}
132
133/// Builds the RFC 4918 finite-depth precondition response.
134pub fn propfind_finite_depth_response() -> Result<DavResponse, DavXmlError> {
135    xml_document_response(
136        StatusCode::FORBIDDEN,
137        dav_error_element(&DavErrorCondition::PropfindFiniteDepth),
138    )
139}
140
141/// Returns a stable label for protocol metrics and tracing.
142#[must_use]
143pub const fn propfind_request_label(request: &DavPropfindRequest) -> &'static str {
144    match request {
145        DavPropfindRequest::AllProp { .. } => "allprop",
146        DavPropfindRequest::PropName => "propname",
147        DavPropfindRequest::Prop(_) => "prop",
148    }
149}
150
151fn resolve_available<E, F>(
152    available: &[DavRequestedProperty],
153    resolve: &mut F,
154) -> Result<Vec<DavXmlElement>, E>
155where
156    F: FnMut(&DavRequestedProperty) -> Result<Option<DavXmlElement>, E>,
157{
158    let mut elements = Vec::with_capacity(available.len());
159    for property in available {
160        if let Some(element) = resolve(property)? {
161            elements.push(element);
162        }
163    }
164    Ok(elements)
165}
166
167fn resolve_requested<E, F>(
168    requested: &[DavRequestedProperty],
169    resolve: &mut F,
170) -> Result<(Vec<DavXmlElement>, Vec<DavXmlElement>), E>
171where
172    F: FnMut(&DavRequestedProperty) -> Result<Option<DavXmlElement>, E>,
173{
174    let mut ok = Vec::new();
175    let mut missing = Vec::new();
176    for property in requested {
177        match resolve(property)? {
178            Some(element) => ok.push(element),
179            None => missing.push(dav_property_name_element(property)),
180        }
181    }
182    Ok((ok, missing))
183}
184
185fn propstat_groups(ok: Vec<DavXmlElement>, missing: Vec<DavXmlElement>) -> Vec<DavPropStat> {
186    let mut groups = Vec::with_capacity(2);
187    if !ok.is_empty() {
188        groups.push(DavPropStat {
189            status: 200,
190            properties: ok,
191        });
192    }
193    if !missing.is_empty() {
194        groups.push(DavPropStat {
195            status: 404,
196            properties: missing,
197        });
198    }
199    groups
200}
201
202fn property_key(property: &DavRequestedProperty) -> PropertyKey {
203    (property.name.clone(), property.namespace.clone())
204}