capsule_core/wasm/utilities/
path_validator.rs1use std::error::Error;
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
6pub enum FileAccessMode {
7 ReadOnly,
8
9 #[default]
10 ReadWrite,
11}
12
13#[derive(Debug)]
14pub struct ParsedPath {
15 pub path: PathBuf,
16 pub guest_path: String,
17 pub mode: FileAccessMode,
18}
19
20#[derive(Debug)]
21pub enum PathValidationError {
22 AbsolutePathNotAllowed(String),
23 EscapesProjectDirectory(String),
24 PathNotFound(String),
25 InvalidMode(String),
26}
27
28impl fmt::Display for PathValidationError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 PathValidationError::AbsolutePathNotAllowed(path) => {
32 write!(f, "Absolute paths are not allowed: {}", path)
33 }
34 PathValidationError::EscapesProjectDirectory(path) => {
35 write!(f, "Path escapes project directory: {}", path)
36 }
37 PathValidationError::PathNotFound(path) => {
38 write!(f, "Path does not exist: {}", path)
39 }
40 PathValidationError::InvalidMode(mode) => {
41 write!(
42 f,
43 "Invalid access mode '{}'. Use :ro (read-only) or :rw (read-write)",
44 mode
45 )
46 }
47 }
48 }
49}
50
51impl Error for PathValidationError {}
52
53fn parse_path_with_mode(path_spec: &str) -> (String, FileAccessMode) {
54 if let Some(pos) = path_spec.rfind(':') {
55 let (path, mode_str) = path_spec.split_at(pos);
56 let mode = &mode_str[1..];
57
58 match mode {
59 "ro" => (path.to_string(), FileAccessMode::ReadOnly),
60 "rw" => (path.to_string(), FileAccessMode::ReadWrite),
61 _ => (path_spec.to_string(), FileAccessMode::default()),
62 }
63 } else {
64 (path_spec.to_string(), FileAccessMode::default())
65 }
66}
67
68pub fn validate_path(
69 path_spec: &str,
70 project_root: &Path,
71) -> Result<ParsedPath, PathValidationError> {
72 let (path_str, mode) = parse_path_with_mode(path_spec);
73 let p = Path::new(&path_str);
74
75 if p.is_absolute() {
76 return Err(PathValidationError::AbsolutePathNotAllowed(path_str));
77 }
78
79 let joined = project_root.join(p);
80 let resolved = joined
81 .canonicalize()
82 .map_err(|_| PathValidationError::PathNotFound(path_str.clone()))?;
83
84 let canonical_root = project_root
85 .canonicalize()
86 .map_err(|_| PathValidationError::EscapesProjectDirectory(path_str.clone()))?;
87
88 if !resolved.starts_with(&canonical_root) {
89 return Err(PathValidationError::EscapesProjectDirectory(path_str));
90 }
91
92 Ok(ParsedPath {
93 path: resolved,
94 guest_path: path_str,
95 mode,
96 })
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::fs;
103
104 #[test]
105 fn test_absolute_path_rejected() {
106 let temp = std::env::temp_dir();
107
108 let result = validate_path("/absolute/path", &temp);
109 assert!(matches!(
110 result,
111 Err(PathValidationError::AbsolutePathNotAllowed(_))
112 ));
113 }
114
115 #[test]
116 fn test_relative_path_works() {
117 let current = std::env::current_dir().unwrap();
118
119 let test_dir = current.join(".capsule_test");
120 let _ = fs::create_dir(&test_dir);
121
122 let result = validate_path("./.capsule_test", ¤t);
123
124 let _ = fs::remove_dir(&test_dir);
125
126 assert!(result.is_ok());
127 let parsed = result.unwrap();
128 assert_eq!(parsed.guest_path, "./.capsule_test");
129 }
130
131 #[test]
132 fn test_non_existent_path_fails() {
133 let current = std::env::current_dir().unwrap();
134
135 let result = validate_path("./nonexistent_dir", ¤t);
136 assert!(matches!(result, Err(PathValidationError::PathNotFound(_))));
137 }
138
139 #[test]
140 fn test_escape_project_root_rejected() {
141 let temp = std::env::temp_dir();
142 let subdir = temp.join("test_subdir");
143 let _ = fs::create_dir(&subdir);
144
145 let result = validate_path("../", &subdir);
146
147 let _ = fs::remove_dir(&subdir);
148
149 assert!(matches!(
150 result,
151 Err(PathValidationError::EscapesProjectDirectory(_))
152 ));
153 }
154
155 #[test]
156 fn test_parse_mode_readonly() {
157 let (path, mode) = parse_path_with_mode("./data:ro");
158 assert_eq!(path, "./data");
159 assert_eq!(mode, FileAccessMode::ReadOnly);
160 }
161
162 #[test]
163 fn test_parse_mode_readwrite() {
164 let (path, mode) = parse_path_with_mode("./output:rw");
165 assert_eq!(path, "./output");
166 assert_eq!(mode, FileAccessMode::ReadWrite);
167 }
168
169 #[test]
170 fn test_parse_mode_default() {
171 let (path, mode) = parse_path_with_mode("./data");
172 assert_eq!(path, "./data");
173 assert_eq!(mode, FileAccessMode::ReadWrite);
174 }
175}