Protocol and format API¶
Worked examples are available for How to use the IPv4 model, How to use the DNS model, How to build and decode PNG files, and How to decode and encode MessagePack values.
IP, TCP, and UDP¶
- class rustruct.protocols.IPv4(**kwargs)[source]¶
- classmethod build(*, source, destination, payload, protocol=None, options=b'', ttl=64, version=4, dscp=0, ecn=0, identification=0, reserved=False, df=False, mf=False, fragment_offset=0, checksum=0)[source]¶
Compute ihl/total_length/protocol, then build one ready-to-pack instance. protocol is inferred from a registered payload (e.g. TCP/UDP); it must be given explicitly when payload is raw bytes for an unregistered protocol number.
DNS¶
DNS message (RFC 1035, plus common extensions).
Domain-name compression is the one thing here that doesn’t fit rustruct’s declarative model: a pointer’s target is an absolute offset into the whole message, and writing one requires remembering every name suffix already emitted anywhere earlier in the message – state that outlives any single field or nested scope. So the record types that carry a domain name (Question, ResourceRecord, and the NS/CNAME/DNAME/SOA/PTR/MX/SRV/NAPTR/ RRSIG/NSEC/RP/HTTPS record types) are small hand-written classes operating on a shared bytearray + a dict context, calling write_name/ decode_name for each name and encode_labels to prepare one for the wire.
Everything else about those same record types – every fixed-width field or run of them, both before and after the domain name(s) – is still a real, nested rustruct.Struct (SOAFixed, SRVFixed, RRSIGFixed, and so on): there is no hand-rolled struct.pack() format string anywhere in this module, right up through the 12-byte message header itself (MessageHeader, nesting DNSFlags). Record types with no domain name at all (A/AAAA/DS/DNSKEY/TLSA/SSHFP/CAA/LOC) need no escape hatch whatsoever and are plain top-level Struct classes.
Domain names are plain strings (“example.com”; the root is “”; a trailing dot is accepted and dropped). Labels are transcoded byte-for-byte as latin-1. Compression pointers are followed on read (backwards only, so hostile messages cannot loop) and emitted on write whenever a name suffix was already written; pack with compress=False to disable that.
A handful of record types carry a domain name that RFC 4034/2782 forbid compressing (RRSIG’s signer, NSEC’s next-domain, SRV’s/HTTPS’s target): those call write_name(out, labels, None) regardless of the message’s own compress= setting, matching what real resolvers expect on the wire.
- class rustruct.protocols.dns.DNS(id: int = 0, flags: DNSFlags = <factory>, questions: list[Question] = <factory>, answers: list[ResourceRecord] = <factory>, authorities: list[ResourceRecord] = <factory>, additionals: list[ResourceRecord] = <factory>)[source]¶
A whole DNS message; the section counts are computed on pack().
- class rustruct.protocols.dns.DNSFlags(**kwargs)[source]¶
The second header word: QR, opcode, AA/TC/RD/RA, Z and RCODE.
- class rustruct.protocols.dns.Question(name: str, qtype: rustruct.protocols.dns.RRType = <RRType.A: 1>, qclass: rustruct.protocols.dns.DNSClass = <DNSClass.IN: 1>)[source]¶
- class rustruct.protocols.dns.ResourceRecord(name: str, rclass: DNSClass | int = DNSClass.IN, ttl: int = 0, data: object = None)[source]¶
rtype is never stored here: a known record type carries its own class-level RTYPE, and UnknownRData already carries the wire tag it was decoded with – storing a third, possibly-inconsistent copy on the record itself would just invite them drifting apart.
rclass is DNSClass | int, not just DNSClass: an EDNS0 OPT pseudo-record (see edns0()) repurposes this field to carry the requestor’s/responder’s UDP payload size instead of an actual class.
- class rustruct.protocols.dns.A(**kwargs)[source]¶
No domain name – a plain 4-byte address, fully declarative.
- class rustruct.protocols.dns.SOA(mname: str, rname: str, serial: int, refresh: int, retry: int, expire: int, minimum: int)[source]¶
PNG¶
- class rustruct.formats.image.png.IHDR(**kwargs)[source]¶
The mandatory, always-first chunk: dimensions and pixel format.
MessagePack¶
MessagePack (https://github.com/msgpack/msgpack/blob/master/spec.md).
MessagePack’s tag byte packs the type discriminant and, for several ranges (fixmap/fixarray/fixstr, positive/negative fixint), the value itself into the same bits: hi4 selects the range, lo4 is either a sub-tag or the count/value directly. Value decodes exactly one tag’s worth of wire content – for a container, that’s just the element count, not the elements.
A MessagePack value is recursive to unbounded depth, which a single rustruct schema cannot express (see docs/explanation/schema-model.md, “What a schema cannot express”). decode/encode supply that recursion in plain Python instead, walking an explicit stack rather than recursing, so neither risks RecursionError regardless of nesting depth.
Deliberately out of scope: ext types (ext8/16/32, fixext1-16) and float32 on encode (Value always encodes floats as float64; both widths decode on read).
- class rustruct.formats.msgpack.Value(**kwargs)[source]¶
One MessagePack tag’s worth of wire content – a container’s elements are not part of this schema (see the module docstring); decode/encode drive Value.unpack_from/Value(…).pack() in a loop to build/consume them.
- rustruct.formats.msgpack.decode(buf)[source]¶
Decode a buffer holding exactly one MessagePack value.
- rustruct.formats.msgpack.decode_from(buf, offset=0)[source]¶
Decode one MessagePack value starting at offset, returning (value, new_offset) – the streaming-friendly building block decode itself is written in terms of. Iterative: a work stack of in-progress containers stands in for the call stack a recursive-descent decoder would use, so nesting depth is bounded only by available memory, not by Python’s own recursion limit.
- rustruct.formats.msgpack.encode(value)[source]¶
Encode a Python value (None/bool/int/float/bytes/str/ list/dict, with dict keys and values themselves encodable) as MessagePack, choosing the most compact tag that fits – the same preference order the reference implementation uses, so the two interoperate byte-for-byte on the common cases (see tests/formats/test_msgpack.py).
Iterative, like decode_from: a preorder walk over an explicit work stack, so encoding a value does not risk RecursionError regardless of how deeply it nests.