basen/cbase32: decode max-length vectors - #1710
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | d6bc100 | Commit Preview URL Branch Preview URL |
Aug 26 2026, 01:41 PM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95dcaac4c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa103b3e9a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Fix is correct — I could not break it. The boundary reproduces on the merge-base: payloads of 1048575 and 1048576 bits (209716 chars) return a silent null, and cBase32ToVec rejects vecToCBase32's own output at those two lengths. Head decodes both, and max+1 (1048577) still returns null, so the bound was replaced, not removed. 371 round-trip cases and a 110,080-case base-vs-head differential over lengths ≤3 show 0 divergences below the boundary.
Two things to fix:
Two mutants survive the new proofs — 3381 pass, 0 fail under both.
-
> maxLength→> maxLength + 2nsurvives. The decoder then accepts 1048577- and 1048578-bit payloads, returningVecs that break the module's ownmaxLengthinvariant, and nothing fails.decodeOverflowonly asserts 1048579; max+1 and max+2 are unpinned. AddassertEq(cBase32ToVec('0'.repeat(209_715) + '4'), null)(= max+1). -
if (tail === null) return null→continuesurvives. That is the rejection path the backward scan actually introduced, and neither new assert reaches it:cBase32ToVec('u')andcBase32ToVec('u8')both put the invalid character at or before the sentinel. With the check skipped,'8u'decodes to a 1-bit vector instead ofnull. AddassertEq(cBase32ToVec('8u'), null).
Changelog: is misplaced. It sits at the end of ### Description with ### Testing after it. Per CONTRIBUTING.md it must be the last section before the trailer block — pure reorder, move it below ### Testing. Content itself is right; changelog/unreleased/1710.md exists and conforms.
Two body nits: ### Description names a test maxLengthBoundary that does not exist (the additions are decodeAtMaxLengthSucceeds and trailingZeroSymbols), and ### Testing cites 3,380 / 7 cbase32 proofs while npm test gives 3381 / 8.
Deleting base64/todo/decode-rejects-max-size-input.md is fine, not premature — base64's own encodeAtMaxLengthSucceeds/decodeAtMaxLengthSucceeds are already at the merge-base; the file was the spun-off cbase32 residual, and its task 3 explicitly permits closing. No dangling links.
One note, not blocking, not yours: vecToCBase32 always appends a full padding block via msb.concat, which has no maxLength guard (only tryListToVec does). At exactly the two lengths this PR makes decodable, the encode-side intermediate Vec is 1048580 bits — over maxLength. So decode now returns values encode cannot round-trip within contract. Pre-existing and encode is untouched here, but no todo records it any more.
Gates: npm test 3381 pass / 0 fail; npx tsc --noEmit exit 0.
…dded intermediate ### Motivation - Prevent decoding from constructing an intermediate `Vec` larger than `maxLength` when handling sentinel-bit padding in cbase32; this fixes a class of boundary rejections where a `maxLength`-sized payload would previously be rejected. - Align cbase32 decoding strategy with the earlier base64 fix by scanning for the sentinel from the end rather than accumulating the entire padded body. - Record the user-visible change in the changelog. ### Description - Replace the eager decode-and-trim approach in `fjs/basen/cbase32/module.f.mjs`'s `cBase32ToVec` with a reverse scan that decodes one character at a time from the end and only constructs the retained `Vec` parts, avoiding oversized intermediate vectors and checking `maxLength` only after trimming. - Update imports to include `maxLength` and adjust logic to validate combined head/tail length before returning. - Extend `fjs/basen/cbase32/proof.f.mjs` with new tests for trailing zero symbols, `decodeAtMaxLengthSucceeds`, and overflow rejections, and remove the obsolete `todo` file. - Add a changelog entry at `changelog/unreleased/1710.md` describing the fix for `basen/cbase32`. ### Testing - Ran the `fjs/basen/cbase32/proof.f.mjs` proof suite, which includes `trailingZeroSymbols`, `decodeAtMaxLengthSucceeds`, and `decodeOverflow`, and the new assertions passed. - Executed the broader basen proof tests that exercise related encoding/decoding invariants, and they completed successfully. - No new failing tests were observed after the change.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Both mutants are dead. > maxLength + 1n and > maxLength + 2n are killed by decodeOverflow — one assert catching both, since '0'×209715 + '4' is max+1 and exceeds neither relaxed bound — and if (tail === null) return null → continue is killed by unterminated. Every previously-dead neighbour is still dead and nothing was displaced: >= maxLength and the merge-base revert still fail decodeAtMaxLengthSucceeds, +3n and dropping the length check still fail decodeOverflow. The head === null guard is still a tsc-kill only, unchanged. Verdicts pinned are the right ones — I recomputed max−1 / max / max+1 / max+2 / max+3 and the '8u'/'8-'/'u'/'u8'/'8' family directly and every one matches. npm test 3381/0, tsc exit 0, assert sites 21 → 23, zero deletions.
The code is done. What's left is all in the PR body, and none of it moved:
Changelog: is still misplaced — line 12, inside ### Description, with ### Testing after it and the trailer below that. It has to be the last section before the trailer block.
And its contents don't match the file. changelog/unreleased/1710.md says "decode vectors at the maxLength boundary without building an oversized sentinel-padded intermediate" as a list item; the body says "ensure decoding at the maxLength boundary succeeds and prevent intermediate Vec overflow when stripping sentinel padding" as inline prose. CONTRIBUTING wants exactly the list items, same Markdown subset — both the wording and the form differ here. I hadn't caught the wording divergence last time; only the placement.
Two nits also unchanged: line 9 still names a test maxLengthBoundary that doesn't exist (decodeAtMaxLengthSucceeds, trailingZeroSymbols, decodeOverflow), and lines 17-18 still cite 7/7 cbase32 proofs and 3,380 where npm test gives 8 and 3381.
The encode-side note from last time is left as-is, which is fine — I'd just mention todo/178.md is about moving padding into bit_vec, not about vecToCBase32's unguarded intermediate, so it doesn't record it.
|
@codex review |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. The delta is merge-only — the PR's own changeset is byte-identical at both heads (same 4 files, same 44/63, identical blob SHAs), so nothing new to review there. Main moved a lot in between (#1711 restructured edag, #1712 changed rtti tuple reading, #1713 touched effects/node), so I re-ran the three mutants at the merged head: > maxLength + 1n and + 2n both still die naming decodeOverflow, and return null → continue still dies naming unterminated. Nothing displaced. npm test 3396/0, tsc exit 0.
Body findings are fixed: Changelog: is now the last section before the trailer, and its contents are character-identical to changelog/unreleased/1710.md — same two-line list item, same continuation indent, same md5. The nonexistent maxLengthBoundary is gone and the three real test names are there; the counts now read 8/8 and 3,381/3,381, matching the run you actually did.
Two things, neither blocking:
There's a blank line between Changelog: and the list item. CONTRIBUTING's example has none. Cosmetic, mentioning it only because you've been tidying these.
The encode-side residual is still unrecorded anywhere. vecToCBase32 is unchanged and still concatenates a full padding block with no maxLength guard, so at exactly the two lengths this PR makes decodable the intermediate Vec is over the limit. todo/178.md doesn't cover it — zero matches for maxLength or overflow. Pre-existing and out of scope for this PR; it just means nothing in the tree remembers it once this merges.
Motivation
cBase32ToVeccould build an intermediateVecwider thanmaxLengthwhile stripping Crockford-style sentinel padding.base64.decodeso an exactly-maxLengthpayload decodes instead of being rejected or causing oversized intermediates.Description
cBase32ToVecto locate the sentinel from the end one character at a time and concatenate only the retained payload, preventing temporary vectors from exceedingmaxLengthduring decoding. (fjs/basen/cbase32/module.f.mjs).trailingZeroSymbols,decodeAtMaxLengthSucceeds, anddecodeOverflowtofjs/basen/cbase32/proof.f.mjs, and importmaxLengthinto the proof to validate boundary behavior and oversized rejection. (fjs/basen/cbase32/proof.f.mjs).fjs/basen/base64/todo/decode-rejects-max-size-input.md).Testing
npx tscand the TypeScript check succeeded.cd fjs/basen/cbase32 && fjs testand all tests passed (8/8).fjs testand all tests passed (3,381/3,381).npm run covand observed 100% line/branch/function coverage across files reported by the runner.cargo clippyandcargo fmt -- --checkand both succeeded.Changelog:
basen/cbase32: decode vectors at themaxLengthboundary without buildingan oversized sentinel-padded intermediate
Codex Task