最終更新日時:

バイナリデータをいい感じに作成するツールを作成する

はじめに

 独自のプロトコルや特定の目的の実験のためにバイナリデータを作成するツールを作成したため、その使い方をざっくりと紹介する。ただし、現状はまだまだ雑な部分は多いし、単体テストの正常系レベルのテストしかしていないことに注意してもらいたい。
 記事執筆時点でのソースコードは以下に公開している。

基本的な作成方法

 今回作成したツールによりバイナリファイルを生成するとき、事前に以下の準備が必要となる。

  • バイナリファイルのフォーマットの定義が記述されたXMLファイル

  • バイナリファイルに設定する値が列挙されたヘッダ付きのCSVファイル

 例えば、XMLファイルであれば、以下のように与える。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- 出力対象の値の設定 -->
  <item name="名称" bytes="64" padding=" ">
    <default-value></default-value>
  </item>
</format>
format.xml
Markup

 ここで、format.xsdはこのようなXMLでどのようなフォーマットが記述できるのかを示したXMLスキーマであり、どのような記述が可能であるかについてはこれを参照することでも確かめることができる。また、Red Hatが開発しているVSCode上での拡張を利用すれば入力の候補が出現するためフォーマットの定義は容易になるだろう。
 例えば、CSVファイルについては、以下のようにヘッダ付きで与える。今回の場合であれば、上記で示したXMLの//item/[@name='名称']に以下のCSVの対応するvalueがディスパッチされ、バイナリファイルとして出力される。

名称
value
data.csv
Plain text

 実際にバイナリファイルを生成する際は、以下のようなコマンドで生成をすることが出来る(とりあえず動けばいいということでコマンドライン引数の解析はかなり雑である)。

実行ファイル名 -csv data.csv -xml format.xml
Bash

 他のコマンドライン引数としては、-g キー 値と入力することで外部からあたかも環境変数のような特別な入力値を設定することができる。

 以下では、ユースケース別の基本的なバイナリファイルの作成方法を示していく。なお、今回は結果を見やすくするためにテキストとして読むことが可能なファイルのみを出力するようにする。

単純な値の出力

 まずは、単純に指定した値を出力する方法を示す。以下にバイナリファイルのフォーマットであるXMLファイルを示す。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- CSVで指定した値を出力(存在しないときは空を出力) -->
  <item name="名称1"></item>
  <!-- CSVで指定した値を出力(存在しないときは'default-value2'を出力) -->
  <item name="名称2">
    <default-value>default-value2</default-value>
  </item>
  <!-- CSVで指定した値に対応する外部から入力した値を出力(存在しないときは空を出力) -->
  <item name="名称3">
    <value type="external"></value>
  </item>
</format>
format.xml
Markup

 次にディスパッチを行うCSVファイルを示す。

名称1,名称3
値1,global
data.csv
Plain text

 バイナリファイルを生成する際のコマンドを以下に示す。

実行ファイル名 -csv data.csv -xml format.xml -g global 値3
Bash

 このとき生成されるバイナリファイルの中身は以下のようになっている。

値1default-value2値3
out.dat
Plain text

固定長項目の出力

 一般にバイナリファイルで出力される基礎となるのはバイト数が固定長の項目である。固定長項目は単にバイト数を固定長にするだけではなく、不足しているバイト数分を埋めるパディングを指定することが多いだろう。例えば、テキスト形式の数値であれば0パディングであったり、通常の文字列であればnullパディングなどを行ったりするだろう。以下にそれを行うためのバイナリファイルのフォーマットであるXMLファイルを示す。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- 左からの0パディングで4バイトの文字列を出力 -->
  <item name="名称1" bytes="4" lpadding="0">
    <default-value>1</default-value>
  </item>

  <!-- 右からの0パディングで4バイトの文字列を出力 -->
  <item name="名称2" bytes="4" rpadding="0">
    <default-value>2</default-value>
  </item>

  <!-- 右からの0パディングで4バイトの文字列を出力 -->
  <item name="名称3" bytes="4" rpadding="0">
    <default-value>3</default-value>
  </item>
</format>
format.xml
Markup

 今回の場合、全ての値をdefualt-valueでデフォルト値が設定されるようにしているため、CSVファイルは省略する。バイナリファイルを生成する際のコマンドを以下に示す。

実行ファイル名 -xml format.xml
Bash

 このとき生成されるバイナリファイルの中身は以下のようになっている。ちゃんと固定長でかつパディングが正常に働いていることがわかるだろう。

