Repository navigation
Conversation
…root entries Recursive readdir withFileTypes computed the root entries' parentPath as dirname(join(root, name)), which normalizes: "./d/../d" came back as "d" and "d/" as "d". Node reports the caller's string for root entries and path.join(root, sub) for nested ones. Compute parentPath once per directory from the caller's path instead. On Windows the sync paths also used the normalized `\\?\`-stripped copy instead of the caller's string.
|
Status Reproduced on bun 1.4.3-canary (Linux x64 and Windows x64) against node v26.3.0: fs.mkdirSync("d/sub", { recursive: true }); fs.writeFileSync("d/a.txt", "");
fs.readdirSync("./d/../d", { recursive: true, withFileTypes: true }).map(e => e.parentPath);
// node: [ './d/../d', './d/../d', 'd/sub'... ] bun: [ 'd', 'd', ... ]
fs.readdirSync("d/", { recursive: true, withFileTypes: true })[0].parentPath;
// node: 'd/' bun: 'd'
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. WalkthroughChanges
Readdir path handling
Suggested reviewers: Priority: ➖ Normal — Schedule the filesystem compatibility fix because it corrects caller-preserved root paths across synchronous, asynchronous, recursive, POSIX, and Windows readdir behavior. Merge Risk: ⚪ Minimal · up to Dirent.parentPath now preserves the caller’s root path spelling while recursive nested entries retain joined parent paths. The change is covered for synchronous and promise-based readdir behavior, with no remaining merge-blocking risk identified. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — focused Node compat fix that also simplifies the per-entry path computation into a per-directory one.
What was reviewed:
- Verified
root_basenamein the async path is the caller's bytes (copied fromargs.path.slice()at task creation, line 2340), so both sync and async now echo the unnormalized string at the root. - Checked the shared
spillbuffer in the async path:dirent_parent_path's result isclone_utf8'd before the loop reusesspillfor entry-name joins, so no aliasing. - Confirmed dropping
without_nt_prefixis safe here — the new source isargs.path.slice(), not theslice_zcopy that carried the NT prefix; the helper is still used elsewhere in this file. - Test uses
tempDir,path.sep/path.joinfor cross-platform, sorts entries, and covers sync + promises × flat + recursive across three unnormalized root spellings.
Extended reasoning...
Overview
This PR fixes Dirent.parentPath in fs.readdir/readdirSync with withFileTypes: true to match Node.js: root-level entries report the caller's path string verbatim (trailing slashes, .., ./ preserved), and nested entries in recursive mode report path.join(root, subdir). The old code computed dirname(join(root, entry_rel_name)) per entry, which normalized the root. The refactor introduces a small dirent_parent_path() helper that returns the caller's bytes at the root and join(root, rel) below it, computed once per directory instead of once per entry. The now-unused basename parameter is dropped from readdir_with_entries and its Windows _u16 twin. A test in fs.test.ts covers three unnormalized root spellings across sync/promises × flat/recursive.
Security risks
None. This is a display-string change to Dirent.parentPath; the actual directory iteration still uses openat relative to the root fd and is unaffected. No untrusted-input parsing, no path-containment check depends on this string, and no auth/crypto/permissions code is touched.
Level of scrutiny
Moderate. It's a Node compat fix in src/runtime/node/, which per repo guidance means Node's observed behavior is the spec — and the PR description cites exactly that (Node's JS queue walker passes the root untouched and path.join for nested). REVIEW.md's "fix the whole class" rule is satisfied: the non-recursive path, sync recursive, async recursive, and the Windows UTF-16 branch all now source parentPath from args.path.slice() / root_path symmetrically. I verified async_task.root_path is an owned copy of args.path.slice() (line 2340), so the async path echoes the same bytes as sync. For nested entries the new join(root, rel) is equivalent to the old dirname(join(root, rel/name)) since name is a single component, so no behavior change there.
Other factors
The change is a net simplification (removes per-iteration join+dirname and the dirent_path_prev cache-compare dance). The test is well-formed per test/CLAUDE.md: tempDir from harness, path.sep/path.join for cross-platform separators, sorted output for determinism, added to the existing fs.test.ts alongside the neighboring "Dirent has the expected fields" test. No CODEOWNERS cover these files. The bug hunter ran to dry_streak with no findings. The PR description notes verification on both Linux and Windows and against the vendored test-fs-readdir* node tests.
|
Updated 8:39 AM PT - Sep 8th, 2026
❌ @robobun, your commit 2b8b385 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 41975That installs a local version of the PR into your bun-41975 --bun |
Problem
fs.readdirSync(p, { recursive: true, withFileTypes: true })andfs.promises.readdirgave root entries a normalizedparentPath:"./d/../d"came back as"d","d/"as"d". Node reports the caller's string for root entries andpath.join(root, sub)for nested ones. Non-recursivereaddiralready echoed the caller's string on POSIX.readdir_with_entries_recursive_{async,sync}(src/runtime/node/node_fs.rs). They computed every entry'sparentPathasdirname(join(root, rel/name)), andjoinnormalizes. On Windows the sync paths (recursive and not) also started from theslice_zcopy, which is\\?\-prefixed and normalized, soC:\x\sub\..came back asC:\x.Fix
parentPathonce per directory with a smalldirent_parent_path(root, rel)helper: the caller's bytes (args.path.slice()) at the root,join(root, rel)below it. For nested entries that is byte-identical to the olddirname(join(root, rel/name)), sincenameis one component.args.path.slice()too, so sync, async, and Windows agree.test/js/node/fs/fs.test.ts("Dirent.parentPath of a root entry is the caller's path as given") fails on 1.4.3-canary and passes with this change, on Linux and on Windows. Also ran the readdir/Dirent tests infs.test.ts,dir.test.ts,glob.test.ts, and the vendoredtest-fs-readdir*node tests on both.Background
Dirent.parentPathis the directory argument that produced the entry. Node's recursive readdir is a JS queue walker: root entries get thepathargument untouched, deeper ones getpath.join(parent, name), so only nested values are normalized.openatrelative to the root fd, so the listing itself never depended on this string; only the reportedparentPathdid.Notes
readdirSync('./d/../d', {recursive, withFileTypes}): bund, node./d/../d;d/link/..: bund, node the caller's string). Bun's recursive listing is kernel-correct where node's JS walker is lexical (node throws ENOENT ford/link/..with recursion); that part is unchanged.fs.cp*JS walker append child names instead ofpath.join(fixes asymlink/..source but changes the strings afiltercallback sees for./src-style roots, where node passessrc/x), and using the resolved path verbatim infs.watchFile(fixesa\bbeing watched asa/bon POSIX, butfs.watchhas the same join and wants the same treatment in one change).test/bundler/compile-asset-bunfs.test.tsalready notes "parentPath is the caller's string verbatim" for the standalone-graph readdir; this makes the real-filesystem paths agree with it.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/fs/fs.test.ts