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_permission(ctx, shape_abi_v1::Permission::FsRead)?,
51 "w" | "a" => {
52 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?
53 }
54 "rw" => {
55 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
56 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
267 let path = args
268 .first()
269 .and_then(|a| a.as_str())
270 .ok_or_else(|| "io.exists() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
280 let path = args
281 .first()
282 .and_then(|a| a.as_str())
283 .ok_or_else(|| "io.stat() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
317 let path = args
318 .first()
319 .and_then(|a| a.as_str())
320 .ok_or_else(|| "io.is_file() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
330 let path = args
331 .first()
332 .and_then(|a| a.as_str())
333 .ok_or_else(|| "io.is_dir() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
343 let path = args
344 .first()
345 .and_then(|a| a.as_str())
346 .ok_or_else(|| "io.mkdir() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
364 let path = args
365 .first()
366 .and_then(|a| a.as_str())
367 .ok_or_else(|| "io.remove() requires a string path".to_string())?;
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 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
384 let old = args
385 .first()
386 .and_then(|a| a.as_str())
387 .ok_or_else(|| "io.rename() requires old path as first argument".to_string())?;
388 let new = args
389 .get(1)
390 .and_then(|a| a.as_str())
391 .ok_or_else(|| "io.rename() requires new path as second argument".to_string())?;
392
393 std::fs::rename(old, new).map_err(|e| format!("io.rename(\"{}\", \"{}\"): {}", old, new, e))?;
394 Ok(ValueWord::unit())
395}
396
397pub fn io_read_dir(
399 args: &[ValueWord],
400 ctx: &crate::module_exports::ModuleContext,
401) -> Result<ValueWord, String> {
402 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
403 let path = args
404 .first()
405 .and_then(|a| a.as_str())
406 .ok_or_else(|| "io.read_dir() requires a string path".to_string())?;
407
408 let entries: Vec<ValueWord> = std::fs::read_dir(path)
409 .map_err(|e| format!("io.read_dir(\"{}\"): {}", path, e))?
410 .filter_map(|entry| {
411 entry.ok().map(|e| {
412 ValueWord::from_string(std::sync::Arc::new(e.path().to_string_lossy().to_string()))
413 })
414 })
415 .collect();
416
417 Ok(ValueWord::from_array(std::sync::Arc::new(entries)))
418}
419
420pub fn io_read_gzip(
424 args: &[ValueWord],
425 ctx: &crate::module_exports::ModuleContext,
426) -> Result<ValueWord, String> {
427 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsRead)?;
428 let path = args
429 .first()
430 .and_then(|a| a.as_str())
431 .ok_or_else(|| "io.read_gzip() requires a string path argument".to_string())?;
432
433 let file =
434 std::fs::File::open(path).map_err(|e| format!("io.read_gzip(\"{}\"): {}", path, e))?;
435
436 let mut decoder = flate2::read::GzDecoder::new(file);
437 let mut output = String::new();
438 decoder
439 .read_to_string(&mut output)
440 .map_err(|e| format!("io.read_gzip(\"{}\"): decompression failed: {}", path, e))?;
441
442 Ok(ValueWord::from_string(std::sync::Arc::new(output)))
443}
444
445pub fn io_write_gzip(
449 args: &[ValueWord],
450 ctx: &crate::module_exports::ModuleContext,
451) -> Result<ValueWord, String> {
452 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::FsWrite)?;
453 let path = args
454 .first()
455 .and_then(|a| a.as_str())
456 .ok_or_else(|| "io.write_gzip() requires a string path argument".to_string())?;
457
458 let data = args
459 .get(1)
460 .and_then(|a| a.as_str())
461 .ok_or_else(|| "io.write_gzip() requires a string data argument".to_string())?;
462
463 let level = args
464 .get(2)
465 .and_then(|a| a.as_i64().or_else(|| a.as_f64().map(|n| n as i64)))
466 .unwrap_or(6) as u32;
467
468 let file =
469 std::fs::File::create(path).map_err(|e| format!("io.write_gzip(\"{}\"): {}", path, e))?;
470
471 let mut encoder = flate2::write::GzEncoder::new(file, flate2::Compression::new(level));
472 encoder
473 .write_all(data.as_bytes())
474 .map_err(|e| format!("io.write_gzip(\"{}\"): compression failed: {}", path, e))?;
475 encoder
476 .finish()
477 .map_err(|e| format!("io.write_gzip(\"{}\"): finalize failed: {}", path, e))?;
478
479 Ok(ValueWord::unit())
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
487 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
488 crate::module_exports::ModuleContext {
489 schemas: registry,
490 invoke_callable: None,
491 raw_invoker: None,
492 function_hashes: None,
493 vm_state: None,
494 granted_permissions: None,
495 scope_constraints: None,
496 set_pending_resume: None,
497 set_pending_frame_resume: None,
498 }
499 }
500
501 #[test]
502 fn test_io_open_write_read_close() {
503 let ctx = test_ctx();
504 let dir = std::env::temp_dir().join("shape_io_test");
505 let _ = std::fs::create_dir_all(&dir);
506 let path = dir.join("test_file.txt");
507 let path_str = path.to_string_lossy().to_string();
508
509 let handle = io_open(
511 &[
512 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
513 ValueWord::from_string(std::sync::Arc::new("w".to_string())),
514 ],
515 &ctx,
516 )
517 .unwrap();
518 assert_eq!(handle.type_name(), "io_handle");
519
520 io_write(
521 &[
522 handle.clone(),
523 ValueWord::from_string(std::sync::Arc::new("hello world".to_string())),
524 ],
525 &ctx,
526 )
527 .unwrap();
528 io_close(&[handle], &ctx).unwrap();
529
530 let handle2 = io_open(
532 &[ValueWord::from_string(std::sync::Arc::new(
533 path_str.clone(),
534 ))],
535 &ctx,
536 )
537 .unwrap();
538 let content = io_read_to_string(&[handle2.clone()], &ctx).unwrap();
539 assert_eq!(content.as_str().unwrap(), "hello world");
540 io_close(&[handle2], &ctx).unwrap();
541
542 let _ = std::fs::remove_file(&path);
544 let _ = std::fs::remove_dir(&dir);
545 }
546
547 #[test]
548 fn test_io_exists() {
549 let ctx = test_ctx();
550 let result = io_exists(
551 &[ValueWord::from_string(std::sync::Arc::new(
552 "/tmp".to_string(),
553 ))],
554 &ctx,
555 )
556 .unwrap();
557 assert_eq!(result.as_bool(), Some(true));
558
559 let result = io_exists(
560 &[ValueWord::from_string(std::sync::Arc::new(
561 "/nonexistent_path_xyz".to_string(),
562 ))],
563 &ctx,
564 )
565 .unwrap();
566 assert_eq!(result.as_bool(), Some(false));
567 }
568
569 #[test]
570 fn test_io_is_file_is_dir() {
571 let ctx = test_ctx();
572 let result = io_is_dir(
573 &[ValueWord::from_string(std::sync::Arc::new(
574 "/tmp".to_string(),
575 ))],
576 &ctx,
577 )
578 .unwrap();
579 assert_eq!(result.as_bool(), Some(true));
580
581 let result = io_is_file(
582 &[ValueWord::from_string(std::sync::Arc::new(
583 "/tmp".to_string(),
584 ))],
585 &ctx,
586 )
587 .unwrap();
588 assert_eq!(result.as_bool(), Some(false));
589 }
590
591 #[test]
592 fn test_io_stat() {
593 let ctx = test_ctx();
594 let result = io_stat(
595 &[ValueWord::from_string(std::sync::Arc::new(
596 "/tmp".to_string(),
597 ))],
598 &ctx,
599 )
600 .unwrap();
601 assert_eq!(result.type_name(), "object");
602 }
603
604 #[test]
605 fn test_io_mkdir_remove() {
606 let ctx = test_ctx();
607 let dir = std::env::temp_dir().join("shape_io_mkdir_test");
608 let path_str = dir.to_string_lossy().to_string();
609
610 let _ = std::fs::remove_dir_all(&dir);
611
612 io_mkdir(
613 &[ValueWord::from_string(std::sync::Arc::new(
614 path_str.clone(),
615 ))],
616 &ctx,
617 )
618 .unwrap();
619 assert!(dir.is_dir());
620
621 io_remove(
622 &[ValueWord::from_string(std::sync::Arc::new(
623 path_str.clone(),
624 ))],
625 &ctx,
626 )
627 .unwrap();
628 assert!(!dir.exists());
629 }
630
631 #[test]
632 fn test_io_read_dir() {
633 let ctx = test_ctx();
634 let result = io_read_dir(
635 &[ValueWord::from_string(std::sync::Arc::new(
636 "/tmp".to_string(),
637 ))],
638 &ctx,
639 )
640 .unwrap();
641 assert_eq!(result.type_name(), "array");
642 }
643
644 #[test]
645 fn test_io_rename() {
646 let ctx = test_ctx();
647 let dir = std::env::temp_dir().join("shape_io_rename_test");
648 let _ = std::fs::create_dir_all(&dir);
649 let old = dir.join("old.txt");
650 let new = dir.join("new.txt");
651 std::fs::write(&old, "data").unwrap();
652
653 io_rename(
654 &[
655 ValueWord::from_string(std::sync::Arc::new(old.to_string_lossy().to_string())),
656 ValueWord::from_string(std::sync::Arc::new(new.to_string_lossy().to_string())),
657 ],
658 &ctx,
659 )
660 .unwrap();
661
662 assert!(!old.exists());
663 assert!(new.exists());
664
665 let _ = std::fs::remove_dir_all(&dir);
666 }
667
668 #[test]
669 fn test_io_read_bytes() {
670 let ctx = test_ctx();
671 let dir = std::env::temp_dir().join("shape_io_bytes_test");
672 let _ = std::fs::create_dir_all(&dir);
673 let path = dir.join("bytes.bin");
674 std::fs::write(&path, &[1u8, 2, 3, 255]).unwrap();
675
676 let handle = io_open(
677 &[ValueWord::from_string(std::sync::Arc::new(
678 path.to_string_lossy().to_string(),
679 ))],
680 &ctx,
681 )
682 .unwrap();
683
684 let result = io_read_bytes(&[handle.clone()], &ctx).unwrap();
685 let arr = result.as_any_array().unwrap().to_generic();
686 assert_eq!(arr.len(), 4);
687
688 io_close(&[handle], &ctx).unwrap();
689 let _ = std::fs::remove_dir_all(&dir);
690 }
691
692 #[test]
693 fn test_io_close_returns_false_on_double_close() {
694 let ctx = test_ctx();
695 let dir = std::env::temp_dir().join("shape_io_double_close");
696 let _ = std::fs::create_dir_all(&dir);
697 let path = dir.join("double.txt");
698 std::fs::write(&path, "x").unwrap();
699
700 let handle = io_open(
701 &[ValueWord::from_string(std::sync::Arc::new(
702 path.to_string_lossy().to_string(),
703 ))],
704 &ctx,
705 )
706 .unwrap();
707
708 let first = io_close(&[handle.clone()], &ctx).unwrap();
709 assert_eq!(first.as_bool(), Some(true));
710
711 let second = io_close(&[handle], &ctx).unwrap();
712 assert_eq!(second.as_bool(), Some(false));
713
714 let _ = std::fs::remove_dir_all(&dir);
715 }
716
717 #[test]
718 fn test_io_open_invalid_mode() {
719 let ctx = test_ctx();
720 let result = io_open(
721 &[
722 ValueWord::from_string(std::sync::Arc::new("/tmp/test.txt".to_string())),
723 ValueWord::from_string(std::sync::Arc::new("x".to_string())),
724 ],
725 &ctx,
726 );
727 assert!(result.is_err());
728 }
729
730 #[test]
731 fn test_io_flush() {
732 let ctx = test_ctx();
733 let dir = std::env::temp_dir().join("shape_io_flush_test");
734 let _ = std::fs::create_dir_all(&dir);
735 let path = dir.join("flush.txt");
736
737 let handle = io_open(
738 &[
739 ValueWord::from_string(std::sync::Arc::new(path.to_string_lossy().to_string())),
740 ValueWord::from_string(std::sync::Arc::new("w".to_string())),
741 ],
742 &ctx,
743 )
744 .unwrap();
745
746 io_write(
747 &[
748 handle.clone(),
749 ValueWord::from_string(std::sync::Arc::new("data".to_string())),
750 ],
751 &ctx,
752 )
753 .unwrap();
754
755 let result = io_flush(&[handle.clone()], &ctx).unwrap();
756 assert!(result.is_unit());
757
758 io_close(&[handle], &ctx).unwrap();
759 let _ = std::fs::remove_dir_all(&dir);
760 }
761
762 #[test]
763 fn test_io_write_gzip_read_gzip_roundtrip() {
764 let ctx = test_ctx();
765 let dir = std::env::temp_dir().join("shape_io_gzip_test");
766 let _ = std::fs::create_dir_all(&dir);
767 let path = dir.join("test.gz");
768 let path_str = path.to_string_lossy().to_string();
769
770 io_write_gzip(
772 &[
773 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
774 ValueWord::from_string(std::sync::Arc::new("hello gzip world".to_string())),
775 ],
776 &ctx,
777 )
778 .unwrap();
779
780 assert!(path.exists());
781
782 let result = io_read_gzip(
784 &[ValueWord::from_string(std::sync::Arc::new(path_str))],
785 &ctx,
786 )
787 .unwrap();
788 assert_eq!(result.as_str(), Some("hello gzip world"));
789
790 let _ = std::fs::remove_dir_all(&dir);
791 }
792
793 #[test]
794 fn test_io_read_gzip_nonexistent() {
795 let ctx = test_ctx();
796 let result = io_read_gzip(
797 &[ValueWord::from_string(std::sync::Arc::new(
798 "/nonexistent/file.gz".to_string(),
799 ))],
800 &ctx,
801 );
802 assert!(result.is_err());
803 }
804
805 #[test]
806 fn test_io_write_gzip_with_level() {
807 let ctx = test_ctx();
808 let dir = std::env::temp_dir().join("shape_io_gzip_level_test");
809 let _ = std::fs::create_dir_all(&dir);
810 let path = dir.join("test_level.gz");
811 let path_str = path.to_string_lossy().to_string();
812
813 io_write_gzip(
814 &[
815 ValueWord::from_string(std::sync::Arc::new(path_str.clone())),
816 ValueWord::from_string(std::sync::Arc::new("level test".to_string())),
817 ValueWord::from_i64(1),
818 ],
819 &ctx,
820 )
821 .unwrap();
822
823 let result = io_read_gzip(
824 &[ValueWord::from_string(std::sync::Arc::new(path_str))],
825 &ctx,
826 )
827 .unwrap();
828 assert_eq!(result.as_str(), Some("level test"));
829
830 let _ = std::fs::remove_dir_all(&dir);
831 }
832}