aster_forge_webdav/
preference.rs

1//! RFC 8144 `WebDAV` preference selection from a validated capability snapshot.
2
3use http::HeaderValue;
4use http::header::HeaderMap;
5
6use crate::{DavCapabilitySnapshot, DavExtensionPackage, DavMethod, DavPreferenceSet, Depth};
7
8/// Applicable RFC 8144 preferences for one request.
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
10pub struct DavPreferencePlan {
11    pub return_minimal: bool,
12    pub return_representation: bool,
13    pub depth_no_root: bool,
14}
15
16impl DavPreferencePlan {
17    /// Canonical `Preference-Applied` value for the subset actually honored by the response.
18    ///
19    /// Request planning only determines eligibility. The response composer must pass a set that
20    /// reflects actual behavior; for example, a failed PROPPATCH must not claim that
21    /// `return=minimal` was applied, and `return=representation` is applied only when a current
22    /// representation is returned.
23    #[must_use]
24    pub fn preference_applied_header(&self, applied: DavPreferenceSet) -> Option<HeaderValue> {
25        applied_header(
26            self.return_minimal && applied.contains(DavPreferenceSet::RETURN_MINIMAL),
27            self.return_representation && applied.contains(DavPreferenceSet::RETURN_REPRESENTATION),
28            self.depth_no_root && applied.contains(DavPreferenceSet::DEPTH_NO_ROOT),
29        )
30    }
31}
32
33/// Selects supported and method-applicable RFC 8144 preferences.
34///
35/// Unknown preferences, parameters, and values are ignored as requested by the generic Prefer
36/// framework. Parsing and canonical response-header selection do not allocate.
37#[must_use]
38pub fn plan_preferences(
39    snapshot: &DavCapabilitySnapshot,
40    headers: &HeaderMap,
41    method: DavMethod,
42    depth: Option<Depth>,
43) -> DavPreferencePlan {
44    if !snapshot.supports_extension(DavExtensionPackage::Prefer) || !snapshot.allows(method) {
45        return DavPreferencePlan::default();
46    }
47
48    let requested = requested_preferences(headers);
49    let return_minimal =
50        requested.contains(DavPreferenceSet::RETURN_MINIMAL) && minimal_applies(snapshot, method);
51    let return_representation = requested.contains(DavPreferenceSet::RETURN_REPRESENTATION)
52        && matches!(
53            method,
54            DavMethod::Put | DavMethod::Copy | DavMethod::Move | DavMethod::Patch | DavMethod::Post
55        );
56    let depth_no_root = requested.contains(DavPreferenceSet::DEPTH_NO_ROOT)
57        && method_supports_depth(method)
58        && matches!(depth, Some(Depth::One | Depth::Infinity));
59
60    DavPreferencePlan {
61        return_minimal,
62        return_representation,
63        depth_no_root,
64    }
65}
66
67fn requested_preferences(headers: &HeaderMap) -> DavPreferenceSet {
68    let mut requested = DavPreferenceSet::empty();
69    for value in headers.get_all("Prefer") {
70        let Ok(value) = value.to_str() else {
71            continue;
72        };
73        let mut start = 0;
74        let mut quoted = false;
75        let mut escaped = false;
76        for (index, byte) in value.bytes().enumerate() {
77            if escaped {
78                escaped = false;
79            } else if quoted && byte == b'\\' {
80                escaped = true;
81            } else if byte == b'"' {
82                quoted = !quoted;
83            } else if byte == b',' && !quoted {
84                add_requested_preference(&value[start..index], &mut requested);
85                start = index + 1;
86            }
87        }
88        add_requested_preference(&value[start..], &mut requested);
89    }
90    requested
91}
92
93fn add_requested_preference(value: &str, requested: &mut DavPreferenceSet) {
94    let mut quoted = false;
95    let mut escaped = false;
96    let mut end = value.len();
97    for (index, byte) in value.bytes().enumerate() {
98        if escaped {
99            escaped = false;
100        } else if quoted && byte == b'\\' {
101            escaped = true;
102        } else if byte == b'"' {
103            quoted = !quoted;
104        } else if byte == b';' && !quoted {
105            end = index;
106            break;
107        }
108    }
109    let preference = value[..end].trim();
110    if preference.eq_ignore_ascii_case("return=minimal") {
111        *requested = requested.union(DavPreferenceSet::RETURN_MINIMAL);
112    } else if preference.eq_ignore_ascii_case("return=representation") {
113        *requested = requested.union(DavPreferenceSet::RETURN_REPRESENTATION);
114    } else if preference.eq_ignore_ascii_case("depth-noroot") {
115        *requested = requested.union(DavPreferenceSet::DEPTH_NO_ROOT);
116    }
117}
118
119fn minimal_applies(snapshot: &DavCapabilitySnapshot, method: DavMethod) -> bool {
120    matches!(
121        method,
122        DavMethod::Propfind | DavMethod::Proppatch | DavMethod::Report
123    ) || (method == DavMethod::Mkcol
124        && snapshot.supports_extension(DavExtensionPackage::ExtendedMkcol))
125}
126
127const fn method_supports_depth(method: DavMethod) -> bool {
128    matches!(
129        method,
130        DavMethod::Propfind
131            | DavMethod::Delete
132            | DavMethod::Copy
133            | DavMethod::Move
134            | DavMethod::Lock
135            | DavMethod::Report
136    )
137}
138
139fn applied_header(
140    return_minimal: bool,
141    return_representation: bool,
142    depth_no_root: bool,
143) -> Option<HeaderValue> {
144    let value = match (return_minimal, return_representation, depth_no_root) {
145        (false, false, false) => return None,
146        (true, false, false) => "return=minimal",
147        (false, true, false) => "return=representation",
148        (false, false, true) => "depth-noroot",
149        (true, true, false) => "return=minimal, return=representation",
150        (true, false, true) => "return=minimal, depth-noroot",
151        (false, true, true) => "return=representation, depth-noroot",
152        (true, true, true) => "return=minimal, return=representation, depth-noroot",
153    };
154    Some(HeaderValue::from_static(value))
155}