1use 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#[must_use]
24pub fn format_creation_date(time: SystemTime) -> String {
25 chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339()
26}
27
28#[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 pub etag: Option<&'a str>,
38 pub last_modified: Option<SystemTime>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct DavQuotaSnapshot {
44 pub used_bytes: u64,
45 pub available_bytes: Option<u64>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum DavCurrentPrincipal<'a> {
52 Href(&'a str),
54 Unauthenticated,
56}
57
58#[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
75pub 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 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 fn extension_value(&self, _property: DavLiveProperty) -> Option<&DavXmlElement> {
112 None
113 }
114}
115
116#[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#[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#[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#[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
188pub 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
205pub 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
898pub struct DavProppatchAtomicPlan {
899 pub apply: bool,
901 pub statuses: Vec<StatusCode>,
903}
904
905pub 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
933pub 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
951pub fn property_multistatus_response(
957 items: Vec<DavMultiStatusItem>,
958) -> Result<DavResponse, DavMultiStatusError> {
959 property_multistatus_response_with_limits(items, DavMultiStatusLimits::default())
960}
961
962pub 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
982pub fn propfind_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
988 xml_request_error_response(error, "Invalid PROPFIND body")
989}
990
991pub fn proppatch_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
997 xml_request_error_response(error, "Invalid PROPPATCH body")
998}
999
1000pub 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#[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}