Rustでバイナリを読み書きするのに必要なクレート3選

f:id:aptpod_tech-writer:20200929185853j:plain

研究開発グループの大久保です。

当社の製品の中にはC/C++で書かれたものが存在し、その中には独自のバイナリフォーマットを取り扱うものが存在します。既存のコードとやり取りするようなRustのプロジェクトを起こすためには、その独自のバイナリフォーマットをRustで取り扱えるようにしなければなりません。しかしながら、Rustの標準ライブラリの機能だけでは、バイナリの読み書きは意外と面倒になります。そのため、今回はRustでバイナリを扱うのならぜひ知っておきたいクレートを3つご紹介します。

byteorder

byteorderはその名の通り、バイトオーダ、つまりエンディアンを扱うためのクレートです。使い方はシンプルで、ByteOrderトレイトと、BigEndian, LittleEndian, NativeEndianのうち自分が扱いたいエンディアンをインポートすれば、バッファと数値型の間で読み書きを行うことができます。

例えば、長さ4バイトのバッファから32bit整数型を読み出す場合、次のようになります。

use byteorder::{BigEndian, LittleEndian, NativeEndian, ByteOrder};

fn main() {
    let buf = [0, 0, 0, 42];

    let a = LittleEndian::read_u32(&buf);
    assert_eq!(a, 704643072);
    let a = BigEndian::read_u32(&buf);
    assert_eq!(a, 42);
    let a = NativeEndian::read_u32(&buf);
    assert_eq!(a, 704643072);
}

32bit整数型の書き込みは次のようになります。

use byteorder::{BigEndian, LittleEndian, NativeEndian, ByteOrder};

fn main() {
    let mut buf = [0; 4];
    
    LittleEndian::write_u32(&mut buf, 42);
    assert_eq!(buf, [42, 0, 0, 0]);
    BigEndian::write_u32(&mut buf, 42);
    assert_eq!(buf, [0, 0, 0, 42]);
    NativeEndian::write_u32(&mut buf, 42);
    assert_eq!(buf, [42, 0, 0, 0]);
}

NativeEndianはこれを実行しているプラットフォームのエンディアンを示します。

bytes

bytesは、Rust用の非同期ライブラリtokioで使われているバイナリ操作用のクレートです。BufBufMutというトレイトを導入することで、バイナリ読み書きのためのメソッドを利用することができます。

例として、用意したデータの先頭から順に整数を読み出していきます。

use bytes::Buf;

fn main() {
    let data = [b'a', 0, 33, 42, 0];
    let mut p = &data[..];
    assert_eq!(p.get_u8(), b'a'); // 0バイト目を8bit整数として読み出し
    assert_eq!(p.get_u16(), 33); // 1〜2バイト目をビッグエンディアン16bit整数として読み出し
    assert_eq!(p.get_u16_le(), 42); // 3〜4バイト目をリトルエンディアン16bit整数として読み出し
}

Vecに順番に整数を書き込んでいくこともできます。

use bytes::BufMut;

fn main() {
    let mut buf = Vec::new();
    
    buf.put_u8(b'r'); // 8bit整数を書き込み
    buf.put_u8(b'u');
    buf.put_u8(b's');
    buf.put_u8(b't');
    buf.put_u16(0xFFEE); // ビッグエンディアンとして16bit整数を書き込み
    buf.put_u16_le(0x1122); // リトルエンディアンとして16bit整数を書き込み
    
    assert_eq!(buf, [b'r', b'u', b's', b't', 0xFF, 0xEE, 0x22, 0x11]);
}

読み書きどちらも関数名の後ろに_leを付けるとリトルエンディアン扱いになります。バッファの先頭から読み書きしていくようなデータ構造の場合、bytesはなかなか便利なクレートと言えるでしょう。

nom

nomはRust用のパーサコンビネータライブラリです。nomが提供するパース用の関数を組み合わせて、対象となるフォーマット用のパーサを作り上げるようにして使います。nomのexampleに示されているのは、テキスト(&str)のパースですが、バイナリ(&[u8])のパースにも使えます。

例えば、先頭から順にバイナリを読んでいき、結果をMyData構造体に格納していく場合は次のようになります。

use nom::IResult;
use nom::number::complete::{be_u8, be_u32};

/// 読み込みたいデータ
#[derive(PartialEq, Debug)]
struct MyData {
    a: u8,
    b: u8,
    c: u32,
    d: u32,
}

