Repository navigation
fix(stack-encrypt): open JSON index floats to the exact value sealed - #1366
Conversation
The JSON index stores a number leaf as its shortest round-trip text (serde_json::Number::to_string in encode_node) and parses it back when the entry opens (decode_node) or the sealed root opens (PlainJson::parse). serde_json without float_roundtrip parses floats with a fast path that is not guaranteed to return the nearest f64, so a value with a full-precision expansion could open one unit in the last place off: -926809.5051008083 opened as -926809.5051008085, through JsonDocument, JsonNode and JsonSubtree alike. Enable float_roundtrip on stack-encrypt's serde_json dependency. Cargo unifies features, so it reaches every build graph that compiles this crate (the EQL workspace through eql-bindings' stack-encrypt feature, and the Go guests) without a change to their manifests or lockfiles. The read side derives no term from the parsed value: value selectors and ordering terms are computed on the write side only, from the caller's value. A regression test reads the issue's value back four ways, and a property test does the same for arbitrary finite f64 under the generated-documents suite's seed and case count. Both fail without the feature; the shrunk seed is kept in the regressions file. Closes #1364 Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Mutation testing (cargo-mutants,
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation: no critical or blocking issues found.
Source: automated /code-review medium, with each finding then verified against head ff287a3 and cross-checked against the existing PR feedback (company-skills:cross-check-review-findings). I read the diff and the source it touches. I did not build the crate or run the tests.
Five candidate findings came out of the review. None survived verification:
- Regressions file not loaded (refuted).
config()setsfailure_persistence = Direct(REGRESSIONS)(json_index.rs:1787), which applies to anyTestRunnerbuilt from it, as the existing generated-documents test already does. Theccseed was saved by this test's own strategy. - Use-after-move in
read_back(refuted).JsonDocument::nodereturns an ownedOption<JsonNode>(src/json/mod.rs:494), andsubtree_nodescollects owned nodes, so nothing borrowssealedwhen it moves intoopen. - Test cost, feature unification and changelog wording (dropped). These are design or style observations, not defects. The PR description already documents the trade-offs, and the case count matches the existing suite.
Existing feedback was bot status comments plus a bare "Human review" comment, with no inline threads, so nothing overlaps.
Non-blocking note: the changeset-bot warning is expected here, since the PR changes only a Rust crate and the description says why no npm changeset is needed.
Generated by Claude Code
Summary
EQL v4's
json_treeis the encrypted JSON column type. The Rust cratestack-encryptwrites it as one encrypted entry per JSON node. A number in a stored document could come back slightly wrong when read:-926809.5051008083came back as-926809.5051008085. Nothing raised an error. With this PR every finite float comes back bit for bit. Integers and short decimals were never affected.The cause: each number is stored as text, and that text is parsed back into a number when the document is read. The JSON library,
serde_json, was built without itsfloat_roundtripfeature. Without that feature it parses floats with a fast method that can land on the neighbouringf64(one unit in the last place off). This PR turns the feature on.Changes
packages/stack-encrypt/Cargo.toml:serde_jsonnow hasfeatures = ["float_roundtrip"], with a comment saying why and what it costs. The cost is slower float parsing and a little more code. Nothing new is compiled and no lockfile changes.packages/stack-encrypt/tests/json_index.rs: new modulegiven_a_float_at_full_precision, with two tests.->returns it, and a subtree read under$.list.f64values (proptest::num::f64::ANYfiltered to finite). It reuses the existing generated-documents suite's configuration: fixed seed 1336, 512 cases,PROPTEST_RNG_SEED/PROPTEST_CASESoverrides, and its regressions file.config()there becamepub(super)so the new module can use it.packages/stack-encrypt/proptest-regressions/json_index.txt: the seed proptest saved when the property failed without the feature. It shrinks to-5.7483763378806435e-244and is now replayed on every run.packages/stack-encrypt/CHANGELOG.md: a### Fixedentry under Unreleased, including the trade.No npm changeset: this only changes the Rust crate
stack-encrypt. The published SQL and theeql-bindingssurface do not change.Where the stored text is parsed back, and which crates needed the feature
stack-encryptis the only crate that parses it, in two places:decode_nodeinsrc/json/derive.rsparses a number entry's text withstr::parse::<serde_json::Number>(). This is how a document with an open root, an extracted node and a subtree read get their numbers.PlainJson::parseinsrc/json/plain.rs(serde_json::from_slice) parses the sealed root's whole-document text. The default plan reads documents this way.eql-bindings(EQL workspace) parses only the stored wire form: base64/base85 text and integer ordinals, never a plaintext number. Itsjson_treereads go throughstack-encrypt'sDecryptInto. It gets the feature through Cargo feature unification whenever itsstack-encryptfeature is on.cargo treeinpackages/eqlshowsserde_json feature "float_roundtrip"coming fromstack-encrypt. No manifest change was needed.languages/golang/encrypt/guest) buildsstack-encryptby path, so the feature reaches it the same way (cargo tree --lockedthere shows it). It does not openjson_treetoday: the dynamic plaintext set ineql-bindingshas no JSON value. No manifest change.protect-ffidoes not depend onstack-encrypt.Cargo.lock, and the feature adds no dependency.Does anything on the read side derive from the parsed float?
No. The value selector (
canonical_number) and the ordering term (leaf_op, the order-preserving term over the number as anf64) are computed only while sealing, inwalk/visit. They come from the caller'sserde_json::Value, which is parsed once by the caller. The read paths (decode_node,rebuild,open_document,open_subtree,open_fallback) only decrypt and decode. Subtree keys come from the path alone. So the parsing defect only affected the value handed back, not any stored or queried term.One side effect is worth knowing. Cargo unifies features, so any program that links
stack-encryptnow parses every float with the exact parser, including the caller's own JSON text. That only matters for a document whose text was parsed by an older build before it was sealed. The JSON index has not been released yet (it is under Unreleased in the changelog), so no such stored data exists.Verification
All commands were run from the worktree root unless noted.
Cargo.tomlchange):2 tests run: 2 passed).mise x --env test -- cargo nextest run -p stack-encrypt --all-features:824 tests run: 824 passed (1 slow), 1 skipped. The skipped test is the#[ignore]dwrite_stored_rows_v1fixture writer; the slow one is the trybuilduisuite.cargo test -p stack-encrypt --doc --all-features:50 passed; 0 failed.cargo clippy -p stack-encrypt --all-features --all-targets -- -D warnings: exit 0.cargo fmt --all --check: exit 0.RUSTDOCFLAGS="-D warnings" cargo doc -p stack-encrypt --all-features --no-deps: exit 0.cargo tree -p stack-encrypt -e features -i serde_json: showsserde_json feature "float_roundtrip"fromstack-encrypt v0.3.0.packages/eql:cargo test --locked -p eql-bindings --all-features: 226 passed, 0 failed.cargo metadata --locked: exit 0.cargo metadata --lockedat the root and in both Go guest workspaces: exit 0. No guest manifest changed, somise run wasm:guest:buildwas not run.pnpm run code:fixwas not run: nothing here is JavaScript or TypeScript.Related
Closes #1364
Refs #1363: its property test
leaf()strategy inpackages/stack-encrypt/tests/json_properties.rsdraws only quarters (f64::from(n) / 4.0) so it would pass without this fix. Once this PR is in, widen that arm back to arbitrary finitef64(proptest::num::f64::ANYfiltered to finite) and drop the comment that explains the narrowing.https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu