Skip to main content

submilli_engine/stdlib/fs/
install.rs

1//! Explicit host registrations for filesystem operations and resource methods.
2use super::handles::{BackendReader, ChargedDirIter};
3use super::{
4    HostMutationBudget, MODULE_NAME, append, append_text, backend_error, begin_work, build_info,
5    charge_path_work, effects, f64_arg, i32_flag, list_next, make_handle_iterator, open_writer,
6    peek_file, read_byte_range, read_whole_capped, reader_next, stat_entry, summary, write,
7    write_text, writer_payload,
8};
9use crate::runtime::StoreData;
10use crate::runtime::call_log::{Payload, Side, record_payload};
11use crate::runtime::effects as runtime_effects;
12use crate::runtime::filesystem::FilesystemError as PortError;
13use crate::runtime::fs::ContainError;
14use crate::runtime::fuel;
15use crate::runtime::host::{
16    abi_arg, abi_result, read_string_arg, read_uint8_array_arg, register_host_fn,
17    register_host_fn_async, register_host_value_async, write_submilli_string_struct,
18    write_submilli_uint8array_struct,
19};
20use crate::runtime::intrinsic_types::{IntrinsicTypes, build_intrinsic_types};
21
22use crate::stdlib::shared::{check_and_approve_call, require_writable};
23use wasmtime::{FuncType, HeapType, Linker, RefType, Val, ValType};
24
25pub fn install(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
26    let engine = linker.engine().clone();
27    let intr = build_intrinsic_types(&engine)?;
28    let string = ValType::Ref(RefType::new(
29        false,
30        HeapType::ConcreteStruct(intr.string.clone()),
31    ));
32    let uint8 = ValType::Ref(RefType::new(
33        false,
34        HeapType::ConcreteStruct(intr.uint8_array.clone()),
35    ));
36    let object = ValType::Ref(RefType::new(
37        false,
38        HeapType::ConcreteStruct(intr.object.clone()),
39    ));
40    let nullable_object = ValType::Ref(RefType::new(
41        true,
42        HeapType::ConcreteStruct(intr.object.clone()),
43    ));
44
45    let fs_fn = |name: &str| crate::mangle::package_symbol(MODULE_NAME, name);
46
47    register_host_fn(
48        linker,
49        MODULE_NAME,
50        fs_fn("maxReadSize"),
51        FuncType::new(&engine, [], [ValType::F64]),
52        |caller, _params, results| {
53            *abi_result(results, 0)? = Val::F64((caller.data().fs_max_read_size as f64).to_bits());
54            Ok(())
55        },
56    )?;
57
58    register_host_fn(
59        linker,
60        MODULE_NAME,
61        fs_fn("cwd"),
62        FuncType::new(&engine, [], [string.clone()]),
63        |caller, _params, results| {
64            let cwd = caller.data().filesystem.cwd().to_owned();
65            let value = write_submilli_string_struct(&mut *caller, &cwd)?;
66            let result = results.first_mut().ok_or_else(|| {
67                crate::runtime::host::fatal_host_error("fs.cwd: missing result slot")
68            })?;
69            *result = Val::AnyRef(Some(value.to_anyref()));
70            Ok(())
71        },
72    )?;
73
74    register_host_fn(
75        linker,
76        MODULE_NAME,
77        fs_fn("info"),
78        FuncType::new(&engine, [], [nullable_object.clone()]),
79        |caller, _params, results| {
80            *abi_result(results, 0)? = build_info(caller)?;
81            Ok(())
82        },
83    )?;
84
85    register_host_value_async(
86        linker,
87        MODULE_NAME,
88        fs_fn("exists"),
89        FuncType::new(&engine, [string.clone()], [ValType::I32]),
90        |caller, params| {
91            Box::pin(async move {
92                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.exists")?;
93                let ticket = check_and_approve_call(
94                    &mut *caller,
95                    "fs.stat",
96                    serde_json::json!({ "path": &path }),
97                )
98                .await?;
99                let filesystem = caller.data().filesystem.clone();
100                // A path the sandbox cannot reach is reported absent, not trapped: that
101                // preserves the documented broken-link behaviour and denies the guest an
102                // existence oracle over host layout.
103                // `exists` is declared to mirror `std::path::exists` — a total predicate that
104                // never throws. Every way a path can be unreachable answers `false`: outside the
105                // sandbox, missing, a file used as a directory component, a symlink cycle. A
106                // guard clause written as `if (!exists(p))` is exactly what an LLM writes, and it
107                // must not abort on the malformed-path case it exists to handle.
108                let found = runtime_effects::execute(
109                    caller,
110                    "fs.exists",
111                    path,
112                    move |caller, effect_id, path| {
113                        Box::pin(async move {
114                            charge_path_work(caller, &[&path])?;
115                            let work = begin_work(caller)?;
116                            let found = match filesystem
117                                .metadata(&effect_id, Some(work.clone()), &path, true)
118                                .await
119                            {
120                                Ok(_) => true,
121                                Err(
122                                    PortError::Containment(
123                                        ContainError::Escape | ContainError::Io(_),
124                                    )
125                                    | PortError::Io(_),
126                                ) => false,
127                                Err(e) => return Err(backend_error("fs.exists", &path, e)),
128                            };
129                            work.settle(&mut *caller)?;
130                            Ok(found)
131                        })
132                    },
133                )
134                .await?;
135                record_payload(&*caller, ticket, Side::Response, || {
136                    Payload::meta(serde_json::json!({ "exists": found }))
137                });
138                Ok(Val::I32(i32::from(found)))
139            })
140        },
141    )?;
142
143    register_host_value_async(
144        linker,
145        MODULE_NAME,
146        fs_fn("size"),
147        FuncType::new(&engine, [string.clone()], [ValType::F64]),
148        |caller, params| {
149            Box::pin(async move {
150                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.size")?;
151                let ticket = check_and_approve_call(
152                    &mut *caller,
153                    "fs.stat",
154                    serde_json::json!({ "path": &path }),
155                )
156                .await?;
157                let size =
158                    runtime_effects::execute(caller, "fs.size", path, |caller, effect_id, path| {
159                        Box::pin(async move {
160                            charge_path_work(caller, &[&path])?;
161                            let work = begin_work(caller)?;
162                            let meta = caller
163                                .data()
164                                .filesystem
165                                .metadata(&effect_id, Some(work.clone()), &path, true)
166                                .await
167                                .map_err(|e| backend_error("fs.size", &path, e))?;
168                            if meta.is_dir() {
169                                wasmtime::bail!("fs.size {}: path is a directory", path);
170                            }
171                            work.settle(&mut *caller)?;
172                            Ok(meta.len())
173                        })
174                    })
175                    .await?;
176                record_payload(&*caller, ticket, Side::Response, || {
177                    Payload::meta(serde_json::json!({ "size": size }))
178                });
179                Ok(Val::F64((size as f64).to_bits()))
180            })
181        },
182    )?;
183
184    register_host_value_async(
185        linker,
186        MODULE_NAME,
187        fs_fn("stat"),
188        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
189        |caller, params| {
190            Box::pin(async move {
191                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.stat")?;
192                check_and_approve_call(
193                    &mut *caller,
194                    "fs.stat",
195                    serde_json::json!({ "path": &path }),
196                )
197                .await?;
198                stat_entry(caller, &path).await
199            })
200        },
201    )?;
202
203    register_host_value_async(
204        linker,
205        MODULE_NAME,
206        fs_fn("peek"),
207        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
208        |caller, params| {
209            Box::pin(async move {
210                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.peek")?;
211                check_and_approve_call(
212                    &mut *caller,
213                    "fs.stat",
214                    serde_json::json!({ "path": &path }),
215                )
216                .await?;
217                peek_file(caller, &path).await
218            })
219        },
220    )?;
221
222    register_host_value_async(
223        linker,
224        MODULE_NAME,
225        fs_fn("read"),
226        FuncType::new(&engine, [string.clone()], [uint8.clone()]),
227        |caller, params| {
228            Box::pin(async move {
229                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.read")?;
230                let output = read_whole_capped(caller, &path, "read").await?;
231                match &output {
232                    Some(bytes) => {
233                        let arr = write_submilli_uint8array_struct(caller, &bytes.0)?;
234                        Ok(Val::AnyRef(Some(arr.to_anyref())))
235                    }
236                    None => Err(crate::runtime::host::range_error(
237                        "file exceeds fs.maxReadSize()",
238                    )),
239                }
240            })
241        },
242    )?;
243
244    register_host_value_async(
245        linker,
246        MODULE_NAME,
247        fs_fn("readText"),
248        FuncType::new(&engine, [string.clone()], [string.clone()]),
249        |caller, params| {
250            Box::pin(async move {
251                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.readText")?;
252                let output = read_whole_capped(caller, &path, "readText").await?;
253                match &output {
254                    Some(bytes) => {
255                        // Lossy UTF-8: invalid bytes become U+FFFD rather than trapping.
256                        let mut text = String::from_utf8_lossy(&bytes.0).into_owned();
257                        if text.starts_with('\u{FEFF}') {
258                            text.remove(0);
259                        }
260                        let st = write_submilli_string_struct(caller, &text)?;
261                        Ok(Val::AnyRef(Some(st.to_anyref())))
262                    }
263                    None => Err(crate::runtime::host::range_error(
264                        "file exceeds fs.maxReadSize()",
265                    )),
266                }
267            })
268        },
269    )?;
270
271    register_host_value_async(
272        linker,
273        MODULE_NAME,
274        fs_fn("readBytes"),
275        FuncType::new(
276            &engine,
277            [string.clone(), ValType::F64, ValType::F64],
278            [uint8.clone()],
279        ),
280        |caller, params| {
281            Box::pin(async move {
282                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.readBytes")?;
283                let offset = f64_arg(abi_arg(params, 1)?, "fs.readBytes (offset)")? as i64;
284                let length = f64_arg(abi_arg(params, 2)?, "fs.readBytes (length)")? as i64;
285                let ticket = check_and_approve_call(
286                    &mut *caller,
287                    "fs.read",
288                    serde_json::json!({
289                        "path": &path,
290                        "length": length,
291                    }),
292                )
293                .await?;
294                let bytes = read_byte_range(caller, &path, offset, length).await?;
295                record_payload(&*caller, ticket, Side::Response, || {
296                    Payload::meta(serde_json::json!({ "offset": offset })).with_body(&bytes.0)
297                });
298                let arr = write_submilli_uint8array_struct(caller, &bytes.0)?;
299                Ok(Val::AnyRef(Some(arr.to_anyref())))
300            })
301        },
302    )?;
303
304    register_host_fn_async(
305        linker,
306        MODULE_NAME,
307        fs_fn("write"),
308        FuncType::new(&engine, [string.clone(), uint8.clone()], []),
309        |caller, params, _results| {
310            Box::pin(async move {
311                let path = read_string_arg(caller, abi_arg(params, 0)?, "fs.write")?;
312                let content = read_uint8_array_arg(caller, abi_arg(params, 1)?, "fs.write")?;
313                write(caller, path, content).await
314            })
315        },
316    )?;
317
318    register_host_fn_async(
319        linker,
320        MODULE_NAME,
321        fs_fn("writeText"),
322        FuncType::new(&engine, [string.clone(), string.clone()], []),
323        |caller, params, _results| {
324            Box::pin(async move {
325                let path = read_string_arg(caller, abi_arg(params, 0)?, "fs.writeText")?;
326                let content = read_string_arg(caller, abi_arg(params, 1)?, "fs.writeText")?;
327                write_text(caller, path, content).await
328            })
329        },
330    )?;
331
332    register_host_fn_async(
333        linker,
334        MODULE_NAME,
335        fs_fn("append"),
336        FuncType::new(&engine, [string.clone(), uint8.clone()], []),
337        |caller, params, _results| {
338            Box::pin(async move {
339                let path = read_string_arg(caller, abi_arg(params, 0)?, "fs.append")?;
340                let content = read_uint8_array_arg(caller, abi_arg(params, 1)?, "fs.append")?;
341                append(caller, path, content).await
342            })
343        },
344    )?;
345
346    register_host_fn_async(
347        linker,
348        MODULE_NAME,
349        fs_fn("appendText"),
350        FuncType::new(&engine, [string.clone(), string.clone()], []),
351        |caller, params, _results| {
352            Box::pin(async move {
353                let path = read_string_arg(caller, abi_arg(params, 0)?, "fs.appendText")?;
354                let content = read_string_arg(caller, abi_arg(params, 1)?, "fs.appendText")?;
355                append_text(caller, path, content).await
356            })
357        },
358    )?;
359
360    register_host_fn_async(
361        linker,
362        MODULE_NAME,
363        fs_fn("mkdir"),
364        FuncType::new(&engine, [string.clone(), nullable_object.clone()], []),
365        |caller, params, _results| {
366            Box::pin(async move {
367                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.mkdir")?;
368                let recursive = recursive_option(caller, abi_arg(params, 1)?).await?;
369                let ticket = check_and_approve_call(
370                    &mut *caller,
371                    "fs.mkdir",
372                    serde_json::json!({ "path": &path, "recursive": recursive }),
373                )
374                .await?;
375                let request = effects::PathMutation { path, recursive };
376                runtime_effects::execute(
377                    caller,
378                    "fs.mkdir",
379                    request,
380                    move |caller, effect_id, request| {
381                        Box::pin(async move {
382                            charge_path_work(caller, &[&request.path])?;
383                            let effects::PathMutation { path, recursive } = request;
384                            let filesystem = caller.data().filesystem.clone();
385                            let work = begin_work(caller)?;
386                            let placement = filesystem
387                                .placement(&path)
388                                .await
389                                .map_err(|e| backend_error("fs.mkdir", &path, e))?;
390                            if !(recursive
391                                && filesystem
392                                    .metadata(&effect_id, Some(work.clone()), &path, true)
393                                    .await
394                                    .is_ok_and(|m| m.is_dir()))
395                            {
396                                require_writable(&*caller, ticket, &placement, "fs.mkdir", &path)?;
397                            }
398                            let result = filesystem
399                                .mkdir(&effect_id, Some(work.clone()), &path, recursive)
400                                .await;
401                            work.settle(&mut *caller)?;
402                            result.map_err(|e| backend_error("fs.mkdir", &path, e))?;
403                            Ok(())
404                        })
405                    },
406                )
407                .await
408            })
409        },
410    )?;
411
412    register_host_fn_async(
413        linker,
414        MODULE_NAME,
415        fs_fn("remove"),
416        FuncType::new(&engine, [string.clone(), nullable_object.clone()], []),
417        |caller, params, _results| {
418            Box::pin(async move {
419                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.remove")?;
420                let recursive = recursive_option(caller, abi_arg(params, 1)?).await?;
421                let ticket = check_and_approve_call(
422                    &mut *caller,
423                    "fs.remove",
424                    serde_json::json!({ "path": &path, "recursive": recursive }),
425                )
426                .await?;
427                let request = effects::PathMutation { path, recursive };
428                runtime_effects::execute(
429                    caller,
430                    "fs.remove",
431                    request,
432                    move |caller, effect_id, request| {
433                        Box::pin(async move {
434                            charge_path_work(caller, &[&request.path])?;
435                            let effects::PathMutation { path, recursive } = request;
436                            let filesystem = caller.data().filesystem.clone();
437                            let work = begin_work(caller)?;
438                            let placement = filesystem
439                                .placement(&path)
440                                .await
441                                .map_err(|e| backend_error("fs.remove", &path, e))?;
442                            require_writable(&*caller, ticket, &placement, "fs.remove", &path)?;
443                            let mut budget = HostMutationBudget::new(caller);
444                            let result = filesystem
445                                .remove(
446                                    &effect_id,
447                                    Some(work.clone()),
448                                    &path,
449                                    recursive,
450                                    &mut budget,
451                                )
452                                .await;
453                            drop(budget);
454                            work.settle(&mut *caller)?;
455                            result.map_err(|error| backend_error("fs.remove", &path, error))
456                        })
457                    },
458                )
459                .await
460            })
461        },
462    )?;
463
464    register_host_fn_async(
465        linker,
466        MODULE_NAME,
467        fs_fn("move"),
468        FuncType::new(&engine, [string.clone(), string.clone()], []),
469        |caller, params, _results| {
470            Box::pin(async move {
471                let from = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.move (from)")?;
472                let to = read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.move (to)")?;
473                let ticket = check_and_approve_call(
474                    &mut *caller,
475                    "fs.move",
476                    serde_json::json!({ "from": &from, "to": &to }),
477                )
478                .await?;
479                let request = effects::MoveEntry { from, to };
480                runtime_effects::execute(
481                    caller,
482                    "fs.move",
483                    request,
484                    move |caller, effect_id, request| {
485                        Box::pin(async move {
486                            charge_path_work(caller, &[&request.from, &request.to])?;
487                            let effects::MoveEntry { from, to } = request;
488                            let filesystem = caller.data().filesystem.clone();
489                            let work = begin_work(caller)?;
490                            let placement = filesystem
491                                .placement(&from)
492                                .await
493                                .map_err(|e| backend_error("fs.move", &from, e))?;
494                            require_writable(&*caller, ticket, &placement, "fs.move", &from)?;
495                            let placement = filesystem
496                                .placement(&to)
497                                .await
498                                .map_err(|e| backend_error("fs.move", &to, e))?;
499                            require_writable(&*caller, ticket, &placement, "fs.move", &to)?;
500                            let mut budget = HostMutationBudget::new(caller);
501                            let result = filesystem
502                                .move_entry(&effect_id, Some(work.clone()), &from, &to, &mut budget)
503                                .await;
504                            drop(budget);
505                            work.settle(&mut *caller)?;
506                            result.map_err(|error| backend_error("fs.move", &to, error))
507                        })
508                    },
509                )
510                .await
511            })
512        },
513    )?;
514
515    register_host_fn_async(
516        linker,
517        MODULE_NAME,
518        fs_fn("copy"),
519        FuncType::new(&engine, [string.clone(), string.clone(), ValType::I32], []),
520        |caller, params, _results| {
521            Box::pin(async move {
522                let from = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.copy (from)")?;
523                let to = read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.copy (to)")?;
524                let recursive = i32_flag(abi_arg(params, 2)?, "fs.copy (recursive)")?;
525                let ticket = check_and_approve_call(
526                    &mut *caller,
527                    "fs.copy",
528                    serde_json::json!({
529                        "from": &from,
530                        "to": &to,
531                        "recursive": recursive,
532                    }),
533                )
534                .await?;
535                let request = effects::CopyEntry {
536                    from,
537                    to,
538                    recursive,
539                };
540                runtime_effects::execute(
541                    caller,
542                    "fs.copy",
543                    request,
544                    move |caller, effect_id, request| {
545                        Box::pin(async move {
546                            charge_path_work(caller, &[&request.from, &request.to])?;
547                            let effects::CopyEntry {
548                                from,
549                                to,
550                                recursive,
551                            } = request;
552                            let filesystem = caller.data().filesystem.clone();
553                            let work = begin_work(caller)?;
554                            let placement = filesystem
555                                .placement(&to)
556                                .await
557                                .map_err(|e| backend_error("fs.copy", &to, e))?;
558                            require_writable(&*caller, ticket, &placement, "fs.copy", &to)?;
559                            let mut budget = HostMutationBudget::new(caller);
560                            let result = filesystem
561                                .copy(
562                                    &effect_id,
563                                    Some(work.clone()),
564                                    &from,
565                                    &to,
566                                    recursive,
567                                    &mut budget,
568                                )
569                                .await;
570                            drop(budget);
571                            work.settle(&mut *caller)?;
572                            result.map_err(|error| backend_error("fs.copy", &to, error))
573                        })
574                    },
575                )
576                .await
577            })
578        },
579    )?;
580
581    register_host_value_async(
582        linker,
583        MODULE_NAME,
584        fs_fn("writer"),
585        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
586        |caller, params| {
587            Box::pin(async move {
588                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.writer (path)")?;
589                let ticket = check_and_approve_call(
590                    &mut *caller,
591                    "fs.write",
592                    serde_json::json!({ "path": &path }),
593                )
594                .await?;
595                open_writer(caller, ticket, &path).await
596            })
597        },
598    )?;
599
600    register_host_value_async(
601        linker,
602        MODULE_NAME,
603        fs_fn("lines"),
604        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
605        |caller, params| {
606            Box::pin(async move {
607                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.lines (path)")?;
608                check_and_approve_call(
609                    &mut *caller,
610                    "fs.read",
611                    serde_json::json!({ "path": &path }),
612                )
613                .await?;
614                let options = crate::runtime::filesystem::ReaderOptions::Lines {
615                    maximum_bytes: caller.data().fs_max_read_size,
616                };
617                let reader = effects::open_reader(caller, path, options, "fs.lines").await?;
618                let reader = BackendReader::new(reader, &caller.data().tenant_limits)
619                    .map_err(wasmtime::Error::new)?;
620                make_handle_iterator(caller, reader, |caller, params| {
621                    Box::new(async move { reader_next(caller, params).await })
622                })
623            })
624        },
625    )?;
626
627    register_host_value_async(
628        linker,
629        MODULE_NAME,
630        fs_fn("bytes"),
631        FuncType::new(
632            &engine,
633            [string.clone(), ValType::F64],
634            [nullable_object.clone()],
635        ),
636        |caller, params| {
637            Box::pin(async move {
638                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.bytes (path)")?;
639                let chunk_size = f64_arg(abi_arg(params, 1)?, "fs.bytes (chunkSize)")? as i64;
640                if chunk_size <= 0 {
641                    return Err(crate::runtime::host::range_error(format!(
642                        "fs.bytes: chunkSize must be > 0, got {chunk_size}"
643                    )));
644                }
645                check_and_approve_call(
646                    &mut *caller,
647                    "fs.read",
648                    serde_json::json!({ "path": &path, "chunkSize": chunk_size }),
649                )
650                .await?;
651                let options = crate::runtime::filesystem::ReaderOptions::Bytes {
652                    chunk_size: chunk_size as usize,
653                };
654                let reader = effects::open_reader(caller, path, options, "fs.bytes").await?;
655                let reader = BackendReader::new(reader, &caller.data().tenant_limits)
656                    .map_err(wasmtime::Error::new)?;
657                make_handle_iterator(caller, reader, |caller, params| {
658                    Box::new(async move { reader_next(caller, params).await })
659                })
660            })
661        },
662    )?;
663
664    register_host_value_async(
665        linker,
666        MODULE_NAME,
667        fs_fn("list"),
668        FuncType::new(
669            &engine,
670            [string.clone(), nullable_object.clone()],
671            [nullable_object.clone()],
672        ),
673        |caller, params| {
674            Box::pin(async move {
675                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.list (path)")?;
676                let recursive = recursive_option(caller, abi_arg(params, 1)?).await?;
677                check_and_approve_call(
678                    &mut *caller,
679                    "fs.list",
680                    serde_json::json!({ "path": &path, "recursive": recursive }),
681                )
682                .await?;
683                let walk = effects::open_directory(caller, path.clone(), recursive).await?;
684                let dir = ChargedDirIter::new(walk, &caller.data().tenant_limits)
685                    .map_err(|e| wasmtime::Error::new(e).context(format!("fs.list {path}")))?;
686                make_handle_iterator(caller, dir, |caller, params| {
687                    Box::new(async move { list_next(caller, params).await })
688                })
689            })
690        },
691    )?;
692
693    register_host_value_async(
694        linker,
695        MODULE_NAME,
696        fs_fn("summary"),
697        FuncType::new(&engine, [string.clone()], [string.clone()]),
698        |caller, params| {
699            Box::pin(async move {
700                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.summary")?;
701                let text = summary::summarize(caller, &path).await?;
702                let value = write_submilli_string_struct(caller, &text)?;
703                Ok(Val::AnyRef(Some(value.to_anyref())))
704            })
705        },
706    )?;
707
708    register_host_value_async(
709        linker,
710        MODULE_NAME,
711        fs_fn("isEmpty"),
712        FuncType::new(&engine, [string.clone()], [ValType::I32]),
713        |caller, params| {
714            Box::pin(async move {
715                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.isEmpty")?;
716                let empty = summary::is_empty(caller, &path).await?;
717                Ok(Val::I32(i32::from(empty)))
718            })
719        },
720    )?;
721
722    install_file_writer_methods(linker, &engine, &intr, object)?;
723    Ok(())
724}
725
726// ---------------------------------------------------------------------------
727// Getters and FileWriter methods
728// ---------------------------------------------------------------------------
729
730fn install_file_writer_methods(
731    linker: &mut Linker<StoreData>,
732    engine: &wasmtime::Engine,
733    intr: &IntrinsicTypes,
734    receiver: ValType,
735) -> wasmtime::Result<()> {
736    let string = ValType::Ref(RefType::new(
737        false,
738        HeapType::ConcreteStruct(intr.string.clone()),
739    ));
740    let uint8 = ValType::Ref(RefType::new(
741        false,
742        HeapType::ConcreteStruct(intr.uint8_array.clone()),
743    ));
744    let writer_key = crate::mangle::package_symbol(MODULE_NAME, "FileWriter");
745
746    register_host_fn_async(
747        linker,
748        MODULE_NAME,
749        crate::mangle::extend(&writer_key, "close"),
750        FuncType::new(engine, [receiver.clone()], []),
751        |caller, params, _results| {
752            Box::pin(async move {
753                let receiver = *abi_arg(params, 0)?;
754                let descriptor = writer_payload(caller, &receiver, "fs.writer.close")?.id();
755                let live_receiver = receiver;
756                runtime_effects::execute(
757                    caller,
758                    "fs.writer.close",
759                    descriptor,
760                    move |caller, effect_id, _| {
761                        let receiver = live_receiver;
762                        Box::pin(async move {
763                            let work = begin_work(caller)?;
764                            let filesystem = caller.data().filesystem.clone();
765                            let result = writer_payload(caller, &receiver, "fs.writer.close")?
766                                .close(filesystem.as_ref(), &effect_id, Some(work.clone()))
767                                .await;
768                            work.settle(&mut *caller)?;
769                            result.map_err(|error| backend_error("fs.writer.close", "", error))
770                        })
771                    },
772                )
773                .await?;
774                writer_payload(caller, &receiver, "fs.writer.close")?.release_buffer();
775                Ok(())
776            })
777        },
778    )?;
779
780    register_host_fn_async(
781        linker,
782        MODULE_NAME,
783        crate::mangle::extend(&writer_key, "writeLine"),
784        FuncType::new(engine, [receiver.clone(), string], []),
785        |caller, params, _results| {
786            Box::pin(async move {
787                let line =
788                    read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.writer.writeLine")?;
789                let mut bytes = line.into_bytes();
790                bytes.push(b'\n');
791                let receiver = *abi_arg(params, 0)?;
792                let descriptor = writer_payload(caller, &receiver, "fs.writer.writeLine")?.id();
793                let request = effects::WriterChunk {
794                    resource: descriptor,
795                    bytes,
796                };
797                runtime_effects::execute(
798                    caller,
799                    "fs.writer.writeLine",
800                    request,
801                    move |caller, effect_id, request| {
802                        Box::pin(async move {
803                            fuel::charge(&mut *caller, fuel::IO, request.bytes.len() as u64)?;
804                            let work = begin_work(caller)?;
805                            let filesystem = caller.data().filesystem.clone();
806                            let result = writer_payload(caller, &receiver, "fs.writer.writeLine")?
807                                .write(
808                                    filesystem.as_ref(),
809                                    &effect_id,
810                                    Some(work.clone()),
811                                    request.bytes,
812                                )
813                                .await;
814                            work.settle(&mut *caller)?;
815                            result.map_err(|error| backend_error("fs.writer.writeLine", "", error))
816                        })
817                    },
818                )
819                .await?;
820                Ok(())
821            })
822        },
823    )?;
824
825    register_host_fn_async(
826        linker,
827        MODULE_NAME,
828        crate::mangle::extend(&writer_key, "writeBytes"),
829        FuncType::new(engine, [receiver, uint8], []),
830        |caller, params, _results| {
831            Box::pin(async move {
832                let bytes = read_uint8_array_arg(
833                    &mut *caller,
834                    abi_arg(params, 1)?,
835                    "fs.writer.writeBytes",
836                )?;
837                let receiver = *abi_arg(params, 0)?;
838                let descriptor = writer_payload(caller, &receiver, "fs.writer.writeBytes")?.id();
839                let request = effects::WriterChunk {
840                    resource: descriptor,
841                    bytes,
842                };
843                runtime_effects::execute(
844                    caller,
845                    "fs.writer.writeBytes",
846                    request,
847                    move |caller, effect_id, request| {
848                        Box::pin(async move {
849                            fuel::charge(&mut *caller, fuel::IO, request.bytes.len() as u64)?;
850                            let work = begin_work(caller)?;
851                            let filesystem = caller.data().filesystem.clone();
852                            let result = writer_payload(caller, &receiver, "fs.writer.writeBytes")?
853                                .write(
854                                    filesystem.as_ref(),
855                                    &effect_id,
856                                    Some(work.clone()),
857                                    request.bytes,
858                                )
859                                .await;
860                            work.settle(&mut *caller)?;
861                            result.map_err(|error| backend_error("fs.writer.writeBytes", "", error))
862                        })
863                    },
864                )
865                .await?;
866                Ok(())
867            })
868        },
869    )?;
870
871    Ok(())
872}
873
874async fn recursive_option(
875    caller: &mut wasmtime::Caller<'_, StoreData>,
876    options: &Val,
877) -> wasmtime::Result<bool> {
878    use crate::runtime::prelude::{collection, undefined};
879    if undefined::is_undefined(caller, options)? {
880        return Ok(false);
881    }
882    let value =
883        crate::runtime::prelude::object::read_property(caller, options, "recursive").await?;
884    if undefined::is_undefined(caller, &value)? {
885        return Ok(false);
886    }
887    collection::unbox_bool(caller, &value)
888}