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