Skip to content

Validate: reject runs past the container end, accept non-minimal run containers - #576

Merged
lemire merged 1 commit into
masterfrom
validate-run-end
Oct 19, 2026
Merged

lemire merged 1 commit into
masterfrom
validate-run-end

Conversation

@lemire

@lemire lemire commented Oct 19, 2026

Copy link
Copy Markdown
Member

This applies to Go the direction taken for Java in RoaringBitmap/RoaringBitmap#884 (issue RoaringBitmap/RoaringBitmap#879): Validate() checks that a bitmap is well formed, as CRoaring's run_container_validate does, and no longer requires run containers to be the smallest encoding.

Changes

  • Runs must end within the container. runContainer16.validate() never checked this, and interval16.last() wraps around. A serialized run patched to cover 65,000..67,000 passed Validate(). Maximum() then returned 1464, which Contains denied. validate() now checks every run first and returns the new ErrRunIntervalOutOfRange.
  • No size rule for run containers. validate() rejected a run container that takes more bytes than an array or bitmap container would. The library produces such containers itself. In a random test with 20,000 inputs per operation, AddOffset64 produced 107 bitmaps that Validate() rejected; for example, AddOffset64 of the run 0..9 by 65,534 leaves a run of two values in the first container. The rule added no safety: a well-formed run container is bounded by its block either way. ErrRunIntervalSize is no longer returned. It stays exported and is marked deprecated, so code that refers to it still compiles.
  • Doc. Validate now says that it does not check for the most compact encoding, and points to RunOptimize.

Tests

  • Two corruption tests weren't testing run containers. The cases named "Corrupts Run Length" in TestBitMapValidationFromDeserialization and TestBitMapValidationFromFrozen never called RunOptimize(), so they serialized array containers. They passed only because the corruption broke an array's sort order and the harness accepted any error. Now:
    • both cases build run containers and make the last run end past 65,535;
    • both harnesses use assert.ErrorIs(t, err, tt.err), so each case must fail for the reason it names.
  • TestIntervalValidationFailing is updated. Its two "too many runs" containers are well formed, so they now pass. It gains cases for a run ending exactly at 65,535 (accepted) and one past it (rejected).
  • TestValidateAcceptsRunContainersThatAreNotTheSmallestEncoding checks the AddOffset64 result above, before and after a serialization round trip.

The new and rewritten tests fail without the change. go test ./... passes, including roaring64, which uses the same run container.

…containers

runContainer16.validate() did not check that a run ends within the container:
interval16.last() wraps around, so a run from 65000 to 67000 passed, and
Maximum() then returned 1464, which Contains denied. Reject it with the new
ErrRunIntervalOutOfRange.

validate() also rejected a run container that was not the smallest encoding of
its values, although AddOffset produces such containers from valid bitmaps.
Drop that rule, as CRoaring does and as RoaringBitmap/RoaringBitmap#884 does
for Java. ErrRunIntervalSize is kept, deprecated, for compatibility.

The deserialization tests named "Corrupts Run Length" did not run-optimize
their bitmaps, so they corrupted array containers, and passed only because
the harness checked for any error. They now use run containers and check the
run end, and both harnesses check the expected error with ErrorIs.
@lemire
lemire merged commit 3cf1fa5 into master Oct 19, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant