1use std::time::SystemTime;
4
5use aster_forge_utils::http_validators;
6use http::header::{ETAG, HeaderName, IF_MODIFIED_SINCE, IF_UNMODIFIED_SINCE, LAST_MODIFIED};
7use http::{HeaderMap, HeaderValue, StatusCode};
8
9use crate::{
10 DavBackendError, DavFileSystem, DavIfEvaluationError, DavIfStateResolver, DavLockSystem,
11 DavMethod, DavPath, DavProtocolError, IfHeader, enforce_if_header,
12 enforce_if_header_with_backends,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct DavConditionalResource<'a> {
18 pub exists: bool,
20 pub etag: Option<&'a str>,
22 pub last_modified: Option<SystemTime>,
24}
25
26impl DavConditionalResource<'_> {
27 #[must_use]
29 pub const fn missing() -> Self {
30 Self {
31 exists: false,
32 etag: None,
33 last_modified: None,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum DavConditionalOutcome {
41 Proceed,
43 NotModified,
45 PreconditionFailed,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum DavRangeEvaluation {
52 Evaluate,
54 Skip,
56}
57
58#[derive(Debug, Clone)]
60pub struct DavConditionalPlan {
61 pub outcome: DavConditionalOutcome,
63 pub range: DavRangeEvaluation,
65 validator_headers: HeaderMap,
66}
67
68impl DavConditionalPlan {
69 pub fn apply_response_headers(&self, status: StatusCode, headers: &mut HeaderMap) {
71 if !matches!(
72 status,
73 StatusCode::OK
74 | StatusCode::PARTIAL_CONTENT
75 | StatusCode::NOT_MODIFIED
76 | StatusCode::PRECONDITION_FAILED
77 | StatusCode::RANGE_NOT_SATISFIABLE
78 ) {
79 return;
80 }
81 for (name, value) in &self.validator_headers {
82 headers.insert(name.clone(), value.clone());
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
89pub enum DavConditionalPlanError {
90 #[error(transparent)]
92 Protocol(#[from] DavProtocolError),
93 #[error("invalid WebDAV conditional representation metadata")]
95 InvalidRepresentation,
96}
97
98#[derive(Debug, thiserror::Error)]
100pub enum DavConditionalEvaluationError {
101 #[error(transparent)]
103 Protocol(#[from] DavProtocolError),
104 #[error(transparent)]
106 Backend(#[from] DavBackendError),
107 #[error("invalid WebDAV conditional representation metadata")]
109 InvalidRepresentation,
110}
111
112pub fn plan_http_conditionals(
122 method: DavMethod,
123 headers: &HeaderMap,
124 resource: DavConditionalResource<'_>,
125) -> Result<DavConditionalPlan, DavConditionalPlanError> {
126 let validator_headers = validator_headers(resource);
127
128 let Ok(if_match) =
129 http_validators::if_match_headers_match(headers, resource.exists, resource.etag)
130 else {
131 if http_validators::if_match_headers_match(headers, resource.exists, None).is_err() {
132 return Err(DavProtocolError::bad_request("Invalid If-Match header").into());
133 }
134 return Err(DavConditionalPlanError::InvalidRepresentation);
135 };
136 if let Some(false) = if_match {
137 return Ok(plan(
138 DavConditionalOutcome::PreconditionFailed,
139 method,
140 validator_headers,
141 ));
142 }
143
144 if if_match.is_none()
145 && let Some(since) = optional_http_date(headers, IF_UNMODIFIED_SINCE)
146 && resource.exists
147 && let Some(last_modified) = resource.last_modified
148 {
149 http_validators::validate_http_date(last_modified)
150 .map_err(|_| DavConditionalPlanError::InvalidRepresentation)?;
151 if http_validators::http_date_epoch_seconds(last_modified)
152 > http_validators::http_date_epoch_seconds(since)
153 {
154 return Ok(plan(
155 DavConditionalOutcome::PreconditionFailed,
156 method,
157 validator_headers,
158 ));
159 }
160 }
161
162 let Ok(if_none_match) =
163 http_validators::if_none_match_headers_match(headers, resource.exists, resource.etag)
164 else {
165 if http_validators::if_none_match_headers_match(headers, resource.exists, None).is_err() {
166 return Err(DavProtocolError::bad_request("Invalid If-None-Match header").into());
167 }
168 return Err(DavConditionalPlanError::InvalidRepresentation);
169 };
170 if let Some(true) = if_none_match {
171 let outcome = if matches!(method, DavMethod::Get | DavMethod::Head) {
172 DavConditionalOutcome::NotModified
173 } else {
174 DavConditionalOutcome::PreconditionFailed
175 };
176 return Ok(plan(outcome, method, validator_headers));
177 }
178
179 if if_none_match.is_none()
180 && matches!(method, DavMethod::Get | DavMethod::Head)
181 && let Some(since) = optional_http_date(headers, IF_MODIFIED_SINCE)
182 && resource.exists
183 && let Some(last_modified) = resource.last_modified
184 {
185 http_validators::validate_http_date(last_modified)
186 .map_err(|_| DavConditionalPlanError::InvalidRepresentation)?;
187 if http_validators::http_date_epoch_seconds(last_modified)
188 <= http_validators::http_date_epoch_seconds(since)
189 {
190 return Ok(plan(
191 DavConditionalOutcome::NotModified,
192 method,
193 validator_headers,
194 ));
195 }
196 }
197
198 Ok(plan(
199 DavConditionalOutcome::Proceed,
200 method,
201 validator_headers,
202 ))
203}
204
205#[expect(
211 clippy::too_many_arguments,
212 reason = "The public conditional planner mirrors the RFC evaluation inputs explicitly."
213)]
214pub async fn plan_conditionals(
219 if_header: Option<&IfHeader>,
220 resolver: &dyn DavIfStateResolver,
221 request_path: &DavPath,
222 prefix: &str,
223 request_scheme: &str,
224 request_host: &str,
225 method: DavMethod,
226 headers: &HeaderMap,
227 resource: DavConditionalResource<'_>,
228) -> Result<DavConditionalPlan, DavConditionalEvaluationError> {
229 enforce_if_header(
230 if_header,
231 resolver,
232 request_path,
233 prefix,
234 request_scheme,
235 request_host,
236 )
237 .await
238 .map_err(map_if_error)?;
239 plan_http_conditionals(method, headers, resource).map_err(map_plan_error)
240}
241
242#[expect(
246 clippy::too_many_arguments,
247 reason = "The backend-aware planner keeps filesystem and lock ports explicit at the boundary."
248)]
249pub async fn plan_conditionals_with_backends(
254 if_header: Option<&IfHeader>,
255 filesystem: &dyn DavFileSystem,
256 lock_system: &dyn DavLockSystem,
257 request_path: &DavPath,
258 prefix: &str,
259 request_scheme: &str,
260 request_host: &str,
261 method: DavMethod,
262 headers: &HeaderMap,
263 resource: DavConditionalResource<'_>,
264) -> Result<DavConditionalPlan, DavConditionalEvaluationError> {
265 enforce_if_header_with_backends(
266 if_header,
267 filesystem,
268 lock_system,
269 request_path,
270 prefix,
271 request_scheme,
272 request_host,
273 )
274 .await
275 .map_err(map_if_error)?;
276 plan_http_conditionals(method, headers, resource).map_err(map_plan_error)
277}
278
279fn map_if_error(error: DavIfEvaluationError) -> DavConditionalEvaluationError {
280 match error {
281 DavIfEvaluationError::Protocol(error) => DavConditionalEvaluationError::Protocol(error),
282 DavIfEvaluationError::Backend(error) => DavConditionalEvaluationError::Backend(error),
283 }
284}
285
286fn map_plan_error(error: DavConditionalPlanError) -> DavConditionalEvaluationError {
287 match error {
288 DavConditionalPlanError::Protocol(error) => DavConditionalEvaluationError::Protocol(error),
289 DavConditionalPlanError::InvalidRepresentation => {
290 DavConditionalEvaluationError::InvalidRepresentation
291 }
292 }
293}
294
295fn plan(
296 outcome: DavConditionalOutcome,
297 method: DavMethod,
298 validator_headers: HeaderMap,
299) -> DavConditionalPlan {
300 let range = if outcome == DavConditionalOutcome::Proceed && method == DavMethod::Get {
301 DavRangeEvaluation::Evaluate
302 } else {
303 DavRangeEvaluation::Skip
304 };
305 DavConditionalPlan {
306 outcome,
307 range,
308 validator_headers,
309 }
310}
311
312fn validator_headers(resource: DavConditionalResource<'_>) -> HeaderMap {
313 let mut headers = HeaderMap::new();
314 if !resource.exists {
315 return headers;
316 }
317 if let Some(last_modified) = resource.last_modified
318 && let Ok(value) = http_validators::try_format_http_date(last_modified)
319 && let Ok(value) = HeaderValue::from_str(&value)
320 {
321 headers.insert(LAST_MODIFIED, value);
322 }
323 if let Some(etag) = resource.etag
324 && let Ok(value) = entity_tag_header_value(etag)
325 {
326 headers.insert(ETAG, value);
327 }
328 headers
329}
330
331fn entity_tag_header_value(etag: &str) -> Result<HeaderValue, DavConditionalPlanError> {
332 let rendered = http_validators::try_format_entity_tag(etag)
333 .map_err(|_| DavConditionalPlanError::InvalidRepresentation)?;
334 HeaderValue::from_str(&rendered).map_err(|_| DavConditionalPlanError::InvalidRepresentation)
335}
336
337fn optional_http_date(headers: &HeaderMap, name: HeaderName) -> Option<SystemTime> {
338 let mut values = headers.get_all(name).iter();
339 let value = values.next()?;
340 if values.next().is_some() {
341 return None;
342 }
343 let raw = value.to_str().ok()?;
344 http_validators::parse_http_date(raw).ok()
345}