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
1because the encoder splits every atomic unit — a header varint, afixlen_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-streambuffer_set():len(buffer) - offsetmust 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 reportsSofaBufferError, and a caller sizing from a generatedMAX_SIZEgets an exact fit.
- sofab.types.MIN_REASSEMBLY = 16¶
The smallest reassembly buffer a
sofab.Decoderaccepts.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
SofaArgumentErrorrather 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.reassemblyis 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:
IntEnumThe 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:
IntEnumThe 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 nextfeedcontinues 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 onsofab.Decoder.error.
There is deliberately no
finish/endstep that could reclassifyINCOMPLETEas an error (§5.2): the statusfeedreturned 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 asSofaLimitError, never asINVALID(§6.3).- COMPLETE = 0¶
- INCOMPLETE = 1¶
- INVALID = 2¶
- class sofab.types.FixlenSubtype(*values)[source]¶
Bases:
IntEnumThe 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:
objectDescribes the field the decoder is currently positioned on.
Mirrors the C field callback’s
(id, size, count)plus the wire type.sizeis the fixlen byte length (or the per-element size of a fixlen array);countis the element count of an array;subtypeis set for fixlen and fixlen-array fields.- Parameters:
id (int)
type (WireType)
size (int)
count (int)
subtype (FixlenSubtype | None)
- id: int¶
- size: int = 0¶
- count: int = 0¶
- subtype: FixlenSubtype | None = None¶
- exception sofab.types.SofaDecodeError[source]¶
Bases:
SofaErrorMalformed 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, soexcept SofaDecodeErrordoes not catch it.
- exception sofab.types.SofaIncompleteError[source]¶
Bases:
SofaErrorTruncated 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
SofaDecodeErrorunderSofaError, not a subclass of it, so callers can tell “need more bytes” apart from “these bytes are garbage”.
- exception sofab.types.SofaLimitError[source]¶
Bases:
SofaErrorA 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 isSofaArgumentError(§6.2.1, §6.3).It is therefore a sibling of
SofaDecodeErrorunderSofaError, not a subclass of it, soexcept SofaDecodeErrordoes 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 itssofab.Bindingentry, or by answeringsofab.Visitor.on_schema_bound()— and that bound governs instead, an over-bound value beingSofaDecodeError(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 fromsofab.Visitor.on_blob_begin()/sofab.Visitor.on_array_begin().
- exception sofab.types.SofaArgumentError[source]¶
Bases:
SofaErrorThe caller’s own request is invalid — the
InvalidArgumentoutcome of CORELIB_PLAN §6.3, which is the only code that taxonomy has for a caller mistake (every remaining malformed input isSofaDecodeError).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::Argumentin Rust,Error::InvalidArgumentin C++), so this one does too.SofaRangeErrorremains 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, onsofab.Decoderand on thesofab.collectorshelpers 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.SofaLimitErrorwould 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. Afloatis therefore rejected,3.0included; convert explicitly withint(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()orsofab.Visitor.on_array_begin()shorter than the size those hooks were told, or areassembly=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 existingexcept SofaRangeErrorandisinstancechecks keep working. It is the same class, not a subclass, so either name catches what the other raises.