aster_forge_webdav/
capability.rs

1//! Resource-aware `WebDAV` capability declarations and validated discovery snapshots.
2
3use std::marker::PhantomData;
4
5use aster_forge_utils::url::parse_absolute_url;
6use aster_forge_xml::is_valid_xml_local_name;
7use headers::Mime;
8use http::HeaderValue;
9
10use crate::extension::{
11    DavExtensionBodyKind, DavExtensionPackage, DavExtensionSet, DavLiveProperty, DavPreferenceSet,
12    DavReportType, extension_body_kind, extension_methods,
13};
14use crate::request::{DavBodyPolicy, DavMethod, DavMethodSet};
15use crate::{DavBackendError, DavPath};
16
17/// Resource state visible to the capability planner.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum DavResourceState {
20    Unmapped,
21    File,
22    Collection,
23    MountRoot,
24    Principal,
25    RedirectReference,
26    AddMemberEndpoint,
27}
28
29/// Product-neutral RFC 3253 state of a request target.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub enum DavVersioningState {
32    /// The target does not expose the core version-control package.
33    #[default]
34    Unsupported,
35    /// An ordinary resource that can be placed under version control.
36    Versionable,
37    /// A version-controlled resource whose current version is checked in.
38    CheckedIn,
39    /// A version-controlled resource whose current version is checked out.
40    CheckedOut,
41    /// An immutable version resource.
42    Version,
43}
44
45/// Automatic side effects selected by a product for checked-in resources.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47pub enum DavAutoVersion {
48    /// Mutation requires an explicitly checked-out resource.
49    #[default]
50    None,
51    /// Automatically check out before mutation and check in after it succeeds.
52    CheckoutCheckin,
53    /// Check out before mutation, then check in unless the resource is write-locked.
54    CheckoutUnlockedCheckin,
55    /// Automatically check out before mutation and leave the resource checked out.
56    Checkout,
57    /// Automatically check out only when the checked-in resource is write-locked.
58    LockedCheckout,
59}
60
61/// RFC 3253 facts projected by the product capability provider.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
63pub struct DavVersioningCapabilities {
64    pub state: DavVersioningState,
65    pub auto_version: DavAutoVersion,
66    /// Whether the current target is protected by an active DAV write lock.
67    pub write_locked: bool,
68    /// Whether the current checkout is associated with a write lock after automatic checkout.
69    pub auto_checkout_lock: bool,
70    /// Whether this server permits DELETE of immutable version resources.
71    pub allow_version_delete: bool,
72}
73
74/// Request target supplied to a capability provider.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct DavCapabilityTarget {
77    pub path: DavPath,
78    pub is_mount_root: bool,
79}
80
81impl DavCapabilityTarget {
82    #[must_use]
83    pub fn new(path: DavPath, is_mount_root: bool) -> Self {
84        Self {
85            path,
86            is_mount_root,
87        }
88    }
89}
90
91/// Request context that can affect a product's capability projection.
92#[derive(Debug, Clone, PartialEq, Eq, Default)]
93pub struct DavCapabilityContext {
94    pub principal: Option<String>,
95}
96
97/// Locking compliance advertised for a resource.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum DavLockingCapability {
100    Disabled,
101    Class2,
102}
103
104/// Optional non-standard compatibility signals.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub struct DavCompatibilityCapabilities {
107    pub ms_author_via: bool,
108}
109
110/// Conditional request policy required before a partial write can execute.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum DavWritePrecondition {
113    Optional,
114    RequireStrongIfMatch,
115}
116
117/// RFC 9110 partial PUT support negotiated by private agreement with the client.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum DavPartialPutCapability {
120    #[default]
121    Disabled,
122    ContentRangeBytes {
123        precondition: DavWritePrecondition,
124    },
125}
126
127/// Body handling selected by one RFC 5789 patch document format.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum DavPatchBodyPolicy {
130    Bounded { maximum: usize },
131    Stream,
132}
133
134/// One statically declared RFC 5789 patch document format.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct DavPatchFormat {
137    /// Complete media type. Parameters participate in exact matching and duplicate detection.
138    pub media_type: &'static str,
139    pub body_policy: DavPatchBodyPolicy,
140    pub precondition: DavWritePrecondition,
141}
142
143/// RFC 5789 PATCH capability for one resource.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum DavPatchCapability {
146    #[default]
147    Disabled,
148    Formats(&'static [DavPatchFormat]),
149}
150
151/// Explicitly named private range-update compatibility surface.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
153pub enum DavPrivateUpdateRangeCapability {
154    #[default]
155    Disabled,
156    XUpdateRange {
157        precondition: DavWritePrecondition,
158    },
159}
160
161/// Mutation capabilities deliberately separate from ordinary replacement PUT.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163pub struct DavWriteCapabilities {
164    pub partial_put: DavPartialPutCapability,
165    pub patch: DavPatchCapability,
166    pub private_update_range: DavPrivateUpdateRangeCapability,
167}
168
169/// RFC 4918 compliance classes represented by a runtime declaration.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub struct DavComplianceClasses {
172    pub class1: bool,
173    /// RFC 4918 class 3 requires class 1 and does not imply class 2 locking.
174    pub class3: bool,
175}
176
177/// Resource-aware RFC 5323 query grammar discovery.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub struct DavSearchCapabilities {
180    /// Static grammar descriptors; SEARCH requires [`DavSearchGrammar::BASICSEARCH`].
181    pub grammars: &'static [DavSearchGrammar],
182}
183
184/// One RFC 5323 query grammar's HTTP identifier and XML element type.
185///
186/// RFC 5323 explicitly states that the DASL coded-URL does not necessarily correspond to the XML
187/// element namespace and local name. Keeping all three fields prevents discovery responses from
188/// guessing an XML `QName` from a URI.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct DavSearchGrammar {
191    /// Absolute-URI rendered inside angle brackets in the `DASL` response header.
192    pub coded_url: &'static str,
193    /// Namespace URI reference of the grammar element; an empty string means no namespace.
194    pub xml_namespace: &'static str,
195    /// XML local name of the grammar element.
196    pub xml_local_name: &'static str,
197}
198
199impl DavSearchGrammar {
200    /// Mandatory RFC 5323 basic search grammar.
201    pub const BASICSEARCH: Self = Self {
202        coded_url: "DAV:basicsearch",
203        xml_namespace: "DAV:",
204        xml_local_name: "basicsearch",
205    };
206
207    /// Declares a static SEARCH grammar without a runtime registry or per-request allocation.
208    #[must_use]
209    pub const fn new(
210        coded_url: &'static str,
211        xml_namespace: &'static str,
212        xml_local_name: &'static str,
213    ) -> Self {
214        Self {
215            coded_url,
216            xml_namespace,
217            xml_local_name,
218        }
219    }
220}
221
222/// Product implementation provides RFC 4918 class 1.
223pub trait DavClass1Support: Send + Sync {}
224/// Product implementation provides RFC 4918 class 2 locking.
225pub trait DavClass2Support: DavClass1Support {}
226/// Product implementation provides RFC 4918 class 3 revised semantics.
227///
228/// ```compile_fail
229/// use aster_forge_webdav::DavClass3Support;
230/// struct MissingClassOne;
231/// impl DavClass3Support for MissingClassOne {}
232/// ```
233pub trait DavClass3Support: DavClass1Support {}
234
235/// Product implementation provides the complete RFC 3744 package.
236pub trait DavAccessControlSupport: DavClass1Support {}
237/// Product implementation provides RFC 3253 version-control.
238pub trait DavVersionControlSupport: DavClass1Support {}
239pub trait DavCheckoutInPlaceSupport: DavVersionControlSupport {}
240pub trait DavVersionHistorySupport: DavVersionControlSupport {}
241/// Complete workspace support includes checkout-in-place and version-history at compile time.
242///
243/// ```compile_fail
244/// use aster_forge_webdav::{DavClass1Support, DavVersionControlSupport, DavWorkspaceSupport};
245/// struct IncompleteWorkspace;
246/// impl DavClass1Support for IncompleteWorkspace {}
247/// impl DavVersionControlSupport for IncompleteWorkspace {}
248/// impl DavWorkspaceSupport for IncompleteWorkspace {}
249/// ```
250pub trait DavWorkspaceSupport: DavCheckoutInPlaceSupport + DavVersionHistorySupport {}
251pub trait DavUpdateSupport: DavVersionControlSupport {}
252pub trait DavLabelSupport: DavVersionControlSupport {}
253pub trait DavWorkingResourceSupport: DavVersionControlSupport {}
254pub trait DavMergeSupport: DavVersionControlSupport {}
255pub trait DavBaselineSupport: DavVersionControlSupport {}
256pub trait DavActivitySupport: DavVersionControlSupport {}
257pub trait DavVersionControlledCollectionSupport: DavVersionControlSupport {}
258pub trait DavSearchSupport: DavClass1Support {}
259pub trait DavQuotaSupport: DavClass1Support {}
260pub trait DavCollectionSyncSupport: DavClass1Support {}
261pub trait DavExtendedMkcolSupport: DavClass1Support {}
262pub trait DavCurrentPrincipalSupport: DavClass1Support {}
263pub trait DavOrderedCollectionsSupport: DavClass1Support {}
264pub trait DavRedirectReferencesSupport: DavClass1Support {}
265pub trait DavBindingsSupport: DavClass1Support {}
266pub trait DavAddMemberSupport: DavClass1Support {}
267pub trait DavPreferSupport: DavClass1Support {}
268
269pub trait DavPartialPutSupport: Send + Sync {}
270pub trait DavPatchSupport: Send + Sync {}
271pub trait DavPrivateUpdateRangeSupport: Send + Sync {}
272
273mod sealed {
274    pub trait Sealed {}
275    pub trait ExtensionSealed {}
276    pub trait Class1Profile<Provider: ?Sized> {}
277}
278
279/// Static maximum capability profile selected by a product provider.
280pub trait DavCapabilityProfile<Provider: ?Sized>: sealed::Sealed {
281    const CLASS1: bool;
282    const CLASS2: bool;
283    const CLASS3: bool;
284    const EXTENSIONS: DavExtensionSet;
285    const PARTIAL_PUT: bool;
286    const PATCH: bool;
287    const PRIVATE_UPDATE_RANGE: bool;
288}
289
290pub struct DavNonDavProfile;
291pub struct DavClass1Profile;
292pub struct DavClass2Profile;
293/// RFC 4918 classes `1, 3`, without class 2 locking.
294pub struct DavClass3Profile;
295/// RFC 4918 classes `1, 2, 3`.
296pub struct DavClass2And3Profile;
297
298impl sealed::Sealed for DavNonDavProfile {}
299impl sealed::Sealed for DavClass1Profile {}
300impl sealed::Sealed for DavClass2Profile {}
301impl sealed::Sealed for DavClass3Profile {}
302impl sealed::Sealed for DavClass2And3Profile {}
303
304macro_rules! base_profile {
305    ($profile:ty, $provider:path, $class1:expr, $class2:expr, $class3:expr) => {
306        impl<Provider: $provider + ?Sized> DavCapabilityProfile<Provider> for $profile {
307            const CLASS1: bool = $class1;
308            const CLASS2: bool = $class2;
309            const CLASS3: bool = $class3;
310            const EXTENSIONS: DavExtensionSet = DavExtensionSet::empty();
311            const PARTIAL_PUT: bool = false;
312            const PATCH: bool = false;
313            const PRIVATE_UPDATE_RANGE: bool = false;
314        }
315    };
316}
317
318impl<Provider: ?Sized> DavCapabilityProfile<Provider> for DavNonDavProfile {
319    const CLASS1: bool = false;
320    const CLASS2: bool = false;
321    const CLASS3: bool = false;
322    const EXTENSIONS: DavExtensionSet = DavExtensionSet::empty();
323    const PARTIAL_PUT: bool = false;
324    const PATCH: bool = false;
325    const PRIVATE_UPDATE_RANGE: bool = false;
326}
327base_profile!(DavClass1Profile, DavClass1Support, true, false, false);
328base_profile!(DavClass2Profile, DavClass2Support, true, true, false);
329base_profile!(DavClass3Profile, DavClass3Support, true, false, true);
330
331impl<Provider: DavClass1Support + ?Sized> sealed::Class1Profile<Provider> for DavClass1Profile {}
332impl<Provider: DavClass2Support + ?Sized> sealed::Class1Profile<Provider> for DavClass2Profile {}
333impl<Provider: DavClass3Support + ?Sized> sealed::Class1Profile<Provider> for DavClass3Profile {}
334
335impl<Provider: DavClass2Support + DavClass3Support + ?Sized> DavCapabilityProfile<Provider>
336    for DavClass2And3Profile
337{
338    const CLASS1: bool = true;
339    const CLASS2: bool = true;
340    const CLASS3: bool = true;
341    const EXTENSIONS: DavExtensionSet = DavExtensionSet::empty();
342    const PARTIAL_PUT: bool = false;
343    const PATCH: bool = false;
344    const PRIVATE_UPDATE_RANGE: bool = false;
345}
346
347impl<Provider: DavClass2Support + DavClass3Support + ?Sized> sealed::Class1Profile<Provider>
348    for DavClass2And3Profile
349{
350}
351
352/// Adds one statically implemented RFC package to another profile.
353pub struct DavWithExtension<Base, Extension>(PhantomData<fn() -> (Base, Extension)>);
354
355impl<Base: sealed::Sealed, Extension> sealed::Sealed for DavWithExtension<Base, Extension> {}
356
357/// Sealed link between a package marker and its product implementation trait.
358pub trait DavExtensionMarker<Provider: ?Sized>: sealed::ExtensionSealed {
359    const PACKAGES: DavExtensionSet;
360}
361
362macro_rules! extension_marker {
363    ($marker:ident, $support:path, $package:ident $(, $required:ident)*) => {
364        pub struct $marker;
365        impl sealed::ExtensionSealed for $marker {}
366        impl<Provider: $support + ?Sized> DavExtensionMarker<Provider> for $marker {
367            const PACKAGES: DavExtensionSet = DavExtensionSet::from_packages(&[
368                $(DavExtensionPackage::$required,)*
369                DavExtensionPackage::$package,
370            ]);
371        }
372    };
373}
374
375extension_marker!(
376    DavAccessControlExtension,
377    DavAccessControlSupport,
378    AccessControl
379);
380extension_marker!(
381    DavVersionControlExtension,
382    DavVersionControlSupport,
383    VersionControl
384);
385extension_marker!(
386    DavCheckoutInPlaceExtension,
387    DavCheckoutInPlaceSupport,
388    CheckoutInPlace,
389    VersionControl
390);
391extension_marker!(
392    DavVersionHistoryExtension,
393    DavVersionHistorySupport,
394    VersionHistory,
395    VersionControl
396);
397extension_marker!(
398    DavWorkspaceExtension,
399    DavWorkspaceSupport,
400    Workspace,
401    VersionControl,
402    CheckoutInPlace,
403    VersionHistory
404);
405extension_marker!(DavUpdateExtension, DavUpdateSupport, Update, VersionControl);
406extension_marker!(DavLabelExtension, DavLabelSupport, Label, VersionControl);
407extension_marker!(
408    DavWorkingResourceExtension,
409    DavWorkingResourceSupport,
410    WorkingResource,
411    VersionControl
412);
413extension_marker!(DavMergeExtension, DavMergeSupport, Merge, VersionControl);
414extension_marker!(
415    DavBaselineExtension,
416    DavBaselineSupport,
417    Baseline,
418    VersionControl
419);
420extension_marker!(
421    DavActivityExtension,
422    DavActivitySupport,
423    Activity,
424    VersionControl
425);
426extension_marker!(
427    DavVersionControlledCollectionExtension,
428    DavVersionControlledCollectionSupport,
429    VersionControlledCollection,
430    VersionControl
431);
432extension_marker!(DavSearchExtension, DavSearchSupport, Search);
433extension_marker!(DavQuotaExtension, DavQuotaSupport, Quota);
434extension_marker!(
435    DavCollectionSyncExtension,
436    DavCollectionSyncSupport,
437    CollectionSync
438);
439extension_marker!(
440    DavExtendedMkcolExtension,
441    DavExtendedMkcolSupport,
442    ExtendedMkcol
443);
444extension_marker!(
445    DavCurrentPrincipalExtension,
446    DavCurrentPrincipalSupport,
447    CurrentPrincipal
448);
449extension_marker!(
450    DavOrderedCollectionsExtension,
451    DavOrderedCollectionsSupport,
452    OrderedCollections
453);
454extension_marker!(
455    DavRedirectReferencesExtension,
456    DavRedirectReferencesSupport,
457    RedirectReferences
458);
459extension_marker!(DavBindingsExtension, DavBindingsSupport, Bindings);
460extension_marker!(DavAddMemberExtension, DavAddMemberSupport, AddMember);
461extension_marker!(DavPreferExtension, DavPreferSupport, Prefer);
462
463impl<Provider, Base, Extension> DavCapabilityProfile<Provider> for DavWithExtension<Base, Extension>
464where
465    Provider: ?Sized,
466    Base: DavCapabilityProfile<Provider> + sealed::Class1Profile<Provider>,
467    Extension: DavExtensionMarker<Provider>,
468{
469    const CLASS1: bool = Base::CLASS1;
470    const CLASS2: bool = Base::CLASS2;
471    const CLASS3: bool = Base::CLASS3;
472    const EXTENSIONS: DavExtensionSet = Base::EXTENSIONS.union(Extension::PACKAGES);
473    const PARTIAL_PUT: bool = Base::PARTIAL_PUT;
474    const PATCH: bool = Base::PATCH;
475    const PRIVATE_UPDATE_RANGE: bool = Base::PRIVATE_UPDATE_RANGE;
476}
477
478impl<Provider, Base, Extension> sealed::Class1Profile<Provider>
479    for DavWithExtension<Base, Extension>
480where
481    Provider: ?Sized,
482    Base: sealed::Class1Profile<Provider>,
483    Extension: DavExtensionMarker<Provider>,
484{
485}
486
487/// Builds a readable aggregate profile without a runtime registry.
488///
489/// Selecting a package without its implementation marker is a compile-time error:
490///
491/// ```compile_fail
492/// use aster_forge_webdav::{
493///     DavBackendError, DavCapabilityContext, DavCapabilityDeclaration, DavCapabilityProvider,
494///     DavCapabilityTarget, DavClass1Profile, DavClass1Support, DavQuotaExtension,
495///     dav_capability_profile,
496/// };
497/// struct MissingQuotaImplementation;
498/// impl DavClass1Support for MissingQuotaImplementation {}
499/// impl DavCapabilityProvider for MissingQuotaImplementation {
500///     type Profile = dav_capability_profile!(DavClass1Profile; DavQuotaExtension);
501///     async fn capabilities(
502///         &self,
503///         _target: &DavCapabilityTarget,
504///         _context: &DavCapabilityContext,
505///     ) -> Result<DavCapabilityDeclaration, DavBackendError> {
506///         loop {}
507///     }
508/// }
509/// ```
510///
511/// An RFC package also cannot be attached to a non-Class-1 base profile:
512///
513/// ```compile_fail
514/// use aster_forge_webdav::{
515///     DavBackendError, DavCapabilityContext, DavCapabilityDeclaration, DavCapabilityProvider,
516///     DavCapabilityTarget, DavClass1Support, DavNonDavProfile, DavQuotaExtension,
517///     DavQuotaSupport, dav_capability_profile,
518/// };
519/// struct InvalidBase;
520/// impl DavClass1Support for InvalidBase {}
521/// impl DavQuotaSupport for InvalidBase {}
522/// impl DavCapabilityProvider for InvalidBase {
523///     type Profile = dav_capability_profile!(DavNonDavProfile; DavQuotaExtension);
524///     async fn capabilities(
525///         &self,
526///         _target: &DavCapabilityTarget,
527///         _context: &DavCapabilityContext,
528///     ) -> Result<DavCapabilityDeclaration, DavBackendError> {
529///         loop {}
530///     }
531/// }
532/// ```
533#[macro_export]
534macro_rules! dav_capability_profile {
535    ($base:ty $(,)?) => { $base };
536    ($base:ty; $extension:ty $(,)?) => {
537        $crate::DavWithExtension<$base, $extension>
538    };
539    ($base:ty; $extension:ty, $($remaining:ty),+ $(,)?) => {
540        $crate::dav_capability_profile!(
541            $crate::DavWithExtension<$base, $extension>;
542            $($remaining),+
543        )
544    };
545}
546
547pub struct DavWithPartialPut<Base>(PhantomData<fn() -> Base>);
548pub struct DavWithPatch<Base>(PhantomData<fn() -> Base>);
549pub struct DavWithPrivateUpdateRange<Base>(PhantomData<fn() -> Base>);
550
551impl<Provider: ?Sized, Base: sealed::Class1Profile<Provider>> sealed::Class1Profile<Provider>
552    for DavWithPartialPut<Base>
553{
554}
555impl<Provider: ?Sized, Base: sealed::Class1Profile<Provider>> sealed::Class1Profile<Provider>
556    for DavWithPatch<Base>
557{
558}
559impl<Provider: ?Sized, Base: sealed::Class1Profile<Provider>> sealed::Class1Profile<Provider>
560    for DavWithPrivateUpdateRange<Base>
561{
562}
563
564impl<Base: sealed::Sealed> sealed::Sealed for DavWithPartialPut<Base> {}
565impl<Base: sealed::Sealed> sealed::Sealed for DavWithPatch<Base> {}
566impl<Base: sealed::Sealed> sealed::Sealed for DavWithPrivateUpdateRange<Base> {}
567
568macro_rules! write_profile {
569    ($wrapper:ident, $support:path, $partial:expr, $patch:expr, $private:expr) => {
570        impl<Provider, Base> DavCapabilityProfile<Provider> for $wrapper<Base>
571        where
572            Provider: $support + ?Sized,
573            Base: DavCapabilityProfile<Provider>,
574        {
575            const CLASS1: bool = Base::CLASS1;
576            const CLASS2: bool = Base::CLASS2;
577            const CLASS3: bool = Base::CLASS3;
578            const EXTENSIONS: DavExtensionSet = Base::EXTENSIONS;
579            const PARTIAL_PUT: bool = $partial || Base::PARTIAL_PUT;
580            const PATCH: bool = $patch || Base::PATCH;
581            const PRIVATE_UPDATE_RANGE: bool = $private || Base::PRIVATE_UPDATE_RANGE;
582        }
583    };
584}
585
586write_profile!(DavWithPartialPut, DavPartialPutSupport, true, false, false);
587write_profile!(DavWithPatch, DavPatchSupport, false, true, false);
588write_profile!(
589    DavWithPrivateUpdateRange,
590    DavPrivateUpdateRangeSupport,
591    false,
592    false,
593    true
594);
595
596/// Product-owned facts projected for one target and request context.
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct DavCapabilityDeclaration {
599    pub resource: DavResourceState,
600    pub versioning: DavVersioningCapabilities,
601    /// Methods allowed for this principal and target. Package implementation methods are derived.
602    pub methods: DavMethodSet,
603    pub locking: DavLockingCapability,
604    pub extensions: DavExtensionSet,
605    pub search: DavSearchCapabilities,
606    pub compatibility: DavCompatibilityCapabilities,
607    pub writes: DavWriteCapabilities,
608    pub compliance: DavComplianceClasses,
609}
610
611impl DavCapabilityDeclaration {
612    #[must_use]
613    pub const fn new(resource: DavResourceState, methods: DavMethodSet) -> Self {
614        Self {
615            resource,
616            versioning: DavVersioningCapabilities {
617                state: DavVersioningState::Unsupported,
618                auto_version: DavAutoVersion::None,
619                write_locked: false,
620                auto_checkout_lock: false,
621                allow_version_delete: false,
622            },
623            methods,
624            locking: DavLockingCapability::Disabled,
625            extensions: DavExtensionSet::empty(),
626            search: DavSearchCapabilities { grammars: &[] },
627            compatibility: DavCompatibilityCapabilities {
628                ms_author_via: false,
629            },
630            writes: DavWriteCapabilities {
631                partial_put: DavPartialPutCapability::Disabled,
632                patch: DavPatchCapability::Disabled,
633                private_update_range: DavPrivateUpdateRangeCapability::Disabled,
634            },
635            compliance: DavComplianceClasses {
636                class1: false,
637                class3: false,
638            },
639        }
640    }
641}
642
643/// Failure raised when a product declaration violates the protocol model.
644#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
645pub enum DavCapabilityPlanError {
646    #[error("OPTIONS must be advertised for every WebDAV target")]
647    OptionsMissing,
648    #[error("HEAD requires GET in a WebDAV capability declaration")]
649    HeadWithoutGet,
650    #[error("DAV locking compliance class 2 requires class 1")]
651    Class2WithoutClass1,
652    #[error("DAV compliance class 2 requires LOCK and UNLOCK methods")]
653    Class2WithoutLockMethods,
654    #[error("LOCK and UNLOCK require DAV compliance class 2")]
655    LockMethodsWithoutClass2,
656    #[error("DAV compliance class 3 requires class 1")]
657    Class3WithoutClass1,
658    #[error("RFC extension {package:?} requires class 1")]
659    ExtensionWithoutClass1 { package: DavExtensionPackage },
660    #[error("RFC extension {package:?} requires {required:?}")]
661    ExtensionMissingPrerequisite {
662        package: DavExtensionPackage,
663        required: DavExtensionPackage,
664    },
665    #[error("RFC extension {package:?} does not apply to {resource:?}")]
666    ExtensionNotApplicable {
667        package: DavExtensionPackage,
668        resource: DavResourceState,
669    },
670    #[error("the RFC 3253 version-control package requires explicit target versioning facts")]
671    VersionControlWithoutTarget,
672    #[error("versioning target facts require the RFC 3253 version-control package")]
673    VersioningTargetWithoutPackage,
674    #[error("automatic versioning is only valid for a checked-in or checked-out resource")]
675    AutoVersionNotApplicable,
676    #[error("a write-locked versioning target requires DAV locking compliance class 2")]
677    WriteLockWithoutClass2,
678    #[error("an automatic checkout lock is only valid for a write-locked checked-out resource")]
679    AutoCheckoutLockNotApplicable,
680    #[error(
681        "an automatic checkout lock requires an auto-version mode that can leave a locked resource checked out"
682    )]
683    AutoCheckoutLockWithoutApplicableMode,
684    #[error("version DELETE policy is only applicable to immutable version resources")]
685    VersionDeletePolicyNotApplicable,
686    #[error("VERSION-CONTROL is only applicable to versionable or version-controlled resources")]
687    VersionControlMethodNotApplicable,
688    #[error("method {method:?} requires an applicable RFC extension package")]
689    ExtensionMethodWithoutPackage { method: DavMethod },
690    #[error("SEARCH requires at least DAV:basicsearch")]
691    SearchWithoutBasicSearch,
692    #[error("SEARCH grammars require the SEARCH extension package")]
693    SearchGrammarsWithoutPackage,
694    #[error("SEARCH grammar {index} has invalid coded URL {coded_url:?}")]
695    InvalidSearchGrammarCodedUrl {
696        index: usize,
697        coded_url: &'static str,
698    },
699    #[error("SEARCH grammar {index} has invalid XML local name {xml_local_name:?}")]
700    InvalidSearchGrammarXmlLocalName {
701        index: usize,
702        xml_local_name: &'static str,
703    },
704    #[error("SEARCH grammar {index} has invalid XML namespace {xml_namespace:?}")]
705    InvalidSearchGrammarXmlNamespace {
706        index: usize,
707        xml_namespace: &'static str,
708    },
709    #[error("SEARCH grammar {index} duplicates grammar {previous_index}")]
710    DuplicateSearchGrammar {
711        index: usize,
712        previous_index: usize,
713        coded_url: &'static str,
714        xml_namespace: &'static str,
715        xml_local_name: &'static str,
716    },
717    #[error("partial PUT requires the PUT method")]
718    PartialPutWithoutPut,
719    #[error("PATCH requires at least one declared patch document format")]
720    PatchWithoutFormats,
721    #[error("declared patch document formats require the PATCH method")]
722    PatchFormatsWithoutMethod,
723    #[error("a patch document media type is invalid")]
724    InvalidPatchMediaType,
725    #[error("patch document media types must be unique")]
726    DuplicatePatchMediaType,
727    #[error("private X-Update-Range support requires the PUT method")]
728    PrivateUpdateRangeWithoutPut,
729    #[error("runtime class 1 capability exceeds the provider's static profile")]
730    Class1ExceedsProfile,
731    #[error("runtime class 2 capability exceeds the provider's static profile")]
732    Class2ExceedsProfile,
733    #[error("runtime class 3 capability exceeds the provider's static profile")]
734    Class3ExceedsProfile,
735    #[error("runtime extension {package:?} exceeds the provider's static profile")]
736    ExtensionExceedsProfile { package: DavExtensionPackage },
737    #[error("runtime partial PUT capability exceeds the provider's static profile")]
738    PartialPutExceedsProfile,
739    #[error("runtime PATCH capability exceeds the provider's static profile")]
740    PatchExceedsProfile,
741    #[error("runtime private update-range capability exceeds the provider's static profile")]
742    PrivateUpdateRangeExceedsProfile,
743    #[error("capability header representation is invalid")]
744    InvalidHeaderRepresentation,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
748pub enum DavCapabilityEvaluationError {
749    #[error(transparent)]
750    Backend(#[from] DavBackendError),
751    #[error(transparent)]
752    Plan(#[from] DavCapabilityPlanError),
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
756pub enum DavMethodGateError {
757    #[error("WebDAV method is not allowed for this resource")]
758    MethodNotAllowed,
759}
760
761/// Immutable capability state consumed by discovery, dispatch and property planning.
762#[derive(Debug, Clone, PartialEq, Eq)]
763pub struct DavCapabilitySnapshot {
764    declaration: DavCapabilityDeclaration,
765    supported_methods: DavMethodSet,
766    dispatch_methods: DavMethodSet,
767    live_properties: DavLivePropertySet,
768    reports: DavReportSet,
769    preferences: DavPreferenceSet,
770    allow: HeaderValue,
771    dav: Option<HeaderValue>,
772    dasl: Option<HeaderValue>,
773    accept_patch: Option<HeaderValue>,
774    ms_author_via: bool,
775}
776
777impl DavCapabilitySnapshot {
778    #[must_use]
779    pub fn declaration(&self) -> &DavCapabilityDeclaration {
780        &self.declaration
781    }
782
783    #[must_use]
784    pub const fn methods(&self) -> DavMethodSet {
785        self.declaration.methods
786    }
787
788    #[must_use]
789    pub const fn supported_methods(&self) -> DavMethodSet {
790        self.supported_methods
791    }
792
793    #[must_use]
794    pub const fn allows(&self, method: DavMethod) -> bool {
795        self.methods().contains(method)
796    }
797
798    #[must_use]
799    pub const fn supports(&self, method: DavMethod) -> bool {
800        self.supported_methods.contains(method)
801    }
802
803    /// Returns whether the protocol engine should dispatch this method for target-state handling.
804    #[must_use]
805    pub const fn dispatches(&self, method: DavMethod) -> bool {
806        self.dispatch_methods.contains(method)
807    }
808
809    #[must_use]
810    pub const fn extensions(&self) -> DavExtensionSet {
811        self.declaration.extensions
812    }
813
814    #[must_use]
815    pub const fn supports_extension(&self, package: DavExtensionPackage) -> bool {
816        self.extensions().contains(package)
817    }
818
819    #[must_use]
820    pub const fn supports_live_property(&self, property: DavLiveProperty) -> bool {
821        self.live_properties.contains(property)
822    }
823
824    #[must_use]
825    pub const fn supports_report(&self, report: DavReportType) -> bool {
826        self.reports.contains(report)
827    }
828
829    #[must_use]
830    pub const fn preferences(&self) -> DavPreferenceSet {
831        self.preferences
832    }
833
834    #[must_use]
835    pub fn allow_header(&self) -> &HeaderValue {
836        &self.allow
837    }
838
839    #[must_use]
840    pub fn dav_header(&self) -> Option<&HeaderValue> {
841        self.dav.as_ref()
842    }
843
844    #[must_use]
845    pub fn dasl_header(&self) -> Option<&HeaderValue> {
846        self.dasl.as_ref()
847    }
848
849    #[must_use]
850    pub fn accept_patch_header(&self) -> Option<&HeaderValue> {
851        self.accept_patch.as_ref()
852    }
853
854    #[must_use]
855    pub const fn writes(&self) -> DavWriteCapabilities {
856        self.declaration.writes
857    }
858
859    pub(crate) const fn patch_formats(&self) -> Option<&'static [DavPatchFormat]> {
860        match self.declaration.writes.patch {
861            DavPatchCapability::Disabled => None,
862            DavPatchCapability::Formats(formats) => Some(formats),
863        }
864    }
865
866    #[must_use]
867    pub const fn has_ms_author_via(&self) -> bool {
868        self.ms_author_via
869    }
870
871    /// Selects body handling from the same snapshot used by dispatch and discovery.
872    ///
873    /// # Errors
874    ///
875    /// Returns [`DavMethodGateError`] when the capability snapshot does not dispatch the method.
876    pub fn body_policy(
877        &self,
878        method: DavMethod,
879        xml_limit: usize,
880    ) -> Result<Option<DavBodyPolicy>, DavMethodGateError> {
881        if !self.dispatches(method) {
882            return Err(DavMethodGateError::MethodNotAllowed);
883        }
884        let extension_kind = || {
885            extension_body_kind(
886                self.declaration.extensions,
887                self.declaration.resource,
888                method,
889            )
890        };
891        Ok(match method {
892            DavMethod::Patch => None,
893            DavMethod::Mkcol => Some(extension_kind().map_or(DavBodyPolicy::Empty, |kind| {
894                extension_body_policy(kind, xml_limit)
895            })),
896            DavMethod::Options
897            | DavMethod::Delete
898            | DavMethod::Copy
899            | DavMethod::Move
900            | DavMethod::Unlock => Some(DavBodyPolicy::Empty),
901            DavMethod::Propfind | DavMethod::Proppatch | DavMethod::Lock => {
902                Some(DavBodyPolicy::BoundedXml { maximum: xml_limit })
903            }
904            DavMethod::Post => Some(extension_kind().map_or(DavBodyPolicy::Stream, |kind| {
905                extension_body_policy(kind, xml_limit)
906            })),
907            DavMethod::Put => Some(DavBodyPolicy::Stream),
908            DavMethod::Get | DavMethod::Head => Some(DavBodyPolicy::Unused),
909            DavMethod::Acl
910            | DavMethod::Report
911            | DavMethod::VersionControl
912            | DavMethod::Checkout
913            | DavMethod::Checkin
914            | DavMethod::Uncheckout
915            | DavMethod::Mkworkspace
916            | DavMethod::Update
917            | DavMethod::Label
918            | DavMethod::Merge
919            | DavMethod::BaselineControl
920            | DavMethod::Mkactivity
921            | DavMethod::Search
922            | DavMethod::Orderpatch
923            | DavMethod::Mkredirectref
924            | DavMethod::Updateredirectref
925            | DavMethod::Bind
926            | DavMethod::Unbind
927            | DavMethod::Rebind => {
928                extension_kind().map(|kind| extension_body_policy(kind, xml_limit))
929            }
930        })
931    }
932}
933
934/// Builds and validates a capability snapshot from product-owned runtime facts.
935///
936/// # Errors
937///
938/// Returns [`DavCapabilityPlanError`] when the declaration violates capability rules.
939pub fn plan_capabilities(
940    mut declaration: DavCapabilityDeclaration,
941) -> Result<DavCapabilitySnapshot, DavCapabilityPlanError> {
942    validate_base_methods(&mut declaration)?;
943    validate_compliance(&declaration)?;
944    validate_extensions(&declaration)?;
945    validate_extension_methods(&declaration)?;
946    validate_writes(&declaration)?;
947
948    let package_methods = extension_methods_for_declaration(&declaration);
949    let supported_methods = declaration.methods.union(package_methods);
950    let dispatch_methods = declaration
951        .methods
952        .union(unmapped_dispatch_methods(declaration.resource));
953    let allow = header_value(&declaration.methods.render())?;
954    let dav = render_dav_header(&declaration)?;
955    let dasl = render_dasl_header(&declaration)?;
956    let accept_patch = match declaration.writes.patch {
957        DavPatchCapability::Disabled => None,
958        DavPatchCapability::Formats(formats) => Some(header_value(&render_patch_formats(formats))?),
959    };
960    let mut live_properties = DavLivePropertySet::empty();
961    let mut reports = DavReportSet::empty();
962    let mut preferences = DavPreferenceSet::empty();
963    for package in declaration.extensions.iter() {
964        let descriptor = package.descriptor();
965        for property in descriptor.live_properties {
966            if package != DavExtensionPackage::VersionControl
967                || versioning_live_property(declaration.versioning.state, *property)
968            {
969                live_properties.insert(*property);
970            }
971        }
972        for report in descriptor.reports {
973            if package != DavExtensionPackage::VersionControl
974                || versioning_report(declaration.versioning.state, *report)
975            {
976                reports.insert(*report);
977            }
978        }
979        preferences = preferences.union(descriptor.preferences);
980    }
981    let ms_author_via = declaration.compatibility.ms_author_via;
982    Ok(DavCapabilitySnapshot {
983        declaration,
984        supported_methods,
985        dispatch_methods,
986        live_properties,
987        reports,
988        preferences,
989        allow,
990        dav,
991        dasl,
992        accept_patch,
993        ms_author_via,
994    })
995}
996
997const fn unmapped_dispatch_methods(resource: DavResourceState) -> DavMethodSet {
998    if matches!(resource, DavResourceState::Unmapped) {
999        DavMethodSet::from_methods(&[
1000            DavMethod::Get,
1001            DavMethod::Head,
1002            DavMethod::Delete,
1003            DavMethod::Copy,
1004            DavMethod::Move,
1005            DavMethod::Propfind,
1006            DavMethod::Proppatch,
1007        ])
1008    } else {
1009        DavMethodSet::empty()
1010    }
1011}
1012
1013#[expect(
1014    async_fn_in_trait,
1015    reason = "The provider is generic and intentionally preserves native async futures."
1016)]
1017pub trait DavCapabilityProvider: Send + Sync {
1018    type Profile: DavCapabilityProfile<Self>;
1019
1020    async fn capabilities(
1021        &self,
1022        target: &DavCapabilityTarget,
1023        context: &DavCapabilityContext,
1024    ) -> Result<DavCapabilityDeclaration, DavBackendError>;
1025}
1026
1027/// Resolves product facts and enforces the static profile maximum before planning.
1028///
1029/// # Errors
1030///
1031/// Returns an error when provider lookup fails or its declaration exceeds the static profile.
1032pub async fn plan_capabilities_with_provider<Provider: DavCapabilityProvider>(
1033    provider: &Provider,
1034    target: &DavCapabilityTarget,
1035    context: &DavCapabilityContext,
1036) -> Result<DavCapabilitySnapshot, DavCapabilityEvaluationError> {
1037    let declaration = provider.capabilities(target, context).await?;
1038    if declaration.compliance.class1 && !Provider::Profile::CLASS1 {
1039        return Err(DavCapabilityPlanError::Class1ExceedsProfile.into());
1040    }
1041    if declaration.locking == DavLockingCapability::Class2 && !Provider::Profile::CLASS2 {
1042        return Err(DavCapabilityPlanError::Class2ExceedsProfile.into());
1043    }
1044    if declaration.compliance.class3 && !Provider::Profile::CLASS3 {
1045        return Err(DavCapabilityPlanError::Class3ExceedsProfile.into());
1046    }
1047    for package in declaration.extensions.iter() {
1048        if !Provider::Profile::EXTENSIONS.contains(package) {
1049            return Err(DavCapabilityPlanError::ExtensionExceedsProfile { package }.into());
1050        }
1051    }
1052    if declaration.writes.partial_put != DavPartialPutCapability::Disabled
1053        && !Provider::Profile::PARTIAL_PUT
1054    {
1055        return Err(DavCapabilityPlanError::PartialPutExceedsProfile.into());
1056    }
1057    if declaration.writes.patch != DavPatchCapability::Disabled && !Provider::Profile::PATCH {
1058        return Err(DavCapabilityPlanError::PatchExceedsProfile.into());
1059    }
1060    if declaration.writes.private_update_range != DavPrivateUpdateRangeCapability::Disabled
1061        && !Provider::Profile::PRIVATE_UPDATE_RANGE
1062    {
1063        return Err(DavCapabilityPlanError::PrivateUpdateRangeExceedsProfile.into());
1064    }
1065    plan_capabilities(declaration).map_err(Into::into)
1066}
1067
1068fn validate_base_methods(
1069    declaration: &mut DavCapabilityDeclaration,
1070) -> Result<(), DavCapabilityPlanError> {
1071    if !declaration.methods.contains(DavMethod::Options) {
1072        return Err(DavCapabilityPlanError::OptionsMissing);
1073    }
1074    if declaration.methods.contains(DavMethod::Get) {
1075        declaration.methods = declaration.methods.with(DavMethod::Head);
1076    }
1077    if declaration.methods.contains(DavMethod::Head)
1078        && !declaration.methods.contains(DavMethod::Get)
1079    {
1080        return Err(DavCapabilityPlanError::HeadWithoutGet);
1081    }
1082    Ok(())
1083}
1084
1085fn validate_compliance(
1086    declaration: &DavCapabilityDeclaration,
1087) -> Result<(), DavCapabilityPlanError> {
1088    let locking_enabled = declaration.locking == DavLockingCapability::Class2;
1089    if locking_enabled && !declaration.compliance.class1 {
1090        return Err(DavCapabilityPlanError::Class2WithoutClass1);
1091    }
1092    if declaration.compliance.class3 && !declaration.compliance.class1 {
1093        return Err(DavCapabilityPlanError::Class3WithoutClass1);
1094    }
1095    let has_lock_method = declaration.methods.contains(DavMethod::Lock)
1096        || declaration.methods.contains(DavMethod::Unlock);
1097    let has_lock_methods = declaration.methods.contains(DavMethod::Lock)
1098        && declaration.methods.contains(DavMethod::Unlock);
1099    if (has_lock_method || locking_enabled) && !has_lock_methods {
1100        return Err(DavCapabilityPlanError::Class2WithoutLockMethods);
1101    }
1102    if has_lock_methods && !locking_enabled {
1103        return Err(DavCapabilityPlanError::LockMethodsWithoutClass2);
1104    }
1105    Ok(())
1106}
1107
1108fn validate_extensions(
1109    declaration: &DavCapabilityDeclaration,
1110) -> Result<(), DavCapabilityPlanError> {
1111    if !declaration.compliance.class1
1112        && let Some(package) = declaration.extensions.iter().next()
1113    {
1114        return Err(DavCapabilityPlanError::ExtensionWithoutClass1 { package });
1115    }
1116    for package in declaration.extensions.iter() {
1117        let descriptor = package.descriptor();
1118        for required in descriptor.prerequisites.iter() {
1119            if !declaration.extensions.contains(required) {
1120                return Err(DavCapabilityPlanError::ExtensionMissingPrerequisite {
1121                    package,
1122                    required,
1123                });
1124            }
1125        }
1126        if !descriptor.resources.contains(declaration.resource) {
1127            return Err(DavCapabilityPlanError::ExtensionNotApplicable {
1128                package,
1129                resource: declaration.resource,
1130            });
1131        }
1132    }
1133    let has_version_control = declaration
1134        .extensions
1135        .contains(DavExtensionPackage::VersionControl);
1136    if has_version_control && declaration.versioning.state == DavVersioningState::Unsupported {
1137        return Err(DavCapabilityPlanError::VersionControlWithoutTarget);
1138    }
1139    if !has_version_control && declaration.versioning.state != DavVersioningState::Unsupported {
1140        return Err(DavCapabilityPlanError::VersioningTargetWithoutPackage);
1141    }
1142    if declaration.versioning.auto_version != DavAutoVersion::None
1143        && !matches!(
1144            declaration.versioning.state,
1145            DavVersioningState::CheckedIn | DavVersioningState::CheckedOut
1146        )
1147    {
1148        return Err(DavCapabilityPlanError::AutoVersionNotApplicable);
1149    }
1150    if declaration.versioning.write_locked && declaration.locking != DavLockingCapability::Class2 {
1151        return Err(DavCapabilityPlanError::WriteLockWithoutClass2);
1152    }
1153    if declaration.versioning.auto_checkout_lock
1154        && (!declaration.versioning.write_locked
1155            || declaration.versioning.state != DavVersioningState::CheckedOut)
1156    {
1157        return Err(DavCapabilityPlanError::AutoCheckoutLockNotApplicable);
1158    }
1159    if declaration.versioning.auto_checkout_lock
1160        && !matches!(
1161            declaration.versioning.auto_version,
1162            DavAutoVersion::CheckoutUnlockedCheckin
1163                | DavAutoVersion::Checkout
1164                | DavAutoVersion::LockedCheckout
1165        )
1166    {
1167        return Err(DavCapabilityPlanError::AutoCheckoutLockWithoutApplicableMode);
1168    }
1169    if declaration.versioning.allow_version_delete
1170        && declaration.versioning.state != DavVersioningState::Version
1171    {
1172        return Err(DavCapabilityPlanError::VersionDeletePolicyNotApplicable);
1173    }
1174    let search_enabled = declaration.extensions.contains(DavExtensionPackage::Search);
1175    if search_enabled {
1176        if !declaration
1177            .search
1178            .grammars
1179            .contains(&DavSearchGrammar::BASICSEARCH)
1180        {
1181            return Err(DavCapabilityPlanError::SearchWithoutBasicSearch);
1182        }
1183        validate_search_grammars(declaration.search.grammars)?;
1184    } else if !declaration.search.grammars.is_empty() {
1185        return Err(DavCapabilityPlanError::SearchGrammarsWithoutPackage);
1186    }
1187    Ok(())
1188}
1189
1190fn validate_extension_methods(
1191    declaration: &DavCapabilityDeclaration,
1192) -> Result<(), DavCapabilityPlanError> {
1193    if declaration
1194        .extensions
1195        .contains(DavExtensionPackage::VersionControl)
1196        && declaration.methods.contains(DavMethod::VersionControl)
1197        && !matches!(
1198            declaration.versioning.state,
1199            DavVersioningState::Versionable
1200                | DavVersioningState::CheckedIn
1201                | DavVersioningState::CheckedOut
1202        )
1203    {
1204        return Err(DavCapabilityPlanError::VersionControlMethodNotApplicable);
1205    }
1206    let package_methods = extension_methods_for_declaration(declaration);
1207    for method in declaration.methods.iter() {
1208        if is_extension_only_method(method) && !package_methods.contains(method) {
1209            return Err(DavCapabilityPlanError::ExtensionMethodWithoutPackage { method });
1210        }
1211    }
1212    Ok(())
1213}
1214
1215const fn versioning_methods(methods: DavMethodSet, state: DavVersioningState) -> DavMethodSet {
1216    match state {
1217        DavVersioningState::Versionable
1218        | DavVersioningState::CheckedIn
1219        | DavVersioningState::CheckedOut => methods,
1220        DavVersioningState::Version => methods.without(DavMethod::VersionControl),
1221        DavVersioningState::Unsupported => methods
1222            .without(DavMethod::VersionControl)
1223            .without(DavMethod::Report),
1224    }
1225}
1226
1227fn extension_methods_for_declaration(declaration: &DavCapabilityDeclaration) -> DavMethodSet {
1228    let methods = extension_methods(declaration.extensions, declaration.resource);
1229    if declaration
1230        .extensions
1231        .contains(DavExtensionPackage::VersionControl)
1232    {
1233        versioning_methods(methods, declaration.versioning.state)
1234    } else {
1235        methods
1236    }
1237}
1238
1239const fn versioning_report(state: DavVersioningState, report: DavReportType) -> bool {
1240    match report {
1241        DavReportType::ExpandProperty => !matches!(state, DavVersioningState::Unsupported),
1242        DavReportType::VersionTree => matches!(
1243            state,
1244            DavVersioningState::CheckedIn
1245                | DavVersioningState::CheckedOut
1246                | DavVersioningState::Version
1247        ),
1248        _ => true,
1249    }
1250}
1251
1252const fn versioning_live_property(state: DavVersioningState, property: DavLiveProperty) -> bool {
1253    match property {
1254        DavLiveProperty::CheckedIn => matches!(state, DavVersioningState::CheckedIn),
1255        DavLiveProperty::CheckedOut => matches!(state, DavVersioningState::CheckedOut),
1256        DavLiveProperty::AutoVersion => matches!(
1257            state,
1258            DavVersioningState::CheckedIn | DavVersioningState::CheckedOut
1259        ),
1260        DavLiveProperty::PredecessorSet => matches!(
1261            state,
1262            DavVersioningState::CheckedOut | DavVersioningState::Version
1263        ),
1264        DavLiveProperty::SuccessorSet
1265        | DavLiveProperty::CheckoutSet
1266        | DavLiveProperty::VersionName => matches!(state, DavVersioningState::Version),
1267        _ => true,
1268    }
1269}
1270
1271const fn is_extension_only_method(method: DavMethod) -> bool {
1272    matches!(
1273        method,
1274        DavMethod::Acl
1275            | DavMethod::Report
1276            | DavMethod::VersionControl
1277            | DavMethod::Checkout
1278            | DavMethod::Checkin
1279            | DavMethod::Uncheckout
1280            | DavMethod::Mkworkspace
1281            | DavMethod::Update
1282            | DavMethod::Label
1283            | DavMethod::Merge
1284            | DavMethod::BaselineControl
1285            | DavMethod::Mkactivity
1286            | DavMethod::Search
1287            | DavMethod::Orderpatch
1288            | DavMethod::Mkredirectref
1289            | DavMethod::Updateredirectref
1290            | DavMethod::Bind
1291            | DavMethod::Unbind
1292            | DavMethod::Rebind
1293    )
1294}
1295
1296fn validate_writes(declaration: &DavCapabilityDeclaration) -> Result<(), DavCapabilityPlanError> {
1297    let put_enabled = declaration.methods.contains(DavMethod::Put);
1298    if declaration.writes.partial_put != DavPartialPutCapability::Disabled && !put_enabled {
1299        return Err(DavCapabilityPlanError::PartialPutWithoutPut);
1300    }
1301    if declaration.writes.private_update_range != DavPrivateUpdateRangeCapability::Disabled
1302        && !put_enabled
1303    {
1304        return Err(DavCapabilityPlanError::PrivateUpdateRangeWithoutPut);
1305    }
1306    let patch_enabled = declaration.methods.contains(DavMethod::Patch);
1307    match declaration.writes.patch {
1308        DavPatchCapability::Disabled if patch_enabled => {
1309            Err(DavCapabilityPlanError::PatchWithoutFormats)
1310        }
1311        DavPatchCapability::Disabled => Ok(()),
1312        DavPatchCapability::Formats(_) if !patch_enabled => {
1313            Err(DavCapabilityPlanError::PatchFormatsWithoutMethod)
1314        }
1315        DavPatchCapability::Formats([]) => Err(DavCapabilityPlanError::PatchWithoutFormats),
1316        DavPatchCapability::Formats(formats) => validate_patch_formats(formats),
1317    }
1318}
1319
1320fn render_dav_header(
1321    declaration: &DavCapabilityDeclaration,
1322) -> Result<Option<HeaderValue>, DavCapabilityPlanError> {
1323    let mut rendered = String::new();
1324    if declaration.compliance.class1 {
1325        push_token(&mut rendered, "1");
1326    }
1327    if declaration.locking == DavLockingCapability::Class2 {
1328        push_token(&mut rendered, "2");
1329    }
1330    if declaration.compliance.class3 {
1331        push_token(&mut rendered, "3");
1332    }
1333    for package in declaration.extensions.iter() {
1334        if let Some(token) = package.descriptor().dav_token {
1335            push_token(&mut rendered, token);
1336        }
1337    }
1338    if rendered.is_empty() {
1339        Ok(None)
1340    } else {
1341        header_value(&rendered).map(Some)
1342    }
1343}
1344
1345fn render_dasl_header(
1346    declaration: &DavCapabilityDeclaration,
1347) -> Result<Option<HeaderValue>, DavCapabilityPlanError> {
1348    if declaration.search.grammars.is_empty() {
1349        return Ok(None);
1350    }
1351    let mut rendered = String::new();
1352    for grammar in declaration.search.grammars {
1353        if !rendered.is_empty() {
1354            rendered.push_str(", ");
1355        }
1356        rendered.push('<');
1357        rendered.push_str(grammar.coded_url);
1358        rendered.push('>');
1359    }
1360    header_value(&rendered).map(Some)
1361}
1362
1363fn validate_search_grammars(grammars: &[DavSearchGrammar]) -> Result<(), DavCapabilityPlanError> {
1364    for (index, grammar) in grammars.iter().enumerate() {
1365        if grammar.coded_url.is_empty()
1366            || grammar.coded_url.bytes().any(|byte| {
1367                byte <= b' ' || byte == b'<' || byte == b'>' || byte == b',' || byte == 0x7f
1368            })
1369            || parse_absolute_url(grammar.coded_url, "SEARCH grammar coded-URL").is_err()
1370        {
1371            return Err(DavCapabilityPlanError::InvalidSearchGrammarCodedUrl {
1372                index,
1373                coded_url: grammar.coded_url,
1374            });
1375        }
1376        if !is_valid_xml_local_name(grammar.xml_local_name) {
1377            return Err(DavCapabilityPlanError::InvalidSearchGrammarXmlLocalName {
1378                index,
1379                xml_local_name: grammar.xml_local_name,
1380            });
1381        }
1382        if !grammar.xml_namespace.is_empty()
1383            && (grammar.xml_namespace.trim() != grammar.xml_namespace
1384                || parse_absolute_url(grammar.xml_namespace, "SEARCH grammar namespace").is_err())
1385        {
1386            return Err(DavCapabilityPlanError::InvalidSearchGrammarXmlNamespace {
1387                index,
1388                xml_namespace: grammar.xml_namespace,
1389            });
1390        }
1391        if let Some(previous_index) = grammars[..index].iter().position(|previous| {
1392            previous.coded_url == grammar.coded_url
1393                || (previous.xml_namespace == grammar.xml_namespace
1394                    && previous.xml_local_name == grammar.xml_local_name)
1395        }) {
1396            return Err(DavCapabilityPlanError::DuplicateSearchGrammar {
1397                index,
1398                previous_index,
1399                coded_url: grammar.coded_url,
1400                xml_namespace: grammar.xml_namespace,
1401                xml_local_name: grammar.xml_local_name,
1402            });
1403        }
1404    }
1405    Ok(())
1406}
1407
1408const LIVE_PROPERTY_SET_WORDS: usize = 2;
1409const LIVE_PROPERTY_SET_CAPACITY: usize = LIVE_PROPERTY_SET_WORDS * u64::BITS as usize;
1410const REPORT_SET_CAPACITY: usize = u16::BITS as usize;
1411
1412const _: () = assert!(DavLiveProperty::COUNT <= LIVE_PROPERTY_SET_CAPACITY);
1413const _: () = assert!(DavReportType::COUNT <= REPORT_SET_CAPACITY);
1414
1415#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1416struct DavLivePropertySet([u64; LIVE_PROPERTY_SET_WORDS]);
1417
1418impl DavLivePropertySet {
1419    const fn empty() -> Self {
1420        Self([0; LIVE_PROPERTY_SET_WORDS])
1421    }
1422
1423    fn insert(&mut self, property: DavLiveProperty) {
1424        let index = property.index();
1425        self.0[index / 64] |= 1u64 << (index % 64);
1426    }
1427
1428    const fn contains(self, property: DavLiveProperty) -> bool {
1429        let index = property.index();
1430        self.0[index / 64] & (1u64 << (index % 64)) != 0
1431    }
1432}
1433
1434#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1435struct DavReportSet(u16);
1436
1437impl DavReportSet {
1438    const fn empty() -> Self {
1439        Self(0)
1440    }
1441
1442    fn insert(&mut self, report: DavReportType) {
1443        self.0 |= 1u16 << report.index();
1444    }
1445
1446    const fn contains(self, report: DavReportType) -> bool {
1447        self.0 & (1u16 << report.index()) != 0
1448    }
1449}
1450
1451fn validate_patch_formats(formats: &[DavPatchFormat]) -> Result<(), DavCapabilityPlanError> {
1452    for (index, format) in formats.iter().enumerate() {
1453        let media_type = format
1454            .media_type
1455            .parse::<Mime>()
1456            .map_err(|_| DavCapabilityPlanError::InvalidPatchMediaType)?;
1457        for duplicate in &formats[..index] {
1458            let other = duplicate
1459                .media_type
1460                .parse::<Mime>()
1461                .map_err(|_| DavCapabilityPlanError::InvalidPatchMediaType)?;
1462            if media_type == other {
1463                return Err(DavCapabilityPlanError::DuplicatePatchMediaType);
1464            }
1465        }
1466    }
1467    Ok(())
1468}
1469
1470fn render_patch_formats(formats: &[DavPatchFormat]) -> String {
1471    let capacity = formats
1472        .iter()
1473        .map(|format| format.media_type.len())
1474        .sum::<usize>()
1475        + formats.len().saturating_sub(1) * 2;
1476    let mut rendered = String::with_capacity(capacity);
1477    for (index, format) in formats.iter().enumerate() {
1478        if index != 0 {
1479            rendered.push_str(", ");
1480        }
1481        rendered.push_str(format.media_type);
1482    }
1483    rendered
1484}
1485
1486fn header_value(value: &str) -> Result<HeaderValue, DavCapabilityPlanError> {
1487    HeaderValue::from_str(value).map_err(|_| DavCapabilityPlanError::InvalidHeaderRepresentation)
1488}
1489
1490fn push_token(rendered: &mut String, token: &str) {
1491    if !rendered.is_empty() {
1492        rendered.push_str(", ");
1493    }
1494    rendered.push_str(token);
1495}
1496
1497const fn extension_body_policy(kind: DavExtensionBodyKind, xml_limit: usize) -> DavBodyPolicy {
1498    match kind {
1499        DavExtensionBodyKind::Xml => DavBodyPolicy::BoundedXml { maximum: xml_limit },
1500        DavExtensionBodyKind::Stream => DavBodyPolicy::Stream,
1501    }
1502}