1use http::header::{CACHE_CONTROL, CONTENT_LOCATION, CONTENT_TYPE};
4use http::{HeaderValue, StatusCode};
5
6use crate::response::no_store_empty_response;
7use crate::{
8 DavErrorCondition, DavFileSystem, DavMultiStatusItem, DavPath, DavResourceKind, DavResponse,
9 DavXmlError, Depth, FsError, backend_error_response, dav_multistatus_element,
10 href_for_dav_path, parent_relative_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
42pub fn validate_collection_create_target(path: &str) -> Result<(), DavMutationPlanError> {
44 if resource_identity_path(path) == "/" {
45 Err(DavMutationPlanError::MethodNotAllowed)
46 } else {
47 Ok(())
48 }
49}
50
51pub async fn enforce_parent_collection(
56 filesystem: &dyn DavFileSystem,
57 target: &DavPath,
58) -> Result<(), DavResponse> {
59 let Some(parent) = parent_relative_path(target.as_str()) else {
60 return Err(mutation_plan_error_response(
61 DavMutationPlanError::MethodNotAllowed,
62 ));
63 };
64 if parent == "/" {
65 return Ok(());
66 }
67 let parent = match DavPath::new(&parent) {
68 Ok(parent) => parent,
69 Err(_) => {
70 return Err(mutation_plan_error_response(
71 DavMutationPlanError::BadRequest,
72 ));
73 }
74 };
75 match filesystem.metadata(&parent).await {
76 Ok(metadata) if metadata.is_dir() => Ok(()),
77 Ok(_) | Err(FsError::NotFound) => {
78 Err(mutation_plan_error_response(DavMutationPlanError::Conflict))
79 }
80 Err(error) => Err(backend_error_response(&error.into())),
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86#[error("invalid mutation Content-Location response header")]
87pub struct DavMutationResponseError;
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct DavMutationFailure {
92 path: DavPath,
93 status: u16,
94 lock_path: Option<DavPath>,
95}
96
97impl DavMutationFailure {
98 #[must_use]
100 pub fn locked(path: DavPath, lock_path: DavPath) -> Self {
101 Self {
102 path,
103 status: StatusCode::LOCKED.as_u16(),
104 lock_path: Some(lock_path),
105 }
106 }
107
108 #[must_use]
110 pub fn status(path: DavPath, status: u16) -> Self {
111 Self {
112 path,
113 status,
114 lock_path: None,
115 }
116 }
117}
118
119pub fn validate_delete_target(
121 kind: DavResourceKind,
122 depth: Depth,
123) -> Result<(), DavMutationPlanError> {
124 if kind == DavResourceKind::Collection && !depth.is_infinity() {
125 Err(DavMutationPlanError::BadRequest)
126 } else {
127 Ok(())
128 }
129}
130
131pub fn plan_copy_move_request(
133 method: DavCopyMoveMethod,
134 depth: Depth,
135 source_kind: DavResourceKind,
136 destination_kind: Option<DavResourceKind>,
137 source_path: &str,
138 destination_path: &str,
139 overwrite: bool,
140) -> Result<DavCopyMovePlan, DavMutationPlanError> {
141 if same_resource_path(source_path, destination_path) {
142 return Err(DavMutationPlanError::Forbidden);
143 }
144 if source_kind == DavResourceKind::Collection {
145 match method {
146 DavCopyMoveMethod::Move if !depth.is_infinity() => {
147 return Err(DavMutationPlanError::BadRequest);
148 }
149 DavCopyMoveMethod::Copy if depth == Depth::One => {
150 return Err(DavMutationPlanError::BadRequest);
151 }
152 DavCopyMoveMethod::Copy | DavCopyMoveMethod::Move => {}
153 }
154 }
155 let recursive_collection = source_kind == DavResourceKind::Collection
156 && (method == DavCopyMoveMethod::Move || depth != Depth::Zero);
157 if recursive_collection && is_descendant_path(source_path, destination_path) {
158 return Err(DavMutationPlanError::Forbidden);
159 }
160 if !overwrite && destination_kind.is_some() {
161 return Err(DavMutationPlanError::PreconditionFailed);
162 }
163 let destination_deep = destination_kind == Some(DavResourceKind::Collection)
164 || source_kind == DavResourceKind::Collection
165 && (method == DavCopyMoveMethod::Move || depth != Depth::Zero);
166 Ok(DavCopyMovePlan {
167 recursive_collection,
168 destination_deep,
169 })
170}
171
172#[must_use]
174pub fn mutation_plan_error_response(error: DavMutationPlanError) -> DavResponse {
175 let status = match error {
176 DavMutationPlanError::BadRequest => StatusCode::BAD_REQUEST,
177 DavMutationPlanError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
178 DavMutationPlanError::Conflict => StatusCode::CONFLICT,
179 DavMutationPlanError::Forbidden => StatusCode::FORBIDDEN,
180 DavMutationPlanError::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
181 };
182 no_store_empty_response(status)
183}
184
185pub fn collection_created_response(
187 prefix: &str,
188 path: &DavPath,
189) -> Result<DavResponse, DavMutationResponseError> {
190 let mut response = DavResponse::empty(StatusCode::CREATED);
191 let location = HeaderValue::from_str(&href_for_dav_path(prefix, path))
192 .map_err(|_| DavMutationResponseError)?;
193 response.headers.insert(CONTENT_LOCATION, location);
194 Ok(response)
195}
196
197#[must_use]
199pub fn delete_success_response() -> DavResponse {
200 DavResponse::empty(StatusCode::NO_CONTENT)
201}
202
203#[must_use]
205pub fn same_resource_path(left: &str, right: &str) -> bool {
206 resource_identity_path(left) == resource_identity_path(right)
207}
208
209#[must_use]
211pub fn is_descendant_path(parent: &str, child: &str) -> bool {
212 let parent = resource_identity_path(parent);
213 let child = resource_identity_path(child);
214 if parent == "/" || parent == child {
215 return false;
216 }
217 child.starts_with(&format!("{parent}/"))
218}
219
220#[must_use]
225pub fn replace_relative_prefix(
226 path: &str,
227 source_prefix: &str,
228 destination_prefix: &str,
229) -> String {
230 let source_prefix = source_prefix.trim_end_matches('/');
231 let destination_prefix = destination_prefix.trim_end_matches('/');
232 let suffix = path
233 .strip_prefix(source_prefix)
234 .filter(|suffix| suffix.is_empty() || suffix.starts_with('/'))
235 .unwrap_or(path);
236 if suffix.is_empty() {
237 format!("{destination_prefix}/")
238 } else {
239 format!("{destination_prefix}{suffix}")
240 }
241}
242
243#[must_use]
245pub fn mutation_success_response(destination_existed: bool) -> DavResponse {
246 let status = if destination_existed {
247 StatusCode::NO_CONTENT
248 } else {
249 StatusCode::CREATED
250 };
251 no_store_empty_response(status)
252}
253
254pub fn mutation_multistatus_response(
256 prefix: &str,
257 failures: &[DavMutationFailure],
258) -> Result<DavResponse, DavXmlError> {
259 let items = failures
260 .iter()
261 .map(|failure| {
262 let item = DavMultiStatusItem::status(
263 href_for_dav_path(prefix, &failure.path),
264 failure.status,
265 );
266 if failure.status == StatusCode::LOCKED.as_u16() {
267 let lock_path = failure.lock_path.as_ref().unwrap_or(&failure.path);
268 item.with_error(DavErrorCondition::LockTokenSubmitted {
269 href: href_for_dav_path(prefix, lock_path),
270 })
271 } else {
272 item
273 }
274 })
275 .collect();
276 let body = dav_multistatus_element(items).to_bytes()?;
277 let mut response = DavResponse::bytes(StatusCode::MULTI_STATUS, body);
278 response.headers.insert(
279 CONTENT_TYPE,
280 HeaderValue::from_static("application/xml; charset=utf-8"),
281 );
282 response
283 .headers
284 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
285 Ok(response)
286}
287
288#[must_use]
290pub fn resource_identity_path(path: &str) -> String {
291 let trimmed = path.trim_end_matches('/');
292 if trimmed.is_empty() {
293 "/".to_string()
294 } else {
295 trimmed.to_string()
296 }
297}