Skip to content

BitReader reads a tight packet-sized buffer without an eight-byte slack - #10

Merged
rowan-claude merged 2 commits into
mainfrom
bitreader-tight-buffer
Sep 8, 2026
Merged

BitReader reads a tight packet-sized buffer without an eight-byte slack#10
rowan-claude merged 2 commits into
mainfrom
bitreader-tight-buffer

Conversation

@rowan-claude

Copy link
Copy Markdown
Collaborator

Fixes #9.

What the reader gets

new ReadStream( packet, packet.length ) on an exactly-sized byte[] now reads every field, including the last one, instead of throwing ArrayIndexOutOfBoundsException from BitReader.readBits in a release JVM. Any array size works; eight or more bytes of slack past the data is still the fastest shape, and an array with that slack takes exactly the path it took before.

Mechanism

reset copies the final min( bytes, 8 ) bytes into a zero-padded 16-byte tail whenever the array has fewer than eight bytes of slack, recording tailBase, the byte index the tail is based at, and otherwise sets tailBase = Integer.MAX_VALUE. readBits picks its window source with one well-predicted branch, i < tailBase ? LONG_LE.get( data, i ) : LONG_LE.get( tail, i - tailBase ), and loads a single window either way: no allocation per read, and the padding is never interpreted, since bits past the end of the data cannot reach a read's output. This is the serialize.go shape (bitpacker.go, fillTail / readBits), sized for this reader's [1,32] field width.

The slack assert in checkReset is gone; it now asserts bytes <= data.length. Version and STANDARD.md untouched, by house rule. ReadStream's constructor and reset Javadoc still say the array "must extend at least 8 bytes past bytes"; the promise at the top of that file is now kept as written, so I left the file alone. That @param wording is now a performance recommendation rather than a requirement and can follow in its own change if wanted.

Tests

Two new tests in test/serialize/tests/StreamTests.java, both on the reported shape new ReadStream( packet, packet.length ):

  • a one-byte packet reads its eight bits and refuses the ninth;
  • an eleven-byte packet (3 + 32 + 17 + 32 + 4 bits) is read to its last bit, with windows starting inside the last eight bytes and on the last byte, then again from a copy with three bytes of slack (the band with some slack, but less than a window of it).

On the unfixed reader both throw: the one-byte read needs indices 0..7 of a one-byte array, and the eleven-byte packet's 17-bit read at byte 4 needs 4..11.

What was run, and what was not

Nothing was compiled or run here. This machine has no JDK: /usr/bin/java is the macOS stub, dist/jdk-21.0.12.1 is absent, and there is no /Library/Java, Homebrew, IntelliJ or Android Studio JDK, and no container runtime. So I have not run make test or make test-release, and I have not confirmed the two tests fail on main by execution; the failure on main follows from the index arithmetic above. CI's checked and release runs are the verification for this PR.

rowan-claude and others added 2 commits September 8, 2026 12:01
readBits always loaded an 8-byte little-endian window at the byte cursor,
and the only guard on the slack past the data was an assert, so a correct
packet in an exactly-sized byte[] threw ArrayIndexOutOfBoundsException on
its last fields in a release JVM. ReadStream promises hostile bytes never
throw, and its constructor forwards straight to reset.

reset now copies the final min(bytes, 8) bytes into a zero-padded 16-byte
tail whenever the array has fewer than 8 bytes of slack, and records the
byte index the tail is based at; otherwise tailBase is Integer.MAX_VALUE.
readBits selects the window source with one well-predicted branch and
loads a single window either way: no allocation per read, and an array
with slack never takes the tail. The padding is never interpreted, since
bits past the end of the data cannot reach the output of a read, so the
outputs match slack bytes exactly. This is the shape serialize.go uses.

Two stream tests cover the reported shape, new ReadStream(packet,
packet.length): a one-byte packet reading its eight bits, and an
eleven-byte packet read to its last bit with windows starting inside the
last eight bytes and on the last byte, plus the band with one to seven
bytes of slack. Both throw on the unfixed reader.

Fixes #9

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ReadStream's Javadoc still required eight bytes of slack, and BitReader's
header still said it had no scratch state; both now say what the code does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit 7b4db15 into main Sep 8, 2026
4 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.

BitReader throws ArrayIndexOutOfBoundsException on a tight packet-sized buffer

1 participant