aster_forge_webdav/
property.rs

1//! `WebDAV` property selection and propstat composition.
2
3use std::collections::BTreeMap;
4use std::time::SystemTime;
5
6use http::header::CONTENT_TYPE;
7use http::{HeaderValue, StatusCode};
8
9use crate::response::{xml_document_response, xml_request_error_response};
10use crate::{
11    DavBackendError, DavCapabilitySnapshot, DavErrorCondition, DavExtensionPackage,
12    DavLiveProperty, DavLockXml, DavMultiStatusError, DavMultiStatusItem, DavMultiStatusLimits,
13    DavPath, DavProp, DavPropStat, DavPropfindRequest, DavRequestedProperty, DavResourceState,
14    DavResponse, DavXmlElement, DavXmlError, DavXmlNode, dav_dead_property_element, dav_element,
15    dav_error_element, dav_lock_discovery_element, dav_multistatus_bytes,
16    dav_property_child_element, dav_property_name_element, dav_property_text_element,
17    dav_supported_lock_element,
18};
19use aster_forge_utils::http_validators;
20use aster_forge_utils::url::parse_absolute_url;
21
22/// Formats a DAV `creationdate` value as RFC 3339 UTC text.
23#[must_use]
24pub fn format_creation_date(time: SystemTime) -> String {
25    chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339()
26}
27
28/// Metadata values fetched once for all standard content and validator properties.
29#[derive(Debug, Clone, Copy, Default)]
30pub struct DavLivePropertyMetadata<'a> {
31    pub creation_date: Option<SystemTime>,
32    pub display_name: Option<&'a str>,
33    pub content_language: Option<&'a str>,
34    pub content_length: Option<u64>,
35    pub content_type: Option<&'a str>,
36    /// A complete entity-tag value suitable for `DAV:getetag`.
37    pub etag: Option<&'a str>,
38    pub last_modified: Option<SystemTime>,
39}
40
41/// One RFC 4331 scope snapshot reused for both quota properties.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct DavQuotaSnapshot {
44    pub used_bytes: u64,
45    /// `None` represents an infinite limit and omits `quota-available-bytes` from `propname`.
46    pub available_bytes: Option<u64>,
47}
48
49/// RFC 5397 current principal value computed once for the request principal.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum DavCurrentPrincipal<'a> {
52    /// Absolute HTTP(S) principal URL required by RFC 5397.
53    Href(&'a str),
54    /// RFC 5397 pseudo-principal used when no authenticated principal exists.
55    Unauthenticated,
56}
57
58/// Value groups that a product adapter should fetch in one batch.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60#[expect(
61    clippy::struct_excessive_bools,
62    reason = "Each flag independently selects one protocol value group for a single batch fetch."
63)]
64pub struct DavLivePropertyRequirements {
65    pub metadata: bool,
66    pub locks: bool,
67    pub dead_properties: bool,
68    pub quota: bool,
69    pub current_principal: bool,
70    pub sync_token: bool,
71    pub add_member: bool,
72    pub extension_values: bool,
73}
74
75/// Product-owned batch snapshot consumed by the property composer.
76pub trait DavLivePropertyValueSnapshot: Send {
77    fn metadata(&self) -> DavLivePropertyMetadata<'_> {
78        DavLivePropertyMetadata::default()
79    }
80
81    fn active_locks(&self) -> &[DavLockXml] {
82        &[]
83    }
84
85    fn dead_properties(&self) -> &[DavProp] {
86        &[]
87    }
88
89    /// Returns quota values for this resource.
90    ///
91    /// A collection or mount root that advertises RFC 4331 quota support must provide this
92    /// snapshot when `DAV:quota-used-bytes` is selected; omission is a typed product-boundary
93    /// error rather than an absent property.
94    fn quota(&self) -> Option<DavQuotaSnapshot> {
95        None
96    }
97
98    fn current_principal(&self) -> Option<DavCurrentPrincipal<'_>> {
99        None
100    }
101
102    fn sync_token(&self) -> Option<&str> {
103        None
104    }
105
106    fn add_member_href(&self) -> Option<&str> {
107        None
108    }
109
110    /// Supplies complex ACL, `DeltaV`, ordering, redirect, or binding property XML by typed ID.
111    fn extension_value(&self, _property: DavLiveProperty) -> Option<&DavXmlElement> {
112        None
113    }
114}
115
116/// Batch product port used by the canonical live-property entrypoint.
117#[expect(
118    async_fn_in_trait,
119    reason = "The provider is generic and intentionally preserves native async futures."
120)]
121pub trait DavLivePropertyProvider: Send + Sync {
122    type Values: DavLivePropertyValueSnapshot;
123
124    async fn live_property_values(
125        &self,
126        path: &DavPath,
127        requirements: DavLivePropertyRequirements,
128    ) -> Result<Self::Values, DavBackendError>;
129}
130
131/// Failure while rendering authoritative product values as `WebDAV` live properties.
132#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
133pub enum DavLivePropertyError {
134    #[error("invalid representation for WebDAV live property {property:?}")]
135    InvalidRepresentation { property: DavLiveProperty },
136    #[error("required WebDAV live property value is missing: {property:?}")]
137    MissingRequiredValue { property: DavLiveProperty },
138}
139
140/// Failure across the product batch port and property representation boundary.
141#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
142pub enum DavLivePropertyEvaluationError {
143    #[error(transparent)]
144    Backend(#[from] DavBackendError),
145    #[error(transparent)]
146    Property(#[from] DavLivePropertyError),
147}
148
149/// Computes the value groups required by one PROPFIND selection without allocating.
150#[must_use]
151pub fn live_property_requirements(
152    snapshot: &DavCapabilitySnapshot,
153    request: &DavPropfindRequest,
154) -> DavLivePropertyRequirements {
155    let mut requirements = DavLivePropertyRequirements::default();
156    match request {
157        DavPropfindRequest::AllProp { include } => {
158            requirements.metadata = true;
159            requirements.locks = snapshot.declaration().compliance.class1;
160            requirements.dead_properties = true;
161            requirements.current_principal =
162                snapshot.supports_extension(DavExtensionPackage::CurrentPrincipal);
163            for property in include {
164                add_requirement(snapshot, property, &mut requirements);
165            }
166        }
167        DavPropfindRequest::PropName => {
168            requirements.metadata = true;
169            requirements.locks = snapshot.declaration().compliance.class1;
170            requirements.dead_properties = true;
171            requirements.quota = snapshot.supports_extension(DavExtensionPackage::Quota);
172            requirements.current_principal =
173                snapshot.supports_extension(DavExtensionPackage::CurrentPrincipal);
174            requirements.sync_token =
175                snapshot.supports_extension(DavExtensionPackage::CollectionSync);
176            requirements.add_member = snapshot.supports_extension(DavExtensionPackage::AddMember);
177            requirements.extension_values = !snapshot.extensions().is_empty();
178        }
179        DavPropfindRequest::Prop(properties) => {
180            for property in properties {
181                add_requirement(snapshot, property, &mut requirements);
182            }
183        }
184    }
185    requirements
186}
187
188/// Fetches one product snapshot and composes a capability-driven PROPFIND item.
189///
190/// # Errors
191///
192/// Returns an error when the provider fails or required live-property values are missing.
193pub async fn build_live_propfind_item_with_provider<P: DavLivePropertyProvider>(
194    provider: &P,
195    path: &DavPath,
196    href: String,
197    snapshot: &DavCapabilitySnapshot,
198    request: &DavPropfindRequest,
199) -> Result<DavMultiStatusItem, DavLivePropertyEvaluationError> {
200    let requirements = live_property_requirements(snapshot, request);
201    let values = provider.live_property_values(path, requirements).await?;
202    build_live_propfind_item(href, snapshot, request, &values).map_err(Into::into)
203}
204
205/// Composes one PROPFIND item from the validated capability and one product value snapshot.
206///
207/// # Errors
208///
209/// Returns [`DavLivePropertyError`] when required metadata or property values are invalid.
210pub fn build_live_propfind_item<V: DavLivePropertyValueSnapshot>(
211    href: String,
212    snapshot: &DavCapabilitySnapshot,
213    request: &DavPropfindRequest,
214    values: &V,
215) -> Result<DavMultiStatusItem, DavLivePropertyError> {
216    let metadata = values.metadata();
217    let groups = match request {
218        DavPropfindRequest::AllProp { include } => {
219            let mut ok = Vec::new();
220            for_each_catalog_property(snapshot, |property| {
221                if property_in_allprop(property)
222                    && let Some(element) = resolve_live_property(
223                        snapshot,
224                        values,
225                        metadata,
226                        property,
227                        &canonical_property(property),
228                    )?
229                {
230                    ok.push(element);
231                }
232                Ok(())
233            })?;
234            append_all_dead_properties(values.dead_properties(), &mut ok);
235
236            let mut missing = Vec::new();
237            for requested in include {
238                if contains_expanded_name(&ok, requested) {
239                    continue;
240                }
241                match resolve_requested_property(snapshot, values, metadata, requested)? {
242                    Some(element) => ok.push(element),
243                    None => missing.push(dav_property_name_element(requested)),
244                }
245            }
246            propstat_groups(ok, missing)
247        }
248        DavPropfindRequest::PropName => {
249            let mut names = Vec::new();
250            for_each_catalog_property(snapshot, |property| {
251                if property_is_defined(snapshot, values, metadata, property) {
252                    names.push(dav_property_name_element(&canonical_property(property)));
253                }
254                Ok(())
255            })?;
256            for dead in values.dead_properties() {
257                names.push(dav_property_name_element(&dead_property_name(dead)));
258            }
259            if names.is_empty() {
260                Vec::new()
261            } else {
262                vec![DavPropStat {
263                    status: 200,
264                    properties: names,
265                }]
266            }
267        }
268        DavPropfindRequest::Prop(requested) => {
269            let mut ok = Vec::new();
270            let mut missing = Vec::new();
271            for property in requested {
272                match resolve_requested_property(snapshot, values, metadata, property)? {
273                    Some(element) => ok.push(element),
274                    None => missing.push(dav_property_name_element(property)),
275                }
276            }
277            propstat_groups(ok, missing)
278        }
279    };
280    Ok(DavMultiStatusItem::properties(href, groups))
281}
282
283/// Returns whether a requested property is a protected standard live property.
284#[must_use]
285pub fn is_protected_live_property(
286    snapshot: &DavCapabilitySnapshot,
287    requested: &DavRequestedProperty,
288) -> bool {
289    live_property(requested).is_some_and(|property| {
290        (is_base_property(property) || snapshot.supports_live_property(property))
291            && property_is_protected(property)
292    })
293}
294
295const BASE_LIVE_PROPERTIES: [DavLiveProperty; 10] = [
296    DavLiveProperty::CreationDate,
297    DavLiveProperty::DisplayName,
298    DavLiveProperty::GetContentLanguage,
299    DavLiveProperty::GetContentLength,
300    DavLiveProperty::GetContentType,
301    DavLiveProperty::GetEtag,
302    DavLiveProperty::GetLastModified,
303    DavLiveProperty::LockDiscovery,
304    DavLiveProperty::ResourceType,
305    DavLiveProperty::SupportedLock,
306];
307
308fn add_requirement(
309    snapshot: &DavCapabilitySnapshot,
310    requested: &DavRequestedProperty,
311    requirements: &mut DavLivePropertyRequirements,
312) {
313    let Some(property) = live_property(requested) else {
314        requirements.dead_properties = true;
315        return;
316    };
317    if !is_base_property(property) && !snapshot.supports_live_property(property) {
318        requirements.dead_properties = true;
319        return;
320    }
321    match property {
322        DavLiveProperty::CreationDate
323        | DavLiveProperty::DisplayName
324        | DavLiveProperty::GetContentLanguage
325        | DavLiveProperty::GetContentLength
326        | DavLiveProperty::GetContentType
327        | DavLiveProperty::GetEtag
328        | DavLiveProperty::GetLastModified
329        | DavLiveProperty::ResourceType => requirements.metadata = true,
330        DavLiveProperty::LockDiscovery | DavLiveProperty::SupportedLock => {
331            requirements.locks = true;
332        }
333        DavLiveProperty::QuotaAvailableBytes | DavLiveProperty::QuotaUsedBytes => {
334            requirements.quota |= snapshot.supports_extension(DavExtensionPackage::Quota);
335        }
336        DavLiveProperty::CurrentUserPrincipal => {
337            requirements.current_principal |=
338                snapshot.supports_extension(DavExtensionPackage::CurrentPrincipal);
339        }
340        DavLiveProperty::SyncToken => {
341            requirements.sync_token |=
342                snapshot.supports_extension(DavExtensionPackage::CollectionSync);
343        }
344        DavLiveProperty::AddMember => {
345            requirements.add_member |= snapshot.supports_extension(DavExtensionPackage::AddMember);
346        }
347        _ => requirements.extension_values |= snapshot.supports_live_property(property),
348    }
349}
350
351fn for_each_catalog_property(
352    snapshot: &DavCapabilitySnapshot,
353    mut visit: impl FnMut(DavLiveProperty) -> Result<(), DavLivePropertyError>,
354) -> Result<(), DavLivePropertyError> {
355    for property in BASE_LIVE_PROPERTIES {
356        visit(property)?;
357    }
358    for package in snapshot.extensions().iter() {
359        for property in package.descriptor().live_properties {
360            visit(*property)?;
361        }
362    }
363    Ok(())
364}
365
366fn property_in_allprop(property: DavLiveProperty) -> bool {
367    is_base_property(property) || property == DavLiveProperty::CurrentUserPrincipal
368}
369
370fn property_is_protected(property: DavLiveProperty) -> bool {
371    matches!(
372        property,
373        DavLiveProperty::GetContentLength
374            | DavLiveProperty::GetEtag
375            | DavLiveProperty::GetLastModified
376            | DavLiveProperty::LockDiscovery
377            | DavLiveProperty::ResourceType
378            | DavLiveProperty::SupportedLock
379            | DavLiveProperty::AlternateUriSet
380            | DavLiveProperty::PrincipalUrl
381            | DavLiveProperty::GroupMembership
382            | DavLiveProperty::SupportedPrivilegeSet
383            | DavLiveProperty::CurrentUserPrivilegeSet
384            | DavLiveProperty::Acl
385            | DavLiveProperty::AclRestrictions
386            | DavLiveProperty::InheritedAclSet
387            | DavLiveProperty::PrincipalCollectionSet
388            | DavLiveProperty::SupportedMethodSet
389            | DavLiveProperty::SupportedLivePropertySet
390            | DavLiveProperty::SupportedReportSet
391            | DavLiveProperty::CheckedIn
392            | DavLiveProperty::CheckedOut
393            | DavLiveProperty::SuccessorSet
394            | DavLiveProperty::CheckoutSet
395            | DavLiveProperty::VersionName
396            | DavLiveProperty::VersionSet
397            | DavLiveProperty::RootVersion
398            | DavLiveProperty::VersionHistory
399            | DavLiveProperty::WorkspaceCheckoutSet
400            | DavLiveProperty::Workspace
401            | DavLiveProperty::LabelNameSet
402            | DavLiveProperty::AutoUpdate
403            | DavLiveProperty::BaselineControlledCollection
404            | DavLiveProperty::BaselineCollection
405            | DavLiveProperty::VersionControlledConfiguration
406            | DavLiveProperty::BaselineControlledCollectionSet
407            | DavLiveProperty::ActivityVersionSet
408            | DavLiveProperty::ActivityCheckoutSet
409            | DavLiveProperty::CurrentWorkspaceSet
410            | DavLiveProperty::EclipsedSet
411            | DavLiveProperty::VersionControlledBindingSet
412            | DavLiveProperty::SupportedQueryGrammarSet
413            | DavLiveProperty::QuotaAvailableBytes
414            | DavLiveProperty::QuotaUsedBytes
415            | DavLiveProperty::SyncToken
416            | DavLiveProperty::CurrentUserPrincipal
417            | DavLiveProperty::OrderingType
418            | DavLiveProperty::RedirectLifetime
419            | DavLiveProperty::RefTarget
420            | DavLiveProperty::ResourceId
421            | DavLiveProperty::ParentSet
422            | DavLiveProperty::AddMember
423    )
424}
425
426fn property_is_defined<V: DavLivePropertyValueSnapshot>(
427    snapshot: &DavCapabilitySnapshot,
428    values: &V,
429    metadata: DavLivePropertyMetadata<'_>,
430    property: DavLiveProperty,
431) -> bool {
432    match property {
433        DavLiveProperty::CreationDate => metadata.creation_date.is_some(),
434        DavLiveProperty::DisplayName => metadata.display_name.is_some(),
435        DavLiveProperty::GetContentLanguage => metadata.content_language.is_some(),
436        DavLiveProperty::GetContentLength => metadata.content_length.is_some(),
437        DavLiveProperty::GetContentType => metadata.content_type.is_some(),
438        DavLiveProperty::GetEtag => metadata.etag.is_some(),
439        DavLiveProperty::GetLastModified => metadata.last_modified.is_some(),
440        DavLiveProperty::LockDiscovery | DavLiveProperty::SupportedLock => {
441            snapshot.declaration().compliance.class1
442        }
443        DavLiveProperty::ResourceType => {
444            snapshot.declaration().resource != DavResourceState::Unmapped
445        }
446        DavLiveProperty::QuotaAvailableBytes => values
447            .quota()
448            .and_then(|quota| quota.available_bytes)
449            .is_some(),
450        DavLiveProperty::QuotaUsedBytes => values.quota().is_some(),
451        DavLiveProperty::SyncToken => {
452            snapshot.supports_extension(DavExtensionPackage::CollectionSync)
453        }
454        DavLiveProperty::CurrentUserPrincipal => {
455            snapshot.supports_extension(DavExtensionPackage::CurrentPrincipal)
456        }
457        DavLiveProperty::AddMember => snapshot.supports_extension(DavExtensionPackage::AddMember),
458        DavLiveProperty::SupportedMethodSet
459        | DavLiveProperty::SupportedLivePropertySet
460        | DavLiveProperty::SupportedReportSet => {
461            snapshot.supports_extension(DavExtensionPackage::VersionControl)
462        }
463        DavLiveProperty::SupportedQueryGrammarSet => {
464            snapshot.supports_extension(DavExtensionPackage::Search)
465        }
466        _ => values.extension_value(property).is_some(),
467    }
468}
469
470fn resolve_requested_property<V: DavLivePropertyValueSnapshot>(
471    snapshot: &DavCapabilitySnapshot,
472    values: &V,
473    metadata: DavLivePropertyMetadata<'_>,
474    requested: &DavRequestedProperty,
475) -> Result<Option<DavXmlElement>, DavLivePropertyError> {
476    if let Some(property) = live_property(requested)
477        && (is_base_property(property) || snapshot.supports_live_property(property))
478    {
479        return resolve_live_property(snapshot, values, metadata, property, requested);
480    }
481    Ok(
482        find_dead_property(values.dead_properties(), requested).map(|dead| {
483            dav_dead_property_element(
484                &dead_property_name(dead),
485                Some(requested),
486                dead.xml.as_deref(),
487            )
488        }),
489    )
490}
491
492fn resolve_live_property<V: DavLivePropertyValueSnapshot>(
493    snapshot: &DavCapabilitySnapshot,
494    values: &V,
495    metadata: DavLivePropertyMetadata<'_>,
496    property: DavLiveProperty,
497    requested: &DavRequestedProperty,
498) -> Result<Option<DavXmlElement>, DavLivePropertyError> {
499    let element = match property {
500        DavLiveProperty::CreationDate => metadata
501            .creation_date
502            .map(|value| dav_property_text_element(requested, format_creation_date(value))),
503        DavLiveProperty::DisplayName => metadata
504            .display_name
505            .map(|value| dav_property_text_element(requested, value)),
506        DavLiveProperty::GetContentLanguage => metadata
507            .content_language
508            .map(|value| dav_property_text_element(requested, value)),
509        DavLiveProperty::GetContentLength => metadata
510            .content_length
511            .map(|value| dav_property_text_element(requested, value.to_string())),
512        DavLiveProperty::GetContentType => metadata
513            .content_type
514            .map(|value| dav_property_text_element(requested, value)),
515        DavLiveProperty::GetEtag => metadata
516            .etag
517            .map(|value| dav_property_text_element(requested, value)),
518        DavLiveProperty::GetLastModified => match metadata.last_modified {
519            Some(value) => Some(dav_property_text_element(
520                requested,
521                http_validators::try_format_http_date(value)
522                    .map_err(|_| DavLivePropertyError::InvalidRepresentation { property })?,
523            )),
524            None => None,
525        },
526        DavLiveProperty::ResourceType => resource_type_element(snapshot, requested),
527        DavLiveProperty::SupportedLock => {
528            let element = if snapshot.declaration().locking == crate::DavLockingCapability::Class2 {
529                dav_supported_lock_element()
530            } else {
531                dav_element("supportedlock")
532            };
533            Some(relexicalize(element, requested))
534        }
535        DavLiveProperty::LockDiscovery => Some(relexicalize(
536            dav_lock_discovery_element(values.active_locks()),
537            requested,
538        )),
539        DavLiveProperty::SupportedMethodSet => Some(supported_method_set(snapshot, requested)),
540        DavLiveProperty::SupportedLivePropertySet => {
541            Some(supported_live_property_set(snapshot, requested)?)
542        }
543        DavLiveProperty::SupportedReportSet => Some(supported_report_set(snapshot, requested)),
544        DavLiveProperty::SupportedQueryGrammarSet => {
545            Some(supported_query_grammar_set(snapshot, requested))
546        }
547        DavLiveProperty::QuotaUsedBytes => match values.quota() {
548            Some(quota) => Some(dav_property_text_element(
549                requested,
550                quota.used_bytes.to_string(),
551            )),
552            None if matches!(
553                snapshot.declaration().resource,
554                DavResourceState::Collection | DavResourceState::MountRoot
555            ) =>
556            {
557                return Err(DavLivePropertyError::MissingRequiredValue { property });
558            }
559            None => None,
560        },
561        DavLiveProperty::QuotaAvailableBytes => values.quota().and_then(|quota| {
562            quota
563                .available_bytes
564                .map(|value| dav_property_text_element(requested, value.to_string()))
565        }),
566        DavLiveProperty::SyncToken => {
567            Some(required_uri_text(values.sync_token(), property, requested)?)
568        }
569        DavLiveProperty::CurrentUserPrincipal => Some(current_principal_element(
570            values.current_principal(),
571            requested,
572        )?),
573        DavLiveProperty::AddMember => Some(required_href_element(
574            values.add_member_href(),
575            property,
576            requested,
577        )?),
578        _ => values
579            .extension_value(property)
580            .cloned()
581            .map(|element| relexicalize(element, requested)),
582    };
583    Ok(element)
584}
585
586fn supported_method_set(
587    snapshot: &DavCapabilitySnapshot,
588    requested: &DavRequestedProperty,
589) -> DavXmlElement {
590    let mut root = dav_property_name_element(requested);
591    for method in snapshot.supported_methods().iter() {
592        let mut supported = dav_element("supported-method");
593        supported
594            .attributes
595            .insert("name".to_owned(), method.as_str().to_owned());
596        root.children.push(DavXmlNode::Element(supported));
597    }
598    root
599}
600
601fn supported_live_property_set(
602    snapshot: &DavCapabilitySnapshot,
603    requested: &DavRequestedProperty,
604) -> Result<DavXmlElement, DavLivePropertyError> {
605    let mut root = dav_property_name_element(requested);
606    for_each_catalog_property(snapshot, |property| {
607        let mut supported = dav_element("supported-live-property");
608        let mut prop = dav_element("prop");
609        prop.children
610            .push(DavXmlNode::Element(dav_element(property.local_name())));
611        supported.children.push(DavXmlNode::Element(prop));
612        root.children.push(DavXmlNode::Element(supported));
613        Ok(())
614    })?;
615    Ok(root)
616}
617
618fn supported_report_set(
619    snapshot: &DavCapabilitySnapshot,
620    requested: &DavRequestedProperty,
621) -> DavXmlElement {
622    let mut root = dav_property_name_element(requested);
623    for package in snapshot.extensions().iter() {
624        for report in package.descriptor().reports {
625            let mut supported = dav_element("supported-report");
626            let mut report_element = dav_element("report");
627            report_element
628                .children
629                .push(DavXmlNode::Element(dav_element(report.local_name())));
630            supported.children.push(DavXmlNode::Element(report_element));
631            root.children.push(DavXmlNode::Element(supported));
632        }
633    }
634    root
635}
636
637fn supported_query_grammar_set(
638    snapshot: &DavCapabilitySnapshot,
639    requested: &DavRequestedProperty,
640) -> DavXmlElement {
641    let mut root = dav_property_name_element(requested);
642    for grammar in snapshot.declaration().search.grammars {
643        let mut supported = dav_element("supported-query-grammar");
644        let mut grammar_element = dav_element("grammar");
645        let mut grammar_type = DavXmlElement::new(grammar.xml_local_name);
646        if !grammar.xml_namespace.is_empty() {
647            grammar_type.namespace = Some(grammar.xml_namespace.to_owned());
648        }
649        grammar_element
650            .children
651            .push(DavXmlNode::Element(grammar_type));
652        supported
653            .children
654            .push(DavXmlNode::Element(grammar_element));
655        root.children.push(DavXmlNode::Element(supported));
656    }
657    root
658}
659
660fn resource_type_element(
661    snapshot: &DavCapabilitySnapshot,
662    requested: &DavRequestedProperty,
663) -> Option<DavXmlElement> {
664    let mut root = dav_property_name_element(requested);
665    let child = match snapshot.declaration().resource {
666        DavResourceState::Unmapped
667        | DavResourceState::File
668        | DavResourceState::AddMemberEndpoint => None,
669        DavResourceState::Collection | DavResourceState::MountRoot => Some("collection"),
670        DavResourceState::Principal => Some("principal"),
671        DavResourceState::RedirectReference => Some("redirectref"),
672    };
673    if let Some(child) = child {
674        root.children.push(DavXmlNode::Element(dav_element(child)));
675    }
676    (snapshot.declaration().resource != DavResourceState::Unmapped).then_some(root)
677}
678
679fn required_uri_text(
680    value: Option<&str>,
681    property: DavLiveProperty,
682    requested: &DavRequestedProperty,
683) -> Result<DavXmlElement, DavLivePropertyError> {
684    let value = value.ok_or(DavLivePropertyError::MissingRequiredValue { property })?;
685    validate_absolute_uri(value, property)?;
686    Ok(dav_property_text_element(requested, value))
687}
688
689fn required_href_element(
690    value: Option<&str>,
691    property: DavLiveProperty,
692    requested: &DavRequestedProperty,
693) -> Result<DavXmlElement, DavLivePropertyError> {
694    let value = value.ok_or(DavLivePropertyError::MissingRequiredValue { property })?;
695    validate_add_member_uri(value, property)?;
696    Ok(dav_property_child_element(
697        requested,
698        crate::dav_text_element("href", value),
699    ))
700}
701
702fn current_principal_element(
703    value: Option<DavCurrentPrincipal<'_>>,
704    requested: &DavRequestedProperty,
705) -> Result<DavXmlElement, DavLivePropertyError> {
706    let value = value.ok_or(DavLivePropertyError::MissingRequiredValue {
707        property: DavLiveProperty::CurrentUserPrincipal,
708    })?;
709    let child = match value {
710        DavCurrentPrincipal::Href(href) => {
711            validate_current_principal_url(href)?;
712            crate::dav_text_element("href", href)
713        }
714        DavCurrentPrincipal::Unauthenticated => dav_element("unauthenticated"),
715    };
716    Ok(dav_property_child_element(requested, child))
717}
718
719fn validate_absolute_uri(
720    value: &str,
721    property: DavLiveProperty,
722) -> Result<(), DavLivePropertyError> {
723    if uri_matches_policy(value, UriValidationPolicy::ABSOLUTE) {
724        Ok(())
725    } else {
726        Err(DavLivePropertyError::InvalidRepresentation { property })
727    }
728}
729
730fn validate_current_principal_url(value: &str) -> Result<(), DavLivePropertyError> {
731    let valid = uri_matches_policy(value, UriValidationPolicy::HTTP_ABSOLUTE);
732    if valid {
733        Ok(())
734    } else {
735        Err(DavLivePropertyError::InvalidRepresentation {
736            property: DavLiveProperty::CurrentUserPrincipal,
737        })
738    }
739}
740
741fn validate_add_member_uri(
742    value: &str,
743    property: DavLiveProperty,
744) -> Result<(), DavLivePropertyError> {
745    let valid = uri_matches_policy(value, UriValidationPolicy::HTTP_OR_ABSOLUTE_PATH);
746    if valid {
747        Ok(())
748    } else {
749        Err(DavLivePropertyError::InvalidRepresentation { property })
750    }
751}
752
753#[derive(Clone, Copy)]
754enum UriSchemePolicy {
755    Any,
756    HttpOrHttps,
757}
758
759#[derive(Clone, Copy)]
760struct UriValidationPolicy {
761    schemes: UriSchemePolicy,
762    allow_path_absolute: bool,
763}
764
765impl UriValidationPolicy {
766    const ABSOLUTE: Self = Self {
767        schemes: UriSchemePolicy::Any,
768        allow_path_absolute: false,
769    };
770    const HTTP_ABSOLUTE: Self = Self {
771        schemes: UriSchemePolicy::HttpOrHttps,
772        allow_path_absolute: false,
773    };
774    const HTTP_OR_ABSOLUTE_PATH: Self = Self {
775        schemes: UriSchemePolicy::HttpOrHttps,
776        allow_path_absolute: true,
777    };
778}
779
780fn uri_matches_policy(value: &str, policy: UriValidationPolicy) -> bool {
781    if value.trim() != value {
782        return false;
783    }
784    if policy.allow_path_absolute && value.starts_with('/') && !value.starts_with("//") {
785        return value
786            .parse::<http::Uri>()
787            .is_ok_and(|uri| uri.scheme().is_none() && uri.authority().is_none());
788    }
789    match policy.schemes {
790        UriSchemePolicy::Any => parse_absolute_url(value, "WebDAV live property").is_ok(),
791        UriSchemePolicy::HttpOrHttps => value.parse::<http::Uri>().is_ok_and(|uri| {
792            uri.scheme_str().is_some_and(|scheme| {
793                scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
794            }) && uri.authority().is_some()
795        }),
796    }
797}
798
799fn relexicalize(mut element: DavXmlElement, requested: &DavRequestedProperty) -> DavXmlElement {
800    element.name.clone_from(&requested.name);
801    element.prefix.clone_from(&requested.prefix);
802    element.namespace.clone_from(&requested.namespace);
803    let inherited = std::mem::take(&mut element.namespaces);
804    for (prefix, namespace) in inherited {
805        if prefix != "xml" && subtree_uses_prefix(&element, &prefix) {
806            element.namespaces.insert(prefix, namespace);
807        }
808    }
809    if let Some(namespace) = &element.namespace {
810        element.namespaces.insert(
811            element.prefix.clone().unwrap_or_default(),
812            namespace.clone(),
813        );
814    }
815    element
816}
817
818fn subtree_uses_prefix(element: &DavXmlElement, prefix: &str) -> bool {
819    element.prefix.as_deref() == Some(prefix)
820        || element.attributes.keys().any(|attribute| {
821            attribute
822                .split_once(':')
823                .is_some_and(|(attribute_prefix, _)| attribute_prefix == prefix)
824        })
825        || element.children.iter().any(|child| match child {
826            DavXmlNode::Element(child) => subtree_uses_prefix(child, prefix),
827            DavXmlNode::Text(_)
828            | DavXmlNode::CData(_)
829            | DavXmlNode::Comment(_)
830            | DavXmlNode::ProcessingInstruction(_, _) => false,
831        })
832}
833
834fn append_all_dead_properties(dead: &[DavProp], output: &mut Vec<DavXmlElement>) {
835    for property in dead {
836        let name = dead_property_name(property);
837        if !contains_expanded_name(output, &name) {
838            output.push(dav_dead_property_element(
839                &name,
840                None,
841                property.xml.as_deref(),
842            ));
843        }
844    }
845}
846
847fn find_dead_property<'a>(
848    dead: &'a [DavProp],
849    requested: &DavRequestedProperty,
850) -> Option<&'a DavProp> {
851    dead.iter().find(|property| {
852        property.name == requested.name && property.namespace == requested.namespace
853    })
854}
855
856fn dead_property_name(property: &DavProp) -> DavRequestedProperty {
857    DavRequestedProperty {
858        name: property.name.clone(),
859        namespace: property.namespace.clone(),
860        prefix: property.prefix.clone(),
861    }
862}
863
864fn canonical_property(property: DavLiveProperty) -> DavRequestedProperty {
865    DavRequestedProperty {
866        name: property.local_name().to_owned(),
867        namespace: Some("DAV:".to_owned()),
868        prefix: Some("D".to_owned()),
869    }
870}
871
872fn live_property(requested: &DavRequestedProperty) -> Option<DavLiveProperty> {
873    if requested.namespace.as_deref() != Some("DAV:") {
874        return None;
875    }
876    BASE_LIVE_PROPERTIES
877        .into_iter()
878        .chain(
879            DavExtensionPackage::ALL
880                .into_iter()
881                .flat_map(|package| package.descriptor().live_properties.iter().copied()),
882        )
883        .find(|property| property.local_name() == requested.name)
884}
885
886fn is_base_property(property: DavLiveProperty) -> bool {
887    BASE_LIVE_PROPERTIES.contains(&property)
888}
889
890fn contains_expanded_name(elements: &[DavXmlElement], requested: &DavRequestedProperty) -> bool {
891    elements
892        .iter()
893        .any(|element| element.name == requested.name && element.namespace == requested.namespace)
894}
895
896/// Atomic PROPPATCH execution decision and per-property protocol statuses.
897#[derive(Debug, Clone, PartialEq, Eq)]
898pub struct DavProppatchAtomicPlan {
899    /// Whether the product adapter should persist every requested property mutation.
900    pub apply: bool,
901    /// Status assigned to each property in request order.
902    pub statuses: Vec<StatusCode>,
903}
904
905/// Selects the RFC 4918 atomic PROPPATCH statuses from product-owned protection decisions.
906///
907/// When any property is protected, that property receives `403` and every otherwise valid
908/// property receives `424`. If none are protected, every property receives `200` and the adapter
909/// may apply the entire transaction.
910pub fn plan_atomic_proppatch(protected: impl IntoIterator<Item = bool>) -> DavProppatchAtomicPlan {
911    let protected = protected.into_iter().collect::<Vec<_>>();
912    let has_protected = protected.iter().any(|protected| *protected);
913    let statuses = protected
914        .into_iter()
915        .map(|protected| {
916            if has_protected {
917                if protected {
918                    StatusCode::FORBIDDEN
919                } else {
920                    StatusCode::FAILED_DEPENDENCY
921                }
922            } else {
923                StatusCode::OK
924            }
925        })
926        .collect();
927    DavProppatchAtomicPlan {
928        apply: !has_protected,
929        statuses,
930    }
931}
932
933/// Builds one PROPPATCH multistatus item by grouping property outcomes by status.
934pub fn build_proppatch_item(
935    href: String,
936    outcomes: impl IntoIterator<Item = (u16, DavXmlElement)>,
937) -> DavMultiStatusItem {
938    let mut groups = BTreeMap::<u16, Vec<DavXmlElement>>::new();
939    for (status, property) in outcomes {
940        groups.entry(status).or_default().push(property);
941    }
942    DavMultiStatusItem::properties(
943        href,
944        groups
945            .into_iter()
946            .map(|(status, properties)| DavPropStat { status, properties })
947            .collect(),
948    )
949}
950
951/// Builds the 207 XML response for PROPFIND or PROPPATCH items.
952///
953/// # Errors
954///
955/// Returns [`DavMultiStatusError`] when the property response exceeds default limits.
956pub fn property_multistatus_response(
957    items: Vec<DavMultiStatusItem>,
958) -> Result<DavResponse, DavMultiStatusError> {
959    property_multistatus_response_with_limits(items, DavMultiStatusLimits::default())
960}
961
962/// Builds a bounded 207 XML response with product-configured Multi-Status limits.
963///
964/// # Errors
965///
966/// Returns [`DavMultiStatusError`] when the property response exceeds supplied limits.
967pub fn property_multistatus_response_with_limits(
968    items: Vec<DavMultiStatusItem>,
969    limits: DavMultiStatusLimits,
970) -> Result<DavResponse, DavMultiStatusError> {
971    let mut response = DavResponse::bytes(
972        StatusCode::MULTI_STATUS,
973        dav_multistatus_bytes(items, limits)?,
974    );
975    response.headers.insert(
976        CONTENT_TYPE,
977        HeaderValue::from_static("application/xml; charset=utf-8"),
978    );
979    Ok(response)
980}
981
982/// Maps PROPFIND XML failures to their protocol response.
983///
984/// # Errors
985///
986/// Returns [`DavXmlError`] when the PROPFIND error response cannot be encoded.
987pub fn propfind_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
988    xml_request_error_response(error, "Invalid PROPFIND body")
989}
990
991/// Maps PROPPATCH XML failures to their protocol response.
992///
993/// # Errors
994///
995/// Returns [`DavXmlError`] when the PROPPATCH error response cannot be encoded.
996pub fn proppatch_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
997    xml_request_error_response(error, "Invalid PROPPATCH body")
998}
999
1000/// Builds the RFC 4918 finite-depth precondition response.
1001///
1002/// # Errors
1003///
1004/// Returns [`DavXmlError`] when the finite-depth error response cannot be encoded.
1005pub fn propfind_finite_depth_response() -> Result<DavResponse, DavXmlError> {
1006    xml_document_response(
1007        StatusCode::FORBIDDEN,
1008        &dav_error_element(&DavErrorCondition::PropfindFiniteDepth),
1009    )
1010}
1011
1012/// Returns a stable label for protocol metrics and tracing.
1013#[must_use]
1014pub const fn propfind_request_label(request: &DavPropfindRequest) -> &'static str {
1015    match request {
1016        DavPropfindRequest::AllProp { .. } => "allprop",
1017        DavPropfindRequest::PropName => "propname",
1018        DavPropfindRequest::Prop(_) => "prop",
1019    }
1020}
1021
1022fn propstat_groups(ok: Vec<DavXmlElement>, missing: Vec<DavXmlElement>) -> Vec<DavPropStat> {
1023    let mut groups = Vec::with_capacity(2);
1024    if !ok.is_empty() {
1025        groups.push(DavPropStat {
1026            status: 200,
1027            properties: ok,
1028        });
1029    }
1030    if !missing.is_empty() {
1031        groups.push(DavPropStat {
1032            status: 404,
1033            properties: missing,
1034        });
1035    }
1036    groups
1037}