sofab.decoder module

SofaBuffers push decoder (CORELIB_PLAN §5.2).

Bytes go in through Decoder.feed(), in chunks of any size, and each call returns the three-valued sofab.Status for the bytes so far. There is no finish/finalize step: an INCOMPLETE at end of input is truncation, and only the caller’s framing can say so (§5.2.4).

Fields come out through a caller-supplied handler — a sofab.Visitor, which §5.3.1 makes the decode surface, or a sofab.Binding, a table mapping field ids to slots in storage the caller owns, or both at once. There is no pull API: no next(), no cursor, no typed read a caller issues.

Hot-path model — “advance a cursor over a contiguous buffer” (protobuf’s trick). Incoming bytes are accumulated into a single contiguous buffer (self._buf) and parsed by advancing an integer cursor (self._pos) with direct indexing — no per-byte function call, no intermediate copies. When the cursor reaches the end mid-item the decoder transparently refills from the reader and continues, so the same code path serves both a fully-buffered message and a reader that dribbles one byte at a time. See _varint / _read_varints / _read_exact below.

Suspend and resume (CORELIB_PLAN §5.2). A reader that runs dry mid-field — a socket with nothing buffered yet — makes the call raise SofaIncompleteError. That is a first-class outcome, not an error, so the call consumes nothing: the cursor goes back to where it started, the bytes already read stay buffered, and re-issuing the same call once more bytes have arrived parses the field from its first byte. See the “resume transactions” section in Decoder.

Typical use:

class Sink(sofab.Visitor):
    def on_unsigned(self, field_id, value):
        ...

dec = Decoder(visitor=Sink())
for chunk in stream:
    status = dec.feed(chunk)
class sofab.decoder.Decoder(*, binding=None, visitor=None, words=None, objects=None, max_dyn_array_count=None, max_dyn_string_len=None, max_dyn_blob_len=None, reassembly=None)[source]

Bases: object

Decodes a SofaBuffers stream, pushing each field at a handler.

Construct it with a visitor, a binding, or both, then hand it bytes with feed(). Where both are given the binding takes every field it names and the visitor gets the rest.

A declared type that contradicts the field on the wire is not an error (MESSAGE_SPEC §7.3, CORELIB_PLAN §6.3). A binding entry whose wire type or subtype does not match the field is skipped: its destination is left untouched and the decode stays COMPLETE. Nothing is raised, nothing is materialized, nothing is validated — and the fallback visitor is not offered it either, because a skipped field is skipped for the whole handler.

Parameters:
  • binding (Binding | None)

  • visitor (Visitor | None)

  • words (Any)

  • objects (list[Any] | None)

  • max_dyn_array_count (int | None)

  • max_dyn_string_len (int | None)

  • max_dyn_blob_len (int | None)

  • reassembly (Any)

property error: SofaError | None

The failure that made feed() return Status.INVALID, or None. Mirrors sofab.Encoder.error: the status feed() returned is the answer, this is the reason behind it.

There is deliberately no status counterpart. The outcome is what feed() hands back and nothing repeats it: a second way to ask the same question is a second thing to keep in step, and this port shipped that drift once already.

feed(data)[source]

Consume data and report the outcome for the bytes so far (§5.2).

Accepts anything with a buffer — bytes, bytearray, a memoryview over either. The chunk is borrowed only for the duration of this call (§6, chunk lifetime): whatever the decoder still needs afterwards — a construct split across the boundary, a decoded string or blob — is copied out before it returns, so the caller may reuse or overwrite that memory the moment feed comes back.

Returns Status.COMPLETE, Status.INCOMPLETE or Status.INVALID. There is deliberately no finish/end counterpart: an INCOMPLETE at end-of-input is truncation, and only the caller’s framing can say so. INVALID is terminal — every later feed returns it again without consuming anything, and the reason stays on error.

A receiver-side limit rejection (§6.2.1) is not one of the three outcomes: the message is well-formed and the receiver simply declined it, so it is raised as SofaLimitError rather than folded into INVALID (§6.3).

Parameters:

data (Any)

Return type:

Status

reset()[source]

Forget the stream and start a new message, keeping the handler and its destinations. Lets one decoder serve many messages without rebuilding the binding — the destinations are the caller’s to clear (or not: a slot the next message does not write keeps whatever is in it, which is how absence is reported).

Return type:

None