1use http::header::{CACHE_CONTROL, CONTENT_LOCATION, CONTENT_TYPE};
4use http::{HeaderValue, StatusCode};
5
6use crate::response::no_store_empty_response;
7use crate::{
8 DavBackendError, DavErrorCondition, DavFileSystem, DavMultiStatusError, DavMultiStatusItem,
9 DavMultiStatusLimits, DavPath, DavResourceKind, DavResponse, Depth, FsError,
10 dav_multistatus_bytes, href_for_dav_path,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DavCopyMoveMethod {
16 Copy,
17 Move,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct DavCopyMovePlan {
23 pub recursive_collection: bool,
24 pub destination_deep: bool,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
29pub enum DavMutationPlanError {
30 #[error("invalid mutation Depth")]
31 BadRequest,
32 #[error("resource mutation is not supported for this target")]
33 MethodNotAllowed,
34 #[error("resource mutation conflicts with the current hierarchy")]
35 Conflict,
36 #[error("forbidden mutation path relation")]
37 Forbidden,
38 #[error("destination exists while Overwrite is disabled")]
39 PreconditionFailed,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
44pub enum DavParentCollectionError {
45 #[error("resource mutation is not supported for this target")]
47 MethodNotAllowed,
48 #[error("resource mutation conflicts with the current hierarchy")]
50 Conflict,
51 #[error(transparent)]
53 Backend(#[from] DavBackendError),
54}
55
56pub fn validate_collection_create_target(path: &str) -> Result<(), DavMutationPlanError> {
62 if resource_identity_path(path) == "/" {
63 Err(DavMutationPlanError::MethodNotAllowed)
64 } else {
65 Ok(())
66 }
67}
68
69pub async fn enforce_parent_collection(
78 filesystem: &dyn DavFileSystem,
79 target: &DavPath,
80) -> Result<(), DavParentCollectionError> {
81 let Some(parent) = target.parent() else {
82 return Err(DavParentCollectionError::MethodNotAllowed);
83 };
84 if parent == DavPath::root() {
85 return Ok(());
86 }
87 match filesystem.metadata(&parent).await {
88 Ok(metadata) if metadata.is_dir() => Ok(()),
89 Ok(_) | Err(FsError::NotFound) => Err(DavParentCollectionError::Conflict),
90 Err(error) => Err(DavParentCollectionError::Backend(error.into())),
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
96#[error("invalid mutation Content-Location response header")]
97pub struct DavMutationResponseError;
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DavMutationFailure {
102 path: DavPath,
103 status: u16,
104 lock_path: Option<DavPath>,
105}
106
107impl DavMutationFailure {
108 #[must_use]
110 pub fn locked(path: DavPath, lock_path: DavPath) -> Self {
111 Self {
112 path,
113 status: StatusCode::LOCKED.as_u16(),
114 lock_path: Some(lock_path),
115 }
116 }
117
118 #[must_use]
120 pub fn status(path: DavPath, status: u16) -> Self {
121 Self {
122 path,
123 status,
124 lock_path: None,
125 }
126 }
127
128 #[must_use]
130 pub fn path(&self) -> &DavPath {
131 &self.path
132 }
133
134 #[must_use]
136 pub const fn status_code(&self) -> u16 {
137 self.status
138 }
139
140 #[must_use]
142 pub fn lock_path(&self) -> Option<&DavPath> {
143 self.lock_path.as_ref()
144 }
145
146 #[must_use]
148 pub fn to_multistatus_item(&self, prefix: &str) -> DavMultiStatusItem {
149 let item = DavMultiStatusItem::status(href_for_dav_path(prefix, &self.path), self.status);
150 if self.status == StatusCode::LOCKED.as_u16() {
151 let lock_path = self.lock_path.as_ref().unwrap_or(&self.path);
152 item.with_error(DavErrorCondition::LockTokenSubmitted {
153 href: href_for_dav_path(prefix, lock_path),
154 })
155 } else {
156 item
157 }
158 }
159}
160
161pub fn validate_delete_target(
167 kind: DavResourceKind,
168 depth: Depth,
169) -> Result<(), DavMutationPlanError> {
170 if kind == DavResourceKind::Collection && !depth.is_infinity() {
171 Err(DavMutationPlanError::BadRequest)
172 } else {
173 Ok(())
174 }
175}
176
177pub fn plan_copy_move_request(
183 method: DavCopyMoveMethod,
184 depth: Depth,
185 source_kind: DavResourceKind,
186 destination_kind: Option<DavResourceKind>,
187 source_path: &str,
188 destination_path: &str,
189 overwrite: bool,
190) -> Result<DavCopyMovePlan, DavMutationPlanError> {
191 if same_resource_path(source_path, destination_path) {
192 return Err(DavMutationPlanError::Forbidden);
193 }
194 if source_kind == DavResourceKind::Collection {
195 match method {
196 DavCopyMoveMethod::Move if !depth.is_infinity() => {
197 return Err(DavMutationPlanError::BadRequest);
198 }
199 DavCopyMoveMethod::Copy if depth == Depth::One => {
200 return Err(DavMutationPlanError::BadRequest);
201 }
202 DavCopyMoveMethod::Copy | DavCopyMoveMethod::Move => {}
203 }
204 }
205 let recursive_collection = source_kind == DavResourceKind::Collection
206 && (method == DavCopyMoveMethod::Move || depth != Depth::Zero);
207 if recursive_collection && is_descendant_path(source_path, destination_path) {
208 return Err(DavMutationPlanError::Forbidden);
209 }
210 if !overwrite && destination_kind.is_some() {
211 return Err(DavMutationPlanError::PreconditionFailed);
212 }
213 let destination_deep = destination_kind == Some(DavResourceKind::Collection)
214 || source_kind == DavResourceKind::Collection
215 && (method == DavCopyMoveMethod::Move || depth != Depth::Zero);
216 Ok(DavCopyMovePlan {
217 recursive_collection,
218 destination_deep,
219 })
220}
221
222#[must_use]
224pub fn mutation_plan_error_response(error: DavMutationPlanError) -> DavResponse {
225 let status = match error {
226 DavMutationPlanError::BadRequest => StatusCode::BAD_REQUEST,
227 DavMutationPlanError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
228 DavMutationPlanError::Conflict => StatusCode::CONFLICT,
229 DavMutationPlanError::Forbidden => StatusCode::FORBIDDEN,
230 DavMutationPlanError::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
231 };
232 no_store_empty_response(status)
233}
234
235pub fn collection_created_response(
241 prefix: &str,
242 path: &DavPath,
243) -> Result<DavResponse, DavMutationResponseError> {
244 let mut response = DavResponse::empty(StatusCode::CREATED);
245 let location = HeaderValue::from_str(&href_for_dav_path(prefix, path))
246 .map_err(|_| DavMutationResponseError)?;
247 response.headers.insert(CONTENT_LOCATION, location);
248 Ok(response)
249}
250
251#[must_use]
253pub fn delete_success_response() -> DavResponse {
254 DavResponse::empty(StatusCode::NO_CONTENT)
255}
256
257#[must_use]
259pub fn same_resource_path(left: &str, right: &str) -> bool {
260 resource_identity_path(left) == resource_identity_path(right)
261}
262
263#[must_use]
265pub fn is_descendant_path(parent: &str, child: &str) -> bool {
266 let parent = resource_identity_path(parent);
267 let child = resource_identity_path(child);
268 if parent == "/" || parent == child {
269 return false;
270 }
271 child.starts_with(&format!("{parent}/"))
272}
273
274#[must_use]
279pub fn replace_relative_prefix(
280 path: &str,
281 source_prefix: &str,
282 destination_prefix: &str,
283) -> String {
284 let source_prefix = source_prefix.trim_end_matches('/');
285 let destination_prefix = destination_prefix.trim_end_matches('/');
286 let suffix = path
287 .strip_prefix(source_prefix)
288 .filter(|suffix| suffix.is_empty() || suffix.starts_with('/'))
289 .unwrap_or(path);
290 if suffix.is_empty() {
291 format!("{destination_prefix}/")
292 } else {
293 format!("{destination_prefix}{suffix}")
294 }
295}
296
297#[must_use]
299pub fn mutation_success_response(destination_existed: bool) -> DavResponse {
300 let status = if destination_existed {
301 StatusCode::NO_CONTENT
302 } else {
303 StatusCode::CREATED
304 };
305 no_store_empty_response(status)
306}
307
308pub fn mutation_multistatus_response(
314 prefix: &str,
315 failures: &[DavMutationFailure],
316) -> Result<DavResponse, DavMultiStatusError> {
317 mutation_multistatus_response_with_limits(prefix, failures, DavMultiStatusLimits::default())
318}
319
320pub fn mutation_multistatus_response_with_limits(
326 prefix: &str,
327 failures: &[DavMutationFailure],
328 limits: DavMultiStatusLimits,
329) -> Result<DavResponse, DavMultiStatusError> {
330 let items = failures
331 .iter()
332 .map(|failure| failure.to_multistatus_item(prefix));
333 let body = dav_multistatus_bytes(items, limits)?;
334 let mut response = DavResponse::bytes(StatusCode::MULTI_STATUS, body);
335 response.headers.insert(
336 CONTENT_TYPE,
337 HeaderValue::from_static("application/xml; charset=utf-8"),
338 );
339 response
340 .headers
341 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
342 Ok(response)
343}
344
345#[must_use]
347pub fn resource_identity_path(path: &str) -> String {
348 let trimmed = path.trim_end_matches('/');
349 if trimmed.is_empty() {
350 "/".to_string()
351 } else {
352 trimmed.to_string()
353 }
354}