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