1use std::time::SystemTime;
4
5use headers::{ContentLength, ContentRange, Header};
6use http::header::{
7 CACHE_CONTROL, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, IF_MATCH, IF_NONE_MATCH,
8};
9use http::{HeaderMap, HeaderValue, StatusCode};
10
11use crate::patch::{DavWritePreconditionError, enforce_write_precondition};
12use crate::response::no_store_empty_response;
13use crate::{
14 DavCapabilitySnapshot, DavConditionalOutcome, DavConditionalPlan, DavConditionalPlanError,
15 DavConditionalResource, DavMethod, DavPartialPutCapability, DavPath,
16 DavPrivateUpdateRangeCapability, DavProtocolError, DavResponse, DavVersioningPrecondition,
17 DavWritePrecondition, href_for_dav_path, plan_http_conditionals, plan_versioning_method,
18 versioning_precondition_response,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DavPutResourceState<'a> {
24 Missing,
25 File {
26 etag: Option<&'a str>,
27 last_modified: Option<SystemTime>,
28 },
29 Collection,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct DavPartialPutPlan {
35 pub offset: u64,
36 pub length: u64,
37 pub complete_length: Option<u64>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum DavPutWritePlan {
43 Replace,
45 Partial(DavPartialPutPlan),
47 PrivateUpdateRange { value: HeaderValue },
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct DavPutPlan {
54 pub resource_existed: bool,
55 pub create: bool,
56 pub create_new: bool,
57 pub content_length_hint: Option<u64>,
58 pub write: DavPutWritePlan,
59}
60
61#[derive(Debug, thiserror::Error)]
63pub enum DavPutPlanError {
64 #[error("PUT is not allowed for this resource")]
65 MethodNotAllowed,
66 #[error(transparent)]
67 Protocol(#[from] DavProtocolError),
68 #[error("PUT request precondition failed")]
69 PreconditionFailed(DavConditionalPlan),
70 #[error("invalid PUT representation metadata")]
71 InvalidRepresentation,
72 #[error("partial PUT requires If-Match with a strong entity-tag")]
73 PreconditionRequired,
74 #[error("partial PUT requires an existing representation")]
75 PartialTargetMissing,
76 #[error("PUT cannot replace a collection")]
77 CollectionTarget,
78 #[error(transparent)]
79 Versioning(#[from] DavVersioningPrecondition),
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
84#[error("invalid PUT Content-Location response header")]
85pub struct DavPutResponseError;
86
87pub fn plan_put_request(
93 snapshot: &DavCapabilitySnapshot,
94 headers: &HeaderMap,
95 state: DavPutResourceState<'_>,
96) -> Result<DavPutPlan, DavPutPlanError> {
97 if !snapshot.allows(DavMethod::Put) {
98 return Err(DavPutPlanError::MethodNotAllowed);
99 }
100 plan_versioning_method(snapshot, DavMethod::Put)?;
101 let resource = match state {
102 DavPutResourceState::Missing => DavConditionalResource::missing(),
103 DavPutResourceState::File {
104 etag,
105 last_modified,
106 } => DavConditionalResource {
107 exists: true,
108 etag,
109 last_modified,
110 },
111 DavPutResourceState::Collection => return Err(DavPutPlanError::CollectionTarget),
112 };
113 let (write, precondition) = plan_put_write(snapshot, headers)?;
114 if matches!(write, DavPutWritePlan::Partial(_)) && !resource.exists {
115 return Err(DavPutPlanError::PartialTargetMissing);
116 }
117 enforce_write_precondition(precondition, headers).map_err(|error| match error {
118 DavWritePreconditionError::Required => DavPutPlanError::PreconditionRequired,
119 DavWritePreconditionError::Protocol(error) => DavPutPlanError::Protocol(error),
120 })?;
121 let conditional =
122 plan_http_conditionals(DavMethod::Put, headers, resource).map_err(|error| match error {
123 DavConditionalPlanError::Protocol(error) => DavPutPlanError::Protocol(error),
124 DavConditionalPlanError::InvalidRepresentation => {
125 DavPutPlanError::InvalidRepresentation
126 }
127 })?;
128 if conditional.outcome != DavConditionalOutcome::Proceed {
129 return Err(DavPutPlanError::PreconditionFailed(conditional));
130 }
131 let content_length_hint = match &write {
132 DavPutWritePlan::Partial(partial) => Some(partial.length),
133 DavPutWritePlan::Replace | DavPutWritePlan::PrivateUpdateRange { .. } => {
134 content_length_hint(headers)
135 }
136 };
137 Ok(DavPutPlan {
138 resource_existed: resource.exists,
139 create: !header_equals(headers, IF_MATCH, "*"),
140 create_new: header_equals(headers, IF_NONE_MATCH, "*"),
141 content_length_hint,
142 write,
143 })
144}
145
146#[must_use]
148pub fn put_plan_error_response(
149 snapshot: &DavCapabilitySnapshot,
150 error: &DavPutPlanError,
151) -> DavResponse {
152 match error {
153 DavPutPlanError::MethodNotAllowed | DavPutPlanError::CollectionTarget => {
154 crate::method_not_allowed_response(snapshot)
155 }
156 DavPutPlanError::Protocol(error) => crate::protocol_error_response(error),
157 DavPutPlanError::PreconditionFailed(plan) => {
158 let mut response = DavResponse::empty(StatusCode::PRECONDITION_FAILED);
159 response
160 .headers
161 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
162 plan.apply_response_headers(response.status, &mut response.headers);
163 response
164 }
165 DavPutPlanError::InvalidRepresentation => {
166 no_store_empty_response(StatusCode::INTERNAL_SERVER_ERROR)
167 }
168 DavPutPlanError::PreconditionRequired => {
169 no_store_empty_response(StatusCode::PRECONDITION_REQUIRED)
170 }
171 DavPutPlanError::PartialTargetMissing => no_store_empty_response(StatusCode::CONFLICT),
172 DavPutPlanError::Versioning(error) => versioning_precondition_response(snapshot, *error)
173 .unwrap_or_else(|_| no_store_empty_response(StatusCode::INTERNAL_SERVER_ERROR)),
174 }
175}
176
177fn plan_put_write(
178 snapshot: &DavCapabilitySnapshot,
179 headers: &HeaderMap,
180) -> Result<(DavPutWritePlan, DavWritePrecondition), DavPutPlanError> {
181 let has_content_range = headers.contains_key(CONTENT_RANGE);
182 let has_private_range = headers.contains_key("X-Update-Range");
183 if has_content_range && has_private_range {
184 return Err(DavProtocolError::bad_request(
185 "Content-Range and X-Update-Range cannot be combined",
186 )
187 .into());
188 }
189 if has_content_range {
190 let (plan, precondition) = plan_partial_put(snapshot, headers)?;
191 return Ok((DavPutWritePlan::Partial(plan), precondition));
192 }
193 if has_private_range {
194 return plan_private_update_range(snapshot, headers);
195 }
196 Ok((DavPutWritePlan::Replace, DavWritePrecondition::Optional))
197}
198
199fn plan_partial_put(
200 snapshot: &DavCapabilitySnapshot,
201 headers: &HeaderMap,
202) -> Result<(DavPartialPutPlan, DavWritePrecondition), DavPutPlanError> {
203 let precondition = match snapshot.writes().partial_put {
204 DavPartialPutCapability::Disabled => {
205 return Err(DavProtocolError::bad_request("Partial PUT is not supported").into());
206 }
207 DavPartialPutCapability::ContentRangeBytes { precondition } => precondition,
208 };
209 let mut values = headers.get_all(CONTENT_RANGE).iter();
210 let value = values
211 .next()
212 .ok_or_else(|| DavProtocolError::bad_request("Invalid Content-Range header"))?;
213 if values.next().is_some() {
214 return Err(DavProtocolError::bad_request("Invalid Content-Range header").into());
215 }
216 let content_range = ContentRange::decode(&mut std::iter::once(value))
217 .map_err(|_| DavProtocolError::bad_request("Invalid Content-Range header"))?;
218 let (first, last) = content_range
219 .bytes_range()
220 .ok_or_else(|| DavProtocolError::bad_request("Invalid Content-Range header"))?;
221 if content_range
222 .bytes_len()
223 .is_some_and(|complete_length| complete_length <= last)
224 {
225 return Err(DavProtocolError::bad_request("Invalid Content-Range header").into());
226 }
227 let length = last
228 .checked_sub(first)
229 .and_then(|length| length.checked_add(1))
230 .ok_or_else(|| DavProtocolError::bad_request("Invalid Content-Range header"))?;
231 if let Some(content_length) = request_content_length(headers)?
232 && content_length != length
233 {
234 return Err(
235 DavProtocolError::bad_request("Content-Length does not match Content-Range").into(),
236 );
237 }
238 Ok((
239 DavPartialPutPlan {
240 offset: first,
241 length,
242 complete_length: content_range.bytes_len(),
243 },
244 precondition,
245 ))
246}
247
248fn plan_private_update_range(
249 snapshot: &DavCapabilitySnapshot,
250 headers: &HeaderMap,
251) -> Result<(DavPutWritePlan, DavWritePrecondition), DavPutPlanError> {
252 let precondition = match snapshot.writes().private_update_range {
253 DavPrivateUpdateRangeCapability::Disabled => {
254 return Err(DavProtocolError::bad_request("X-Update-Range is not supported").into());
255 }
256 DavPrivateUpdateRangeCapability::XUpdateRange { precondition } => precondition,
257 };
258 let mut values = headers.get_all("X-Update-Range").iter();
259 let value = values
260 .next()
261 .ok_or_else(|| DavProtocolError::bad_request("Invalid X-Update-Range header"))?;
262 if values.next().is_some() || value.to_str().map_or(true, |value| value.trim().is_empty()) {
263 return Err(DavProtocolError::bad_request("Invalid X-Update-Range header").into());
264 }
265 Ok((
266 DavPutWritePlan::PrivateUpdateRange {
267 value: value.clone(),
268 },
269 precondition,
270 ))
271}
272
273fn request_content_length(headers: &HeaderMap) -> Result<Option<u64>, DavPutPlanError> {
274 if !headers.contains_key(CONTENT_LENGTH) {
275 return Ok(None);
276 }
277 let content_length = ContentLength::decode(&mut headers.get_all(CONTENT_LENGTH).iter())
278 .map_err(|_| DavProtocolError::bad_request("Invalid Content-Length header"))?;
279 Ok(Some(content_length.0))
280}
281
282pub fn put_success_response(
288 plan: &DavPutPlan,
289 prefix: &str,
290 path: &DavPath,
291) -> Result<DavResponse, DavPutResponseError> {
292 if plan.resource_existed {
293 return Ok(DavResponse::empty(StatusCode::NO_CONTENT));
294 }
295 let mut response = DavResponse::empty(StatusCode::CREATED);
296 let location =
297 HeaderValue::from_str(&href_for_dav_path(prefix, path)).map_err(|_| DavPutResponseError)?;
298 response.headers.insert(CONTENT_LOCATION, location);
299 Ok(response)
300}
301
302fn content_length_hint(headers: &HeaderMap) -> Option<u64> {
303 headers
304 .get("X-Expected-Entity-Length")
305 .and_then(|value| value.to_str().ok())
306 .and_then(|value| value.trim().parse::<u64>().ok())
307 .or_else(|| {
308 headers
309 .get(http::header::CONTENT_LENGTH)
310 .and_then(|value| value.to_str().ok())
311 .and_then(|value| value.trim().parse::<u64>().ok())
312 })
313}
314
315fn header_equals(headers: &HeaderMap, name: http::header::HeaderName, expected: &str) -> bool {
316 headers
317 .get(name)
318 .and_then(|value| value.to_str().ok())
319 .is_some_and(|value| value.trim() == expected)
320}