Skip to main content

nodejs/stdlib/
fs.rs

1//! Node `fs` module: synchronous file operations, the async callback forms, and
2//! the file-descriptor / directory / stream / watcher surfaces.
3//!
4//! Path operations map straight onto `std::fs`; descriptor operations
5//! (`open`/`read`/`write`/`fstat`/…) keep a thread-local table of open
6//! `std::fs::File`s keyed by a synthetic integer fd (starting at 3, after the
7//! stdio fds). Ownership / timestamp / statvfs operations that `std::fs` does not
8//! cover use `libc` directly (all targets are unix). Async callback variants run
9//! the same synchronous work then schedule the callback as a microtask, matching
10//! Node's ordering for simple scripts.
11
12use super::{arg_num, arg_str, native_tag};
13use crate::host::{invoke, with_host, JsObj};
14use fusevm::Value;
15use indexmap::IndexMap;
16use std::cell::RefCell;
17use std::collections::HashMap;
18use std::ffi::CString;
19use std::fs::File;
20use std::io::{Read, Seek, SeekFrom, Write};
21use std::os::unix::fs::{MetadataExt, PermissionsExt};
22use std::os::unix::io::AsRawFd;
23use std::path::{Path, PathBuf};
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::Arc;
26use std::time::{Duration, UNIX_EPOCH};
27
28pub const METHODS: &[&str] = &[
29    // path — sync
30    "readFileSync",
31    "writeFileSync",
32    "appendFileSync",
33    "existsSync",
34    "readdirSync",
35    "mkdirSync",
36    "rmdirSync",
37    "unlinkSync",
38    "rmSync",
39    "statSync",
40    "lstatSync",
41    "statfsSync",
42    "accessSync",
43    "chmodSync",
44    "chownSync",
45    "lchownSync",
46    "copyFileSync",
47    "cpSync",
48    "linkSync",
49    "symlinkSync",
50    "readlinkSync",
51    "realpathSync",
52    "renameSync",
53    "truncateSync",
54    "utimesSync",
55    "lutimesSync",
56    "mkdtempSync",
57    "opendirSync",
58    "globSync",
59    // fd — sync
60    "openSync",
61    "closeSync",
62    "readSync",
63    "writeSync",
64    "readvSync",
65    "writevSync",
66    "fstatSync",
67    "fchmodSync",
68    "fchownSync",
69    "ftruncateSync",
70    "futimesSync",
71    "fsyncSync",
72    "fdatasyncSync",
73    // path — async (callback)
74    "readFile",
75    "writeFile",
76    "appendFile",
77    "readdir",
78    "mkdir",
79    "rmdir",
80    "rm",
81    "unlink",
82    "stat",
83    "lstat",
84    "statfs",
85    "access",
86    "chmod",
87    "chown",
88    "lchown",
89    "copyFile",
90    "cp",
91    "link",
92    "symlink",
93    "readlink",
94    "realpath",
95    "rename",
96    "truncate",
97    "utimes",
98    "lutimes",
99    "mkdtemp",
100    "opendir",
101    "glob",
102    "exists",
103    // fd — async (callback)
104    "open",
105    "close",
106    "read",
107    "write",
108    "readv",
109    "writev",
110    "fstat",
111    "fchmod",
112    "fchown",
113    "ftruncate",
114    "futimes",
115    "fsync",
116    "fdatasync",
117    // watchers + streams
118    "watchFile",
119    "unwatchFile",
120    "createReadStream",
121    "createWriteStream",
122];
123
124// ── file-descriptor table ────────────────────────────────────────────────────
125
126thread_local! {
127    static FD_TABLE: RefCell<HashMap<i32, File>> = RefCell::new(HashMap::new());
128    static NEXT_FD: RefCell<i32> = const { RefCell::new(3) };
129    static WATCHERS: RefCell<Vec<WatchEntry>> = const { RefCell::new(Vec::new()) };
130    static NEXT_WATCH_ID: RefCell<u64> = const { RefCell::new(1) };
131}
132
133struct WatchEntry {
134    // Reserved for a future StatWatcher handle; assigned but not yet read.
135    #[allow(dead_code)]
136    id: u64,
137    path: String,
138    listener: Value,
139    stop: Arc<AtomicBool>,
140}
141
142fn register_fd(file: File) -> i32 {
143    NEXT_FD.with(|n| {
144        let fd = *n.borrow();
145        *n.borrow_mut() = fd + 1;
146        FD_TABLE.with(|t| t.borrow_mut().insert(fd, file));
147        fd
148    })
149}
150
151fn with_file<R>(fd: i32, f: impl FnOnce(&File) -> R) -> Option<R> {
152    FD_TABLE.with(|t| t.borrow().get(&fd).map(f))
153}
154
155fn close_fd(fd: i32) -> bool {
156    FD_TABLE.with(|t| t.borrow_mut().remove(&fd).is_some())
157}
158
159// ── dispatch ─────────────────────────────────────────────────────────────────
160
161pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
162    Some(match method {
163        // ── path, sync ──
164        "readFileSync" => read_file_sync(args),
165        "writeFileSync" => write_file_impl(args),
166        "appendFileSync" => append_file_impl(args),
167        "existsSync" => Ok(Value::Bool(Path::new(&arg_str(args, 0)).exists())),
168        "readdirSync" => readdir_impl(args),
169        "mkdirSync" => mkdir_impl(args),
170        "rmdirSync" | "unlinkSync" | "rmSync" => rm_impl(method, args),
171        "statSync" => stat_impl("statSync", args, true),
172        "lstatSync" => stat_impl("lstatSync", args, false),
173        "statfsSync" => statfs_impl(args),
174        "accessSync" => access_impl(args),
175        "chmodSync" => chmod_impl(args),
176        "chownSync" => chown_impl(args, true),
177        "lchownSync" => chown_impl(args, false),
178        "copyFileSync" => copy_file_impl(args),
179        "cpSync" => cp_impl(args),
180        "linkSync" => link_impl(args),
181        "symlinkSync" => symlink_impl(args),
182        "readlinkSync" => readlink_impl(args),
183        "realpathSync" => realpath_impl(args),
184        "renameSync" => rename_impl(args),
185        "truncateSync" => truncate_impl(args),
186        "utimesSync" => utimes_impl(args, true),
187        "lutimesSync" => utimes_impl(args, false),
188        "mkdtempSync" => mkdtemp_impl(args),
189        "opendirSync" => opendir_impl(args),
190        "globSync" => glob_impl(args),
191        // ── fd, sync ──
192        "openSync" => open_impl(args),
193        "closeSync" => close_impl(args),
194        "readSync" => read_impl(args).map(|n| Value::Float(n as f64)),
195        "writeSync" => write_impl(args).map(|n| Value::Float(n as f64)),
196        "readvSync" => readv_impl(args).map(|n| Value::Float(n as f64)),
197        "writevSync" => writev_impl(args).map(|n| Value::Float(n as f64)),
198        "fstatSync" => fstat_impl(args),
199        "fchmodSync" => fchmod_impl(args),
200        "fchownSync" => fchown_impl(args),
201        "ftruncateSync" => ftruncate_impl(args),
202        "futimesSync" => futimes_impl(args),
203        "fsyncSync" => fsync_impl(args, false),
204        "fdatasyncSync" => fsync_impl(args, true),
205        // ── path, async ──
206        "readFile" => return Some(read_file_async(args)),
207        "writeFile" => async_cb(args, write_file_impl(args)),
208        "appendFile" => async_cb(args, append_file_impl(args)),
209        "readdir" => async_cb(args, readdir_impl(args)),
210        "mkdir" => async_cb(args, mkdir_impl(args)),
211        "rmdir" => async_cb(args, rm_impl("rmdir", args)),
212        "rm" => async_cb(args, rm_impl("rm", args)),
213        "unlink" => async_cb(args, rm_impl("unlink", args)),
214        "stat" => async_cb(args, stat_impl("stat", args, true)),
215        "lstat" => async_cb(args, stat_impl("lstat", args, false)),
216        "statfs" => async_cb(args, statfs_impl(args)),
217        "access" => async_cb(args, access_impl(args)),
218        "chmod" => async_cb(args, chmod_impl(args)),
219        "chown" => async_cb(args, chown_impl(args, true)),
220        "lchown" => async_cb(args, chown_impl(args, false)),
221        "copyFile" => async_cb(args, copy_file_impl(args)),
222        "cp" => async_cb(args, cp_impl(args)),
223        "link" => async_cb(args, link_impl(args)),
224        "symlink" => async_cb(args, symlink_impl(args)),
225        "readlink" => async_cb(args, readlink_impl(args)),
226        "realpath" => async_cb(args, realpath_impl(args)),
227        "rename" => async_cb(args, rename_impl(args)),
228        "truncate" => async_cb(args, truncate_impl(args)),
229        "utimes" => async_cb(args, utimes_impl(args, true)),
230        "lutimes" => async_cb(args, utimes_impl(args, false)),
231        "mkdtemp" => async_cb(args, mkdtemp_impl(args)),
232        "opendir" => async_cb(args, opendir_impl(args)),
233        "glob" => async_cb(args, glob_impl(args)),
234        "exists" => exists_async(args),
235        // ── fd, async ──
236        "open" => async_cb(args, open_impl(args)),
237        "close" => async_cb(args, close_impl(args)),
238        "read" => return Some(read_write_async(args, read_impl(args))),
239        "write" => return Some(read_write_async(args, write_impl(args))),
240        "readv" => async_cb(args, readv_impl(args).map(|n| Value::Float(n as f64))),
241        "writev" => async_cb(args, writev_impl(args).map(|n| Value::Float(n as f64))),
242        "fstat" => async_cb(args, fstat_impl(args)),
243        "fchmod" => async_cb(args, fchmod_impl(args)),
244        "fchown" => async_cb(args, fchown_impl(args)),
245        "ftruncate" => async_cb(args, ftruncate_impl(args)),
246        "futimes" => async_cb(args, futimes_impl(args)),
247        "fsync" => async_cb(args, fsync_impl(args, false)),
248        "fdatasync" => async_cb(args, fsync_impl(args, true)),
249        // ── watchers + streams ──
250        "watchFile" => watch_file(args),
251        "unwatchFile" => unwatch_file(args),
252        "createReadStream" => create_read_stream(args),
253        "createWriteStream" => create_write_stream(args),
254        _ => return None,
255    })
256}
257
258// ── async callback plumbing ──────────────────────────────────────────────────
259
260/// Schedule the trailing callback as a microtask: `cb(null, value)` on success,
261/// `cb(err)` (an error string, matching the existing `readFile`/`writeFile`
262/// callbacks) on failure. Always returns `undefined` (the method's own result).
263fn async_cb(args: &[Value], result: Result<Value, String>) -> Result<Value, String> {
264    let Some(cb) = args.last().cloned().filter(is_fn) else {
265        return Ok(Value::Undef);
266    };
267    match result {
268        Ok(v) => with_host(|h| {
269            let n = h.null();
270            h.queue_micro(cb, vec![n, v]);
271        }),
272        // An error-first callback is handed an ERROR OBJECT, not the message:
273        // `fs.stat(missing, (err) => …)` gives an `err` carrying `code`,
274        // `syscall`, `path` and `errno`, and `err.code === 'ENOENT'` is the most
275        // common check in node code. A bare string made every one of those
276        // `undefined` — and `err.message` too, so even printing it said
277        // `undefined`. The promise forms already built the object.
278        Err(e) => with_host(|h| {
279            let ev = crate::builtins::synth_error(h, &e);
280            h.queue_micro(cb, vec![ev]);
281        }),
282    }
283    Ok(Value::Undef)
284}
285
286/// `read`/`write` callbacks carry a third argument: `cb(err, count, buffer)`.
287fn read_write_async(args: &[Value], result: Result<usize, String>) -> Result<Value, String> {
288    let Some(cb) = args.last().cloned().filter(is_fn) else {
289        return Ok(Value::Undef);
290    };
291    let buffer = args.get(1).cloned().unwrap_or(Value::Undef);
292    match result {
293        Ok(n) => with_host(|h| {
294            let nul = h.null();
295            h.queue_micro(cb, vec![nul, Value::Float(n as f64), buffer]);
296        }),
297        Err(e) => with_host(|h| {
298            let ev = crate::builtins::synth_error(h, &e);
299            h.queue_micro(cb, vec![ev]);
300        }),
301    }
302    Ok(Value::Undef)
303}
304
305fn is_fn(v: &Value) -> bool {
306    with_host(|h| crate::host::is_callable(h, v))
307}
308
309// ── read/write/append ────────────────────────────────────────────────────────
310
311fn read_file_sync(args: &[Value]) -> Result<Value, String> {
312    let path = arg_str(args, 0);
313    let enc = encoding_arg(args, 1);
314    match std::fs::read(&path) {
315        // The encoding names the REPRESENTATION to return, not just "decode as
316        // text": `readFileSync(p, 'hex')` gives the hex digits of the file, and
317        // `'base64'` its base64. Every encoding used to come back as the raw
318        // UTF-8 text.
319        Ok(bytes) => Ok(match enc {
320            Some(e) => with_host(|h| h.new_str(super::buffer::encode_bytes(&bytes, &e))),
321            None => super::buffer::from_bytes(&bytes),
322        }),
323        // A DIRECTORY opens fine and fails at the `read`, which is the syscall
324        // node names — and it names no path there, because the descriptor no
325        // longer carries one. This reported `open '<dir>'`, an operation that
326        // had in fact succeeded.
327        // The raw errno rather than `ErrorKind::IsADirectory`, which is stable
328        // only since 1.83 and this crate's MSRV is 1.80.
329        Err(e) if e.raw_os_error() == Some(libc::EISDIR) => Err(err_str("read", "", &e)),
330        Err(e) => Err(err_str("readFileSync", &path, &e)),
331    }
332}
333
334/// The bytes a `writeFileSync`/`appendFileSync` should put on disk.
335///
336/// When an encoding is given, the DATA STRING is in that encoding and has to be
337/// decoded first — `writeFileSync(p, '68656c6c6f', 'hex')` writes `hello`, five
338/// bytes, not the ten digits. The encoding argument was ignored, so the digits
339/// themselves were written. A Buffer argument carries its own bytes and is
340/// unaffected.
341fn encoded_bytes(args: &[Value]) -> Vec<u8> {
342    let v = args.get(1).cloned().unwrap_or(Value::Undef);
343    // Any byte VIEW writes its bytes; only a string goes through the encoding.
344    // The Buffer case alone was handled, so `writeFileSync(p, new Uint8Array(…))`
345    // wrote the text `[object Object]`.
346    if let Some(bytes) = super::buffer::view_bytes(&v) {
347        return bytes;
348    }
349    let text = with_host(|h| h.str_of(&v));
350    match encoding_arg(args, 2) {
351        Some(e) => super::buffer::decode_str(&text, &e),
352        None => text.into_bytes(),
353    }
354}
355
356fn write_file_impl(args: &[Value]) -> Result<Value, String> {
357    let path = arg_str(args, 0);
358    let data = encoded_bytes(args);
359    match std::fs::write(&path, data) {
360        Ok(_) => Ok(Value::Undef),
361        Err(e) => Err(err_str("writeFile", &path, &e)),
362    }
363}
364
365fn append_file_impl(args: &[Value]) -> Result<Value, String> {
366    let path = arg_str(args, 0);
367    let data = encoded_bytes(args);
368    let r = std::fs::OpenOptions::new()
369        .create(true)
370        .append(true)
371        .open(&path)
372        .and_then(|mut f| f.write_all(&data));
373    match r {
374        Ok(_) => Ok(Value::Undef),
375        Err(e) => Err(err_str("appendFile", &path, &e)),
376    }
377}
378
379fn read_file_async(args: &[Value]) -> Result<Value, String> {
380    let path = arg_str(args, 0);
381    let Some(cb) = args.last().cloned().filter(is_fn) else {
382        return Ok(Value::Undef);
383    };
384    let enc = if args.len() >= 3 {
385        encoding_arg(args, 1)
386    } else {
387        None
388    };
389    let (err, data) = match std::fs::read(&path) {
390        Ok(bytes) => (
391            with_host(|h| h.null()),
392            // Same representation rule as the sync read: the encoding names
393            // what to return, so `readFile(p, 'hex', cb)` hands the callback
394            // hex digits.
395            match enc {
396                Some(e) => with_host(|h| h.new_str(super::buffer::encode_bytes(&bytes, &e))),
397                None => super::buffer::from_bytes(&bytes),
398            },
399        ),
400        Err(e) => (
401            // An ERROR OBJECT, as every error-first callback receives — see
402            // `async_cb`; this path built its own and handed over a string.
403            with_host(|h| {
404                // The ASYNC form names the path where the sync one does not:
405                // node's `fs.readFile(dir, cb)` reports `read '<dir>'` and
406                // `fs.readFileSync(dir)` reports a bare `read`. Measured, not
407                // assumed — the two really do differ.
408                let msg = if e.raw_os_error() == Some(libc::EISDIR) {
409                    err_str("read", &path, &e)
410                } else {
411                    err_str("readFile", &path, &e)
412                };
413                crate::builtins::synth_error(h, &msg)
414            }),
415            Value::Undef,
416        ),
417    };
418    with_host(|h| h.queue_micro(cb, vec![err, data]));
419    Ok(Value::Undef)
420}
421
422fn exists_async(args: &[Value]) -> Result<Value, String> {
423    let path = arg_str(args, 0);
424    let Some(cb) = args.last().cloned().filter(is_fn) else {
425        return Ok(Value::Undef);
426    };
427    let ex = Path::new(&path).exists();
428    with_host(|h| h.queue_micro(cb, vec![Value::Bool(ex)]));
429    Ok(Value::Undef)
430}
431
432// ── directories ──────────────────────────────────────────────────────────────
433
434fn mkdir_impl(args: &[Value]) -> Result<Value, String> {
435    let path = arg_str(args, 0);
436    let recursive = opt_flag(args, "recursive");
437    let r = if recursive {
438        std::fs::create_dir_all(&path)
439    } else {
440        std::fs::create_dir(&path)
441    };
442    match r {
443        Ok(_) => Ok(Value::Undef),
444        Err(e) => Err(err_str("mkdir", &path, &e)),
445    }
446}
447
448fn rm_impl(op: &str, args: &[Value]) -> Result<Value, String> {
449    let path = arg_str(args, 0);
450    let p = Path::new(&path);
451    let force = opt_flag(args, "force");
452    let r = if p.is_dir() {
453        if opt_flag(args, "recursive") {
454            std::fs::remove_dir_all(p)
455        } else {
456            std::fs::remove_dir(p)
457        }
458    } else {
459        std::fs::remove_file(p)
460    };
461    match r {
462        Ok(_) => Ok(Value::Undef),
463        Err(e) if force && e.kind() == std::io::ErrorKind::NotFound => Ok(Value::Undef),
464        Err(e) => Err(err_str(op, &path, &e)),
465    }
466}
467
468fn readdir_impl(args: &[Value]) -> Result<Value, String> {
469    let path = arg_str(args, 0);
470    let file_types = opt_flag(args, "withFileTypes");
471    let recursive = opt_flag(args, "recursive");
472    let mut names: Vec<(String, String, std::fs::FileType)> = Vec::new();
473    collect_dir(Path::new(&path), &path, "", recursive, &mut names)
474        .map_err(|e| err_str("readdir", &path, &e))?;
475    names.sort_by(|a, b| a.0.cmp(&b.0));
476    Ok(with_host(|h| {
477        let items: Vec<Value> = names
478            .into_iter()
479            .map(|(rel, parent, ft)| {
480                if file_types {
481                    let base = rel.rsplit('/').next().unwrap_or(&rel).to_string();
482                    build_dirent(h, base, &parent, ft)
483                } else {
484                    h.new_str(rel)
485                }
486            })
487            .collect();
488        h.new_array(items)
489    }))
490}
491
492/// Collect directory entries. `rel_prefix` is the path (relative to the root)
493/// under which `dir`'s children are listed; `parent` is the absolute-ish parent
494/// path recorded on each `Dirent`.
495fn collect_dir(
496    dir: &Path,
497    parent: &str,
498    rel_prefix: &str,
499    recursive: bool,
500    out: &mut Vec<(String, String, std::fs::FileType)>,
501) -> std::io::Result<()> {
502    for e in std::fs::read_dir(dir)? {
503        let e = e?;
504        let name = e.file_name().to_string_lossy().into_owned();
505        let rel = if rel_prefix.is_empty() {
506            name.clone()
507        } else {
508            format!("{rel_prefix}/{name}")
509        };
510        let ft = e.file_type()?;
511        out.push((rel.clone(), parent.to_string(), ft));
512        if recursive && ft.is_dir() {
513            let sub = e.path();
514            let sub_parent = sub.to_string_lossy().into_owned();
515            collect_dir(&sub, &sub_parent, &rel, recursive, out)?;
516        }
517    }
518    Ok(())
519}
520
521fn opendir_impl(args: &[Value]) -> Result<Value, String> {
522    let path = arg_str(args, 0);
523    let rd = std::fs::read_dir(&path).map_err(|e| err_str("opendir", &path, &e))?;
524    let mut entries: Vec<(String, std::fs::FileType)> = rd
525        .filter_map(|e| e.ok())
526        .filter_map(|e| {
527            e.file_type()
528                .ok()
529                .map(|ft| (e.file_name().to_string_lossy().into_owned(), ft))
530        })
531        .collect();
532    entries.sort_by(|a, b| a.0.cmp(&b.0));
533    Ok(with_host(|h| {
534        let dirents: Vec<Value> = entries
535            .into_iter()
536            .map(|(name, ft)| build_dirent(h, name, &path, ft))
537            .collect();
538        let arr = h.new_array(dirents);
539        let mut m = IndexMap::new();
540        m.insert("@@native".into(), h.new_str("Dir"));
541        m.insert("path".into(), h.new_str(path.clone()));
542        m.insert("@@entries".into(), arr);
543        m.insert("@@pos".into(), Value::Float(0.0));
544        h.new_object(m)
545    }))
546}
547
548// ── ownership / permissions / timestamps ─────────────────────────────────────
549
550fn access_impl(args: &[Value]) -> Result<Value, String> {
551    let path = arg_str(args, 0);
552    match std::fs::metadata(&path) {
553        Ok(_) => Ok(Value::Undef),
554        Err(e) => Err(err_str("access", &path, &e)),
555    }
556}
557
558fn chmod_impl(args: &[Value]) -> Result<Value, String> {
559    let path = arg_str(args, 0);
560    let mode = arg_num(args, 1) as u32;
561    match std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)) {
562        Ok(_) => Ok(Value::Undef),
563        Err(e) => Err(err_str("chmod", &path, &e)),
564    }
565}
566
567fn chown_impl(args: &[Value], follow: bool) -> Result<Value, String> {
568    let path = arg_str(args, 0);
569    let uid = arg_num(args, 1) as libc::uid_t;
570    let gid = arg_num(args, 2) as libc::gid_t;
571    let c = cpath(&path, "chown")?;
572    let rc = unsafe {
573        if follow {
574            libc::chown(c.as_ptr(), uid, gid)
575        } else {
576            libc::lchown(c.as_ptr(), uid, gid)
577        }
578    };
579    ok_or_errno(rc, "chown", &path)
580}
581
582fn utimes_impl(args: &[Value], follow: bool) -> Result<Value, String> {
583    let path = arg_str(args, 0);
584    let times = [
585        to_timeval(time_secs(args, 1)),
586        to_timeval(time_secs(args, 2)),
587    ];
588    let c = cpath(&path, "utimes")?;
589    let rc = unsafe {
590        if follow {
591            libc::utimes(c.as_ptr(), times.as_ptr())
592        } else {
593            libc::lutimes(c.as_ptr(), times.as_ptr())
594        }
595    };
596    ok_or_errno(rc, "utimes", &path)
597}
598
599// ── copy / link / rename / truncate ──────────────────────────────────────────
600
601const COPYFILE_EXCL: u32 = 1;
602
603fn copy_file_impl(args: &[Value]) -> Result<Value, String> {
604    let src = arg_str(args, 0);
605    let dest = arg_str(args, 1);
606    let mode = arg_num(args, 2);
607    if !mode.is_nan() && (mode as u32) & COPYFILE_EXCL != 0 && Path::new(&dest).exists() {
608        return Err(format!(
609            "Error: EEXIST: file already exists, copyfile '{src}' -> '{dest}'"
610        ));
611    }
612    match std::fs::copy(&src, &dest) {
613        Ok(_) => Ok(Value::Undef),
614        Err(e) => Err(err_str2("copyFile", &src, &dest, &e)),
615    }
616}
617
618fn cp_impl(args: &[Value]) -> Result<Value, String> {
619    let src = arg_str(args, 0);
620    let dest = arg_str(args, 1);
621    let recursive = opt_flag(args, "recursive");
622    let r = if recursive {
623        cp_recursive(Path::new(&src), Path::new(&dest))
624    } else {
625        std::fs::copy(&src, &dest).map(|_| ())
626    };
627    match r {
628        Ok(_) => Ok(Value::Undef),
629        Err(e) => Err(err_str("cp", &src, &e)),
630    }
631}
632
633fn cp_recursive(src: &Path, dest: &Path) -> std::io::Result<()> {
634    if src.is_dir() {
635        std::fs::create_dir_all(dest)?;
636        for e in std::fs::read_dir(src)? {
637            let e = e?;
638            cp_recursive(&e.path(), &dest.join(e.file_name()))?;
639        }
640        Ok(())
641    } else {
642        if let Some(parent) = dest.parent() {
643            std::fs::create_dir_all(parent).ok();
644        }
645        std::fs::copy(src, dest).map(|_| ())
646    }
647}
648
649fn link_impl(args: &[Value]) -> Result<Value, String> {
650    let existing = arg_str(args, 0);
651    let new = arg_str(args, 1);
652    match std::fs::hard_link(&existing, &new) {
653        Ok(_) => Ok(Value::Undef),
654        Err(e) => Err(err_str("link", &existing, &e)),
655    }
656}
657
658fn symlink_impl(args: &[Value]) -> Result<Value, String> {
659    let target = arg_str(args, 0);
660    let path = arg_str(args, 1);
661    match std::os::unix::fs::symlink(&target, &path) {
662        Ok(_) => Ok(Value::Undef),
663        Err(e) => Err(err_str("symlink", &path, &e)),
664    }
665}
666
667fn readlink_impl(args: &[Value]) -> Result<Value, String> {
668    let path = arg_str(args, 0);
669    match std::fs::read_link(&path) {
670        Ok(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().into_owned()))),
671        Err(e) => Err(err_str("readlink", &path, &e)),
672    }
673}
674
675fn realpath_impl(args: &[Value]) -> Result<Value, String> {
676    let path = arg_str(args, 0);
677    match std::fs::canonicalize(&path) {
678        Ok(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().into_owned()))),
679        // The message names the path as far as it was RESOLVED before the walk
680        // failed — on macOS `/var` is a symlink, so node reports
681        // `/private/var/…` where the caller wrote `/var/…`. Reporting the
682        // argument verbatim names a path the failing `lstat` never saw.
683        Err(e) => Err(err_str("realpath", &resolved_prefix(&path), &e)),
684    }
685}
686
687/// `path` with its deepest EXISTING ancestor canonicalized and the rest of the
688/// components appended — what `realpath(3)` has resolved at the point it gives
689/// up.
690fn resolved_prefix(path: &str) -> String {
691    let p = Path::new(path);
692    let mut rest: Vec<&std::ffi::OsStr> = Vec::new();
693    let mut cur = p;
694    loop {
695        if let Ok(base) = std::fs::canonicalize(cur) {
696            let mut out = base;
697            for part in rest.iter().rev() {
698                out.push(part);
699            }
700            return out.to_string_lossy().into_owned();
701        }
702        match (cur.file_name(), cur.parent()) {
703            (Some(name), Some(parent)) if !parent.as_os_str().is_empty() => {
704                rest.push(name);
705                cur = parent;
706            }
707            _ => return path.to_string(),
708        }
709    }
710}
711
712fn rename_impl(args: &[Value]) -> Result<Value, String> {
713    let from = arg_str(args, 0);
714    let to = arg_str(args, 1);
715    match std::fs::rename(&from, &to) {
716        Ok(_) => Ok(Value::Undef),
717        Err(e) => Err(err_str2("rename", &from, &to, &e)),
718    }
719}
720
721fn truncate_impl(args: &[Value]) -> Result<Value, String> {
722    let path = arg_str(args, 0);
723    let len = arg_num(args, 1);
724    let len = if len.is_nan() { 0 } else { len as u64 };
725    let r = std::fs::OpenOptions::new()
726        .write(true)
727        .open(&path)
728        .and_then(|f| f.set_len(len));
729    match r {
730        Ok(_) => Ok(Value::Undef),
731        // The failure is in the `open` this does first, which is the syscall
732        // node names — `truncate` only appears once a descriptor exists.
733        Err(e) => Err(err_str("open", &path, &e)),
734    }
735}
736
737fn mkdtemp_impl(args: &[Value]) -> Result<Value, String> {
738    let prefix = arg_str(args, 0);
739    for _ in 0..64 {
740        let candidate = format!("{prefix}{}", random_suffix());
741        match std::fs::create_dir(&candidate) {
742            Ok(_) => return Ok(with_host(|h| h.new_str(candidate))),
743            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
744            Err(e) => return Err(err_str("mkdtemp", &prefix, &e)),
745        }
746    }
747    Err(format!(
748        "Error: EEXIST: file already exists, mkdtemp '{prefix}'"
749    ))
750}
751
752/// Six random `[0-9A-Za-z]` characters — Node's `mkdtemp` suffix alphabet.
753fn random_suffix() -> String {
754    const CHARS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
755    let mut raw = [0u8; 6];
756    if getrandom::getrandom(&mut raw).is_err() {
757        // Fall back to a clock-derived seed; uniqueness is retried by the caller.
758        let nanos = std::time::SystemTime::now()
759            .duration_since(UNIX_EPOCH)
760            .map(|d| d.subsec_nanos())
761            .unwrap_or(0);
762        raw = nanos
763            .to_le_bytes()
764            .iter()
765            .cycle()
766            .take(6)
767            .copied()
768            .collect::<Vec<_>>()
769            .try_into()
770            .unwrap();
771    }
772    raw.iter()
773        .map(|b| CHARS[(*b as usize) % 62] as char)
774        .collect()
775}
776
777// ── file descriptors ─────────────────────────────────────────────────────────
778
779fn open_impl(args: &[Value]) -> Result<Value, String> {
780    let path = arg_str(args, 0);
781    let flags = match args.get(1) {
782        Some(v) if !matches!(v, Value::Undef) && !is_fn(v) => arg_str(args, 1),
783        _ => "r".to_string(),
784    };
785    match open_options(&flags).open(&path) {
786        Ok(f) => Ok(Value::Float(register_fd(f) as f64)),
787        Err(e) => Err(err_str("open", &path, &e)),
788    }
789}
790
791fn open_options(flags: &str) -> std::fs::OpenOptions {
792    let mut o = std::fs::OpenOptions::new();
793    match flags {
794        "r" | "rs" | "sr" => {
795            o.read(true);
796        }
797        "r+" | "rs+" | "sr+" => {
798            o.read(true).write(true);
799        }
800        "w" => {
801            o.write(true).create(true).truncate(true);
802        }
803        "wx" | "xw" => {
804            o.write(true).create_new(true);
805        }
806        "w+" => {
807            o.read(true).write(true).create(true).truncate(true);
808        }
809        "wx+" | "xw+" => {
810            o.read(true).write(true).create_new(true);
811        }
812        "a" => {
813            o.append(true).create(true);
814        }
815        "ax" | "xa" => {
816            o.append(true).create_new(true);
817        }
818        "a+" => {
819            o.read(true).append(true).create(true);
820        }
821        "ax+" | "xa+" => {
822            o.read(true).append(true).create_new(true);
823        }
824        _ => {
825            o.read(true);
826        }
827    }
828    o
829}
830
831fn close_impl(args: &[Value]) -> Result<Value, String> {
832    let fd = arg_num(args, 0) as i32;
833    if close_fd(fd) {
834        Ok(Value::Undef)
835    } else {
836        Err("Error: EBADF: bad file descriptor, close".to_string())
837    }
838}
839
840fn read_impl(args: &[Value]) -> Result<usize, String> {
841    let fd = arg_num(args, 0) as i32;
842    let buffer = args.get(1).cloned().unwrap_or(Value::Undef);
843    let cap = buf_len(&buffer);
844    let offset = num_or(args, 2, 0.0) as usize;
845    let length = num_or(args, 3, (cap.saturating_sub(offset)) as f64) as usize;
846    let position = position_arg(args, 4);
847    let n = with_file(fd, |file| {
848        let mut fr: &File = file;
849        if let Some(pos) = position {
850            fr.seek(SeekFrom::Start(pos)).ok();
851        }
852        let mut buf = vec![0u8; length];
853        fr.read(&mut buf).map(|n| {
854            buf.truncate(n);
855            buf
856        })
857    });
858    match n {
859        Some(Ok(data)) => Ok(write_into_buffer(&buffer, offset, &data)),
860        Some(Err(e)) => Err(err_str("read", "", &e)),
861        None => Err("Error: EBADF: bad file descriptor, read".to_string()),
862    }
863}
864
865fn write_impl(args: &[Value]) -> Result<usize, String> {
866    let fd = arg_num(args, 0) as i32;
867    let src = args.get(1).cloned().unwrap_or(Value::Undef);
868    // The positional form differs for a byte VIEW and a string, so the two are
869    // distinguished by what the argument IS, not by whether it is a Buffer.
870    let view = super::buffer::view_bytes(&src);
871    // Buffer form: write(fd, buffer, offset, length, position)
872    // String form: write(fd, string, position, encoding)
873    let (data, position) = if let Some(all) = view {
874        let offset = num_or(args, 2, 0.0) as usize;
875        let length = num_or(args, 3, (all.len().saturating_sub(offset)) as f64) as usize;
876        let end = (offset + length).min(all.len());
877        (
878            all[offset.min(all.len())..end].to_vec(),
879            position_arg(args, 4),
880        )
881    } else {
882        (
883            with_host(|h| h.str_of(&src)).into_bytes(),
884            position_arg(args, 2),
885        )
886    };
887    let r = with_file(fd, |file| {
888        let mut fr: &File = file;
889        if let Some(pos) = position {
890            fr.seek(SeekFrom::Start(pos)).ok();
891        }
892        fr.write(&data)
893    });
894    match r {
895        Some(Ok(n)) => Ok(n),
896        Some(Err(e)) => Err(err_str("write", "", &e)),
897        None => Err("Error: EBADF: bad file descriptor, write".to_string()),
898    }
899}
900
901fn readv_impl(args: &[Value]) -> Result<usize, String> {
902    let fd = arg_num(args, 0) as i32;
903    let buffers = array_items(args.get(1));
904    let position = position_arg(args, 2);
905    let total = with_file(fd, |file| {
906        let mut fr: &File = file;
907        if let Some(pos) = position {
908            fr.seek(SeekFrom::Start(pos)).ok();
909        }
910        let mut chunks: Vec<(Value, Vec<u8>)> = Vec::new();
911        for b in &buffers {
912            let cap = buf_len(b);
913            let mut buf = vec![0u8; cap];
914            match fr.read(&mut buf) {
915                Ok(0) => break,
916                Ok(n) => {
917                    buf.truncate(n);
918                    chunks.push((b.clone(), buf));
919                }
920                Err(e) => return Err(e),
921            }
922        }
923        Ok(chunks)
924    });
925    match total {
926        Some(Ok(chunks)) => Ok(chunks.iter().map(|(b, d)| write_into_buffer(b, 0, d)).sum()),
927        Some(Err(e)) => Err(err_str("readv", "", &e)),
928        None => Err("Error: EBADF: bad file descriptor, readv".to_string()),
929    }
930}
931
932fn writev_impl(args: &[Value]) -> Result<usize, String> {
933    let fd = arg_num(args, 0) as i32;
934    let buffers = array_items(args.get(1));
935    let position = position_arg(args, 2);
936    let mut data = Vec::new();
937    for b in &buffers {
938        data.extend(buf_bytes(b));
939    }
940    let r = with_file(fd, |file| {
941        let mut fr: &File = file;
942        if let Some(pos) = position {
943            fr.seek(SeekFrom::Start(pos)).ok();
944        }
945        fr.write(&data)
946    });
947    match r {
948        Some(Ok(n)) => Ok(n),
949        Some(Err(e)) => Err(err_str("writev", "", &e)),
950        None => Err("Error: EBADF: bad file descriptor, writev".to_string()),
951    }
952}
953
954fn fstat_impl(args: &[Value]) -> Result<Value, String> {
955    let fd = arg_num(args, 0) as i32;
956    let md = with_file(fd, |file| file.metadata());
957    match md {
958        Some(Ok(md)) => Ok(with_host(|h| build_stats(h, &md))),
959        Some(Err(e)) => Err(err_str("fstat", "", &e)),
960        None => Err("Error: EBADF: bad file descriptor, fstat".to_string()),
961    }
962}
963
964fn fchmod_impl(args: &[Value]) -> Result<Value, String> {
965    let fd = arg_num(args, 0) as i32;
966    let mode = arg_num(args, 1) as libc::mode_t;
967    let rc = with_file(fd, |file| unsafe { libc::fchmod(file.as_raw_fd(), mode) });
968    fd_result(rc, "fchmod")
969}
970
971fn fchown_impl(args: &[Value]) -> Result<Value, String> {
972    let fd = arg_num(args, 0) as i32;
973    let uid = arg_num(args, 1) as libc::uid_t;
974    let gid = arg_num(args, 2) as libc::gid_t;
975    let rc = with_file(fd, |file| unsafe {
976        libc::fchown(file.as_raw_fd(), uid, gid)
977    });
978    fd_result(rc, "fchown")
979}
980
981fn futimes_impl(args: &[Value]) -> Result<Value, String> {
982    let fd = arg_num(args, 0) as i32;
983    let times = [
984        to_timeval(time_secs(args, 1)),
985        to_timeval(time_secs(args, 2)),
986    ];
987    let rc = with_file(fd, |file| unsafe {
988        libc::futimes(file.as_raw_fd(), times.as_ptr())
989    });
990    fd_result(rc, "futimes")
991}
992
993fn ftruncate_impl(args: &[Value]) -> Result<Value, String> {
994    let fd = arg_num(args, 0) as i32;
995    let len = arg_num(args, 1);
996    let len = if len.is_nan() { 0 } else { len as u64 };
997    match with_file(fd, |file| file.set_len(len)) {
998        Some(Ok(_)) => Ok(Value::Undef),
999        Some(Err(e)) => Err(err_str("ftruncate", "", &e)),
1000        None => Err("Error: EBADF: bad file descriptor, ftruncate".to_string()),
1001    }
1002}
1003
1004fn fsync_impl(args: &[Value], data_only: bool) -> Result<Value, String> {
1005    let fd = arg_num(args, 0) as i32;
1006    let op = if data_only { "fdatasync" } else { "fsync" };
1007    let r = with_file(fd, |file| {
1008        if data_only {
1009            file.sync_data()
1010        } else {
1011            file.sync_all()
1012        }
1013    });
1014    match r {
1015        Some(Ok(_)) => Ok(Value::Undef),
1016        Some(Err(e)) => Err(err_str(op, "", &e)),
1017        None => Err(format!("Error: EBADF: bad file descriptor, {op}")),
1018    }
1019}
1020
1021/// Translate the return of a `with_file` libc call: `None` = bad fd, `Some(0)` =
1022/// success, `Some(_)` = the C failure whose reason is in `errno`.
1023fn fd_result(rc: Option<libc::c_int>, op: &str) -> Result<Value, String> {
1024    match rc {
1025        Some(0) => Ok(Value::Undef),
1026        Some(_) => Err(err_str(op, "", &std::io::Error::last_os_error())),
1027        None => Err(format!("Error: EBADF: bad file descriptor, {op}")),
1028    }
1029}
1030
1031// ── stat / statfs ────────────────────────────────────────────────────────────
1032
1033fn stat_impl(op: &str, args: &[Value], follow: bool) -> Result<Value, String> {
1034    let path = arg_str(args, 0);
1035    let md = if follow {
1036        std::fs::metadata(&path)
1037    } else {
1038        std::fs::symlink_metadata(&path)
1039    };
1040    match md {
1041        Ok(md) => Ok(with_host(|h| build_stats(h, &md))),
1042        Err(e) => Err(err_str(op, &path, &e)),
1043    }
1044}
1045
1046fn statfs_impl(args: &[Value]) -> Result<Value, String> {
1047    let path = arg_str(args, 0);
1048    let c = cpath(&path, "statfs")?;
1049    let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
1050    if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
1051        return Err(err_str("statfs", &path, &std::io::Error::last_os_error()));
1052    }
1053    Ok(with_host(|h| {
1054        let mut m = IndexMap::new();
1055        m.insert("type".into(), Value::Float(st.f_fsid as f64));
1056        m.insert("bsize".into(), Value::Float(st.f_bsize as f64));
1057        m.insert("blocks".into(), Value::Float(st.f_blocks as f64));
1058        m.insert("bfree".into(), Value::Float(st.f_bfree as f64));
1059        m.insert("bavail".into(), Value::Float(st.f_bavail as f64));
1060        m.insert("files".into(), Value::Float(st.f_files as f64));
1061        m.insert("ffree".into(), Value::Float(st.f_ffree as f64));
1062        h.new_object(m)
1063    }))
1064}
1065
1066/// Build a full `fs.Stats` object from `Metadata` (unix fields via `MetadataExt`).
1067fn build_stats(h: &mut crate::host::JsHost, md: &std::fs::Metadata) -> Value {
1068    let ns = |s: i64, n: i64| s as f64 * 1000.0 + n as f64 / 1_000_000.0;
1069    let atime_ms = ns(md.atime(), md.atime_nsec());
1070    let mtime_ms = ns(md.mtime(), md.mtime_nsec());
1071    let ctime_ms = ns(md.ctime(), md.ctime_nsec());
1072    let birth_ms = md
1073        .created()
1074        .ok()
1075        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
1076        .map(|d| d.as_secs_f64() * 1000.0)
1077        .unwrap_or(mtime_ms);
1078    let ft = md.file_type();
1079    let date = |h: &mut crate::host::JsHost, ms: f64| {
1080        let mut d = IndexMap::new();
1081        d.insert("@@native".into(), h.new_str("Date"));
1082        d.insert("@@ms".into(), Value::Float(ms));
1083        h.new_object(d)
1084    };
1085    let mut m = IndexMap::new();
1086    m.insert("@@native".into(), h.new_str("Stats"));
1087    m.insert("@@isFile".into(), Value::Bool(md.is_file()));
1088    m.insert("@@isDir".into(), Value::Bool(md.is_dir()));
1089    m.insert("@@isSymlink".into(), Value::Bool(ft.is_symlink()));
1090    m.insert("dev".into(), Value::Float(md.dev() as f64));
1091    m.insert("mode".into(), Value::Float(md.mode() as f64));
1092    m.insert("nlink".into(), Value::Float(md.nlink() as f64));
1093    m.insert("uid".into(), Value::Float(md.uid() as f64));
1094    m.insert("gid".into(), Value::Float(md.gid() as f64));
1095    m.insert("rdev".into(), Value::Float(md.rdev() as f64));
1096    m.insert("blksize".into(), Value::Float(md.blksize() as f64));
1097    m.insert("ino".into(), Value::Float(md.ino() as f64));
1098    m.insert("size".into(), Value::Float(md.len() as f64));
1099    m.insert("blocks".into(), Value::Float(md.blocks() as f64));
1100    m.insert("atimeMs".into(), Value::Float(atime_ms));
1101    m.insert("mtimeMs".into(), Value::Float(mtime_ms));
1102    m.insert("ctimeMs".into(), Value::Float(ctime_ms));
1103    m.insert("birthtimeMs".into(), Value::Float(birth_ms));
1104    let atime = date(h, atime_ms);
1105    m.insert("atime".into(), atime);
1106    let mtime = date(h, mtime_ms);
1107    m.insert("mtime".into(), mtime);
1108    let ctime = date(h, ctime_ms);
1109    m.insert("ctime".into(), ctime);
1110    let birthtime = date(h, birth_ms);
1111    m.insert("birthtime".into(), birthtime);
1112    let obj = h.new_object(m);
1113    // The four Date fields are ACCESSORS on `Stats.prototype` in node, so they
1114    // are not own enumerable keys: `Object.keys(statSync(f))` is the fourteen
1115    // numeric fields and nothing else. They are stored here rather than
1116    // computed, so the equivalent is to hide them — a `JSON.stringify(stat)`
1117    // or an `{...stat}` carried four extra entries node does not.
1118    for k in ["atime", "mtime", "ctime", "birthtime"] {
1119        h.hide_prop(&obj, k);
1120    }
1121    obj
1122}
1123
1124// ── watchFile / unwatchFile ──────────────────────────────────────────────────
1125
1126fn watch_file(args: &[Value]) -> Result<Value, String> {
1127    let path = arg_str(args, 0);
1128    let Some(listener) = args.last().cloned().filter(is_fn) else {
1129        return Ok(Value::Undef);
1130    };
1131    let interval = interval_opt(args).unwrap_or(5007.0).max(1.0) as u64;
1132    let abs = std::fs::canonicalize(&path)
1133        .map(|p| p.to_string_lossy().into_owned())
1134        .unwrap_or_else(|_| path.clone());
1135
1136    let stop = Arc::new(AtomicBool::new(false));
1137    let id = NEXT_WATCH_ID.with(|n| {
1138        let v = *n.borrow();
1139        *n.borrow_mut() = v + 1;
1140        v
1141    });
1142    WATCHERS.with(|w| {
1143        w.borrow_mut().push(WatchEntry {
1144            id,
1145            path: abs.clone(),
1146            listener: listener.clone(),
1147            stop: stop.clone(),
1148        });
1149    });
1150    with_host(|h| h.incr_handle());
1151
1152    let tx = with_host(|h| h.io_sender());
1153    let poll_stop = stop.clone();
1154    let poll_listener = listener;
1155    std::thread::spawn(move || {
1156        let mut prev = stat_parts(&abs);
1157        loop {
1158            if poll_stop.load(Ordering::Acquire) {
1159                break;
1160            }
1161            std::thread::sleep(Duration::from_millis(interval));
1162            if poll_stop.load(Ordering::Acquire) {
1163                break;
1164            }
1165            let curr = stat_parts(&abs);
1166            if curr != prev {
1167                let l = poll_listener.clone();
1168                let (p0, p1, p2) = prev;
1169                let (c0, c1, c2) = curr;
1170                let _ = tx.send(Box::new(move || {
1171                    let cur = with_host(|h| stats_from_parts(h, c0, c1, c2));
1172                    let old = with_host(|h| stats_from_parts(h, p0, p1, p2));
1173                    if let Err(e) = invoke(&l, vec![cur, old], None) {
1174                        eprintln!("{e}");
1175                    }
1176                    Ok(())
1177                }));
1178                prev = curr;
1179            }
1180        }
1181    });
1182    Ok(Value::Undef)
1183}
1184
1185fn unwatch_file(args: &[Value]) -> Result<Value, String> {
1186    let path = arg_str(args, 0);
1187    let abs = std::fs::canonicalize(&path)
1188        .map(|p| p.to_string_lossy().into_owned())
1189        .unwrap_or_else(|_| path.clone());
1190    let listener = args.get(1).cloned().filter(is_fn);
1191    let removed = WATCHERS.with(|w| {
1192        let mut w = w.borrow_mut();
1193        let mut count = 0;
1194        w.retain(|e| {
1195            let matches =
1196                e.path == abs && listener.as_ref().map(|l| *l == e.listener).unwrap_or(true);
1197            if matches {
1198                e.stop.store(true, Ordering::Release);
1199                count += 1;
1200            }
1201            !matches
1202        });
1203        count
1204    });
1205    for _ in 0..removed {
1206        with_host(|h| h.decr_handle());
1207    }
1208    // Wake the event loop so it can re-evaluate the handle count.
1209    if removed > 0 {
1210        let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1211    }
1212    Ok(Value::Undef)
1213}
1214
1215/// A file's watch-relevant state: `(exists, mtime_ms, size)`.
1216fn stat_parts(path: &str) -> (bool, i64, u64) {
1217    match std::fs::metadata(path) {
1218        Ok(md) => (
1219            true,
1220            md.mtime() * 1000 + md.mtime_nsec() / 1_000_000,
1221            md.len(),
1222        ),
1223        Err(_) => (false, 0, 0),
1224    }
1225}
1226
1227fn stats_from_parts(h: &mut crate::host::JsHost, exists: bool, mtime_ms: i64, size: u64) -> Value {
1228    let ms = mtime_ms as f64;
1229    let date = |h: &mut crate::host::JsHost, ms: f64| {
1230        let mut d = IndexMap::new();
1231        d.insert("@@native".into(), h.new_str("Date"));
1232        d.insert("@@ms".into(), Value::Float(ms));
1233        h.new_object(d)
1234    };
1235    let mut m = IndexMap::new();
1236    m.insert("@@native".into(), h.new_str("Stats"));
1237    m.insert("@@isFile".into(), Value::Bool(exists));
1238    m.insert("@@isDir".into(), Value::Bool(false));
1239    m.insert("@@isSymlink".into(), Value::Bool(false));
1240    m.insert(
1241        "size".into(),
1242        Value::Float(if exists { size as f64 } else { 0.0 }),
1243    );
1244    m.insert("atimeMs".into(), Value::Float(ms));
1245    m.insert("mtimeMs".into(), Value::Float(ms));
1246    m.insert("ctimeMs".into(), Value::Float(ms));
1247    m.insert("birthtimeMs".into(), Value::Float(ms));
1248    let mt = date(h, ms);
1249    m.insert("mtime".into(), mt);
1250    let at = date(h, ms);
1251    m.insert("atime".into(), at);
1252    h.new_object(m)
1253}
1254
1255// ── read / write streams ─────────────────────────────────────────────────────
1256
1257fn create_read_stream(args: &[Value]) -> Result<Value, String> {
1258    let path = arg_str(args, 0);
1259    let enc = encoding_arg(args, 1);
1260    let stream = with_host(|h| {
1261        let mut extra = IndexMap::new();
1262        extra.insert("path".into(), h.new_str(path.clone()));
1263        if let Some(e) = &enc {
1264            extra.insert("@@encoding".into(), h.new_str(e.clone()));
1265        }
1266        extra
1267    });
1268    let stream = super::net::new_emitter_object("FSReadStream", stream);
1269    with_host(|h| h.incr_handle());
1270    let recv = stream.clone();
1271    let p = path;
1272    with_host(|h| {
1273        h.queue_micro_native(Box::new(move || {
1274            read_stream_pump(&recv, &p);
1275            Ok(())
1276        }))
1277    });
1278    Ok(stream)
1279}
1280
1281/// Read the whole file and drive the stream's events on the microtask tick after
1282/// creation (so synchronously-attached `on('data')`/`on('end')` listeners fire).
1283fn read_stream_pump(recv: &Value, path: &str) {
1284    with_host(|h| h.decr_handle());
1285    let bytes = match std::fs::read(path) {
1286        Ok(b) => b,
1287        Err(e) => {
1288            let ev = with_host(|h| crate::builtins::synth_error(h, &err_str("open", path, &e)));
1289            let _ = super::events::instance_call(
1290                recv,
1291                "emit",
1292                vec![with_host(|h| h.new_str("error")), ev],
1293            );
1294            return;
1295        }
1296    };
1297    let enc = get_prop(recv, "@@encoding").map(|v| with_host(|h| h.str_of(&v)));
1298    let chunk = match enc.as_deref() {
1299        Some(e) if e != "buffer" => {
1300            with_host(|h| h.new_str(String::from_utf8_lossy(&bytes).into_owned()))
1301        }
1302        _ => super::buffer::from_bytes(&bytes),
1303    };
1304    if let Some(dest) = get_prop(recv, "@@pipeDest") {
1305        let _ = crate::host::call_method(&dest, "write", vec![chunk]);
1306        let _ = crate::host::call_method(&dest, "end", vec![]);
1307    } else {
1308        let name = with_host(|h| h.new_str("data"));
1309        let _ = super::events::instance_call(recv, "emit", vec![name, chunk]);
1310    }
1311    let _ = super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("end"))]);
1312    let _ = super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1313}
1314
1315pub const READ_STREAM_METHODS: &[&str] = &[
1316    "pipe",
1317    "pause",
1318    "resume",
1319    "setEncoding",
1320    "destroy",
1321    "close",
1322    "read",
1323];
1324
1325/// `fs.ReadStream` instance dispatch (tag `FSReadStream`). Emitter methods are
1326/// handled by the parent's shared emitter routing; this covers the stream-only
1327/// surface.
1328pub fn read_stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1329    match method {
1330        "pipe" => {
1331            if let Some(dest) = args.first().cloned() {
1332                set_prop(recv, "@@pipeDest", dest.clone());
1333                Ok(dest)
1334            } else {
1335                Ok(recv.clone())
1336            }
1337        }
1338        "setEncoding" => {
1339            set_prop(
1340                recv,
1341                "@@encoding",
1342                with_host(|h| h.new_str(super::arg_str(&args, 0))),
1343            );
1344            Ok(recv.clone())
1345        }
1346        "pause" | "resume" | "read" => Ok(recv.clone()),
1347        "destroy" | "close" => {
1348            let _ =
1349                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1350            Ok(recv.clone())
1351        }
1352        _ => Err(crate::host::type_error(&format!(
1353            "stream.{method} is not a function"
1354        ))),
1355    }
1356}
1357
1358fn create_write_stream(args: &[Value]) -> Result<Value, String> {
1359    let path = arg_str(args, 0);
1360    let flags = match encoding_flag(args, "flags") {
1361        Some(f) => f,
1362        None => "w".to_string(),
1363    };
1364    let file = open_options(&flags)
1365        .open(&path)
1366        .map_err(|e| err_str("open", &path, &e))?;
1367    let fd = register_fd(file);
1368    let stream = with_host(|h| {
1369        let mut extra = IndexMap::new();
1370        extra.insert("path".into(), h.new_str(path));
1371        extra.insert("@@wfd".into(), Value::Float(fd as f64));
1372        extra.insert("bytesWritten".into(), Value::Float(0.0));
1373        extra
1374    });
1375    let stream = super::net::new_emitter_object("FSWriteStream", stream);
1376    with_host(|h| h.incr_handle());
1377    let recv = stream.clone();
1378    with_host(|h| {
1379        h.queue_micro_native(Box::new(move || {
1380            let _ =
1381                super::events::instance_call(&recv, "emit", vec![with_host(|h| h.new_str("open"))]);
1382            let _ = super::events::instance_call(
1383                &recv,
1384                "emit",
1385                vec![with_host(|h| h.new_str("ready"))],
1386            );
1387            Ok(())
1388        }))
1389    });
1390    Ok(stream)
1391}
1392
1393pub const WRITE_STREAM_METHODS: &[&str] = &[
1394    "write",
1395    "end",
1396    "destroy",
1397    "close",
1398    "cork",
1399    "uncork",
1400    "setDefaultEncoding",
1401];
1402
1403/// `fs.WriteStream` instance dispatch (tag `FSWriteStream`).
1404pub fn write_stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1405    match method {
1406        "write" => {
1407            write_stream_bytes(recv, args.first());
1408            if let Some(cb) = args.iter().find(|v| is_fn(v)).cloned() {
1409                let _ = invoke(&cb, vec![], None);
1410            }
1411            Ok(Value::Bool(true))
1412        }
1413        "end" => {
1414            if let Some(chunk) = args
1415                .first()
1416                .filter(|v| !matches!(v, Value::Undef) && !is_fn(v))
1417            {
1418                write_stream_bytes(recv, Some(chunk));
1419            }
1420            if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32)
1421            {
1422                close_fd(fd);
1423            }
1424            let _ = super::events::instance_call(
1425                recv,
1426                "emit",
1427                vec![with_host(|h| h.new_str("finish"))],
1428            );
1429            let _ =
1430                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1431            with_host(|h| h.decr_handle());
1432            if let Some(cb) = args.iter().find(|v| is_fn(v)).cloned() {
1433                let _ = invoke(&cb, vec![], None);
1434            }
1435            Ok(recv.clone())
1436        }
1437        "destroy" | "close" => {
1438            if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32)
1439            {
1440                close_fd(fd);
1441            }
1442            let _ =
1443                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1444            with_host(|h| h.decr_handle());
1445            Ok(recv.clone())
1446        }
1447        "cork" | "uncork" | "setDefaultEncoding" => Ok(recv.clone()),
1448        _ => Err(crate::host::type_error(&format!(
1449            "stream.{method} is not a function"
1450        ))),
1451    }
1452}
1453
1454fn write_stream_bytes(recv: &Value, chunk: Option<&Value>) {
1455    let Some(chunk) = chunk else { return };
1456    let data = value_bytes(chunk);
1457    if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32) {
1458        let written = with_file(fd, |file| {
1459            let mut fr: &File = file;
1460            fr.write(&data).unwrap_or(0)
1461        })
1462        .unwrap_or(0);
1463        let prev = get_prop(recv, "bytesWritten")
1464            .map(|v| with_host(|h| h.to_number(&v)))
1465            .unwrap_or(0.0);
1466        set_prop(recv, "bytesWritten", Value::Float(prev + written as f64));
1467    }
1468}
1469
1470// ── Stats / Dirent / Dir instance surfaces ───────────────────────────────────
1471
1472/// `fs.Stats` method dispatch (`isFile`/`isDirectory`/…).
1473pub fn stats_call(recv: &Value, method: &str) -> Result<Value, String> {
1474    type_test(recv, method, "stats")
1475}
1476
1477pub const DIRENT_METHODS: &[&str] = &[
1478    "isFile",
1479    "isDirectory",
1480    "isSymbolicLink",
1481    "isBlockDevice",
1482    "isCharacterDevice",
1483    "isFIFO",
1484    "isSocket",
1485];
1486
1487/// `fs.Dirent` method dispatch (tag `Dirent`); `name`/`parentPath`/`path` are
1488/// plain data properties resolved by the generic object path.
1489pub fn dirent_call(recv: &Value, method: &str) -> Result<Value, String> {
1490    type_test(recv, method, "dirent")
1491}
1492
1493/// Shared `is*` predicates for `Stats` and `Dirent` (both carry `@@isFile`/
1494/// `@@isDir`/`@@isSymlink`; the device/FIFO/socket predicates are always false —
1495/// node-js does not model those file types).
1496fn type_test(recv: &Value, method: &str, what: &str) -> Result<Value, String> {
1497    let read = |key: &str| {
1498        with_host(|h| match h.get(recv) {
1499            Some(JsObj::Object(p)) => matches!(p.get(key), Some(Value::Bool(true))),
1500            _ => false,
1501        })
1502    };
1503    match method {
1504        "isFile" => Ok(Value::Bool(read("@@isFile"))),
1505        "isDirectory" => Ok(Value::Bool(read("@@isDir"))),
1506        "isSymbolicLink" => Ok(Value::Bool(read("@@isSymlink"))),
1507        "isBlockDevice" | "isCharacterDevice" | "isFIFO" | "isSocket" => Ok(Value::Bool(false)),
1508        _ => Err(crate::host::type_error(&format!(
1509            "{what}.{method} is not a function"
1510        ))),
1511    }
1512}
1513
1514pub const DIR_METHODS: &[&str] = &["read", "readSync", "close", "closeSync"];
1515
1516/// `fs.Dir` method dispatch (tag `Dir`); `path` is a plain data property.
1517pub fn dir_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1518    match method {
1519        "readSync" => Ok(dir_next(recv)),
1520        "read" => {
1521            let v = dir_next(recv);
1522            if let Some(cb) = args.first().filter(|c| is_fn(c)).cloned() {
1523                with_host(|h| {
1524                    let n = h.null();
1525                    h.queue_micro(cb, vec![n, v]);
1526                });
1527                Ok(Value::Undef)
1528            } else {
1529                Ok(settled_ok(v))
1530            }
1531        }
1532        "closeSync" => Ok(Value::Undef),
1533        "close" => {
1534            if let Some(cb) = args.first().filter(|c| is_fn(c)).cloned() {
1535                with_host(|h| {
1536                    let n = h.null();
1537                    h.queue_micro(cb, vec![n]);
1538                });
1539                Ok(Value::Undef)
1540            } else {
1541                Ok(settled_ok(Value::Undef))
1542            }
1543        }
1544        _ => Err(crate::host::type_error(&format!(
1545            "dir.{method} is not a function"
1546        ))),
1547    }
1548}
1549
1550/// Advance the `Dir` cursor and return the next `Dirent`, or `null` when drained.
1551fn dir_next(recv: &Value) -> Value {
1552    with_host(|h| {
1553        let (entries, pos) = match h.get(recv) {
1554            Some(JsObj::Object(p)) => (
1555                p.get("@@entries").cloned(),
1556                p.get("@@pos").map(|v| h.to_number(v) as usize).unwrap_or(0),
1557            ),
1558            _ => (None, 0),
1559        };
1560        let item = match entries.as_ref().and_then(|e| h.get(e)) {
1561            Some(JsObj::Array(items)) => items.get(pos).cloned(),
1562            _ => None,
1563        };
1564        if item.is_some() {
1565            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1566                p.insert("@@pos".into(), Value::Float((pos + 1) as f64));
1567            }
1568        }
1569        item.unwrap_or_else(|| h.null())
1570    })
1571}
1572
1573fn build_dirent(
1574    h: &mut crate::host::JsHost,
1575    name: String,
1576    parent: &str,
1577    ft: std::fs::FileType,
1578) -> Value {
1579    let mut m = IndexMap::new();
1580    m.insert("@@native".into(), h.new_str("Dirent"));
1581    m.insert("name".into(), h.new_str(name));
1582    let pp = h.new_str(parent.to_string());
1583    m.insert("parentPath".into(), pp.clone());
1584    m.insert("path".into(), pp);
1585    m.insert("@@isFile".into(), Value::Bool(ft.is_file()));
1586    m.insert("@@isDir".into(), Value::Bool(ft.is_dir()));
1587    m.insert("@@isSymlink".into(), Value::Bool(ft.is_symlink()));
1588    h.new_object(m)
1589}
1590
1591// ── glob ─────────────────────────────────────────────────────────────────────
1592
1593fn glob_impl(args: &[Value]) -> Result<Value, String> {
1594    let pattern = arg_str(args, 0);
1595    let absolute = pattern.starts_with('/');
1596    let base = if absolute {
1597        PathBuf::from("/")
1598    } else {
1599        std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1600    };
1601    let segs: Vec<String> = pattern
1602        .split('/')
1603        .filter(|s| !s.is_empty())
1604        .map(String::from)
1605        .collect();
1606    let mut out: Vec<String> = Vec::new();
1607    let prefix = if absolute {
1608        "/".to_string()
1609    } else {
1610        String::new()
1611    };
1612    glob_walk(&base, &segs, 0, &prefix, &mut out);
1613    out.sort();
1614    out.dedup();
1615    Ok(with_host(|h| {
1616        let items: Vec<Value> = out.into_iter().map(|s| h.new_str(s)).collect();
1617        h.new_array(items)
1618    }))
1619}
1620
1621fn glob_walk(dir: &Path, segs: &[String], idx: usize, prefix: &str, out: &mut Vec<String>) {
1622    if idx >= segs.len() {
1623        if !prefix.is_empty() && prefix != "/" {
1624            out.push(prefix.trim_end_matches('/').to_string());
1625        }
1626        return;
1627    }
1628    let seg = &segs[idx];
1629    if seg == "**" {
1630        glob_walk(dir, segs, idx + 1, prefix, out);
1631        if let Ok(rd) = std::fs::read_dir(dir) {
1632            for e in rd.flatten() {
1633                if e.path().is_dir() {
1634                    let name = e.file_name().to_string_lossy().into_owned();
1635                    let np = join_glob(prefix, &name);
1636                    glob_walk(&e.path(), segs, idx, &np, out);
1637                }
1638            }
1639        }
1640        return;
1641    }
1642    let last = idx + 1 == segs.len();
1643    if let Ok(rd) = std::fs::read_dir(dir) {
1644        for e in rd.flatten() {
1645            let name = e.file_name().to_string_lossy().into_owned();
1646            if name.starts_with('.') && !seg.starts_with('.') {
1647                continue;
1648            }
1649            if wildcard_match(seg, &name) {
1650                let np = join_glob(prefix, &name);
1651                if last {
1652                    out.push(np);
1653                } else if e.path().is_dir() {
1654                    glob_walk(&e.path(), segs, idx + 1, &np, out);
1655                }
1656            }
1657        }
1658    }
1659}
1660
1661fn join_glob(prefix: &str, name: &str) -> String {
1662    if prefix.is_empty() {
1663        name.to_string()
1664    } else if prefix.ends_with('/') {
1665        format!("{prefix}{name}")
1666    } else {
1667        format!("{prefix}/{name}")
1668    }
1669}
1670
1671/// Match a single path segment against a `*`/`?` glob (no character classes).
1672fn wildcard_match(pat: &str, name: &str) -> bool {
1673    let p: Vec<char> = pat.chars().collect();
1674    let n: Vec<char> = name.chars().collect();
1675    let (mut pi, mut ni) = (0usize, 0usize);
1676    let (mut star, mut mark) = (None, 0usize);
1677    while ni < n.len() {
1678        if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
1679            pi += 1;
1680            ni += 1;
1681        } else if pi < p.len() && p[pi] == '*' {
1682            star = Some(pi);
1683            mark = ni;
1684            pi += 1;
1685        } else if let Some(s) = star {
1686            pi = s + 1;
1687            mark += 1;
1688            ni = mark;
1689        } else {
1690            return false;
1691        }
1692    }
1693    while pi < p.len() && p[pi] == '*' {
1694        pi += 1;
1695    }
1696    pi == p.len()
1697}
1698
1699// ── small helpers ────────────────────────────────────────────────────────────
1700
1701fn settled_ok(v: Value) -> Value {
1702    let p = with_host(|h| h.new_promise());
1703    let id = with_host(|h| h.promise_id(&p).unwrap_or(0));
1704    crate::host::resolve_promise_val(id, v);
1705    p
1706}
1707
1708fn get_prop(recv: &Value, key: &str) -> Option<Value> {
1709    with_host(|h| match h.get(recv) {
1710        Some(JsObj::Object(p)) => p.get(key).cloned(),
1711        _ => None,
1712    })
1713}
1714
1715fn set_prop(recv: &Value, key: &str, val: Value) {
1716    with_host(|h| {
1717        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1718            p.insert(key.to_string(), val);
1719        }
1720    });
1721}
1722
1723/// Raw bytes of a byte-view arg, or the utf-8 bytes of anything else.
1724fn value_bytes(v: &Value) -> Vec<u8> {
1725    match super::buffer::view_bytes(v) {
1726        Some(b) => b,
1727        None => with_host(|h| h.str_of(v)).into_bytes(),
1728    }
1729}
1730
1731fn buf_bytes(v: &Value) -> Vec<u8> {
1732    with_host(|h| match h.get(v) {
1733        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
1734            Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
1735            _ => Vec::new(),
1736        },
1737        _ => Vec::new(),
1738    })
1739}
1740
1741fn buf_len(v: &Value) -> usize {
1742    with_host(|h| match h.get(v) {
1743        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
1744            Some(JsObj::Array(items)) => items.len(),
1745            _ => 0,
1746        },
1747        _ => 0,
1748    })
1749}
1750
1751/// Write `data` into a Buffer's backing byte array at `offset`; returns the count
1752/// actually written (bounded by the buffer capacity).
1753fn write_into_buffer(buf: &Value, offset: usize, data: &[u8]) -> usize {
1754    let Some(arr) = get_prop(buf, "@@bytes") else {
1755        return 0;
1756    };
1757    with_host(|h| {
1758        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
1759            let mut n = 0;
1760            for (i, b) in data.iter().enumerate() {
1761                let idx = offset + i;
1762                if idx >= items.len() {
1763                    break;
1764                }
1765                items[idx] = Value::Float(*b as f64);
1766                n += 1;
1767            }
1768            n
1769        } else {
1770            0
1771        }
1772    })
1773}
1774
1775fn array_items(v: Option<&Value>) -> Vec<Value> {
1776    match v {
1777        Some(v) => with_host(|h| match h.get(v) {
1778            Some(JsObj::Array(items)) => items.clone(),
1779            _ => Vec::new(),
1780        }),
1781        None => Vec::new(),
1782    }
1783}
1784
1785fn opt_flag(args: &[Value], key: &str) -> bool {
1786    with_host(|h| {
1787        args.iter().any(|v| {
1788            matches!(h.get(v), Some(JsObj::Object(p)) if matches!(p.get(key), Some(Value::Bool(true))))
1789        })
1790    })
1791}
1792
1793fn encoding_flag(args: &[Value], key: &str) -> Option<String> {
1794    with_host(|h| {
1795        for v in args {
1796            if let Some(JsObj::Object(p)) = h.get(v) {
1797                if let Some(val) = p.get(key) {
1798                    return Some(h.str_of(val));
1799                }
1800            }
1801        }
1802        None
1803    })
1804}
1805
1806fn interval_opt(args: &[Value]) -> Option<f64> {
1807    with_host(|h| {
1808        for v in args {
1809            if let Some(JsObj::Object(p)) = h.get(v) {
1810                if let Some(val) = p.get("interval") {
1811                    return Some(h.to_number(val));
1812                }
1813            }
1814        }
1815        None
1816    })
1817}
1818
1819fn num_or(args: &[Value], i: usize, default: f64) -> f64 {
1820    match args.get(i) {
1821        Some(v) if !matches!(v, Value::Undef) => {
1822            let n = with_host(|h| h.to_number(v));
1823            if n.is_nan() {
1824                default
1825            } else {
1826                n
1827            }
1828        }
1829        _ => default,
1830    }
1831}
1832
1833/// A byte `position` argument: `null`/`undefined` means "current file offset".
1834fn position_arg(args: &[Value], i: usize) -> Option<u64> {
1835    match args.get(i) {
1836        Some(Value::Undef) | None => None,
1837        Some(v) if with_host(|h| h.is_null(v)) => None,
1838        Some(v) => {
1839            let n = with_host(|h| h.to_number(v));
1840            if n.is_nan() || n < 0.0 {
1841                None
1842            } else {
1843                Some(n as u64)
1844            }
1845        }
1846    }
1847}
1848
1849/// A timestamp argument in **seconds**. A `Date` argument stores milliseconds, so
1850/// divide; a bare number is already seconds (Node's `fs.utimes` convention).
1851fn time_secs(args: &[Value], i: usize) -> f64 {
1852    match args.get(i) {
1853        Some(v) if native_tag(v).as_deref() == Some("Date") => arg_num(args, i) / 1000.0,
1854        _ => arg_num(args, i),
1855    }
1856}
1857
1858fn to_timeval(secs: f64) -> libc::timeval {
1859    let s = secs.floor();
1860    let us = ((secs - s) * 1_000_000.0).round();
1861    libc::timeval {
1862        tv_sec: s as libc::time_t,
1863        tv_usec: us as libc::suseconds_t,
1864    }
1865}
1866
1867fn cpath(path: &str, op: &str) -> Result<CString, String> {
1868    CString::new(path).map_err(|_| format!("Error: EINVAL: invalid argument, {op} '{path}'"))
1869}
1870
1871fn ok_or_errno(rc: libc::c_int, op: &str, path: &str) -> Result<Value, String> {
1872    if rc == 0 {
1873        Ok(Value::Undef)
1874    } else {
1875        Err(err_str(op, path, &std::io::Error::last_os_error()))
1876    }
1877}
1878
1879/// The encoding an fs call was given, from either the string form
1880/// (`readFileSync(p, 'hex')`) or the options-object form
1881/// (`readFileSync(p, { encoding: 'hex' })`).
1882///
1883/// The object form used to be discarded — it was detected by its
1884/// `"[object Object]"` stringification and treated as absent — so
1885/// `{ encoding: 'utf8' }` returned a Buffer. `{ encoding: null }` really does
1886/// mean "no encoding", and so returns `None`.
1887fn encoding_arg(args: &[Value], i: usize) -> Option<String> {
1888    match args.get(i) {
1889        Some(Value::Undef) | None => None,
1890        Some(v) => {
1891            if with_host(|h| matches!(h.get(v), Some(JsObj::Object(_)))) {
1892                let e = crate::builtins::get_property(v, "encoding").ok()?;
1893                if matches!(e, Value::Undef) || with_host(|h| h.is_null(&e)) {
1894                    return None;
1895                }
1896                return Some(with_host(|h| h.str_of(&e)));
1897            }
1898            let s = with_host(|h| h.str_of(v));
1899            if s == "undefined" || s == "null" {
1900                None
1901            } else {
1902                Some(s)
1903            }
1904        }
1905    }
1906}
1907
1908/// The failing *syscall* Node names in a filesystem error, for the JS-level
1909/// entry points whose API name differs from it. `fs.readFileSync('/nope')`
1910/// reports `open`, not `readFileSync`, because `open(2)` is what failed —
1911/// and `err.syscall` is load-bearing for packages that branch on it.
1912fn syscall_name(op: &str) -> &str {
1913    match op {
1914        "readFileSync" | "readFile" | "writeFile" | "writeFileSync" | "appendFile"
1915        | "appendFileSync" | "createReadStream" | "createWriteStream" => "open",
1916        "readdir" | "readdirSync" | "opendir" => "scandir",
1917        // The rest name the C call, which for most of these is the JS name
1918        // minus its `Sync` suffix — `fs.statSync('/nope')` reports `stat`. Only
1919        // the ones whose spelling ALSO changes need an entry: libuv writes
1920        // `copyfile` in one word, and `realpath` fails inside the `lstat` it
1921        // walks the path with.
1922        "copyFile" | "copyFileSync" => "copyfile",
1923        "realpath" | "realpathSync" => "lstat",
1924        other => other.strip_suffix("Sync").unwrap_or(other),
1925    }
1926}
1927
1928/// The second path a two-argument call names in its message: node renders
1929/// `rename '/a' -> '/b'`, and a message with only the source reads as though
1930/// the destination were never part of the operation.
1931fn err_str2(op: &str, from: &str, to: &str, e: &std::io::Error) -> String {
1932    format!("{} -> '{to}'", err_str(op, from, e))
1933}
1934
1935/// The `CODE: reason` half of a libuv error message (`ENOENT: no such file or
1936/// directory`), shared by every syscall wrapper that has to render one.
1937///
1938/// Deliberately NOT Rust's `io::Error` `Display`: Rust capitalizes and appends
1939/// its own `(os error 2)`, so `No such file or directory (os error 2)` is a
1940/// string no Node has ever printed. `process.chdir` was assembling its message
1941/// that way.
1942/// The libuv error CODE for an `io::Error` — `ENOENT`, `EACCES` and the rest.
1943/// Node exposes it as `err.code`, and every error it renders leads with it.
1944pub(crate) fn libuv_code(e: &std::io::Error) -> &'static str {
1945    use std::io::ErrorKind::*;
1946    match e.kind() {
1947        NotFound => "ENOENT",
1948        PermissionDenied => "EACCES",
1949        AlreadyExists => "EEXIST",
1950        NotADirectory => "ENOTDIR",
1951        IsADirectory => "EISDIR",
1952        DirectoryNotEmpty => "ENOTEMPTY",
1953        InvalidInput => "EINVAL",
1954        BrokenPipe => "EPIPE",
1955        _ => "EIO",
1956    }
1957}
1958
1959pub(crate) fn libuv_message(e: &std::io::Error) -> String {
1960    let code = libuv_code(e);
1961    format!("{code}: {}", libuv_reason(code))
1962}
1963
1964/// libuv's own lowercase reason string for a code.
1965fn libuv_reason(code: &str) -> &'static str {
1966    match code {
1967        "ENOENT" => "no such file or directory",
1968        "EACCES" => "permission denied",
1969        "EEXIST" => "file already exists",
1970        "ENOTDIR" => "not a directory",
1971        "EISDIR" => "illegal operation on a directory",
1972        "ENOTEMPTY" => "directory not empty",
1973        "EINVAL" => "invalid argument",
1974        "EPIPE" => "broken pipe",
1975        _ => "i/o error",
1976    }
1977}
1978
1979fn err_str(op: &str, path: &str, e: &std::io::Error) -> String {
1980    let code = libuv_code(e);
1981    // libuv's own reason strings, not Rust's `io::Error` `Display` — Rust
1982    // capitalizes ("No such file or directory") where libuv (and therefore
1983    // Node's `err.message`) does not.
1984    let reason = libuv_reason(code);
1985    let op = syscall_name(op);
1986    if path.is_empty() {
1987        format!("Error: {code}: {reason}, {op}")
1988    } else {
1989        format!("Error: {code}: {reason}, {op} '{path}'")
1990    }
1991}