1use std::path::{Path, PathBuf};
15
16pub fn reject_traversal(path: &Path) -> std::io::Result<PathBuf> {
26 let text = path.to_string_lossy().into_owned();
27 if text.contains("..") {
28 return Err(std::io::Error::new(
29 std::io::ErrorKind::InvalidInput,
30 "refusing filesystem path with a parent-directory (`..`) segment",
31 ));
32 }
33 Ok(PathBuf::from(text))
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39
40 #[test]
41 fn accepts_plain_relative_path() {
42 let p = Path::new("out/web/report.html");
43 let got = reject_traversal(p).expect("plain path is accepted");
44 assert_eq!(got, PathBuf::from("out/web/report.html"));
45 }
46
47 #[test]
48 fn accepts_absolute_path() {
49 let p = Path::new("/var/lib/oxide-sloc/run.json");
50 let got = reject_traversal(p).expect("absolute path is accepted");
51 assert_eq!(got, PathBuf::from("/var/lib/oxide-sloc/run.json"));
52 }
53
54 #[test]
55 fn accepts_name_with_non_adjacent_dots() {
56 let p = Path::new("out/web/report.final.html");
58 assert!(reject_traversal(p).is_ok());
59 }
60
61 #[test]
62 fn rejects_unix_traversal() {
63 let p = Path::new("out/../../etc/passwd");
64 assert!(reject_traversal(p).is_err());
65 }
66
67 #[test]
68 fn rejects_windows_traversal() {
69 let p = Path::new(r"out\..\..\secret");
70 assert!(reject_traversal(p).is_err());
71 }
72
73 #[test]
74 fn rejects_leading_traversal() {
75 assert!(reject_traversal(Path::new("../escape")).is_err());
76 }
77
78 #[test]
79 fn error_kind_is_invalid_input() {
80 let err = reject_traversal(Path::new("a/../b")).unwrap_err();
81 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
82 }
83}