Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 42 additions & 35 deletions src/runtime/node/node_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6107,15 +6107,14 @@ impl NodeFS {
fn readdir_with_entries<T: ReaddirEntry>(
args: &args::Readdir,
fd: FD,
basename: &ZStr,
entries: &mut Vec<T>,
) -> Maybe<()> {
// On Windows, String/Dirent results read native UTF-16 entry names via the
// wide iterator so surrogate pairs survive; Buffer results (and all POSIX)
// use the u8 iterator.
#[cfg(windows)]
if T::IS_U16 {
return Self::readdir_with_entries_u16::<T>(args, fd, basename, entries);
return Self::readdir_with_entries_u16::<T>(args, fd, entries);
}

let mut dirent_path = BunString::DEAD;
Expand All @@ -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::<u8>(basename.as_bytes()),
args.path.slice(),
encoding_to_node(args.encoding),
);
}
Expand Down Expand Up @@ -6158,7 +6158,6 @@ impl NodeFS {
fn readdir_with_entries_u16<T: ReaddirEntry>(
args: &args::Readdir,
fd: FD,
basename: &ZStr,
entries: &mut Vec<T>,
) -> Maybe<()> {
let mut dirent_path = BunString::DEAD;
Expand All @@ -6183,7 +6182,7 @@ impl NodeFS {

if T::IS_DIRENT && dirent_path.is_empty() {
dirent_path = webcore::encoding::to_bun_string(
without_nt_prefix::<u8>(basename.as_bytes()),
args.path.slice(),
encoding_to_node(args.encoding),
);
}
Expand All @@ -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<u8>,
) -> &'a [u8] {
match rel {
None => root,
Some(rel) => {
paths::resolve_path::join_spill::<paths::platform::Auto>(spill, &[root, rel])
}
}
}

pub(crate) fn readdir_with_entries_recursive_async<T: ReaddirEntry>(
buf: &mut PathBuffer,
async_task: &mut AsyncReaddirRecursiveTask,
Expand Down Expand Up @@ -6278,9 +6291,16 @@ impl NodeFS {
});

let mut iterator = DirIterator::WrappedIterator::init(fd);
let mut dirent_path_prev = BunString::EMPTY;
let mut spill: Vec<u8> = Vec::new();
let mut dirent_spill: Vec<u8> = 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() {
Expand Down Expand Up @@ -6360,22 +6380,12 @@ impl NodeFS {
}
}

if T::IS_DIRENT {
let joined = paths::resolve_path::join_spill::<paths::platform::Auto>(
&mut dirent_spill,
&[root_basename, name_to_copy],
);
let path_u8 = paths::resolve_path::dirname::<paths::platform::Auto>(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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -6533,25 +6554,12 @@ impl NodeFS {
}
}

if T::IS_DIRENT {
let joined = paths::resolve_path::join_spill::<paths::platform::Auto>(
&mut dirent_spill,
&[root_basename.as_bytes(), name_to_copy],
);
let path_u8 = paths::resolve_path::dirname::<paths::platform::Auto>(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::<u8>(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,
Expand Down Expand Up @@ -6643,8 +6651,7 @@ impl NodeFS {
let _close = scopeguard::guard(fd, |fd| fd.close());

let mut entries: Vec<T> = Vec::new();
Self::readdir_with_entries::<T>(args, fd, path, &mut entries)
.map(|()| T::into_readdir(entries))
Self::readdir_with_entries::<T>(args, fd, &mut entries).map(|()| T::into_readdir(entries))
}

/// Caller has already checked `is_bun_standalone_file_path(path)`.
Expand Down
22 changes: 22 additions & 0 deletions test/js/node/fs/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
Loading