1use shape_value::ValueWord;
4use shape_value::heap_value::{IoHandleData, IoResource};
5use std::io::{Read, Seek, Write};
6
7fn lock_as_file<'a>(
9 handle: &'a IoHandleData,
10 fn_name: &str,
11) -> Result<std::sync::MutexGuard<'a, Option<IoResource>>, String> {
12 let guard = handle
13 .resource
14 .lock()
15 .map_err(|_| format!("{}: lock poisoned", fn_name))?;
16 match guard.as_ref() {
17 None => Err(format!("{}: handle is closed", fn_name)),
18 Some(IoResource::File(_)) => Ok(guard),
19 Some(_) => Err(format!("{}: handle is not a file", fn_name)),
20 }
21}
22
23fn as_file_mut(resource: &mut Option<IoResource>) -> &mut std::fs::File {
25 match resource.as_mut().unwrap() {
26 IoResource::File(f) => f,
27 _ => unreachable!(),
28 }
29}
30
31pub fn io_open(
33 args: &[ValueWord],
34 ctx: &crate::module_exports::ModuleContext,
35) -> Result<ValueWord, String> {
36 let path = args
37 .first()
38 .and_then(|a| a.as_str())
39 .ok_or_else(|| "io.open() requires a string path argument".to_string())?
40 .to_string();
41
42 let mode = args
43 .get(1)
44 .and_then(|a| a.as_str())
45 .unwrap_or("r")
46 .to_string();
47
48 match mode.as_str() {
50 "r" => crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, &path)?,
51 "w" | "a" => {
52 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, &path)?
53 }
54 "rw" => {
55 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, &path)?;
56 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, &path)?;
57 }
58 _ => {} }
60
61 let file = match mode.as_str() {
62 "r" => std::fs::OpenOptions::new()
63 .read(true)
64 .open(&path)
65 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
66 "w" => std::fs::OpenOptions::new()
67 .write(true)
68 .create(true)
69 .truncate(true)
70 .open(&path)
71 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
72 "a" => std::fs::OpenOptions::new()
73 .append(true)
74 .create(true)
75 .open(&path)
76 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
77 "rw" => std::fs::OpenOptions::new()
78 .read(true)
79 .write(true)
80 .create(true)
81 .open(&path)
82 .map_err(|e| format!("io.open(\"{}\"): {}", path, e))?,
83 _ => {
84 return Err(format!(
85 "io.open(): invalid mode '{}'. Use \"r\", \"w\", \"a\", or \"rw\"",
86 mode
87 ));
88 }
89 };
90
91 let handle = IoHandleData::new_file(file, path, mode);
92 Ok(ValueWord::from_io_handle(handle))
93}
94
95pub fn io_read_to_string(
97 args: &[ValueWord],
98 ctx: &crate::module_exports::ModuleContext,
99) -> Result<ValueWord, String> {
100 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
101 let handle = args
102 .first()
103 .and_then(|a| a.as_io_handle())
104 .ok_or_else(|| "io.read_to_string() requires an IoHandle argument".to_string())?;
105
106 let mut guard = lock_as_file(handle, "io.read_to_string()")?;
107 let file = as_file_mut(&mut guard);
108
109 file.seek(std::io::SeekFrom::Start(0))
111 .map_err(|e| format!("io.read_to_string(): seek failed: {}", e))?;
112
113 let mut contents = String::new();
114 file.read_to_string(&mut contents)
115 .map_err(|e| format!("io.read_to_string(): {}", e))?;
116 Ok(ValueWord::from_string(std::sync::Arc::new(contents)))
117}
118
119pub fn io_read(
121 args: &[ValueWord],
122 ctx: &crate::module_exports::ModuleContext,
123) -> Result<ValueWord, String> {
124 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
125 let handle = args
126 .first()
127 .and_then(|a| a.as_io_handle())
128 .ok_or_else(|| "io.read() requires an IoHandle argument".to_string())?;
129
130 let n = args.get(1).and_then(|a| a.as_number_coerce());
131
132 let mut guard = lock_as_file(handle, "io.read()")?;
133 let file = as_file_mut(&mut guard);
134
135 let contents = if let Some(n) = n {
136 let n = n as usize;
137 let mut buf = vec![0u8; n];
138 let bytes_read = file
139 .read(&mut buf)
140 .map_err(|e| format!("io.read(): {}", e))?;
141 buf.truncate(bytes_read);
142 String::from_utf8(buf).map_err(|e| format!("io.read(): invalid UTF-8: {}", e))?
143 } else {
144 let mut s = String::new();
145 file.read_to_string(&mut s)
146 .map_err(|e| format!("io.read(): {}", e))?;
147 s
148 };
149
150 Ok(ValueWord::from_string(std::sync::Arc::new(contents)))
151}
152
153pub fn io_read_bytes(
155 args: &[ValueWord],
156 ctx: &crate::module_exports::ModuleContext,
157) -> Result<ValueWord, String> {
158 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
159 let handle = args
160 .first()
161 .and_then(|a| a.as_io_handle())
162 .ok_or_else(|| "io.read_bytes() requires an IoHandle argument".to_string())?;
163
164 let n = args.get(1).and_then(|a| a.as_number_coerce());
165
166 let mut guard = lock_as_file(handle, "io.read_bytes()")?;
167 let file = as_file_mut(&mut guard);
168
169 let bytes = if let Some(n) = n {
170 let n = n as usize;
171 let mut buf = vec![0u8; n];
172 let bytes_read = file
173 .read(&mut buf)
174 .map_err(|e| format!("io.read_bytes(): {}", e))?;
175 buf.truncate(bytes_read);
176 buf
177 } else {
178 let mut buf = Vec::new();
179 file.read_to_end(&mut buf)
180 .map_err(|e| format!("io.read_bytes(): {}", e))?;
181 buf
182 };
183
184 let arr: Vec<ValueWord> = bytes
185 .iter()
186 .map(|&b| ValueWord::from_i64(b as i64))
187 .collect();
188 Ok(ValueWord::from_array(std::sync::Arc::new(arr)))
189}
190
191pub fn io_write(
193 args: &[ValueWord],
194 ctx: &crate::module_exports::ModuleContext,
195) -> Result<ValueWord, String> {
196 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
197 let handle = args
198 .first()
199 .and_then(|a| a.as_io_handle())
200 .ok_or_else(|| "io.write() requires an IoHandle as first argument".to_string())?;
201
202 let data = args
203 .get(1)
204 .ok_or_else(|| "io.write() requires data as second argument".to_string())?;
205
206 let mut guard = lock_as_file(handle, "io.write()")?;
207 let file = as_file_mut(&mut guard);
208
209 let bytes_written = if let Some(s) = data.as_str() {
210 file.write(s.as_bytes())
211 .map_err(|e| format!("io.write(): {}", e))?
212 } else if let Some(view) = data.as_any_array() {
213 let arr = view.to_generic();
214 let bytes: Vec<u8> = arr
215 .iter()
216 .map(|nb| nb.as_number_coerce().unwrap_or(0.0) as u8)
217 .collect();
218 file.write(&bytes)
219 .map_err(|e| format!("io.write(): {}", e))?
220 } else {
221 let s = format!("{}", data);
222 file.write(s.as_bytes())
223 .map_err(|e| format!("io.write(): {}", e))?
224 };
225
226 Ok(ValueWord::from_i64(bytes_written as i64))
227}
228
229pub fn io_close(
231 args: &[ValueWord],
232 ctx: &crate::module_exports::ModuleContext,
233) -> Result<ValueWord, String> {
234 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
235 let handle = args
236 .first()
237 .and_then(|a| a.as_io_handle())
238 .ok_or_else(|| "io.close() requires an IoHandle argument".to_string())?;
239
240 Ok(ValueWord::from_bool(handle.close()))
241}
242
243pub fn io_flush(
245 args: &[ValueWord],
246 ctx: &crate::module_exports::ModuleContext,
247) -> Result<ValueWord, String> {
248 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
249 let handle = args
250 .first()
251 .and_then(|a| a.as_io_handle())
252 .ok_or_else(|| "io.flush() requires an IoHandle argument".to_string())?;
253
254 let mut guard = lock_as_file(handle, "io.flush()")?;
255 let file = as_file_mut(&mut guard);
256
257 file.flush().map_err(|e| format!("io.flush(): {}", e))?;
258 Ok(ValueWord::unit())
259}
260
261pub fn io_exists(
263 args: &[ValueWord],
264 ctx: &crate::module_exports::ModuleContext,
265) -> Result<ValueWord, String> {
266 let path = args
267 .first()
268 .and_then(|a| a.as_str())
269 .ok_or_else(|| "io.exists() requires a string path".to_string())?;
270 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
271 Ok(ValueWord::from_bool(std::path::Path::new(path).exists()))
272}
273
274pub fn io_stat(
276 args: &[ValueWord],
277 ctx: &crate::module_exports::ModuleContext,
278) -> Result<ValueWord, String> {
279 let path = args
280 .first()
281 .and_then(|a| a.as_str())
282 .ok_or_else(|| "io.stat() requires a string path".to_string())?;
283 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
284
285 let metadata = std::fs::metadata(path).map_err(|e| format!("io.stat(\"{}\"): {}", path, e))?;
286
287 let modified_ms = metadata
288 .modified()
289 .ok()
290 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
291 .map(|d| d.as_millis() as f64)
292 .unwrap_or(0.0);
293
294 let created_ms = metadata
295 .created()
296 .ok()
297 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
298 .map(|d| d.as_millis() as f64)
299 .unwrap_or(0.0);
300
301 let pairs: Vec<(&str, ValueWord)> = vec![
302 ("size", ValueWord::from_i64(metadata.len() as i64)),
303 ("modified", ValueWord::from_f64(modified_ms)),
304 ("created", ValueWord::from_f64(created_ms)),
305 ("is_file", ValueWord::from_bool(metadata.is_file())),
306 ("is_dir", ValueWord::from_bool(metadata.is_dir())),
307 ];
308 Ok(crate::type_schema::typed_object_from_pairs(&pairs))
309}
310
311pub fn io_is_file(
313 args: &[ValueWord],
314 ctx: &crate::module_exports::ModuleContext,
315) -> Result<ValueWord, String> {
316 let path = args
317 .first()
318 .and_then(|a| a.as_str())
319 .ok_or_else(|| "io.is_file() requires a string path".to_string())?;
320 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
321 Ok(ValueWord::from_bool(std::path::Path::new(path).is_file()))
322}
323
324pub fn io_is_dir(
326 args: &[ValueWord],
327 ctx: &crate::module_exports::ModuleContext,
328) -> Result<ValueWord, String> {
329 let path = args
330 .first()
331 .and_then(|a| a.as_str())
332 .ok_or_else(|| "io.is_dir() requires a string path".to_string())?;
333 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
334 Ok(ValueWord::from_bool(std::path::Path::new(path).is_dir()))
335}
336
337pub fn io_mkdir(
339 args: &[ValueWord],
340 ctx: &crate::module_exports::ModuleContext,
341) -> Result<ValueWord, String> {
342 let path = args
343 .first()
344 .and_then(|a| a.as_str())
345 .ok_or_else(|| "io.mkdir() requires a string path".to_string())?;
346 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
347
348 let recursive = args.get(1).and_then(|a| a.as_bool()).unwrap_or(false);
349
350 if recursive {
351 std::fs::create_dir_all(path).map_err(|e| format!("io.mkdir(\"{}\"): {}", path, e))?;
352 } else {
353 std::fs::create_dir(path).map_err(|e| format!("io.mkdir(\"{}\"): {}", path, e))?;
354 }
355 Ok(ValueWord::unit())
356}
357
358pub fn io_remove(
360 args: &[ValueWord],
361 ctx: &crate::module_exports::ModuleContext,
362) -> Result<ValueWord, String> {
363 let path = args
364 .first()
365 .and_then(|a| a.as_str())
366 .ok_or_else(|| "io.remove() requires a string path".to_string())?;
367 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
368
369 let p = std::path::Path::new(path);
370 if p.is_dir() {
371 std::fs::remove_dir_all(path).map_err(|e| format!("io.remove(\"{}\"): {}", path, e))?;
372 } else {
373 std::fs::remove_file(path).map_err(|e| format!("io.remove(\"{}\"): {}", path, e))?;
374 }
375 Ok(ValueWord::unit())
376}
377
378pub fn io_rename(
380 args: &[ValueWord],
381 ctx: &crate::module_exports::ModuleContext,
382) -> Result<ValueWord, String> {
383 let old = args
384 .first()
385 .and_then(|a| a.as_str())
386 .ok_or_else(|| "io.rename() requires old path as first argument".to_string())?;
387 let new = args
388 .get(1)
389 .and_then(|a| a.as_str())
390 .ok_or_else(|| "io.rename() requires new path as second argument".to_string())?;
391 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, old)?;
393 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, new)?;
394
395 std::fs::rename(old, new).map_err(|e| format!("io.rename(\"{}\", \"{}\"): {}", old, new, e))?;
396 Ok(ValueWord::unit())
397}
398
399pub fn io_read_dir(
401 args: &[ValueWord],
402 ctx: &crate::module_exports::ModuleContext,
403) -> Result<ValueWord, String> {
404 let path = args
405 .first()
406 .and_then(|a| a.as_str())
407 .ok_or_else(|| "io.read_dir() requires a string path".to_string())?;
408 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
409
410 let entries: Vec<ValueWord> = std::fs::read_dir(path)
411 .map_err(|e| format!("io.read_dir(\"{}\"): {}", path, e))?
412 .filter_map(|entry| {
413 entry.ok().map(|e| {
414 ValueWord::from_string(std::sync::Arc::new(e.path().to_string_lossy().to_string()))
415 })
416 })
417 .collect();
418
419 Ok(ValueWord::from_array(std::sync::Arc::new(entries)))
420}
421
422pub fn io_read_gzip(
426 args: &[ValueWord],
427 ctx: &crate::module_exports::ModuleContext,
428) -> Result<ValueWord, String> {
429 let path = args
430 .first()
431 .and_then(|a| a.as_str())
432 .ok_or_else(|| "io.read_gzip() requires a string path argument".to_string())?;
433 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsRead, path)?;
434
435 let file =
436 std::fs::File::open(path).map_err(|e| format!("io.read_gzip(\"{}\"): {}", path, e))?;
437
438 let mut decoder = flate2::read::GzDecoder::new(file);
439 let mut output = String::new();
440 decoder
441 .read_to_string(&mut output)
442 .map_err(|e| format!("io.read_gzip(\"{}\"): decompression failed: {}", path, e))?;
443
444 Ok(ValueWord::from_string(std::sync::Arc::new(output)))
445}
446
447pub fn io_write_gzip(
451 args: &[ValueWord],
452 ctx: &crate::module_exports::ModuleContext,
453) -> Result<ValueWord, String> {
454 let path = args
455 .first()
456 .and_then(|a| a.as_str())
457 .ok_or_else(|| "io.write_gzip() requires a string path argument".to_string())?;
458 crate::module_exports::check_fs_permission(ctx, shape_abi_v1::Permission::FsWrite, path)?;
459
460 let data = args
461 .get(1)
462 .and_then(|a| a.as_str())
463 .ok_or_else(|| "io.write_gzip() requires a string data argument".to_string())?;
464
465 let level = args
466 .get(2)
467 .and_then(|a| a.as_i64().or_else(|| a.as_f64().map(|n| n as i64)))
468 .unwrap_or(6) as u32;
469
470 let file =
471 std::fs::File::create(path).map_err(|e| format!("io.write_gzip(\"{}\"): {}", path, e))?;
472
473 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::new(level));
474 encoder
475 .write_all(data.as_bytes())
476 .map_err(|e| format!("io.write_gzip(\"{}\"): compression failed: {}", path, e))?;
477 encoder
478 .finish()
479 .map_err(|e| format!("io.write_gzip(\"{}\"): finalize failed: {}", path, e))?;
480
481 Ok(ValueWord::unit())
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
489 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
490 crate::module_exports::ModuleContext {
491 schemas: registry,
492 invoke_callable: None,
493 raw_invoker: None,
494 function_hashes: None,
495 vm_state: None,
496 granted_permissions: None,
497 scope_constraints: None,
498 set_pending_resume: None,
499 set_pending_frame_resume: None,
500 }
501 }
502
503 #[test]
504 fn test_io_open_write_read_close() {
505 let ctx = test_ctx();
506 let dir = std::env::temp_dir().join("shape_io_test");
507 let _ = std::fs::create_dir_all(&dir);
508 let path = dir.join("test_file.txt");
509 let path_str = path.to_string_lossy().to_string();
510
511 let handle = io_open(
513 &[
514 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
515 ValueWord::from_string(std::sync::Arc::new("w".to_string())),
516 ],
517 &ctx,
518 )
519 .unwrap();
520 assert_eq!(handle.type_name(), "io_handle");
521
522 io_write(
523 &[
524 handle.clone(),
525 ValueWord::from_string(std::sync::Arc::new("hello world".to_string())),
526 ],
527 &ctx,
528 )
529 .unwrap();
530 io_close(&[handle], &ctx).unwrap();
531
532 let handle2 = io_open(
534 &[ValueWord::from_string(std::sync::Arc::new(
535 path_str.clone(),
536 ))],
537 &ctx,
538 )
539 .unwrap();
540 let content = io_read_to_string(&[handle2.clone()], &ctx).unwrap();
541 assert_eq!(content.as_str().unwrap(), "hello world");
542 io_close(&[handle2], &ctx).unwrap();
543
544 let _ = std::fs::remove_file(&path);
546 let _ = std::fs::remove_dir(&dir);
547 }
548
549 #[test]
550 fn test_io_exists() {
551 let ctx = test_ctx();
552 let result = io_exists(
553 &[ValueWord::from_string(std::sync::Arc::new(
554 "/tmp".to_string(),
555 ))],
556 &ctx,
557 )
558 .unwrap();
559 assert_eq!(result.as_bool(), Some(true));
560
561 let result = io_exists(
562 &[ValueWord::from_string(std::sync::Arc::new(
563 "/nonexistent_path_xyz".to_string(),
564 ))],
565 &ctx,
566 )
567 .unwrap();
568 assert_eq!(result.as_bool(), Some(false));
569 }
570
571 #[test]
572 fn test_io_is_file_is_dir() {
573 let ctx = test_ctx();
574 let result = io_is_dir(
575 &[ValueWord::from_string(std::sync::Arc::new(
576 "/tmp".to_string(),
577 ))],
578 &ctx,
579 )
580 .unwrap();
581 assert_eq!(result.as_bool(), Some(true));
582
583 let result = io_is_file(
584 &[ValueWord::from_string(std::sync::Arc::new(
585 "/tmp".to_string(),
586 ))],
587 &ctx,
588 )
589 .unwrap();
590 assert_eq!(result.as_bool(), Some(false));
591 }
592
593 #[test]
594 fn test_io_stat() {
595 let ctx = test_ctx();
596 let result = io_stat(
597 &[ValueWord::from_string(std::sync::Arc::new(
598 "/tmp".to_string(),
599 ))],
600 &ctx,
601 )
602 .unwrap();
603 assert_eq!(result.type_name(), "object");
604 }
605
606 #[test]
607 fn test_io_mkdir_remove() {
608 let ctx = test_ctx();
609 let dir = std::env::temp_dir().join("shape_io_mkdir_test");
610 let path_str = dir.to_string_lossy().to_string();
611
612 let _ = std::fs::remove_dir_all(&dir);
613
614 io_mkdir(
615 &[ValueWord::from_string(std::sync::Arc::new(
616 path_str.clone(),
617 ))],
618 &ctx,
619 )
620 .unwrap();
621 assert!(dir.is_dir());
622
623 io_remove(
624 &[ValueWord::from_string(std::sync::Arc::new(
625 path_str.clone(),
626 ))],
627 &ctx,
628 )
629 .unwrap();
630 assert!(!dir.exists());
631 }
632
633 #[test]
634 fn test_io_read_dir() {
635 let ctx = test_ctx();
636 let result = io_read_dir(
637 &[ValueWord::from_string(std::sync::Arc::new(
638 "/tmp".to_string(),
639 ))],
640 &ctx,
641 )
642 .unwrap();
643 assert_eq!(result.type_name(), "array");
644 }
645
646 #[test]
647 fn test_io_rename() {
648 let ctx = test_ctx();
649 let dir = std::env::temp_dir().join("shape_io_rename_test");
650 let _ = std::fs::create_dir_all(&dir);
651 let old = dir.join("old.txt");
652 let new = dir.join("new.txt");
653 std::fs::write(&old, "data").unwrap();
654
655 io_rename(
656 &[
657 ValueWord::from_string(std::sync::Arc::new(old.to_string_lossy().to_string())),
658 ValueWord::from_string(std::sync::Arc::new(new.to_string_lossy().to_string())),
659 ],
660 &ctx,
661 )
662 .unwrap();
663
664 assert!(!old.exists());
665 assert!(new.exists());
666
667 let _ = std::fs::remove_dir_all(&dir);
668 }
669
670 #[test]
671 fn test_io_read_bytes() {
672 let ctx = test_ctx();
673 let dir = std::env::temp_dir().join("shape_io_bytes_test");
674 let _ = std::fs::create_dir_all(&dir);
675 let path = dir.join("bytes.bin");
676 std::fs::write(&path, &[1u8, 2, 3, 255]).unwrap();
677
678 let handle = io_open(
679 &[ValueWord::from_string(std::sync::Arc::new(
680 path.to_string_lossy().to_string(),
681 ))],
682 &ctx,
683 )
684 .unwrap();
685
686 let result = io_read_bytes(&[handle.clone()], &ctx).unwrap();
687 let arr = result.as_any_array().unwrap().to_generic();
688 assert_eq!(arr.len(), 4);
689
690 io_close(&[handle], &ctx).unwrap();
691 let _ = std::fs::remove_dir_all(&dir);
692 }
693
694 #[test]
695 fn test_io_close_returns_false_on_double_close() {
696 let ctx = test_ctx();
697 let dir = std::env::temp_dir().join("shape_io_double_close");
698 let _ = std::fs::create_dir_all(&dir);
699 let path = dir.join("double.txt");
700 std::fs::write(&path, "x").unwrap();
701
702 let handle = io_open(
703 &[ValueWord::from_string(std::sync::Arc::new(
704 path.to_string_lossy().to_string(),
705 ))],
706 &ctx,
707 )
708 .unwrap();
709
710 let first = io_close(&[handle.clone()], &ctx).unwrap();
711 assert_eq!(first.as_bool(), Some(true));
712
713 let second = io_close(&[handle], &ctx).unwrap();
714 assert_eq!(second.as_bool(), Some(false));
715
716 let _ = std::fs::remove_dir_all(&dir);
717 }
718
719 #[test]
720 fn test_io_open_invalid_mode() {
721 let ctx = test_ctx();
722 let result = io_open(
723 &[
724 ValueWord::from_string(std::sync::Arc::new("/tmp/test.txt".to_string())),
725 ValueWord::from_string(std::sync::Arc::new("x".to_string())),
726 ],
727 &ctx,
728 );
729 assert!(result.is_err());
730 }
731
732 #[test]
733 fn test_io_flush() {
734 let ctx = test_ctx();
735 let dir = std::env::temp_dir().join("shape_io_flush_test");
736 let _ = std::fs::create_dir_all(&dir);
737 let path = dir.join("flush.txt");
738
739 let handle = io_open(
740 &[
741 ValueWord::from_string(std::sync::Arc::new(path.to_string_lossy().to_string())),
742 ValueWord::from_string(std::sync::Arc::new("w".to_string())),
743 ],
744 &ctx,
745 )
746 .unwrap();
747
748 io_write(
749 &[
750 handle.clone(),
751 ValueWord::from_string(std::sync::Arc::new("data".to_string())),
752 ],
753 &ctx,
754 )
755 .unwrap();
756
757 let result = io_flush(&[handle.clone()], &ctx).unwrap();
758 assert!(result.is_unit());
759
760 io_close(&[handle], &ctx).unwrap();
761 let _ = std::fs::remove_dir_all(&dir);
762 }
763
764 #[test]
765 fn test_io_write_gzip_read_gzip_roundtrip() {
766 let ctx = test_ctx();
767 let dir = std::env::temp_dir().join("shape_io_gzip_test");
768 let _ = std::fs::create_dir_all(&dir);
769 let path = dir.join("test.gz");
770 let path_str = path.to_string_lossy().to_string();
771
772 io_write_gzip(
774 &[
775 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
776 ValueWord::from_string(std::sync::Arc::new("hello gzip world".to_string())),
777 ],
778 &ctx,
779 )
780 .unwrap();
781
782 assert!(path.exists());
783
784 let result = io_read_gzip(
786 &[ValueWord::from_string(std::sync::Arc::new(path_str))],
787 &ctx,
788 )
789 .unwrap();
790 assert_eq!(result.as_str(), Some("hello gzip world"));
791
792 let _ = std::fs::remove_dir_all(&dir);
793 }
794
795 #[test]
796 fn test_io_read_gzip_nonexistent() {
797 let ctx = test_ctx();
798 let result = io_read_gzip(
799 &[ValueWord::from_string(std::sync::Arc::new(
800 "/nonexistent/file.gz".to_string(),
801 ))],
802 &ctx,
803 );
804 assert!(result.is_err());
805 }
806
807 #[test]
808 fn test_io_write_gzip_with_level() {
809 let ctx = test_ctx();
810 let dir = std::env::temp_dir().join("shape_io_gzip_level_test");
811 let _ = std::fs::create_dir_all(&dir);
812 let path = dir.join("test_level.gz");
813 let path_str = path.to_string_lossy().to_string();
814
815 io_write_gzip(
816 &[
817 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
818 ValueWord::from_string(std::sync::Arc::new("level test".to_string())),
819 ValueWord::from_i64(1),
820 ],
821 &ctx,
822 )
823 .unwrap();
824
825 let result = io_read_gzip(
826 &[ValueWord::from_string(std::sync::Arc::new(path_str))],
827 &ctx,
828 )
829 .unwrap();
830 assert_eq!(result.as_str(), Some("level test"));
831
832 let _ = std::fs::remove_dir_all(&dir);
833 }
834}