1use 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 "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 "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 "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 "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 "watchFile",
119 "unwatchFile",
120 "createReadStream",
121 "createWriteStream",
122];
123
124thread_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 #[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
159pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
162 Some(match method {
163 "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 "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 "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 "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 "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
258fn 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| {
279 let ev = crate::builtins::synth_error(h, &e);
280 h.queue_micro(cb, vec![ev]);
281 }),
282 }
283 Ok(Value::Undef)
284}
285
286fn 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
309fn 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 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 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
334fn encoded_bytes(args: &[Value]) -> Vec<u8> {
342 let v = args.get(1).cloned().unwrap_or(Value::Undef);
343 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 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 with_host(|h| {
404 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
432fn 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
492fn 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
548fn 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
599const 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 Err(e) => Err(err_str("realpath", &resolved_prefix(&path), &e)),
684 }
685}
686
687fn 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 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
752fn 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 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
777fn 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 let view = super::buffer::view_bytes(&src);
871 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
1021fn 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
1031fn 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
1066fn 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 for k in ["atime", "mtime", "ctime", "birthtime"] {
1119 h.hide_prop(&obj, k);
1120 }
1121 obj
1122}
1123
1124fn 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 if removed > 0 {
1210 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1211 }
1212 Ok(Value::Undef)
1213}
1214
1215fn 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
1255fn 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
1281fn 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
1325pub 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
1403pub 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
1470pub 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
1487pub fn dirent_call(recv: &Value, method: &str) -> Result<Value, String> {
1490 type_test(recv, method, "dirent")
1491}
1492
1493fn 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
1516pub 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
1550fn 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
1591fn 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
1671fn 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
1699fn 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
1723fn 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
1751fn 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
1833fn 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
1849fn 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
1879fn 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
1908fn 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 "copyFile" | "copyFileSync" => "copyfile",
1923 "realpath" | "realpathSync" => "lstat",
1924 other => other.strip_suffix("Sync").unwrap_or(other),
1925 }
1926}
1927
1928fn err_str2(op: &str, from: &str, to: &str, e: &std::io::Error) -> String {
1932 format!("{} -> '{to}'", err_str(op, from, e))
1933}
1934
1935pub(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
1964fn 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 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}