webdav_handler/
davpath.rs

1//! Utility module to handle the path part of an URL as a filesytem path.
2//!
3use std::error::Error;
4use std::ffi::OsStr;
5use std::os::unix::ffi::OsStrExt;
6use std::path::{Path, PathBuf};
7
8use mime_guess;
9use percent_encoding as pct;
10
11use crate::DavError;
12
13/// URL path, with hidden prefix.
14#[derive(Clone)]
15pub struct DavPath {
16    fullpath: Vec<u8>,
17    pfxlen:   Option<usize>,
18}
19
20/// Reference to DavPath, no prefix.
21/// It's what you get when you `Deref` `DavPath`, and returned by `DavPath::with_prefix()`.
22pub struct DavPathRef {
23    fullpath: [u8],
24}
25
26#[derive(Copy, Clone, Debug)]
27#[allow(non_camel_case_types)]
28struct ENCODE_SET;
29
30impl pct::EncodeSet for ENCODE_SET {
31    // Encode all non-unreserved characters, except '/'.
32    // See RFC3986, and https://en.wikipedia.org/wiki/Percent-encoding .
33    #[inline]
34    fn contains(&self, byte: u8) -> bool {
35        let unreserved = (byte >= b'A' && byte <= b'Z') ||
36            (byte >= b'a' && byte <= b'z') ||
37            (byte >= b'0' && byte <= b'9') ||
38            byte == b'-' ||
39            byte == b'_' ||
40            byte == b'.' ||
41            byte == b'~';
42        !unreserved && byte != b'/'
43    }
44}
45
46impl std::fmt::Display for DavPath {
47    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
48        write!(f, "{:?}", &self.as_url_string_with_prefix_debug())
49    }
50}
51
52impl std::fmt::Debug for DavPath {
53    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
54        write!(f, "{:?}", &self.as_url_string_with_prefix_debug())
55    }
56}
57
58/// Error returned by some of the DavPath methods.
59#[derive(Debug)]
60pub enum ParseError {
61    /// cannot parse
62    InvalidPath,
63    /// outside of prefix
64    PrefixMismatch,
65    /// too many dotdots
66    ForbiddenPath,
67}
68
69impl Error for ParseError {
70    fn description(&self) -> &str {
71        "DavPath parse error"
72    }
73    fn cause(&self) -> Option<&dyn Error> {
74        None
75    }
76}
77
78impl std::fmt::Display for ParseError {
79    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
80        write!(f, "{:?}", self)
81    }
82}
83
84impl From<ParseError> for DavError {
85    fn from(e: ParseError) -> Self {
86        match e {
87            ParseError::InvalidPath => DavError::InvalidPath,
88            ParseError::PrefixMismatch => DavError::IllegalPath,
89            ParseError::ForbiddenPath => DavError::ForbiddenPath,
90        }
91    }
92}
93
94// a decoded segment can contain any value except '/' or '\0'
95fn valid_segment(src: &[u8]) -> Result<(), ParseError> {
96    let mut p = pct::percent_decode(src);
97    if p.any(|x| x == 0 || x == b'/') {
98        return Err(ParseError::InvalidPath);
99    }
100    Ok(())
101}
102
103// encode path segment with user-defined ENCODE_SET
104fn encode_path(src: &[u8]) -> Vec<u8> {
105    pct::percent_encode(src, ENCODE_SET).to_string().into_bytes()
106}
107
108// make path safe:
109// - raw path before decoding can contain only printable ascii
110// - make sure path is absolute
111// - remove query part (everything after ?)
112// - merge consecutive slashes
113// - process . and ..
114// - decode percent encoded bytes, fail on invalid encodings.
115// - do not allow NUL or '/' in segments.
116fn normalize_path(rp: &[u8]) -> Result<Vec<u8>, ParseError> {
117    // must consist of printable ASCII
118    if rp.iter().any(|&x| x < 32 || x > 126) {
119        Err(ParseError::InvalidPath)?;
120    }
121
122    // don't allow fragments. query part gets deleted.
123    let mut rawpath = rp;
124    if let Some(pos) = rawpath.iter().position(|&x| x == b'?' || x == b'#') {
125        if rawpath[pos] == b'#' {
126            Err(ParseError::InvalidPath)?;
127        }
128        rawpath = &rawpath[..pos];
129    }
130
131    // must start with "/"
132    if rawpath.is_empty() || rawpath[0] != b'/' {
133        Err(ParseError::InvalidPath)?;
134    }
135
136    // split up in segments
137    let isdir = match rawpath.last() {
138        Some(x) if *x == b'/' => true,
139        _ => false,
140    };
141    let segments = rawpath.split(|c| *c == b'/');
142    let mut v: Vec<&[u8]> = Vec::new();
143    for segment in segments {
144        match segment {
145            b"." | b"" => {},
146            b".." => {
147                if v.len() < 2 {
148                    return Err(ParseError::ForbiddenPath);
149                }
150                v.pop();
151                v.pop();
152            },
153            s => {
154                if let Err(e) = valid_segment(s) {
155                    Err(e)?;
156                }
157                v.push(b"/");
158                v.push(s);
159            },
160        }
161    }
162    if isdir || v.is_empty() {
163        v.push(b"/");
164    }
165    Ok(v.iter().flat_map(|s| pct::percent_decode(s)).collect())
166}
167
168/// Comparision ignores any trailing slash, so /foo == /foo/
169impl PartialEq for DavPath {
170    fn eq(&self, rhs: &DavPath) -> bool {
171        let mut a = self.fullpath.as_slice();
172        if a.len() > 1 && a.ends_with(b"/") {
173            a = &a[..a.len() - 1];
174        }
175        let mut b = rhs.fullpath.as_slice();
176        if b.len() > 1 && b.ends_with(b"/") {
177            b = &b[..b.len() - 1];
178        }
179        a == b
180    }
181}
182
183impl DavPath {
184    /// from URL encoded path
185    pub fn new(src: &str) -> Result<DavPath, ParseError> {
186        let path = normalize_path(src.as_bytes())?;
187        Ok(DavPath {
188            fullpath: path.to_vec(),
189            pfxlen:   None,
190        })
191    }
192
193    /// Set prefix.
194    pub fn set_prefix(&mut self, prefix: &str) -> Result<(), ParseError> {
195        let path = &mut self.fullpath;
196        let prefix = prefix.as_bytes();
197        if !path.starts_with(prefix) {
198            return Err(ParseError::PrefixMismatch);
199        }
200        let mut pfxlen = prefix.len();
201        if prefix.ends_with(b"/") {
202            pfxlen -= 1;
203            if path[pfxlen] != b'/' {
204                return Err(ParseError::PrefixMismatch);
205            }
206        } else if path.len() == pfxlen {
207            path.push(b'/');
208        }
209        self.pfxlen = Some(pfxlen);
210        Ok(())
211    }
212
213    /// Return a DavPathRef that refers to the entire URL path with prefix.
214    pub fn with_prefix(&self) -> &DavPathRef {
215        DavPathRef::new(&self.fullpath)
216    }
217
218    /// from URL encoded path and non-encoded prefix.
219    pub(crate) fn from_str_and_prefix(src: &str, prefix: &str) -> Result<DavPath, ParseError> {
220        let path = normalize_path(src.as_bytes())?;
221        let mut davpath = DavPath {
222            fullpath: path.to_vec(),
223            pfxlen:   None,
224        };
225        davpath.set_prefix(prefix)?;
226        Ok(davpath)
227    }
228
229    /// from request.uri
230    pub(crate) fn from_uri_and_prefix(uri: &http::uri::Uri, prefix: &str) -> Result<Self, ParseError> {
231        match uri.path() {
232            "*" => {
233                Ok(DavPath {
234                    fullpath: b"*".to_vec(),
235                    pfxlen:   None,
236                })
237            },
238            path if path.starts_with("/") => DavPath::from_str_and_prefix(path, prefix),
239            _ => Err(ParseError::InvalidPath),
240        }
241    }
242
243    /// from request.uri
244    pub fn from_uri(uri: &http::uri::Uri) -> Result<Self, ParseError> {
245        Ok(DavPath {
246            fullpath: uri.path().as_bytes().to_vec(),
247            pfxlen:   None,
248        })
249    }
250
251    /// add a slash to the end of the path (if not already present).
252    pub(crate) fn add_slash(&mut self) {
253        if !self.is_collection() {
254            self.fullpath.push(b'/');
255        }
256    }
257
258    // add a slash
259    pub(crate) fn add_slash_if(&mut self, b: bool) {
260        if b && !self.is_collection() {
261            self.fullpath.push(b'/');
262        }
263    }
264
265    /// Add a segment to the end of the path.
266    pub(crate) fn push_segment(&mut self, b: &[u8]) {
267        if !self.is_collection() {
268            self.fullpath.push(b'/');
269        }
270        self.fullpath.extend_from_slice(b);
271    }
272
273    // as URL encoded string, with prefix.
274    pub(crate) fn as_url_string_with_prefix_debug(&self) -> String {
275        let mut p = encode_path(self.get_path());
276        if self.get_prefix().len() > 0 {
277            let mut u = encode_path(self.get_prefix());
278            u.extend_from_slice(b"[");
279            u.extend_from_slice(&p);
280            u.extend_from_slice(b"]");
281            p = u;
282        }
283        std::string::String::from_utf8(p).unwrap()
284    }
285
286    // Return the prefix.
287    fn get_prefix(&self) -> &[u8] {
288        &self.fullpath[..self.pfxlen.unwrap_or(0)]
289    }
290
291    /// return the URL prefix.
292    pub fn prefix(&self) -> &str {
293        std::str::from_utf8(self.get_prefix()).unwrap()
294    }
295
296    /// Return the parent directory.
297    pub(crate) fn parent(&self) -> DavPath {
298        let mut segs = self
299            .fullpath
300            .split(|&c| c == b'/')
301            .filter(|e| e.len() > 0)
302            .collect::<Vec<&[u8]>>();
303        segs.pop();
304        if segs.len() > 0 {
305            segs.push(b"");
306        }
307        segs.insert(0, b"");
308        DavPath {
309            pfxlen:   self.pfxlen,
310            fullpath: segs.join(&b'/').to_vec(),
311        }
312    }
313}
314
315impl std::ops::Deref for DavPath {
316    type Target = DavPathRef;
317
318    fn deref(&self) -> &DavPathRef {
319        let pfxlen = self.pfxlen.unwrap_or(0);
320        DavPathRef::new(&self.fullpath[pfxlen..])
321    }
322}
323
324impl DavPathRef {
325    // NOTE: this is safe, it is what libstd does in std::path::Path::new(), see
326    // https://github.com/rust-lang/rust/blob/6700e186883a83008963d1fdba23eff2b1713e56/src/libstd/path.rs#L1788
327    fn new(path: &[u8]) -> &DavPathRef {
328        unsafe { &*(path as *const [u8] as *const DavPathRef) }
329    }
330
331    /// as raw bytes, not encoded, no prefix.
332    pub fn as_bytes(&self) -> &[u8] {
333        self.get_path()
334    }
335
336    /// as OS specific Path. never ends in "/".
337    pub fn as_pathbuf(&self) -> PathBuf {
338        let mut b = self.get_path();
339        if b.len() > 1 && b.ends_with(b"/") {
340            b = &b[..b.len() - 1];
341        }
342        let os_string = OsStr::from_bytes(b).to_owned();
343        PathBuf::from(os_string)
344    }
345
346    /// as URL encoded string, with prefix.
347    pub fn as_url_string(&self) -> String {
348        let p = encode_path(self.get_path());
349        std::string::String::from_utf8(p).unwrap()
350    }
351
352    /// is this a collection i.e. does the original URL path end in "/".
353    pub fn is_collection(&self) -> bool {
354        self.get_path().ends_with(b"/")
355    }
356
357    // non-public functions
358    //
359
360    // Return the path.
361    fn get_path(&self) -> &[u8] {
362        &self.fullpath
363    }
364
365    // is this a "star" request (only used with OPTIONS)
366    pub(crate) fn is_star(&self) -> bool {
367        self.get_path() == b"*"
368    }
369
370    /// as OS specific Path, relative (remove first slash)
371    ///
372    /// Used to `push()` onto a pathbuf.
373    pub fn as_rel_ospath(&self) -> &Path {
374        let spath = self.get_path();
375        let mut path = if spath.len() > 0 { &spath[1..] } else { spath };
376        if path.ends_with(b"/") {
377            path = &path[..path.len() - 1];
378        }
379        let os_string = OsStr::from_bytes(path);
380        Path::new(os_string)
381    }
382
383    // get parent.
384    #[allow(dead_code)]
385    pub(crate) fn parent(&self) -> &DavPathRef {
386        let path = self.get_path();
387
388        let mut end = path.len();
389        while end > 0 {
390            end -= 1;
391            if path[end] == b'/' {
392                if end == 0 {
393                    end = 1;
394                }
395                break;
396            }
397        }
398        DavPathRef::new(&path[..end])
399    }
400
401    /// The filename is the last segment of the path. Can be empty.
402    pub(crate) fn file_name(&self) -> &[u8] {
403        let segs = self
404            .get_path()
405            .split(|&c| c == b'/')
406            .filter(|e| e.len() > 0)
407            .collect::<Vec<&[u8]>>();
408        if segs.len() > 0 {
409            segs[segs.len() - 1]
410        } else {
411            b""
412        }
413    }
414
415    pub(crate) fn get_mime_type_str(&self) -> &'static str {
416        let name = self.file_name();
417        let d = name.rsplitn(2, |&c| c == b'.').collect::<Vec<&[u8]>>();
418        if d.len() > 1 {
419            if let Ok(ext) = std::str::from_utf8(d[0]) {
420                if let Some(t) = mime_guess::from_ext(ext).first_raw() {
421                    return t;
422                }
423            }
424        }
425        "application/octet-stream"
426    }
427}