Private KeeperPrivate Keeper
in-Line Kit/Функции in-Line Kit

Сжатие и двоичные данные

UTF-8, массивы байтов, ZLIB, GZIP и Protobuf с обратимыми примерами и точными границами каждого представления.

Здесь решаются три разные задачи: перевод текста в байты, сжатие текста и создание двоичного сообщения Protobuf. Не смешивайте их: массив чисел, Base64 и Protobuf могут описывать одни и те же байты, но принимаются разными функциями. Все вызовы используют общую запись функций in-Line.

Base64 не сжимает и не защищает данные

Base64 только записывает двоичные данные печатными знаками. Размер такой записи обычно больше исходного массива байтов; восстановить её можно без ключа.

Что выбрать

Текст ↔ массив байтов

STRTOBYTES возвращает байты UTF-8 как массив десятичных чисел JSON, а BYTESTOSTR выполняет обратное преобразование.

Сжать текст для своей стороны

Выберите согласованную пару ZLIB или GZIP. Обе пары возвращают Base64, но их двоичные обёртки несовместимы друг с другом.

Обмен по известной схеме

PROTOBUF создаёт и разбирает сообщение по описанию полей. Описание должно точно повторять номера и типы полей другой стороны.

Только сменить запись байтов

Для перехода между Base64 и шестнадцатеричной строкой используйте функции из раздела Кодирование и форматы.

Текст, байты и их запись

Функции сначала получают строку программы, а затем сами выбирают, как представить её байты. На примере слова Привет цепочка выглядит так:

Текст

Привет — шесть знаков Unicode. Сам по себе текст ещё не определяет последовательность байтов без выбранной кодировки.

Байты UTF-8

D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82 — двенадцать байтов того же текста в шестнадцатеричной записи.

Массив JSON

[208,159,209,128,208,184,208,178,208,181,209,130] — те же байты как десятичные числа, которые возвращает STRTOBYTES.

ZLIB и GZIP также начинают с байтов UTF-8, но после сжатия записывают двоичный результат в Base64. PROTOBUF строит байты по номерам и типам полей, поэтому одного исходного текста для него недостаточно.

STRTOBYTES и BYTESTOSTR

Эти функции образуют обратимую пару, если массив не менялся и содержит допустимый текст UTF-8.

#beginScript
|DV|[Utf8Bytes]=(|STRTOBYTES|Привет|STRTOBYTES|)
|DV|[Utf8Text]=(|BYTESTOSTR||DV|[Utf8Bytes]|BYTESTOSTR|)
#endScript

После выполнения:

|DV|[Utf8Bytes] = [208,159,209,128,208,184,208,178,208,181,209,130]
|DV|[Utf8Text]  = Привет

STRTOBYTES

Принимает обычный текст, выполняет вложенные значения, преобразует результат в UTF-8 и возвращает однострочный массив JSON.

(|STRTOBYTES|hello|STRTOBYTES|)

Результат:

[104,101,108,108,111]

Числа записаны в десятичном виде и всегда обозначают байты от 0 до 255.

BYTESTOSTR не предназначена для произвольного файла

Случайный набор байтов может не быть допустимым UTF-8 и при чтении потерять исходное значение. Для надёжной обратимости передавайте в BYTESTOSTR результат STRTOBYTES либо заранее известный массив UTF-8; двоичный файл храните в Base64 или шестнадцатеричной форме.

Некорректный JSON, значение вместо массива или нечисловой элемент вызывают ошибку выполнения. Не передавайте дроби и числа вне диапазона байта: это не входит в договор функции.

ZLIB и GZIP

Обе пары выполняют одну цепочку: текст → UTF-8 → сжатые байты → Base64. При восстановлении шаги идут в обратном порядке. Входом функций распаковки служит Base64, а результатом — текст UTF-8.

ZLIBCOMPRESS ↔ ZLIBDECOMPRESS

Использует поток ZLIB: заголовок ZLIB, данные DEFLATE и контрольную сумму Adler-32. Подходит только стороне, которая ожидает именно такую обёртку.

GZIPCOMPRESS ↔ GZIPDECOMPRESS

Использует поток GZIP: заголовок GZIP, данные DEFLATE и собственную завершающую часть. Это не архив с несколькими файлами и именами, а один сжатый поток.

Обратимая пара ZLIB

