sofab.types module

Wire-format constants, enums, the Field descriptor, and errors.

These mirror the shared SofaBuffers definitions used by corelib-c-cpp, corelib-rs, corelib-go, corelib-java and corelib-cs so the Python runtime produces byte-identical output.

sofab.types.API_VERSION = 1

SofaBuffers API version (mirrors C SOFAB_API_VERSION). Callers and the code generator use this to verify wire compatibility.

sofab.types.ID_MAX = 2147483647

Highest valid field ID (INT32_MAX).

sofab.types.UNSIGNED_MAX = 18446744073709551615

Largest unsigned wire value (UINT64_MAX).

sofab.types.SIGNED_MIN = -9223372036854775808

Signed wire value range (INT64_MIN .. INT64_MAX).

sofab.types.FIXLEN_MAX = 2147483647

Largest fixlen payload length in bytes (INT32_MAX).

sofab.types.ARRAY_MAX = 2147483647

Largest array element count (INT32_MAX).

sofab.types.MAX_DEPTH = 255

Maximum nested-sequence depth. An encoder must not open more than this many nested sequences; a decoder rejects a message nesting deeper.

sofab.types.MIN_OUTPUT_BUFFER = 1

Smallest output buffer this port accepts for streaming (CORELIB_PLAN §5.1). It is 1 because the encoder splits every atomic unit — a header varint, a fixlen_word, an element count, a scalar, one float — at any byte boundary, so a one-byte scratch buffer already produces exactly the one-shot bytes. The declaration binds a buffer installed with a flush sink, at installation and at every mid-stream buffer_set(): len(buffer) - offset must be at least this. A buffer installed without a sink is subject to no minimum — no flush can occur, so it simply holds the message or reports SofaBufferError, and a caller sizing from a generated MAX_SIZE gets an exact fit.

sofab.types.MIN_REASSEMBLY = 16

The smallest reassembly buffer a sofab.Decoder accepts.

A construct split across fed chunks has to be joined somewhere, and CORELIB_PLAN §6.6.2 puts that somewhere in the caller’s hands: “A codec MUST NOT grow a private accumulator instead.” So the decoder holds one buffer, sized once at construction and never grown — a sender cannot make it bigger by sending different bytes, which is the property §6.6 protects — and a construct that does not fit it is refused with SofaArgumentError rather than accommodated.

There is deliberately no default size to go with this minimum. §6.2.1 says a codec “MUST NOT hold a limit of its own” and “MUST NOT supply a default for one it was not given”, and a reassembly size is exactly such a number: it decides which well-formed messages this receiver can stream. The library used to answer 4096 for every caller who did not ask, which made the number the corelib’s rather than the deployment’s — the same defect the three max_dyn_* caps had before #137. reassembly is now required, and generated code derives it from the schema.

The floor is what a single spanning construct’s framing needs: an id header and a length/count word are ten bytes each at the outside (§4.1). A buffer that cannot hold those is not a smaller buffer, it is a broken one.

sofab.types.MASK64 = 18446744073709551615

64-bit mask used by varint/zigzag wrap-around to match the C uint64_t.

class sofab.types.WireType(*values)[source]

Bases: IntEnum

The 3 low bits of a field header.

UNSIGNED = 0
SIGNED = 1
FIXLEN = 2
ARRAY_UNSIGNED = 3
ARRAY_SIGNED = 4
ARRAY_FIXLEN = 5
SEQUENCE_START = 6
SEQUENCE_END = 7
class sofab.types.Status(*values)[source]

Bases: IntEnum

The three-valued decode outcome CORELIB_PLAN §5.2 requires.

Returned by every sofab.Decoder.feed(), describing the bytes consumed so far — not a verdict on the message as a whole:

  • COMPLETE — the consumed bytes end exactly at a field boundary. A valid message may end here; more fields may also still follow.

  • INCOMPLETE — the bytes end inside a construct. This is not an error. The partial tail is retained and the next feed continues from it. Whether an incomplete message is acceptable is the caller’s decision: only its framing (a length prefix, a datagram boundary, EOF) knows whether more bytes can still come.

  • INVALID — the bytes are malformed regardless of what follows. Terminal; the reason is on sofab.Decoder.error.

There is deliberately no finish/end step that could reclassify INCOMPLETE as an error (§5.2): the status feed returned is the answer. A receiver-side limit rejection (§6.2.1) is not one of these three — it is a well-formed message the receiver declined, so it arrives on the error channel as SofaLimitError, never as INVALID (§6.3).

COMPLETE = 0
INCOMPLETE = 1
INVALID = 2
class sofab.types.FixlenSubtype(*values)[source]

Bases: IntEnum

The 3 low bits of a fixlen length header.

FP32 = 0
FP64 = 1
STRING = 2
BLOB = 3
class sofab.types.Field(id, type, size=0, count=0, subtype=None)[source]

Bases: object

Describes the field the decoder is currently positioned on.

Mirrors the C field callback’s (id, size, count) plus the wire type. size is the fixlen byte length (or the per-element size of a fixlen array); count is the element count of an array; subtype is set for fixlen and fixlen-array fields.

Parameters:
id: int
type: WireType
size: int = 0
count: int = 0
subtype: FixlenSubtype | None = None
exception sofab.types.SofaError[source]

Bases: Exception

Base class for all SofaBuffers errors.

