1use std::fs;
4use std::io::Read;
5use std::path::Path;
6
7use base64::Engine;
8use serde::Serialize;
9use serde_json::Value;
10
11use crate::sandbox::{
12 atomic_write, is_dir_allowed, is_path_allowed, safe_lock, validate_path, SharedList,
13 DEFAULT_MAX_ALLOWED_PATHS, DEFAULT_MAX_FILE_SIZE,
14};
15
16pub struct FsState {
17 pub allowed_paths: SharedList,
18 pub allowed_dirs: SharedList,
19}
20
21fn arg_str<'a>(args: &'a Value, key: &str) -> Result<&'a str, String> {
22 args.get(key)
23 .and_then(|v| v.as_str())
24 .ok_or_else(|| format!("missing arg: {}", key))
25}
26
27pub fn allow_path(state: &FsState, args: &Value) -> Result<Value, String> {
28 let path = arg_str(args, "path")?;
29 validate_path(path)?;
30 let canonical = fs::canonicalize(path)
31 .map_err(|_| "Invalid file path".to_string())?
32 .to_string_lossy()
33 .to_string();
34 let mut paths = safe_lock(&state.allowed_paths);
35 if paths.len() >= DEFAULT_MAX_ALLOWED_PATHS {
36 return Err("Too many allowed paths".to_string());
37 }
38 if !paths.contains(&canonical) {
39 paths.push(canonical);
40 }
41 Ok(Value::Null)
42}
43
44pub fn allow_dir(state: &FsState, args: &Value) -> Result<Value, String> {
45 let path = arg_str(args, "path")?;
46 validate_path(path)?;
47 let canonical = fs::canonicalize(path)
48 .map_err(|_| "Invalid directory path".to_string())?
49 .to_string_lossy()
50 .to_string();
51 let mut dirs = safe_lock(&state.allowed_dirs);
52 if dirs.len() >= DEFAULT_MAX_ALLOWED_PATHS {
53 return Err("Too many allowed directories".to_string());
54 }
55 if !dirs.contains(&canonical) {
56 dirs.push(canonical);
57 }
58 Ok(Value::Null)
59}
60
61#[derive(Serialize)]
62struct DirEntry {
63 name: String,
64 path: String,
65 is_dir: bool,
66}
67
68pub fn list_directory(state: &FsState, args: &Value) -> Result<Value, String> {
69 let path = arg_str(args, "path")?;
70 validate_path(path)?;
71 is_dir_allowed(path, &state.allowed_dirs)?;
72
73 let entries = fs::read_dir(path).map_err(|e| format!("Cannot read directory: {}", e))?;
74 let mut result: Vec<DirEntry> = Vec::new();
75 for entry in entries {
76 let entry = entry.map_err(|e| format!("Cannot read entry: {}", e))?;
77 let metadata = entry
78 .metadata()
79 .map_err(|e| format!("Cannot read metadata: {}", e))?;
80 let name = entry.file_name().to_string_lossy().to_string();
81 if name.starts_with('.') {
82 continue;
83 }
84 result.push(DirEntry {
85 name,
86 path: entry.path().to_string_lossy().to_string(),
87 is_dir: metadata.is_dir(),
88 });
89 }
90 serde_json::to_value(&result).map_err(|e| e.to_string())
91}
92
93pub fn read_file(state: &FsState, args: &Value) -> Result<Value, String> {
94 let path = arg_str(args, "path")?;
95 validate_path(path)?;
96 let canonical = is_path_allowed(path, &state.allowed_paths, &state.allowed_dirs)?;
97
98 let metadata = fs::metadata(&canonical).map_err(|_| "Cannot read file".to_string())?;
99 if metadata.len() > DEFAULT_MAX_FILE_SIZE {
100 return Err(format!(
101 "File too large: {} bytes (max {})",
102 metadata.len(),
103 DEFAULT_MAX_FILE_SIZE
104 ));
105 }
106 let mut buf = String::with_capacity(metadata.len() as usize);
107 fs::File::open(&canonical)
108 .map_err(|_| "Cannot read file".to_string())?
109 .take(DEFAULT_MAX_FILE_SIZE + 1)
110 .read_to_string(&mut buf)
111 .map_err(|_| "Cannot read file".to_string())?;
112 if buf.len() as u64 > DEFAULT_MAX_FILE_SIZE {
113 return Err(format!(
114 "File too large: exceeds {} bytes",
115 DEFAULT_MAX_FILE_SIZE
116 ));
117 }
118 Ok(Value::from(buf))
119}
120
121pub fn read_file_binary(state: &FsState, args: &Value) -> Result<Value, String> {
122 let path = arg_str(args, "path")?;
123 validate_path(path)?;
124 let canonical = is_path_allowed(path, &state.allowed_paths, &state.allowed_dirs)?;
125
126 let metadata = fs::metadata(&canonical).map_err(|_| "Cannot read file".to_string())?;
127 if metadata.len() > DEFAULT_MAX_FILE_SIZE {
128 return Err(format!(
129 "File too large: {} bytes (max {})",
130 metadata.len(),
131 DEFAULT_MAX_FILE_SIZE
132 ));
133 }
134 let mut bytes = Vec::with_capacity(metadata.len() as usize);
135 fs::File::open(&canonical)
136 .map_err(|_| "Cannot read file".to_string())?
137 .take(DEFAULT_MAX_FILE_SIZE + 1)
138 .read_to_end(&mut bytes)
139 .map_err(|_| "Cannot read file".to_string())?;
140 if bytes.len() as u64 > DEFAULT_MAX_FILE_SIZE {
141 return Err(format!(
142 "File too large: exceeds {} bytes",
143 DEFAULT_MAX_FILE_SIZE
144 ));
145 }
146 Ok(Value::from(
147 base64::engine::general_purpose::STANDARD.encode(&bytes),
148 ))
149}
150
151pub fn write_file(state: &FsState, args: &Value) -> Result<Value, String> {
152 let path = arg_str(args, "path")?;
153 let content = arg_str(args, "content")?;
154 validate_path(path)?;
155 let canonical = is_path_allowed(path, &state.allowed_paths, &state.allowed_dirs)?;
156 if content.len() as u64 > DEFAULT_MAX_FILE_SIZE {
157 return Err(format!(
158 "Content too large: {} bytes (max {})",
159 content.len(),
160 DEFAULT_MAX_FILE_SIZE
161 ));
162 }
163 atomic_write(Path::new(&canonical), content.as_bytes())?;
164 Ok(Value::Null)
165}
166
167pub fn write_file_binary(state: &FsState, args: &Value) -> Result<Value, String> {
168 let path = arg_str(args, "path")?;
169 let data = arg_str(args, "data")?;
170 validate_path(path)?;
171 let parent = Path::new(path)
172 .parent()
173 .ok_or("Invalid file path")?
174 .to_string_lossy()
175 .to_string();
176 let canonical_parent = is_dir_allowed(&parent, &state.allowed_dirs)?;
177 let bytes = base64::engine::general_purpose::STANDARD
178 .decode(data)
179 .map_err(|e| format!("base64 decode error: {}", e))?;
180 if bytes.len() as u64 > DEFAULT_MAX_FILE_SIZE {
181 return Err(format!(
182 "File too large: {} bytes (max {})",
183 bytes.len(),
184 DEFAULT_MAX_FILE_SIZE
185 ));
186 }
187 let filename = Path::new(path).file_name().ok_or("Invalid file name")?;
188 let canonical_path = Path::new(&canonical_parent).join(filename);
189 if let Ok(meta) = fs::symlink_metadata(&canonical_path) {
190 if meta.file_type().is_symlink() {
191 return Err("Write rejected: target is a symlink".to_string());
192 }
193 }
194 atomic_write(&canonical_path, &bytes)?;
195 let final_canonical = fs::canonicalize(&canonical_path)
196 .map_err(|e| format!("Cannot resolve written file: {}", e))?;
197 validate_path(&final_canonical.to_string_lossy())?;
198 if !final_canonical.starts_with(Path::new(&canonical_parent)) {
199 let _ = fs::remove_file(&canonical_path);
200 return Err("Write rejected: symlink escape detected".to_string());
201 }
202 Ok(Value::Null)
203}
204
205pub fn ensure_dir(state: &FsState, args: &Value) -> Result<Value, String> {
206 let path = arg_str(args, "path")?;
207 validate_path(path)?;
208 let canonical_parent = is_dir_allowed(path, &state.allowed_dirs).or_else(|_| {
209 let parent = Path::new(path)
210 .parent()
211 .ok_or("Invalid path")?
212 .to_string_lossy()
213 .to_string();
214 is_dir_allowed(&parent, &state.allowed_dirs)
215 })?;
216 let dir_name = Path::new(path)
217 .file_name()
218 .ok_or("Invalid directory name")?;
219 let target = Path::new(&canonical_parent).join(dir_name);
220 fs::create_dir_all(&target).map_err(|e| format!("Cannot create directory: {}", e))?;
221 let canonical_target = fs::canonicalize(&target)
222 .map_err(|e| format!("Cannot resolve created directory: {}", e))?;
223 validate_path(&canonical_target.to_string_lossy())?;
224 Ok(Value::Null)
225}