#beginScript
|DV|[Source]={"event":"heartbeat"}
|DV|[ZlibPacked]=(|ZLIBCOMPRESS||DV|[Source]|ZLIBCOMPRESS|)
|DV|[ZlibRestored]=(|ZLIBDECOMPRESS||DV|[ZlibPacked]|ZLIBDECOMPRESS|)
#endScript

|DV|[ZlibRestored] будет равно {"event":"heartbeat"}. При текущей реализации |DV|[ZlibPacked] имеет вид eJyrVkotS80rUbJSykhNLCpJSk0sUaoFAFHbB40=, но проверяйте восстановленный текст, а не точное совпадение сжатой строки: допустимый поток может отличаться после смены средства сжатия или его настроек.

Устройство потока определено в описании ZLIB.

Обратимая пара GZIP

#beginScript
|DV|[Source]={"event":"sync"}
|DV|[GzipPacked]=(|GZIPCOMPRESS||DV|[Source]|GZIPCOMPRESS|)
|DV|[GzipRestored]=(|GZIPDECOMPRESS||DV|[GzipPacked]|GZIPDECOMPRESS|)
#endScript

|DV|[GzipRestored] будет равно {"event":"sync"}. Один из допустимых результатов сжатия — H4sIAAAAAAAAAKtWSi1LzStRslIqrsxLVqoFADsT6ZcQAAAA; заголовок GZIP допускает служебные поля, поэтому точная строка также не является устойчивой проверкой.

Устройство потока определено в описании GZIP.

Пары нельзя перекрещивать

Результат ZLIBCOMPRESS передавайте только в ZLIBDECOMPRESS, а результат GZIPCOMPRESS — только в GZIPDECOMPRESS. Внутри обеих обёрток применяется DEFLATE, но заголовки и контрольные суммы различаются.

Пустой ввод и ошибки

  • Пустая строка на входе любой из четырёх функций сразу даёт пустую строку. Это особое поведение, а не Base64 сжатого пустого потока.
  • Функции сжатия принимают текст, а не произвольный массив байтов. Для файла или уже готового двоичного тела отдельного входа здесь нет.
  • Неверный Base64, чужая обёртка или повреждённый поток не останавливают исполнение исключением: функция возвращает текст вида Error: <описание>. Такой результат нельзя передавать дальше как успешно восстановленные данные.

PROTOBUF

PROTOBUF строит или разбирает одно сообщение по описанию полей в JSON. Функция не читает .proto, не получает схему из сообщения и не угадывает типы: номер и тип каждого нужного поля задаются при каждом вызове.

action: encode

Создаёт новое сообщение из field_values и возвращает его байты в Base64.

action: decode

Разбирает protobuf_base64 по описанию полей и возвращает объект JSON одной строкой.

action: update

Разбирает имеющееся сообщение, добавляет поля из field_values и возвращает новый Base64. Это добавление записей, а не надёжная замена прежних значений.

Договор входного объекта

{
  "action": "encode | decode | update",
  "message_type": "AuthRequest",
  "field_mapping": {
    "AuthRequest": {
      "login": { "number": 1, "type": "string" },
      "password": { "number": 2, "type": "string" }
    }
  },
  "field_values": {
    "login": "Arthas",
    "password": "Scourge"
  },
  "protobuf_base64": "CgZBcnRoYXMSB1Njb3VyZ2U="
}
action

Обязательная строка encode, decode или update. Регистр значения не важен, потому что исполнитель приводит его к нижнему регистру.

message_type

Обязательное имя сообщения. Оно выбирает одноимённый объект внутри field_mapping, но само не записывается в двоичный результат.

field_mapping

Обязательный объект для всех действий. Сначала содержит имя сообщения, затем имена полей; у каждого поля обязательны number и type.

field_values

Обязательный объект для encode и update. Ключи должны существовать в выбранном описании полей; отсутствующие ключи просто не записываются.

protobuf_base64

Обязательная непустая строка для decode и update. Это Base64 ровно одного двоичного сообщения Protobuf.

fields и values

Поле типа message получает вложенный объект fields; поле типа enum — объект values, который сопоставляет названия перечисления с целыми числами.

Свойство name внутри описания поля не используется: именем служит ключ login, password и так далее. Лишнее name не заменяет правильный ключ и не исправляет неверный number.

Поддерживаемые типы

int32, int64

Знаковые целые числа. В field_values передаются числом JSON; при разборе возвращаются числом JSON.

uint32, uint64

