# Spicy
###### tags: `summary`, `spicy`
## Обновление:
У проекта новая жизнь: https://github.com/zeek/spicy
Проект на С++, позиционируется как генератор парсеров для протоколов и файлов.
Вполне полноценный C-like скриптовый язык.
Spicy — фреймворк для работы с потоковыми бинарными форматами. Основное назначение — инструменты DPI.
Включает в себя:
* **[язык](##Язык-Spicy)** спецификации формата
* **комиплятор** этого языка в код разборщика
* **API** для использования в коде
HILTI — платформа для выполнения на абстрактной машине. Состоит из *языка* HILTI и *компилятора*, основанного на LLVM и превращающего код HILTI в исполняемый.
Описанный в Spicy *формат данных* при помощии соответствующего компилятора транслируется в код для машины HILTI, после чего непосредственно в объектный код разборщика.

### Характеристики
* Синтаксис и семантика объединены: не нужен сторонний код для таких вещей, как контрольная сумма. Богатая система пользовательской обработки позволяет получать готовый результат без стороннего кода
* Отслеживается глобальное состояние протокола
* Потоковая обработка
* [Поддержка концепции слоев](###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)