1use crate::marshal::{
11 register_typed_fn_1, register_typed_fn_2, register_typed_fn_2_full,
12 register_typed_fn_3_full,
13};
14use crate::module_exports::{ModuleExports, ModuleParam};
15use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
16use shape_value::heap_value::{IoHandleData, IoResource};
17use std::io::{Read, Seek, Write};
18use std::sync::Arc;
19
20fn lock_as_file<'a>(
22 handle: &'a IoHandleData,
23 fn_name: &str,
24) -> Result<std::sync::MutexGuard<'a, Option<IoResource>>, String> {
25 let guard = handle
26 .resource
27 .lock()
28 .map_err(|_| format!("{}: lock poisoned", fn_name))?;
29 match guard.as_ref() {
30 None => Err(format!("{}: handle is closed", fn_name)),
31 Some(IoResource::File(_)) => Ok(guard),
32 Some(_) => Err(format!("{}: handle is not a file", fn_name)),
33 }
34}
35
36fn as_file_mut(resource: &mut Option<IoResource>) -> &mut std::fs::File {
38 match resource.as_mut().unwrap() {
39 IoResource::File(f) => f,
40 _ => unreachable!(),
41 }
42}
43
44pub fn register_file_io_handle_ops(module: &mut ModuleExports) {
47 register_typed_fn_2_full::<_, Arc<String>, Arc<String>>(
49 module,
50 "open",
51 "Open a file and return a handle",
52 [
53 ModuleParam {
54 name: "path".to_string(),
55 type_name: "string".to_string(),
56 required: true,
57 description: "File path to open".to_string(),
58 ..Default::default()
59 },
60 ModuleParam {
61 name: "mode".to_string(),
62 type_name: "string".to_string(),
63 required: false,
64 description: "Open mode: \"r\" (default), \"w\", \"a\", \"rw\"".to_string(),
65 default_snippet: Some("\"r\"".to_string()),
66 allowed_values: Some(vec![
67 "r".to_string(),
68 "w".to_string(),
69 "a".to_string(),
70 "rw".to_string(),
71 ]),
72 ..Default::default()
73 },
74 ],
75 ConcreteType::IoHandle,
76 |path, mode, ctx| {
77 let path = path.as_str();
78 let mode = mode.as_str();
79 match mode {
80 "r" => crate::module_exports::check_fs_permission(
81 ctx,
82 shape_abi_v1::Permission::FsRead,
83 path,
84 )?,
85 "w" | "a" => crate::module_exports::check_fs_permission(
86 ctx,
87 shape_abi_v1::Permission::FsWrite,
88 path,
89 )?,
90 "rw" => {
91 crate::module_exports::check_fs_permission(
92 ctx,
93 shape_abi_v1::Permission::FsRead,
94 path,
95 )?;
96 crate::module_exports::check_fs_permission(
97 ctx,
98 shape_abi_v1::Permission::FsWrite,
99 path,
100 )?;
101 }
102 _ => {}
103 }
104
105 let file = match mode {
106 "r" => std::fs::OpenOptions::new()
107 .read(true)
108 .open(path)
109 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
110 "w" => std::fs::OpenOptions::new()
111 .write(true)
112 .create(true)
113 .truncate(true)
114 .open(path)
115 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
116 "a" => std::fs::OpenOptions::new()
117 .append(true)
118 .create(true)
119 .open(path)
120 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
121 "rw" => std::fs::OpenOptions::new()
122 .read(true)
123 .write(true)
124 .create(true)
125 .open(path)
126 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
127 _ => {
128 return Err(format!(
129 "io.open(): invalid mode '{}'. Use \"r\", \"w\", \"a\", or \"rw\"",
130 mode
131 ));
132 }
133 };
134
135 let handle = IoHandleData::new_file(file, path.to_string(), mode.to_string());
136 Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(handle))))
137 },
138 );
139
140 register_typed_fn_1::<_, Arc<IoHandleData>>(
142 module,
143 "read_to_string",
144 "Read the entire file as a UTF-8 string",
145 "handle",
146 "IoHandle",
147 ConcreteType::String,
148 |handle, ctx| {
149 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
150 let mut guard = lock_as_file(&handle, "io.read_to_string()")?;
151 let file = as_file_mut(&mut guard);
152 file.seek(std::io::SeekFrom::Start(0))
153 .map_err(|e| format!("io.read_to_string(): seek failed: {}", e))?;
154 let mut contents = String::new();
155 file.read_to_string(&mut contents)
156 .map_err(|e| format!("io.read_to_string(): {}", e))?;
157 Ok(TypedReturn::Concrete(ConcreteReturn::String(contents)))
158 },
159 );
160
161 register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
163 module,
164 "read",
165 "Read from a file handle (n bytes or all)",
166 [
167 ModuleParam {
168 name: "handle".to_string(),
169 type_name: "IoHandle".to_string(),
170 required: true,
171 description: "File handle from io.open()".to_string(),
172 ..Default::default()
173 },
174 ModuleParam {
175 name: "n".to_string(),
176 type_name: "int".to_string(),
177 required: false,
178 description: "Number of bytes to read (omit for all)".to_string(),
179 default_snippet: Some("-1".to_string()),
180 ..Default::default()
181 },
182 ],
183 ConcreteType::String,
184 |handle, n, ctx| {
185 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
186 let mut guard = lock_as_file(&handle, "io.read()")?;
187 let file = as_file_mut(&mut guard);
188 let contents = if n >= 0 {
189 let n = n as usize;
190 let mut buf = vec![0u8; n];
191 let bytes_read = file.read(&mut buf).map_err(|e| format!("io.read(): {}", e))?;
192 buf.truncate(bytes_read);
193 String::from_utf8(buf).map_err(|e| format!("io.read(): invalid UTF-8: {}", e))?
194 } else {
195 let mut s = String::new();
196 file.read_to_string(&mut s)
197 .map_err(|e| format!("io.read(): {}", e))?;
198 s
199 };
200 Ok(TypedReturn::Concrete(ConcreteReturn::String(contents)))
201 },
202 );
203
204 register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
206 module,
207 "read_bytes",
208 "Read bytes from a file handle into an Array<int>",
209 [
210 ModuleParam {
211 name: "handle".to_string(),
212 type_name: "IoHandle".to_string(),
213 required: true,
214 description: "File handle from io.open()".to_string(),
215 ..Default::default()
216 },
217 ModuleParam {
218 name: "n".to_string(),
219 type_name: "int".to_string(),
220 required: false,
221 description: "Number of bytes to read (omit for all)".to_string(),
222 default_snippet: Some("-1".to_string()),
223 ..Default::default()
224 },
225 ],
226 ConcreteType::Bytes,
227 |handle, n, ctx| {
228 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
229 let mut guard = lock_as_file(&handle, "io.read_bytes()")?;
230 let file = as_file_mut(&mut guard);
231 let bytes = if n >= 0 {
232 let n = n as usize;
233 let mut buf = vec![0u8; n];
234 let bytes_read = file
235 .read(&mut buf)
236 .map_err(|e| format!("io.read_bytes(): {}", e))?;
237 buf.truncate(bytes_read);
238 buf
239 } else {
240 let mut buf = Vec::new();
241 file.read_to_end(&mut buf)
242 .map_err(|e| format!("io.read_bytes(): {}", e))?;
243 buf
244 };
245 Ok(TypedReturn::Concrete(ConcreteReturn::Bytes(bytes)))
246 },
247 );
248
249 register_typed_fn_2::<_, Arc<IoHandleData>, Arc<String>>(
251 module,
252 "write",
253 "Write a string to a file handle, returning bytes written",
254 [("handle", "IoHandle"), ("data", "string")],
255 ConcreteType::Int,
256 |handle, data, ctx| {
257 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
258 let mut guard = lock_as_file(&handle, "io.write()")?;
259 let file = as_file_mut(&mut guard);
260 let bytes_written = file
261 .write(data.as_bytes())
262 .map_err(|e| format!("io.write(): {}", e))?;
263 Ok(TypedReturn::Concrete(ConcreteReturn::I64(bytes_written as i64)))
264 },
265 );
266
267 register_typed_fn_1::<_, Arc<IoHandleData>>(
269 module,
270 "close",
271 "Close a file handle, returning whether it was open",
272 "handle",
273 "IoHandle",
274 ConcreteType::Bool,
275 |handle, ctx| {
276 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
277 Ok(TypedReturn::Concrete(ConcreteReturn::Bool(handle.close())))
278 },
279 );
280
281 register_typed_fn_1::<_, Arc<IoHandleData>>(
283 module,
284 "flush",
285 "Flush pending writes to disk",
286 "handle",
287 "IoHandle",
288 ConcreteType::Unit,
289 |handle, ctx| {
290 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
291 let mut guard = lock_as_file(&handle, "io.flush()")?;
292 let file = as_file_mut(&mut guard);
293 file.flush().map_err(|e| format!("io.flush(): {}", e))?;
294 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
295 },
296 );
297}
298
299pub fn register_file_path_ops(module: &mut ModuleExports) {
303 register_typed_fn_1::<_, Arc<String>>(
305 module,
306 "exists",
307 "Check if a file or directory exists",
308 "path",
309 "string",
310 ConcreteType::Bool,
311 |path, ctx| {
312 let path = path.as_str();
313 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
314 Ok(TypedReturn::Concrete(ConcreteReturn::Bool(
315 std::path::Path::new(path).exists(),
316 )))
317 },
318 );
319
320 register_typed_fn_1::<_, Arc<String>>(
322 module,
323 "stat",
324 "Return file metadata as an object",
325 "path",
326 "string",
327 ConcreteType::TypedObject,
328 |path, ctx| {
329 let path = path.as_str();
330 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
331 let metadata = std::fs::metadata(path)
332 .map_err(|e| format!("io.stat(\"{}\"): {}", path, e))?;
333 let modified_ms = metadata
334 .modified()
335 .ok()
336 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
337 .map(|d| d.as_millis() as f64)
338 .unwrap_or(0.0);
339 let created_ms = metadata
340 .created()
341 .ok()
342 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
343 .map(|d| d.as_millis() as f64)
344 .unwrap_or(0.0);
345 Ok(TypedReturn::TypedObject(vec![
346 ("size".to_string(), ConcreteReturn::I64(metadata.len() as i64)),
347 ("modified".to_string(), ConcreteReturn::F64(modified_ms)),
348 ("created".to_string(), ConcreteReturn::F64(created_ms)),
349 ("is_file".to_string(), ConcreteReturn::Bool(metadata.is_file())),
350 ("is_dir".to_string(), ConcreteReturn::Bool(metadata.is_dir())),
351 ]))
352 },
353 );
354
355 register_typed_fn_1::<_, Arc<String>>(
357 module,
358 "is_file",
359 "Check if a path refers to a regular file",
360 "path",
361 "string",
362 ConcreteType::Bool,
363 |path, ctx| {
364 let path = path.as_str();
365 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
366 Ok(TypedReturn::Concrete(ConcreteReturn::Bool(
367 std::path::Path::new(path).is_file(),
368 )))
369 },
370 );
371
372 register_typed_fn_1::<_, Arc<String>>(
374 module,
375 "is_dir",
376 "Check if a path refers to a directory",
377 "path",
378 "string",
379 ConcreteType::Bool,
380 |path, ctx| {
381 let path = path.as_str();
382 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
383 Ok(TypedReturn::Concrete(ConcreteReturn::Bool(
384 std::path::Path::new(path).is_dir(),
385 )))
386 },
387 );
388
389 register_typed_fn_2_full::<_, Arc<String>, bool>(
391 module,
392 "mkdir",
393 "Create a directory",
394 [
395 ModuleParam {
396 name: "path".to_string(),
397 type_name: "string".to_string(),
398 required: true,
399 description: "Directory path to create".to_string(),
400 ..Default::default()
401 },
402 ModuleParam {
403 name: "recursive".to_string(),
404 type_name: "bool".to_string(),
405 required: false,
406 description: "Create parent directories if needed".to_string(),
407 default_snippet: Some("false".to_string()),
408 ..Default::default()
409 },
410 ],
411 ConcreteType::Unit,
412 |path, recursive, ctx| {
413 let path = path.as_str();
414 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
415 if recursive {
416 std::fs::create_dir_all(path)
417 .map_err(|e| format!("io.mkdir(\"{}\"): {}", path, e))?;
418 } else {
419 std::fs::create_dir(path)
420 .map_err(|e| format!("io.mkdir(\"{}\"): {}", path, e))?;
421 }
422 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
423 },
424 );
425
426 register_typed_fn_1::<_, Arc<String>>(
428 module,
429 "remove",
430 "Remove a file or directory (recursive for directories)",
431 "path",
432 "string",
433 ConcreteType::Unit,
434 |path, ctx| {
435 let path = path.as_str();
436 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
437 let p = std::path::Path::new(path);
438 if p.is_dir() {
439 std::fs::remove_dir_all(path)
440 .map_err(|e| format!("io.remove(\"{}\"): {}", path, e))?;
441 } else {
442 std::fs::remove_file(path)
443 .map_err(|e| format!("io.remove(\"{}\"): {}", path, e))?;
444 }
445 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
446 },
447 );
448
449 register_typed_fn_2::<_, Arc<String>, Arc<String>>(
451 module,
452 "rename",
453 "Rename a file or directory",
454 [("old", "string"), ("new", "string")],
455 ConcreteType::Unit,
456 |old, new, ctx| {
457 let old = old.as_str();
458 let new = new.as_str();
459 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, old)?;
460 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, new)?;
461 std::fs::rename(old, new)
462 .map_err(|e| format!("io.rename(\"{}\", \"{}\"): {}", old, new, e))?;
463 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
464 },
465 );
466
467 register_typed_fn_1::<_, Arc<String>>(
469 module,
470 "read_dir",
471 "List entries in a directory",
472 "path",
473 "string",
474 ConcreteType::ArrayString,
475 |path, ctx| {
476 let path = path.as_str();
477 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
478 let entries: Vec<String> = std::fs::read_dir(path)
479 .map_err(|e| format!("io.read_dir(\"{}\"): {}", path, e))?
480 .filter_map(|entry| entry.ok().map(|e| e.path().to_string_lossy().to_string()))
481 .collect();
482 Ok(TypedReturn::Concrete(ConcreteReturn::ArrayString(entries)))
483 },
484 );
485
486 register_typed_fn_1::<_, Arc<String>>(
488 module,
489 "read_gzip",
490 "Read and decompress a gzip-compressed file",
491 "path",
492 "string",
493 ConcreteType::String,
494 |path, ctx| {
495 let path = path.as_str();
496 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
497 let file = std::fs::File::open(path)
498 .map_err(|e| format!("io.read_gzip(\"{}\"): {}", path, e))?;
499 let mut decoder = flate2::read::GzDecoder::new(file);
500 let mut output = String::new();
501 decoder
502 .read_to_string(&mut output)
503 .map_err(|e| format!("io.read_gzip(\"{}\"): decompression failed: {}", path, e))?;
504 Ok(TypedReturn::Concrete(ConcreteReturn::String(output)))
505 },
506 );
507
508 register_typed_fn_3_full::<_, Arc<String>, Arc<String>, i64>(
510 module,
511 "write_gzip",
512 "Compress and write a string to a file with gzip",
513 [
514 ModuleParam {
515 name: "path".to_string(),
516 type_name: "string".to_string(),
517 required: true,
518 description: "Destination file path".to_string(),
519 ..Default::default()
520 },
521 ModuleParam {
522 name: "data".to_string(),
523 type_name: "string".to_string(),
524 required: true,
525 description: "String content to compress and write".to_string(),
526 ..Default::default()
527 },
528 ModuleParam {
529 name: "level".to_string(),
530 type_name: "int".to_string(),
531 required: false,
532 description: "Compression level 0-9 (default: 6)".to_string(),
533 default_snippet: Some("6".to_string()),
534 ..Default::default()
535 },
536 ],
537 ConcreteType::Unit,
538 |path, data, level, ctx| {
539 let path = path.as_str();
540 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
541 let level = level as u32;
542 let file = std::fs::File::create(path)
543 .map_err(|e| format!("io.write_gzip(\"{}\"): {}", path, e))?;
544 let mut encoder =
545 flate2::write::GzEncoder::new(file, flate2::Compression::new(level));
546 encoder
547 .write_all(data.as_bytes())
548 .map_err(|e| format!("io.write_gzip(\"{}\"): compression failed: {}", path, e))?;
549 encoder
550 .finish()
551 .map_err(|e| format!("io.write_gzip(\"{}\"): finalize failed: {}", path, e))?;
552 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
553 },
554 );
555}