Skip to main content

shape_runtime/stdlib/
file.rs

1//! Native `file` module for high-level filesystem operations.
2//!
3//! Exports: file.read_text, file.write_text, file.read_lines, file.append
4//!
5//! All operations go through [`FileSystemProvider`] so that sandbox/VFS modes
6//! work transparently. The default provider is [`RealFileSystem`].
7//!
8//! Policy gated: read ops require FsRead, write ops require FsWrite.
9//!
10//! Phase 2c migration: ported to the typed marshal layer.
11//! `file.read_bytes` / `file.write_bytes` are deferred until the
12//! `Array<number>` marshal extension (FromSlot/ToSlot for typed-array
13//! heap pointers) lands. Tracked alongside the parser-module deferral
14//! list. The functions previously here read/wrote byte arrays via
15//! the deleted `as_any_array().to_generic()` tag_bits dispatch —
16//! strict-typed answer is `Arc<TypedBuffer<f64>>` typed args + ToSlot
17//! projection of `ConcreteReturn::ArrayF64` to a heap-allocated
18//! TypedArray slot.
19
20use crate::marshal::{register_typed_fn_1, register_typed_fn_2};
21use crate::module_exports::ModuleExports;
22use crate::stdlib::runtime_policy::{FileSystemProvider, RealFileSystem};
23use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
24use std::path::Path;
25use std::sync::Arc;
26
27/// Create a file module that uses the given filesystem provider.
28/// The default `create_file_module()` uses [`RealFileSystem`]; callers can
29/// substitute a `PolicyEnforcedFs` or `VirtualFileSystem` for sandboxing.
30pub fn create_file_module_with_provider(fs: Arc<dyn FileSystemProvider>) -> ModuleExports {
31    let mut module = ModuleExports::new("std::core::file");
32    module.description = "High-level filesystem operations".to_string();
33
34    // file.read_text(path: string) -> Result<string>
35    {
36        let fs = Arc::clone(&fs);
37        register_typed_fn_1::<_, Arc<String>>(
38            &mut module,
39            "read_text",
40            "Read the entire contents of a file as a UTF-8 string",
41            "path",
42            "string",
43            ConcreteType::Result(Box::new(ConcreteType::String)),
44            move |path_str, ctx| {
45                crate::module_exports::check_fs_permission(
46                    ctx,
47                    shape_abi_v1::Permission::FsRead,
48                    path_str.as_str(),
49                )?;
50                let bytes = fs
51                    .read(Path::new(path_str.as_str()))
52                    .map_err(|e| format!("file.read_text() failed: {}", e))?;
53                let text = String::from_utf8(bytes)
54                    .map_err(|e| format!("file.read_text() invalid UTF-8: {}", e))?;
55                Ok(TypedReturn::Ok(ConcreteReturn::String(text)))
56            },
57        );
58    }
59
60    // file.write_text(path: string, content: string) -> Result<unit>
61    {
62        let fs = Arc::clone(&fs);
63        register_typed_fn_2::<_, Arc<String>, Arc<String>>(
64            &mut module,
65            "write_text",
66            "Write a string to a file, creating or truncating it",
67            [("path", "string"), ("content", "string")],
68            ConcreteType::Result(Box::new(ConcreteType::Unit)),
69            move |path_str, content, ctx| {
70                crate::module_exports::check_fs_permission(
71                    ctx,
72                    shape_abi_v1::Permission::FsWrite,
73                    path_str.as_str(),
74                )?;
75                fs.write(Path::new(path_str.as_str()), content.as_bytes())
76                    .map_err(|e| format!("file.write_text() failed: {}", e))?;
77                Ok(TypedReturn::Ok(ConcreteReturn::Unit))
78            },
79        );
80    }
81
82    // file.read_lines(path: string) -> Result<Array<string>>
83    {
84        let fs = Arc::clone(&fs);
85        register_typed_fn_1::<_, Arc<String>>(
86            &mut module,
87            "read_lines",
88            "Read a file and return its lines as an array of strings",
89            "path",
90            "string",
91            ConcreteType::Result(Box::new(ConcreteType::ArrayString)),
92            move |path_str, ctx| {
93                crate::module_exports::check_fs_permission(
94                    ctx,
95                    shape_abi_v1::Permission::FsRead,
96                    path_str.as_str(),
97                )?;
98                let bytes = fs
99                    .read(Path::new(path_str.as_str()))
100                    .map_err(|e| format!("file.read_lines() failed: {}", e))?;
101                let text = String::from_utf8(bytes)
102                    .map_err(|e| format!("file.read_lines() invalid UTF-8: {}", e))?;
103                let lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
104                Ok(TypedReturn::Ok(ConcreteReturn::ArrayString(lines)))
105            },
106        );
107    }
108
109    // file.append(path: string, content: string) -> Result<unit>
110    {
111        let fs = Arc::clone(&fs);
112        register_typed_fn_2::<_, Arc<String>, Arc<String>>(
113            &mut module,
114            "append",
115            "Append a string to a file, creating it if it does not exist",
116            [("path", "string"), ("content", "string")],
117            ConcreteType::Result(Box::new(ConcreteType::Unit)),
118            move |path_str, content, ctx| {
119                crate::module_exports::check_fs_permission(
120                    ctx,
121                    shape_abi_v1::Permission::FsWrite,
122                    path_str.as_str(),
123                )?;
124                fs.append(Path::new(path_str.as_str()), content.as_bytes())
125                    .map_err(|e| format!("file.append() failed: {}", e))?;
126                Ok(TypedReturn::Ok(ConcreteReturn::Unit))
127            },
128        );
129    }
130
131    module
132}
133
134/// Create the `file` module using the default real filesystem.
135pub fn create_file_module() -> ModuleExports {
136    create_file_module_with_provider(Arc::new(RealFileSystem))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_file_module_creation() {
145        let module = create_file_module();
146        assert_eq!(module.name, "std::core::file");
147        assert!(module.has_export("read_text"));
148        assert!(module.has_export("write_text"));
149        assert!(module.has_export("read_lines"));
150        assert!(module.has_export("append"));
151    }
152
153    #[test]
154    fn test_file_schemas() {
155        let module = create_file_module();
156        let read_schema = module.get_schema("read_text").unwrap();
157        assert_eq!(read_schema.params.len(), 1);
158        assert_eq!(read_schema.return_type.as_deref(), Some("Result<string>"));
159
160        let write_schema = module.get_schema("write_text").unwrap();
161        assert_eq!(write_schema.params.len(), 2);
162    }
163
164    // Behavioural roundtrip tests removed — they used `module.invoke_export`
165    // with `ValueWord` arrays (deleted dynamic dispatch entry point).
166    // End-to-end coverage through typed-slot dispatch belongs in
167    // `shape-test`'s integration suite.
168}