webdav_headers/
lib.rs

1// SPDX-FileCopyrightText: d-k-bo <d-k-bo@mailbox.org>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! HTTP headers for WebDAV as defined in
6//! [RFC 4918](http://webdav.org/specs/rfc4918.html#http.headers.for.distributed.authoring)
7//! implementing the [`headers::Header`] trait.
8
9mod dav;
10mod depth;
11mod destination;
12mod if_;
13mod lock_token;
14mod overwrite;
15mod timeout;
16mod utils;
17
18use self::utils::ParseString;
19
20pub use self::{
21    coded_url::{CodedUrl, InvalidCodedUrl},
22    dav::{ComplianceClass, Dav, InvalidComplianceClass, Tokens},
23    depth::Depth,
24    destination::Destination,
25    if_::{Condition, If, InvalidIf, ResourceTag},
26    lock_token::LockToken,
27    names::*,
28    overwrite::Overwrite,
29    timeout::Timeout,
30};
31
32mod names {
33    /// Header name of the [`DAV`](super::Dav) header.
34    pub static DAV: headers::HeaderName = headers::HeaderName::from_static("dav");
35    /// Header name of the [`Depth`](super::Depth) header.
36    pub static DEPTH: headers::HeaderName = headers::HeaderName::from_static("depth");
37    /// Header name of the [`Destination`](super::Destination) header.
38    pub static DESTINATION: headers::HeaderName = headers::HeaderName::from_static("destination");
39    /// Header name of the [`If`](super::If) header.
40    pub static IF: headers::HeaderName = headers::HeaderName::from_static("if");
41    /// Header name of the [`LockToken`](super::LockToken) header.
42    pub static LOCK_TOKEN: headers::HeaderName = headers::HeaderName::from_static("lock-token");
43    /// Header name of the [`Overwrite`](super::Overwrite) header.
44    pub static OVERWRITE: headers::HeaderName = headers::HeaderName::from_static("overwrite");
45    /// Header name of the [`Timeout`](super::Timeout) header.
46    pub static TIMEOUT: headers::HeaderName = headers::HeaderName::from_static("timeout");
47}
48
49mod coded_url {
50    use crate::utils::ParseString;
51
52    pub use self::error::InvalidCodedUrl;
53
54    /// Coded-URL used in the `DAV` and `If` headers
55    #[derive(Clone, Debug, PartialEq)]
56    pub struct CodedUrl(pub uniresid::AbsoluteUri);
57
58    impl std::fmt::Display for CodedUrl {
59        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60            write!(f, "<{}>", self.0)
61        }
62    }
63
64    impl std::str::FromStr for CodedUrl {
65        type Err = InvalidCodedUrl;
66
67        fn from_str(mut s: &str) -> Result<Self, Self::Err> {
68            Self::parse(&mut s)
69        }
70    }
71
72    impl ParseString for CodedUrl {
73        type Err = InvalidCodedUrl;
74
75        fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
76            if s.starts_with('<') {
77                s = &s[1..];
78            } else {
79                return Err(InvalidCodedUrl::ExpectedChar('<'));
80            }
81            let Some(end) = s.find('>') else {
82                return Err(InvalidCodedUrl::ExpectedChar('>'));
83            };
84
85            let uri = uniresid::AbsoluteUri::parse(&s[..end]).map_err(InvalidCodedUrl::Uri)?;
86
87            Ok((CodedUrl(uri), &s[end + 1..]))
88        }
89    }
90
91    mod error {
92        /// Error returned when parsing [`CodedUrl`](super::CodedUrl) from a
93        /// string fails.
94        #[derive(Debug)]
95        pub enum InvalidCodedUrl {
96            ExpectedChar(char),
97            Uri(uniresid::Error),
98        }
99
100        impl std::fmt::Display for InvalidCodedUrl {
101            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102                match self {
103                    Self::ExpectedChar(c) => write!(f, "expected '{c}'"),
104                    Self::Uri(..) => write!(f, "invalid Absolute-URI"),
105                }
106            }
107        }
108
109        impl std::error::Error for InvalidCodedUrl {
110            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111                match self {
112                    Self::Uri(e) => Some(e),
113                    _ => None,
114                }
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121mod test {
122    // based on https://github.com/hyperium/headers/blob/2b9fc5be92f0346482aa6d09917a434a56ade3f3/src/common/mod.rs#L72-L88
123    #[track_caller]
124    pub(crate) fn test_decode<T: headers::Header>(values: &[&str]) -> Option<T> {
125        use headers::HeaderMapExt;
126        let mut map = http::HeaderMap::new();
127        for val in values {
128            map.append(T::name(), val.parse().unwrap());
129        }
130        map.typed_get()
131    }
132
133    pub(crate) fn test_encode<T: headers::Header>(header: T) -> http::HeaderMap {
134        use headers::HeaderMapExt;
135        let mut map = http::HeaderMap::new();
136        map.typed_insert(header);
137        map
138    }
139
140    #[track_caller]
141    pub(crate) fn test<T>(s: &'static str, header: T)
142    where
143        T: headers::Header + std::fmt::Debug + PartialEq,
144    {
145        use pretty_assertions::assert_eq;
146
147        assert_eq!(
148            header,
149            match test_decode::<T>(&[s]) {
150                Some(header) => header,
151                None => panic!("failed to decode \"{s}\""),
152            }
153        );
154        assert_eq!(s, test_encode(header)[T::name()]);
155    }
156
157    #[track_caller]
158    pub(crate) fn test_all<T>(testcases: impl IntoIterator<Item = (&'static str, T)>)
159    where
160        T: headers::Header + std::fmt::Debug + PartialEq,
161    {
162        for (s, header) in testcases {
163            test(s, header)
164        }
165    }
166}