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