1use std::path::{Path, PathBuf};
4use std::error::Error;
5use std::fmt;
6#[cfg(unix)]
7use std::ffi::OsStr;
8#[cfg(unix)]
9use std::os::unix::ffi::OsStrExt;
10
11use percent_encoding_lite::Bitmask;
12
13use crate::core::{HttpError, HttpErrorType};
14use crate::reqres::StatusCode;
15
16pub fn sanitize(route: &str) -> Result<PathBuf, DangerousPathError> {
20 let decoded = percent_encoding_lite::decode(route);
21 #[cfg(windows)]
25 return sanitize_win(str::from_utf8(&decoded).map_err(|_| DangerousPathError::InvalidCharacters)?);
26 #[cfg(unix)]
27 return sanitize_unix(&decoded);
28 #[cfg(target_os = "cygwin")] {
29 not_implemented("This function assumes unix paths on unix targets. Cygwin uses windows paths and so this code has to be refactored to support it");
30 }
31 #[cfg(not(any(windows, unix)))]
32 not_implemented
33}
34
35#[cfg(any(unix, test))]
37fn sanitize_unix(route: &[u8]) -> Result<PathBuf, DangerousPathError> {
38 if route.contains(&0) { return Err(DangerousPathError::InvalidCharacters); }
39
40 let mut out = PathBuf::new();
41 for segment in route.split(|&c| c == b'/') {
42 if segment.is_empty() || segment == b"." { continue; }
43 if segment == b".." { return Err(DangerousPathError::DangerousPath); }
44 out.push(OsStr::from_bytes(segment));
45 }
46
47 if out.as_os_str().is_empty() { out.push("."); }
48 Ok(out)
49}
50
51#[cfg(any(windows, test))]
52fn sanitize_win(route: &str) -> Result<PathBuf, DangerousPathError> {
53 if route.contains(|c| c < ' ') { return Err(DangerousPathError::InvalidCharacters); }
54 const INVALID_CHARS: &[char] = &['<', '>', ':', '"', '|', '?', '*'];
57 if route.contains(INVALID_CHARS) { return Err(DangerousPathError::InvalidCharacters); }
58
59 let mut out = PathBuf::new();
60 for segment in route.split(['/', '\\']) {
61 if segment.is_empty() || segment == "." { continue; }
62 if segment == ".." { return Err(DangerousPathError::DangerousPath); }
63 out.push(segment);
64 }
65
66 if out.as_os_str().is_empty() { out.push("."); }
67 Ok(out)
68}
69
70#[derive(Debug, Clone, PartialEq)]
71#[non_exhaustive]
72pub enum DangerousPathError {
73 DangerousPath,
75 InvalidCharacters,
79}
80
81impl fmt::Display for DangerousPathError {
82 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83 match self {
84 DangerousPathError::DangerousPath => f.write_str("path contains `..` or drive letters"),
85 DangerousPathError::InvalidCharacters => f.write_str("path contains forbidden characters"),
86 }
87 }
88}
89
90impl Error for DangerousPathError {}
91impl HttpError for DangerousPathError {
92 fn error_type(&self) -> HttpErrorType { HttpErrorType::Hidden }
93 fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST }
94}
95
96pub fn encode(path: &Path) -> String {
98 #[cfg(windows)]
99 return percent_encoding_lite::encode(&path.to_string_lossy(), Bitmask::PATH);
100 #[cfg(unix)]
101 return percent_encoding_lite::encode(path.as_os_str().as_bytes(), Bitmask::PATH);
102 #[cfg(not(any(windows, unix)))]
103 not_implemented
104}
105
106#[cfg(test)]
108mod tests {
109 use super::{sanitize_win, sanitize_unix, DangerousPathError};
110 use DangerousPathError::*;
111 #[test]
112 fn win() {
113 assert_eq!(sanitize_win("/.."), Err(DangerousPath));
114 assert_eq!(sanitize_win("/\\..\\"), Err(DangerousPath));
115 assert_eq!(sanitize_win("Dir\\.."), Err(DangerousPath));
116 assert_eq!(sanitize_win("/C:/Windows"), Err(InvalidCharacters));
117 assert_eq!(sanitize_win("/C:\\Windows"), Err(InvalidCharacters));
118 assert_eq!(sanitize_win("C:file.txt"), Err(InvalidCharacters));
119 assert_eq!(sanitize_win("/\\\\?\\"), Err(InvalidCharacters));
120 assert_eq!(sanitize_win("/\0"), Err(InvalidCharacters));
121 assert!(sanitize_win("/status.json").is_ok());
122 assert!(sanitize_win("/files/examples/fileserver.rs").is_ok());
123 assert!(sanitize_win("/F1SHMSOaYAA1M2G.jpeg").is_ok());
124 }
125 #[test]
126 fn unix() {
127 assert_eq!(sanitize_unix(b"/.."), Err(DangerousPath));
128 assert_eq!(sanitize_unix(b"../"), Err(DangerousPath));
129 assert_eq!(sanitize_unix(b".."), Err(DangerousPath));
130 assert_eq!(sanitize_unix(b"/dir/.."), Err(DangerousPath));
131 assert_eq!(sanitize_unix(b"/\0"), Err(InvalidCharacters));
132 assert_eq!(sanitize_unix(b"/nulls\0instead\0of\0spaces"), Err(InvalidCharacters));
133 assert!(sanitize_unix(b"<>:\"/\\|?*").is_ok());
134 assert!(sanitize_unix(b"/just/a/path").is_ok());
135 assert!(sanitize_unix(b"/dev/sda").is_ok());
136 assert!(sanitize_unix(b"\\..\\This is a filename").is_ok());
137 assert!(sanitize_unix(b"/C:/Windows").is_ok());
138 }
139}