Skip to main content

aster_forge_webdav/
lock.rs

1//! LOCK/UNLOCK protocol request planning and response composition.
2
3use std::time::Duration;
4
5use http::header::{CACHE_CONTROL, CONTENT_TYPE};
6use http::{HeaderMap, HeaderValue, StatusCode};
7
8use crate::DavLockSystem;
9use crate::response::{no_store_empty_response, xml_request_error_response};
10use crate::{
11    DavErrorCondition, DavFileSystem, DavLock, DavLockXml, DavPath, DavProtocolError,
12    DavRequestHead, DavResponse, DavXmlElement, DavXmlError, FsError, IfHeader, OpenOptions,
13    dav_error_element, dav_lock_discovery_element, dav_lock_response_element, href_for_dav_path,
14    parent_relative_path, parse_lock_request, parse_lock_timeout, protocol_error_response,
15    submitted_lock_tokens,
16};
17
18/// Backend operation selected from a LOCK request.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum DavLockPlan {
21    Acquire {
22        owner: Option<DavXmlElement>,
23        timeout: Duration,
24        shared: bool,
25        deep: bool,
26    },
27    Refresh {
28        token: String,
29        timeout: Duration,
30    },
31}
32
33/// Failure while parsing and selecting a LOCK operation.
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35pub enum DavLockPlanError {
36    #[error(transparent)]
37    Protocol(#[from] DavProtocolError),
38    #[error(transparent)]
39    Xml(#[from] DavXmlError),
40}
41
42/// Rejects an operation when a conflicting lock token was not submitted for its lock root.
43pub async fn enforce_unlocked(
44    lock_system: &dyn DavLockSystem,
45    path: &DavPath,
46    deep: bool,
47    prefix: &str,
48    if_header: Option<&IfHeader>,
49    request_scheme: &str,
50    request_host: &str,
51) -> Result<(), DavResponse> {
52    if let Some(lock) = unsubmitted_lock_conflicts(
53        lock_system,
54        path,
55        deep,
56        prefix,
57        if_header,
58        request_scheme,
59        request_host,
60    )
61    .await
62    .into_iter()
63    .next()
64    {
65        return Err(lock_conflict_response(prefix, &lock.path)
66            .unwrap_or_else(|_| DavResponse::empty(StatusCode::INTERNAL_SERVER_ERROR)));
67    }
68    Ok(())
69}
70
71/// Returns conflicting locks whose tokens were not submitted for their corresponding lock root.
72pub async fn unsubmitted_lock_conflicts(
73    lock_system: &dyn DavLockSystem,
74    path: &DavPath,
75    deep: bool,
76    prefix: &str,
77    if_header: Option<&IfHeader>,
78    request_scheme: &str,
79    request_host: &str,
80) -> Vec<crate::DavLock> {
81    let mut conflicts = lock_system.conflicting_locks(path, deep).await;
82    conflicts.retain(|lock| {
83        let lock_href = href_for_dav_path(prefix, &lock.path);
84        let submitted_tokens = if_header.map_or_else(Vec::new, |if_header| {
85            submitted_lock_tokens(if_header, &lock_href, request_scheme, request_host)
86        });
87        !submitted_tokens.iter().any(|token| token == &lock.token)
88    });
89    conflicts
90}
91
92/// Applies [`enforce_unlocked`] to the canonical parent of a mutation target.
93pub async fn enforce_parent_unlocked(
94    lock_system: &dyn DavLockSystem,
95    path: &DavPath,
96    prefix: &str,
97    if_header: Option<&IfHeader>,
98    request_scheme: &str,
99    request_host: &str,
100) -> Result<(), DavResponse> {
101    let Some(parent) = parent_relative_path(path.as_str()) else {
102        return Ok(());
103    };
104    let parent_path = DavPath::new(&parent).map_err(|_| {
105        protocol_error_response(&DavProtocolError::bad_request("Invalid request path"))
106    })?;
107    enforce_unlocked(
108        lock_system,
109        &parent_path,
110        false,
111        prefix,
112        if_header,
113        request_scheme,
114        request_host,
115    )
116    .await
117}
118
119/// Ensures that a LOCK target exists, creating an empty lock-null file when allowed.
120///
121/// Existing resources are left untouched. A missing collection target remains missing because
122/// creating its hierarchy is outside LOCK semantics.
123pub async fn ensure_lock_target_exists(
124    filesystem: &dyn DavFileSystem,
125    path: &DavPath,
126) -> Result<bool, FsError> {
127    match filesystem.metadata(path).await {
128        Ok(_) => Ok(true),
129        Err(FsError::NotFound) if !path.is_collection() => {
130            let mut file = filesystem
131                .open(
132                    path,
133                    OpenOptions {
134                        write: true,
135                        create: true,
136                        truncate: true,
137                        size: Some(0),
138                        ..OpenOptions::default()
139                    },
140                )
141                .await?;
142            file.flush().await?;
143            Ok(false)
144        }
145        Err(FsError::NotFound) => Err(FsError::NotFound),
146        Err(error) => Err(error),
147    }
148}
149
150/// Selects lock acquisition or refresh and validates all protocol-owned inputs.
151pub fn plan_lock_request(
152    headers: &HeaderMap,
153    body: &[u8],
154    request_head: &DavRequestHead,
155    prefix: &str,
156    maximum_timeout: Duration,
157) -> Result<DavLockPlan, DavLockPlanError> {
158    let timeout = parse_lock_timeout(headers, maximum_timeout)?;
159    if body.is_empty() {
160        let request_href = href_for_dav_path(prefix, &request_head.target);
161        let tokens = request_head
162            .if_header
163            .as_ref()
164            .map_or_else(Vec::new, |if_header| {
165                submitted_lock_tokens(
166                    if_header,
167                    &request_href,
168                    &request_head.origin.scheme,
169                    &request_head.origin.host,
170                )
171            });
172        if tokens.len() != 1 {
173            return Err(DavProtocolError::bad_request("Invalid LOCK refresh token").into());
174        }
175        return Ok(DavLockPlan::Refresh {
176            token: tokens[0].clone(),
177            timeout,
178        });
179    }
180
181    let request = parse_lock_request(body)?;
182    let depth = request_head
183        .depth
184        .ok_or_else(|| DavProtocolError::bad_request("LOCK Depth was not parsed"))?;
185    Ok(DavLockPlan::Acquire {
186        owner: request.owner,
187        timeout,
188        shared: request.shared,
189        deep: depth.is_infinity(),
190    })
191}
192
193fn lock_success_response(
194    lock: &DavLock,
195    status: StatusCode,
196    prefix: &str,
197    include_lock_token_header: bool,
198) -> Result<DavResponse, DavXmlError> {
199    let body = dav_lock_response_element(&[DavLockXml {
200        token: lock.token.clone(),
201        owner: lock.owner.as_deref().cloned(),
202        timeout: lock.timeout,
203        shared: lock.shared,
204        deep: lock.deep,
205        root_href: href_for_dav_path(prefix, &lock.path),
206    }])
207    .to_bytes()?;
208    let mut response = DavResponse::bytes(status, body);
209    response.headers.insert(
210        CONTENT_TYPE,
211        HeaderValue::from_static("application/xml; charset=utf-8"),
212    );
213    if include_lock_token_header {
214        let value = HeaderValue::from_str(&format!("<{}>", lock.token))
215            .map_err(|_| DavXmlError::Malformed)?;
216        response.headers.insert("Lock-Token", value);
217    }
218    Ok(response)
219}
220
221/// Builds the successful response for a LOCK refresh.
222pub fn lock_refresh_success_response(
223    lock: &DavLock,
224    prefix: &str,
225) -> Result<DavResponse, DavXmlError> {
226    lock_success_response(lock, StatusCode::OK, prefix, false)
227}
228
229/// Builds the 200/201 response for a LOCK acquisition.
230pub fn lock_acquire_success_response(
231    lock: &DavLock,
232    prefix: &str,
233    resource_existed: bool,
234) -> Result<DavResponse, DavXmlError> {
235    lock_success_response(
236        lock,
237        if resource_existed {
238            StatusCode::OK
239        } else {
240            StatusCode::CREATED
241        },
242        prefix,
243        true,
244    )
245}
246
247/// Builds the `DAV:lockdiscovery` property from backend lock values.
248#[must_use]
249pub fn lock_discovery_element(locks: &[DavLock], prefix: &str) -> DavXmlElement {
250    let locks = locks
251        .iter()
252        .map(|lock| DavLockXml {
253            token: lock.token.clone(),
254            owner: lock.owner.as_deref().cloned(),
255            timeout: lock.timeout,
256            shared: lock.shared,
257            deep: lock.deep,
258            root_href: href_for_dav_path(prefix, &lock.path),
259        })
260        .collect::<Vec<_>>();
261    dav_lock_discovery_element(&locks)
262}
263
264/// Builds a 423 response identifying the lock whose token must be submitted.
265pub fn lock_conflict_response(prefix: &str, path: &DavPath) -> Result<DavResponse, DavXmlError> {
266    lock_condition_response(
267        StatusCode::LOCKED,
268        DavErrorCondition::LockTokenSubmitted {
269            href: href_for_dav_path(prefix, path),
270        },
271    )
272}
273
274/// Builds the 409 response for an UNLOCK token that does not match the request URI.
275pub fn unlock_token_mismatch_response() -> Result<DavResponse, DavXmlError> {
276    lock_condition_response(
277        StatusCode::CONFLICT,
278        DavErrorCondition::LockTokenMatchesRequestUri,
279    )
280}
281
282/// Builds the successful cache-safe UNLOCK response.
283#[must_use]
284pub fn unlock_success_response() -> DavResponse {
285    no_store_empty_response(StatusCode::NO_CONTENT)
286}
287
288/// Builds the active-lock capacity response.
289#[must_use]
290pub fn lock_limit_response() -> DavResponse {
291    let mut response = DavResponse::bytes(
292        StatusCode::INSUFFICIENT_STORAGE,
293        "WebDAV active lock limit exceeded",
294    );
295    response.headers.insert(
296        CONTENT_TYPE,
297        HeaderValue::from_static("text/plain; charset=utf-8"),
298    );
299    response
300        .headers
301        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
302    response
303}
304
305/// Maps LOCK XML failures to their protocol response.
306pub fn lock_xml_error_response(error: DavXmlError) -> Result<DavResponse, DavXmlError> {
307    xml_request_error_response(error, "Invalid LOCK body")
308}
309
310fn lock_condition_response(
311    status: StatusCode,
312    condition: DavErrorCondition,
313) -> Result<DavResponse, DavXmlError> {
314    let mut response = DavResponse::bytes(status, dav_error_element(&condition).to_bytes()?);
315    response.headers.insert(
316        CONTENT_TYPE,
317        HeaderValue::from_static("application/xml; charset=utf-8"),
318    );
319    response
320        .headers
321        .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
322    Ok(response)
323}