1use std::{
2 borrow::Cow,
3 path::{Component, Path, PathBuf},
4 str::FromStr,
5};
6
7use actix_utils::future::{ready, Ready};
8use actix_web::{dev::Payload, FromRequest, HttpRequest};
9
10use crate::error::UriSegmentError;
11
12#[derive(Debug, PartialEq, Eq)]
16pub struct PathBufWrap(PathBuf);
17
18impl FromStr for PathBufWrap {
19 type Err = UriSegmentError;
20
21 fn from_str(path: &str) -> Result<Self, Self::Err> {
22 Self::parse_path(path, false)
23 }
24}
25
26impl PathBufWrap {
27 #[inline]
38 pub fn parse_unprocessed_req(
39 req: &HttpRequest,
40 hidden_files: bool,
41 ) -> Result<Self, UriSegmentError> {
42 Self::parse_path(req.match_info().unprocessed(), hidden_files)
43 }
44
45 #[inline]
54 pub fn parse_req_path(req: &HttpRequest, hidden_files: bool) -> Result<Self, UriSegmentError> {
55 Self::parse_path(req.path(), hidden_files)
56 }
57
58 pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
62 let mut buf = PathBuf::new();
63
64 let mut segment_count = path.matches('/').count() + 1;
66
67 let path = percent_encoding::percent_decode_str(path)
70 .decode_utf8()
71 .map_err(|_| UriSegmentError::NotValidUtf8)?;
72
73 if let Cow::Owned(ref path) = path {
75 if segment_count != path.matches('/').count() + 1 {
76 return Err(UriSegmentError::BadChar('/'));
77 }
78 }
79
80 for segment in path.split('/') {
81 if segment == "." {
82 return Err(UriSegmentError::BadStart('.'));
83 } else if segment == ".." {
84 segment_count -= 1;
85 buf.pop();
86 } else if !hidden_files && segment.starts_with('.') {
87 return Err(UriSegmentError::BadStart('.'));
88 } else if segment.starts_with('*') {
89 return Err(UriSegmentError::BadStart('*'));
90 } else if segment.ends_with(':') {
91 return Err(UriSegmentError::BadEnd(':'));
92 } else if segment.ends_with('>') {
93 return Err(UriSegmentError::BadEnd('>'));
94 } else if segment.ends_with('<') {
95 return Err(UriSegmentError::BadEnd('<'));
96 } else if segment.is_empty() {
97 segment_count -= 1;
98 continue;
99 } else if cfg!(windows) && segment.contains('\\') {
100 return Err(UriSegmentError::BadChar('\\'));
101 } else if cfg!(windows) && segment.contains(':') {
102 return Err(UriSegmentError::BadChar(':'));
103 } else {
104 buf.push(segment)
105 }
106 }
107
108 for (i, component) in buf.components().enumerate() {
110 assert!(
111 matches!(component, Component::Normal(_)),
112 "component `{:?}` is not normal",
113 component
114 );
115 assert!(i < segment_count);
116 }
117
118 Ok(PathBufWrap(buf))
119 }
120}
121
122impl AsRef<Path> for PathBufWrap {
123 fn as_ref(&self) -> &Path {
124 self.0.as_ref()
125 }
126}
127
128impl FromRequest for PathBufWrap {
129 type Error = UriSegmentError;
130 type Future = Ready<Result<Self, Self::Error>>;
131
132 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
133 ready(req.match_info().unprocessed().parse())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_path_buf() {
144 assert_eq!(
145 PathBufWrap::from_str("/test/.tt").map(|t| t.0),
146 Err(UriSegmentError::BadStart('.'))
147 );
148 assert_eq!(
149 PathBufWrap::from_str("/test/*tt").map(|t| t.0),
150 Err(UriSegmentError::BadStart('*'))
151 );
152 assert_eq!(
153 PathBufWrap::from_str("/test/tt:").map(|t| t.0),
154 Err(UriSegmentError::BadEnd(':'))
155 );
156 assert_eq!(
157 PathBufWrap::from_str("/test/tt<").map(|t| t.0),
158 Err(UriSegmentError::BadEnd('<'))
159 );
160 assert_eq!(
161 PathBufWrap::from_str("/test/tt>").map(|t| t.0),
162 Err(UriSegmentError::BadEnd('>'))
163 );
164 assert_eq!(
165 PathBufWrap::from_str("/seg1/seg2/").unwrap().0,
166 PathBuf::from_iter(vec!["seg1", "seg2"])
167 );
168 assert_eq!(
169 PathBufWrap::from_str("/seg1/../seg2/").unwrap().0,
170 PathBuf::from_iter(vec!["seg2"])
171 );
172 }
173
174 #[test]
175 fn test_parse_path() {
176 assert_eq!(
177 PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
178 Err(UriSegmentError::BadStart('.'))
179 );
180
181 assert_eq!(
182 PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
183 PathBuf::from_iter(vec!["test", ".tt"])
184 );
185
186 assert_eq!(
187 PathBufWrap::parse_path("/test/./file.txt", true).map(|t| t.0),
188 Err(UriSegmentError::BadStart('.'))
189 );
190 }
191
192 #[test]
193 fn path_traversal() {
194 assert_eq!(
195 PathBufWrap::parse_path("/../README.md", false).unwrap().0,
196 PathBuf::from_iter(vec!["README.md"])
197 );
198
199 assert_eq!(
200 PathBufWrap::parse_path("/../README.md", true).unwrap().0,
201 PathBuf::from_iter(vec!["README.md"])
202 );
203
204 assert_eq!(
205 PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
206 .unwrap()
207 .0,
208 PathBuf::from_iter(vec!["etc/passwd"])
209 );
210 }
211
212 #[test]
213 fn encoded_slash_is_rejected() {
214 assert_eq!(
215 PathBufWrap::parse_path("/test%2Ffile.txt", false),
216 Err(UriSegmentError::BadChar('/'))
217 );
218 }
219
220 #[test]
221 #[cfg_attr(windows, should_panic)]
222 fn windows_drive_traversal() {
223 assert_eq!(
227 PathBufWrap::parse_path("C:test.txt", false).unwrap().0,
228 PathBuf::from_iter(vec!["C:test.txt"])
229 );
230
231 assert_eq!(
232 PathBufWrap::parse_path("C:../whatever", false).unwrap().0,
233 PathBuf::from_iter(vec!["C:../whatever"])
234 );
235
236 assert_eq!(
237 PathBufWrap::parse_path(":test.txt", false).unwrap().0,
238 PathBuf::from_iter(vec![":test.txt"])
239 );
240 }
241}