Reject unsafe unquoted integer floats#274
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
The original comment justified the cutoff with f64 representability, which is wrong at this boundary (integers are exact up to 2^53). The actual hazard is serde_json's default float parsing, which is not correctly rounded: from 2^52 a one-ulp parse error is a whole integer, observed concretely as "9007199254740991.0" parsing to ...990.0. Below 2^52 the same error breaks integrality and the exactness check rejects it loudly. Document that, pin the observed misparse as a reject test, extend rejection coverage across [2^52, 2^53), drop two no-op rustfmt::skip attributes, and rewrite the changelog fragment accordingly with the PR reference. No-Verification-Needed: doc/test-only fixups, bound value unchanged
iainmcgin
left a comment
There was a problem hiding this comment.
[claude code] Thanks @fallintoplace — this fix is correct, and the review process turned up something worth recording: your constant was right for a reason the comment didn't state.
The original doc justified 2^52 - 1 via f64 spacing ("adjacent representable f64s are spaced at least one integer apart"), which is inaccurate at that boundary — every integer up to 2^53 is exactly representable, and our reviewer initially concluded the principled bound was 2^53 - 1 (Number.MAX_SAFE_INTEGER) and that the PR over-rejected a full binade. Moving the bound there failed empirically: "9007199254740991.0" (2^53 - 1, exactly representable) decodes to …990 — because serde_json's default float parsing is not correctly rounded (that's its opt-in float_roundtrip feature), and in [2^52, 2^53) a one-ulp parse error is a whole integer. Below 2^52 the same error breaks integrality and the existing exactness check rejects the token loudly instead of mis-decoding it. So 2^52 - 1 is exactly right for buffa's serde configuration.
Pushed as fe1b75e: the doc comment now states that real rationale (with the observed misparse as evidence), the reject tests pin "9007199254740991.0" and extend coverage across [2^52, 2^53), two no-op #[rustfmt::skip]s are dropped, and the changelog fragment is rewritten accordingly with the PR reference. The constant itself is unchanged from your version.
Residual limitation, documented rather than fixed: no f64-side bound can catch a non-integer token rounding to an integer ("1.0000000000000001" → 1 at any magnitude) — the complete fix would be serde_json's float_roundtrip or arbitrary_precision, which is a dependency-wide decision out of scope here.
Approving; will queue for merge once CI runs.
|
[claude code] Merged as 19b2c25 — thanks @fallintoplace! This one was the most instructive review of the batch: your bound survived a challenge from our own reviewer, and the empirical check that settled it (serde_json's default parse rounding …991.0 down to …990) is now pinned as a regression test. |
Summary
f64si64/u64values9007199254740993.0and exponent forms, including negativei64casesNotes
visit_f64does not expose the original JSON number text, so the parser may already have rounded before the integer visitor runs. The guard keeps small integer-valued float notation working while rejecting larger values that should be quoted for exact parsing.Tests
cargo test -p buffa --all-features json_helpers::testscargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-features