---
# System prepended metadata

title: Spicy
tags: [summary, spicy]

---

# 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)
