diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 828858d06994..2cb08d97ab21 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -6107,7 +6107,6 @@ impl NodeFS { fn readdir_with_entries( args: &args::Readdir, fd: FD, - basename: &ZStr, entries: &mut Vec, ) -> Maybe<()> { // On Windows, String/Dirent results read native UTF-16 entry names via the @@ -6115,7 +6114,7 @@ impl NodeFS { // use the u8 iterator. #[cfg(windows)] if T::IS_U16 { - return Self::readdir_with_entries_u16::(args, fd, basename, entries); + return Self::readdir_with_entries_u16::(args, fd, entries); } let mut dirent_path = BunString::DEAD; @@ -6128,9 +6127,10 @@ impl NodeFS { Ok(Some(ent)) => ent, }; + // `parentPath` is the caller's path as given, like node. if T::IS_DIRENT && dirent_path.is_empty() { dirent_path = webcore::encoding::to_bun_string( - without_nt_prefix::(basename.as_bytes()), + args.path.slice(), encoding_to_node(args.encoding), ); } @@ -6158,7 +6158,6 @@ impl NodeFS { fn readdir_with_entries_u16( args: &args::Readdir, fd: FD, - basename: &ZStr, entries: &mut Vec, ) -> Maybe<()> { let mut dirent_path = BunString::DEAD; @@ -6183,7 +6182,7 @@ impl NodeFS { if T::IS_DIRENT && dirent_path.is_empty() { dirent_path = webcore::encoding::to_bun_string( - without_nt_prefix::(basename.as_bytes()), + args.path.slice(), encoding_to_node(args.encoding), ); } @@ -6209,6 +6208,20 @@ impl NodeFS { matches!(errno, E::ENOENT | E::ENOTDIR | E::EPERM) } + /// `Dirent.parentPath`: the caller's path as given at the root, `join(root, rel)` below it (as node). + fn dirent_parent_path<'a>( + root: &'a [u8], + rel: Option<&[u8]>, + spill: &'a mut Vec, + ) -> &'a [u8] { + match rel { + None => root, + Some(rel) => { + paths::resolve_path::join_spill::(spill, &[root, rel]) + } + } + } + pub(crate) fn readdir_with_entries_recursive_async( buf: &mut PathBuffer, async_task: &mut AsyncReaddirRecursiveTask, @@ -6278,9 +6291,16 @@ impl NodeFS { }); let mut iterator = DirIterator::WrappedIterator::init(fd); - let mut dirent_path_prev = BunString::EMPTY; let mut spill: Vec = Vec::new(); - let mut dirent_spill: Vec = Vec::new(); + let dirent_path = if T::IS_DIRENT { + BunString::clone_utf8(Self::dirent_parent_path( + root_basename, + (!is_root).then(|| basename.as_bytes()), + &mut spill, + )) + } else { + BunString::EMPTY + }; loop { let current = match iterator.next() { @@ -6360,22 +6380,12 @@ impl NodeFS { } } - if T::IS_DIRENT { - let joined = paths::resolve_path::join_spill::( - &mut dirent_spill, - &[root_basename, name_to_copy], - ); - let path_u8 = paths::resolve_path::dirname::(joined); - if dirent_path_prev.is_empty() || dirent_path_prev.byte_slice() != path_u8 { - dirent_path_prev = BunString::clone_utf8(path_u8); - } - } // async path: uses raw `BunString::clone_utf8` — do not apply encoding. T::append_entry_recursive( entries, utf8_name, name_to_copy, - &dirent_path_prev, + &dirent_path, effective_kind, async_task.encoding, false, @@ -6469,7 +6479,18 @@ impl NodeFS { }); let mut iterator = DirIterator::WrappedIterator::init(fd); - let mut dirent_path_prev = BunString::DEAD; + let dirent_path = if T::IS_DIRENT { + webcore::encoding::to_bun_string( + Self::dirent_parent_path( + args.path.slice(), + (!is_root).then_some(basename_bytes), + &mut dirent_spill, + ), + encoding_to_node(args.encoding), + ) + } else { + BunString::EMPTY + }; loop { let current = match iterator.next() { @@ -6533,25 +6554,12 @@ impl NodeFS { } } - if T::IS_DIRENT { - let joined = paths::resolve_path::join_spill::( - &mut dirent_spill, - &[root_basename.as_bytes(), name_to_copy], - ); - let path_u8 = paths::resolve_path::dirname::(joined); - if dirent_path_prev.is_empty() || dirent_path_prev.byte_slice() != path_u8 { - dirent_path_prev = webcore::encoding::to_bun_string( - without_nt_prefix::(path_u8), - encoding_to_node(args.encoding), - ); - } - } // sync path: uses `webcore::encoding::to_bun_string(.., args.encoding)`. T::append_entry_recursive( entries, utf8_name, name_to_copy, - &dirent_path_prev, + &dirent_path, effective_kind, args.encoding, true, @@ -6643,8 +6651,7 @@ impl NodeFS { let _close = scopeguard::guard(fd, |fd| fd.close()); let mut entries: Vec = Vec::new(); - Self::readdir_with_entries::(args, fd, path, &mut entries) - .map(|()| T::into_readdir(entries)) + Self::readdir_with_entries::(args, fd, &mut entries).map(|()| T::into_readdir(entries)) } /// Caller has already checked `is_bun_standalone_file_path(path)`. diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 55663f10f4e3..250ee8ce6811 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1125,6 +1125,28 @@ it("Dirent has the expected fields", () => { expect(dirs[0].parentPath).toBe(dir); }); +it("Dirent.parentPath of a root entry is the caller's path as given", async () => { + using dir = tempDir("readdir-parentPath", { "a.txt": "", "sub/b.txt": "" }); + const pairs = (ents: Dirent[]) => ents.map(e => [e.parentPath, e.name]).sort((a, b) => a[1].localeCompare(b[1])); + // Node hands root entries the path string untouched and nested entries + // path.join(root, subdir), so only the nested ones come back normalized. + for (const root of [`${dir}${path.sep}sub${path.sep}..`, `${dir}${path.sep}`, `${dir}${path.sep}.${path.sep}`]) { + const flat = [ + [root, "a.txt"], + [root, "sub"], + ]; + const recursive = [ + [root, "a.txt"], + [path.join(root, "sub"), "b.txt"], + [root, "sub"], + ]; + expect(pairs(readdirSync(root, { withFileTypes: true }))).toEqual(flat); + expect(pairs(await promises.readdir(root, { withFileTypes: true }))).toEqual(flat); + expect(pairs(readdirSync(root, { recursive: true, withFileTypes: true }))).toEqual(recursive); + expect(pairs(await promises.readdir(root, { recursive: true, withFileTypes: true }))).toEqual(recursive); + } +}); + it("promises.readdir on a large folder", async () => { const huge = tmpdirSync(); for (let i = 0; i < 128; i++) {