From c97a1e35845ca04924b818a6a0d60074040723c9 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Sat, 30 May 2026 03:40:26 +0000 Subject: [PATCH] fix(fs): throw opendir errors for invalid paths --- crates/perry-runtime/src/fs/callbacks.rs | 10 ++-- crates/perry-runtime/src/fs/dir_glob_watch.rs | 44 +++++++++------ crates/perry-runtime/src/fs/mod.rs | 10 ++++ .../fs/readdir/opendir-invalid-paths.ts | 53 +++++++++++++++++++ 4 files changed, 95 insertions(+), 22 deletions(-) create mode 100644 test-parity/node-suite/fs/readdir/opendir-invalid-paths.ts diff --git a/crates/perry-runtime/src/fs/callbacks.rs b/crates/perry-runtime/src/fs/callbacks.rs index 92b6a7230..1cdba3d2e 100644 --- a/crates/perry-runtime/src/fs/callbacks.rs +++ b/crates/perry-runtime/src/fs/callbacks.rs @@ -324,13 +324,13 @@ pub extern "C" fn js_fs_opendir_callback(path_value: f64, arg1: f64, arg2: f64) const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; let cb = last_callback(&[arg1, arg2]); - unsafe { - if let Some(err_val) = fs_callback_read_error(path_value, "opendir") { - call_cb_err2(cb, err_val); + let dir = match js_fs_opendir_value(path_value) { + Ok(dir) => dir, + Err(err) => { + unsafe { call_cb_err2(cb, err) }; return f64::from_bits(TAG_UNDEFINED); } - } - let dir = js_fs_opendir_sync(path_value); + }; if !cb.is_null() { crate::closure::js_closure_call2(cb, f64::from_bits(TAG_NULL), dir); } diff --git a/crates/perry-runtime/src/fs/dir_glob_watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch.rs index b81bc085d..aaee20e2e 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch.rs @@ -8,29 +8,39 @@ use crate::closure::ClosureHeader; use super::*; pub extern "C" fn js_fs_opendir_sync(path_value: f64) -> f64 { + match js_fs_opendir_value(path_value) { + Ok(dir) => dir, + Err(err) => crate::exception::js_throw(err), + } +} + +pub(crate) fn js_fs_opendir_value(path_value: f64) -> Result { + validate::validate_path("path", path_value); unsafe { let path = match decode_path_value(path_value) { - Some(s) => s, - None => return build_dir_object(alloc_dir_state(Vec::new()), ""), + Some(path) => path, + None => validate::throw_invalid_path_arg("path", path_value), + }; + let read_dir = match fs::read_dir(&path) { + Ok(read_dir) => read_dir, + Err(err) => return Err(build_fs_error_value_no_path(&err, "opendir")), }; let mut entries = Vec::new(); - if let Ok(read_dir) = fs::read_dir(&path) { - let mut items: Vec<(String, std::fs::FileType)> = Vec::new(); - for entry in read_dir.flatten() { - if let (Some(name), Ok(ft)) = (entry.file_name().to_str(), entry.file_type()) { - items.push((name.to_string(), ft)); - } - } - items.sort_by(|a, b| a.0.cmp(&b.0)); - for (name, ft) in items { - entries.push(build_dirent_object( - &name, - &path, - DirentKind::from_file_type(&ft), - )); + let mut items: Vec<(String, std::fs::FileType)> = Vec::new(); + for entry in read_dir.flatten() { + if let (Some(name), Ok(ft)) = (entry.file_name().to_str(), entry.file_type()) { + items.push((name.to_string(), ft)); } } - build_dir_object(alloc_dir_state(entries), &path) + items.sort_by(|a, b| a.0.cmp(&b.0)); + for (name, ft) in items { + entries.push(build_dirent_object( + &name, + &path, + DirentKind::from_file_type(&ft), + )); + } + Ok(build_dir_object(alloc_dir_state(entries), &path)) } } diff --git a/crates/perry-runtime/src/fs/mod.rs b/crates/perry-runtime/src/fs/mod.rs index 779a171ef..5bde8e8e6 100644 --- a/crates/perry-runtime/src/fs/mod.rs +++ b/crates/perry-runtime/src/fs/mod.rs @@ -1760,6 +1760,16 @@ unsafe fn build_fs_error_value(err: &std::io::Error, syscall: &'static str, path crate::value::js_nanbox_pointer(err_ptr as i64) } +unsafe fn build_fs_error_value_no_path(err: &std::io::Error, syscall: &'static str) -> f64 { + let code = io_error_code(err); + let msg = format!("{}: {}, {}", code, err, syscall); + let msg_ptr = js_string_from_bytes(msg.as_ptr(), msg.len() as u32); + let err_ptr = crate::error::js_error_new_with_message(msg_ptr); + crate::node_submodules::register_error_code_pub(msg_ptr, code); + crate::node_submodules::register_error_syscall(msg_ptr, syscall); + crate::value::js_nanbox_pointer(err_ptr as i64) +} + /// Probe a path for read access and produce a NaN-boxed Error if the /// underlying syscall would fail. Returns `None` on success. unsafe fn fs_callback_read_error(path_value: f64, syscall: &'static str) -> Option { diff --git a/test-parity/node-suite/fs/readdir/opendir-invalid-paths.ts b/test-parity/node-suite/fs/readdir/opendir-invalid-paths.ts new file mode 100644 index 000000000..2ef8eb6ec --- /dev/null +++ b/test-parity/node-suite/fs/readdir/opendir-invalid-paths.ts @@ -0,0 +1,53 @@ +import * as fs from "node:fs"; + +const ROOT = "/tmp/perry_node_suite_fs_opendir_invalid_paths"; +try { fs.rmSync(ROOT, { recursive: true, force: true }); } catch (_e) {} +fs.mkdirSync(ROOT); +const file = ROOT + "/file.txt"; +const missing = ROOT + "/missing"; +fs.writeFileSync(file, "x"); + +function probe(label: string, fn: () => any) { + try { + const value = fn(); + console.log(label, "no-throw", !!value); + if (value) value.closeSync(); + } catch (err: any) { + console.log(label, err.name, err.code || "", err.syscall || ""); + } +} + +probe("opendirSync missing", () => fs.opendirSync(missing)); +probe("opendirSync file", () => fs.opendirSync(file)); +probe("opendirSync null", () => fs.opendirSync(null as any)); +probe("opendirSync object", () => fs.opendirSync({} as any)); + +const dir = fs.opendirSync(ROOT); +console.log("opendirSync valid path:", dir.path === ROOT); +dir.closeSync(); + +await new Promise((resolve) => { + fs.opendir(missing, (err, dir) => { + console.log( + "opendir callback missing", + err && err.name, + err && (err as any).code, + err && (err as any).syscall, + dir === undefined, + ); + resolve(); + }); +}); + +await new Promise((resolve) => { + fs.opendir(file, (err, dir) => { + console.log( + "opendir callback file", + err && err.name, + err && (err as any).code, + err && (err as any).syscall, + dir === undefined, + ); + resolve(); + }); +});