В предыдущей части были описаны подходы, примененные при написании парсера для схемы MTProto. Статья получилась чуть более общей, чем я рассчитывал, на этот раз я постараюсь рассказать больше про специфику Telegram. Клиент на Go продолжает развиваться, а мы вернемся в прошлое и вспомним, как писался для него сериализатор и десериализатор протокола. Есть два пути десериализации: поточный и работающий с буфером. На практике в MTProto сообщение больше мегабайта передать не получится, поэтому я выбрал вариант с буфером: допустим, что мы всегда сможем держать полное сообщение в памяти. Получится вот такая структура: И еще, MTProto в основном выравнивает значения по 4 байтам (32 битам), вынесем это в константу: Зная, что почти всё в MTProto little-endian, мы можем начать с сериализации uint32: Все остальные значения мы будем сериализовать аналогично: сначала аллоцировали слайс (компилятор Go достаточно умный, чтобы в этом случае не помещать его в heap, так как размер слайса маленький и константа), потом записали туда значение, а затем дописали слайс в буфер.  Основная сложность заключается в том, чтобы аккуратно обработать все типы, так как подробных примеров в документации нет. Приходится искать примеры по тестам у других клиентов, например grammers, замечательного клиента на Rust для Telegram. Возможно, не стоило использовать одну и ту же структуру для десериализации и сериализации, но на практике это оказалось удобным, а сам пакет gotd/td/bin получился довольно компактным. Продолжим пример с uint32: Сначала мы проверяем, что в буфере достаточно данных, а иначе возвращаем ошибку io.ErrUnexpectedEOF. Затем вычитываем значение и сдвигаем буфер на длину этого значения. Этот подход тоже будет общим для десериализации. Строки ([]byte и string) сериализуются по-разному в зависимости от их длины и выравниваются по 4 байтам. Если длина меньше или равна 253, то она пишется в первый байт, а затем строка: Если длина больше, то первым байтом будет 254, потом три байта длины в little-endian, а затем уже строка: Сигнатура функции encodeString(b []byte, v string) []byte позволяет просто дописывать в b, не аллоцируя каждый раз новый слайс: Кстати, в своем десериализаторе строк я нашел несколько багов с помощью фаззинга. Про него я писал в первой части, повторяться не буду. Мы базово разобрали, как происходит работа со значениями, но нам нужно сериализировать сложные типы. Делается это довольно просто: сначала пишется ID типа (это те самые #5b38c6c1 из схемы, просто как uint32), а затем все его поля по порядку, как в схеме.  Например, у нас есть такая структура (это упрощенный выдуманный пример из теста для сериализатора): И сериализовать её c помощью нашего Buffer довольно просто: Если попробовать вызвать Encode, в буфере будет следующее содержимое: Для десериализации мы должны сначала проверить, что у нас действительно нужный тип. Лучше это делать без изменения Buf, а значит нам потребуется новый метод: Я опущу тело метода ConsumeID(id uint32) для краткости: он вызывает PeekID и возвращет ошибку на неожиданный тип, сдвигая буфер только в случае успеха. С помощью него получится вот такая десериализация: Теперь мы можем аналогично написать (де-)сериализатор для любого типа из схемы, имплементируя следующие интерфейсы: Некоторые нюансы бинарного протокола заслуживают отдельного рассмотрения, так как могут быть не очевидны из документации. Называются векторами. Например: Тут понятно, как сериализовать title, но что делать с users? Непосредственно Vector определен вот так: Но что это значит в плане бинарного представления с первого взгляда не совсем понятно. Да и со второго тоже, так как понять из схемы это невозможно, а описано как специальный случай. На практике вектор сериализуется следующим образом: сначала пишется тип вектора (0x1cb5c415), затем количество элементов, а потом уже сами элементы подряд: Значит, для массива из 10 чисел uint32, мы вызовем PutVectorHeader(10), а затем запишем 10 раз uint32. С ними примерно так же, как и с векторами, но чуть проще: Таким образом, тобы записать Bool, нужно написать либо 0x997275b5, либо 0xbc799737: Для булевых значений есть специальный случай, они могут передаваться битовой маской, если являются частью флагов, о них чуть позже. Схема поддерживает возможность не передавать значение некоторых полей, если они не нужны, то есть сделать поля опциональными. Это похоже на битовое поле: если соответствующий бит задан, то поле (де-)сериализируется, если нет, то пропускается. Особым случаем опционального поля являются булевые значения (flags.0?true): они целиком кодируются одним битом в маске, непосредственная сериализация 0x997275b5 не используется, так как избыточна. Но есть нюанс! Если поле определено как flags.0?Bool, то оно сериализуется как обычный Bool, значит есть вариант опционального булевого поля, которое избыточно сериализуется. Видимо, это legacy. Представить такой bitfield в Go можно довольно просто: Это поле сериализуется как обычный uint32. А непосредственное использование будет таким: Видно, что TTLSeconds пишется только если задан флаг 1, а Escape полностью кодируется в Flags. Еще одим специальным случаем будут типы int128 и int256: В go их можно представить как массивы: Таким образом, сериализуются они очень просто: Самым сложным было понять, как конвертировать значения между ними и big.Int. MTProto использует big-endian для этих значений по причине аналогичного поведения в OpenSSL. В Go big.Int поступает так же. Получается, можно просто использовать SetBytes для чтения и FillBytes для записи: Пакет bin теперь готов, и появилась возможность сериализовать любой тип. Проблема в том, что в схеме очень, очень много типов и писать их вручную будет крайне накладно. Эта проблема решается генерацией кода (де-)сериализации из схемы (вот для этого мы и писали парсер!). Возможно, я уделю генератору отдельную часть в серии статей. Этот модуль проекта получился сложным, переписывался несколько раз и я бы хотел хоть немного облегчить жизнь людям, которые будут писать кодогенераторы на Go для других форматов. Для справки, на текущий момент генерируется около 180K SLOC из схем телеграма (api, mtproto, секретные чаты). Хочу поблагодарить tdakkota и zweihander за их неоценимый вклад в развитие проекта! Без вас было бы очень тяжело. Руковожу разработкой