[О блоге] [наверх] [пред] [2024-09-25 21:48:58+03:00] [54996a124bd917fbe7a000bfd578030401ab40f2]
Темы: [yac]

Разработал собственный формат сериализации

А то что-то их маловато, конечно же. Вообще всё началось с вопроса:
какой формат есть пригодный для криптографии (где детерминированное
кодирование данных бы было), при этом как можно более простой в плане
реализации, при этом бы ещё и довольно компактный по получающемуся
бинарю. И сразу откидываем форматы требующие схему. Я не против них,
protobuf мне нравился, они компакты, быстры и эффективны, но это тот ещё
геморрой работать с ними в действительно гетерогенных средах, где не всё
под твоим контролем, когда не всё касается твоей экосистемы. Ну и
хочется чтобы им можно было бы заменить, грубо говоря, JSON -- то есть
достаточное количество типов данных бы поддерживал.

Изначально я смотрел на мои любимые Netstring-и. Думал из серии: пускай
это будут только NS-ы, вложенные друг в друга, пускай где-то
интерпретируемые как пары из ключ-значение, и всё в таком духе. Да, сам
кодек будет простой, но после него ещё нужно делать постообработку, в
которой по сути тоже будет вшиваться схема что дальше надо и как
декодировать.

Смотрел на bencode, который тоже использовали на работе, прелесть
которого в детерминированном кодировании (если не забывать про
дополнительное требование к integer-ам). Большим минусом bencode точно
можно назвать неотличимость человекочитаемых строк от бинарных.
Насколько понял, это же было основной причиной создания fork-а от
MessagePack в виде CBOR. Если мы хотим без схемы декодировать
произвольные данные, то да, декодировать то сможем, но всё же
переваривать это глазами человеку уже будет проблематично. Различие
между UTF-8 и чисто бинарями обязано быть.

И мы с коллегой и CBOR формат строго также не хотим иметь дело с ASCII
decimal значениями, которые в NS/bencode для кодирования длины. Они
несут определённое удобство, безусловно, но всё же в условиях написания
кода для смарт-карт каких-нибудь -- это слишком лишний код и не очень
компактное использование места, особенно учитывая частоту использования
строчек. В криптографических форматах например вообще нет integer-ов
(serial в X.509 сертификатах -- анахронизм от которого надо бы
избавляться).

Поэтому начали думать в сторону замены этих decimal на бинарные
представления. Не один день ломал я голову и переделывал то так, то сяк.
И только после этого снова вчитался в CBOR. Изначально я его отбросил
из-за его заметки в самом начале RFC, о том что он прям явно не
собирается блюсти детерминированное кодирование данных, что нас не
устраивает. Потом я всё же увидел раздел про Canonical CBOR, как-раз
решающий эту проблему. В общем всё это время мы почти изобретали CBOR.

Прежде я к нему надменно относился -- мол, очередной хипстерский формат.
Но поменял своё мнение -- я сам пришёл к очень и очень похожему формату.
Даже подумал чтобы просто использовать (Canonical) CBOR. Но... нет. В
нём есть опциональные (многие кодеки не реализуют их поддержку) тэги
прикрепляемые к данным. Выглядит интересно, но нам точно не хотелось бы
с этим иметь дела. Это избыточно в данном кодеке. Но это мелочь. А что
серьёзно не понравилось: именно Canonical CBOR не позволяет потоково
формировать объекты. Он как ASN.1 DER -- везде должна быть заранее
известна длина. Я же хочу как бы аналог ASN.1 CER, где по сути
SEQUENCE*/SET* имеют indefinite length, плюс все бинари представлены в
виде indefinite length, после которой идут килобайтные фиксированные
кусочки данных, где только последний может быть меньшего размера,
закрывающим. В Canonical CBOR потоковости нет. Плюс в моём получившемся
кодеке может выходить более компактное представление integer-ов и строк
недлинных. В моём кодеке я явно поддерживаю как просто строки, так и
потоковые blob-ы. Если потоковость не нужна, то можно использовать
непрерывные бинари как и прежде.

