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