Skip to main content

shape_runtime/stdlib_io/
async_file_ops.rs

1//! Async file I/O operations for the io module.
2//!
3//! Provides non-blocking file read/write using tokio's async file API.
4//! These integrate with the VM's async/await system via `AsyncModuleFn`.
5
6use shape_value::ValueWord;
7use std::sync::Arc;
8
9/// io.read_file_async(path: string) -> string
10///
11/// Asynchronously reads the entire contents of a file as a string.
12pub async fn io_read_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
13    let path = args
14        .first()
15        .and_then(|a| a.as_str())
16        .ok_or_else(|| "io.read_file_async() requires a string path argument".to_string())?
17        .to_string();
18
19    let contents = tokio::fs::read_to_string(&path)
20        .await
21        .map_err(|e| format!("io.read_file_async(\"{}\"): {}", path, e))?;
22
23    Ok(ValueWord::from_string(Arc::new(contents)))
24}
25
26/// io.write_file_async(path: string, data: string) -> int
27///
28/// Asynchronously writes a string to a file, creating or truncating as needed.
29/// Returns the number of bytes written.
30pub async fn io_write_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
31    let path = args
32        .first()
33        .and_then(|a| a.as_str())
34        .ok_or_else(|| "io.write_file_async() requires a string path argument".to_string())?
35        .to_string();
36
37    let data = args
38        .get(1)
39        .and_then(|a| a.as_str())
40        .ok_or_else(|| "io.write_file_async() requires a string data argument".to_string())?
41        .to_string();
42
43    let bytes_written = data.len();
44    tokio::fs::write(&path, &data)
45        .await
46        .map_err(|e| format!("io.write_file_async(\"{}\"): {}", path, e))?;
47
48    Ok(ValueWord::from_i64(bytes_written as i64))
49}
50
51/// io.append_file_async(path: string, data: string) -> int
52///
53/// Asynchronously appends a string to a file.
54/// Returns the number of bytes written.
55pub async fn io_append_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
56    use tokio::io::AsyncWriteExt;
57
58    let path = args
59        .first()
60        .and_then(|a| a.as_str())
61        .ok_or_else(|| "io.append_file_async() requires a string path argument".to_string())?
62        .to_string();
63
64    let data = args
65        .get(1)
66        .and_then(|a| a.as_str())
67        .ok_or_else(|| "io.append_file_async() requires a string data argument".to_string())?
68        .to_string();
69
70    let bytes_written = data.len();
71    let mut file = tokio::fs::OpenOptions::new()
72        .append(true)
73        .create(true)
74        .open(&path)
75        .await
76        .map_err(|e| format!("io.append_file_async(\"{}\"): {}", path, e))?;
77
78    file.write_all(data.as_bytes())
79        .await
80        .map_err(|e| format!("io.append_file_async(\"{}\"): {}", path, e))?;
81
82    file.flush()
83        .await
84        .map_err(|e| format!("io.append_file_async(\"{}\"): flush: {}", path, e))?;
85
86    Ok(ValueWord::from_i64(bytes_written as i64))
87}
88
89/// io.read_bytes_async(path: string) -> Array<int>
90///
91/// Asynchronously reads a file as raw bytes, returning an array of ints.
92pub async fn io_read_bytes_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
93    let path = args
94        .first()
95        .and_then(|a| a.as_str())
96        .ok_or_else(|| "io.read_bytes_async() requires a string path argument".to_string())?
97        .to_string();
98
99    let bytes = tokio::fs::read(&path)
100        .await
101        .map_err(|e| format!("io.read_bytes_async(\"{}\"): {}", path, e))?;
102
103    let arr: Vec<ValueWord> = bytes
104        .iter()
105        .map(|&b| ValueWord::from_i64(b as i64))
106        .collect();
107    Ok(ValueWord::from_array(Arc::new(arr)))
108}
109
110/// io.exists_async(path: string) -> bool
111///
112/// Asynchronously checks if a path exists.
113pub async fn io_exists_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
114    let path = args
115        .first()
116        .and_then(|a| a.as_str())
117        .ok_or_else(|| "io.exists_async() requires a string path argument".to_string())?
118        .to_string();
119
120    // tokio::fs doesn't have an exists() — use metadata
121    let exists = tokio::fs::metadata(&path).await.is_ok();
122    Ok(ValueWord::from_bool(exists))
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[tokio::test]
130    async fn test_read_file_async() {
131        let dir = tempfile::tempdir().unwrap();
132        let path = dir.path().join("test.txt");
133        std::fs::write(&path, "async hello").unwrap();
134
135        let result = io_read_file_async(vec![ValueWord::from_string(Arc::new(
136            path.to_string_lossy().to_string(),
137        ))])
138        .await
139        .unwrap();
140
141        assert_eq!(result.as_str(), Some("async hello"));
142    }
143
144    #[tokio::test]
145    async fn test_write_file_async() {
146        let dir = tempfile::tempdir().unwrap();
147        let path = dir.path().join("test.txt");
148
149        let result = io_write_file_async(vec![
150            ValueWord::from_string(Arc::new(path.to_string_lossy().to_string())),
151            ValueWord::from_string(Arc::new("async world".to_string())),
152        ])
153        .await
154        .unwrap();
155
156        assert_eq!(result.as_i64(), Some(11)); // "async world".len()
157        let contents = std::fs::read_to_string(&path).unwrap();
158        assert_eq!(contents, "async world");
159    }
160
161    #[tokio::test]
162    async fn test_append_file_async() {
163        let dir = tempfile::tempdir().unwrap();
164        let path = dir.path().join("test.txt");
165        std::fs::write(&path, "first").unwrap();
166
167        let result = io_append_file_async(vec![
168            ValueWord::from_string(Arc::new(path.to_string_lossy().to_string())),
169            ValueWord::from_string(Arc::new("_second".to_string())),
170        ])
171        .await
172        .unwrap();
173
174        assert_eq!(result.as_i64(), Some(7)); // "_second".len()
175        let contents = std::fs::read_to_string(&path).unwrap();
176        assert_eq!(contents, "first_second");
177    }
178
179    #[tokio::test]
180    async fn test_read_bytes_async() {
181        let dir = tempfile::tempdir().unwrap();
182        let path = dir.path().join("test.bin");
183        std::fs::write(&path, &[0xAB, 0xCD, 0xEF]).unwrap();
184
185        let result = io_read_bytes_async(vec![ValueWord::from_string(Arc::new(
186            path.to_string_lossy().to_string(),
187        ))])
188        .await
189        .unwrap();
190
191        let arr = result.as_any_array().unwrap().to_generic();
192        assert_eq!(arr.len(), 3);
193        assert_eq!(arr[0].as_i64(), Some(0xAB));
194        assert_eq!(arr[1].as_i64(), Some(0xCD));
195        assert_eq!(arr[2].as_i64(), Some(0xEF));
196    }
197
198    #[tokio::test]
199    async fn test_exists_async() {
200        let result = io_exists_async(vec![ValueWord::from_string(Arc::new("/tmp".to_string()))])
201            .await
202            .unwrap();
203        assert_eq!(result.as_bool(), Some(true));
204
205        let result = io_exists_async(vec![ValueWord::from_string(Arc::new(
206            "/nonexistent_xyz_test".to_string(),
207        ))])
208        .await
209        .unwrap();
210        assert_eq!(result.as_bool(), Some(false));
211    }
212
213    #[tokio::test]
214    async fn test_read_file_async_missing() {
215        let result = io_read_file_async(vec![ValueWord::from_string(Arc::new(
216            "/nonexistent_file_xyz".to_string(),
217        ))])
218        .await;
219        assert!(result.is_err());
220    }
221
222    #[tokio::test]
223    async fn test_write_file_async_requires_args() {
224        let result = io_write_file_async(vec![]).await;
225        assert!(result.is_err());
226    }
227}