diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 86c0e9fd6c71..2139e600590a 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -7770,7 +7770,13 @@ impl NodeFS { ..Default::default() }); }; - let path_len = joined.len(); + // Node's JS realpath walks components, so trailing separators on a + // regular file are ignored; only realpath.native (realpath(3)) errors. + let path_len = if variant == RealpathVariant::Emulated { + strings::without_trailing_slash(joined).len() + } else { + joined.len() + }; inbuf[path_len] = 0; let path = ZStr::from_buf(&inbuf[..], path_len); @@ -7790,7 +7796,6 @@ impl NodeFS { Ok(buf_) => buf_, }; - let _ = variant; if args.encoding == Encoding::Utf8 { if let PathLike::SliceWithUnderlyingString(s) = &args.path { if strings::eql_long(s.slice(), buf, true) { diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 7770856c2eb0..040c80aad834 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2497,6 +2497,32 @@ it.if(isPosix)("realpathSync resolves root, regular files, and symlinks", () => expect(realpathSync(linkPath)).toBe(self); }); +// Node's JS realpath/realpathSync walks path components, so a trailing +// separator on a regular file is a no-op. Only realpath.native (realpath(3)) +// rejects it with ENOTDIR. +it.if(isPosix)("realpathSync ignores trailing slash on a regular file", async () => { + using dir = tempDir("fs-realpath-trailing-slash", { "f": "hello" }); + const real = realpathSync(String(dir)); + const file = join(real, "f"); + const link = join(real, "link"); + symlinkSync(file, link); + + expect(realpathSync(file + "/")).toBe(file); + expect(realpathSync(file + "//")).toBe(file); + expect(realpathSync(link + "/")).toBe(file); + expect(realpathSync(Buffer.from(file + "/"))).toBe(file); + expect(realpathSync(real + "/")).toBe(real); + expect(realpathSync("/")).toBe("/"); + + const { promise, resolve, reject } = Promise.withResolvers(); + fs.realpath(file + "/", (err, p) => (err ? reject(err) : resolve(p))); + expect(await promise).toBe(file); + + expect(() => realpathSync.native(file + "/")).toThrow( + expect.objectContaining({ code: "ENOTDIR", syscall: "realpath" }), + ); +}); + // src/sys/sys.zig getFdPath has an exhaustive per-OS switch: .windows // (GetFinalPathNameByHandle), .mac (F_GETPATH), .linux (/proc/self/fd, also // covers Android), .freebsd (fcntl F_KINFO + struct_kinfo_file). On every