Понравилось, что и я и CBOR отметают variable length кодирование чисел,
где высшим битом показывается есть ли у него продолжение. Это ужасно
неудобно декодировать. И это более сложный код. В моём кодеке код по
идее должен быть не сложнее, а скорее проще из-за отсутствия тэгов.

Преобладающая часть данных в нём, для аналога всяких X.509/PKCS#7 нужд
кодируется вообще без затрат на байты отводимые под длину. Он ощутимо
компактнее ASN.1 DER/CER. Его реализацию кодирования/декодирования с
валидацией я за пару часов на Python реализовывал. По сути он компактнее
CBOR, должен быть чуть проще в реализации. Его спецификация сильно
проще: ибо по сути есть ровно одна таблица расписывающая все возможные
значения первого байта и как дальше после них парсить данные, почти
всегда известной (по значению типа данных) длины. Исключение, как и в
CBOR -- строки, которые у меня можно до 59-байт без дополнительных
байтов на длину закодировать, тогда как в CBOR всего-лишь 20 с чем-то.

Я добавил (ну по сути забронировал значения для типов данных) и
128-битные integer и float256, ибо для них уже есть спецификации. Чисто
на будущее.

В отличии от CBOR, JSON, MessagePack и кучи других -- я добавил
TAI64(N(A)) поддержку в качестве datetime базового типа. Регулярно при
использовании любых форматов сериализации -- приходилось добавлять
поддержку datetime для удобства. Я убеждён, что TAI64 формат (ну и его
TAI64N и, чисто для галочки, TAI64NA) более правильным для хранения
времени. Преобразовать из него в UTC -- легко. Зато не надо парсить, в
отличии от каких-нибудь ISO-строк, которые ещё и существенно длиннее.

Мне казалось, что, в принципе, это не сложное дело спроектировать
бинарный формат-аналог JSON (очень грубо говоря). Но если хочется и
компактности и эффективности и простоты, то оказалось не раз приходилось
и так и сяк его переделывать.

Но на мой взгляд, вышло очень здорово. Настолько здорово, что даже если
начальство не одобрит его применение вместо ебучего говёного уродского
мерзкого ASN.1 DER (тем более BER), то я всё-равно других задач буду
использовать. Изначально предполагалось только для криптографических
задач. Например не предполагались null/nil/none значения. Как и float.
Потом добавил из-за тривиальности. А потом я вижу, что из-за
компактности представления даже integer-ов, его вполне себе вообще для
чего угодно произвольного использовать. Хотя прежде надо реализовать на
Си, но я уже видел как это не сложно делают для CBOR-а.

Я даже добавлял такие отдельные типы данных как UUID и IPv4/IPv6 адреса.
Ибо они фиксированного размера. Но потом всё же решил убрать.
Кодирование любого из них тоже будет занимать всего 1-байт избыточности.
По сути это выходит как просто подсказка как интерпретировать эти
бинарные строки, для чего тэги в CBOR могут использоваться (сказать что
это URI, а не просто так строчка, например). То есть остались базовые
для всяких языков программирования типы, плюс TAI64* (который по сути
тоже же ведь бинарная строка фиксированной длины, но datetime уж очень
часто нужен много где).

Но из-за отсутствия схемы, в его структурах уже придётся помнить о
длинах ключей в словарях. Один коллега как-то говорил, что когда
начнутся однобуквенные ключи в словарях, то это уже пройденная граница
адекватности. С одной стороны -- верно. Но с другой вот есть словарик:
    "sig": {"algo" "gost3410-256A", "v": b"..."}
ведь по контексту тут точно понятно что "v" это value, значение подписи.
Вместо OID-ов мы все однозначно за использование человекочитаемых строк.
Даже просто использование "id-tc26-..." названий OID-ов уже было бы куда
лучше, пускай и немного поболее места займёт. Вместо "notBefore" я бы
использовал "since", вместо "notAfter" -- "till". Коротко и ясно.

Кроме длин ключей ещё придётся помнить про то, что они при кодировании
должны быть отсортированы. Поэтому если надо, чтобы в потоке данных
что-то шло раньше из словаря, то надо имя ключа делать лексикографически
"меньшим". Но это уж сущие мелочи, по сравнению с удобством от
отсутствия схем.

    [оставить комментарий]