aster_forge_webdav/
preference.rs1use http::HeaderValue;
4use http::header::HeaderMap;
5
6use crate::{DavCapabilitySnapshot, DavExtensionPackage, DavMethod, DavPreferenceSet, Depth};
7
8#[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 #[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#[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}