Binary / wire format ==================== Модель записывается без имён полей, type tags, выравнивания и padding. Получателю заранее нужны класс модели и его настройки. Заголовок и payload ------------------- По умолчанию используются ``Constructor(2), Flags(8), PayloadLength(4)``: .. code-block:: text constructor (2 bytes) | flags (8 bytes) | body length (4 bytes) | fields Header занимает 14 байт, даже без необязательных полей. ``PayloadLength`` хранит число байтов всех полей после полного header. Header целиком исключён из этой длины. ``__header__`` позволяет изменить порядок, представление или убрать элементы, включая весь header — :doc:`headers`. ``__constructor__`` по умолчанию равен 1, ``__byte_order__`` — ``ByteOrder.BIG``. Constructor сверяется с выбранным классом; библиотека не назначает уникальные идентификаторы и не выбирает модель по этому числу. Точный пример ------------- .. testcode:: 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(" ")) .. testoutput:: 12 34 00 00 00 00 00 00 00 01 00 00 00 03 07 01 41 .. code-block:: text 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 не используется. .. testcode:: 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(" ")) .. testoutput:: 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. Поддерживаемые размеры префиксов и их пределы приведены в :doc:`field-configuration`. Размер поля в байтах равен размеру префикса плюс его значению. 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``. Списки и вложенные сообщения ---------------------------- .. code-block:: text 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. Пример точных байтов — :ref:`nested-headers`. .. _unknown-wire-data: Границы и неизвестные данные ---------------------------- При чтении decoder обрабатывает только объявленные элементы header. Если есть ``Constructor``, он проверяется (при обычном отдельном чтении). Если есть ``PayloadLength``, проверяется наличие объявленного body. В этом случае поля получают буфер, обрезанный по концу payload: встроенный codec не может дочитать поле из следующего сообщения. Вложенные модели ограничены payload родителя, элементы списка — его отдельным буфером. Повреждённые значения отклоняются согласно :doc:`errors`. Есть важное различие между двумя видами дополнительных байтов: * **Внутри объявленного 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; изменение байтов, которое остаётся корректным значением, само по себе не обнаруживается. Точные сигнатуры и настройки классов собраны в :doc:`api/index`.