ITER-0002: compressed postings codecs (DeltaVarint + BlockDelta) - #12
Conversation
170b448 to
c2c1f07
Compare
waywardmonkeys
left a comment
There was a problem hiding this comment.
Should Fix
-
crates/leit_postings/src/codec.rs:460-494 and :547-579: block decode mutates out_docs before full validation finishes. On malformed input, callers can observe partially decoded docs or non-parallel docs/tfs after Err. That is risky with reusable decode scratch. Either document that output buffers are unspecified on error, or decode a block into temporary lengths and truncate/clear on error.
-
crates/leit_postings/src/codec.rs:447 and :543: prefer checked arithmetic for pos + doc_bytes_len. The decoded length is attacker-controlled. On 64-bit this is mostly theoretical due slice-size limits, but this is a malformed-input hardening patch in a no_std crate, so checked_add(...).filter(|end| *end <= bytes.len()) is the calmer invariant.
Could Improve
-
decode_block_doc_count is a good extraction, but it advances pos before validating the count. It is only local state today, so fine. A future helper that validates before committing the cursor position would be easier to reason about.
-
Add a short doc note to Codec::decode / BlockDecoder::decode_block: “on error, output buffers are unspecified” or “on error, output buffers are cleared.” Pick one and enforce it.
… (STORY-0096) ITER-0001 dependency hygiene per the usage-site rule: the leit_wind_tunnel harness uses only rapidhash in its library surface; leit_core/leit_index/ leit_text are used solely by its #[cfg(test)] integration tests, so they move to [dev-dependencies] and no longer appear in the harness's production dependency graph. The bench crates were already correct (empty lib; all deps dev). Library build, 17 unit tests, and both bench crates verified green.
… (STORY-0112) ITER-0001: BlockId, FilterExprId, SegmentOrd, SegmentLocalDocId in leit_core, each a #[repr(transparent)] newtype over a [u8; 4] little-endian inner deriving bytemuck Pod/Zeroable. The on-disk form is the in-memory form: a &[u8] slice from an mmap'd buffer casts in place to &[Id] with no allocation or deserialization (zero-copy), stable across host endianness; ordering is numeric. bytemuck chosen over zerocopy because zerocopy's derives emit internal #[allow(non_ascii_idents)]/#[allow(non_local_definitions)] that conflict with the workspace's forbid-level Linebender lints (E0453); bytemuck is no_std and lint-clean under the same forbid set. Proven by SCENARIO-0005 (6 unit tests: value + slice + unaligned round-trip, numeric ordering, LE byte layout).
Records the design-decidable decisions for the Phase 2 segment format (DEC-01..10) with rationale, a Phase 3 forward-compatibility audit, and decision->enforcement traceability. Human-confirmed key calls: - DEC-01 segment offsets: u64 (no size cap; removes the only Phase 3 format-migration risk) - DEC-10 integrity: single footer checksum, verified in Full validation mode - DEC-06 block-aware API: public dedicated BlockCursor trait (Phase 3 WAND consumes it without a format/API break) - DEC-05 header: fixed-layout little-endian POD, absolute u64 section offsets, magic + version + format_flags, reserved stored-fields/columnar slots Decision-documentation ACs of STORY-0078/0081-0084/0090/0043-0047 are satisfied here (decided:ITER-0001); their code-enforcement ACs are deferred to ITER-0003/0004. Forward constraint recorded for ITER-0005: block-metadata schema must carry per-block max_score + doc-range for Phase 3 WAND/MaxScore.
…ORY-0112 AC-2) ITER-0001 audit corrective: SCENARIO-0005 now also exercises try_from_bytes/ try_cast_slice (Ok on well-formed, Err on malformed) per AC-2's validated-read obligation.
…elta) [ITER-0002] Codec layer for ITER-0002. A Codec trait with two implementations over a stable v1 block format, plus the layout decisions (DEC-11 fixed 128-doc blocks, DEC-12 layout) and a new TermFreq segment-resident type. - DeltaVarint (CodecId 0) + BlockDelta (CodecId 1, 128-doc independently-decodable blocks with validated first/last-doc header range). - Hand-rolled LEB128 varint into a type-enforced [u8;5]; no_std + alloc; no new deps. - API speaks named segment-resident types SegmentLocalDocId + TermFreq (no anonymous u32 drift); EntityId stays the in-memory abstraction, lowered at the segment boundary. - Decode into caller-provided &mut Vec<..> — scratch-ownership-agnostic (TODO(ITER-0003) / STORY-0079). Doc-sorted precondition enforced via checked_sub (deterministic panic). - CodecId marker per list; segment-format reservation deferred:ITER-0004. Stories: STORY-0002/0003/0004/0005(AC1-2)/0009 done; STORY-0087/0088 decided. Proof: SCENARIO-0006 (36 leit_postings tests). PAR spec + quality reviewed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the requested malformed-input invariants: checked section arithmetic, block-count and varint hardening, full doc-stream consumption, and transactional decode outputs. Codec and BlockDecoder APIs now guarantee both output buffers are empty on error. |
Summary
Phase 2 compressed-postings codec layer — the third iteration in the Phase 2 stack (after #10 wind tunnel and #11 decisions + core IDs). Adds a
Codectrait with two implementations over a stable v1 block format, plus the layout decisions that govern it.Stacked on #11 (
phase2-decisions). Until #10/#11 merge, this PR's diff againstmainincludes their commits; review only theleit_postingscodec +leit_coreTermFreq+ DEC-11/12 changes here.What's in it
Codectrait +CodecIdmarker + structuredCodecError(crates/leit_postings/src/codec.rs).DeltaVarintCodec(CodecId 0): single-stream delta-encoded doc IDs + LEB128 varint TFs.BlockDeltaCodec(CodecId 1): fixed 128-doc blocks (DEC-11), each self-contained (first doc absolute) and independently decodable via a per-block header (doc_count,first_doc,last_doc,doc_bytes_len); the header doc-range is validated on decode.[u8; 5]buffer.no_std+alloc, no new deps.SegmentLocalDocId+ newleit_core::TermFreq(no anonymousu32); the genericEntityIdstays the in-memory abstraction and lowers toSegmentLocalDocIdat the segment-write boundary.&mut Vec<…>buffers — scratch-ownership-agnostic, so theDecodeScratchownership decision is left to the cursor iteration (markedTODO(ITER-0003)).checked_sub(deterministic panic, no silent corruption).Evidence
leit_postingstests (cargo test -p leit_postings codec): lossless round-trip for both codecs incl. multi-block (128/129/256/300 docs), byte-size reduction, varint edge cases (u32::MAX, truncation, over-long), bad-marker / corrupt-header rejection, unsorted-input panic.no_stdverified onaarch64-unknown-none; workspace clippy/fmt clean.Deferred (carried forward, not dropped)
advance_toblock-skip (cursor layer) and the e2e ranking-equivalence proof → ITER-0003.CodecIdmarker field → ITER-0004.🤖 Generated with Claude Code