Беззнаковые целые числа. Вход — число JSON; разобранный uint64 возвращается строкой, чтобы не переполнить знаковое целое программы.

bool

Принимает true, false, 1 или 0. Любое числовое значение, кроме 1, будет прочитано как false, поэтому используйте только эти четыре явных значения.

float, double

Числа с плавающей точкой одинарной и двойной точности. Вход и результат представлены числами JSON.

string

Обычная строка, которая записывается и читается как UTF-8.

bytes

Двоичное поле. Значение в field_values и результат decode представлены строкой Base64, а не массивом чисел.

enum

Перечисление с объектом values. При создании принимает известное название или число; при разборе возвращает название, если число найдено, иначе само число.

message

Вложенное сообщение с собственным объектом fields. Можно передать один объект или массив объектов для повторяющегося вложенного поля.

Полная пара: создать и разобрать

#beginScript
|DV|[AuthPacket]=(|PROTOBUF|{
  "action":"encode",
  "message_type":"AuthRequest",
  "field_mapping":{
    "AuthRequest":{
      "login":{"number":1,"type":"string"},
      "password":{"number":2,"type":"string"}
    }
  },
  "field_values":{
    "login":"Arthas",
    "password":"Scourge"
  }
}|PROTOBUF|)

|DV|[AuthData]=(|PROTOBUF|{
  "action":"decode",
  "message_type":"AuthRequest",
  "field_mapping":{
    "AuthRequest":{
      "login":{"number":1,"type":"string"},
      "password":{"number":2,"type":"string"}
    }
  },
  "protobuf_base64":"|DV|[AuthPacket]"
}|PROTOBUF|)
#endScript

Результаты:

|DV|[AuthPacket] = CgZBcnRoYXMSB1Njb3VyZ2U=
|DV|[AuthData]   = {"login":"Arthas","password":"Scourge"}

Имена login и password в двоичном сообщении не хранятся. При разборе они восстанавливаются только из field_mapping; если поменять номера местами, значения будут подписаны неверными именами.

Что в действительности делает update

Следующий вызов получает созданный выше пакет и добавляет ещё одну запись поля login:

(|PROTOBUF|{
  "action":"update",
  "message_type":"AuthRequest",
  "field_mapping":{
    "AuthRequest":{
      "login":{"number":1,"type":"string"},
      "password":{"number":2,"type":"string"}
    }
  },
  "field_values":{"login":"Jaina"},
  "protobuf_base64":"CgZBcnRoYXMSB1Njb3VyZ2U="
}|PROTOBUF|)

Результат — CgZBcnRoYXMSB1Njb3VyZ2UKBUphaW5h. При последующем decode эта реализация покажет два вхождения login как массив ["Arthas","Jaina"]; порядок свойств самого объекта JSON не определён.

update не сохраняет сообщение целиком

При разборе неизвестные описанию поля пропускаются, а при повторной записи уже не возвращаются. Известное поле update добавляет повторно вместо удаления прежнего значения. Не применяйте это действие как обычное «заменить поле», если необходимо сохранить неизвестные данные или получить единственное значение.

Повторяющиеся известные поля при decode собираются в массив. При encode массив поддержан только для вложенных полей типа message; массив простых строк, чисел или bytes не принимается. Упакованные повторяющиеся числа также не входят в поддерживаемый договор.

Границы и ошибки

  • Номер поля должен совпадать со схемой другой стороны. Допустимы номера от 1 до 536870911, кроме зарезервированного промежутка 19000–19999; исполнитель сам полностью это правило не проверяет.
  • Публичный договор ограничен одиннадцатью типами выше. Частично присутствующие во внутреннем исполнителе названия sint32, sint64, fixed32, fixed64, sfixed32 и sfixed64 записываются несовместимо либо разбираются неполно, поэтому использовать их нельзя.
  • Неизвестное поле при decode пропускается. Отсутствующее известное поле не дополняется значением по умолчанию.
  • Некорректный JSON, отсутствующее обязательное свойство, неизвестное действие, неверный тип значения или повреждённый Base64 вызывают ошибку выполнения; PROTOBUF не возвращает строку Error: ..., как функции сжатия.

Почему номер и вид записи поля определяют смысл двоичных данных, показано в официальном описании Protobuf.

Сначала получите точную схему

Для внешнего запроса возьмите исходный .proto или точное описание службы и вручную перенесите только нужные поля. Пример из этой страницы объясняет форму вызова, но его AuthRequest не является общей схемой для чужого узла.