Skip to content

Consolidate node fs/path/types helpers - #32021

Open
alii wants to merge 3 commits into
mainfrom
claude/split/node-compat
Open

alii wants to merge 3 commits into
mainfrom
claude/split/node-compat

Conversation

@alii

@alii alii commented Jun 9, 2026 •

Copy link
Copy Markdown
Member

What this does

Dedupes repeated argument-conversion and error-path blocks in node_fs.rs and removes dead helpers in node path/types. Net -330 lines in src (323 added, 653 removed) after the rebases below.

Split from #31912 (whole-repo simplification pass; closing that PR in favor of module-scoped splits). This PR only moves and removes code — zero intended behavior change. Verified there by a per-file behavioral-equivalence audit and full CI; verified here by a standalone full-workspace compile check.

Tests

test/js/node/fs/fs-chown-utimes.test.ts pins the consolidated surface: chown/fchown/lchown must validate uid and gid identically and name the failing argument itself, utimes/futimes/lutimes likewise for atime/mtime, and utimesSync must follow symlinks while lutimesSync must not (the dispatch the merged utimes_with preserves). Because this PR intends zero behavior change, these tests pass before and after the refactor by design; they guard the shared readers against future drift. A test that fails on main cannot exist for this PR: any such failure would mean the consolidation changed observable behavior, which is the one outcome it must not have. The correctness argument is the behavioral-equivalence audit plus the full existing suite passing on both sides, not a fail-before proof.

The error.syscall suite added on main by #32964 runs through the consolidated utimes_with and passes, so the utime/lutime naming is covered by an existing test that would fail if the consolidation got it wrong. The upstream node path-* and fs utimes/chown/copyfile/cp suites (102 files, including the ones #34505 added) also pass on this branch.

Rebase notes

Rebased eight times as main moved; the consolidation itself is unchanged except where noted.

Every function main has modified in these files that this PR does not consolidate is byte-identical to main on this branch, and the only main-added lines this branch removes are the signature lines of the functions it rewrites. The Linux copy helper was re-verified against current main: everything before the removed loop block is identical at both call sites, the removed block at the fs.copyFile site contains the EINTR retry (EintrPolicy::Retry) and the one at the fs.cp site does not (EintrPolicy::Surface), and the new FreeBSD copy paths main added are separate cfg blocks the helper does not touch. cargo check passes for the windows-msvc, apple-darwin, and freebsd targets in addition to the full Linux debug build and clippy.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Refactors Node filesystem and path runtime: shared UID/GID and timestamp argument parsers, centralized Linux/Android copy_file_range with EINTR policy and fallbacks, unified utimes/lutimes helper, path-scanning core for basename/extname, macro-generated enum labels, a type-detection simplification, and added tests.

Changes

Node Runtime Consolidation

Layer / File(s) Summary
Argument parsing helpers
src/runtime/node/node_fs.rs
id_from_js, time_arg_from_js, and times_from_js helpers eliminate duplicated uid/gid and timestamp validation; Chown, Fchown, Lutimes, and Futimes now delegate to these helpers.
Copy file range EINTR and fallback logic
src/runtime/node/node_fs.rs
EintrPolicy enum and copy_file_range_with_fallbacks() helper centralize Linux/Android copy logic: chunked syscalls with policy-based EINTR handling, unsupported-error detection, and sendfile/read-write fallback.
Copy operations routing
src/runtime/node/node_fs.rs
copy_file_inner and _copy_single_file_sync delegate to the shared copy helper with appropriate EINTR policies (Retry vs Surface), removing inline loop and fallback scaffolding.
Timestamp operations consolidation
src/runtime/node/node_fs.rs
utimes_with() helper unifies Windows and non-Windows timestamp syscall invocation; utimes and lutimes delegate to it with platform-specific function pointers, eliminating duplicated branching.
Function enum codegen
src/runtime/node/node_fs.rs
node_fs_function_enum! macro generates both NodeFSFunctionEnum variants and the heap_label() match mapping, replacing hand-written enum and function.
Path basename refactoring
src/runtime/node/path.rs
is_sep_t predicate and basename_t::<T, IS_WINDOWS> const-generic implementation unify POSIX and Windows separator and drive-letter handling; public entry points delegate to the shared core.
Path component scanning
src/runtime/node/path.rs
LastComponentScan struct and scan_last_component_t helper centralize reverse-scanning for path components; extname_t::<T, IS_WINDOWS> consumes this to compute extensions for both platforms.
Path parse integration
src/runtime/node/path.rs
parse_posix_t and parse_windows_t use scan_last_component_t for non-dir info extraction, eliminating duplicated reverse-scanning loops.
Type predicate simplification
src/runtime/node/types.rs
StringOrBuffer::from_js_maybe_async_into uses is_array_buffer_like() predicate instead of explicit JSType enumeration, simplifying ArrayBufferView detection.
FS tests added
test/js/node/fs/fs-chown-utimes.test.ts
Adds tests for chown*/utimes* argument validation and symlink vs target behavior for utimesSync/lutimesSync.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: consolidating helpers across the Node fs, path, and types modules.
Description check ✅ Passed The description provides substantial details about the changes, scope, behavior-preservation goal, tests, verification, and rebase context. It does not use the template headings exactly, but it covers…

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jun 9, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 12:26 AM PT - Sep 8th, 2026

❌ @robobun, your commit c1e08db has 1 failures in Build #112481 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32021

That installs a local version of the PR into your bun-32021 executable, so you can run:

bun-32021 --bun

@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Jun 9, 2026 •

Copy link
Copy Markdown
Collaborator

Adopted. Audited the full diff against main for behavior changes and found none; details and the mechanical equivalence checks are in the comments below. Added test/js/node/fs/fs-chown-utimes.test.ts pinning the consolidated uid/gid and atime/mtime readers (message, constructor and err.code) and the utimes/lutimes symlink dispatch.

