バイナリデータのパースに便利なKaitai Struct
はじめに
バイナリデータをパースするためのライブラリである『Kaitai Struct』について紹介します。
IoTのサービスにおいては、機器から送信されてくるデータはJSONのような人に優しいフォーマットではなく、独自のバイナリフォーマットであるシーンがあります。
筆者らが開発・運営している「Cariot(キャリオット)」というサービスでは、サーバ側のコードをJavaで開発していますが、バイナリデータのパースに関するデファクトのJavaライブラリというものはありません。その中で、「Kaitai Struct」は使い心地がよく、利用を始めて少し時間も経ち、慣れてきたので、この機会に紹介します。
バイナリデータのパーサに対する要件
筆者らが取り扱うバイナリデータの仕様に合わせて、次のような要件を考慮して技術選定しました。
- 大量のデータを効率よくパースしたい
- 問題が起きた時に、原因がデータ構造なのかパーサなのかをスムーズに切り分けるため、専用のDSLで構造を定義し、そこからコード生成するやり方にしたい
- 1つのデータに対して、先頭のバイト列の値(プレフィックス)により、後続のデータフォーマットが変わる場合に対応したい
- 扱いが特殊な型があった場合の逃げ道として、専用のコンバータも使いたい
バイナリデータのパースは、簡単なものであれば、Java言語にある標準APIを使って自前で実装しても、そこまで大きなコード量にはならないかもしれません。
が、上記のような要件は汎用的なものですし、例えばエンディアン(バイトオーダー)やビット単位での操作などの細かい作り込みをプロジェクト限定の独自実装で作るよりは、サードパーティのライブラリの方がよいと考え、今回はライブラリに頼ることにしました。
Kaitai Struct
公式ページ
特徴
- YAML形式に近い独自のDSLで、バイナリデータの構造を
.ksy
ファイルとして定義する -
.ksy
ファイルを専用のコンパイラでコンパイラし、コードを生成する - Javaだけでなく複数の言語に対応している
使い方
流れとしては以下のようになります。
-
.ksy
ファイルの作成 -
.ksy
ファイルのコンパイラを使って、コードを生成する - 生成したコードを使って、パース処理を呼び出す
実例があったほうがわかりやすいと思うので、下のようなフォーマットのバイナリデータを扱うことを例として取り上げます(注:プロダクトで使っているものと同じではありません)
.ksy ファイルの作成
上記のサンプルのデータ構造を定義する .ksy
ファイルは以下のようになります。
説明はファイル中のコメントを参照してください。
type
にある u2
や s4
は、 u
= unsigned(符号なし整数)、 s
= signed(符号つき整数)、後ろの数値がバイト数を意味しています。
Javaだと符号のあり/なしをあまり意識はしませんが、大きく意味が変わるので、定義上は意識する必要があります。
meta:
id: hoge_struct
# ビッグエンディアン
endian: be
seq:
- id: version
type: u2
- id: prefix
type: str
size: 2
encoding: ASCII
- id: checksum
type: u2
# データの最後まで item 構造の繰り返し
- id: items
type: item
repeat: eos
types:
# item 構造(独自の型)定義
item:
seq:
- id: timestamp
type: u4
- id: custom_form
type:
# prefix の値に応じて、データのフォーマットを分岐
switch-on: _parent.prefix
cases:
'"F1"': f1_form
'"F2"': f2_form
f1_form:
seq:
- id: value
type: s4
f2_form:
seq:
- id: x
type: s4
- id: y
type: s4
詳しいスタイルガイドは以下です。
コンパイラのインストール
.ksy
ファイルからコード生成するためにコンパイラをインストールします。
公式サイトに書かれていますが、Macの場合はHomebrewで一発でインストールができます。
brew install kaitai-struct-compiler
インストール後は以下のコマンドでJavaコードを生成します(見づらいので改行しています)
出力言語 ( -t java
), 出力後のコードのパッケージ ( --java-package
), 出力先 ( --outdir
), .ksy
ファイルのパスの4つを指定しています。他にもオプションはありますが、最低限このあたりがあれば、使うことはできるでしょう。
kaitai-struct-compiler -t java
--java-package sample.kaitai
--outdir proj1/src/main/java
proj1/src/main/resources/kaitai_struct/ksy/atrack-struct.ksy
パース
実際にパースする処理は次のようになります。
パース対象のバイト列に対して、 ByteBufferKaitaiStream
で読み取る形です(経験上、巨大なバイナリデータをパースすることはあまりないので、全てメモリで処理させています)
import io.kaitai.struct.ByteBufferKaitaiStream;
import sample.kaitai.HogeStruct;
public class Sample {
public void parse(byte[] bytes) throws Exception {
HogeStruct parsed = new HogeStruct(new ByteBufferKaitaiStream(bytes));
String prefix = parsed.prefix();
int version = parsed.version();
...
}
}
ビルド/デプロイパイプラインへの組み込み
筆者らのプロジェクトでは、 .ksy
ファイルと生成した .java
ファイルの両方をリポジトリで管理するようにしています。
本質的には自動生成が可能な .java
ファイルはリポジトリで管理する必要はないのですが、運用管理の観点で、次の理由から、リポジトリに含めるようにしました。
- パイプラインに組み込もうとすると専用コンパイラが必要なのでひと手間増えてしまう
- 他のメンバーが手元でビルドする場合にも、専用コンパイラが必要になってしまう
- 一度作った後は、これらのファイルは変更頻度・変更の量がそこまで高くないので
.ksy
と.java
2つを構成管理する負荷は少なく、ズレる心配も少ない
その他のライブラリ
今回はKaitai Structを紹介しましたが、検討していた他の選択肢としてのライブラリも紹介します。
preon
特徴
-
@BoundNumber
や@BoundNumber
といったアノテーションを、直接クラスに付与して構造を宣言するタイプ - Mavenリポジトリに公開されていないため、ビルドに組み込む場合には野良でビルドする必要があります(以前使っていましたが、筆者らは AWS CodeArtifact で管理していました)
JBBP (Java Binary Block Parser)
特徴
- Pythonのstructライクな構文によるDSLで構造を記述し、それを読み込んでパースするタイプ
- 筆者らが実際のデータで試した場合、空配列や文字列のパース等の一部データで失敗しました。カスタムパーサで対応できることが書かれていますが、ちょっとコスト感に見合わず見送りました。
おわりに
「Kaitai Struct」というライブラリの紹介でした。
複数の言語に対応していることもあり、GitHubのスター数も4,002(2024年10月21日時点)と一定数以上獲得しているライブラリですが、日本語での紹介記事は以下くらいしか見つかりませんでした。
同じようなライブラリをお探しの方の何かの参考になれば幸いです。
Discussion