1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
12use crate::stdlib::runtime_policy::{FileSystemProvider, RealFileSystem};
13use shape_value::ValueWord;
14use std::path::Path;
15use std::sync::Arc;
16
17pub fn create_file_module_with_provider(fs: Arc<dyn FileSystemProvider>) -> ModuleExports {
21 let mut module = ModuleExports::new("file");
22 module.description = "High-level filesystem operations".to_string();
23
24 {
26 let fs = Arc::clone(&fs);
27 module.add_function_with_schema(
28 "read_text",
29 move |args: &[ValueWord], _ctx: &ModuleContext| {
30 let path_str = args
31 .first()
32 .and_then(|a| a.as_str())
33 .ok_or_else(|| "file.read_text() requires a path string".to_string())?;
34
35 let bytes = fs
36 .read(Path::new(path_str))
37 .map_err(|e| format!("file.read_text() failed: {}", e))?;
38
39 let text = String::from_utf8(bytes)
40 .map_err(|e| format!("file.read_text() invalid UTF-8: {}", e))?;
41
42 Ok(ValueWord::from_ok(ValueWord::from_string(Arc::new(text))))
43 },
44 ModuleFunction {
45 description: "Read the entire contents of a file as a UTF-8 string".to_string(),
46 params: vec![ModuleParam {
47 name: "path".to_string(),
48 type_name: "string".to_string(),
49 required: true,
50 description: "Path to the file".to_string(),
51 ..Default::default()
52 }],
53 return_type: Some("Result<string>".to_string()),
54 },
55 );
56 }
57
58 {
60 let fs = Arc::clone(&fs);
61 module.add_function_with_schema(
62 "write_text",
63 move |args: &[ValueWord], _ctx: &ModuleContext| {
64 let path_str = args
65 .first()
66 .and_then(|a| a.as_str())
67 .ok_or_else(|| "file.write_text() requires a path string".to_string())?;
68
69 let content = args
70 .get(1)
71 .and_then(|a| a.as_str())
72 .ok_or_else(|| "file.write_text() requires a content string".to_string())?;
73
74 fs.write(Path::new(path_str), content.as_bytes())
75 .map_err(|e| format!("file.write_text() failed: {}", e))?;
76
77 Ok(ValueWord::from_ok(ValueWord::unit()))
78 },
79 ModuleFunction {
80 description: "Write a string to a file, creating or truncating it".to_string(),
81 params: vec![
82 ModuleParam {
83 name: "path".to_string(),
84 type_name: "string".to_string(),
85 required: true,
86 description: "Path to the file".to_string(),
87 ..Default::default()
88 },
89 ModuleParam {
90 name: "content".to_string(),
91 type_name: "string".to_string(),
92 required: true,
93 description: "Text content to write".to_string(),
94 ..Default::default()
95 },
96 ],
97 return_type: Some("Result<unit>".to_string()),
98 },
99 );
100 }
101
102 {
104 let fs = Arc::clone(&fs);
105 module.add_function_with_schema(
106 "read_lines",
107 move |args: &[ValueWord], _ctx: &ModuleContext| {
108 let path_str = args
109 .first()
110 .and_then(|a| a.as_str())
111 .ok_or_else(|| "file.read_lines() requires a path string".to_string())?;
112
113 let bytes = fs
114 .read(Path::new(path_str))
115 .map_err(|e| format!("file.read_lines() failed: {}", e))?;
116
117 let text = String::from_utf8(bytes)
118 .map_err(|e| format!("file.read_lines() invalid UTF-8: {}", e))?;
119
120 let lines: Vec<ValueWord> = text
121 .lines()
122 .map(|l| ValueWord::from_string(Arc::new(l.to_string())))
123 .collect();
124
125 Ok(ValueWord::from_ok(ValueWord::from_array(Arc::new(lines))))
126 },
127 ModuleFunction {
128 description: "Read a file and return its lines as an array of strings".to_string(),
129 params: vec![ModuleParam {
130 name: "path".to_string(),
131 type_name: "string".to_string(),
132 required: true,
133 description: "Path to the file".to_string(),
134 ..Default::default()
135 }],
136 return_type: Some("Result<Array<string>>".to_string()),
137 },
138 );
139 }
140
141 {
143 let fs = Arc::clone(&fs);
144 module.add_function_with_schema(
145 "append",
146 move |args: &[ValueWord], _ctx: &ModuleContext| {
147 let path_str = args
148 .first()
149 .and_then(|a| a.as_str())
150 .ok_or_else(|| "file.append() requires a path string".to_string())?;
151
152 let content = args
153 .get(1)
154 .and_then(|a| a.as_str())
155 .ok_or_else(|| "file.append() requires a content string".to_string())?;
156
157 fs.append(Path::new(path_str), content.as_bytes())
158 .map_err(|e| format!("file.append() failed: {}", e))?;
159
160 Ok(ValueWord::from_ok(ValueWord::unit()))
161 },
162 ModuleFunction {
163 description: "Append a string to a file, creating it if it does not exist"
164 .to_string(),
165 params: vec![
166 ModuleParam {
167 name: "path".to_string(),
168 type_name: "string".to_string(),
169 required: true,
170 description: "Path to the file".to_string(),
171 ..Default::default()
172 },
173 ModuleParam {
174 name: "content".to_string(),
175 type_name: "string".to_string(),
176 required: true,
177 description: "Text content to append".to_string(),
178 ..Default::default()
179 },
180 ],
181 return_type: Some("Result<unit>".to_string()),
182 },
183 );
184 }
185
186 {
188 let fs = Arc::clone(&fs);
189 module.add_function_with_schema(
190 "read_bytes",
191 move |args: &[ValueWord], _ctx: &ModuleContext| {
192 let path_str = args
193 .first()
194 .and_then(|a| a.as_str())
195 .ok_or_else(|| "file.read_bytes() requires a path string".to_string())?;
196
197 let bytes = fs
198 .read(Path::new(path_str))
199 .map_err(|e| format!("file.read_bytes() failed: {}", e))?;
200
201 let arr: Vec<ValueWord> = bytes
202 .iter()
203 .map(|&b| ValueWord::from_f64(b as f64))
204 .collect();
205
206 Ok(ValueWord::from_ok(ValueWord::from_array(Arc::new(arr))))
207 },
208 ModuleFunction {
209 description: "Read the entire contents of a file as an array of byte values"
210 .to_string(),
211 params: vec![ModuleParam {
212 name: "path".to_string(),
213 type_name: "string".to_string(),
214 required: true,
215 description: "Path to the file".to_string(),
216 ..Default::default()
217 }],
218 return_type: Some("Result<Array<number>>".to_string()),
219 },
220 );
221 }
222
223 {
225 let fs = Arc::clone(&fs);
226 module.add_function_with_schema(
227 "write_bytes",
228 move |args: &[ValueWord], _ctx: &ModuleContext| {
229 let path_str = args
230 .first()
231 .and_then(|a| a.as_str())
232 .ok_or_else(|| "file.write_bytes() requires a path string".to_string())?;
233
234 let arr = args
235 .get(1)
236 .and_then(|a| a.as_any_array())
237 .ok_or_else(|| "file.write_bytes() requires a data array".to_string())?
238 .to_generic();
239
240 let bytes: Vec<u8> = arr
241 .iter()
242 .enumerate()
243 .map(|(i, nb)| {
244 let n = nb.as_number_coerce().ok_or_else(|| {
245 format!("file.write_bytes() element {} is not a number", i)
246 })?;
247 if n < 0.0 || n > 255.0 || n.fract() != 0.0 {
248 return Err(format!(
249 "file.write_bytes() element {} is not a valid byte (0-255): {}",
250 i, n
251 ));
252 }
253 Ok(n as u8)
254 })
255 .collect::<Result<Vec<u8>, String>>()?;
256
257 fs.write(Path::new(path_str), &bytes)
258 .map_err(|e| format!("file.write_bytes() failed: {}", e))?;
259
260 Ok(ValueWord::from_ok(ValueWord::unit()))
261 },
262 ModuleFunction {
263 description: "Write an array of byte values to a file".to_string(),
264 params: vec![
265 ModuleParam {
266 name: "path".to_string(),
267 type_name: "string".to_string(),
268 required: true,
269 description: "Path to the file".to_string(),
270 ..Default::default()
271 },
272 ModuleParam {
273 name: "data".to_string(),
274 type_name: "Array<number>".to_string(),
275 required: true,
276 description: "Array of byte values (0-255)".to_string(),
277 ..Default::default()
278 },
279 ],
280 return_type: Some("Result<unit>".to_string()),
281 },
282 );
283 }
284
285 module
286}
287
288pub fn create_file_module() -> ModuleExports {
290 create_file_module_with_provider(Arc::new(RealFileSystem))
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
298 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
299 crate::module_exports::ModuleContext {
300 schemas: registry,
301 invoke_callable: None,
302 raw_invoker: None,
303 function_hashes: None,
304 vm_state: None,
305 granted_permissions: None,
306 scope_constraints: None,
307 set_pending_resume: None,
308 set_pending_frame_resume: None,
309 }
310 }
311
312 #[test]
313 fn test_file_module_creation() {
314 let module = create_file_module();
315 assert_eq!(module.name, "file");
316 assert!(module.has_export("read_text"));
317 assert!(module.has_export("write_text"));
318 assert!(module.has_export("read_lines"));
319 assert!(module.has_export("append"));
320 assert!(module.has_export("read_bytes"));
321 assert!(module.has_export("write_bytes"));
322 }
323
324 #[test]
325 fn test_file_read_write_roundtrip() {
326 let module = create_file_module();
327 let ctx = test_ctx();
328 let write_fn = module.get_export("write_text").unwrap();
329 let read_fn = module.get_export("read_text").unwrap();
330
331 let dir = tempfile::tempdir().unwrap();
332 let path = dir.path().join("test.txt");
333 let path_str = path.to_str().unwrap();
334
335 let result = write_fn(
337 &[
338 ValueWord::from_string(Arc::new(path_str.to_string())),
339 ValueWord::from_string(Arc::new("hello world".to_string())),
340 ],
341 &ctx,
342 )
343 .unwrap();
344 assert!(result.as_ok_inner().is_some());
345
346 let result = read_fn(
348 &[ValueWord::from_string(Arc::new(path_str.to_string()))],
349 &ctx,
350 )
351 .unwrap();
352 let inner = result.as_ok_inner().expect("should be Ok");
353 assert_eq!(inner.as_str(), Some("hello world"));
354 }
355
356 #[test]
357 fn test_file_read_lines() {
358 let module = create_file_module();
359 let ctx = test_ctx();
360 let write_fn = module.get_export("write_text").unwrap();
361 let read_lines_fn = module.get_export("read_lines").unwrap();
362
363 let dir = tempfile::tempdir().unwrap();
364 let path = dir.path().join("lines.txt");
365 let path_str = path.to_str().unwrap();
366
367 write_fn(
368 &[
369 ValueWord::from_string(Arc::new(path_str.to_string())),
370 ValueWord::from_string(Arc::new("line1\nline2\nline3".to_string())),
371 ],
372 &ctx,
373 )
374 .unwrap();
375
376 let result = read_lines_fn(
377 &[ValueWord::from_string(Arc::new(path_str.to_string()))],
378 &ctx,
379 )
380 .unwrap();
381 let inner = result.as_ok_inner().expect("should be Ok");
382 let arr = inner.as_any_array().expect("should be array").to_generic();
383 assert_eq!(arr.len(), 3);
384 assert_eq!(arr[0].as_str(), Some("line1"));
385 assert_eq!(arr[1].as_str(), Some("line2"));
386 assert_eq!(arr[2].as_str(), Some("line3"));
387 }
388
389 #[test]
390 fn test_file_append() {
391 let module = create_file_module();
392 let ctx = test_ctx();
393 let write_fn = module.get_export("write_text").unwrap();
394 let append_fn = module.get_export("append").unwrap();
395 let read_fn = module.get_export("read_text").unwrap();
396
397 let dir = tempfile::tempdir().unwrap();
398 let path = dir.path().join("append.txt");
399 let path_str = path.to_str().unwrap();
400
401 write_fn(
402 &[
403 ValueWord::from_string(Arc::new(path_str.to_string())),
404 ValueWord::from_string(Arc::new("hello".to_string())),
405 ],
406 &ctx,
407 )
408 .unwrap();
409
410 append_fn(
411 &[
412 ValueWord::from_string(Arc::new(path_str.to_string())),
413 ValueWord::from_string(Arc::new(" world".to_string())),
414 ],
415 &ctx,
416 )
417 .unwrap();
418
419 let result = read_fn(
420 &[ValueWord::from_string(Arc::new(path_str.to_string()))],
421 &ctx,
422 )
423 .unwrap();
424 let inner = result.as_ok_inner().expect("should be Ok");
425 assert_eq!(inner.as_str(), Some("hello world"));
426 }
427
428 #[test]
429 fn test_file_read_bytes_write_bytes_roundtrip() {
430 let module = create_file_module();
431 let ctx = test_ctx();
432 let write_fn = module.get_export("write_bytes").unwrap();
433 let read_fn = module.get_export("read_bytes").unwrap();
434
435 let dir = tempfile::tempdir().unwrap();
436 let path = dir.path().join("bytes.bin");
437 let path_str = path.to_str().unwrap();
438
439 let data = ValueWord::from_array(Arc::new(vec![
440 ValueWord::from_f64(0.0),
441 ValueWord::from_f64(127.0),
442 ValueWord::from_f64(255.0),
443 ]));
444
445 write_fn(
446 &[ValueWord::from_string(Arc::new(path_str.to_string())), data],
447 &ctx,
448 )
449 .unwrap();
450
451 let result = read_fn(
452 &[ValueWord::from_string(Arc::new(path_str.to_string()))],
453 &ctx,
454 )
455 .unwrap();
456 let inner = result.as_ok_inner().expect("should be Ok");
457 let arr = inner.as_any_array().expect("should be array").to_generic();
458 assert_eq!(arr.len(), 3);
459 assert_eq!(arr[0].as_f64(), Some(0.0));
460 assert_eq!(arr[1].as_f64(), Some(127.0));
461 assert_eq!(arr[2].as_f64(), Some(255.0));
462 }
463
464 #[test]
465 fn test_file_write_bytes_validates_range() {
466 let module = create_file_module();
467 let ctx = test_ctx();
468 let write_fn = module.get_export("write_bytes").unwrap();
469
470 let dir = tempfile::tempdir().unwrap();
471 let path = dir.path().join("bad.bin");
472 let path_str = path.to_str().unwrap();
473
474 let data = ValueWord::from_array(Arc::new(vec![ValueWord::from_f64(256.0)]));
476 let result = write_fn(
477 &[ValueWord::from_string(Arc::new(path_str.to_string())), data],
478 &ctx,
479 );
480 assert!(result.is_err());
481
482 let data = ValueWord::from_array(Arc::new(vec![ValueWord::from_f64(-1.0)]));
484 let result = write_fn(
485 &[ValueWord::from_string(Arc::new(path_str.to_string())), data],
486 &ctx,
487 );
488 assert!(result.is_err());
489 }
490
491 #[test]
492 fn test_file_read_nonexistent() {
493 let module = create_file_module();
494 let ctx = test_ctx();
495 let read_fn = module.get_export("read_text").unwrap();
496 let result = read_fn(
497 &[ValueWord::from_string(Arc::new(
498 "/nonexistent/path/file.txt".to_string(),
499 ))],
500 &ctx,
501 );
502 assert!(result.is_err());
503 }
504
505 #[test]
506 fn test_file_requires_string_args() {
507 let module = create_file_module();
508 let ctx = test_ctx();
509 let read_fn = module.get_export("read_text").unwrap();
510 assert!(read_fn(&[ValueWord::from_f64(42.0)], &ctx).is_err());
511 assert!(read_fn(&[], &ctx).is_err());
512 }
513
514 #[test]
515 fn test_file_schemas() {
516 let module = create_file_module();
517
518 let read_schema = module.get_schema("read_text").unwrap();
519 assert_eq!(read_schema.params.len(), 1);
520 assert_eq!(read_schema.return_type.as_deref(), Some("Result<string>"));
521
522 let write_schema = module.get_schema("write_text").unwrap();
523 assert_eq!(write_schema.params.len(), 2);
524
525 let read_bytes_schema = module.get_schema("read_bytes").unwrap();
526 assert_eq!(
527 read_bytes_schema.return_type.as_deref(),
528 Some("Result<Array<number>>")
529 );
530
531 let write_bytes_schema = module.get_schema("write_bytes").unwrap();
532 assert_eq!(write_bytes_schema.params.len(), 2);
533 assert_eq!(write_bytes_schema.params[1].type_name, "Array<number>");
534 }
535}