Skip to content

Fix array payload encoding from sequences - #31

Merged
glopesdev merged 2 commits into
mainfrom
fix-payload-format-and-parse
Aug 20, 2026
Merged

Fix array payload encoding from sequences#31
glopesdev merged 2 commits into
mainfrom
fix-payload-format-and-parse

Conversation

@glopesdev

@glopesdev glopesdev commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

An array register mis-encoded a Python list or tuple. Its payload dtype is a sub-array, ('<u2', (2,)) for a two-element U16 register, and converting a sequence against that dtype makes numpy broadcast each element into the full shape, so [1, 2] would produce four elements and eight bytes, round-tripping to [1, 1], while the equivalent ndarray produced the correct two. A caller writing a list put wrong bytes on the wire with nothing raised.

Encoding a sequence

format and format_bulk both convert against the element type and let the declared shape decide how many elements are expected, so a list, a tuple and an ndarray agree, and on the bulk path a nested sequence and an element-typed array produce the same frames. A flat sequence of the declared length is a single frame.

Shapes that do not end in the declared payload shape raise instead of being interpreted, which covers a sequence of the wrong length and the ambiguous flat form, where four elements for a two-element register could be one frame or two. Nothing shielded the original defect, since format handles PayloadBase and ndarray before reaching the constructor and only the sequence path was affected.

Naming the register on a short buffer

parse handed the buffer straight to numpy, which reported buffer is smaller than requested size, naming neither the register, its declared payload type, nor the byte count. It now raises HarpParseError with all three, and parse_bulk does the same when the buffer holds less than one frame, in place of an out-of-bounds index against its strided view. The smallest frame on the wire becomes a single constant, which HarpMessage.parse reads as well.

The parse check is a lower bound rather than equality, deliberately. parse reads one item from the front, which is what count=1 means, and parse_bulk is its sibling for reading many items from one buffer. HarpMessage.decode already requires the payload to be exactly the declared size, so the strict form sits at the level that interprets a whole message. That also bounds who this affects. A message going through decode is rejected earlier with a better message, so the improvement is for callers reaching parse directly, which the wider bytes | bytearray | memoryview signature invites.

The trailing frame stays as it is

A partial trailing frame is still dropped without complaint. One path serves both an in-memory buffer and a file read from a path, and DatasetReader.read concatenates chunks as raw bytes, so raising would make a recording cut off mid-acquisition unreadable. Reporting a dropped tail belongs with a strict option, as its own change.

An array register now formats a sequence element-wise rather than
against the sub-array dtype, which broadcast each element into the full
shape. A list or tuple of the declared length produces the same bytes
as the equivalent ndarray, where before [1, 2] for a two-element U16
register would produce four elements. A sequence of the wrong length
raises instead of producing a longer payload.

parse also raises HarpParseError naming the register, its payload type
and the byte count when the buffer is too short to read one item,
replacing a numpy message that named neither side.
@glopesdev
glopesdev requested a review from bruno-f-cruz August 20, 2026 10:29
@glopesdev glopesdev added the fix Pull request that fixes an issue label Aug 20, 2026

@bruno-f-cruz bruno-f-cruz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! If I understand correctly this should not affect the bulk parsing/format path, but but worth a check.

format_bulk converts a sequence against the element type of an array
register and lets the declared shape decide the frame count, so a nested
list and an element-typed array produce the same frames, and a flat
sequence of the declared length produces one frame. A shape not ending
in the declared payload shape raises.

parse_bulk raises HarpParseError naming the register and the byte counts
when the buffer holds less than one frame. A partial trailing frame is
still dropped, so a truncated recording still loads.
@glopesdev

Copy link
Copy Markdown
Contributor Author

Checked, and you were right that the bulk path is unaffected. format_bulk converts the array itself instead of building a payload, so it never had the broadcasting problem, and a Batch cannot be constructed from a value at all, so nothing in the first commit could reach it.

It did leave an asymmetry though, with format accepting a sequence while format_bulk rejected one, and parse_bulk still reporting a numpy out-of-bounds index where parse now names the register. Rather than leave the two paths inconsistent I folded both into a second commit.

The partial trailing frame stays as it is. The same path serves a file read through DatasetReader.read, so raising would make a recording cut off mid-acquisition unreadable. We can do a strict option for that later.

@glopesdev
glopesdev requested a review from bruno-f-cruz August 20, 2026 22:08
@glopesdev
glopesdev merged commit 5991c3f into main Aug 20, 2026
13 checks passed
@glopesdev
glopesdev deleted the fix-payload-format-and-parse branch August 20, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull request that fixes an issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants