1use std::path::{Path, PathBuf};
7
8#[derive(Debug)]
10pub enum PathError {
11 FileNotFound(String),
13 InvalidPath(String),
15 NotAFile(String),
17 IoError(String),
19}
20
21impl std::fmt::Display for PathError {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 PathError::FileNotFound(msg) => write!(f, "File not found: {}", msg),
25 PathError::InvalidPath(msg) => write!(f, "Invalid path: {}", msg),
26 PathError::NotAFile(msg) => write!(f, "Not a file: {}", msg),
27 PathError::IoError(msg) => write!(f, "I/O error: {}", msg),
28 }
29 }
30}
31
32impl std::error::Error for PathError {}
33
34pub fn normalize_file_path(file_path: &str) -> Result<PathBuf, PathError> {
64 if file_path.contains('\0') {
66 return Err(PathError::InvalidPath(
67 "Null bytes not allowed in file path".to_string(),
68 ));
69 }
70
71 let dangerous_chars = ['|', '&', ';', '`', '$', '<', '>', '\n', '\r'];
74
75 if file_path.chars().any(|c| dangerous_chars.contains(&c)) {
76 return Err(PathError::InvalidPath(
77 "File path contains dangerous characters".to_string(),
78 ));
79 }
80
81 let path = Path::new(file_path);
84
85 let canonical_path = path.canonicalize().map_err(|e| {
88 PathError::FileNotFound(format!(
89 "Failed to resolve path '{}': {}",
90 file_path, e
91 ))
92 })?;
93
94 if !canonical_path.is_file() {
96 return Err(PathError::NotAFile("Path is not a regular file".to_string()));
97 }
98
99 Ok(canonical_path)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use std::fs::File;
106 use std::io::Write;
107
108 #[test]
109 fn test_normalize_file_path_with_null_bytes() {
110 let result = normalize_file_path("test\0file.txt");
111 assert!(matches!(result, Err(PathError::InvalidPath(_))));
112 }
113
114 #[test]
115 fn test_normalize_file_path_with_dangerous_chars() {
116 let dangerous_paths = [
117 "test|file.txt",
118 "test&file.txt",
119 "test;file.txt",
120 "test`file.txt",
121 "test$file.txt",
122 "test<file>.txt",
123 ];
124
125 for dangerous_path in dangerous_paths {
126 let result = normalize_file_path(dangerous_path);
127 assert!(
128 matches!(result, Err(PathError::InvalidPath(_))),
129 "Should reject dangerous path: {}",
130 dangerous_path
131 );
132 }
133 }
134
135 #[test]
136 fn test_normalize_file_path_nonexistent_file() {
137 let result = normalize_file_path("definitely_nonexistent_file.txt");
138 assert!(matches!(result, Err(PathError::FileNotFound(_))));
139 }
140
141 #[test]
142 fn test_normalize_file_path_directory() {
143 let result = normalize_file_path(".");
144 assert!(matches!(result, Err(PathError::NotAFile(_))));
145 }
146
147 #[test]
148 fn test_normalize_file_path_success() -> Result<(), Box<dyn std::error::Error>>
149 {
150 let temp_file = std::env::temp_dir().join("hygg_test_file.txt");
152 {
153 let mut file = File::create(&temp_file)?;
154 file.write_all(b"test content")?;
155 }
156
157 let result = normalize_file_path(temp_file.to_str().unwrap());
159 assert!(result.is_ok());
160
161 std::fs::remove_file(&temp_file)?;
163 Ok(())
164 }
165
166 #[test]
167 fn test_path_error_display() {
168 let file_error = PathError::FileNotFound("test.txt".to_string());
169 assert_eq!(format!("{}", file_error), "File not found: test.txt");
170
171 let invalid_error = PathError::InvalidPath("Bad path".to_string());
172 assert_eq!(format!("{}", invalid_error), "Invalid path: Bad path");
173
174 let not_file_error = PathError::NotAFile("is directory".to_string());
175 assert_eq!(format!("{}", not_file_error), "Not a file: is directory");
176
177 let io_error = PathError::IoError("permission denied".to_string());
178 assert_eq!(format!("{}", io_error), "I/O error: permission denied");
179 }
180}