aster_forge_webdav/
request.rs

1//! Transport-neutral `WebDAV` request head parsing.
2
3use http::{HeaderMap, Method, Uri};
4
5use crate::DavPath;
6use crate::event::DavOperation;
7use crate::protocol::{
8    DavProtocolError, Depth, Destination, IfHeader, destination_relative_path, parse_copy_depth,
9    parse_delete_depth, parse_if_header, parse_lock_depth, parse_move_depth, parse_overwrite,
10    parse_propfind_depth, strip_mount_prefix,
11};
12
13/// `WebDAV` method recognized by the protocol layer.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DavMethod {
16    Options,
17    Propfind,
18    Proppatch,
19    Get,
20    Head,
21    Put,
22    Patch,
23    Mkcol,
24    Delete,
25    Copy,
26    Move,
27    Lock,
28    Unlock,
29    Acl,
30    Report,
31    VersionControl,
32    Checkout,
33    Checkin,
34    Uncheckout,
35    Mkworkspace,
36    Update,
37    Label,
38    Merge,
39    BaselineControl,
40    Mkactivity,
41    Search,
42    Orderpatch,
43    Mkredirectref,
44    Updateredirectref,
45    Bind,
46    Unbind,
47    Rebind,
48    Post,
49}
50
51/// How the transport adapter must handle a request body before product code runs.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DavBodyPolicy {
54    /// Reject the first non-empty body chunk.
55    Empty,
56    /// Collect an XML body up to the supplied byte limit.
57    BoundedXml { maximum: usize },
58    /// Collect an opaque body up to the patch format's byte limit.
59    Bounded { maximum: usize },
60    /// Leave the body as a stream for the product storage adapter.
61    Stream,
62    /// Preserve the existing method behavior without consuming the body.
63    Unused,
64}
65
66impl DavMethod {
67    /// Methods in the canonical `Allow` rendering order.
68    pub const ALL: [Self; 33] = [
69        Self::Options,
70        Self::Get,
71        Self::Head,
72        Self::Post,
73        Self::Put,
74        Self::Patch,
75        Self::Delete,
76        Self::Mkcol,
77        Self::Copy,
78        Self::Move,
79        Self::Propfind,
80        Self::Proppatch,
81        Self::Lock,
82        Self::Unlock,
83        Self::Acl,
84        Self::Report,
85        Self::VersionControl,
86        Self::Checkout,
87        Self::Checkin,
88        Self::Uncheckout,
89        Self::Mkworkspace,
90        Self::Update,
91        Self::Label,
92        Self::Merge,
93        Self::BaselineControl,
94        Self::Mkactivity,
95        Self::Search,
96        Self::Orderpatch,
97        Self::Mkredirectref,
98        Self::Updateredirectref,
99        Self::Bind,
100        Self::Unbind,
101        Self::Rebind,
102    ];
103
104    #[must_use]
105    pub const fn index(self) -> u32 {
106        match self {
107            Self::Options => 0,
108            Self::Get => 1,
109            Self::Head => 2,
110            Self::Post => 3,
111            Self::Put => 4,
112            Self::Patch => 5,
113            Self::Delete => 6,
114            Self::Mkcol => 7,
115            Self::Copy => 8,
116            Self::Move => 9,
117            Self::Propfind => 10,
118            Self::Proppatch => 11,
119            Self::Lock => 12,
120            Self::Unlock => 13,
121            Self::Acl => 14,
122            Self::Report => 15,
123            Self::VersionControl => 16,
124            Self::Checkout => 17,
125            Self::Checkin => 18,
126            Self::Uncheckout => 19,
127            Self::Mkworkspace => 20,
128            Self::Update => 21,
129            Self::Label => 22,
130            Self::Merge => 23,
131            Self::BaselineControl => 24,
132            Self::Mkactivity => 25,
133            Self::Search => 26,
134            Self::Orderpatch => 27,
135            Self::Mkredirectref => 28,
136            Self::Updateredirectref => 29,
137            Self::Bind => 30,
138            Self::Unbind => 31,
139            Self::Rebind => 32,
140        }
141    }
142
143    #[must_use]
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::Options => "OPTIONS",
147            Self::Propfind => "PROPFIND",
148            Self::Proppatch => "PROPPATCH",
149            Self::Get => "GET",
150            Self::Head => "HEAD",
151            Self::Post => "POST",
152            Self::Put => "PUT",
153            Self::Patch => "PATCH",
154            Self::Mkcol => "MKCOL",
155            Self::Delete => "DELETE",
156            Self::Copy => "COPY",
157            Self::Move => "MOVE",
158            Self::Lock => "LOCK",
159            Self::Unlock => "UNLOCK",
160            Self::Acl => "ACL",
161            Self::Report => "REPORT",
162            Self::VersionControl => "VERSION-CONTROL",
163            Self::Checkout => "CHECKOUT",
164            Self::Checkin => "CHECKIN",
165            Self::Uncheckout => "UNCHECKOUT",
166            Self::Mkworkspace => "MKWORKSPACE",
167            Self::Update => "UPDATE",
168            Self::Label => "LABEL",
169            Self::Merge => "MERGE",
170            Self::BaselineControl => "BASELINE-CONTROL",
171            Self::Mkactivity => "MKACTIVITY",
172            Self::Search => "SEARCH",
173            Self::Orderpatch => "ORDERPATCH",
174            Self::Mkredirectref => "MKREDIRECTREF",
175            Self::Updateredirectref => "UPDATEREDIRECTREF",
176            Self::Bind => "BIND",
177            Self::Unbind => "UNBIND",
178            Self::Rebind => "REBIND",
179        }
180    }
181
182    /// Parses a supported HTTP/WebDAV method.
183    #[must_use]
184    pub fn from_method(method: &Method) -> Option<Self> {
185        Self::from_name(method.as_str())
186    }
187
188    /// Parses a supported HTTP/WebDAV method name across transport implementations.
189    #[must_use]
190    pub fn from_name(method: &str) -> Option<Self> {
191        match method {
192            "OPTIONS" => Some(Self::Options),
193            "PROPFIND" => Some(Self::Propfind),
194            "PROPPATCH" => Some(Self::Proppatch),
195            "GET" => Some(Self::Get),
196            "HEAD" => Some(Self::Head),
197            "POST" => Some(Self::Post),
198            "PUT" => Some(Self::Put),
199            "PATCH" => Some(Self::Patch),
200            "MKCOL" => Some(Self::Mkcol),
201            "DELETE" => Some(Self::Delete),
202            "COPY" => Some(Self::Copy),
203            "MOVE" => Some(Self::Move),
204            "LOCK" => Some(Self::Lock),
205            "UNLOCK" => Some(Self::Unlock),
206            "ACL" => Some(Self::Acl),
207            "REPORT" => Some(Self::Report),
208            "VERSION-CONTROL" => Some(Self::VersionControl),
209            "CHECKOUT" => Some(Self::Checkout),
210            "CHECKIN" => Some(Self::Checkin),
211            "UNCHECKOUT" => Some(Self::Uncheckout),
212            "MKWORKSPACE" => Some(Self::Mkworkspace),
213            "UPDATE" => Some(Self::Update),
214            "LABEL" => Some(Self::Label),
215            "MERGE" => Some(Self::Merge),
216            "BASELINE-CONTROL" => Some(Self::BaselineControl),
217            "MKACTIVITY" => Some(Self::Mkactivity),
218            "SEARCH" => Some(Self::Search),
219            "ORDERPATCH" => Some(Self::Orderpatch),
220            "MKREDIRECTREF" => Some(Self::Mkredirectref),
221            "UPDATEREDIRECTREF" => Some(Self::Updateredirectref),
222            "BIND" => Some(Self::Bind),
223            "UNBIND" => Some(Self::Unbind),
224            "REBIND" => Some(Self::Rebind),
225            _ => None,
226        }
227    }
228
229    /// Returns the corresponding observable operation.
230    #[must_use]
231    pub const fn operation(self) -> DavOperation {
232        match self {
233            Self::Options => DavOperation::Options,
234            Self::Propfind => DavOperation::Propfind,
235            Self::Proppatch => DavOperation::Proppatch,
236            Self::Get => DavOperation::Get,
237            Self::Head => DavOperation::Head,
238            Self::Post => DavOperation::Post,
239            Self::Put => DavOperation::Put,
240            Self::Patch => DavOperation::Patch,
241            Self::Mkcol => DavOperation::Mkcol,
242            Self::Delete => DavOperation::Delete,
243            Self::Copy => DavOperation::Copy,
244            Self::Move => DavOperation::Move,
245            Self::Lock => DavOperation::Lock,
246            Self::Unlock => DavOperation::Unlock,
247            Self::Acl => DavOperation::Acl,
248            Self::Report => DavOperation::Report,
249            Self::VersionControl => DavOperation::VersionControl,
250            Self::Checkout => DavOperation::Checkout,
251            Self::Checkin => DavOperation::Checkin,
252            Self::Uncheckout => DavOperation::Uncheckout,
253            Self::Mkworkspace => DavOperation::Mkworkspace,
254            Self::Update => DavOperation::Update,
255            Self::Label => DavOperation::Label,
256            Self::Merge => DavOperation::Merge,
257            Self::BaselineControl => DavOperation::BaselineControl,
258            Self::Mkactivity => DavOperation::Mkactivity,
259            Self::Search => DavOperation::Search,
260            Self::Orderpatch => DavOperation::Orderpatch,
261            Self::Mkredirectref => DavOperation::Mkredirectref,
262            Self::Updateredirectref => DavOperation::Updateredirectref,
263            Self::Bind => DavOperation::Bind,
264            Self::Unbind => DavOperation::Unbind,
265            Self::Rebind => DavOperation::Rebind,
266        }
267    }
268}
269
270/// Compact, duplicate-free set of methods in canonical protocol order.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
272pub struct DavMethodSet(u64);
273
274impl DavMethodSet {
275    #[must_use]
276    pub const fn empty() -> Self {
277        Self(0)
278    }
279
280    #[must_use]
281    pub const fn from_methods(methods: &[DavMethod]) -> Self {
282        let mut set = Self::empty();
283        let mut index = 0;
284        while index < methods.len() {
285            set = set.with(methods[index]);
286            index += 1;
287        }
288        set
289    }
290
291    #[must_use]
292    pub const fn with(self, method: DavMethod) -> Self {
293        Self(self.0 | (1u64 << method.index()))
294    }
295
296    #[must_use]
297    pub const fn without(self, method: DavMethod) -> Self {
298        Self(self.0 & !(1u64 << method.index()))
299    }
300
301    #[must_use]
302    pub const fn union(self, other: Self) -> Self {
303        Self(self.0 | other.0)
304    }
305
306    #[must_use]
307    pub const fn contains(self, method: DavMethod) -> bool {
308        self.0 & (1u64 << method.index()) != 0
309    }
310
311    #[must_use]
312    pub const fn is_subset_of(self, other: Self) -> bool {
313        self.0 & !other.0 == 0
314    }
315
316    #[must_use]
317    pub const fn is_empty(self) -> bool {
318        self.0 == 0
319    }
320
321    #[must_use]
322    pub const fn iter(self) -> DavMethodSetIter {
323        DavMethodSetIter {
324            set: self,
325            index: 0,
326        }
327    }
328
329    #[must_use]
330    pub fn render(self) -> String {
331        let mut rendered = String::new();
332        for method in self.iter() {
333            if !rendered.is_empty() {
334                rendered.push_str(", ");
335            }
336            rendered.push_str(method.as_str());
337        }
338        rendered
339    }
340}
341
342/// Iterator over methods in canonical protocol order.
343#[derive(Debug, Clone)]
344pub struct DavMethodSetIter {
345    set: DavMethodSet,
346    index: usize,
347}
348
349impl Iterator for DavMethodSetIter {
350    type Item = DavMethod;
351
352    fn next(&mut self) -> Option<Self::Item> {
353        while self.index < DavMethod::ALL.len() {
354            let method = DavMethod::ALL[self.index];
355            self.index += 1;
356            if self.set.contains(method) {
357                return Some(method);
358            }
359        }
360        None
361    }
362}
363
364/// Request origin needed for same-origin tagged URI and destination validation.
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct DavRequestOrigin {
367    pub scheme: String,
368    pub host: String,
369}
370
371/// Parsed request target shared by known and unknown method handling.
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct DavRequestTarget<'a> {
374    pub target: DavPath,
375    pub origin: DavRequestOrigin,
376    pub mount_path: &'a str,
377}
378
379/// Parsed, body-independent `WebDAV` request data.
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct DavRequestHead {
382    pub method: DavMethod,
383    pub target: DavPath,
384    pub origin: DavRequestOrigin,
385    pub depth: Option<Depth>,
386    pub overwrite: Option<bool>,
387    pub destination: Option<Destination>,
388    pub if_header: Option<IfHeader>,
389}
390
391impl DavRequestHead {
392    /// Parses a mount-relative target before method dispatch or product code runs.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`DavProtocolError`] when the URI, mount-relative path, or request origin is invalid.
397    pub fn parse_target<'a>(
398        uri: &Uri,
399        mount_path: &'a str,
400        origin: &DavRequestOrigin,
401    ) -> Result<DavRequestTarget<'a>, DavProtocolError> {
402        let relative = strip_mount_prefix(uri.path(), mount_path).ok_or_else(|| {
403            DavProtocolError::bad_request("Request target must stay under WebDAV prefix")
404        })?;
405        let target = DavPath::new(relative)
406            .map_err(|_| DavProtocolError::bad_request("Invalid request path"))?;
407        Ok(DavRequestTarget {
408            target,
409            origin: origin.clone(),
410            mount_path,
411        })
412    }
413
414    /// Parses method-specific protocol headers after the target has been resolved.
415    ///
416    /// # Errors
417    ///
418    /// Returns [`DavProtocolError`] when the target, destination, or method headers are invalid.
419    pub fn parse_known_method(
420        method: DavMethod,
421        request_target: &DavRequestTarget<'_>,
422        headers: &HeaderMap,
423    ) -> Result<Self, DavProtocolError> {
424        let depth = match method {
425            DavMethod::Propfind => Some(parse_propfind_depth(headers)?),
426            DavMethod::Copy => Some(parse_copy_depth(headers)?),
427            DavMethod::Move => Some(parse_move_depth(headers)?),
428            DavMethod::Delete => Some(parse_delete_depth(headers)?),
429            DavMethod::Lock => Some(parse_lock_depth(headers)?),
430            _ => None,
431        };
432        let (overwrite, destination) = match method {
433            DavMethod::Copy | DavMethod::Move => (
434                Some(parse_overwrite(headers)?),
435                Some(destination_relative_path(
436                    headers,
437                    request_target.mount_path,
438                    &request_target.origin.scheme,
439                    &request_target.origin.host,
440                )?),
441            ),
442            _ => (None, None),
443        };
444
445        Ok(Self {
446            method,
447            target: request_target.target.clone(),
448            origin: request_target.origin.clone(),
449            depth,
450            overwrite,
451            destination,
452            if_header: parse_if_header(headers)?,
453        })
454    }
455}