fusabi_stdlib_ext/
fs.rs

1//! Filesystem module.
2//!
3//! Provides functions for filesystem operations with safety controls.
4
5use std::path::Path;
6use std::sync::Arc;
7
8use fusabi_host::ExecutionContext;
9use fusabi_host::Value;
10
11use crate::safety::SafetyConfig;
12
13/// Read a file's contents.
14pub fn read_file(
15    safety: &Arc<SafetyConfig>,
16    args: &[Value],
17    _ctx: &ExecutionContext,
18) -> fusabi_host::Result<Value> {
19    let path_str = args
20        .first()
21        .and_then(|v| v.as_str())
22        .ok_or_else(|| fusabi_host::Error::host_function("fs.read: missing path argument"))?;
23
24    let path = Path::new(path_str);
25
26    // Check safety
27    safety.paths.check_read(path).map_err(|e| {
28        fusabi_host::Error::host_function(e.to_string())
29    })?;
30
31    // Read file
32    let content = std::fs::read_to_string(path)
33        .map_err(|e| fusabi_host::Error::host_function(format!("fs.read: {}", e)))?;
34
35    Ok(Value::String(content))
36}
37
38/// Write content to a file.
39pub fn write_file(
40    safety: &Arc<SafetyConfig>,
41    args: &[Value],
42    _ctx: &ExecutionContext,
43) -> fusabi_host::Result<Value> {
44    let path_str = args
45        .first()
46        .and_then(|v| v.as_str())
47        .ok_or_else(|| fusabi_host::Error::host_function("fs.write: missing path argument"))?;
48
49    let content = args
50        .get(1)
51        .and_then(|v| v.as_str())
52        .ok_or_else(|| fusabi_host::Error::host_function("fs.write: missing content argument"))?;
53
54    let path = Path::new(path_str);
55
56    // Check safety
57    safety.paths.check_write(path).map_err(|e| {
58        fusabi_host::Error::host_function(e.to_string())
59    })?;
60
61    // Write file
62    std::fs::write(path, content)
63        .map_err(|e| fusabi_host::Error::host_function(format!("fs.write: {}", e)))?;
64
65    Ok(Value::Null)
66}
67
68/// Check if a path exists.
69pub fn exists(
70    safety: &Arc<SafetyConfig>,
71    args: &[Value],
72    _ctx: &ExecutionContext,
73) -> fusabi_host::Result<Value> {
74    let path_str = args
75        .first()
76        .and_then(|v| v.as_str())
77        .ok_or_else(|| fusabi_host::Error::host_function("fs.exists: missing path argument"))?;
78
79    let path = Path::new(path_str);
80
81    // Check safety (need read permission to check existence)
82    safety.paths.check_read(path).map_err(|e| {
83        fusabi_host::Error::host_function(e.to_string())
84    })?;
85
86    Ok(Value::Bool(path.exists()))
87}
88
89/// List directory contents.
90pub fn list_dir(
91    safety: &Arc<SafetyConfig>,
92    args: &[Value],
93    _ctx: &ExecutionContext,
94) -> fusabi_host::Result<Value> {
95    let path_str = args
96        .first()
97        .and_then(|v| v.as_str())
98        .ok_or_else(|| fusabi_host::Error::host_function("fs.list: missing path argument"))?;
99
100    let path = Path::new(path_str);
101
102    // Check safety
103    safety.paths.check_read(path).map_err(|e| {
104        fusabi_host::Error::host_function(e.to_string())
105    })?;
106
107    // List directory
108    let entries: Vec<Value> = std::fs::read_dir(path)
109        .map_err(|e| fusabi_host::Error::host_function(format!("fs.list: {}", e)))?
110        .filter_map(|entry| entry.ok())
111        .map(|entry| Value::String(entry.file_name().to_string_lossy().into_owned()))
112        .collect();
113
114    Ok(Value::List(entries))
115}
116
117/// Create a directory.
118pub fn mkdir(
119    safety: &Arc<SafetyConfig>,
120    args: &[Value],
121    _ctx: &ExecutionContext,
122) -> fusabi_host::Result<Value> {
123    let path_str = args
124        .first()
125        .and_then(|v| v.as_str())
126        .ok_or_else(|| fusabi_host::Error::host_function("fs.mkdir: missing path argument"))?;
127
128    let path = Path::new(path_str);
129
130    // Check safety
131    safety.paths.check_write(path).map_err(|e| {
132        fusabi_host::Error::host_function(e.to_string())
133    })?;
134
135    // Create directory
136    std::fs::create_dir_all(path)
137        .map_err(|e| fusabi_host::Error::host_function(format!("fs.mkdir: {}", e)))?;
138
139    Ok(Value::Null)
140}
141
142/// Remove a file or directory.
143pub fn remove(
144    safety: &Arc<SafetyConfig>,
145    args: &[Value],
146    _ctx: &ExecutionContext,
147) -> fusabi_host::Result<Value> {
148    let path_str = args
149        .first()
150        .and_then(|v| v.as_str())
151        .ok_or_else(|| fusabi_host::Error::host_function("fs.remove: missing path argument"))?;
152
153    let path = Path::new(path_str);
154
155    // Check safety
156    safety.paths.check_write(path).map_err(|e| {
157        fusabi_host::Error::host_function(e.to_string())
158    })?;
159
160    // Remove
161    if path.is_dir() {
162        std::fs::remove_dir_all(path)
163            .map_err(|e| fusabi_host::Error::host_function(format!("fs.remove: {}", e)))?;
164    } else {
165        std::fs::remove_file(path)
166            .map_err(|e| fusabi_host::Error::host_function(format!("fs.remove: {}", e)))?;
167    }
168
169    Ok(Value::Null)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use fusabi_host::Capabilities;
176    use fusabi_host::{Sandbox, SandboxConfig};
177    use fusabi_host::Limits;
178    use crate::safety::PathAllowlist;
179
180    fn create_test_ctx() -> ExecutionContext {
181        let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
182        ExecutionContext::new(1, Capabilities::none(), Limits::default(), sandbox)
183    }
184
185    #[test]
186    fn test_read_safety_check() {
187        let safety = Arc::new(SafetyConfig::strict());
188        let ctx = create_test_ctx();
189
190        let result = read_file(&safety, &[Value::String("/etc/passwd".into())], &ctx);
191        assert!(result.is_err()); // Should fail - path not allowed
192    }
193
194    #[test]
195    fn test_exists_with_permission() {
196        let safety = Arc::new(
197            SafetyConfig::new()
198                .with_paths(PathAllowlist::none().allow_read("/tmp"))
199        );
200        let ctx = create_test_ctx();
201
202        let result = exists(&safety, &[Value::String("/tmp".into())], &ctx);
203        assert!(result.is_ok());
204        assert_eq!(result.unwrap(), Value::Bool(true));
205    }
206}