000120003000
out.dat
Plain text

一般の可変長項目の出力

 一般にバイナリファイルでは可変長のデータを出力する際はその項目長を出力する。そのような機能の実現のためにXPathを利用した値の出力をサポートしている。以下にそれを行うためのバイナリファイルのフォーマットであるXMLファイルを示す。ポイントとしては、//item/@evallazyを指定していることである。これは遅延評価を行うための指定であり、これとXPathの評価を合わせることにより、かなり自由度の高い表現を行うことができる。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- バイナリファイルの全長を左からの0埋めで出力(//writerは出力対象ではないため除くようにする) -->
  <item name="バイナリファイルの全長" bytes="4" lpadding="0" eval="lazy">
    <default-value type="xpath">sum(//item[not(ancestor::writer)]/@result-bytes)</default-value>
  </item>

  <!-- item[@name='名称1']の評価後に出力結果のバイト数を参照してその値を左からの0埋めで出力 -->
  <item name="名称1の項目長" bytes="4" lpadding="0" eval="lazy">
    <default-value type="xpath">../item[@name='名称1']/@result-bytes</default-value>
  </item>
  <!-- 可変長項目の出力 -->
  <item name="名称1"></item>
</format>
format.xml
Markup

 次にディスパッチを行うCSVファイルを示す。

名称1
値1
data.csv
Plain text

 バイナリファイルを生成する際のコマンドを以下に示す。

実行ファイル名 -csv data.csv -xml format.xml
Bash

 このとき生成されるバイナリファイルの中身は以下のようになっている。値1はUTF-8で4バイト、バイナリファイルの全長が12バイトのデータになるため、正しく項目長が出力できていることがわかる。

00120004値1
out.dat
Plain text

繰り返し項目の出力

 バイナリデータ中のリストデータの出力のためには繰り返し項目が出力できるような仕組みが必要である。そのような機能の実現のためにrepeatタグを利用した値の出力をサポートしている。以下にそれを行うためのバイナリファイルのフォーマットであるXMLファイルを示す。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- 繰り返し前の値の出力 -->
  <item name="繰り返し前"></item>

  <!-- CSVファイルのフェッチが完了するまで制限なく繰り返す -->
  <repeat fetch="true">
    <item name="繰り返し中"></item>
  </repeat>

  <!-- 繰り返し後の値の出力 -->
  <item name="繰り返し後"></item>
</format>
format.xml
Markup

 次にディスパッチを行うCSVファイルを示す。

繰り返し前,繰り返し中,繰り返し後
値1,値2,値5
,値3
,値4
data.csv
Plain text

 バイナリファイルを生成する際のコマンドを以下に示す。

実行ファイル名 -csv data.csv -xml format.xml
Bash

 このとき生成されるバイナリファイルの中身は以下のようになっている。値1から順に値5と出力できているため、繰り返し項目が正しく出力できていることがわかる。

値1値2値3値4値5
out.dat
Plain text

文字の変換

 入力された文字を適当に変換したりして加工したものを出力したいこともあるだろう。そのような機能の実現のために、入力値を変換する規則をサポートしている。以下にそれを行うためのバイナリファイルのフォーマットであるXMLファイルを示す。

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- 入力値をchara-map.xmlに従って変換した結果を出力する -->
  <item name="項目1">
    <transform>chara-map.xml</transform>
  </item>
</format>
format.xml
Markup

 chara-map.xmlは変換に用いる変換の定義が記載されたXMLファイルであり、以下のようなfromtoによる変換規則が記載されている。

<?xml version="1.0" encoding="utf-8" ?>
<transform type="chara-map">
  <!-- ○はそのまま出力 -->
  <map from="" to="" />
  <!-- 数字はそのまま出力 -->
  <map from-regex="\d" to="{0}" />
  <!-- 何にもマッチしなかった文字は空にして出力 -->
  <map from-regex="." to="" />
</transform>
chara-map.xml
Markup

 次にディスパッチを行うCSVファイルを示す。

項目1
1○2×3□4
data.csv
Plain text

 バイナリファイルを生成する際のコマンドを以下に示す。

実行ファイル名 -csv data.csv -xml format.xml
Bash

 このとき生成されるバイナリファイルの中身は以下のようになっている。chara-map.xmlで示した通りの変換規則で出力できていることがわかる。

1○234
out.dat
Plain text

応用

 実用的なものではないが、応用すればこういったこともできるという一例を示す。まぁ、ほぼほぼXPathによる計算の効果なだけではあるが。

FizzBuzz

XMLファイル

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- FizzBuzzで出力する定数の定義 -->
  <item name="Fizz" bytes="0"><default-value>Fizz</default-value></item>
  <item name="Buzz" bytes="0"><default-value>Buzz</default-value></item>

  <!-- CSVファイルのフェッチが完了するまで制限なく繰り返す -->
  <repeat fetch="true">
    <item name="x"></item>
    <item name=""><default-value>: </default-value></item>
    <!-- FizzBuzzの出力(位置計算がちょっと面倒なためFizzとBuzzは文書全体から検索) -->
    <item name=""><default-value type="xpath">//item[@name='Fizz'][number(//item[@name='x']/@result) mod 3 = 0]/@result</default-value></item>
    <item name=""><default-value type="xpath">//item[@name='Buzz'][number(//item[@name='x']/@result) mod 5 = 0]/@result</default-value></item>
    <item name=""><default-value type="xpath">../item[@name='x'][number(../item[@name='x']/@result) mod 3 != 0 and number(../item[@name='x']/@result) mod 5 != 0]/@result</default-value></item>
    <!-- 改行コード -->
    <item name="" encoding="hexadecimal"><default-value>0D0A</default-value></item>
  </repeat>
</format>
Markup

CSVファイル

x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Plain text

出力結果

1: 1
2: 2
3: Fizz
4: 4
5: Buzz
6: Fizz
7: 7
8: 8
9: Fizz
10: Buzz
11: 11
12: Fizz
13: 13
14: 14
15: FizzBuzz
Plain text

フィボナッチ数列の計算

XMLファイル

<?xml version="1.0" encoding="utf-8" ?>
<format xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="format.xsd">
  <!-- 出力先のファイルの指定 -->
  <writer type="binary-file">
    <item name="ファイル名">
      <default-value>out.dat</default-value>
    </item>
  </writer>

  <!-- XPathでの計算用に展開 -->
  <item name="x" bytes="0"></item>

  <!-- 面倒なのでF0とF1は問答無用で出力する -->
  <item name=""><default-value>F0: 0</default-value></item>
  <item name="" encoding="hexadecimal"><default-value>0D0A</default-value></item>
  <item name=""><default-value>F1: 1</default-value></item>
  <item name="" encoding="hexadecimal"><default-value>0D0A</default-value></item>

  <!-- 2以降のフィボナッチ数列の計算 -->
  <repeat xmax="number(../item[@name='x']/@result)-1">
    <item name=""><default-value>F</default-value></item>
    <!-- 自動採番される連番の宣言 -->
    <item name=""><default-value type="auto-increment">2</default-value></item>
    <item name=""><default-value>: </default-value></item>
    <!-- フィボナッチ数列の計算 -->
    <item name="Fn_2"><default-value type="xpath">number(../item[@name='Fn']/@result)+number(../item[@name='Fn_1']/@result)</default-value></item>
    <!-- フィボナッチ数列の前回まで項の記憶 -->
    <item name="Fn" result="0" bytes="0"><default-value type="xpath">../item[@name='Fn_1']/@result</default-value></item>
    <item name="Fn_1" result="1" bytes="0"><default-value type="xpath">../item[@name='Fn_2']/@result</default-value></item>
    <!-- 改行コード -->
    <item name="" encoding="hexadecimal"><default-value>0D0A</default-value></item>
  </repeat>
</format>
Markup

CSVファイル

x
20
Plain text

出力結果

F0: 0
F1: 1
F2: 1
F3: 2
F4: 3
F5: 5
F6: 8
F7: 13
F8: 21
F9: 34
F10: 55
F11: 89
F12: 144
F13: 233
F14: 377
F15: 610
F16: 987
F17: 1597
F18: 2584
F19: 4181
F20: 6765
Plain text

おわりに

 今回実装したツールが持つ機能は今回紹介したもの以外にも割とたくさんある。とはいえ、面倒と思って作成していない機能もある。代表例として挙げるとすれば、バイナリファイルからCSVへ変換する機能である。一応、直ちに拡張できるようには作ってあるが、バイナリ値を逆変換して文字列値に起こすのが面倒(特にパディングの除去回りはどうしても不完全性をもつためルール作りから始める必要がある)だし、私は使う予定は今のところないので作成していない。
 現状でもそれなりに複雑なデータが作成することができるため、テスト用のアプリケーションに組み込むなどしても有効に働くかもしれない。そのうち、いくつかのプロトコルのフォーマットのXMLを作成するのもいいかもしれない。