Current state: rebased onto main at c1e08db (conflict-free, 323+/653- in src; the Windows arm of the shared utimes helper follows #40860's to_error(tag) form). CI build 112481: 178 jobs passed and every fs and path suite is green on all lanes that ran. The red is unrelated to this PR: the x64-asan shard fails test-crypto-dh-leak.js, which is red on every PR build in this window (reported), and the darwin x64 test shards keep expiring in the agent queue without running. Ready for review and merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/node/node_fs.rs (1)

8083-8142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

lutimes() now reports the wrong syscall on Windows failures.

utimes_with() hardcodes sys::Tag::utime, and lutimes() now funnels through it. On Windows that changes the exposed err.syscall from lutime to utime for fs.lutimes() failures.

Suggested fix
     fn utimes_with(
         &mut self,
         args: &args::Utimes,
+        syscall: sys::Tag,
         #[cfg(windows)] uv_utime: unsafe extern "C" fn(
             *mut uv::Loop,
             *mut uv::fs_t,
             *const core::ffi::c_char,
@@
             return if let Some(errno) = rc.errno() {
                 Err(sys::Error {
                     errno,
-                    syscall: sys::Tag::utime,
+                    syscall,
                     path: args.path.slice().into(),
                     ..Default::default()
                 })
             } else {
                 Ok(())
@@
     pub fn utimes(&mut self, args: &args::Utimes, _: Flavor) -> Maybe<ret::Utimes> {
         #[cfg(windows)]
-        return self.utimes_with(args, uv::uv_fs_utime);
+        return self.utimes_with(args, sys::Tag::utime, uv::uv_fs_utime);
         #[cfg(not(windows))]
-        self.utimes_with(args, Syscall::utimens)
+        self.utimes_with(args, sys::Tag::utime, Syscall::utimens)
     }

     pub fn lutimes(&mut self, args: &args::Lutimes, _: Flavor) -> Maybe<ret::Lutimes> {
         #[cfg(windows)]
-        return self.utimes_with(args, uv::uv_fs_lutime);
+        return self.utimes_with(args, sys::Tag::lutime, uv::uv_fs_lutime);
         #[cfg(not(windows))]
-        self.utimes_with(args, Syscall::lutimens)
+        self.utimes_with(args, sys::Tag::lutime, Syscall::lutimens)
     }

As per coding guidelines: "Node.js compatibility: match Node's full error contract including error code, constructor class, message text, properties, and check ordering."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node/node_fs.rs` around lines 8083 - 8142, The Windows branch in
utimes_with() always sets sys::Tag::utime, so when lutimes() calls utimes_with()
failures report the wrong syscall; update utimes_with() to accept an explicit
sys::Tag (or a boolean flag) and use that tag when constructing the Error (i.e.,
pass sys::Tag::lutime from lutimes() and sys::Tag::utime from utimes()), or
alternatively detect which wrapper called it and set errno.syscall accordingly;
change the signatures/call sites (utimes(), lutimes(), and utimes_with()) to
thread the correct tag through so errors on Windows report sys::Tag::lutime for
lutimes() failures.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/runtime/node/node_fs.rs`:
- Around line 8083-8142: The Windows branch in utimes_with() always sets
sys::Tag::utime, so when lutimes() calls utimes_with() failures report the wrong
syscall; update utimes_with() to accept an explicit sys::Tag (or a boolean flag)
and use that tag when constructing the Error (i.e., pass sys::Tag::lutime from
lutimes() and sys::Tag::utime from utimes()), or alternatively detect which
wrapper called it and set errno.syscall accordingly; change the signatures/call
sites (utimes(), lutimes(), and utimes_with()) to thread the correct tag through
so errors on Windows report sys::Tag::lutime for lutimes() failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef6ee141-6998-469a-861e-af590e172bf8

📥 Commits

Reviewing files that changed from the base of the PR and between 09703da and 68223b1.

📒 Files selected for processing (3)
  • src/runtime/node/node_fs.rs
  • src/runtime/node/path.rs
  • src/runtime/node/types.rs

@robobun

robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

On the CodeRabbit finding that lutimes() "now reports the wrong syscall on Windows": the premise is wrong, this PR does not change the reported syscall. The pre-PR Windows lutimes block already built its error with sys::Tag::utime:

return if let Some(errno) = rc.errno() {
Err(sys::Error {
errno,
syscall: sys::Tag::utime,
path: args.path.slice().into(),
..Default::default()
})

utimes_with keeps that exact tag, so before and after are identical. The POSIX side is also unchanged: Syscall::utimens and Syscall::lutimens both tag Tag::utimensat, so fs.utimesSync and fs.lutimesSync both report syscall: "utimensat" on Linux today, while Node reports "utime" and "lutime".

That divergence from Node's error contract is real but pre-existing and cross-platform, and fixing it here would contradict this PR's zero-behavior-change contract. Tracked in #32047.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/fs/fs-chown-utimes.test.ts`:
- Line 10: The test suite describe blocks for "chown/fchown/lchown argument
validation" (and the other describe at the same file) should be switched to
concurrent execution to speed up tests: replace describe("chown/fchown/lchown
argument validation", ...) with describe.concurrent("chown/fchown/lchown
argument validation", ...) and do the same for the second describe block
referenced in the comment so both describe blocks run tests in parallel safely
(each test already uses tmpdirSync()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1057555e-b5da-443a-a082-55d9fb630623

📥 Commits

Reviewing files that changed from the base of the PR and between 68223b1 and 26e1333.

📒 Files selected for processing (1)
  • test/js/node/fs/fs-chown-utimes.test.ts

Comment thread test/js/node/fs/fs-chown-utimes.test.ts Resolved
@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main and force-pushed (29ece58). Two things landed on main inside the functions this PR consolidates, so the conflict resolution was not purely textual:

  • node:fs: report node's operation name in error.syscall, not the raw syscall #32964 made utimes and lutimes report node's operation name (utime / lutime) in err.syscall on both platforms. The consolidated utimes_with helper now takes that sys::Tag as a parameter and each caller passes its own. Taking the pre-rebase helper shape would have compiled but silently made lutimes report utime again; main's new error.syscall suite in fs.test.ts covers this and passes through the consolidation.
  • node:fs: implement lchown on Windows #32052 implemented fs.lchown on Windows, so I dropped the two interim commits that had skipped lchown's success-path assertions there. The test exercises all three chown variants on every platform again.

After the rebase, every function main changed that this PR does not consolidate (futimes, lchown, chown, fchown, the stat family, readdir, rm, open, link) is byte-identical to main. Verified locally: cargo check, fs-chown-utimes.test.ts, the error.syscall suite, the node path suites including normalize, cp.test.ts, promises.test.js, and the upstream test-path-* / test-fs-{utimes,chown-type-check,copyfile*,lchown} tests all pass on the rebased branch. PR description updated with the same notes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this is a large refactor of production-critical fs/path runtime code (copy_file_range fallback chains, basename/extname/parse loop merges, utimes dispatch) where the correctness argument rests on exact behavioral equivalence — worth a human pass.

Extended reasoning...

Overview

This PR consolidates duplicated argument-parsing and syscall-dispatch code across three Rust runtime files, net −500 lines:

  • node_fs.rs: extracts id_from_js (uid/gid reader, 4 copies → 1), times_from_js (atime/mtime reader, 3 copies → 1), copy_file_range_with_fallbacks (merges the two near-identical Linux copy loops from fs.copyFile and fs.cp, parameterized by an EintrPolicy enum to preserve their one intentional difference), utimes_with (merges utimes/lutimes, parameterized by syscall tag and platform fn pointer), and a node_fs_function_enum! macro that derives both the enum and heap_label() from one list.
  • path.rs: merges posix/win32 basename and extname into const-generic basename_t<T, IS_WINDOWS> / extname_t<T, IS_WINDOWS>, and extracts the shared reverse-scan loop used by extname and parse into scan_last_component_t.
  • types.rs: replaces a 14-arm JSType enumeration with the is_array_buffer_like() predicate.
  • Adds test/js/node/fs/fs-chown-utimes.test.ts pinning argument validation and the utimes/lutimes symlink-follow split.

Security risks

None identified. No auth, crypto, permissions, or untrusted-input parsing surface is changed; the modifications are internal deduplication of already-shipped code paths. Argument validation (uid/gid range, time-like coercion) is moved verbatim into helpers.

Level of scrutiny

High. These files implement Node's fs and path modules in a production JS runtime. The consolidations are mechanical in intent but each carries subtle invariants whose breakage would be user-visible: the EintrPolicy split (one caller retries EINTR, the other surfaces it), the scopeguard's ftruncate/fchmod/close ordering on every exit path of the merged copy helper, the loop-bound equivalence (> -1 vs >= start where start = 0) in the posix basename merge, and the drive-letter prologue now gated on a const-generic flag. I traced each of these and they look correct, and robobun's audit plus full CI corroborate, but the blast radius if any one is off (silent data corruption in copy, wrong path parsing) is large enough that a human should sign off.

Other factors

  • The only reviewer feedback (CodeRabbit's describe.concurrent nit) is addressed and resolved.
  • The PR has been rebased over two intervening changes (#32964, #32052) with conflict resolution described in detail; that resolution itself is part of what a human should verify.
  • CI was green on all executed lanes per robobun; the only red was infra (queue-expired darwin shards).
  • No CODEOWNERS check performed, but core runtime fs code typically warrants maintainer review regardless.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

For the human pass: two mechanical checks back the equivalence claim after the rebase, in addition to the per-function audit in the comments above.

  • Revert check: intersecting the lines main added to these three files since the old base with the lines this branch deletes relative to main leaves only the utimes/lutimes lines that moved into utimes_with, which is now parameterized by sys::Tag. Nothing else main landed in these files is removed or rewritten.
  • Identity check: every function main modified that this PR does not consolidate (futimes, lchown, chown, fchown, stat, lstat, fstat, readdir, rm, open, link) is byte-identical to origin/main on this branch.

The part of the rebase that is hardest to eyeball, keeping err.syscall as utime for utimes and lutime for lutimes through the shared helper, is exercised by the error.syscall suite that #32964 added to fs.test.ts. That suite runs through the consolidated code on this branch and fails if the helper collapses the two names.

@alii

alii commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

@robobun are there certainly no behavioural changes here?

@robobun

robobun commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

I found none, and none are intended, with one caveat on the word "certainly": the claim rests on a line-by-line equivalence audit plus the existing test surface, not an exhaustive proof. What I checked at each place where behavior could have moved:

  • path.basename / extname / parse: the posix and win32 bodies differed only in the separator set and the win32 drive-letter prologue. The merged basename_t / extname_t / scan_last_component_t gate the prologue on the const generic and keep the exact loop bounds (i >= start with start = 0 is the old posix i > -1; posix parse still passes start_part = 0 with the scan bounded at start). The upstream node tables in basename/extname/parse-format, which cover the drive-letter and suffix edge cases, pass unchanged.
  • chown/fchown/lchown and utimes/futimes/lutimes argument parsing: id_from_js and times_from_js are verbatim extractions, and the per-argument error naming (uid vs gid, atime vs mtime) is asserted for every sibling by test/js/node/fs/fs-chown-utimes.test.ts.
  • fs.copyFile vs fs.cp on Linux: the two old loops differed only in EINTR handling (copyFile retried, cp surfaced it), which is now the explicit EintrPolicy. The errno fallback set, the conditions that disable copy_file_range, the size 0 until-EOF loop, and the ftruncate + fchmod + close scopeguard are unchanged.
  • utimes vs lutimes: after the rebase over node:fs: report node's operation name in error.syscall, not the raw syscall #32964, the shared helper takes the node operation name (utime / lutime) as a parameter, so err.syscall matches current main exactly. The error.syscall suite that node:fs: report node's operation name in error.syscall, not the raw syscall #32964 added runs through the consolidated code on this branch and fails if the two collapse.
  • StringOrBuffer: is_array_buffer_like() is exactly the 14 JSType variants the deleted match listed, and the enum macro emits the same variants and Async<Name>Task labels as the hand-written pair it replaces.

Two mechanical checks on top of the reading: every function main has modified that this PR does not consolidate (futimes, lchown, chown, fchown, the stat family, readdir, rm, open, link) is byte-identical to main on this branch, and intersecting the lines main added to these files with the lines this branch deletes leaves only the utimes/lutimes lines that moved into the parameterized helper.

Empirically, the node path suites, fs.test.ts (including the syscall-name suite), cp.test.ts, promises.test.js, and the ported upstream node tests pass on this branch on every CI platform. If you want me to walk through any specific hunk, say which.

@robobun
robobun force-pushed the claude/split/node-compat branch from 29ece58 to 417c6e9 Compare July 10, 2026 11:08
@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main and force-pushed (417c6e9).

The only conflict was a placement collision: main's #33844 added encode_path_result (for mkdtemp/readlink/realpath encoding) at the same spot this PR adds the EintrPolicy enum. The two are unrelated; resolution keeps both, one after the other.

The refactor content itself did not change this round. None of main's 8 commits to these files since the previous rebase touched any of the consolidated regions (utimes/lutimes, the copy loops, the arg readers, the enum macro, basename/extname/parse, the is_array_buffer_like match). Every function main modified is byte-identical to main on this branch, and the PR delta vs main is exactly the same 343+/711- lines as before. Verified locally: fs-chown-utimes.test.ts, the error.syscall suite, the mkdtemp-encoding suite from #33844, the node path suites, cp.test.ts, promises.test.js, the upstream test-path-* and test-fs-* tests, and the 14-type buffer-like probe all pass on the rebased branch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found in automated review, but this is a ~700-line consolidation touching fd-ownership/scopeguard ordering in the Linux copy path and const-generic merges of the path.rs reverse-scan loops — worth a human pass on the equivalence claims before merge.

Extended reasoning...

Overview

Consolidates duplicated code across three node-compat runtime files: node_fs.rs (shared id_from_js/times_from_js arg readers, merged utimes_with parameterized by sys::Tag + function pointer, unified copy_file_range_with_fallbacks with an EintrPolicy enum, macro-generated NodeFSFunctionEnum/heap_label), path.rs (const-generic basename_t/extname_t/scan_last_component_t replacing separate posix/win32 bodies), and types.rs (is_array_buffer_like() replacing a 14-variant match). Net ~−370 lines. Adds test/js/node/fs/fs-chown-utimes.test.ts pinning the shared arg-validation surface and utimes/lutimes symlink dispatch.

Security risks

None identified. No new external-input parsing, no auth/crypto/permissions surface. The change is internal deduplication of existing validation and syscall-dispatch code; error paths and errno sets are preserved per the diff.

Level of scrutiny

High. This is production-critical node:fs and node:path runtime code, and the copy_file_range_with_fallbacks extraction moves fd ownership + the ftruncate/fchmod/close scopeguard into a helper — exactly the acquisition/release-pairing and single-owner class CLAUDE.md flags as most-blocked. The path.rs merge relies on loop-bound equivalences (i >= start with start=0 ≡ i > -1; posix parse passing initial_start_part=0 while bounding at start) that are subtle enough to warrant a human trace even though robobun's audit and the upstream node test tables cover them.

Other factors

  • The bug-hunting system found no issues.
  • robobun has posted a detailed per-hunk behavioral-equivalence audit, two mechanical revert/identity checks against main, and confirmed the err.syscall naming from #32964 is preserved via the parameterized helper (covered by that PR's test suite).
  • alii is already engaged and asked directly about behavior changes; robobun answered thoroughly but no human approval has followed.
  • The PR is explicitly zero-behavior-change by design, so its own tests pass before and after; correctness rests on the audit + existing suite rather than a fail-before test.
  • Given scope, the fd-lifecycle move, and an already-engaged human reviewer, this exceeds the auto-approve bar of simple/mechanical/obvious.

@robobun

robobun commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Those two areas are the ones I'd point a human reviewer to as well. On the fd-ownership/scopeguard point specifically, the concrete thing to check is placement: in both original call sites the (dest_fd, st_mode, &wrote) scopeguard was armed after dest_fd was opened and after the ioctl_ficlone fast path (which closes dest_fd itself) had returned. The extracted copy_file_range_with_fallbacks receives dest_fd from the caller after that fast path and arms the same scopeguard as its first statement, so the set of exit paths it covers (the early !can_use_copy_file_range return, every errno return inside the loop, and the Ok(()) fall-through) is identical to the set each inline block covered. The ftruncate + fchmod + close body and the Cell<u64> accounting are lifted verbatim.

The path.rs loop bounds and start_part seeds are spelled out in my earlier reply to @alii above; the upstream node tables (which cover drive-letter, suffix, and trailing-separator edges for both platforms) pass through the merged code.

@alii

alii commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@robobun get this mergeable — or close it if the changes are already covered on main / too stale to salvage.

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

On it. Main has moved ~830 commits since the last rebase (28 of them touch these three files), so this is a fresh rebase plus re-verification. First pass: every consolidation target is still duplicated on main except heap_label, which main deleted as dead code, so that hunk gets dropped. Will report back once it is rebased and the suites pass.

@robobun
robobun force-pushed the claude/split/node-compat branch from 417c6e9 to 05d6938 Compare August 10, 2026 22:28
@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

@alii rebased onto current main and force-pushed (05d6938); the PR is conflict-free again and still worth landing rather than closing: every duplicated block it removes is still duplicated on main (the uid/gid and atime/mtime readers, both Linux copy loops, utimes/lutimes, posix/win32 basename and extname, the parse scan loops, the 14-arm buffer match). The one exception is heap_label, which main has since deleted as dead code, so the hunk that derived the enum and that label from a macro is dropped; the enum is now byte-identical to main. Net delta is now 322 added / 659 removed in src.

What the rebase had to absorb, all mechanical: main narrowed visibility across these files, so the consolidated wrappers take each replaced function's current visibility; main switched ERR_OUT_OF_RANGE to node's && wording, so the test tracks that. Nothing in the consolidated bodies changed. I re-ran the equivalence checks against current main: the only main-added lines this branch removes are the signature lines of functions it rewrites, every main-modified function it does not touch is byte-identical, and at the two copy sites everything before the removed loop block is identical to main with the removed block containing the EINTR retry only at the copyFile site (the Retry / Surface split). The FreeBSD copy paths main added since are separate cfg blocks the helper does not touch.

Verified locally on the rebased branch: full debug build, clippy clean, the new source-lint suite (75 tests, including the byte-search deny list), cargo check for windows-msvc, apple-darwin, and freebsd, fs-chown-utimes.test.ts, the error.syscall / copyFile / utimes / chown / mkdtemp suites in fs.test.ts, the whole test/js/node/path directory, cp and promises suites, and all 102 upstream node path-* and fs utimes/chown/copyfile/cp tests (including the ones #34505 added). CI is running on the new head now.

One thing I cannot make green: the robobun/evidence check wants the added test to fail on main, which is impossible for a change whose contract is zero behavior change; any test that failed on main would mean the refactor is wrong. Everything else should go green on its own, so this needs a review and merge past that check.

Comment thread test/js/node/fs/fs-chown-utimes.test.ts Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR at 805e601 and the bug-hunting pass found no issues; the earlier tmpdirSync nit is addressed. Because it's a ~340-line net refactor across node_fs.rs (fd-owning copy loop, utimes dispatch), path.rs (basename/extname/parse scan), and types.rs, a human sign-off is still worthwhile.

What was checked:

  • is_array_buffer_like() at src/jsc/JSType.rs:668 is exactly the 14 variants the removed match arm listed.
  • errno_sys_p takes impl AsRef<[u8]>, so the helper's dest.as_bytes() is equivalent to copy_file_inner's bare dest: &ZStr.
  • scan_last_component_t loop bound i_i64 >= lower_bound with lower_bound = 0 matches the removed posix i_i64 > -1; parse_posix_t still seeds start_part = 0 while bounding at start, and parse_windows_t seeds both at root_end.
  • EintrPolicy::Retry vs Surface reproduces the only difference between the two removed loops; the scopeguard body (ftruncate/fchmod/close) and Cell<u64> accounting are lifted verbatim.
Extended reasoning...

Overview

This PR consolidates duplicated code across three src/runtime/node/ files: node_fs.rs (uid/gid and atime/mtime argument readers, the two Linux copy_file_range loops with an EintrPolicy parameter, and a shared utimes_with for utimes/lutimes), path.rs (const-generic basename_t/extname_t/scan_last_component_t merging the posix/win32 twins), and types.rs (replacing a 14-arm JSType match with is_array_buffer_like()). It adds test/js/node/fs/fs-chown-utimes.test.ts pinning the shared readers and the utimes/lutimes symlink dispatch. Net -337 lines in src.

Security risks

None identified. No new input surface, no auth/crypto/permission code, no untrusted parsing added. The refactor moves existing validation into shared helpers without changing bounds or coercion order.

Level of scrutiny

High. This touches production-critical hot paths (fs.copyFile, fs.cp, path.basename/extname/parse) with fd-ownership transfer into an extracted helper, cfg-gated Windows/POSIX branches, and loop-bound arithmetic that must stay bit-identical to the Node reference. The PR's own contract is zero behavior change, so any subtle divergence is a regression. That warrants a maintainer's eyes rather than bot approval.

Other factors

  • I spot-checked the equivalences most likely to drift: is_array_buffer_like() covers exactly the 14 removed JSType arms; errno_sys_p's impl AsRef<[u8]> parameter makes the dest vs dest.as_bytes() change a no-op; the const-generic loop bounds (>= 0 vs > -1, start_part seeding for posix vs win32 parse) match the removed bodies; the extracted scopeguard is armed at the same point relative to the ficlone fast path as both originals.
  • The tmpdirSync nit from my previous run was addressed in 805e601 (test now uses using dir = tempDir(...)).
  • A maintainer (alii) is already engaged and asked to get the PR mergeable; robobun has done a detailed line-by-line equivalence audit in-thread and noted the robobun/evidence check cannot go green for a zero-behavior-change refactor. Given the scope and the fd-ownership/scopeguard extraction, deferring to that human review rather than auto-approving.

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Status on the final head (805e601): CI is green on every lane that has finished (45 checks, one Windows aarch64 test shard still running, no test failures), the review threads are closed, and the automated review pass at this head independently checked the same equivalences (the 14-variant match, the errno_sys_p argument, the scan loop bounds and start_part seeds, the EINTR split and scopeguard) and found nothing. The only red check is robobun/evidence, which requires the added test to fail on main and so cannot pass for a zero-behavior-change refactor. From my side this is ready to merge; it just needs your approval past that check.

@alii

alii commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@robobun retry ci — darwin lanes expired (runner timeout)

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Retriggered: pushed an empty commit (1efebaa), new build is https://buildkite.com/bun/bun/builds/91724. The previous build (91669) ended with 190 passed and no test failures; the only incomplete jobs were the six darwin shards whose retries never got an agent, and the push canceled those. My Buildkite access is read-only, so a full rebuild is the only retry I can do; if the darwin pool is still starved, retrying just those shards from the Buildkite UI will be quicker than another push from me.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the current head and found no bugs; the earlier tmpdirSync nit is addressed. Given the size (~660 lines removed across the Linux copy loop, cfg-gated utimes dispatch, and posix/win32 path scan merging) a human sign-off is still warranted — which this PR needs anyway to merge past robobun/evidence.

What was reviewed:

  • copy_file_range_with_fallbacks: scopeguard is armed at the same point both inline blocks armed it (after the ficlone fast path, before any fallible copy call); the EintrPolicy::Retry/Surface split matches the old copyFile/cp EINTR arms; the errno_sys_p dest.as_bytes() and fchmod(fd, m) normalizations are type-level only.
  • scan_last_component_t seeds: (lower_bound, initial_start_part) = (start, 0) for posix parse, (root_end, root_end) for win32 parse, (start, start) for extname — each matches the removed loop's bound and start_part init; posix's old i_i64 > -1 is >= 0 with start = 0.
  • utimes_with: each caller passes its own sys::Tag (utime/lutime) and syscall fn, so #32964's err.syscall naming is preserved on both platforms.
  • t.is_array_buffer_like() replaces the 14-arm JSType match — checked against the helper's variant set.
Extended reasoning...

Overview

This PR consolidates duplicated code across three src/runtime/node/ files: node_fs.rs (uid/gid and atime/mtime argument readers shared across chown/fchown/lchown and utimes/futimes/lutimes; a single copy_file_range_with_fallbacks helper replacing two ~100-line inline Linux copy loops in fs.copyFile and fs.cp; a utimes_with helper parameterized by syscall tag and function pointer), path.rs (const-generic basename_t/extname_t/scan_last_component_t replacing separate posix/win32 copies), and types.rs (a 14-arm JSType match collapsed to is_array_buffer_like()). A new test file pins the shared argument-reader error naming and the utimes/lutimes symlink dispatch. Net -337 lines in src.

Security risks

None identified. The change is a mechanical dedupe of existing argument validation and syscall wrappers; no new input surface, no auth/crypto/permissions code, and the validation ranges ([-1, u32::MAX] for uid/gid, the timestamp coercion) are lifted verbatim.

Level of scrutiny

High. The consolidated regions include fd-ownership and scopeguard placement in the Linux copy path (where a misplaced guard is a leak or double-close), cfg-gated Windows/POSIX dispatch via function pointers, and index arithmetic in the path scanner where an off-by-one in lower_bound/initial_start_part would silently change path.parse/extname output. The PR's own contract is zero behavior change, so correctness rests entirely on line-by-line equivalence — exactly the kind of review a human should confirm rather than an automated approval.

Other factors

  • The PR thread already contains a detailed per-hunk equivalence audit and two mechanical checks (main-added-lines ∩ branch-deleted-lines; byte-identity of untouched functions). CI is green across platforms on the rebased head; the one red check (robobun/evidence) is structurally impossible to satisfy for a zero-behavior-change refactor and requires human override regardless.
  • A maintainer (alii) is actively engaged — asked for behavioral-change confirmation, requested the rebase, and is managing CI retries — so a human review is already in progress.
  • My prior inline nit (tmpdirSync → tempDir) was addressed in 805e601; the only commit since is a CI retrigger. This is my first review-body post on the PR.

@robobun
robobun force-pushed the claude/split/node-compat branch from 1efebaa to 04908a0 Compare August 11, 2026 02:12
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

The darwin expiry was not a transient runner timeout, so the retrigger could not have cleared it: the bare-metal mac fleet is offline for reimaging, and main rerouted darwin tests to a hosted queue (and stopped scheduling them on PR pushes) in #37354, #37364 and #37369, all of which landed after my last rebase. This branch was still generating pipeline steps that target the offline fleet, so every darwin shard expired no matter how many times it was retried.

Rebased onto main again (04908a0, no conflicts, src delta byte-identical to before, the empty retrigger commit dropped). The new build is https://buildkite.com/bun/bun/builds/91839 and it schedules no darwin test shards, same as current main builds, so it should be able to finish. The one failure on the previous build was the mariadb Docker image failing to build on an alpine runner, which I reported separately; it is unrelated to this PR.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the automated pass found no bugs. Given the scope — ~660 lines removed across the Linux copy_file_range fd/scopeguard path, the cfg-gated utimes_with dispatch, and the shared path.{basename,extname,parse} scan loops — a human look before merge is still worthwhile.

What was checked and held up:

  • is_array_buffer_like() is exactly the 14 JSType variants the deleted match arm listed.
  • scan_last_component_t loop bounds/seeds match each original: posix > -1 ≡ >= 0 with start = 0; parse_posix_t passes initial_start_part = 0 (not start); parse_windows_t/extname_windows_t pass root_end/start for both.
  • copy_file_range_with_fallbacks: scopeguard armed at the same point (post-ficlone) at both call sites; errno_sys_p takes impl AsRef<[u8]> so dest vs dest.as_bytes() is identical; Mode is u32 so the fchmod cast is unchanged; the EintrPolicy::Retry/Surface split matches the two original loops.
  • utimes_with preserves the per-caller sys::Tag (utime/lutime) from #32964.
Extended reasoning...

Overview

This PR consolidates duplicated argument-parsing and syscall-loop code in src/runtime/node/node_fs.rs (uid/gid readers, atime/mtime readers, the two Linux copy_file_range loops for fs.copyFile and fs.cp, and the utimes/lutimes bodies), merges the posix/win32 twins of basename/extname and the parse scan loop in src/runtime/node/path.rs behind const-generic helpers, and replaces a 14-arm JSType match in src/runtime/node/types.rs with the existing is_array_buffer_like() predicate. Net -337 lines in src. A new test file pins the shared uid/gid and atime/mtime validation and the utimes/lutimes symlink dispatch.

Security risks

None identified. The change is a mechanical extraction of existing control flow into shared helpers; no new input-validation surface, no new unsafe blocks (the one unsafe in the copy helper is the same libc copy_file_range FFI call that was there before), and no changes to path-traversal or permission checks.

Level of scrutiny

High. This touches production-critical Node.js compat code: fs.copyFile/fs.cp on Linux involves fd ownership across a scopeguard (ftruncate/fchmod/close on every exit path), and node:path parsing is called on every module resolution. The refactor merges four near-identical loops whose only differences (separator predicate, drive-letter prologue, EINTR policy, initial start_part) are now parameters — each of those axes needs to be verified against the original per call site. The PR author documented a line-by-line equivalence audit and I independently spot-checked the specific points listed in the message above; all held. But the diff is large enough and the fd-ownership placement subtle enough that a maintainer eyeball is warranted, and alii is already engaged and asked for it to be made mergeable.

Other factors

The PR has been rebased three times over ~830 main commits with non-trivial conflict resolution (notably absorbing #32964's per-operation sys::Tag into utimes_with). CI is green across all platforms per the thread; the only red check is robobun/evidence, which is expected to fail for a zero-behavior-change refactor. My prior inline nit (tmpdirSync → tempDir) was addressed in 04908a0. The added test file follows harness conventions (using tempDir, describe.concurrent, fd closed in finally).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this at the current head and found no bugs. Given it's a -337-line consolidation across node_fs.rs (the Linux copy_file_range loop with fd-owning scopeguard, utimes_with) and path.rs (const-generic scan loops), a human sign-off is still worthwhile before merging past the evidence check.

What was checked: the EintrPolicy::Retry/Surface split matches the two original loops' EINTR arms; the scopeguard is armed at the same point (after ficlone, owning dest_fd) with the same ftruncate/fchmod/close body; utimes_with preserves the per-caller sys::Tag so err.syscall stays utime/lutime; scan_last_component_t loop bounds (i >= lower_bound with lower_bound = 0 ≡ the old posix i > -1) and start_part seeds match all four original call sites; is_array_buffer_like() covers exactly the 14 removed JSType arms.

Extended reasoning...

Overview

The PR consolidates duplicated code across three Node compat files: node_fs.rs (uid/gid and atime/mtime argument readers shared by chown/fchown/lchown and utimes/futimes/lutimes; the two near-identical Linux copy_file_range loops merged into copy_file_range_with_fallbacks parameterized by EintrPolicy; utimes/lutimes merged into utimes_with taking the syscall tag and a function pointer), path.rs (posix/win32 basename/extname/parse scan loops merged behind a const IS_WINDOWS: bool generic and a LastComponentScan helper), and types.rs (a 14-arm JSType match replaced with is_array_buffer_like()). Net 322+/659- in src plus a new test file pinning the shared readers' per-argument error naming and the utimes/lutimes symlink dispatch.

Security risks

None identified. No new user-input parsing surface; the argument validators are verbatim extractions with the same bounds ([-1, u32::MAX] for uid/gid). No auth, crypto, or path-traversal changes.

Level of scrutiny

High. node:fs and node:path are among the most-exercised Node compat modules, and the copy-loop consolidation touches fd ownership and scopeguard placement — the review guidelines flag this class ("Pair every acquisition with its release at the acquisition site", "Treat every refactor as guilty until proven behavior-preserving"). The path scan-loop merge changes loop-bound spellings (i > -1 → i >= 0) and threads start_part through a struct, which is exactly where off-by-ones hide. The PR author has done a thorough per-hunk equivalence audit and the bug hunter independently checked the same points without findings, but a refactor of this scope in these files should have a human's name on the approval.

Other factors

The maintainer has been actively shepherding this PR (three rebase rounds, explicitly asked whether there are behavioral changes, and asked for it to be made mergeable). The robobun/evidence check is red by construction for a zero-behavior-change refactor, so merging requires a human override regardless. My only prior comment (the tmpdirSync → tempDir nit) was addressed. The head has moved since the last recorded automated review pass (merge c15d0b5), so this run's zero-findings result is new information for the current head.

@robobun
robobun force-pushed the claude/split/node-compat branch from c15d0b5 to fc027ff Compare August 16, 2026 19:27

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the automated bug-hunting pass found no issues. Given the scope — a ~1000-line refactor across the fs.copyFile/fs.cp Linux copy loops, the utimes/lutimes dispatch, and the path.basename/extname/parse scan loops — a human sign-off is still worthwhile before merging past the evidence check.

What was reviewed: is_array_buffer_like() matches exactly the 14 JSType variants the deleted arm listed; errno_sys_p takes impl AsRef<[u8]> so the dest vs dest.as_bytes() unification is a no-op; the merged scan_last_component_t loop bound >= lower_bound with lower_bound = 0 is the old posix > -1, and each caller's initial_start_part seed matches the value the removed loop initialized (0 for posix parse, root_end for win32 parse, start for extname); id_from_js/time_arg_from_js preserve the exact next → eat ordering and error text of the blocks they replace; the copy_file_range_with_fallbacks scopeguard is armed at the same point relative to dest_fd as both original inline blocks (after the ficlone fast path), and EintrPolicy::Retry/Surface reproduces the one behavioral difference between the two removed loops. The earlier tmpdirSync nit is resolved.

Extended reasoning...

Overview

This PR consolidates duplicated argument-parsing and syscall-dispatch code across three files in the Node.js compatibility layer: src/runtime/node/node_fs.rs (uid/gid and atime/mtime readers, the Linux copy_file_range loop shared by fs.copyFile and fs.cp, and the utimes/lutimes bodies), src/runtime/node/path.rs (posix/win32 basename, extname, and the reverse-scan loop inside parse merged behind a const IS_WINDOWS generic), and src/runtime/node/types.rs (a 14-arm JSType match replaced by is_array_buffer_like()). Net −337 lines in src/. A new test file pins the argument-validation surface and the utimes-vs-lutimes symlink dispatch. The PR has been rebased four times over two months with detailed equivalence audits at each step.

Security risks

None identified. The changes are pure extractions of existing control flow into shared helpers; no new input is accepted, no validation is loosened, and no security-relevant path (auth, TLS, path traversal) is touched. The one memory-safety-adjacent piece — the dest_fd scopeguard in the copy helper — is armed at the same point as both originals and the ftruncate/fchmod/close body is lifted verbatim.

Level of scrutiny

High. fs.copyFile, fs.cp, path.basename, path.extname, and path.parse are all high-traffic Node-compat APIs where an off-by-one in a loop bound or a dropped early-return would be a user-visible regression. The individual consolidations are each mechanically verifiable (and I did verify the loop bounds, start_part seeds, EINTR split, and is_array_buffer_like variant set against the removed code), but the aggregate diff is large enough and the fd-ownership/scopeguard reasoning subtle enough that it exceeds the "simple, mechanical, or obvious" bar for automated approval.

Other factors

The maintainer has been actively shepherding this PR ("get this mergeable") and robobun has stated it needs a human approval past the robobun/evidence check, which cannot pass by construction for a zero-behavior-change refactor. My earlier inline nit (tmpdirSync → tempDir) was addressed in 805e601 and the test file now uses disposable temp directories. The bug-hunting system raised and refuted one candidate (new test file vs. adding to fs.test.ts); the separate file is reasonable given it targets a specific consolidated surface. CI was green on the last completed build modulo darwin runner-pool issues unrelated to this change.

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main again and force-pushed (fc027ff). The branch is back to two commits on top of main: the refactor (still authored by @alii) and the test file.

One conflict this round, in types.rs: #39164 replaced the StringOrBuffer parser's bool flags with enums, which rewrote the body of the same match arm whose 14-variant pattern this PR collapses to t if t.is_array_buffer_like(). The two changes are orthogonal, so the resolution keeps main's new body (flavor == Flavor::Async) under this PR's pattern; is_array_buffer_like() on current main is still exactly those 14 variants. node_fs.rs and path.rs merged automatically, and the resulting delta against main is line-for-line identical to the previously audited one (same 322+/659-), with none of main's intervening changes to these files removed.

Verified on the rebased tree: full debug build, fs-chown-utimes.test.ts, the whole test/js/node/path directory, the error.syscall / copyFile / utimes / chown / mkdtemp suites in fs.test.ts, and cp.test.ts plus promises.test.js four times over (one earlier run hit a failure while the build host was recovering from a full disk; it did not reproduce in four clean runs). CI is running as build 99465.

@robobun
robobun force-pushed the claude/split/node-compat branch from fc027ff to 303001d Compare August 17, 2026 21:21
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main again (303001d). The only conflict was positional: #39448 deleted unwatch_file, which was the function directly above this PR's utimes_with, so git could not place the insertion; the resolution keeps it deleted and adds utimes_with in the same spot. path.rs and types.rs merged automatically. The delta against main is line-for-line the same as before (322+/659- in src, no main-side changes removed). Verified on the rebased tree: debug build, fs-chown-utimes.test.ts, the test/js/node/path directory, the error.syscall / copyFile / utimes / chown / mkdtemp suites, and cp.test.ts plus promises.test.js, all passing; rustfmt clean. CI is build 100175.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR at the current head and found no bugs. Given it's a ~340-line net refactor of the Linux copy_file_range loop (fd ownership + scopeguard placement), the node:path reverse-scan loops, and utimes/lutimes dispatch, a maintainer sign-off is still worthwhile.

What was checked:

  • is_array_buffer_like() is exactly the 14 JSType variants the deleted match listed; errno_sys_p takes impl AsRef<[u8]> so dest vs dest.as_bytes() is equivalent.
  • EintrPolicy::Surface at the fs.cp site falls through to _ => return err for EINTR, matching the old cp loop that had no EINTR arm; the scopeguard is armed at the same point (post-ficlone) at both callers.
  • scan_last_component_t seeds (lower_bound, initial_start_part) match each of the four original loops: (0,0) / (start,start) / (start,0) / (root_end,root_end); posix's i_i64 > -1 ≡ i_i64 >= 0.
  • utimes_with threads sys::Tag::utime / lutime per caller, preserving #32964's err.syscall split.
Extended reasoning...

Overview

This PR consolidates duplicated code in three node:* runtime files: node_fs.rs (uid/gid and atime/mtime argument readers, the two Linux copy_file_range loops merged behind an EintrPolicy enum, and a shared utimes_with body for utimes/lutimes), path.rs (const-generic basename_t/extname_t and a shared scan_last_component_t reverse-scan used by four call sites), and types.rs (14-arm JSType match → is_array_buffer_like()). It adds a test file pinning the consolidated argument-validation surface and the utimes/lutimes symlink dispatch. Net -337 lines in src/.

Security risks

None identified. The change is a behavior-preserving extraction; no new inputs are accepted, no validation is loosened, and no security-sensitive code (auth, crypto, path traversal checks) is touched. The path.rs changes are to basename/extname/parse, which are pure string operations already covered by upstream Node test tables.

Level of scrutiny

High. The Linux copy loop consolidation moves fd-ownership and a scopeguard (ftruncate/fchmod/close) into a helper — getting the arm point wrong would leak or double-close an fd. The path.rs merge collapses four hand-written reverse-scan loops with subtly different bounds (> -1 vs >= start) and start_part seeds into one parameterized function; an off-by-one here would silently change path.parse/extname output for edge cases. The utimes_with consolidation must preserve the per-operation err.syscall name that #32964 introduced. All of these were spot-checked against the deleted code and hold, but the density of load-bearing detail is exactly what the repo's REVIEW.md flags for "treat every refactor as guilty until proven behavior-preserving."

Other factors

The PR has been through five rebases with documented per-rebase equivalence checks, the maintainer is already engaged and driving toward merge, and the existing error.syscall suite plus 102 upstream Node path/fs tests exercise the consolidated code. My earlier tmpdirSync nit was addressed. No outstanding review threads remain. The one candidate issue raised this run (new test file vs adding to fs.test.ts) was ruled out — the file pins a specific consolidation surface and follows the same pattern as other sibling-validation test files in test/js/node/fs/. I'm deferring rather than approving because the scope and subtlety fall outside "simple, mechanical, or obvious."

@robobun
robobun force-pushed the claude/split/node-compat branch from 303001d to 39c8135 Compare August 25, 2026 02:43
@robobun

robobun commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main again (39c8135). One conflict, in the fs args module: #40374 gave the argument structs borrowed lifetimes (Chown<'a>, Lutimes<'a>, PathLike<'a>) on the exact lines where this PR inserts the shared id_from_js / times_from_js readers. Resolution keeps main's lifetime annotations and this PR's helpers; the helpers take no lifetime because they return plain integers and TimeLike. path.rs and types.rs merged automatically, and after normalizing lifetime syntax the delta against main is line-for-line the same as before (322+/659-), with none of main's intervening changes removed (the new readdir readdir_skips_subdir early return is retained verbatim).

Verified on the rebased tree: debug build, rustfmt, fs-chown-utimes.test.ts, the test/js/node/path directory, the error.syscall / copyFile / utimes / chown / mkdtemp / readdir suites in fs.test.ts, and cp.test.ts plus promises.test.js. One cp test (fs.promises.cp recursive does not free parent task while subtasks are in flight after an error) timed out in 2 of 5 full-file runs on the loaded build host; isolated it runs in about 3s against a 5s budget on both clean main (6/6, 2.8-3.0s) and this branch (6/6, 2.9-3.6s), so that is load, not a behavior change. CI is build 105263.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR at the current head and didn't find any bugs. Because it's a ~1000-line refactor of the Linux copy_file_range loop (fd ownership + scopeguard), the merged posix/win32 path scanners, and the utimes/lutimes dispatch, a maintainer sign-off is still the right call — especially since it needs to land past the robobun/evidence check.

What was reviewed:

  • is_array_buffer_like() is exactly the 14 JSType variants the removed match arm listed.
  • EintrPolicy::Surface reproduces the old fs.cp loop (no EINTR arm → falls through to the error return); Retry reproduces fs.copyFile's E::EINTR => continue.
  • The scopeguard in copy_file_range_with_fallbacks is armed at the same point relative to the ioctl_ficlone fast path as both original inline blocks.
  • scan_last_component_t loop bounds and start_part seeds match each of the four original call sites (posix i_i64 > -1 ≡ i_i64 >= 0; win32 >= start/>= root_end preserved).
Extended reasoning...

Overview

This PR consolidates duplicated code in src/runtime/node/node_fs.rs, path.rs, and types.rs with zero intended behavior change: extracts id_from_js / times_from_js for the six chown/utimes argument readers, extracts copy_file_range_with_fallbacks with an EintrPolicy enum to unify the two Linux copy loops, extracts utimes_with parameterized by sys::Tag and a per-platform function pointer, merges posix/win32 basename/extname via const-generic is_sep_t, extracts scan_last_component_t for the four reverse-scan loops in extname/parse, and replaces a 14-arm JSType match with is_array_buffer_like(). Net −337 lines in src. A new test file pins the shared uid/gid and atime/mtime validation and the utimes/lutimes symlink dispatch.

Security risks

None identified. The change touches file-descriptor ownership (the dest_fd scopeguard) but does not alter it: the guard is armed at the same relative position and its body (ftruncate + fchmod + close) is lifted verbatim. No new user-controlled input paths, no changes to validation ranges or error-code mapping.

Level of scrutiny

High. This is a large refactor of production-critical filesystem code — the Linux copy_file_range loop with its EINTR/fallback/scopeguard handling is exactly the class of code REVIEW.md flags as most-blocked ("pair every acquisition with its release at the acquisition site"; "treat every refactor as guilty until proven behavior-preserving"). The path.rs consolidation touches the core of basename/extname/parse for both platforms. While the per-hunk equivalence checks all hold, this is not the kind of mechanical change an automated reviewer should approve unilaterally.

Other factors

The PR has been extensively self-audited across five rebases with detailed mechanical checks (revert-set intersection, byte-identity of untouched functions, per-target cargo check). A maintainer has been actively shepherding it and asked for it to be made mergeable; robobun reports it is ready and waiting for approval past the robobun/evidence check (which structurally cannot pass for a zero-behavior-change refactor). My prior inline nit (tmpdirSync → tempDir) was addressed in 805e601. The bug-hunting system found no issues at the current head. Given all that, this is ready for a human to merge — but the decision to land a refactor of this scope past a red required check should be a maintainer's, not mine.

@robobun
robobun force-pushed the claude/split/node-compat branch from 39c8135 to 9fb19a3 Compare August 27, 2026 01:42
@robobun

robobun commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main again (9fb19a3). One conflict, in types.rs, same shape as the #39164 round: #40511 rewrote the body of the StringOrBuffer buffer arm (it now delegates to Self::buffer_from_js(global, value, flavor)? so async Buffer paths no longer stay rooted forever), while this PR only changes that arm's pattern. Resolution keeps main's new body under the t if t.is_array_buffer_like() guard; the helper on current main is still exactly the 14 variants the old pattern listed. node_fs.rs and path.rs merged automatically, and the delta against main is line-for-line the same as before (322+/659-), with main's intervening changes (the truncate undefined length handling, the Buffer-path rooting change, the lifetime annotations) all retained.

Verified on the rebased tree: debug build, rustfmt, fs-chown-utimes.test.ts, the test/js/node/path directory, the error.syscall / copyFile / utimes / chown / mkdtemp / readdir / truncate suites in fs.test.ts, cp.test.ts plus promises.test.js, and the upstream test-fs-{utimes,copyfile,lchown,chown-type-check} and test-path-{parse-format,basename,extname} files. CI is build 106527.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun
robobun force-pushed the claude/split/node-compat branch from 9fb19a3 to 8c930a0 Compare September 8, 2026 00:54

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread test/js/node/fs/fs-chown-utimes.test.ts Resolved
alii and others added 2 commits September 8, 2026 01:26
Pins the per-argument error naming shared by chown/fchown/lchown and by
utimes/futimes/lutimes, and the follow/nofollow split between utimesSync
and lutimesSync.
@robobun
robobun force-pushed the claude/split/node-compat branch from 8c930a0 to 64e262a Compare September 8, 2026 01:28

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

.toThrow(err) compares only the message, so each validation case now also
asserts the constructor and err.code via toThrowWithCode.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main again; head is now c1e08db.

This round had the first conflict that touches the consolidated code itself rather than its surroundings: #40860 replaced libuv ReturnCode::errno() plus hand-built sys::Error with rc.to_error(tag) (which also maps unmapped Win32 codes to EUNKNOWN), and rewrote the Windows arms of utimes and lutimes accordingly. The shared utimes_with now uses that same form, rc.to_error(syscall) followed by err.with_path(...), with each caller still passing its own sys::Tag, so the Windows error path is identical to what main's two functions do today. cargo check --target x86_64-pc-windows-msvc passes on the rebased tree (that arm is cfg(windows), so the Linux build alone would not have type-checked it). The second conflict was the lutimes Windows block that this PR replaces with the one-line delegation. path.rs and types.rs merged automatically, and apart from those Windows lines the delta against main is the same as before; the src delta is now 323+/653- because main's new Windows blocks are shorter than the ones they replaced.

Also in this push, per the review comment above: the test now asserts constructor and err.code for every validation case (toThrowWithCode), not just the message.

Verified on the rebased tree: debug build, rustfmt, the Windows cargo check, fs-chown-utimes.test.ts (85 assertions), the test/js/node/path directory, the error.syscall / copyFile / utimes / chown / mkdtemp / readdir / truncate suites in fs.test.ts, cp.test.ts plus promises.test.js, and the upstream test-fs-{utimes,copyfile,lchown,chown-type-check} and test-path-{parse-format,basename,extname} files. One note on cp.test.ts: fs.promises.cp recursive does not free parent task while subtasks are in flight after an error hit its 5s budget on this build host at load ~130, and does exactly the same on clean main under the same load (4/4 at 5000ms on both; about 3s on both when the host was quieter), so it is host load, not this change.

One correction for the record: the first force-push of this round (8c930a0) briefly carried only the test commit, because a local git stash on a full disk dropped the resolved src files from the working tree before the commits were built. It was replaced within the hour by 64e262a rebuilt from the saved resolution; build 112378 ran against that incomplete head and can be ignored.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants