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                                .await
383                                .map_err(|e| backend_error("fs.mkdir", &path, e))?;
384                            if !(recursive
385                                && filesystem
386                                    .metadata(&effect_id, Some(work.clone()), &path, true)
387                                    .await
388                                    .is_ok_and(|m| m.is_dir()))
389                            {
390                                require_writable(&*caller, ticket, &placement, "fs.mkdir", &path)?;
391                            }
392                            let result = filesystem
393                                .mkdir(&effect_id, Some(work.clone()), &path, recursive)
394                                .await;
395                            work.settle(&mut *caller)?;
396                            result.map_err(|e| backend_error("fs.mkdir", &path, e))?;
397                            Ok(())
398                        })
399                    },
400                )
401                .await
402            })
403        },
404    )?;
405
406    register_host_fn_async(
407        linker,
408        MODULE_NAME,
409        fs_fn("remove"),
410        FuncType::new(&engine, [string.clone(), ValType::I32], []),
411        |caller, params, _results| {
412            Box::pin(async move {
413                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.remove")?;
414                let recursive = i32_flag(abi_arg(params, 1)?, "fs.remove (recursive)")?;
415                let ticket = check_security_call(
416                    &mut *caller,
417                    "fs.remove",
418                    serde_json::json!({ "path": &path, "recursive": recursive }),
419                )?;
420                let request = effects::PathMutation { path, recursive };
421                runtime_effects::execute(
422                    caller,
423                    "fs.remove",
424                    request,
425                    move |caller, effect_id, request| {
426                        Box::pin(async move {
427                            charge_path_work(caller, &[&request.path])?;
428                            let effects::PathMutation { path, recursive } = request;
429                            let filesystem = caller.data().filesystem.clone();
430                            let work = begin_work(caller)?;
431                            let placement = filesystem
432                                .placement(&path)
433                                .await
434                                .map_err(|e| backend_error("fs.remove", &path, e))?;
435                            require_writable(&*caller, ticket, &placement, "fs.remove", &path)?;
436                            let mut budget = HostMutationBudget::new(caller);
437                            let result = filesystem
438                                .remove(
439                                    &effect_id,
440                                    Some(work.clone()),
441                                    &path,
442                                    recursive,
443                                    &mut budget,
444                                )
445                                .await;
446                            drop(budget);
447                            work.settle(&mut *caller)?;
448                            result.map_err(|error| backend_error("fs.remove", &path, error))
449                        })
450                    },
451                )
452                .await
453            })
454        },
455    )?;
456
457    register_host_fn_async(
458        linker,
459        MODULE_NAME,
460        fs_fn("move"),
461        FuncType::new(&engine, [string.clone(), string.clone()], []),
462        |caller, params, _results| {
463            Box::pin(async move {
464                let from = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.move (from)")?;
465                let to = read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.move (to)")?;
466                let ticket = check_security_call(
467                    &mut *caller,
468                    "fs.move",
469                    serde_json::json!({ "from": &from, "to": &to }),
470                )?;
471                let request = effects::MoveEntry { from, to };
472                runtime_effects::execute(
473                    caller,
474                    "fs.move",
475                    request,
476                    move |caller, effect_id, request| {
477                        Box::pin(async move {
478                            charge_path_work(caller, &[&request.from, &request.to])?;
479                            let effects::MoveEntry { from, to } = request;
480                            let filesystem = caller.data().filesystem.clone();
481                            let work = begin_work(caller)?;
482                            let placement = filesystem
483                                .placement(&from)
484                                .await
485                                .map_err(|e| backend_error("fs.move", &from, e))?;
486                            require_writable(&*caller, ticket, &placement, "fs.move", &from)?;
487                            let placement = filesystem
488                                .placement(&to)
489                                .await
490                                .map_err(|e| backend_error("fs.move", &to, e))?;
491                            require_writable(&*caller, ticket, &placement, "fs.move", &to)?;
492                            let mut budget = HostMutationBudget::new(caller);
493                            let result = filesystem
494                                .move_entry(&effect_id, Some(work.clone()), &from, &to, &mut budget)
495                                .await;
496                            drop(budget);
497                            work.settle(&mut *caller)?;
498                            result.map_err(|error| backend_error("fs.move", &to, error))
499                        })
500                    },
501                )
502                .await
503            })
504        },
505    )?;
506
507    register_host_fn_async(
508        linker,
509        MODULE_NAME,
510        fs_fn("copy"),
511        FuncType::new(&engine, [string.clone(), string.clone(), ValType::I32], []),
512        |caller, params, _results| {
513            Box::pin(async move {
514                let from = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.copy (from)")?;
515                let to = read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.copy (to)")?;
516                let recursive = i32_flag(abi_arg(params, 2)?, "fs.copy (recursive)")?;
517                let ticket = check_security_call(
518                    &mut *caller,
519                    "fs.copy",
520                    serde_json::json!({
521                        "from": &from,
522                        "to": &to,
523                        "recursive": recursive,
524                    }),
525                )?;
526                let request = effects::CopyEntry {
527                    from,
528                    to,
529                    recursive,
530                };
531                runtime_effects::execute(
532                    caller,
533                    "fs.copy",
534                    request,
535                    move |caller, effect_id, request| {
536                        Box::pin(async move {
537                            charge_path_work(caller, &[&request.from, &request.to])?;
538                            let effects::CopyEntry {
539                                from,
540                                to,
541                                recursive,
542                            } = request;
543                            let filesystem = caller.data().filesystem.clone();
544                            let work = begin_work(caller)?;
545                            let placement = filesystem
546                                .placement(&to)
547                                .await
548                                .map_err(|e| backend_error("fs.copy", &to, e))?;
549                            require_writable(&*caller, ticket, &placement, "fs.copy", &to)?;
550                            let mut budget = HostMutationBudget::new(caller);
551                            let result = filesystem
552                                .copy(
553                                    &effect_id,
554                                    Some(work.clone()),
555                                    &from,
556                                    &to,
557                                    recursive,
558                                    &mut budget,
559                                )
560                                .await;
561                            drop(budget);
562                            work.settle(&mut *caller)?;
563                            result.map_err(|error| backend_error("fs.copy", &to, error))
564                        })
565                    },
566                )
567                .await
568            })
569        },
570    )?;
571
572    register_host_value_async(
573        linker,
574        MODULE_NAME,
575        fs_fn("writer"),
576        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
577        |caller, params| {
578            Box::pin(async move {
579                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.writer (path)")?;
580                let ticket = check_security_call(
581                    &mut *caller,
582                    "fs.write",
583                    serde_json::json!({ "path": &path }),
584                )?;
585                open_writer(caller, ticket, &path).await
586            })
587        },
588    )?;
589
590    register_host_value_async(
591        linker,
592        MODULE_NAME,
593        fs_fn("lines"),
594        FuncType::new(&engine, [string.clone()], [nullable_object.clone()]),
595        |caller, params| {
596            Box::pin(async move {
597                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.lines (path)")?;
598                check_security_call(
599                    &mut *caller,
600                    "fs.read",
601                    serde_json::json!({ "path": &path }),
602                )?;
603                let options = crate::runtime::filesystem::ReaderOptions::Lines {
604                    maximum_bytes: caller.data().fs_max_read_size,
605                };
606                let reader = effects::open_reader(caller, path, options, "fs.lines").await?;
607                let reader = BackendReader::new(reader, &caller.data().tenant_limits)
608                    .map_err(wasmtime::Error::new)?;
609                make_handle_iterator(caller, reader, |caller, params| {
610                    Box::new(async move { reader_next(caller, params).await })
611                })
612            })
613        },
614    )?;
615
616    register_host_value_async(
617        linker,
618        MODULE_NAME,
619        fs_fn("bytes"),
620        FuncType::new(
621            &engine,
622            [string.clone(), ValType::F64],
623            [nullable_object.clone()],
624        ),
625        |caller, params| {
626            Box::pin(async move {
627                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.bytes (path)")?;
628                let chunk_size = f64_arg(abi_arg(params, 1)?, "fs.bytes (chunkSize)")? as i64;
629                if chunk_size <= 0 {
630                    return Err(crate::runtime::host::range_error(format!(
631                        "fs.bytes: chunkSize must be > 0, got {chunk_size}"
632                    )));
633                }
634                check_security_call(
635                    &mut *caller,
636                    "fs.read",
637                    serde_json::json!({ "path": &path, "chunkSize": chunk_size }),
638                )?;
639                let options = crate::runtime::filesystem::ReaderOptions::Bytes {
640                    chunk_size: chunk_size as usize,
641                };
642                let reader = effects::open_reader(caller, path, options, "fs.bytes").await?;
643                let reader = BackendReader::new(reader, &caller.data().tenant_limits)
644                    .map_err(wasmtime::Error::new)?;
645                make_handle_iterator(caller, reader, |caller, params| {
646                    Box::new(async move { reader_next(caller, params).await })
647                })
648            })
649        },
650    )?;
651
652    register_host_value_async(
653        linker,
654        MODULE_NAME,
655        fs_fn("list"),
656        FuncType::new(
657            &engine,
658            [string.clone(), ValType::I32],
659            [nullable_object.clone()],
660        ),
661        |caller, params| {
662            Box::pin(async move {
663                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.list (path)")?;
664                let recursive = i32_flag(abi_arg(params, 1)?, "fs.list (recursive)")?;
665                check_security_call(
666                    &mut *caller,
667                    "fs.list",
668                    serde_json::json!({ "path": &path, "recursive": recursive }),
669                )?;
670                let walk = effects::open_directory(caller, path.clone(), recursive).await?;
671                let dir = ChargedDirIter::new(walk, &caller.data().tenant_limits)
672                    .map_err(|e| wasmtime::Error::new(e).context(format!("fs.list {path}")))?;
673                make_handle_iterator(caller, dir, |caller, params| {
674                    Box::new(async move { list_next(caller, params).await })
675                })
676            })
677        },
678    )?;
679
680    register_host_value_async(
681        linker,
682        MODULE_NAME,
683        fs_fn("summary"),
684        FuncType::new(&engine, [string.clone()], [string.clone()]),
685        |caller, params| {
686            Box::pin(async move {
687                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.summary")?;
688                let text = summary::summarize(caller, &path).await?;
689                let value = write_submilli_string_struct(caller, &text)?;
690                Ok(Val::AnyRef(Some(value.to_anyref())))
691            })
692        },
693    )?;
694
695    register_host_value_async(
696        linker,
697        MODULE_NAME,
698        fs_fn("isEmpty"),
699        FuncType::new(&engine, [string.clone()], [ValType::I32]),
700        |caller, params| {
701            Box::pin(async move {
702                let path = read_string_arg(&mut *caller, abi_arg(params, 0)?, "fs.isEmpty")?;
703                let empty = summary::is_empty(caller, &path).await?;
704                Ok(Val::I32(i32::from(empty)))
705            })
706        },
707    )?;
708
709    for view in HOST_VIEWS {
710        install_field_getters(linker, &engine, &object, view)?;
711    }
712    install_file_writer_methods(linker, &engine, &intr, object)?;
713    Ok(())
714}
715
716// ---------------------------------------------------------------------------
717// Getters and FileWriter methods
718// ---------------------------------------------------------------------------
719
720const STAT_VIEW: HostView = HostView {
721    module: MODULE_NAME,
722    interface: "Stat",
723    backing: stat_backing_struct,
724    properties: &[
725        ("kind", STAT_KIND),
726        ("size", STAT_SIZE),
727        ("modifiedAt", STAT_MODIFIED_AT),
728    ],
729};
730
731const PEEK_VIEW: HostView = HostView {
732    module: MODULE_NAME,
733    interface: "Peek",
734    backing: peek_backing_struct,
735    properties: &[
736        ("preview", PEEK_PREVIEW),
737        ("encoding", PEEK_ENCODING),
738        ("lineEnding", PEEK_LINE_ENDING),
739        ("size", PEEK_SIZE),
740    ],
741};
742
743const DIR_ENTRY_VIEW: HostView = HostView {
744    module: MODULE_NAME,
745    interface: "DirEntry",
746    backing: dir_entry_backing_struct,
747    properties: &[
748        ("kind", ENTRY_KIND),
749        ("name", ENTRY_NAME),
750        ("path", ENTRY_PATH),
751        ("size", ENTRY_SIZE),
752    ],
753};
754
755const INFO_VIEW: HostView = HostView {
756    module: MODULE_NAME,
757    interface: "Info",
758    backing: info_backing_struct,
759    properties: &[
760        ("mode", INFO_MODE),
761        ("sizeLimit", INFO_SIZE_LIMIT),
762        ("access", INFO_ACCESS),
763        ("volume", INFO_VOLUME),
764        ("mounts", INFO_MOUNTS),
765    ],
766};
767
768const MOUNT_INFO_VIEW: HostView = HostView {
769    module: MODULE_NAME,
770    interface: "MountInfo",
771    backing: mount_info_backing_struct,
772    properties: &[
773        ("path", MOUNT_PATH),
774        ("mode", MOUNT_MODE),
775        ("volume", MOUNT_VOLUME),
776        ("access", MOUNT_ACCESS),
777        ("sizeLimit", MOUNT_SIZE_LIMIT),
778    ],
779};
780
781/// The host-backed interfaces this package's values are.
782pub(crate) const HOST_VIEWS: &[&HostView] = &[
783    &STAT_VIEW,
784    &PEEK_VIEW,
785    &DIR_ENTRY_VIEW,
786    &INFO_VIEW,
787    &MOUNT_INFO_VIEW,
788];
789
790fn install_file_writer_methods(
791    linker: &mut Linker<StoreData>,
792    engine: &wasmtime::Engine,
793    intr: &IntrinsicTypes,
794    receiver: ValType,
795) -> wasmtime::Result<()> {
796    let string = ValType::Ref(RefType::new(
797        false,
798        HeapType::ConcreteStruct(intr.string.clone()),
799    ));
800    let uint8 = ValType::Ref(RefType::new(
801        false,
802        HeapType::ConcreteStruct(intr.uint8_array.clone()),
803    ));
804    let writer_key = crate::mangle::package_symbol(MODULE_NAME, "FileWriter");
805
806    register_host_fn_async(
807        linker,
808        MODULE_NAME,
809        crate::mangle::extend(&writer_key, "close"),
810        FuncType::new(engine, [receiver.clone()], []),
811        |caller, params, _results| {
812            Box::pin(async move {
813                let receiver = *abi_arg(params, 0)?;
814                let descriptor = writer_payload(caller, &receiver, "fs.writer.close")?.id();
815                let live_receiver = receiver;
816                runtime_effects::execute(
817                    caller,
818                    "fs.writer.close",
819                    descriptor,
820                    move |caller, effect_id, _| {
821                        let receiver = live_receiver;
822                        Box::pin(async move {
823                            let work = begin_work(caller)?;
824                            let filesystem = caller.data().filesystem.clone();
825                            let result = writer_payload(caller, &receiver, "fs.writer.close")?
826                                .close(filesystem.as_ref(), &effect_id, Some(work.clone()))
827                                .await;
828                            work.settle(&mut *caller)?;
829                            result.map_err(|error| backend_error("fs.writer.close", "", error))
830                        })
831                    },
832                )
833                .await?;
834                writer_payload(caller, &receiver, "fs.writer.close")?.release_buffer();
835                Ok(())
836            })
837        },
838    )?;
839
840    register_host_fn_async(
841        linker,
842        MODULE_NAME,
843        crate::mangle::extend(&writer_key, "writeLine"),
844        FuncType::new(engine, [receiver.clone(), string], []),
845        |caller, params, _results| {
846            Box::pin(async move {
847                let line =
848                    read_string_arg(&mut *caller, abi_arg(params, 1)?, "fs.writer.writeLine")?;
849                let mut bytes = line.into_bytes();
850                bytes.push(b'\n');
851                let receiver = *abi_arg(params, 0)?;
852                let descriptor = writer_payload(caller, &receiver, "fs.writer.writeLine")?.id();
853                let request = effects::WriterChunk {
854                    resource: descriptor,
855                    bytes,
856                };
857                runtime_effects::execute(
858                    caller,
859                    "fs.writer.writeLine",
860                    request,
861                    move |caller, effect_id, request| {
862                        Box::pin(async move {
863                            fuel::charge(&mut *caller, fuel::IO, request.bytes.len() as u64)?;
864                            let work = begin_work(caller)?;
865                            let filesystem = caller.data().filesystem.clone();
866                            let result = writer_payload(caller, &receiver, "fs.writer.writeLine")?
867                                .write(
868                                    filesystem.as_ref(),
869                                    &effect_id,
870                                    Some(work.clone()),
871                                    request.bytes,
872                                )
873                                .await;
874                            work.settle(&mut *caller)?;
875                            result.map_err(|error| backend_error("fs.writer.writeLine", "", error))
876                        })
877                    },
878                )
879                .await?;
880                Ok(())
881            })
882        },
883    )?;
884
885    register_host_fn_async(
886        linker,
887        MODULE_NAME,
888        crate::mangle::extend(&writer_key, "writeBytes"),
889        FuncType::new(engine, [receiver, uint8], []),
890        |caller, params, _results| {
891            Box::pin(async move {
892                let bytes = read_uint8_array_arg(
893                    &mut *caller,
894                    abi_arg(params, 1)?,
895                    "fs.writer.writeBytes",
896                )?;
897                let receiver = *abi_arg(params, 0)?;
898                let descriptor = writer_payload(caller, &receiver, "fs.writer.writeBytes")?.id();
899                let request = effects::WriterChunk {
900                    resource: descriptor,
901                    bytes,
902                };
903                runtime_effects::execute(
904                    caller,
905                    "fs.writer.writeBytes",
906                    request,
907                    move |caller, effect_id, request| {
908                        Box::pin(async move {
909                            fuel::charge(&mut *caller, fuel::IO, request.bytes.len() as u64)?;
910                            let work = begin_work(caller)?;
911                            let filesystem = caller.data().filesystem.clone();
912                            let result = writer_payload(caller, &receiver, "fs.writer.writeBytes")?
913                                .write(
914                                    filesystem.as_ref(),
915                                    &effect_id,
916                                    Some(work.clone()),
917                                    request.bytes,
918                                )
919                                .await;
920                            work.settle(&mut *caller)?;
921                            result.map_err(|error| backend_error("fs.writer.writeBytes", "", error))
922                        })
923                    },
924                )
925                .await?;
926                Ok(())
927            })
928        },
929    )?;
930
931    Ok(())
932}