/// バイナリをパースしてMyDataを読み込む関数
fn parse_mydata(input: &[u8]) -> IResult<&[u8], MyData> {
    let (input, a) = be_u8(input)?;
    let (input, b) = be_u8(input)?;
    let (input, c) = be_u32(input)?;
    let (input, d) = be_u32(input)?;

    Ok((input, MyData {
        a, b, c, d
    }))
}

fn main() {
    let data = [10, 20, 0, 0, 0, 30, 0, 0, 0, 40];

    let mydata = parse_mydata(&data).unwrap().1;

    assert_eq!(
        mydata,
        MyData {
            a: 10,
            b: 20,
            c: 30,
            d: 40,
        }
    );
}

基本的にnomにおけるパーサ関数は、パースしたい領域のスライスを受け取り、読み残しのスライスと読み取った結果のタプルを返します。そのため、返り値のスライスを読み取れば、先頭から順に値を読み込んでいくことができます。

先頭から順に読んでいくだけならbytesでも可能ですが、nomの関数を使えば複雑な構造のバイナリを読み取ることも可能です。例えば、先頭にmydataというマジックナンバーがついているバイナリをパースしたい場合は、tagを使うことができます。

use nom::bytes::complete::tag;
use nom::number::complete::{be_u32, be_u8};
use nom::IResult;

/// 読み込みたいデータ
#[derive(PartialEq, Debug)]
struct MyData {
    a: u8,
    b: u8,
    c: u32,
    d: u32,
}

/// バイナリをパースしてMyDataを読み込む関数
fn parse_mydata(input: &[u8]) -> IResult<&[u8], MyData> {
    let (input, _) = tag(b"mydata")(input)?; // inputの先頭6バイトが"mydata"かどうか確かめる
    let (input, a) = be_u8(input)?;
    let (input, b) = be_u8(input)?;
    let (input, c) = be_u32(input)?;
    let (input, d) = be_u32(input)?;

    Ok((input, MyData { a, b, c, d }))
}

fn main() {
    let data = [
        b'm', b'y', b'd', b'a', b't', b'a', 10, 20, 0, 0, 0, 30, 0, 0, 0, 40,
    ];

    let mydata = parse_mydata(&data).unwrap().1;

    assert_eq!(
        mydata,
        MyData {
            a: 10,
            b: 20,
            c: 30,
            d: 40,
        }
    );
}

また、このデータの末尾に、ヌル終端のASCII文字列が格納されていた場合を考えてみます。この場合、take_untilを使うことで、0が現れるまでのスライスを取得することができます。

use nom::bytes::complete::{tag, take_until};
use nom::number::complete::{be_u32, be_u8};
use nom::IResult;

/// 読み込みたいデータ
#[derive(PartialEq, Debug)]
struct MyData {
    a: u8,
    b: u8,
    c: u32,
    d: u32,
    id: Vec<u8>,
}

/// バイナリをパースしてMyDataを読み込む関数
fn parse_mydata(input: &[u8]) -> IResult<&[u8], MyData> {
    let (input, _) = tag(b"mydata")(input)?;
    let (input, a) = be_u8(input)?;
    let (input, b) = be_u8(input)?;
    let (input, c) = be_u32(input)?;
    let (input, d) = be_u32(input)?;
    let (input, id) = take_until(&b"\0"[..])(input)?; // ヌル文字が現れるまでのデータを取得

    Ok((
        input,
        MyData {
            a,
            b,
            c,
            d,
            id: id.to_vec(),
        },
    ))
}

fn main() {
    let data = [
        b'm', b'y', b'd', b'a', b't', b'a', 10, 20, 0, 0, 0, 30, 0, 0, 0, 40,
        b'a', b'b', b'c', b'd', b'e', b'f', 0,
    ];

    let mydata = parse_mydata(&data).unwrap().1;

    assert_eq!(
        mydata,
        MyData {
            a: 10,
            b: 20,
            c: 30,
            d: 40,
            id: b"abcdef".to_vec(),
        }
    );
}

他にもnomにはいろいろな機能が用意されていますので、うまく使えばもっと多様なフォーマットにも対応できます。

最後に

Rustでちょっと凝ったことをすると、標準ライブラリ以外のクレートが必要になることが多く、適したクレートを探すのは少し大変です。そのため、今回はバイナリを扱う場合に必須になりそうなクレートをご紹介しました。Rustはその適用範囲上、バイナリを扱うことも多いかと思いますので、この記事がご参考になれば幸いです。