Skip to main content

ferrijs_std/fs/
mod.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3mod access;
4mod chmod;
5mod file_handle;
6mod guard;
7mod mkdir;
8mod open;
9mod read_dir;
10mod read_file;
11mod rename;
12mod rm;
13mod stats;
14mod symlink;
15mod write_file;
16
17use crate::utils::module::ModuleInfo;
18use rquickjs::Value;
19use rquickjs::{
20    module::{Declarations, Exports, ModuleDef},
21    prelude::{Async, Func},
22};
23use rquickjs::{Class, Ctx, Object, Result};
24
25use self::file_handle::FileHandle;
26use self::guard::*;
27use self::read_dir::Dirent;
28use self::stats::Stats;
29
30pub const CONSTANT_F_OK: u32 = 0;
31pub const CONSTANT_R_OK: u32 = 4;
32pub const CONSTANT_W_OK: u32 = 2;
33pub const CONSTANT_X_OK: u32 = 1;
34
35pub struct FsPromisesModule;
36
37impl ModuleDef for FsPromisesModule {
38    fn declare(declare: &Declarations) -> Result<()> {
39        declare.declare("access")?;
40        declare.declare("open")?;
41        declare.declare("readFile")?;
42        declare.declare("writeFile")?;
43        declare.declare("rename")?;
44        declare.declare("readdir")?;
45        declare.declare("mkdir")?;
46        declare.declare("mkdtemp")?;
47        declare.declare("rm")?;
48        declare.declare("rmdir")?;
49        declare.declare("stat")?;
50        declare.declare("lstat")?;
51        declare.declare("constants")?;
52        declare.declare("chmod")?;
53        declare.declare("symlink")?;
54
55        declare.declare("default")?;
56
57        Ok(())
58    }
59
60    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
61        // The VM's ONE `fs/promises` namespace, not a second copy: Node
62        // answers the same object for `fs.promises` and for an import of
63        // this specifier.
64        export_object(exports, &fs_promises_object(ctx)?)
65    }
66}
67
68impl From<FsPromisesModule> for ModuleInfo<FsPromisesModule> {
69    fn from(val: FsPromisesModule) -> Self {
70        ModuleInfo {
71            name: "fs/promises",
72            module: val,
73        }
74    }
75}
76
77pub struct FsModule;
78
79impl ModuleDef for FsModule {
80    fn declare(declare: &Declarations) -> Result<()> {
81        declare.declare("promises")?;
82        declare.declare("accessSync")?;
83        declare.declare("mkdirSync")?;
84        declare.declare("mkdtempSync")?;
85        declare.declare("readdirSync")?;
86        declare.declare("readFileSync")?;
87        declare.declare("existsSync")?;
88        declare.declare("rmdirSync")?;
89        declare.declare("rmSync")?;
90        declare.declare("statSync")?;
91        declare.declare("lstatSync")?;
92        declare.declare("writeFileSync")?;
93        declare.declare("constants")?;
94        declare.declare("chmodSync")?;
95        declare.declare("renameSync")?;
96        declare.declare("symlinkSync")?;
97
98        declare.declare("default")?;
99
100        Ok(())
101    }
102
103    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
104        export_object(exports, &fs_object(ctx)?)
105    }
106}
107
108/// The `fs` namespace: every sync entry point, `promises`, `constants`.
109///
110/// Shared with the module definition so an `import fs from "node:fs"` and
111/// a host that installs `fs` some other way cannot disagree about what
112/// the surface is.
113pub fn fill_fs<'js>(ctx: &Ctx<'js>, target: &Object<'js>) -> Result<()> {
114    let promises = Object::new(ctx.clone())?;
115    export_promises(ctx, &promises)?;
116    export_constants(ctx, target)?;
117
118    // LOCAL DELTA: every entry point is the guarded wrapper from
119    // `guard.rs`, and `existsSync` (which upstream lacks) lives there
120    // too. See that file.
121    target.set("promises", promises)?;
122    target.set("accessSync", Func::from(access_sync_guarded))?;
123    target.set("mkdirSync", Func::from(mkdir_sync_guarded))?;
124    target.set("mkdtempSync", Func::from(mkdtemp_sync_guarded))?;
125    target.set("readdirSync", Func::from(read_dir_sync_guarded))?;
126    target.set("readFileSync", Func::from(read_file_sync_guarded))?;
127    target.set("existsSync", Func::from(exists_sync_guarded))?;
128    target.set("rmdirSync", Func::from(rmdir_sync_guarded))?;
129    target.set("rmSync", Func::from(rmfile_sync_guarded))?;
130    target.set("statSync", Func::from(stat_sync_guarded))?;
131    target.set("lstatSync", Func::from(lstat_sync_guarded))?;
132    target.set("writeFileSync", Func::from(write_file_sync_guarded))?;
133    target.set("chmodSync", Func::from(chmod_sync_guarded))?;
134    target.set("renameSync", Func::from(rename_sync_guarded))?;
135    target.set("symlinkSync", Func::from(symlink_sync_guarded))?;
136
137    Ok(())
138}
139
140/// The VM's one `fs` namespace object.
141///
142/// Built once per context and remembered, because Node answers the same
143/// object for every spelling: `require("fs") === require("node:fs")`, and
144/// the global is that object too. Handing back a fresh one each time
145/// would make those comparisons false and give a caller who patched a
146/// method a copy nobody else sees.
147pub fn fs_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
148    cached(ctx, false)
149}
150
151/// The VM's one `fs/promises` namespace object.
152pub fn fs_promises_object<'js>(ctx: &Ctx<'js>) -> Result<Object<'js>> {
153    cached(ctx, true)
154}
155
156struct FsNamespaces {
157    fs: rquickjs::Persistent<Object<'static>>,
158    promises: rquickjs::Persistent<Object<'static>>,
159}
160
161// SAFETY: holds only `Persistent`s, which are lifetime-erased by
162// construction.
163#[allow(unsafe_code)]
164unsafe impl rquickjs::JsLifetime<'_> for FsNamespaces {
165    type Changed<'to> = FsNamespaces;
166}
167
168fn cached<'js>(ctx: &Ctx<'js>, promises: bool) -> Result<Object<'js>> {
169    if let Some(ud) = ctx.userdata::<FsNamespaces>() {
170        let held = if promises { ud.promises.clone() } else { ud.fs.clone() };
171        if let Ok(obj) = held.restore(ctx) {
172            return Ok(obj);
173        }
174    }
175    define_classes(ctx)?;
176    let fs = Object::new(ctx.clone())?;
177    fill_fs(ctx, &fs)?;
178    let promises_obj = Object::new(ctx.clone())?;
179    export_promises(ctx, &promises_obj)?;
180    // `fs.promises` and the `fs/promises` module are the same object in
181    // Node, so the namespace built here is the one `fs` carries.
182    fs.set("promises", promises_obj.clone())?;
183    let answer = if promises { promises_obj.clone() } else { fs.clone() };
184    let _ = ctx.store_userdata(FsNamespaces {
185        fs: rquickjs::Persistent::save(ctx, fs),
186        promises: rquickjs::Persistent::save(ctx, promises_obj),
187    });
188    Ok(answer)
189}
190
191/// `Dirent` / `FileHandle` / `Stats` are returned BY these functions, so
192/// they have to be defined whichever way the surface was reached.
193fn define_classes(ctx: &Ctx<'_>) -> Result<()> {
194    let globals = ctx.globals();
195    Class::<Dirent>::define(&globals)?;
196    Class::<FileHandle>::define(&globals)?;
197    Class::<Stats>::define(&globals)?;
198    Ok(())
199}
200
201fn export_promises<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> {
202    export_constants(ctx, exports)?;
203
204    exports.set("access", Func::from(Async(access_guarded)))?;
205    exports.set("open", Func::from(Async(open_guarded)))?;
206    exports.set("readFile", Func::from(Async(read_file_guarded)))?;
207    exports.set("writeFile", Func::from(Async(write_file_guarded)))?;
208    exports.set("rename", Func::from(Async(rename_guarded)))?;
209    exports.set("readdir", Func::from(Async(read_dir_guarded)))?;
210    exports.set("mkdir", Func::from(Async(mkdir_guarded)))?;
211    exports.set("mkdtemp", Func::from(Async(mkdtemp_guarded)))?;
212    exports.set("rm", Func::from(Async(rmfile_guarded)))?;
213    exports.set("rmdir", Func::from(Async(rmdir_guarded)))?;
214    exports.set("stat", Func::from(Async(stat_guarded)))?;
215    exports.set("lstat", Func::from(Async(lstat_guarded)))?;
216    exports.set("chmod", Func::from(Async(chmod_guarded)))?;
217    exports.set("symlink", Func::from(Async(symlink_guarded)))?;
218
219    Ok(())
220}
221
222fn export_constants<'js>(ctx: &Ctx<'js>, exports: &Object<'js>) -> Result<()> {
223    let constants = Object::new(ctx.clone())?;
224    constants.set("F_OK", CONSTANT_F_OK)?;
225    constants.set("R_OK", CONSTANT_R_OK)?;
226    constants.set("W_OK", CONSTANT_W_OK)?;
227    constants.set("X_OK", CONSTANT_X_OK)?;
228
229    exports.set("constants", constants)?;
230
231    Ok(())
232}
233
234impl From<FsModule> for ModuleInfo<FsModule> {
235    fn from(val: FsModule) -> Self {
236        ModuleInfo {
237            name: "fs",
238            module: val,
239        }
240    }
241}
242
243/// Install `fs` as a global.
244///
245/// Node has no global `fs`. A host that wants one (a scripting surface
246/// where `fs.readFileSync` with no import is the expected ergonomics)
247/// opts in here; what it names is this module's own surface — the same
248/// object an `import` of `node:fs` answers with, so the two cannot drift.
249///
250/// # Errors
251///
252/// Returns an error if the global cannot be defined.
253pub fn init(ctx: &Ctx<'_>) -> Result<()> {
254    ctx.globals().set("fs", fs_object(ctx)?)
255}
256
257/// Export every member of `namespace`, plus `namespace` itself as
258/// `default`.
259///
260/// Unlike `export_default`, the object handed out IS the one passed in,
261/// so an import and the global stay the same object.
262fn export_object<'js>(exports: &Exports<'js>, namespace: &Object<'js>) -> Result<()> {
263    for name in namespace.keys::<String>() {
264        let name = name?;
265        let value: Value<'js> = namespace.get(&name)?;
266        exports.export(name, value)?;
267    }
268    exports.export("default", namespace.clone())?;
269    Ok(())
270}