aster_forge_webdav/
deltav.rs

1//! Product-neutral RFC 3253 core request planning and response composition.
2
3use std::collections::{BTreeMap, HashSet};
4use std::future::Future;
5use std::pin::Pin;
6
7use aster_forge_xml::XmlSafetyPolicy;
8use async_trait::async_trait;
9use http::header::{CACHE_CONTROL, CONTENT_TYPE};
10use http::{HeaderValue, StatusCode};
11
12use crate::xml::{parse_report_request, parse_version_control_request};
13use crate::{
14    DavAutoVersion, DavBackendError, DavBackendErrorKind, DavCancellation, DavCapabilitySnapshot,
15    DavErrorCondition, DavMethod, DavMultiStatusError, DavMultiStatusErrorKind, DavMultiStatusItem,
16    DavMultiStatusLimits, DavPropStat, DavReportType, DavRequestedProperty, DavResponse,
17    DavVersioningState, DavXmlElement, DavXmlError, DavXmlNode, Depth, dav_element,
18    dav_error_element, dav_multistatus_bytes, dav_property_name_element, dav_property_text_element,
19    dav_text_element,
20};
21
22const DAV_NAMESPACE: &str = "DAV:";
23
24/// Independent hard limits for REPORT grammar, traversal, and response composition.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct DavReportLimits {
27    pub maximum_input_bytes: usize,
28    pub maximum_xml_depth: usize,
29    /// Maximum nested `DAV:property` levels in the request selection AST, counted from one.
30    pub maximum_selection_depth: usize,
31    /// Maximum `DAV:property` nodes in the request selection AST.
32    pub maximum_selection_properties: usize,
33    /// Maximum resource expansion hops, counted from zero at the root resource.
34    pub maximum_expansion_depth: usize,
35    /// Maximum resources visited during expansion, including the root resource.
36    pub maximum_expanded_resources: usize,
37    /// Maximum backend property lookups performed during expansion.
38    pub maximum_expanded_properties: usize,
39    pub multistatus: DavMultiStatusLimits,
40}
41
42impl DavReportLimits {
43    fn is_valid(self) -> bool {
44        self.maximum_input_bytes != 0
45            && self.maximum_xml_depth != 0
46            && self.maximum_selection_depth != 0
47            && self.maximum_selection_properties != 0
48            && self.maximum_expansion_depth != 0
49            && self.maximum_expanded_resources != 0
50            && self.maximum_expanded_properties != 0
51    }
52}
53
54impl Default for DavReportLimits {
55    fn default() -> Self {
56        let xml = XmlSafetyPolicy::untrusted();
57        Self {
58            maximum_input_bytes: xml.max_input_bytes,
59            maximum_xml_depth: xml.max_depth,
60            maximum_selection_depth: 16,
61            maximum_selection_properties: 100_000,
62            maximum_expansion_depth: 16,
63            maximum_expanded_resources: 10_000,
64            maximum_expanded_properties: 100_000,
65            multistatus: DavMultiStatusLimits::default(),
66        }
67    }
68}
69
70/// One nested RFC 3253 `DAV:property` selection.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DavExpandPropertySelection {
73    pub property: DavRequestedProperty,
74    pub nested: Vec<Self>,
75}
76
77/// Parsed `DAV:version-tree` request.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct DavVersionTreeRequest {
80    /// `None` selects the canonical core default property set.
81    pub properties: Option<Vec<DavRequestedProperty>>,
82    /// REPORT defaults to Depth 0 when the header is absent.
83    pub depth: Depth,
84}
85
86/// Parsed `DAV:expand-property` request.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct DavExpandPropertyRequest {
89    pub properties: Vec<DavExpandPropertySelection>,
90    /// REPORT defaults to Depth 0 when the header is absent.
91    pub depth: Depth,
92}
93
94/// Transport-neutral REPORT request selected through one capability snapshot.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum DavReportRequest {
97    VersionTree(DavVersionTreeRequest),
98    ExpandProperty(DavExpandPropertyRequest),
99    /// A report owned by another advertised RFC package.
100    Other {
101        report: DavReportType,
102        depth: Depth,
103    },
104}
105
106impl DavReportRequest {
107    #[must_use]
108    pub const fn report_type(&self) -> DavReportType {
109        match self {
110            Self::VersionTree(_) => DavReportType::VersionTree,
111            Self::ExpandProperty(_) => DavReportType::ExpandProperty,
112            Self::Other { report, .. } => *report,
113        }
114    }
115
116    #[must_use]
117    pub const fn depth(&self) -> Depth {
118        match self {
119            Self::VersionTree(request) => request.depth,
120            Self::ExpandProperty(request) => request.depth,
121            Self::Other { depth, .. } => *depth,
122        }
123    }
124}
125
126pub(crate) enum DavParsedReport {
127    VersionTree(Option<Vec<DavRequestedProperty>>),
128    ExpandProperty(Vec<DavExpandPropertySelection>),
129    Other(DavRequestedProperty),
130}
131
132/// Failure while parsing and selecting a REPORT through a capability snapshot.
133#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
134pub enum DavReportPlanError {
135    #[error("invalid REPORT limits")]
136    InvalidLimits,
137    #[error(transparent)]
138    Xml(#[from] DavXmlError),
139    #[error("unknown WebDAV REPORT type {name:?} in namespace {namespace:?}")]
140    UnknownType {
141        namespace: Option<String>,
142        name: String,
143    },
144    #[error("WebDAV REPORT type is not supported by this resource: {report:?}")]
145    NotAvailable { report: DavReportType },
146}
147
148/// Product-owned mapping for REPORT selection errors that are not XML syntax failures.
149pub trait DavReportErrorResponsePolicy {
150    fn unknown_type(&self, namespace: Option<&str>, name: &str) -> DavResponse;
151    fn not_available(&self, report: DavReportType) -> DavResponse;
152}
153
154/// Parses a REPORT using the RFC default Depth 0 and default hard limits.
155///
156/// # Errors
157///
158/// Returns an error when grammar, limits, or the target capability snapshot reject the report.
159pub fn plan_report_request(
160    snapshot: &DavCapabilitySnapshot,
161    body: &[u8],
162) -> Result<DavReportRequest, DavReportPlanError> {
163    plan_report_request_with_limits(snapshot, body, None, DavReportLimits::default())
164}
165
166/// Parses a REPORT with an explicit parsed Depth and product-configured limits.
167///
168/// `depth == None` applies the RFC 3253 default of Depth 0. An explicitly supplied Depth is
169/// retained even when it is also zero, allowing the product traversal adapter to select the
170/// required Multi-Status execution shape.
171///
172/// # Errors
173///
174/// Returns an error when grammar, limits, or the target capability snapshot reject the report.
175pub fn plan_report_request_with_limits(
176    snapshot: &DavCapabilitySnapshot,
177    body: &[u8],
178    depth: Option<Depth>,
179    limits: DavReportLimits,
180) -> Result<DavReportRequest, DavReportPlanError> {
181    if !limits.is_valid() {
182        return Err(DavReportPlanError::InvalidLimits);
183    }
184    let parsed = parse_report_request(
185        body,
186        limits.maximum_input_bytes,
187        limits.maximum_xml_depth,
188        limits.maximum_selection_depth,
189        limits.maximum_selection_properties,
190    )?;
191    let depth = depth.unwrap_or(Depth::Zero);
192    let request = match parsed {
193        DavParsedReport::VersionTree(properties) => {
194            DavReportRequest::VersionTree(DavVersionTreeRequest { properties, depth })
195        }
196        DavParsedReport::ExpandProperty(properties) => {
197            DavReportRequest::ExpandProperty(DavExpandPropertyRequest { properties, depth })
198        }
199        DavParsedReport::Other(root) => {
200            if root.namespace.as_deref() != Some(DAV_NAMESPACE) {
201                return Err(DavReportPlanError::UnknownType {
202                    namespace: root.namespace,
203                    name: root.name,
204                });
205            }
206            let report = report_type(&root.name).ok_or(DavReportPlanError::UnknownType {
207                namespace: root.namespace,
208                name: root.name,
209            })?;
210            DavReportRequest::Other { report, depth }
211        }
212    };
213    let report = request.report_type();
214    if !snapshot.supports_report(report) {
215        return Err(DavReportPlanError::NotAvailable { report });
216    }
217    Ok(request)
218}
219
220/// Builds the protocol response for a REPORT grammar selection failure.
221///
222/// # Errors
223///
224/// Returns [`DavXmlError`] when a DAV error body cannot be encoded.
225pub fn report_plan_error_response<P: DavReportErrorResponsePolicy>(
226    error: &DavReportPlanError,
227    policy: &P,
228) -> Result<DavResponse, DavXmlError> {
229    match error {
230        DavReportPlanError::InvalidLimits => {
231            Ok(text_response(StatusCode::INTERNAL_SERVER_ERROR, ""))
232        }
233        DavReportPlanError::Xml(error) => xml_request_error_response(*error),
234        DavReportPlanError::UnknownType { namespace, name } => {
235            Ok(policy.unknown_type(namespace.as_deref(), name))
236        }
237        DavReportPlanError::NotAvailable { report } => Ok(policy.not_available(*report)),
238    }
239}
240
241/// VERSION-CONTROL operation selected from target facts.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum DavVersionControlAction {
244    PutUnderVersionControl,
245    AlreadyControlled,
246}
247
248/// Transport-neutral VERSION-CONTROL plan passed to a product transaction port.
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub struct DavVersionControlPlan {
251    pub action: DavVersionControlAction,
252}
253
254/// VERSION-CONTROL planning failure.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
256pub enum DavVersionControlPlanError {
257    #[error("VERSION-CONTROL is not allowed for this target")]
258    MethodNotAllowed,
259    #[error(transparent)]
260    Xml(#[from] DavXmlError),
261}
262
263/// Plans VERSION-CONTROL without performing product persistence.
264///
265/// # Errors
266///
267/// Returns a typed failure when body grammar, method availability, or target facts reject it.
268pub fn plan_version_control_request(
269    snapshot: &DavCapabilitySnapshot,
270    body: &[u8],
271) -> Result<DavVersionControlPlan, DavVersionControlPlanError> {
272    let DavVersioningMethodPlan::VersionControl(action) =
273        plan_versioning_method(snapshot, DavMethod::VersionControl)
274            .map_err(|_| DavVersionControlPlanError::MethodNotAllowed)?
275    else {
276        return Err(DavVersionControlPlanError::MethodNotAllowed);
277    };
278    parse_version_control_request(body)?;
279    Ok(DavVersionControlPlan { action })
280}
281
282/// Product transaction result used only for RFC response composition.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct DavVersionControlResult {
285    /// Optional future-extension children for `DAV:version-control-response`.
286    pub response_extensions: Vec<DavXmlElement>,
287}
288
289/// Product-owned atomic VERSION-CONTROL transaction boundary.
290#[async_trait]
291pub trait DavVersionControlPort: Send + Sync {
292    async fn version_control(
293        &self,
294        plan: DavVersionControlPlan,
295    ) -> Result<DavVersionControlResult, DavBackendError>;
296}
297
298/// Executes an already validated VERSION-CONTROL plan through the product transaction port.
299///
300/// # Errors
301///
302/// Returns the product-neutral backend classification without mapping product error text.
303pub async fn execute_version_control<P: DavVersionControlPort>(
304    port: &P,
305    plan: DavVersionControlPlan,
306) -> Result<DavVersionControlResult, DavBackendError> {
307    port.version_control(plan).await
308}
309
310/// Composes the optional successful VERSION-CONTROL response body.
311///
312/// # Errors
313///
314/// Returns [`DavXmlError`] when extension elements cannot be encoded safely.
315pub fn version_control_response(
316    result: DavVersionControlResult,
317) -> Result<DavResponse, DavXmlError> {
318    if result.response_extensions.is_empty() {
319        return Ok(DavResponse::empty(StatusCode::OK));
320    }
321    let mut root = dav_element("version-control-response");
322    root.namespaces
323        .insert("D".to_owned(), DAV_NAMESPACE.to_owned());
324    root.children.extend(
325        result
326            .response_extensions
327            .into_iter()
328            .map(DavXmlNode::Element),
329    );
330    xml_response(StatusCode::OK, &root)
331}
332
333/// Builds the protocol response for VERSION-CONTROL planning failure through its snapshot.
334///
335/// # Errors
336///
337/// Returns [`DavXmlError`] when an XML error body cannot be encoded.
338pub fn version_control_plan_error_response(
339    snapshot: &DavCapabilitySnapshot,
340    error: DavVersionControlPlanError,
341) -> Result<DavResponse, DavXmlError> {
342    match error {
343        DavVersionControlPlanError::MethodNotAllowed => {
344            Ok(crate::method_not_allowed_response(snapshot))
345        }
346        DavVersionControlPlanError::Xml(error) => xml_request_error_response(error),
347    }
348}
349
350/// RFC 3253 method action selected before a product mutation transaction.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum DavVersioningMethodPlan {
353    ReadOnly,
354    DirectMutation,
355    AutoCheckout,
356    AutoCheckoutCheckin,
357    AutoCheckinOnUnlock,
358    DeleteVersion,
359    VersionControl(DavVersionControlAction),
360}
361
362/// Typed RFC 3253 precondition selected by core method planning.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
364pub enum DavVersioningPrecondition {
365    #[error("method is not allowed by the capability snapshot")]
366    MethodNotAllowed,
367    #[error("checked-in content requires checkout or auto-version")]
368    CannotModifyVersionControlledContent,
369    #[error("checked-in dead properties require checkout or auto-version")]
370    CannotModifyVersionControlledProperty,
371    #[error("immutable version content or dead properties cannot be modified")]
372    CannotModifyVersion,
373    #[error("immutable versions cannot be renamed")]
374    CannotRenameVersion,
375    #[error("server policy rejects deleting an immutable version")]
376    NoVersionDelete,
377}
378
379/// Plans core method behavior from the same target facts used for capability discovery.
380///
381/// # Errors
382///
383/// Returns a typed RFC precondition when the target state forbids the method.
384pub fn plan_versioning_method(
385    snapshot: &DavCapabilitySnapshot,
386    method: DavMethod,
387) -> Result<DavVersioningMethodPlan, DavVersioningPrecondition> {
388    if !snapshot.allows(method) {
389        return Err(DavVersioningPrecondition::MethodNotAllowed);
390    }
391    let facts = snapshot.declaration().versioning;
392    match method {
393        DavMethod::VersionControl => match facts.state {
394            DavVersioningState::Versionable => Ok(DavVersioningMethodPlan::VersionControl(
395                DavVersionControlAction::PutUnderVersionControl,
396            )),
397            DavVersioningState::CheckedIn | DavVersioningState::CheckedOut => Ok(
398                DavVersioningMethodPlan::VersionControl(DavVersionControlAction::AlreadyControlled),
399            ),
400            DavVersioningState::Unsupported | DavVersioningState::Version => {
401                Err(DavVersioningPrecondition::MethodNotAllowed)
402            }
403        },
404        DavMethod::Put | DavMethod::Proppatch => match facts.state {
405            DavVersioningState::Version => Err(DavVersioningPrecondition::CannotModifyVersion),
406            DavVersioningState::CheckedIn => {
407                let rejected = if method == DavMethod::Put {
408                    DavVersioningPrecondition::CannotModifyVersionControlledContent
409                } else {
410                    DavVersioningPrecondition::CannotModifyVersionControlledProperty
411                };
412                match facts.auto_version {
413                    DavAutoVersion::None => Err(rejected),
414                    DavAutoVersion::CheckoutCheckin => {
415                        Ok(DavVersioningMethodPlan::AutoCheckoutCheckin)
416                    }
417                    DavAutoVersion::CheckoutUnlockedCheckin => {
418                        if facts.write_locked {
419                            Ok(DavVersioningMethodPlan::AutoCheckout)
420                        } else {
421                            Ok(DavVersioningMethodPlan::AutoCheckoutCheckin)
422                        }
423                    }
424                    DavAutoVersion::Checkout => Ok(DavVersioningMethodPlan::AutoCheckout),
425                    DavAutoVersion::LockedCheckout => {
426                        if facts.write_locked {
427                            Ok(DavVersioningMethodPlan::AutoCheckout)
428                        } else {
429                            Err(rejected)
430                        }
431                    }
432                }
433            }
434            DavVersioningState::Unsupported
435            | DavVersioningState::Versionable
436            | DavVersioningState::CheckedOut => Ok(DavVersioningMethodPlan::DirectMutation),
437        },
438        DavMethod::Move if facts.state == DavVersioningState::Version => {
439            Err(DavVersioningPrecondition::CannotRenameVersion)
440        }
441        DavMethod::Delete if facts.state == DavVersioningState::Version => {
442            if facts.allow_version_delete {
443                Ok(DavVersioningMethodPlan::DeleteVersion)
444            } else {
445                Err(DavVersioningPrecondition::NoVersionDelete)
446            }
447        }
448        DavMethod::Unlock if facts.auto_checkout_lock => {
449            Ok(DavVersioningMethodPlan::AutoCheckinOnUnlock)
450        }
451        DavMethod::Delete | DavMethod::Copy | DavMethod::Move | DavMethod::Unlock => {
452            Ok(DavVersioningMethodPlan::DirectMutation)
453        }
454        _ => Ok(DavVersioningMethodPlan::ReadOnly),
455    }
456}
457
458/// Composes a DAV error response for a typed RFC 3253 precondition through its snapshot.
459///
460/// # Errors
461///
462/// Returns [`DavXmlError`] when the XML error body cannot be encoded.
463pub fn versioning_precondition_response(
464    snapshot: &DavCapabilitySnapshot,
465    error: DavVersioningPrecondition,
466) -> Result<DavResponse, DavXmlError> {
467    if error == DavVersioningPrecondition::MethodNotAllowed {
468        return Ok(crate::method_not_allowed_response(snapshot));
469    }
470    let condition = match error {
471        DavVersioningPrecondition::CannotModifyVersionControlledContent => {
472            DavErrorCondition::CannotModifyVersionControlledContent
473        }
474        DavVersioningPrecondition::CannotModifyVersionControlledProperty => {
475            DavErrorCondition::CannotModifyVersionControlledProperty
476        }
477        DavVersioningPrecondition::CannotModifyVersion => DavErrorCondition::CannotModifyVersion,
478        DavVersioningPrecondition::CannotRenameVersion => DavErrorCondition::CannotRenameVersion,
479        DavVersioningPrecondition::NoVersionDelete => DavErrorCondition::NoVersionDelete,
480        DavVersioningPrecondition::MethodNotAllowed => {
481            return Ok(crate::method_not_allowed_response(snapshot));
482        }
483    };
484    xml_response(StatusCode::FORBIDDEN, &dav_error_element(&condition))
485}
486
487/// One product-resolved property for an immutable version resource.
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct DavVersionProperty {
490    pub property: DavRequestedProperty,
491    pub result: DavVersionPropertyResult,
492}
493
494impl DavVersionProperty {
495    #[must_use]
496    pub fn value(property: DavRequestedProperty, value: DavXmlElement) -> Self {
497        Self {
498            property,
499            result: DavVersionPropertyResult::Value(value),
500        }
501    }
502
503    #[must_use]
504    pub fn text(property: DavRequestedProperty, value: impl Into<String>) -> Self {
505        let element = dav_property_text_element(&property, value);
506        Self::value(property, element)
507    }
508
509    #[must_use]
510    pub fn hrefs(property: DavRequestedProperty, hrefs: impl IntoIterator<Item = String>) -> Self {
511        let mut element = property_element(&property);
512        element.children.extend(
513            hrefs
514                .into_iter()
515                .map(|href| DavXmlNode::Element(dav_text_element("href", href))),
516        );
517        Self::value(property, element)
518    }
519
520    #[must_use]
521    pub fn missing(property: DavRequestedProperty) -> Self {
522        Self {
523            property,
524            result: DavVersionPropertyResult::Missing,
525        }
526    }
527
528    #[must_use]
529    pub fn backend_error(property: DavRequestedProperty, kind: DavBackendErrorKind) -> Self {
530        Self {
531            property,
532            result: DavVersionPropertyResult::BackendError(kind),
533        }
534    }
535}
536
537/// Product property outcome grouped into canonical property-level propstats.
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub enum DavVersionPropertyResult {
540    Value(DavXmlElement),
541    Missing,
542    BackendError(DavBackendErrorKind),
543}
544
545/// One version resource supplied by the product history adapter.
546#[derive(Debug, Clone, PartialEq, Eq)]
547pub struct DavVersionReportItem {
548    pub href: String,
549    pub properties: Vec<DavVersionProperty>,
550}
551
552/// Builds a requested-property-driven bounded version-tree Multi-Status response.
553///
554/// # Errors
555///
556/// Returns [`DavMultiStatusError`] when the canonical response exceeds the supplied limits.
557pub fn version_tree_response(
558    request: &DavVersionTreeRequest,
559    versions: Vec<DavVersionReportItem>,
560) -> Result<DavResponse, DavMultiStatusError> {
561    version_tree_response_with_limits(request, versions, DavMultiStatusLimits::default())
562}
563
564/// Builds a requested-property-driven bounded version-tree Multi-Status response.
565///
566/// # Errors
567///
568/// Returns [`DavMultiStatusError`] when the canonical response exceeds the supplied limits.
569pub fn version_tree_response_with_limits(
570    request: &DavVersionTreeRequest,
571    versions: Vec<DavVersionReportItem>,
572    limits: DavMultiStatusLimits,
573) -> Result<DavResponse, DavMultiStatusError> {
574    let requested = request
575        .properties
576        .clone()
577        .unwrap_or_else(default_version_tree_properties);
578    let items = versions
579        .into_iter()
580        .map(|version| version_multistatus_item(version, &requested));
581    let mut response = DavResponse::bytes(
582        StatusCode::MULTI_STATUS,
583        dav_multistatus_bytes(items, limits)?,
584    );
585    response.headers.insert(
586        CONTENT_TYPE,
587        HeaderValue::from_static("application/xml; charset=utf-8"),
588    );
589    Ok(response)
590}
591
592/// Value returned by an expand-property backend port.
593#[derive(Debug, Clone, PartialEq, Eq)]
594pub enum DavExpandPropertyValue {
595    Element(DavXmlElement),
596    Hrefs(Vec<String>),
597}
598
599/// Product port for resolving one property without exposing persistence entities to Forge.
600#[async_trait]
601pub trait DavExpandPropertyProvider: Send + Sync {
602    async fn property(
603        &self,
604        href: &str,
605        property: &DavRequestedProperty,
606    ) -> Result<Option<DavExpandPropertyValue>, DavBackendError>;
607}
608
609/// Bounded expand-property execution failure.
610#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
611pub enum DavExpandPropertyError {
612    #[error("invalid expand-property limits")]
613    InvalidLimits,
614    #[error("expand-property execution was cancelled")]
615    Cancelled,
616    #[error("expand-property cycle detected at {href}")]
617    Cycle { href: String },
618    #[error("expand-property depth limit exceeded")]
619    DepthLimitExceeded,
620    #[error("expand-property resource limit exceeded")]
621    ResourceLimitExceeded,
622    #[error("expand-property property limit exceeded")]
623    PropertyLimitExceeded,
624    #[error(transparent)]
625    Backend(#[from] DavBackendError),
626    #[error(transparent)]
627    MultiStatus(#[from] DavMultiStatusError),
628}
629
630/// Executes one bounded `DAV:expand-property` request and composes canonical nested responses.
631///
632/// Deadline handling is supplied by the same cancellation checkpoint: a transport or product
633/// deadline flips its cancellation source, and Forge checks it before every property lookup and
634/// nested href traversal.
635///
636/// # Errors
637///
638/// Returns a typed limit, cancellation, cycle, backend, or response-composition failure.
639pub async fn execute_expand_property<P: DavExpandPropertyProvider, C: DavCancellation>(
640    provider: &P,
641    root_href: &str,
642    request: &DavExpandPropertyRequest,
643    limits: DavReportLimits,
644    cancellation: &C,
645) -> Result<DavResponse, DavExpandPropertyError> {
646    if !limits.is_valid() {
647        return Err(DavExpandPropertyError::InvalidLimits);
648    }
649    let mut state = DavExpandExecutionState {
650        resources: 0,
651        properties: 0,
652        active_hrefs: HashSet::new(),
653    };
654    let item = expand_response_item(
655        provider,
656        root_href,
657        &request.properties,
658        0,
659        limits,
660        cancellation,
661        &mut state,
662    )
663    .await?;
664    let mut response = DavResponse::bytes(
665        StatusCode::MULTI_STATUS,
666        dav_multistatus_bytes([item], limits.multistatus)?,
667    );
668    response.headers.insert(
669        CONTENT_TYPE,
670        HeaderValue::from_static("application/xml; charset=utf-8"),
671    );
672    Ok(response)
673}
674
675struct DavExpandExecutionState {
676    resources: usize,
677    properties: usize,
678    active_hrefs: HashSet<String>,
679}
680
681#[expect(
682    clippy::too_many_lines,
683    reason = "The nested expand-property executor keeps one bounded traversal transaction and one canonical response grouping boundary."
684)]
685fn expand_response_item<'a, P: DavExpandPropertyProvider, C: DavCancellation>(
686    provider: &'a P,
687    href: &'a str,
688    selections: &'a [DavExpandPropertySelection],
689    depth: usize,
690    limits: DavReportLimits,
691    cancellation: &'a C,
692    state: &'a mut DavExpandExecutionState,
693) -> Pin<Box<dyn Future<Output = Result<DavMultiStatusItem, DavExpandPropertyError>> + Send + 'a>> {
694    Box::pin(async move {
695        checkpoint(cancellation)?;
696        if depth > limits.maximum_expansion_depth {
697            return Err(DavExpandPropertyError::DepthLimitExceeded);
698        }
699        state.resources = state
700            .resources
701            .checked_add(1)
702            .ok_or(DavExpandPropertyError::ResourceLimitExceeded)?;
703        if state.resources > limits.maximum_expanded_resources {
704            return Err(DavExpandPropertyError::ResourceLimitExceeded);
705        }
706        if !state.active_hrefs.insert(href.to_owned()) {
707            return Err(DavExpandPropertyError::Cycle {
708                href: href.to_owned(),
709            });
710        }
711
712        let result = async {
713            let mut groups: BTreeMap<u16, Vec<DavXmlElement>> = BTreeMap::new();
714            for selection in selections {
715                checkpoint(cancellation)?;
716                state.properties = state
717                    .properties
718                    .checked_add(1)
719                    .ok_or(DavExpandPropertyError::PropertyLimitExceeded)?;
720                if state.properties > limits.maximum_expanded_properties {
721                    return Err(DavExpandPropertyError::PropertyLimitExceeded);
722                }
723                match provider.property(href, &selection.property).await? {
724                    None => groups
725                        .entry(StatusCode::NOT_FOUND.as_u16())
726                        .or_default()
727                        .push(dav_property_name_element(&selection.property)),
728                    Some(DavExpandPropertyValue::Element(element))
729                        if selection.nested.is_empty() =>
730                    {
731                        groups
732                            .entry(StatusCode::OK.as_u16())
733                            .or_default()
734                            .push(relexicalize(element, &selection.property));
735                    }
736                    Some(DavExpandPropertyValue::Hrefs(hrefs)) if selection.nested.is_empty() => {
737                        let mut property = property_element(&selection.property);
738                        property.children.extend(
739                            hrefs
740                                .into_iter()
741                                .map(|href| DavXmlNode::Element(dav_text_element("href", href))),
742                        );
743                        groups
744                            .entry(StatusCode::OK.as_u16())
745                            .or_default()
746                            .push(property);
747                    }
748                    Some(DavExpandPropertyValue::Hrefs(hrefs)) => {
749                        let mut property = property_element(&selection.property);
750                        for nested_href in hrefs {
751                            checkpoint(cancellation)?;
752                            match expand_response_item(
753                                provider,
754                                &nested_href,
755                                &selection.nested,
756                                depth + 1,
757                                limits,
758                                cancellation,
759                                state,
760                            )
761                            .await
762                            {
763                                Ok(item) => property
764                                    .children
765                                    .push(DavXmlNode::Element(nested_response_element(item))),
766                                Err(DavExpandPropertyError::Backend(error))
767                                    if error.kind == DavBackendErrorKind::NotFound =>
768                                {
769                                    property.children.push(DavXmlNode::Element(
770                                        nested_response_element(DavMultiStatusItem::status(
771                                            nested_href,
772                                            StatusCode::NOT_FOUND.as_u16(),
773                                        )),
774                                    ));
775                                }
776                                Err(error) => return Err(error),
777                            }
778                        }
779                        groups
780                            .entry(StatusCode::OK.as_u16())
781                            .or_default()
782                            .push(property);
783                    }
784                    Some(DavExpandPropertyValue::Element(_)) => {
785                        groups
786                            .entry(StatusCode::FORBIDDEN.as_u16())
787                            .or_default()
788                            .push(dav_property_name_element(&selection.property));
789                    }
790                }
791            }
792            if groups.is_empty() {
793                groups.insert(StatusCode::OK.as_u16(), Vec::new());
794            }
795            Ok(DavMultiStatusItem::properties(
796                href,
797                groups
798                    .into_iter()
799                    .map(|(status, properties)| DavPropStat { status, properties })
800                    .collect(),
801            ))
802        }
803        .await;
804        state.active_hrefs.remove(href);
805        result
806    })
807}
808
809fn checkpoint(cancellation: &impl DavCancellation) -> Result<(), DavExpandPropertyError> {
810    if cancellation.is_cancelled() {
811        Err(DavExpandPropertyError::Cancelled)
812    } else {
813        Ok(())
814    }
815}
816
817fn version_multistatus_item(
818    version: DavVersionReportItem,
819    requested: &[DavRequestedProperty],
820) -> DavMultiStatusItem {
821    let mut groups: BTreeMap<u16, Vec<DavXmlElement>> = BTreeMap::new();
822    for property in requested {
823        let resolved = version.properties.iter().find(|candidate| {
824            candidate.property.name == property.name
825                && candidate.property.namespace == property.namespace
826        });
827        let (status, element) = match resolved.map(|property| &property.result) {
828            Some(DavVersionPropertyResult::Value(element)) => {
829                (StatusCode::OK, relexicalize(element.clone(), property))
830            }
831            Some(DavVersionPropertyResult::Missing) | None => {
832                (StatusCode::NOT_FOUND, dav_property_name_element(property))
833            }
834            Some(DavVersionPropertyResult::BackendError(kind)) => {
835                (backend_status(*kind), dav_property_name_element(property))
836            }
837        };
838        groups.entry(status.as_u16()).or_default().push(element);
839    }
840    if groups.is_empty() {
841        groups.insert(StatusCode::OK.as_u16(), Vec::new());
842    }
843    DavMultiStatusItem::properties(
844        version.href,
845        groups
846            .into_iter()
847            .map(|(status, properties)| DavPropStat { status, properties })
848            .collect(),
849    )
850}
851
852fn default_version_tree_properties() -> Vec<DavRequestedProperty> {
853    [
854        "version-name",
855        "creator-displayname",
856        "comment",
857        "predecessor-set",
858        "successor-set",
859        "checkout-set",
860        "getetag",
861        "getcontentlength",
862        "getcontenttype",
863        "getlastmodified",
864    ]
865    .into_iter()
866    .map(dav_property)
867    .collect()
868}
869
870fn dav_property(name: &str) -> DavRequestedProperty {
871    DavRequestedProperty {
872        name: name.to_owned(),
873        namespace: Some(DAV_NAMESPACE.to_owned()),
874        prefix: Some("D".to_owned()),
875    }
876}
877
878fn property_element(property: &DavRequestedProperty) -> DavXmlElement {
879    let mut element = property.prefix.as_ref().map_or_else(
880        || DavXmlElement::new(&property.name),
881        |prefix| DavXmlElement::new(&format!("{prefix}:{}", property.name)),
882    );
883    element.namespace.clone_from(&property.namespace);
884    if let Some(namespace) = &property.namespace {
885        element.namespaces.insert(
886            property.prefix.clone().unwrap_or_default(),
887            namespace.clone(),
888        );
889    }
890    element
891}
892
893fn relexicalize(mut element: DavXmlElement, property: &DavRequestedProperty) -> DavXmlElement {
894    element.name.clone_from(&property.name);
895    element.prefix.clone_from(&property.prefix);
896    element.namespace.clone_from(&property.namespace);
897    if let Some(namespace) = &property.namespace {
898        element.namespaces.insert(
899            property.prefix.clone().unwrap_or_default(),
900            namespace.clone(),
901        );
902    }
903    element
904}
905
906fn nested_response_element(item: DavMultiStatusItem) -> DavXmlElement {
907    let mut response = dav_element("response");
908    response
909        .children
910        .push(DavXmlNode::Element(dav_text_element("href", item.href)));
911    if let Some(status) = item.status {
912        response.children.push(DavXmlNode::Element(dav_text_element(
913            "status",
914            status_line(status),
915        )));
916    }
917    for propstat in item.propstats {
918        let mut element = dav_element("propstat");
919        let mut prop = dav_element("prop");
920        prop.children
921            .extend(propstat.properties.into_iter().map(DavXmlNode::Element));
922        element.children.push(DavXmlNode::Element(prop));
923        element.children.push(DavXmlNode::Element(dav_text_element(
924            "status",
925            status_line(propstat.status),
926        )));
927        response.children.push(DavXmlNode::Element(element));
928    }
929    response
930}
931
932fn status_line(status: u16) -> String {
933    let status = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
934    format!(
935        "HTTP/1.1 {} {}",
936        status.as_u16(),
937        status.canonical_reason().unwrap_or("Unknown Status")
938    )
939}
940
941fn backend_status(kind: DavBackendErrorKind) -> StatusCode {
942    match kind {
943        DavBackendErrorKind::NotFound => StatusCode::NOT_FOUND,
944        DavBackendErrorKind::Forbidden => StatusCode::FORBIDDEN,
945        DavBackendErrorKind::Conflict | DavBackendErrorKind::AlreadyExists => StatusCode::CONFLICT,
946        DavBackendErrorKind::InsufficientStorage => StatusCode::INSUFFICIENT_STORAGE,
947        DavBackendErrorKind::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
948        DavBackendErrorKind::Locked => StatusCode::LOCKED,
949        DavBackendErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
950        DavBackendErrorKind::Unsupported => StatusCode::NOT_IMPLEMENTED,
951        DavBackendErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
952    }
953}
954
955/// Maps an expand-property execution failure that occurs before a response starts.
956///
957/// # Errors
958///
959/// Returns [`DavXmlError`] when a DAV error response cannot be encoded.
960pub fn expand_property_error_response(
961    error: &DavExpandPropertyError,
962) -> Result<DavResponse, DavXmlError> {
963    let response = match error {
964        DavExpandPropertyError::InvalidLimits => {
965            text_response(StatusCode::INTERNAL_SERVER_ERROR, "")
966        }
967        DavExpandPropertyError::Cancelled => text_response(StatusCode::SERVICE_UNAVAILABLE, ""),
968        DavExpandPropertyError::Cycle { .. }
969        | DavExpandPropertyError::DepthLimitExceeded
970        | DavExpandPropertyError::ResourceLimitExceeded
971        | DavExpandPropertyError::PropertyLimitExceeded => {
972            text_response(StatusCode::INSUFFICIENT_STORAGE, "")
973        }
974        DavExpandPropertyError::Backend(error) => {
975            return Ok(crate::backend_error_response(error));
976        }
977        DavExpandPropertyError::MultiStatus(error) => match &error.kind {
978            DavMultiStatusErrorKind::ItemLimitExceeded
979            | DavMultiStatusErrorKind::PropertyLimitExceeded
980            | DavMultiStatusErrorKind::OutputLimitExceeded => {
981                text_response(StatusCode::INSUFFICIENT_STORAGE, "")
982            }
983            DavMultiStatusErrorKind::Cancelled => {
984                text_response(StatusCode::SERVICE_UNAVAILABLE, "")
985            }
986            DavMultiStatusErrorKind::Backend(error) => {
987                return Ok(crate::backend_error_response(error));
988            }
989            DavMultiStatusErrorKind::InvalidLimits
990            | DavMultiStatusErrorKind::InvalidItem
991            | DavMultiStatusErrorKind::Xml
992            | DavMultiStatusErrorKind::Write => {
993                text_response(StatusCode::INTERNAL_SERVER_ERROR, "")
994            }
995        },
996    };
997    Ok(response)
998}
999
1000fn text_response(status: StatusCode, body: impl Into<String>) -> DavResponse {
1001    let mut response = DavResponse::bytes(status, body.into());
1002    response.headers.insert(
1003        CONTENT_TYPE,
1004        HeaderValue::from_static("text/plain; charset=utf-8"),
1005    );
1006    if status.is_client_error() || status.is_server_error() {
1007        response
1008            .headers
1009            .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
1010    }
1011    response
1012}
1013
1014fn xml_request_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
1015    match error {
1016        DavXmlError::ExternalEntity => xml_response(
1017            StatusCode::FORBIDDEN,
1018            &dav_error_element(&DavErrorCondition::NoExternalEntities),
1019        ),
1020        DavXmlError::TooLarge => Ok(text_response(
1021            StatusCode::PAYLOAD_TOO_LARGE,
1022            "WebDAV XML body too large",
1023        )),
1024        DavXmlError::TooDeep | DavXmlError::Malformed | DavXmlError::InvalidGrammar => {
1025            Ok(text_response(StatusCode::BAD_REQUEST, "Invalid XML body"))
1026        }
1027    }
1028}
1029
1030fn xml_response(status: StatusCode, root: &DavXmlElement) -> Result<DavResponse, DavXmlError> {
1031    let mut response = DavResponse::bytes(status, root.to_bytes()?);
1032    response.headers.insert(
1033        CONTENT_TYPE,
1034        HeaderValue::from_static("application/xml; charset=utf-8"),
1035    );
1036    if status.is_client_error() || status.is_server_error() {
1037        response
1038            .headers
1039            .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
1040    }
1041    Ok(response)
1042}
1043
1044fn report_type(name: &str) -> Option<DavReportType> {
1045    DavReportType::ALL
1046        .into_iter()
1047        .find(|report| report.local_name() == name)
1048}