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