feat(derive): hold the bytes a word arrived as - #841
Conversation
A value used to reach its field through `from_utf8_lossy`, so a path with a stray byte in it became a path with `U+FFFD` in it — a different file, silently. That is the worst shape a bug can take: no error, wrong answer. The partial holds the bytes now and the conversion happens once, where the struct is built. `apply` cannot report anything — it answers whether an event belonged to this command — which is why the bytes have to travel that far before anyone can complain about them. A word that is not UTF-8 is an `InvalidValue` naming the field, showing the value lossily *for the message only*, and saying what was wrong with it. It also retires a hazard review raised on the last PR: `String` was recognised by how it was written so that its value could be moved rather than converted, which broke for an adopter who shadowed the name. There is no identity case left — everything converts — so recognising the spelling only skips a second step now, and getting it wrong is a compile error rather than a mangled value. Costs +656 instructions (1.6%) and one allocation against main, measured on the same fixture: the collecting path rebuilds a `Vec<String>` from bytes rather than moving one. That is what not corrupting a value is worth. Reporting such a value is not the same as accepting it, and accepting it needs `OsStr::from_encoded_bytes_unchecked` — `unsafe`, in a crate that has none. The call would be sound and PLAN.md says why, but that is jdx`s call rather than mine.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe parser now preserves CLI and environment values as bytes until final conversion. UTF-8 validation reports ChangesNon-UTF-8 value handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Mergeability Score: 🟡 Moderate · up to Invalid UTF-8 values used with choices may be reported as the wrong error type, which can mislead callers and violate the documented validation behavior. The PR should address this before merge, and the diagnostic-value assertion should be added. Sequence Diagram(s)sequenceDiagram
participant CLI
participant GeneratedParser
participant FieldConverter
participant TargetStruct
CLI->>GeneratedParser: provide argument or environment bytes
GeneratedParser->>FieldConverter: pass byte-preserving values
FieldConverter->>FieldConverter: validate UTF-8 and apply FromStr
FieldConverter->>TargetStruct: construct converted fields
FieldConverter-->>CLI: return InvalidValue for invalid UTF-8
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5257420. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
derive/src/codegen.rs (1)
1540-1549: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not convert invalid UTF-8 into
InvalidChoice.
checkruns beforefield_final. Line 1542 converts invalid bytes to"". Line 1543 then returnsInvalidChoicebefore final conversion can returnInvalidValue.If
""is a declared choice, this check passes and later returnsInvalidValue. The error type must not depend on the choice list. Skip choice validation when UTF-8 decoding fails. Letfield_finalreportInvalidValue.Proposed fix
- let __usage_text = ::std::str::from_utf8(value).unwrap_or_default(); - if !`#choices.contains`(&__usage_text) { - return ::std::result::Result::Err( - ::usage_argv::Error::InvalidChoice { - name: `#name`, - choices: `#choices`, - }, - ); + if let ::std::result::Result::Ok(__usage_text) = ::std::str::from_utf8(value) { + if !`#choices.contains`(&__usage_text) { + return ::std::result::Result::Err( + ::usage_argv::Error::InvalidChoice { + name: `#name`, + choices: `#choices`, + }, + ); + } }The PR objective requires
InvalidValuefor invalid UTF-8.🤖 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 `@derive/src/codegen.rs` around lines 1540 - 1549, Update the choice validation around __usage_text to skip the contains check when ::std::str::from_utf8(value) fails, rather than replacing invalid UTF-8 with an empty string for validation. Preserve choice validation for successfully decoded text, and let field_final produce InvalidValue for decoding failures regardless of the declared choices.
🤖 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 `@conformance/tests/typed.rs`:
- Around line 342-344: Guard the non-UTF-8 test and its Unix-specific imports in
the typed conformance tests with #[cfg(unix)], including the OsStrExt usage, so
the test module compiles on Windows while retaining the existing behavior on
Unix.
---
Outside diff comments:
In `@derive/src/codegen.rs`:
- Around line 1540-1549: Update the choice validation around __usage_text to
skip the contains check when ::std::str::from_utf8(value) fails, rather than
replacing invalid UTF-8 with an empty string for validation. Preserve choice
validation for successfully decoded text, and let field_final produce
InvalidValue for decoding failures regardless of the declared choices.
🪄 Autofix
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: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 6d317488-11ff-4f27-aa1d-90b315ee812c
📒 Files selected for processing (4)
PLAN.mdconformance/tests/typed.rsderive/src/codegen.rsderive/src/lib.rs
Instruction counts
No instruction-count regression above 1%. Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run. Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes. Shadow comparisonParsing
|
Greptile SummaryThis PR preserves argv values as bytes until typed struct construction, preventing lossy UTF-8 substitution.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "fix(derive): report the UTF-8 failure, n..." | Re-trigger Greptile |
A field with `choices` or `value_enum` reached the choices check before the struct was built, and that check compared the value as text through `from_utf8(...).unwrap_or_default()` — so a value that is not UTF-8 became the empty string, matched none of the choices, and came back as `InvalidChoice`: a message listing words, about a value that is not a word. The comment beside it already claimed `build` was where this got reported. It was not. Bytes that will not convert are passed over here and left for `build`, which names the field and gives the UTF-8 error as the reason. A value that *is* text and is not one of the choices still gets the list, which is what the check is for. Found by Cursor Bugbot on #841. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressed in the latest push. `choices` masked a UTF-8 failure — all three of you found this independently, which it deserved. A non-UTF-8 value became `""` through `unwrap_or_default()`, matched no choice, and came back as `InvalidChoice`: a message listing words, about a value that is not a word. The comment beside it already claimed `build` reported this; it did not. Bytes that will not convert are now passed over and left for `build`, which names the field and gives the UTF-8 error as the reason. A value that is text and is not one of the choices still gets the list. Reproduced first, then fixed — `ea7b427`. AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable. |

