Binary / wire format

Модель записывается без имён полей, type tags, выравнивания и padding. Получателю заранее нужны класс модели и его настройки.

Заголовок и payload

По умолчанию используются Constructor(2), Flags(8), PayloadLength(4):

constructor (2 bytes) | flags (8 bytes) | body length (4 bytes) | fields

Header занимает 14 байт, даже без необязательных полей. PayloadLength хранит число байтов всех полей после полного header. Header целиком исключён из этой длины. __header__ позволяет изменить порядок, представление или убрать элементы, включая весь header — Header и framing.

__constructor__ по умолчанию равен 1, __byte_order__ByteOrder.BIG. Constructor сверяется с выбранным классом; библиотека не назначает уникальные идентификаторы и не выбирает модель по этому числу.

Точный пример

from bytespec import ProtoModel, field
from bytespec.types import UInt8

class Packet(ProtoModel):
    __constructor__ = 0x1234
    number: UInt8
    note: str | None = field(flag=0, prefix_length=1)

encoded = Packet(number=7, note="A").encode()
print(encoded.hex(" "))
12 34 00 00 00 00 00 00 00 01 00 00 00 03 07 01 41
12 34                    constructor = 0x1234
00 00 00 00 00 00 00 01   flags: bit 0 set
00 00 00 03              payload length = 3
07                       number = 7
01 41                    note: byte length = 1, UTF-8 "A"

С note=None бит был бы сброшен, байты 01 41 отсутствовали бы, а длина payload равнялась бы 1. Порядок полей определяется индексами, номер flag его не меняет. Ненулевые defaults кодируются как обычные значения; отдельных маркеров default в формате нет.

Порядок байтов

ByteOrder.BIG — big-endian, ByteOrder.LITTLE — little-endian. Настройка распространяется на фиксированные целые и float, числовые части заголовка и фиксированные length prefixes. Native alignment не используется.

from bytespec import ByteOrder
from bytespec.types import UInt16

class Point(ProtoModel):
    __byte_order__ = ByteOrder.LITTLE
    x: UInt16

print(Point(x=0x1234).encode()[-2:].hex(" "))
34 12

Для varint порядок байтов фиксирован алгоритмом и не зависит от модели. Содержимое bytes и UUID.bytes не меняется; текст определяется encoding. Вложенная модель всегда использует собственную настройку __byte_order__.

Для UTF-16 порядок байтов текста задаётся самой кодировкой: выбирайте utf-16-le или utf-16-be явно, если протокол требует конкретный вариант.

Значения внутри payload

Фиксированные целые занимают 1, 2, 4 или 8 байт. Знаковые числа используют дополнительный код. Float записывается как IEEE 754 binary32/binary64. Bool — один байт 00 или 01.

str содержит префикс длины закодированного текста, затем текст. bytes содержит длину и исходные байты. FixedBytesCodec(N) и UUID префикса не имеют. Datetime использует представление строки ISO, строковый enum — представление своего .value. IntEnum использует VarInt через EnumCodec; обычный int тоже использует VarInt. date и time не имеют автоматического codec.

Поддерживаемые размеры префиксов и их пределы приведены в Строки, длины и настройки полей. Размер поля в байтах равен размеру префикса плюс его значению.

VarUInt и VarInt

VarUInt записывает группы по 7 бит, начиная с младшей. Старший бит байта означает продолжение. Ноль записывается как 00, 127 — 7f, 128 — 80 01, 300 — ac 02. Допускаются только канонические записи диапазона 0 .. 2**64 - 1: не более 10 байт, в десятом байте максимум один значащий бит и нет продолжения. Лишняя нулевая старшая группа запрещена.

VarInt сначала применяет ZigZag: n >= 0 превращается в 2*n, отрицательное n — в 2*abs(n)-1. Затем используется VarUInt. Таким образом, 0 00, -1 01, 1 02, -2 03. Поддерживается диапазон -2**63 .. 2**63 - 1.

Списки и вложенные сообщения

list[T]:
+------------------------+----------+----------+-----+
| byte length of items   | item 0   | item 1   | ... |
+------------------------+----------+----------+-----+

model field / model list item (default header):
+-------+----------------+-------------+
| flags | payload length | own fields  |
+-------+----------------+-------------+

Список не хранит число элементов. Decoder выделяет буфер указанной длины и читает значения, пока не исчерпает его. Строки и bytes внутри списка сохраняют свои префиксы. Модель сохраняет собственный header, пропуская Constructor, и использует свой порядок байтов. Оставшийся header входит в общую длину списка. При __header__ = () модель состоит только из полей; границы определяются их codecs. Пример точных байтов — Как записывается header вложенной модели.

Границы и неизвестные данные

При чтении decoder обрабатывает только объявленные элементы header. Если есть Constructor, он проверяется (при обычном отдельном чтении). Если есть PayloadLength, проверяется наличие объявленного body. В этом случае поля получают буфер, обрезанный по концу payload: встроенный codec не может дочитать поле из следующего сообщения. Вложенные модели ограничены payload родителя, элементы списка — его отдельным буфером. Повреждённые значения отклоняются согласно Обработка ошибок.

Есть важное различие между двумя видами дополнительных байтов:

  • Внутри объявленного payload: после всех известных полей остаток пропускается. Неизвестные биты flags тоже игнорируются. Пропущенные байты и биты не сохраняются в экземпляре и исчезают после повторного encode().

  • После объявленного payload: decode() выдаёт DecodeError. decode_from() возвращает конец текущего сообщения, оставляя следующие байты вызывающему коду.

Без PayloadLength конец сообщения совпадает с концом известных полей: неизвестный хвост не пропускается. decode_from() не может отделить доступные байты следующего сообщения от текущего body, поэтому внешние границы нужно ограничивать срезом.

Это не произвольная совместимость разных схем. Добавление данных в середину payload сдвигает последующие известные поля: у них нет индивидуальных тегов. Отсутствующее новое обязательное поле не восстанавливается через default. Меняя схему, проверяйте чтение в обе стороны.

Проверка границ не заменяет лимиты ресурсов: в текущем API нет max_payload_size, max_string_size, max_list_elements или max_depth. При работе с внешними сообщениями размер входа и нужные приложению ограничения задаются вне библиотеки. В формате нет checksum; изменение байтов, которое остаётся корректным значением, само по себе не обнаруживается.

Точные сигнатуры и настройки классов собраны в API Reference.