🧩

バイナリデータのパースに便利なKaitai Struct

2024/10/21に公開

はじめに

バイナリデータをパースするためのライブラリである『Kaitai Struct』について紹介します。

IoTのサービスにおいては、機器から送信されてくるデータはJSONのような人に優しいフォーマットではなく、独自のバイナリフォーマットであるシーンがあります。
筆者らが開発・運営している「Cariot(キャリオット)」というサービスでは、サーバ側のコードをJavaで開発していますが、バイナリデータのパースに関するデファクトのJavaライブラリというものはありません。その中で、「Kaitai Struct」は使い心地がよく、利用を始めて少し時間も経ち、慣れてきたので、この機会に紹介します。

バイナリデータのパーサに対する要件

筆者らが取り扱うバイナリデータの仕様に合わせて、次のような要件を考慮して技術選定しました。

  • 大量のデータを効率よくパースしたい
  • 問題が起きた時に、原因がデータ構造なのかパーサなのかをスムーズに切り分けるため、専用のDSLで構造を定義し、そこからコード生成するやり方にしたい
  • 1つのデータに対して、先頭のバイト列の値(プレフィックス)により、後続のデータフォーマットが変わる場合に対応したい
  • 扱いが特殊な型があった場合の逃げ道として、専用のコンバータも使いたい

バイナリデータのパースは、簡単なものであれば、Java言語にある標準APIや機能を使って自前で実装しても、そこまで大きなコード量にはならないかもしれません。
が、上記のような要件を加味した上で、エンディアン(バイトオーダー)やビット単位での操作などが必要なことを考えると、プロジェクト専用の独自実装で下手に作るよりは、サードパーティのライブラリの方がよい場合もあります。

Kaitai Struct

公式ページ

https://kaitai.io/

特徴

  • YAML形式に近い独自のDSLで、バイナリデータの構造を .ksy ファイルとして定義する
  • Javaだけでなく複数の言語に対応している

使い方

流れとしては以下のようになります。

  1. .ksy ファイルの作成
  2. .ksy ファイルのコンパイラを使って、コードを生成する
  3. 生成したコードを使って、パース処理を呼び出す

実例があったほうがわかりやすいと思うので、下のようなフォーマットのバイナリデータを扱うことを例として取り上げます(注:プロダクトで使っているものと同じではありません)

.ksy ファイルの作成

上記のサンプルのデータ構造を定義する .ksy ファイルは以下のようになります。
説明はファイル中のコメントを参照してください。

hoge.ksy
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

詳しいスタイルガイドは以下です。

https://doc.kaitai.io/ksy_style_guide.html

コンパイラのインストール

.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

パース

実際にパースする処理は次のようになります。

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つを構成管理する負荷は少なく、ズレる心配も少ない

その他のライブラリ

検討していた他の選択肢としてのライブラリも紹介します。

preon

https://github.com/preon/preon

特徴

  • @BoundNumber@BoundNumber といったアノテーションを、直接クラスに付与して構造を宣言するタイプ
  • Mavenリポジトリに公開されていないため、ビルドに組み込む場合には野良でビルドする必要があります(以前使っていましたが、筆者らは AWS CodeArtifact で管理していました)

JBBP (Java Binary Block Parser)

https://github.com/raydac/java-binary-block-parser

特徴

  • Pythonのstructライクな構文によるDSLで構造を記述し、それを読み込んでパースするタイプ
  • 筆者らが実際のデータで試した場合、空配列や文字列のパース等の一部データで失敗しました。カスタムパーサで対応できることが書かれていますが、ちょっとコスト感に見合わず見送りました。

おわりに

「Kaitai Struct」というライブラリの紹介でした。
複数の言語に対応していることもあり、GitHubのスター数も4,002(2024年10月21日時点)と一定数以上獲得しているライブラリですが、日本語での紹介記事は以下くらいしか見つかりませんでした。

https://qiita.com/mueru/items/7a288aec25b48594e42a

同じようなライブラリをお探しの方の何かの参考になれば幸いです。

Cariot開発チーム

Discussion