exception sofab.types.SofaDecodeError[source]

Bases: SofaError

Malformed input — invalid regardless of what bytes might follow: an overflowing (>64-bit) varint, a bad fixlen subtype, an out-of-range id/count/length, invalid UTF-8, nesting past MAX_DEPTH, or a dangling sequence end (MESSAGE_SPEC §7 INVALID).

This is deliberately not raised for truncation — bytes that simply end inside a field are SofaIncompleteError (§7 INCOMPLETE), a distinct non-error outcome that is not a subclass of this class, so except SofaDecodeError does not catch it.

exception sofab.types.SofaIncompleteError[source]

Bases: SofaError

Truncated input — the bytes end inside a field (MESSAGE_SPEC §7 INCOMPLETE): an unterminated varint, a fixlen/array payload shorter than its declared length, an array element that runs off the end, or a nested sequence that is never closed.

This is not malformed: more bytes could complete the message, and the caller owns end-of-input. It is a sibling of SofaDecodeError under SofaError, not a subclass of it, so callers can tell “need more bytes” apart from “these bytes are garbage”.

exception sofab.types.SofaLimitError[source]

Bases: SofaError

A wire-declared array count or fixlen (string/blob) length exceeded a receiver-configured decode limit (Decoder(max_dyn_array_count=…, max_dyn_string_len=…, max_dyn_blob_len=…)).

This is a policy rejection, not wire malformation: the bytes are perfectly well-formed and would decode fine under a looser limit — the receiver simply declined to allocate for them. It is never what a decoder raises for a limit that was not stated at all: there is no limit to raise then, so an omitted max_dyn_* argument is SofaArgumentError (§6.2.1, §6.3).

It is therefore a sibling of SofaDecodeError under SofaError, not a subclass of it, so except SofaDecodeError does not catch it and differential fuzzing does not see a limit rejection as a conformance divergence from another engine.

It is raised only for a field the schema leaves unbounded: where the schema states a count:/maxlen: the handler says so — by declaring that bound on its sofab.Binding entry, or by answering sofab.Visitor.on_schema_bound() — and that bound governs instead, an over-bound value being SofaDecodeError (CORELIB_PLAN §6.2.1/§6.3, MESSAGE_SPEC §7.1). Nor is it raised for a field nothing materializes — a skipped one, or one read into storage the handler returned from sofab.Visitor.on_blob_begin() / sofab.Visitor.on_array_begin().

exception sofab.types.SofaArgumentError[source]

Bases: SofaError

The caller’s own request is invalid — the InvalidArgument outcome of CORELIB_PLAN §6.3, which is the only code that taxonomy has for a caller mistake (every remaining malformed input is SofaDecodeError).

Named after the code it carries. §6.3 lets a port “adapt casing and idiom”, and this class used to take that as far as SofaRangeError — which read narrower than the code is: a destination too short for what a hook was told is a caller mistake, not a value out of range. Every other port keeps the word (Error::Argument in Rust, Error::InvalidArgument in C++), so this one does too. SofaRangeError remains as an alias.

On construction it covers a receiver cap the caller did not state. §6.2.1 fixes the provenance of the three max_dyn_* numbers even where the codec performs the comparison: a codec “MUST NOT hold a limit of its own, MUST NOT supply a default for one it was not given, MUST NOT read an omitted argument as unlimited, and MUST NOT clamp to one”. So all three are required, on sofab.Decoder and on the sofab.collectors helpers alike, and omitting one is a mistake in the call rather than a property of the message or of the deployment — which is exactly what this code is for. SofaLimitError would say something untrue about it: it promises a limit to raise that was never configured.

On encode, a value (or id/count) is not writable: either it is outside the permitted range, or it is not an integer at all: integer fields accept whatever Python considers losslessly an integer (any object with __index__ — int, bool, IntEnum, NumPy integers), and refuse everything else rather than silently truncating it. A float is therefore rejected, 3.0 included; convert explicitly with int(x) if that is what you mean. The same code covers the encoder’s other invalid calls: getvalue() on a caller-owned fixed buffer, and a sequence end without a matching begin.

On decode it covers the caller’s own storage not fitting what the message announced — a destination that cannot hold what was announced: a buffer handed back from sofab.Visitor.on_blob_begin() or sofab.Visitor.on_array_begin() shorter than the size those hooks were told, or a reassembly= buffer too small for a construct spanning a chunk. The handler was given the count or length first and answered with storage that does not fit it, so the mistake is the caller’s; §6.6.3 has the codec refuse such a destination “rather than growing it”, and §6.2.1 forbids clamping into it.

That second case is the only ceiling left on a destination the caller supplies. A receiver’s configured max_dyn_* limit does not also apply to it: the limit exists to stop the sender dictating the receiver’s allocation, and a handler that returns a buffer has sized that buffer itself (SofaLimitError).

It is not raised when a field’s wire type merely contradicts the type a binding declares for it: that is MESSAGE_SPEC §7.3, which the decoder answers by skipping the field (see sofab.Decoder).

sofab.types.SofaRangeError

Deprecated alias for SofaArgumentError, kept so existing except SofaRangeError and isinstance checks keep working. It is the same class, not a subclass, so either name catches what the other raises.

exception sofab.types.SofaBufferError[source]

Bases: SofaError

A fixed encoder buffer filled up and no flush sink was provided.