A value used to reach its field through
from_utf8_lossy, so a path with a stray byte in itbecame a path with
U+FFFDin it — a different file, silently. That is the worst shape a bugcan take: no error, wrong answer.
The partial holds the bytes now and the conversion happens once, where the struct is built.
applycannot report anything — it answers whether an event belonged to this command — whichis why the bytes have to travel that far before anyone can complain about them. A word that
is not UTF-8 is an
InvalidValuenaming the field, showing the value lossily for the messageonly, and saying what was wrong with it.
It also retires a hazard review raised on the last PR:
Stringwas recognised by how it waswritten so that its value could be moved rather than converted, which broke for an adopter
who shadowed the name. There is no identity case left — everything converts — so recognising
the spelling only skips a second step now, and getting it wrong is a compile error rather
than a mangled value.
Costs +656 instructions (1.6%) and one allocation against main, measured on the same fixture:
the collecting path rebuilds a
Vec<String>from bytes rather than moving one. That is whatnot corrupting a value is worth.
Reporting such a value is not the same as accepting it, and accepting it needs
OsStr::from_encoded_bytes_unchecked—unsafe, in a crate that has none. The call would besound and PLAN.md says why, but that is jdx`s call rather than mine.
Stack created with GitHub Stacks CLI • Give Feedback 💬
Note
Medium Risk
Changes how all CLI string values are stored and converted during parse; behavior improves for invalid UTF-8 but touches hot derive codegen paths with a small measured perf cost.
Overview
Argv values are kept as raw bytes through binding instead of being turned into strings with
from_utf8_lossy. The derive partial now storesVec<u8>for string-like fields; conversion toString/FromStrtypes happens once inbuild, where invalid UTF-8 becomesInvalidValue(the error message may show the value lossily for display only).value_enum/choiceschecks skip bytes that are not valid UTF-8 so users get a UTF-8 error instead of a misleadingInvalidChoicelist.The old
Stringidentity fast path is removed—every typed field goes through the same UTF-8 validation path. Defaults andenvfallbacks write UTF-8 bytes into the partial to match.Conformance tests cover non-UTF-8
--outpaths and enumerated--shellvalues. PLAN.md marks “report, don’t mangle” done and notes accepting exact non-UTF-8 paths is still future work.Reviewed by Cursor Bugbot for commit ea7b427. Bugbot is set up for automated code reviews on this repo. Configure here.
Cost
Measured on the mise-scale shadow (
use -g node@20), differencing two runs of the samebinary against the same fixture on
main:+656 instructions, or 1.6%, and one allocation — the
Vec<u8>a value is held in before itbecomes a
String. That is the price of not silently substituting a different filename, andit is paid only by invocations that carry a value.
What this does not do
A non-UTF-8 value is reported, not accepted:
PathBufon Unix could hold those bytesfaithfully, but constructing an
OsStrfrom them needsOsStr::from_encoded_bytes_unchecked,and usage-argv has no
unsafein it today. The bytes do come fromas_encoded_bytesin thissame process and every split the parser makes is at an ASCII byte, so the call would be sound
— but that is a policy decision about the crate rather than a detail of this change, so this
branch takes the conservative half and names the field in the error instead.
Resolved in #844, which is stacked on this one: jdx approved the
unsafe, and it thenturned out to be needed only on Windows — on Unix the safe
OsString::from_vecdoes the job.A
PathBuffield there accepts the bytes exactly, and does so for 567 instructions against aStringfield's 660.AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.
Summary by CodeRabbit
Bug Fixes
Documentation
Tests