# Spicy ###### tags: `summary`, `spicy` ## Обновление: У проекта новая жизнь: https://github.com/zeek/spicy Проект на С++, позиционируется как генератор парсеров для протоколов и файлов. Вполне полноценный C-like скриптовый язык. Spicy — фреймворк для работы с потоковыми бинарными форматами. Основное назначение — инструменты DPI. Включает в себя: * **[язык](##Язык-Spicy)** спецификации формата * **комиплятор** этого языка в код разборщика * **API** для использования в коде HILTI — платформа для выполнения на абстрактной машине. Состоит из *языка* HILTI и *компилятора*, основанного на LLVM и превращающего код HILTI в исполняемый. Описанный в Spicy *формат данных* при помощии соответствующего компилятора транслируется в код для машины HILTI, после чего непосредственно в объектный код разборщика. ![Приблизительная схема взаимодействия](https://i.imgur.com/mKWGqSU.png) ### Характеристики * Синтаксис и семантика объединены: не нужен сторонний код для таких вещей, как контрольная сумма. Богатая система пользовательской обработки позволяет получать готовый результат без стороннего кода * Отслеживается глобальное состояние протокола * Потоковая обработка * [Поддержка концепции слоев](###Sink) (`sink`) * [Обработка ошибок парсинга данных](##Обработка-ошибок) * [Динамическое распознавание форматов](##Динамическое-распознавание-форматов) * Генератор бинарных данных (сборщик) ## Язык Spicy Спецификация основана на определении *unit-типов*. *Unit* представляет из себя некоторую семантическую единицу формата (например, заголовок). Основной (внешний) тип помечается ключевым словом `export`. Поток будет передаваться именно в него. ### Unit Внутри каждого *unit*-а определяется набор *атрибутов* — полей данного типа — которые как раз и читаются из потока. Каждый атрибут имеет свой тип (базовый или пользовательский). Также при определении *unit*-а можно указать: * параметры типа (у данного типа будет доступ к их полям) * свойства типа: `%byteorder`, `%description`, `%sync-(after|at)` (управление ошибками) * опциональные атрибуты (`if <cond>`) — читаются только если выполнено условие * конструкция `switch` для типа атрибута, причем как перебирающая значения указанного выражения, так и типы — какой тип "подходит" (*lookahead*) * хуки (см. ниже) * переменные (синтезируемые атрибуты, инстансы) — ключевое слово `var` * анонимные атрибуты (имя не нужно, например, для отступов, см. пример) * неявный терминальный символ (см. пример) ``` type Archive = unit { files: list<File>; : uint<8>(0x0); : bytes &length=511; }; ``` ### Атрибуты У атрибута могут присутствовать различные *аннотации* — дополнительные параметры, определяющие размер или тип атрибута: * произвольная длина поля (`&length`) * преобразование прочитанных байтов (`&convert`) — сохранение преобразованного значения (например, интерпретация `to_uint`, или произвольное пользовательское преобразование) * значение по умолчанию (`&default`) * порядок байт/бит (`byteorder`/`bitorder`) * чтение до конца потока (`&eod`) * постепенное накопление данных (`&chunked`) *(остальные см. статью)* ``` bytes depad(b: bytes) { return b.match(/^[^\x00 ]+/); }; type Header = unit { name : bytes &length=100 &convert=depad($$); ... }; ``` ### Типы Атрибуты должны иметь тип — это может быть один из базовых типов, контейнер или другой определенный пользователем *unit*-тип. Все типы, поддерживаемые в языке Spicy: * Базовые `addr`, `bitfield`, `bool`, `bytes`, `double`, `enum`, `int<N>`, `uint<N>`, `interval`, `time`, `string`, `regexp` * Контейнеры `iterator<T>`, `list<T>`, `vector<T>`, `map<T_1,T_2>`, `set<T>` * Составные `unit<T_1,...,T_n>`, `sink` ### Hook Также в определении unit-ов могут присутствовать *хуки* (*hook*) — обработчики событий (например, окончания чтения типа или конкретного поля), которые представляют из себя пользовательский код, близкий к некоторому скриптовому языку и имеющий доступ к прочитанным данным. Внутри хуков можно вызывать сторонние C-функции (как?). ``` type Greeting = unit { name : /[^\r\n ]*/; on %done { print self.name; } }; ``` ### Sink Специальный тип `sink` удобен для реализации слоев протоколов. `sink` представляет из себя аналог *pipe*-а: поток направляется в него и разбор осуществляется возможно даже другим модулем (оператор `->`). Альтернатива: фиксированно определить тип данного атрибута как некоторый *unit*. Дополнительные возможности: * фильтры — перед разбором, `sink` проводит предобработку данных (поддерживаются декодер `base64` и `unzip`) * метки (помогают при обработке ошибок) * явный вызов метода `write()` ``` type Header = unit(msg: Message) { . . . } type Message = unit { headers: list<Header(self)>; : /\r?\n/; body: bytes &length=self.content_length -> self.data; on headers { self.data.connect_mime_type(self.content_type); } var data: sink; var content_type: bytes; var content_length: uint<64>; }; type JPEG = unit { %mimetype="image/jpeg"; ...}; type tar = unit { %mimetype="application/x-tar"; ...}; ``` ## Обработка ошибок *Spicy* предлагает два способа обработки ошибок парсинга. Первый - это явный вызов некоторого пользовательского обработчика (хук `%error`). Второй — это сдвиг "чуть-чуть дальше" по потоку и анализ заново (`&synchronize`). Второй способ подразумевает, что пользователю необходимо описать положение, с которого начать новый разбор: ``` type RequestLine = unit { %synchronize-at = /(GET|POST|HEAD) /; method: Token; ... }; ``` При этом в одном из родительских типов необходимо указать атрибут, на котором эта синхронизация будет выполняться: ``` type Requests = unit { requests: list<Request> &synchronize; }; type Request = unit { request: RequestLine; message: Message; }; ``` Таким образом, при возникновении ошибки в разборе Requests парсер увидит синхронизуемый атрибут `requests`, будет рекурсивно искать в нем синхронизуемый тип и найдет `RequestLine`. Затем он будет сдвигаться по потоку так, чтобы соответствовать указанному регулярному выражению (` /(GET|POST|HEAD) /`). Далее разбор пойдет заново с этой точки начиная с атрибута `Requests::requests.request.method` и так далее. ## Динамическое распознавание форматов *Spicy* предоставляет возможность при имеющихся парсерах, получая на вход неизвестную последовательность, определять, какому формату из известных она соответствует. Для этого используется простой перебор: каждый парсер пробует разобрать буфер в *пробном режиме* и если натыкается на ошибку, просто прекращает работу. В итоге (в хорошем случае) остается один справившийся парсер, он и определяет формат буфера. Дополнительные возможности: * Поиск оптимизирован: если тип поддерживает `%synchronize-at`, регулярные выражения в начале ищутся в буфере и ненайденные сразу отметаются. Для оставшихся запускается основной процесс перебора. * К потоку (например при вызове `sink`-а) привязывается MIME тип (`connect_mime_type`). MIME тип здесь более широкое понятие, к примеру поддерживается MIME-type: "tcp/80" * *Unit* типы могут "подтверждать" принадлежность буфера своему типу, явно выходя из *пробного режима* вызовом `confirm()` * Поддерживаются хуки для такого подтверждения и наоборот для неуспешного выхода из пробного режима. ## Пайплайн использования Наиболее тривиальный способ использования: ``` файл *.spicy -> spicy-driver -> исполняемый код парсера, разбирающего stdin ``` Пример использования CLI: ``` echo "123" | spicy-driver hello-world.spicy ``` ## Установка Для тестирования рекомендуется использовать Docker. ``` docker run -i -t "rsmmr/hilti" ``` ## Пример <details><summary>Фрагмент описания формата архива tar</summary> ``` module tar; export type Archive = unit { files: list<File>; : uint<8>(0x0); : bytes &length=511; }; type File = unit { header: Header; data : bytes &length=self.header.size; : bytes &length=512-(self.header.size mod 512) }; type Type = enum { REG=0, LNK=1, SYM=2, CHR=3, BLK=4, DIR=5, FIFO=6 }; bytes depad(b: bytes) { return b.match(/^[^\x00 ]+/); # Strip trailing padding } type Header = unit { name : bytes &length=100 &convert=depad($$); mode : bytes &length=8 &convert=depad($$); uid : bytes &length=8 &convert=depad($$); gid : bytes &length=8 &convert=depad($$); size : bytes &length=12 &convert=depad($$).to_uint(8); mtime : bytes &length=12 &convert=depad($$).to_time(8); chksum: bytes &length=8 &convert=depad($$).to_uint(8); tflag : bytes &length=1 &convert=Type($$.to_uint()); lname : bytes &length=100 &convert=depad($$); : bytes &length=88; prefix: bytes &length=155 &convert=depad($$); : bytes &length=12; var full_path: bytes; on %done { if ( ! self.tflag ) self.tflag = Type::REG; self.full_path = self.prefix + b"/" + self.name; } }; ``` </details> ## Ссылки * http://www.icir.org/hilti/ (deprecated) * https://github.com/zeek/spicy (new GitHub version)