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        Err(e) => with_host(|h| {
273            let ev = h.new_str(e);
274            h.queue_micro(cb, vec![ev]);
275        }),
276    }
277    Ok(Value::Undef)
278}
279
280/// `read`/`write` callbacks carry a third argument: `cb(err, count, buffer)`.
281fn read_write_async(args: &[Value], result: Result<usize, String>) -> Result<Value, String> {
282    let Some(cb) = args.last().cloned().filter(is_fn) else {
283        return Ok(Value::Undef);
284    };
285    let buffer = args.get(1).cloned().unwrap_or(Value::Undef);
286    match result {
287        Ok(n) => with_host(|h| {
288            let nul = h.null();
289            h.queue_micro(cb, vec![nul, Value::Float(n as f64), buffer]);
290        }),
291        Err(e) => with_host(|h| {
292            let ev = h.new_str(e);
293            h.queue_micro(cb, vec![ev]);
294        }),
295    }
296    Ok(Value::Undef)
297}
298
299fn is_fn(v: &Value) -> bool {
300    with_host(|h| crate::host::is_callable(h, v))
301}
302
303// ── read/write/append ────────────────────────────────────────────────────────
304
305fn read_file_sync(args: &[Value]) -> Result<Value, String> {
306    let path = arg_str(args, 0);
307    let enc = encoding_arg(args, 1);
308    match std::fs::read(&path) {
309        Ok(bytes) => Ok(match enc {
310            Some(_) => with_host(|h| h.new_str(String::from_utf8_lossy(&bytes).into_owned())),
311            None => super::buffer::from_bytes(&bytes),
312        }),
313        Err(e) => Err(err_str("readFileSync", &path, &e)),
314    }
315}
316
317fn write_file_impl(args: &[Value]) -> Result<Value, String> {
318    let path = arg_str(args, 0);
319    let data = value_bytes(args.get(1).unwrap_or(&Value::Undef));
320    match std::fs::write(&path, data) {
321        Ok(_) => Ok(Value::Undef),
322        Err(e) => Err(err_str("writeFile", &path, &e)),
323    }
324}
325
326fn append_file_impl(args: &[Value]) -> Result<Value, String> {
327    let path = arg_str(args, 0);
328    let data = value_bytes(args.get(1).unwrap_or(&Value::Undef));
329    let r = std::fs::OpenOptions::new()
330        .create(true)
331        .append(true)
332        .open(&path)
333        .and_then(|mut f| f.write_all(&data));
334    match r {
335        Ok(_) => Ok(Value::Undef),
336        Err(e) => Err(err_str("appendFile", &path, &e)),
337    }
338}
339
340fn read_file_async(args: &[Value]) -> Result<Value, String> {
341    let path = arg_str(args, 0);
342    let Some(cb) = args.last().cloned().filter(is_fn) else {
343        return Ok(Value::Undef);
344    };
345    let enc = if args.len() >= 3 {
346        encoding_arg(args, 1)
347    } else {
348        None
349    };
350    let (err, data) = match std::fs::read(&path) {
351        Ok(bytes) => (
352            with_host(|h| h.null()),
353            match enc {
354                Some(_) => with_host(|h| h.new_str(String::from_utf8_lossy(&bytes).into_owned())),
355                None => super::buffer::from_bytes(&bytes),
356            },
357        ),
358        Err(e) => (
359            with_host(|h| h.new_str(err_str("readFile", &path, &e))),
360            Value::Undef,
361        ),
362    };
363    with_host(|h| h.queue_micro(cb, vec![err, data]));
364    Ok(Value::Undef)
365}
366
367fn exists_async(args: &[Value]) -> Result<Value, String> {
368    let path = arg_str(args, 0);
369    let Some(cb) = args.last().cloned().filter(is_fn) else {
370        return Ok(Value::Undef);
371    };
372    let ex = Path::new(&path).exists();
373    with_host(|h| h.queue_micro(cb, vec![Value::Bool(ex)]));
374    Ok(Value::Undef)
375}
376
377// ── directories ──────────────────────────────────────────────────────────────
378
379fn mkdir_impl(args: &[Value]) -> Result<Value, String> {
380    let path = arg_str(args, 0);
381    let recursive = opt_flag(args, "recursive");
382    let r = if recursive {
383        std::fs::create_dir_all(&path)
384    } else {
385        std::fs::create_dir(&path)
386    };
387    match r {
388        Ok(_) => Ok(Value::Undef),
389        Err(e) => Err(err_str("mkdir", &path, &e)),
390    }
391}
392
393fn rm_impl(op: &str, args: &[Value]) -> Result<Value, String> {
394    let path = arg_str(args, 0);
395    let p = Path::new(&path);
396    let force = opt_flag(args, "force");
397    let r = if p.is_dir() {
398        if opt_flag(args, "recursive") {
399            std::fs::remove_dir_all(p)
400        } else {
401            std::fs::remove_dir(p)
402        }
403    } else {
404        std::fs::remove_file(p)
405    };
406    match r {
407        Ok(_) => Ok(Value::Undef),
408        Err(e) if force && e.kind() == std::io::ErrorKind::NotFound => Ok(Value::Undef),
409        Err(e) => Err(err_str(op, &path, &e)),
410    }
411}
412
413fn readdir_impl(args: &[Value]) -> Result<Value, String> {
414    let path = arg_str(args, 0);
415    let file_types = opt_flag(args, "withFileTypes");
416    let recursive = opt_flag(args, "recursive");
417    let mut names: Vec<(String, String, std::fs::FileType)> = Vec::new();
418    collect_dir(Path::new(&path), &path, "", recursive, &mut names)
419        .map_err(|e| err_str("readdir", &path, &e))?;
420    names.sort_by(|a, b| a.0.cmp(&b.0));
421    Ok(with_host(|h| {
422        let items: Vec<Value> = names
423            .into_iter()
424            .map(|(rel, parent, ft)| {
425                if file_types {
426                    let base = rel.rsplit('/').next().unwrap_or(&rel).to_string();
427                    build_dirent(h, base, &parent, ft)
428                } else {
429                    h.new_str(rel)
430                }
431            })
432            .collect();
433        h.new_array(items)
434    }))
435}
436
437/// Collect directory entries. `rel_prefix` is the path (relative to the root)
438/// under which `dir`'s children are listed; `parent` is the absolute-ish parent
439/// path recorded on each `Dirent`.
440fn collect_dir(
441    dir: &Path,
442    parent: &str,
443    rel_prefix: &str,
444    recursive: bool,
445    out: &mut Vec<(String, String, std::fs::FileType)>,
446) -> std::io::Result<()> {
447    for e in std::fs::read_dir(dir)? {
448        let e = e?;
449        let name = e.file_name().to_string_lossy().into_owned();
450        let rel = if rel_prefix.is_empty() {
451            name.clone()
452        } else {
453            format!("{rel_prefix}/{name}")
454        };
455        let ft = e.file_type()?;
456        out.push((rel.clone(), parent.to_string(), ft));
457        if recursive && ft.is_dir() {
458            let sub = e.path();
459            let sub_parent = sub.to_string_lossy().into_owned();
460            collect_dir(&sub, &sub_parent, &rel, recursive, out)?;
461        }
462    }
463    Ok(())
464}
465
466fn opendir_impl(args: &[Value]) -> Result<Value, String> {
467    let path = arg_str(args, 0);
468    let rd = std::fs::read_dir(&path).map_err(|e| err_str("opendir", &path, &e))?;
469    let mut entries: Vec<(String, std::fs::FileType)> = rd
470        .filter_map(|e| e.ok())
471        .filter_map(|e| {
472            e.file_type()
473                .ok()
474                .map(|ft| (e.file_name().to_string_lossy().into_owned(), ft))
475        })
476        .collect();
477    entries.sort_by(|a, b| a.0.cmp(&b.0));
478    Ok(with_host(|h| {
479        let dirents: Vec<Value> = entries
480            .into_iter()
481            .map(|(name, ft)| build_dirent(h, name, &path, ft))
482            .collect();
483        let arr = h.new_array(dirents);
484        let mut m = IndexMap::new();
485        m.insert("@@native".into(), h.new_str("Dir"));
486        m.insert("path".into(), h.new_str(path.clone()));
487        m.insert("@@entries".into(), arr);
488        m.insert("@@pos".into(), Value::Float(0.0));
489        h.new_object(m)
490    }))
491}
492
493// ── ownership / permissions / timestamps ─────────────────────────────────────
494
495fn access_impl(args: &[Value]) -> Result<Value, String> {
496    let path = arg_str(args, 0);
497    match std::fs::metadata(&path) {
498        Ok(_) => Ok(Value::Undef),
499        Err(e) => Err(err_str("access", &path, &e)),
500    }
501}
502
503fn chmod_impl(args: &[Value]) -> Result<Value, String> {
504    let path = arg_str(args, 0);
505    let mode = arg_num(args, 1) as u32;
506    match std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)) {
507        Ok(_) => Ok(Value::Undef),
508        Err(e) => Err(err_str("chmod", &path, &e)),
509    }
510}
511
512fn chown_impl(args: &[Value], follow: bool) -> Result<Value, String> {
513    let path = arg_str(args, 0);
514    let uid = arg_num(args, 1) as libc::uid_t;
515    let gid = arg_num(args, 2) as libc::gid_t;
516    let c = cpath(&path, "chown")?;
517    let rc = unsafe {
518        if follow {
519            libc::chown(c.as_ptr(), uid, gid)
520        } else {
521            libc::lchown(c.as_ptr(), uid, gid)
522        }
523    };
524    ok_or_errno(rc, "chown", &path)
525}
526
527fn utimes_impl(args: &[Value], follow: bool) -> Result<Value, String> {
528    let path = arg_str(args, 0);
529    let times = [
530        to_timeval(time_secs(args, 1)),
531        to_timeval(time_secs(args, 2)),
532    ];
533    let c = cpath(&path, "utimes")?;
534    let rc = unsafe {
535        if follow {
536            libc::utimes(c.as_ptr(), times.as_ptr())
537        } else {
538            libc::lutimes(c.as_ptr(), times.as_ptr())
539        }
540    };
541    ok_or_errno(rc, "utimes", &path)
542}
543
544// ── copy / link / rename / truncate ──────────────────────────────────────────
545
546const COPYFILE_EXCL: u32 = 1;
547
548fn copy_file_impl(args: &[Value]) -> Result<Value, String> {
549    let src = arg_str(args, 0);
550    let dest = arg_str(args, 1);
551    let mode = arg_num(args, 2);
552    if !mode.is_nan() && (mode as u32) & COPYFILE_EXCL != 0 && Path::new(&dest).exists() {
553        return Err(format!(
554            "Error: EEXIST: file already exists, copyfile '{src}' -> '{dest}'"
555        ));
556    }
557    match std::fs::copy(&src, &dest) {
558        Ok(_) => Ok(Value::Undef),
559        Err(e) => Err(err_str("copyFile", &src, &e)),
560    }
561}
562
563fn cp_impl(args: &[Value]) -> Result<Value, String> {
564    let src = arg_str(args, 0);
565    let dest = arg_str(args, 1);
566    let recursive = opt_flag(args, "recursive");
567    let r = if recursive {
568        cp_recursive(Path::new(&src), Path::new(&dest))
569    } else {
570        std::fs::copy(&src, &dest).map(|_| ())
571    };
572    match r {
573        Ok(_) => Ok(Value::Undef),
574        Err(e) => Err(err_str("cp", &src, &e)),
575    }
576}
577
578fn cp_recursive(src: &Path, dest: &Path) -> std::io::Result<()> {
579    if src.is_dir() {
580        std::fs::create_dir_all(dest)?;
581        for e in std::fs::read_dir(src)? {
582            let e = e?;
583            cp_recursive(&e.path(), &dest.join(e.file_name()))?;
584        }
585        Ok(())
586    } else {
587        if let Some(parent) = dest.parent() {
588            std::fs::create_dir_all(parent).ok();
589        }
590        std::fs::copy(src, dest).map(|_| ())
591    }
592}
593
594fn link_impl(args: &[Value]) -> Result<Value, String> {
595    let existing = arg_str(args, 0);
596    let new = arg_str(args, 1);
597    match std::fs::hard_link(&existing, &new) {
598        Ok(_) => Ok(Value::Undef),
599        Err(e) => Err(err_str("link", &existing, &e)),
600    }
601}
602
603fn symlink_impl(args: &[Value]) -> Result<Value, String> {
604    let target = arg_str(args, 0);
605    let path = arg_str(args, 1);
606    match std::os::unix::fs::symlink(&target, &path) {
607        Ok(_) => Ok(Value::Undef),
608        Err(e) => Err(err_str("symlink", &path, &e)),
609    }
610}
611
612fn readlink_impl(args: &[Value]) -> Result<Value, String> {
613    let path = arg_str(args, 0);
614    match std::fs::read_link(&path) {
615        Ok(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().into_owned()))),
616        Err(e) => Err(err_str("readlink", &path, &e)),
617    }
618}
619
620fn realpath_impl(args: &[Value]) -> Result<Value, String> {
621    let path = arg_str(args, 0);
622    match std::fs::canonicalize(&path) {
623        Ok(p) => Ok(with_host(|h| h.new_str(p.to_string_lossy().into_owned()))),
624        Err(e) => Err(err_str("realpath", &path, &e)),
625    }
626}
627
628fn rename_impl(args: &[Value]) -> Result<Value, String> {
629    let from = arg_str(args, 0);
630    let to = arg_str(args, 1);
631    match std::fs::rename(&from, &to) {
632        Ok(_) => Ok(Value::Undef),
633        Err(e) => Err(err_str("rename", &from, &e)),
634    }
635}
636
637fn truncate_impl(args: &[Value]) -> Result<Value, String> {
638    let path = arg_str(args, 0);
639    let len = arg_num(args, 1);
640    let len = if len.is_nan() { 0 } else { len as u64 };
641    let r = std::fs::OpenOptions::new()
642        .write(true)
643        .open(&path)
644        .and_then(|f| f.set_len(len));
645    match r {
646        Ok(_) => Ok(Value::Undef),
647        Err(e) => Err(err_str("truncate", &path, &e)),
648    }
649}
650
651fn mkdtemp_impl(args: &[Value]) -> Result<Value, String> {
652    let prefix = arg_str(args, 0);
653    for _ in 0..64 {
654        let candidate = format!("{prefix}{}", random_suffix());
655        match std::fs::create_dir(&candidate) {
656            Ok(_) => return Ok(with_host(|h| h.new_str(candidate))),
657            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
658            Err(e) => return Err(err_str("mkdtemp", &prefix, &e)),
659        }
660    }
661    Err(format!(
662        "Error: EEXIST: file already exists, mkdtemp '{prefix}'"
663    ))
664}
665
666/// Six random `[0-9A-Za-z]` characters — Node's `mkdtemp` suffix alphabet.
667fn random_suffix() -> String {
668    const CHARS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
669    let mut raw = [0u8; 6];
670    if getrandom::getrandom(&mut raw).is_err() {
671        // Fall back to a clock-derived seed; uniqueness is retried by the caller.
672        let nanos = std::time::SystemTime::now()
673            .duration_since(UNIX_EPOCH)
674            .map(|d| d.subsec_nanos())
675            .unwrap_or(0);
676        raw = nanos
677            .to_le_bytes()
678            .iter()
679            .cycle()
680            .take(6)
681            .copied()
682            .collect::<Vec<_>>()
683            .try_into()
684            .unwrap();
685    }
686    raw.iter()
687        .map(|b| CHARS[(*b as usize) % 62] as char)
688        .collect()
689}
690
691// ── file descriptors ─────────────────────────────────────────────────────────
692
693fn open_impl(args: &[Value]) -> Result<Value, String> {
694    let path = arg_str(args, 0);
695    let flags = match args.get(1) {
696        Some(v) if !matches!(v, Value::Undef) && !is_fn(v) => arg_str(args, 1),
697        _ => "r".to_string(),
698    };
699    match open_options(&flags).open(&path) {
700        Ok(f) => Ok(Value::Float(register_fd(f) as f64)),
701        Err(e) => Err(err_str("open", &path, &e)),
702    }
703}
704
705fn open_options(flags: &str) -> std::fs::OpenOptions {
706    let mut o = std::fs::OpenOptions::new();
707    match flags {
708        "r" | "rs" | "sr" => {
709            o.read(true);
710        }
711        "r+" | "rs+" | "sr+" => {
712            o.read(true).write(true);
713        }
714        "w" => {
715            o.write(true).create(true).truncate(true);
716        }
717        "wx" | "xw" => {
718            o.write(true).create_new(true);
719        }
720        "w+" => {
721            o.read(true).write(true).create(true).truncate(true);
722        }
723        "wx+" | "xw+" => {
724            o.read(true).write(true).create_new(true);
725        }
726        "a" => {
727            o.append(true).create(true);
728        }
729        "ax" | "xa" => {
730            o.append(true).create_new(true);
731        }
732        "a+" => {
733            o.read(true).append(true).create(true);
734        }
735        "ax+" | "xa+" => {
736            o.read(true).append(true).create_new(true);
737        }
738        _ => {
739            o.read(true);
740        }
741    }
742    o
743}
744
745fn close_impl(args: &[Value]) -> Result<Value, String> {
746    let fd = arg_num(args, 0) as i32;
747    if close_fd(fd) {
748        Ok(Value::Undef)
749    } else {
750        Err("Error: EBADF: bad file descriptor, close".to_string())
751    }
752}
753
754fn read_impl(args: &[Value]) -> Result<usize, String> {
755    let fd = arg_num(args, 0) as i32;
756    let buffer = args.get(1).cloned().unwrap_or(Value::Undef);
757    let cap = buf_len(&buffer);
758    let offset = num_or(args, 2, 0.0) as usize;
759    let length = num_or(args, 3, (cap.saturating_sub(offset)) as f64) as usize;
760    let position = position_arg(args, 4);
761    let n = with_file(fd, |file| {
762        let mut fr: &File = file;
763        if let Some(pos) = position {
764            fr.seek(SeekFrom::Start(pos)).ok();
765        }
766        let mut buf = vec![0u8; length];
767        fr.read(&mut buf).map(|n| {
768            buf.truncate(n);
769            buf
770        })
771    });
772    match n {
773        Some(Ok(data)) => Ok(write_into_buffer(&buffer, offset, &data)),
774        Some(Err(e)) => Err(err_str("read", "", &e)),
775        None => Err("Error: EBADF: bad file descriptor, read".to_string()),
776    }
777}
778
779fn write_impl(args: &[Value]) -> Result<usize, String> {
780    let fd = arg_num(args, 0) as i32;
781    let src = args.get(1).cloned().unwrap_or(Value::Undef);
782    let is_buffer = native_tag(&src).as_deref() == Some("Buffer");
783    // Buffer form: write(fd, buffer, offset, length, position)
784    // String form: write(fd, string, position, encoding)
785    let (data, position) = if is_buffer {
786        let all = buf_bytes(&src);
787        let offset = num_or(args, 2, 0.0) as usize;
788        let length = num_or(args, 3, (all.len().saturating_sub(offset)) as f64) as usize;
789        let end = (offset + length).min(all.len());
790        (
791            all[offset.min(all.len())..end].to_vec(),
792            position_arg(args, 4),
793        )
794    } else {
795        (
796            with_host(|h| h.str_of(&src)).into_bytes(),
797            position_arg(args, 2),
798        )
799    };
800    let r = with_file(fd, |file| {
801        let mut fr: &File = file;
802        if let Some(pos) = position {
803            fr.seek(SeekFrom::Start(pos)).ok();
804        }
805        fr.write(&data)
806    });
807    match r {
808        Some(Ok(n)) => Ok(n),
809        Some(Err(e)) => Err(err_str("write", "", &e)),
810        None => Err("Error: EBADF: bad file descriptor, write".to_string()),
811    }
812}
813
814fn readv_impl(args: &[Value]) -> Result<usize, String> {
815    let fd = arg_num(args, 0) as i32;
816    let buffers = array_items(args.get(1));
817    let position = position_arg(args, 2);
818    let total = with_file(fd, |file| {
819        let mut fr: &File = file;
820        if let Some(pos) = position {
821            fr.seek(SeekFrom::Start(pos)).ok();
822        }
823        let mut chunks: Vec<(Value, Vec<u8>)> = Vec::new();
824        for b in &buffers {
825            let cap = buf_len(b);
826            let mut buf = vec![0u8; cap];
827            match fr.read(&mut buf) {
828                Ok(0) => break,
829                Ok(n) => {
830                    buf.truncate(n);
831                    chunks.push((b.clone(), buf));
832                }
833                Err(e) => return Err(e),
834            }
835        }
836        Ok(chunks)
837    });
838    match total {
839        Some(Ok(chunks)) => Ok(chunks.iter().map(|(b, d)| write_into_buffer(b, 0, d)).sum()),
840        Some(Err(e)) => Err(err_str("readv", "", &e)),
841        None => Err("Error: EBADF: bad file descriptor, readv".to_string()),
842    }
843}
844
845fn writev_impl(args: &[Value]) -> Result<usize, String> {
846    let fd = arg_num(args, 0) as i32;
847    let buffers = array_items(args.get(1));
848    let position = position_arg(args, 2);
849    let mut data = Vec::new();
850    for b in &buffers {
851        data.extend(buf_bytes(b));
852    }
853    let r = with_file(fd, |file| {
854        let mut fr: &File = file;
855        if let Some(pos) = position {
856            fr.seek(SeekFrom::Start(pos)).ok();
857        }
858        fr.write(&data)
859    });
860    match r {
861        Some(Ok(n)) => Ok(n),
862        Some(Err(e)) => Err(err_str("writev", "", &e)),
863        None => Err("Error: EBADF: bad file descriptor, writev".to_string()),
864    }
865}
866
867fn fstat_impl(args: &[Value]) -> Result<Value, String> {
868    let fd = arg_num(args, 0) as i32;
869    let md = with_file(fd, |file| file.metadata());
870    match md {
871        Some(Ok(md)) => Ok(with_host(|h| build_stats(h, &md))),
872        Some(Err(e)) => Err(err_str("fstat", "", &e)),
873        None => Err("Error: EBADF: bad file descriptor, fstat".to_string()),
874    }
875}
876
877fn fchmod_impl(args: &[Value]) -> Result<Value, String> {
878    let fd = arg_num(args, 0) as i32;
879    let mode = arg_num(args, 1) as libc::mode_t;
880    let rc = with_file(fd, |file| unsafe { libc::fchmod(file.as_raw_fd(), mode) });
881    fd_result(rc, "fchmod")
882}
883
884fn fchown_impl(args: &[Value]) -> Result<Value, String> {
885    let fd = arg_num(args, 0) as i32;
886    let uid = arg_num(args, 1) as libc::uid_t;
887    let gid = arg_num(args, 2) as libc::gid_t;
888    let rc = with_file(fd, |file| unsafe {
889        libc::fchown(file.as_raw_fd(), uid, gid)
890    });
891    fd_result(rc, "fchown")
892}
893
894fn futimes_impl(args: &[Value]) -> Result<Value, String> {
895    let fd = arg_num(args, 0) as i32;
896    let times = [
897        to_timeval(time_secs(args, 1)),
898        to_timeval(time_secs(args, 2)),
899    ];
900    let rc = with_file(fd, |file| unsafe {
901        libc::futimes(file.as_raw_fd(), times.as_ptr())
902    });
903    fd_result(rc, "futimes")
904}
905
906fn ftruncate_impl(args: &[Value]) -> Result<Value, String> {
907    let fd = arg_num(args, 0) as i32;
908    let len = arg_num(args, 1);
909    let len = if len.is_nan() { 0 } else { len as u64 };
910    match with_file(fd, |file| file.set_len(len)) {
911        Some(Ok(_)) => Ok(Value::Undef),
912        Some(Err(e)) => Err(err_str("ftruncate", "", &e)),
913        None => Err("Error: EBADF: bad file descriptor, ftruncate".to_string()),
914    }
915}
916
917fn fsync_impl(args: &[Value], data_only: bool) -> Result<Value, String> {
918    let fd = arg_num(args, 0) as i32;
919    let op = if data_only { "fdatasync" } else { "fsync" };
920    let r = with_file(fd, |file| {
921        if data_only {
922            file.sync_data()
923        } else {
924            file.sync_all()
925        }
926    });
927    match r {
928        Some(Ok(_)) => Ok(Value::Undef),
929        Some(Err(e)) => Err(err_str(op, "", &e)),
930        None => Err(format!("Error: EBADF: bad file descriptor, {op}")),
931    }
932}
933
934/// Translate the return of a `with_file` libc call: `None` = bad fd, `Some(0)` =
935/// success, `Some(_)` = the C failure whose reason is in `errno`.
936fn fd_result(rc: Option<libc::c_int>, op: &str) -> Result<Value, String> {
937    match rc {
938        Some(0) => Ok(Value::Undef),
939        Some(_) => Err(err_str(op, "", &std::io::Error::last_os_error())),
940        None => Err(format!("Error: EBADF: bad file descriptor, {op}")),
941    }
942}
943
944// ── stat / statfs ────────────────────────────────────────────────────────────
945
946fn stat_impl(op: &str, args: &[Value], follow: bool) -> Result<Value, String> {
947    let path = arg_str(args, 0);
948    let md = if follow {
949        std::fs::metadata(&path)
950    } else {
951        std::fs::symlink_metadata(&path)
952    };
953    match md {
954        Ok(md) => Ok(with_host(|h| build_stats(h, &md))),
955        Err(e) => Err(err_str(op, &path, &e)),
956    }
957}
958
959fn statfs_impl(args: &[Value]) -> Result<Value, String> {
960    let path = arg_str(args, 0);
961    let c = cpath(&path, "statfs")?;
962    let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
963    if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
964        return Err(err_str("statfs", &path, &std::io::Error::last_os_error()));
965    }
966    Ok(with_host(|h| {
967        let mut m = IndexMap::new();
968        m.insert("type".into(), Value::Float(st.f_fsid as f64));
969        m.insert("bsize".into(), Value::Float(st.f_bsize as f64));
970        m.insert("blocks".into(), Value::Float(st.f_blocks as f64));
971        m.insert("bfree".into(), Value::Float(st.f_bfree as f64));
972        m.insert("bavail".into(), Value::Float(st.f_bavail as f64));
973        m.insert("files".into(), Value::Float(st.f_files as f64));
974        m.insert("ffree".into(), Value::Float(st.f_ffree as f64));
975        h.new_object(m)
976    }))
977}
978
979/// Build a full `fs.Stats` object from `Metadata` (unix fields via `MetadataExt`).
980fn build_stats(h: &mut crate::host::JsHost, md: &std::fs::Metadata) -> Value {
981    let ns = |s: i64, n: i64| s as f64 * 1000.0 + n as f64 / 1_000_000.0;
982    let atime_ms = ns(md.atime(), md.atime_nsec());
983    let mtime_ms = ns(md.mtime(), md.mtime_nsec());
984    let ctime_ms = ns(md.ctime(), md.ctime_nsec());
985    let birth_ms = md
986        .created()
987        .ok()
988        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
989        .map(|d| d.as_secs_f64() * 1000.0)
990        .unwrap_or(mtime_ms);
991    let ft = md.file_type();
992    let date = |h: &mut crate::host::JsHost, ms: f64| {
993        let mut d = IndexMap::new();
994        d.insert("@@native".into(), h.new_str("Date"));
995        d.insert("@@ms".into(), Value::Float(ms));
996        h.new_object(d)
997    };
998    let mut m = IndexMap::new();
999    m.insert("@@native".into(), h.new_str("Stats"));
1000    m.insert("@@isFile".into(), Value::Bool(md.is_file()));
1001    m.insert("@@isDir".into(), Value::Bool(md.is_dir()));
1002    m.insert("@@isSymlink".into(), Value::Bool(ft.is_symlink()));
1003    m.insert("dev".into(), Value::Float(md.dev() as f64));
1004    m.insert("mode".into(), Value::Float(md.mode() as f64));
1005    m.insert("nlink".into(), Value::Float(md.nlink() as f64));
1006    m.insert("uid".into(), Value::Float(md.uid() as f64));
1007    m.insert("gid".into(), Value::Float(md.gid() as f64));
1008    m.insert("rdev".into(), Value::Float(md.rdev() as f64));
1009    m.insert("blksize".into(), Value::Float(md.blksize() as f64));
1010    m.insert("ino".into(), Value::Float(md.ino() as f64));
1011    m.insert("size".into(), Value::Float(md.len() as f64));
1012    m.insert("blocks".into(), Value::Float(md.blocks() as f64));
1013    m.insert("atimeMs".into(), Value::Float(atime_ms));
1014    m.insert("mtimeMs".into(), Value::Float(mtime_ms));
1015    m.insert("ctimeMs".into(), Value::Float(ctime_ms));
1016    m.insert("birthtimeMs".into(), Value::Float(birth_ms));
1017    let atime = date(h, atime_ms);
1018    m.insert("atime".into(), atime);
1019    let mtime = date(h, mtime_ms);
1020    m.insert("mtime".into(), mtime);
1021    let ctime = date(h, ctime_ms);
1022    m.insert("ctime".into(), ctime);
1023    let birthtime = date(h, birth_ms);
1024    m.insert("birthtime".into(), birthtime);
1025    h.new_object(m)
1026}
1027
1028// ── watchFile / unwatchFile ──────────────────────────────────────────────────
1029
1030fn watch_file(args: &[Value]) -> Result<Value, String> {
1031    let path = arg_str(args, 0);
1032    let Some(listener) = args.last().cloned().filter(is_fn) else {
1033        return Ok(Value::Undef);
1034    };
1035    let interval = interval_opt(args).unwrap_or(5007.0).max(1.0) as u64;
1036    let abs = std::fs::canonicalize(&path)
1037        .map(|p| p.to_string_lossy().into_owned())
1038        .unwrap_or_else(|_| path.clone());
1039
1040    let stop = Arc::new(AtomicBool::new(false));
1041    let id = NEXT_WATCH_ID.with(|n| {
1042        let v = *n.borrow();
1043        *n.borrow_mut() = v + 1;
1044        v
1045    });
1046    WATCHERS.with(|w| {
1047        w.borrow_mut().push(WatchEntry {
1048            id,
1049            path: abs.clone(),
1050            listener: listener.clone(),
1051            stop: stop.clone(),
1052        });
1053    });
1054    with_host(|h| h.incr_handle());
1055
1056    let tx = with_host(|h| h.io_sender());
1057    let poll_stop = stop.clone();
1058    let poll_listener = listener;
1059    std::thread::spawn(move || {
1060        let mut prev = stat_parts(&abs);
1061        loop {
1062            if poll_stop.load(Ordering::Acquire) {
1063                break;
1064            }
1065            std::thread::sleep(Duration::from_millis(interval));
1066            if poll_stop.load(Ordering::Acquire) {
1067                break;
1068            }
1069            let curr = stat_parts(&abs);
1070            if curr != prev {
1071                let l = poll_listener.clone();
1072                let (p0, p1, p2) = prev;
1073                let (c0, c1, c2) = curr;
1074                let _ = tx.send(Box::new(move || {
1075                    let cur = with_host(|h| stats_from_parts(h, c0, c1, c2));
1076                    let old = with_host(|h| stats_from_parts(h, p0, p1, p2));
1077                    if let Err(e) = invoke(&l, vec![cur, old], None) {
1078                        eprintln!("{e}");
1079                    }
1080                    Ok(())
1081                }));
1082                prev = curr;
1083            }
1084        }
1085    });
1086    Ok(Value::Undef)
1087}
1088
1089fn unwatch_file(args: &[Value]) -> Result<Value, String> {
1090    let path = arg_str(args, 0);
1091    let abs = std::fs::canonicalize(&path)
1092        .map(|p| p.to_string_lossy().into_owned())
1093        .unwrap_or_else(|_| path.clone());
1094    let listener = args.get(1).cloned().filter(is_fn);
1095    let removed = WATCHERS.with(|w| {
1096        let mut w = w.borrow_mut();
1097        let mut count = 0;
1098        w.retain(|e| {
1099            let matches =
1100                e.path == abs && listener.as_ref().map(|l| *l == e.listener).unwrap_or(true);
1101            if matches {
1102                e.stop.store(true, Ordering::Release);
1103                count += 1;
1104            }
1105            !matches
1106        });
1107        count
1108    });
1109    for _ in 0..removed {
1110        with_host(|h| h.decr_handle());
1111    }
1112    // Wake the event loop so it can re-evaluate the handle count.
1113    if removed > 0 {
1114        let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1115    }
1116    Ok(Value::Undef)
1117}
1118
1119/// A file's watch-relevant state: `(exists, mtime_ms, size)`.
1120fn stat_parts(path: &str) -> (bool, i64, u64) {
1121    match std::fs::metadata(path) {
1122        Ok(md) => (
1123            true,
1124            md.mtime() * 1000 + md.mtime_nsec() / 1_000_000,
1125            md.len(),
1126        ),
1127        Err(_) => (false, 0, 0),
1128    }
1129}
1130
1131fn stats_from_parts(h: &mut crate::host::JsHost, exists: bool, mtime_ms: i64, size: u64) -> Value {
1132    let ms = mtime_ms as f64;
1133    let date = |h: &mut crate::host::JsHost, ms: f64| {
1134        let mut d = IndexMap::new();
1135        d.insert("@@native".into(), h.new_str("Date"));
1136        d.insert("@@ms".into(), Value::Float(ms));
1137        h.new_object(d)
1138    };
1139    let mut m = IndexMap::new();
1140    m.insert("@@native".into(), h.new_str("Stats"));
1141    m.insert("@@isFile".into(), Value::Bool(exists));
1142    m.insert("@@isDir".into(), Value::Bool(false));
1143    m.insert("@@isSymlink".into(), Value::Bool(false));
1144    m.insert(
1145        "size".into(),
1146        Value::Float(if exists { size as f64 } else { 0.0 }),
1147    );
1148    m.insert("atimeMs".into(), Value::Float(ms));
1149    m.insert("mtimeMs".into(), Value::Float(ms));
1150    m.insert("ctimeMs".into(), Value::Float(ms));
1151    m.insert("birthtimeMs".into(), Value::Float(ms));
1152    let mt = date(h, ms);
1153    m.insert("mtime".into(), mt);
1154    let at = date(h, ms);
1155    m.insert("atime".into(), at);
1156    h.new_object(m)
1157}
1158
1159// ── read / write streams ─────────────────────────────────────────────────────
1160
1161fn create_read_stream(args: &[Value]) -> Result<Value, String> {
1162    let path = arg_str(args, 0);
1163    let enc = encoding_arg(args, 1);
1164    let stream = with_host(|h| {
1165        let mut extra = IndexMap::new();
1166        extra.insert("path".into(), h.new_str(path.clone()));
1167        if let Some(e) = &enc {
1168            extra.insert("@@encoding".into(), h.new_str(e.clone()));
1169        }
1170        extra
1171    });
1172    let stream = super::net::new_emitter_object("FSReadStream", stream);
1173    with_host(|h| h.incr_handle());
1174    let recv = stream.clone();
1175    let p = path;
1176    with_host(|h| {
1177        h.queue_micro_native(Box::new(move || {
1178            read_stream_pump(&recv, &p);
1179            Ok(())
1180        }))
1181    });
1182    Ok(stream)
1183}
1184
1185/// Read the whole file and drive the stream's events on the microtask tick after
1186/// creation (so synchronously-attached `on('data')`/`on('end')` listeners fire).
1187fn read_stream_pump(recv: &Value, path: &str) {
1188    with_host(|h| h.decr_handle());
1189    let bytes = match std::fs::read(path) {
1190        Ok(b) => b,
1191        Err(e) => {
1192            let ev = with_host(|h| crate::builtins::synth_error(h, &err_str("open", path, &e)));
1193            let _ = super::events::instance_call(
1194                recv,
1195                "emit",
1196                vec![with_host(|h| h.new_str("error")), ev],
1197            );
1198            return;
1199        }
1200    };
1201    let enc = get_prop(recv, "@@encoding").map(|v| with_host(|h| h.str_of(&v)));
1202    let chunk = match enc.as_deref() {
1203        Some(e) if e != "buffer" => {
1204            with_host(|h| h.new_str(String::from_utf8_lossy(&bytes).into_owned()))
1205        }
1206        _ => super::buffer::from_bytes(&bytes),
1207    };
1208    if let Some(dest) = get_prop(recv, "@@pipeDest") {
1209        let _ = crate::host::call_method(&dest, "write", vec![chunk]);
1210        let _ = crate::host::call_method(&dest, "end", vec![]);
1211    } else {
1212        let name = with_host(|h| h.new_str("data"));
1213        let _ = super::events::instance_call(recv, "emit", vec![name, chunk]);
1214    }
1215    let _ = super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("end"))]);
1216    let _ = super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1217}
1218
1219pub const READ_STREAM_METHODS: &[&str] = &[
1220    "pipe",
1221    "pause",
1222    "resume",
1223    "setEncoding",
1224    "destroy",
1225    "close",
1226    "read",
1227];
1228
1229/// `fs.ReadStream` instance dispatch (tag `FSReadStream`). Emitter methods are
1230/// handled by the parent's shared emitter routing; this covers the stream-only
1231/// surface.
1232pub fn read_stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1233    match method {
1234        "pipe" => {
1235            if let Some(dest) = args.first().cloned() {
1236                set_prop(recv, "@@pipeDest", dest.clone());
1237                Ok(dest)
1238            } else {
1239                Ok(recv.clone())
1240            }
1241        }
1242        "setEncoding" => {
1243            set_prop(
1244                recv,
1245                "@@encoding",
1246                with_host(|h| h.new_str(super::arg_str(&args, 0))),
1247            );
1248            Ok(recv.clone())
1249        }
1250        "pause" | "resume" | "read" => Ok(recv.clone()),
1251        "destroy" | "close" => {
1252            let _ =
1253                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1254            Ok(recv.clone())
1255        }
1256        _ => Err(crate::host::type_error(&format!(
1257            "stream.{method} is not a function"
1258        ))),
1259    }
1260}
1261
1262fn create_write_stream(args: &[Value]) -> Result<Value, String> {
1263    let path = arg_str(args, 0);
1264    let flags = match encoding_flag(args, "flags") {
1265        Some(f) => f,
1266        None => "w".to_string(),
1267    };
1268    let file = open_options(&flags)
1269        .open(&path)
1270        .map_err(|e| err_str("open", &path, &e))?;
1271    let fd = register_fd(file);
1272    let stream = with_host(|h| {
1273        let mut extra = IndexMap::new();
1274        extra.insert("path".into(), h.new_str(path));
1275        extra.insert("@@wfd".into(), Value::Float(fd as f64));
1276        extra.insert("bytesWritten".into(), Value::Float(0.0));
1277        extra
1278    });
1279    let stream = super::net::new_emitter_object("FSWriteStream", stream);
1280    with_host(|h| h.incr_handle());
1281    let recv = stream.clone();
1282    with_host(|h| {
1283        h.queue_micro_native(Box::new(move || {
1284            let _ =
1285                super::events::instance_call(&recv, "emit", vec![with_host(|h| h.new_str("open"))]);
1286            let _ = super::events::instance_call(
1287                &recv,
1288                "emit",
1289                vec![with_host(|h| h.new_str("ready"))],
1290            );
1291            Ok(())
1292        }))
1293    });
1294    Ok(stream)
1295}
1296
1297pub const WRITE_STREAM_METHODS: &[&str] = &[
1298    "write",
1299    "end",
1300    "destroy",
1301    "close",
1302    "cork",
1303    "uncork",
1304    "setDefaultEncoding",
1305];
1306
1307/// `fs.WriteStream` instance dispatch (tag `FSWriteStream`).
1308pub fn write_stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1309    match method {
1310        "write" => {
1311            write_stream_bytes(recv, args.first());
1312            if let Some(cb) = args.iter().find(|v| is_fn(v)).cloned() {
1313                let _ = invoke(&cb, vec![], None);
1314            }
1315            Ok(Value::Bool(true))
1316        }
1317        "end" => {
1318            if let Some(chunk) = args
1319                .first()
1320                .filter(|v| !matches!(v, Value::Undef) && !is_fn(v))
1321            {
1322                write_stream_bytes(recv, Some(chunk));
1323            }
1324            if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32)
1325            {
1326                close_fd(fd);
1327            }
1328            let _ = super::events::instance_call(
1329                recv,
1330                "emit",
1331                vec![with_host(|h| h.new_str("finish"))],
1332            );
1333            let _ =
1334                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1335            with_host(|h| h.decr_handle());
1336            if let Some(cb) = args.iter().find(|v| is_fn(v)).cloned() {
1337                let _ = invoke(&cb, vec![], None);
1338            }
1339            Ok(recv.clone())
1340        }
1341        "destroy" | "close" => {
1342            if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32)
1343            {
1344                close_fd(fd);
1345            }
1346            let _ =
1347                super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))]);
1348            with_host(|h| h.decr_handle());
1349            Ok(recv.clone())
1350        }
1351        "cork" | "uncork" | "setDefaultEncoding" => Ok(recv.clone()),
1352        _ => Err(crate::host::type_error(&format!(
1353            "stream.{method} is not a function"
1354        ))),
1355    }
1356}
1357
1358fn write_stream_bytes(recv: &Value, chunk: Option<&Value>) {
1359    let Some(chunk) = chunk else { return };
1360    let data = value_bytes(chunk);
1361    if let Some(fd) = get_prop(recv, "@@wfd").map(|v| with_host(|h| h.to_number(&v)) as i32) {
1362        let written = with_file(fd, |file| {
1363            let mut fr: &File = file;
1364            fr.write(&data).unwrap_or(0)
1365        })
1366        .unwrap_or(0);
1367        let prev = get_prop(recv, "bytesWritten")
1368            .map(|v| with_host(|h| h.to_number(&v)))
1369            .unwrap_or(0.0);
1370        set_prop(recv, "bytesWritten", Value::Float(prev + written as f64));
1371    }
1372}
1373
1374// ── Stats / Dirent / Dir instance surfaces ───────────────────────────────────
1375
1376/// `fs.Stats` method dispatch (`isFile`/`isDirectory`/…).
1377pub fn stats_call(recv: &Value, method: &str) -> Result<Value, String> {
1378    type_test(recv, method, "stats")
1379}
1380
1381pub const DIRENT_METHODS: &[&str] = &[
1382    "isFile",
1383    "isDirectory",
1384    "isSymbolicLink",
1385    "isBlockDevice",
1386    "isCharacterDevice",
1387    "isFIFO",
1388    "isSocket",
1389];
1390
1391/// `fs.Dirent` method dispatch (tag `Dirent`); `name`/`parentPath`/`path` are
1392/// plain data properties resolved by the generic object path.
1393pub fn dirent_call(recv: &Value, method: &str) -> Result<Value, String> {
1394    type_test(recv, method, "dirent")
1395}
1396
1397/// Shared `is*` predicates for `Stats` and `Dirent` (both carry `@@isFile`/
1398/// `@@isDir`/`@@isSymlink`; the device/FIFO/socket predicates are always false —
1399/// node-js does not model those file types).
1400fn type_test(recv: &Value, method: &str, what: &str) -> Result<Value, String> {
1401    let read = |key: &str| {
1402        with_host(|h| match h.get(recv) {
1403            Some(JsObj::Object(p)) => matches!(p.get(key), Some(Value::Bool(true))),
1404            _ => false,
1405        })
1406    };
1407    match method {
1408        "isFile" => Ok(Value::Bool(read("@@isFile"))),
1409        "isDirectory" => Ok(Value::Bool(read("@@isDir"))),
1410        "isSymbolicLink" => Ok(Value::Bool(read("@@isSymlink"))),
1411        "isBlockDevice" | "isCharacterDevice" | "isFIFO" | "isSocket" => Ok(Value::Bool(false)),
1412        _ => Err(crate::host::type_error(&format!(
1413            "{what}.{method} is not a function"
1414        ))),
1415    }
1416}
1417
1418pub const DIR_METHODS: &[&str] = &["read", "readSync", "close", "closeSync"];
1419
1420/// `fs.Dir` method dispatch (tag `Dir`); `path` is a plain data property.
1421pub fn dir_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1422    match method {
1423        "readSync" => Ok(dir_next(recv)),
1424        "read" => {
1425            let v = dir_next(recv);
1426            if let Some(cb) = args.first().filter(|c| is_fn(c)).cloned() {
1427                with_host(|h| {
1428                    let n = h.null();
1429                    h.queue_micro(cb, vec![n, v]);
1430                });
1431                Ok(Value::Undef)
1432            } else {
1433                Ok(settled_ok(v))
1434            }
1435        }
1436        "closeSync" => Ok(Value::Undef),
1437        "close" => {
1438            if let Some(cb) = args.first().filter(|c| is_fn(c)).cloned() {
1439                with_host(|h| {
1440                    let n = h.null();
1441                    h.queue_micro(cb, vec![n]);
1442                });
1443                Ok(Value::Undef)
1444            } else {
1445                Ok(settled_ok(Value::Undef))
1446            }
1447        }
1448        _ => Err(crate::host::type_error(&format!(
1449            "dir.{method} is not a function"
1450        ))),
1451    }
1452}
1453
1454/// Advance the `Dir` cursor and return the next `Dirent`, or `null` when drained.
1455fn dir_next(recv: &Value) -> Value {
1456    with_host(|h| {
1457        let (entries, pos) = match h.get(recv) {
1458            Some(JsObj::Object(p)) => (
1459                p.get("@@entries").cloned(),
1460                p.get("@@pos").map(|v| h.to_number(v) as usize).unwrap_or(0),
1461            ),
1462            _ => (None, 0),
1463        };
1464        let item = match entries.as_ref().and_then(|e| h.get(e)) {
1465            Some(JsObj::Array(items)) => items.get(pos).cloned(),
1466            _ => None,
1467        };
1468        if item.is_some() {
1469            if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1470                p.insert("@@pos".into(), Value::Float((pos + 1) as f64));
1471            }
1472        }
1473        item.unwrap_or_else(|| h.null())
1474    })
1475}
1476
1477fn build_dirent(
1478    h: &mut crate::host::JsHost,
1479    name: String,
1480    parent: &str,
1481    ft: std::fs::FileType,
1482) -> Value {
1483    let mut m = IndexMap::new();
1484    m.insert("@@native".into(), h.new_str("Dirent"));
1485    m.insert("name".into(), h.new_str(name));
1486    let pp = h.new_str(parent.to_string());
1487    m.insert("parentPath".into(), pp.clone());
1488    m.insert("path".into(), pp);
1489    m.insert("@@isFile".into(), Value::Bool(ft.is_file()));
1490    m.insert("@@isDir".into(), Value::Bool(ft.is_dir()));
1491    m.insert("@@isSymlink".into(), Value::Bool(ft.is_symlink()));
1492    h.new_object(m)
1493}
1494
1495// ── glob ─────────────────────────────────────────────────────────────────────
1496
1497fn glob_impl(args: &[Value]) -> Result<Value, String> {
1498    let pattern = arg_str(args, 0);
1499    let absolute = pattern.starts_with('/');
1500    let base = if absolute {
1501        PathBuf::from("/")
1502    } else {
1503        std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1504    };
1505    let segs: Vec<String> = pattern
1506        .split('/')
1507        .filter(|s| !s.is_empty())
1508        .map(String::from)
1509        .collect();
1510    let mut out: Vec<String> = Vec::new();
1511    let prefix = if absolute {
1512        "/".to_string()
1513    } else {
1514        String::new()
1515    };
1516    glob_walk(&base, &segs, 0, &prefix, &mut out);
1517    out.sort();
1518    out.dedup();
1519    Ok(with_host(|h| {
1520        let items: Vec<Value> = out.into_iter().map(|s| h.new_str(s)).collect();
1521        h.new_array(items)
1522    }))
1523}
1524
1525fn glob_walk(dir: &Path, segs: &[String], idx: usize, prefix: &str, out: &mut Vec<String>) {
1526    if idx >= segs.len() {
1527        if !prefix.is_empty() && prefix != "/" {
1528            out.push(prefix.trim_end_matches('/').to_string());
1529        }
1530        return;
1531    }
1532    let seg = &segs[idx];
1533    if seg == "**" {
1534        glob_walk(dir, segs, idx + 1, prefix, out);
1535        if let Ok(rd) = std::fs::read_dir(dir) {
1536            for e in rd.flatten() {
1537                if e.path().is_dir() {
1538                    let name = e.file_name().to_string_lossy().into_owned();
1539                    let np = join_glob(prefix, &name);
1540                    glob_walk(&e.path(), segs, idx, &np, out);
1541                }
1542            }
1543        }
1544        return;
1545    }
1546    let last = idx + 1 == segs.len();
1547    if let Ok(rd) = std::fs::read_dir(dir) {
1548        for e in rd.flatten() {
1549            let name = e.file_name().to_string_lossy().into_owned();
1550            if name.starts_with('.') && !seg.starts_with('.') {
1551                continue;
1552            }
1553            if wildcard_match(seg, &name) {
1554                let np = join_glob(prefix, &name);
1555                if last {
1556                    out.push(np);
1557                } else if e.path().is_dir() {
1558                    glob_walk(&e.path(), segs, idx + 1, &np, out);
1559                }
1560            }
1561        }
1562    }
1563}
1564
1565fn join_glob(prefix: &str, name: &str) -> String {
1566    if prefix.is_empty() {
1567        name.to_string()
1568    } else if prefix.ends_with('/') {
1569        format!("{prefix}{name}")
1570    } else {
1571        format!("{prefix}/{name}")
1572    }
1573}
1574
1575/// Match a single path segment against a `*`/`?` glob (no character classes).
1576fn wildcard_match(pat: &str, name: &str) -> bool {
1577    let p: Vec<char> = pat.chars().collect();
1578    let n: Vec<char> = name.chars().collect();
1579    let (mut pi, mut ni) = (0usize, 0usize);
1580    let (mut star, mut mark) = (None, 0usize);
1581    while ni < n.len() {
1582        if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
1583            pi += 1;
1584            ni += 1;
1585        } else if pi < p.len() && p[pi] == '*' {
1586            star = Some(pi);
1587            mark = ni;
1588            pi += 1;
1589        } else if let Some(s) = star {
1590            pi = s + 1;
1591            mark += 1;
1592            ni = mark;
1593        } else {
1594            return false;
1595        }
1596    }
1597    while pi < p.len() && p[pi] == '*' {
1598        pi += 1;
1599    }
1600    pi == p.len()
1601}
1602
1603// ── small helpers ────────────────────────────────────────────────────────────
1604
1605fn settled_ok(v: Value) -> Value {
1606    let p = with_host(|h| h.new_promise());
1607    let id = with_host(|h| h.promise_id(&p).unwrap_or(0));
1608    crate::host::resolve_promise_val(id, v);
1609    p
1610}
1611
1612fn get_prop(recv: &Value, key: &str) -> Option<Value> {
1613    with_host(|h| match h.get(recv) {
1614        Some(JsObj::Object(p)) => p.get(key).cloned(),
1615        _ => None,
1616    })
1617}
1618
1619fn set_prop(recv: &Value, key: &str, val: Value) {
1620    with_host(|h| {
1621        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1622            p.insert(key.to_string(), val);
1623        }
1624    });
1625}
1626
1627/// Raw bytes of a Buffer arg, or the utf-8 bytes of anything else.
1628fn value_bytes(v: &Value) -> Vec<u8> {
1629    if native_tag(v).as_deref() == Some("Buffer") {
1630        buf_bytes(v)
1631    } else {
1632        with_host(|h| h.str_of(v)).into_bytes()
1633    }
1634}
1635
1636fn buf_bytes(v: &Value) -> Vec<u8> {
1637    with_host(|h| match h.get(v) {
1638        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
1639            Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
1640            _ => Vec::new(),
1641        },
1642        _ => Vec::new(),
1643    })
1644}
1645
1646fn buf_len(v: &Value) -> usize {
1647    with_host(|h| match h.get(v) {
1648        Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
1649            Some(JsObj::Array(items)) => items.len(),
1650            _ => 0,
1651        },
1652        _ => 0,
1653    })
1654}
1655
1656/// Write `data` into a Buffer's backing byte array at `offset`; returns the count
1657/// actually written (bounded by the buffer capacity).
1658fn write_into_buffer(buf: &Value, offset: usize, data: &[u8]) -> usize {
1659    let Some(arr) = get_prop(buf, "@@bytes") else {
1660        return 0;
1661    };
1662    with_host(|h| {
1663        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
1664            let mut n = 0;
1665            for (i, b) in data.iter().enumerate() {
1666                let idx = offset + i;
1667                if idx >= items.len() {
1668                    break;
1669                }
1670                items[idx] = Value::Float(*b as f64);
1671                n += 1;
1672            }
1673            n
1674        } else {
1675            0
1676        }
1677    })
1678}
1679
1680fn array_items(v: Option<&Value>) -> Vec<Value> {
1681    match v {
1682        Some(v) => with_host(|h| match h.get(v) {
1683            Some(JsObj::Array(items)) => items.clone(),
1684            _ => Vec::new(),
1685        }),
1686        None => Vec::new(),
1687    }
1688}
1689
1690fn opt_flag(args: &[Value], key: &str) -> bool {
1691    with_host(|h| {
1692        args.iter().any(|v| {
1693            matches!(h.get(v), Some(JsObj::Object(p)) if matches!(p.get(key), Some(Value::Bool(true))))
1694        })
1695    })
1696}
1697
1698fn encoding_flag(args: &[Value], key: &str) -> Option<String> {
1699    with_host(|h| {
1700        for v in args {
1701            if let Some(JsObj::Object(p)) = h.get(v) {
1702                if let Some(val) = p.get(key) {
1703                    return Some(h.str_of(val));
1704                }
1705            }
1706        }
1707        None
1708    })
1709}
1710
1711fn interval_opt(args: &[Value]) -> Option<f64> {
1712    with_host(|h| {
1713        for v in args {
1714            if let Some(JsObj::Object(p)) = h.get(v) {
1715                if let Some(val) = p.get("interval") {
1716                    return Some(h.to_number(val));
1717                }
1718            }
1719        }
1720        None
1721    })
1722}
1723
1724fn num_or(args: &[Value], i: usize, default: f64) -> f64 {
1725    match args.get(i) {
1726        Some(v) if !matches!(v, Value::Undef) => {
1727            let n = with_host(|h| h.to_number(v));
1728            if n.is_nan() {
1729                default
1730            } else {
1731                n
1732            }
1733        }
1734        _ => default,
1735    }
1736}
1737
1738/// A byte `position` argument: `null`/`undefined` means "current file offset".
1739fn position_arg(args: &[Value], i: usize) -> Option<u64> {
1740    match args.get(i) {
1741        Some(Value::Undef) | None => None,
1742        Some(v) if with_host(|h| h.is_null(v)) => None,
1743        Some(v) => {
1744            let n = with_host(|h| h.to_number(v));
1745            if n.is_nan() || n < 0.0 {
1746                None
1747            } else {
1748                Some(n as u64)
1749            }
1750        }
1751    }
1752}
1753
1754/// A timestamp argument in **seconds**. A `Date` argument stores milliseconds, so
1755/// divide; a bare number is already seconds (Node's `fs.utimes` convention).
1756fn time_secs(args: &[Value], i: usize) -> f64 {
1757    match args.get(i) {
1758        Some(v) if native_tag(v).as_deref() == Some("Date") => arg_num(args, i) / 1000.0,
1759        _ => arg_num(args, i),
1760    }
1761}
1762
1763fn to_timeval(secs: f64) -> libc::timeval {
1764    let s = secs.floor();
1765    let us = ((secs - s) * 1_000_000.0).round();
1766    libc::timeval {
1767        tv_sec: s as libc::time_t,
1768        tv_usec: us as libc::suseconds_t,
1769    }
1770}
1771
1772fn cpath(path: &str, op: &str) -> Result<CString, String> {
1773    CString::new(path).map_err(|_| format!("Error: EINVAL: invalid argument, {op} '{path}'"))
1774}
1775
1776fn ok_or_errno(rc: libc::c_int, op: &str, path: &str) -> Result<Value, String> {
1777    if rc == 0 {
1778        Ok(Value::Undef)
1779    } else {
1780        Err(err_str(op, path, &std::io::Error::last_os_error()))
1781    }
1782}
1783
1784fn encoding_arg(args: &[Value], i: usize) -> Option<String> {
1785    match args.get(i) {
1786        Some(Value::Undef) | None => None,
1787        Some(v) => {
1788            let s = with_host(|h| h.str_of(v));
1789            if s == "undefined" || s == "[object Object]" || s == "null" {
1790                None
1791            } else {
1792                Some(s)
1793            }
1794        }
1795    }
1796}
1797
1798/// The failing *syscall* Node names in a filesystem error, for the JS-level
1799/// entry points whose API name differs from it. `fs.readFileSync('/nope')`
1800/// reports `open`, not `readFileSync`, because `open(2)` is what failed —
1801/// and `err.syscall` is load-bearing for packages that branch on it.
1802fn syscall_name(op: &str) -> &str {
1803    match op {
1804        "readFileSync" | "readFile" | "writeFile" | "writeFileSync" | "appendFile"
1805        | "appendFileSync" | "createReadStream" | "createWriteStream" => "open",
1806        "readdir" | "readdirSync" | "opendir" => "scandir",
1807        other => other,
1808    }
1809}
1810
1811/// The `CODE: reason` half of a libuv error message (`ENOENT: no such file or
1812/// directory`), shared by every syscall wrapper that has to render one.
1813///
1814/// Deliberately NOT Rust's `io::Error` `Display`: Rust capitalizes and appends
1815/// its own `(os error 2)`, so `No such file or directory (os error 2)` is a
1816/// string no Node has ever printed. `process.chdir` was assembling its message
1817/// that way.
1818pub(crate) fn libuv_message(e: &std::io::Error) -> String {
1819    use std::io::ErrorKind::*;
1820    let code = match e.kind() {
1821        NotFound => "ENOENT",
1822        PermissionDenied => "EACCES",
1823        AlreadyExists => "EEXIST",
1824        NotADirectory => "ENOTDIR",
1825        IsADirectory => "EISDIR",
1826        DirectoryNotEmpty => "ENOTEMPTY",
1827        InvalidInput => "EINVAL",
1828        BrokenPipe => "EPIPE",
1829        _ => "EIO",
1830    };
1831    format!("{code}: {}", libuv_reason(code))
1832}
1833
1834/// libuv's own lowercase reason string for a code.
1835fn libuv_reason(code: &str) -> &'static str {
1836    match code {
1837        "ENOENT" => "no such file or directory",
1838        "EACCES" => "permission denied",
1839        "EEXIST" => "file already exists",
1840        "ENOTDIR" => "not a directory",
1841        "EISDIR" => "illegal operation on a directory",
1842        "ENOTEMPTY" => "directory not empty",
1843        "EINVAL" => "invalid argument",
1844        "EPIPE" => "broken pipe",
1845        _ => "i/o error",
1846    }
1847}
1848
1849fn err_str(op: &str, path: &str, e: &std::io::Error) -> String {
1850    use std::io::ErrorKind::*;
1851    let code = match e.kind() {
1852        NotFound => "ENOENT",
1853        PermissionDenied => "EACCES",
1854        AlreadyExists => "EEXIST",
1855        NotADirectory => "ENOTDIR",
1856        IsADirectory => "EISDIR",
1857        DirectoryNotEmpty => "ENOTEMPTY",
1858        InvalidInput => "EINVAL",
1859        BrokenPipe => "EPIPE",
1860        _ => "EIO",
1861    };
1862    // libuv's own reason strings, not Rust's `io::Error` `Display` — Rust
1863    // capitalizes ("No such file or directory") where libuv (and therefore
1864    // Node's `err.message`) does not.
1865    let reason = libuv_reason(code);
1866    let op = syscall_name(op);
1867    if path.is_empty() {
1868        format!("Error: {code}: {reason}, {op}")
1869    } else {
1870        format!("Error: {code}: {reason}, {op} '{path}'")
1871    }
1872}