graphify_security/
path_validator.rs1use std::path::{Path, PathBuf};
4
5use crate::SecurityError;
6
7pub fn safe_path(path: &Path, allowed_root: &Path) -> Result<PathBuf, SecurityError> {
12 let canonical = path
13 .canonicalize()
14 .map_err(|_| SecurityError::PathTraversal(path.to_string_lossy().to_string()))?;
15 let root = allowed_root
16 .canonicalize()
17 .map_err(|_| SecurityError::PathTraversal(allowed_root.to_string_lossy().to_string()))?;
18
19 if canonical.starts_with(&root) {
20 Ok(canonical)
21 } else {
22 Err(SecurityError::PathTraversal(
23 path.to_string_lossy().to_string(),
24 ))
25 }
26}
27
28pub fn validate_graph_path(path: &str) -> Result<PathBuf, SecurityError> {
30 let p = PathBuf::from(path);
31 if p.extension().and_then(|e| e.to_str()) != Some("json") {
32 return Err(SecurityError::InvalidPath(
33 "graph file must be .json".into(),
34 ));
35 }
36 Ok(p)
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use std::fs;
43
44 #[test]
45 fn test_safe_path_within_root() {
46 let dir = std::env::temp_dir().join("graphify_security_test_safe");
47 let _ = fs::create_dir_all(&dir);
48 let file = dir.join("test.json");
49 fs::write(&file, "{}").unwrap();
50
51 let result = safe_path(&file, &dir);
52 assert!(result.is_ok());
53
54 let _ = fs::remove_file(&file);
55 let _ = fs::remove_dir(&dir);
56 }
57
58 #[test]
59 fn test_safe_path_traversal_blocked() {
60 let dir = std::env::temp_dir().join("graphify_security_test_traversal");
62 let sub = dir.join("sub");
63 let _ = fs::create_dir_all(&sub);
64 let file = dir.join("secret.txt");
66 fs::write(&file, "secret").unwrap();
67
68 let traversal = sub.join("../secret.txt");
70 let result = safe_path(&traversal, &sub);
71 assert!(matches!(result, Err(SecurityError::PathTraversal(_))));
72
73 let _ = fs::remove_file(&file);
74 let _ = fs::remove_dir(&sub);
75 let _ = fs::remove_dir(&dir);
76 }
77
78 #[test]
79 fn test_safe_path_nonexistent_file() {
80 let result = safe_path(Path::new("/nonexistent/path/file.txt"), Path::new("/tmp"));
81 assert!(matches!(result, Err(SecurityError::PathTraversal(_))));
82 }
83
84 #[test]
85 fn test_validate_graph_path_json() {
86 let result = validate_graph_path("output/graph.json");
87 assert!(result.is_ok());
88 assert_eq!(result.unwrap(), PathBuf::from("output/graph.json"));
89 }
90
91 #[test]
92 fn test_validate_graph_path_non_json() {
93 let result = validate_graph_path("output/graph.xml");
94 assert!(matches!(result, Err(SecurityError::InvalidPath(_))));
95 }
96
97 #[test]
98 fn test_validate_graph_path_no_extension() {
99 let result = validate_graph_path("output/graph");
100 assert!(matches!(result, Err(SecurityError::InvalidPath(_))));
101 }
102
103 #[test]
104 fn test_validate_graph_path_dot_json_in_middle() {
105 let result = validate_graph_path("foo.json.bak");
107 assert!(matches!(result, Err(SecurityError::InvalidPath(_))));
108 }
109}