Repository navigation
fix(json,deps): tolerate UTF-8 BOM at the remaining JSON parse sites - #3751
Conversation
rtk init -g --auto-patch aborted with 'expected value at line 1 column 1'
on a settings.json that begins with a UTF-8 BOM — which Notepad and
PowerShell 5.1's Out-File -Encoding utf8 both write by default. The hook
was never installed and the error blamed the user's (valid) JSON. Worse,
the statusline patch path fell back to json!({}) on the parse failure
and would silently wipe existing settings on write-back.
strip_leading_bom already existed in hook_cmd.rs, guarding all four hook
stdin entry points; the file-read paths never got the same treatment.
Promote it to core::utils and apply it at every site that parses JSON a
human may have hand-edited: settings.json / hooks.json readers in init.rs
(12 sites), hook_check.rs, integrity.rs, and permissions.rs.
TOML sites (config.toml, filter DSL) are deliberately untouched: the toml
crate tolerates a leading BOM, pinned by a new regression test. The BOM
is dropped on write-back — RFC 8259 s8.1 forbids adding one to JSON.
Fork issue: #58 (stays open until the upstream PR is submitted)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ment Review round on PR #62 found two hand-edited JSON parse sites the sweep missed — read_composer_bin_dir (core/utils.rs) and parse_global_json_mtp_mode (dotnet_cmd.rs, where BOMs are most common) — plus a test comment claiming the toml crate rejects BOMs when it in fact tolerates them (which is the very reason TOML sites are untouched). Also adds the BOM+invalid-JSON edge test the spec asked for: stripping must not mask genuinely broken JSON. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sites Follow-up to upstream rtk-ai#3203 (fork PR #62), which added strip_leading_bom() for config reads. KuSh's review flagged three more parse sites that hard-fail on BOM-prefixed files, scoped out of rtk-ai#3203 into this follow-up: - deps.rs summarize_package_json_str: a package.json saved by Notepad or PowerShell 5.1 Out-File killed `rtk deps` with a bare "expected value at line 1 column 1" (no file path). Now BOM-tolerant, with .context() naming the file on both read and parse failures. - json_cmd.rs filter_json_compact / filter_json_string: `rtk json` (both compact and --keys-only) errored on BOM-prefixed files -- the most likely command to be pointed at human-saved JSON. Unit tests per site: BOM input parses identically to plain input; invalid package.json errors now carry the path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n_str wrapper Review response for upstream rtk-ai#3203 (KuSh): - patch_gemini_settings: a non-BOM parse failure (trailing comma, comment, truncated file) now propagates with .context() naming the file, matching the Claude path, instead of unwrap_or(json!({})) silently replacing the user's settings with an empty object on write-back. Regression test pins both the error and that the file on disk is untouched. - New core::utils::from_json_str: BOM-tolerant serde_json::from_str with Deserialize<'a> (zero-copy preserved, tested with a borrowed type). Every JSON parse site in this PR's diff now routes through it -- init.rs, hook_check.rs, permissions.rs, integrity.rs, dotnet_cmd.rs, and the composer.json site in utils.rs -- so the policy is one discoverable helper instead of a per-site convention. strip_leading_bom stays pub as the primitive for non-serde consumers. The three out-of-diff sites KuSh flagged (deps.rs, json_cmd.rs) are the follow-up PR rtk-ai#3751, as requested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n_str The from_json_str wrapper landed on the base branch (rtk-ai#3203 review round) after this branch's fix was written; convert its three sites from the raw serde_json::from_str(strip_leading_bom(..)) combination to the wrapper so the whole series states the policy one way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Updated: merged the current #3203 branch (which now carries the |
KuSh
left a comment
There was a problem hiding this comment.
Reviewed the full cumulative diff (5 commits, 10 files). Verified on a clean worktree at beee9df: cargo fmt --all -- --check clean, cargo clippy --all-targets zero warnings, cargo test --all 2568 passed / 0 failed.
The core change is sound. from_json_str keeps the zero-copy <'a, T: Deserialize<'a>> signature, and the strip_leading_bom -> trim().is_empty() ordering is correct at every site that needed it: U+FEFF is Cf, not whitespace, so the empty-file guard has to run after stripping, and the new BOM-only / BOM-plus-invalid-JSON tests pin exactly that. Panic safety on the write paths is intact, and the remaining serde_json::from_str call sites in the tree are command-output parsers (aws, pnpm, vitest, ruff, ...) or RTK-written state (trust.rs), correctly left alone.
Three things to address before merge, see the inline comments. The hook_cmd.rs one matters most, given the PR is scoped as "the remaining parse sites".
…M on claude/gemini hook stdin and rtk json fallback Review response for upstream rtk-ai#3751 (KuSh): - patch_gemini_settings: a parse failure no longer aborts rtk init after the hook script, GEMINI.md, and integrity baseline are already written. It now warns with the file and error, prints the same manual-setup instructions as PatchMode::Skip (shared helper), and continues -- still never writing over the unparsed file. - run_gemini and run_claude strip a leading BOM from the hook stdin payload like the copilot/cursor/droid paths. run_gemini's parse logic is extracted into run_gemini_inner, called by production and tests alike, so the BOM test exercises the real path rather than a mirrored copy. - rtk json strips the BOM once at the read (shared render_json helper), before never_worse, so the raw fallback for already-compact files no longer re-emits the BOM into piped output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three addressed as of b6f354c. Half-installed state: Hook stdin:
Gate: fmt clean, clippy zero warnings, 2516 tests passing. |
…appens Encodes the protocol validated on upstream PRs rtk-ai#3199/rtk-ai#3751: an uncontaminated run of upstream's own .claude/agents/code-reviewer.md prompt, plus the empirical techniques the maintainer layers on top -- fixture A/B against the merge base, prior-probe re-runs, bisect attribution -- and the fix-round discipline (load-bearing proof by revert, total contracts over examples, no documented deferrals, no scope-creep riders). do-issue Phase 6 now delegates its second review pass to /preclear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KuSh
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround — I re-reviewed b6f354c and all three earlier findings are properly fixed; I've resolved those threads. The run_gemini_inner extraction in particular is the right shape: routing production run_gemini through it (rather than mirroring the parse in a test) means a dropped BOM strip fails for real.
Re-ran the gate on the head: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --all all clean on a default environment.
One blocker and three non-blocking notes, all on the new commit:
- Blocking —
test_gemini_strips_utf8_bomis non-hermetic. It reads the developer's real~/.gemini/settings.json; I reproduced a red test on a machine that uses Gemini CLI. Details inline. - Non-blocking: the lost serde diagnostic in
run_gemini, the "installed" summary printed after a failed settings patch, and an avoidable full copy of the input inrender_json. Plus one nitpick on the tracking baseline.
Happy to see the rest land as-is if you'd rather keep the PR tight — only the first one needs to change before merge.
… honest init summary, zero-copy fallback Review response for upstream rtk-ai#3751 (KuSh round 2): - test_gemini_strips_utf8_bom is hermetic: rules are injected through a shared run_gemini_inner_impl so the test no longer reads the real ~/.gemini/settings.json, while production and the test still exercise the identical parse/strip/render path. Verified under KuSh's exact dirty-HOME reproduction (allow-listed git status): full suite green. - run_gemini_inner returns serde_json::Result, so a malformed payload reports the real serde diagnostic (line/column) under the context message again. - After a settings.json parse failure, the init summary now prints 'settings.json: NOT patched (existing file could not be parsed; see warning above)' instead of unconditional success text; exit stays 0 per the graceful-degrade design. PatchMode::Skip is unchanged. - render_json returns Cow<str>: the never_worse raw-fallback path borrows instead of allocating a third full copy of the input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All addressed as of ed8b0eb — the blocker plus the three notes; one nit deliberately declined, reasoning below. The hermetic test (blocker): went with your second option — rules injected through a shared Serde diagnostic: Honest summary: Fallback copy: The Gate: fmt clean, clippy zero warnings, 2517 tests passing — under both a clean HOME and your dirty-HOME reproduction. |
…ip, name files in deps read errors Review follow-ups for rtk-ai#3751 (round 3): - render_json compares the `&str` fat pointers instead of `as_ptr()` when deciding which side never_worse handed back. An address-only comparison also matches a sub-slice of `content`, so a future truncating never_worse would take the borrow branch and silently re-expand the output, with no test failure. - run_droid and the droid test entry point now share `droid_payload`, which strips the BOM, trims, and reports an empty payload as nothing to do. Droid's BOM strip had zero coverage: the test path re-parsed without it, so deleting the strip left every test green. Verified by mutation -- removing it now fails test_droid_strips_utf8_bom. - The four remaining bare `fs::read_to_string(path)?` calls in deps.rs gained the same `.with_context(...)` the package.json read already had, so `chmod 000 Cargo.toml && rtk deps .` names the file instead of reporting a bare "Permission denied (os error 13)". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Conflict in src/hooks/hook_cmd.rs: develop added the Mistral Vibe hook right where this branch refactored run_gemini's tail into run_gemini_inner_impl. Kept both -- the refactored gemini arm and develop's Vibe section verbatim -- and dropped print_allow/print_gemini, which the refactor already replaced with gemini_json and which nothing else calls.
run_vibe landed on develop while this branch was in review, making it the sixth hook stdin entry point and the only one that did not strip a leading UTF-8 BOM. Windows hosts prepend one (confirmed for Cursor); without the strip, serde_json rejects the payload, run_vibe_inner logs to stderr and returns None, and every command silently stops being rewritten -- the same failure the other five entry points already guard against. Verified by mutation: removing the strip fails test_vibe_strips_utf8_bom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KuSh
left a comment
There was a problem hiding this comment.
Approving. Three rounds in, everything raised is addressed and the BOM coverage is now genuinely complete: all six hook stdin entry points strip, and every non-test serde_json::from_str over a human- or tool-written file routes through from_json_str. The sites deliberately left raw — the trust.rs trust store, analytics JSONL, nested toolArgs strings, fixtures — are RTK/machine-written and correctly untouched.
Worth restating what the patch_gemini_settings change actually buys: the old unwrap_or(json!({})) would silently overwrite an unparseable ~/.gemini/settings.json with a fresh {} plus the hook, destroying user config. That's a real data-loss fix, not just BOM tolerance.
Two things I liked in the review responses: routing production run_gemini through run_gemini_inner_impl rather than mirroring the parse in a test, so a dropped strip fails for real; and returning Ok(true) from patch_gemini_settings only on the parse-failure path, so the summary can distinguish it from Skip, already-patched, declined-at-prompt and dry-run.
Disclosure — the last three commits are mine, pushed via maintainer edit rather than sent back for another round:
9d78af2— the three round-3 findings:std::ptr::eqon the&strfat pointers instead ofas_ptr()(an address-only compare also matches a sub-slice, so a future truncatingnever_worsewould silently re-expand the output);run_droidand its test entry point now share adroid_payloadhelper, closing a gap where droid's BOM strip had zero coverage; and.with_context(...)on the four remaining bare reads indeps.rs.3865a84— merge ofdevelop. The conflict was inhook_cmd.rsonly:developadded the Vibe hook exactly where this branch refactoredrun_gemini's tail. Kept both sides, dropped the now-unusedprint_allow/print_gemini.ac3389a—run_vibelanded ondevelopmid-review as the sixth hook stdin entry point and the only one not stripping, so mergingdevelopin would have quietly broken the invariant this PR exists to establish. One line plus a test.
Both new tests are mutation-verified: deleting the strip fails test_droid_strips_utf8_bom / test_vibe_strips_utf8_bom and nothing else. Gate on ac3389a: cargo fmt --all -- --check clean, cargo clippy --all-targets -- -D warnings clean, cargo test --all 2735 passed / 0 failed.
Please sanity-check those three commits — happy to drop or rework any of them if you'd rather they were separate.
Two follow-ups filed from this review, both pre-existing and out of scope here: #3767 (one malformed manifest aborts the whole rtk deps scan instead of degrading) and #3768 (test_resolved_command_executes_known_command asserts on rustup's exit status and fails whenever HOME is redirected).
|
Sanity-checked all three commits — keep them all, and thanks for pushing directly rather than spending another round.
Independently re-ran the gate on |
Stacked on #3203 (
submit/bom-config-reads) — only the last commit (fix(json,deps): tolerate UTF-8 BOM at the three remaining JSON parse sites) is new; the first two commits are #3203 itself and will drop out of the diff once it merges. Happy to rebase onto develop after #3203 lands.Follow-up covering the three parse sites @KuSh flagged in the #3203 review as needing the same BOM tolerance, which were scoped out of that PR:
src/cmds/system/deps.rs—summarize_package_json_str: parsedpackage.jsonwith a rawserde_json::from_str(&content)?(also missing.context()). A BOM'd package.json — written by Notepad or PowerShell 5.1Out-File -Encoding utf8— madertk depshard-fail with a bareexpected value at line 1 column 1that didn't name the file. Now strips the BOM viastrip_leading_bomand carries the file path in both read and parse errors.src/cmds/system/json_cmd.rs—filter_json_compactandfilter_json_string:rtk json <file>(compact and--keys-only) errored on BOM-prefixed files — the single most likely command to be pointed at human-saved JSON. Both entry points (file and stdin) route through these functions, so both are covered.Repro
Verified before/after on all three paths, including
--keys-only.Tests
Per site: BOM-prefixed input parses and produces output identical to the plain input; a plain-input test guards existing behavior; the invalid-package.json error now contains the path.
cargo fmt --all && cargo clippy --all-targets && cargo test --all— clean.🤖 Generated with Claude Code