m13o.net

2020-12-01 Tue 23:00
Rustでバイナリを読む その1   AdventCalendar2020 Rust programming

世間にはアドベントカレンダーの時期が到来しているようで, Qから始まるあのサイトへの投稿はもはやしたくもないなぁと思っているので, 今年も関係ないイベントだという気持ちでいましたが, ふと, 尊敬するとあるかつての同僚が一人アドカレをしていたのを思い出し, 最近Rustのお勉強をしているので, そのお勉強過程の記録をアドベントカレンダーとしてやってみようかなと思った次第.

利用する環境の制約としては以下.

  • macOS Catalina
  • rustc: 1.48.0 stable
  • IDEはCLionにRust Pluginを導入
  • crateは極力stdのみを利用

既存crateを使ってもいいけど, 今回は言語と標準crateの勉強を兼ねるので極力stdのみでやっていこうと思います. rustcのバージョンは書いている最中にうっかり上げる可能性はある.

さて. Rustでバイナリデータをパースしようとした時に, BufReaderを利用するというのがまず思い付く方法なのですが, このBufReader, 困った事にテキストを読む事に長けてはいるもののバイナリデータを読むには少々使い難いです. ですので, BufReader等を利用して取得したバイナリデータを受け取り, 特定バイト数だけ読むといった機能を持ったものを作ってみます.

名前は安直にBinaryReaderとし, 以下のようなstructを用意します.

struct BinaryReader<'a> {
    buffer: &'a Vec<u8>,
    current_index: usize,
}

bufferは読み取るバイト列への参照として&Vec<u8>を, current_indexは次に読み取るbufferの開始位置を表します.

ああそうです, 我々は現代に生きる人類なので当然テストを書かねばなりません. まずはテストです. これは摂理です. というわけでテストを書きましょう. とりあえずまだ実装はしていませんが, やりたい事をそのままテストとして記述しています.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn read_bytes() {
        let buffer = vec![0x02, 0x34, 0x58, 0xFF, 0x80, 0x99, 0x00, 0x42];
        let mut reader = BinaryReader::new(&buffer);
        let (values, rest) = reader.read(5);
        assert_eq!(values.len(), 5);
        assert_eq!(values[0], 0x02);
        assert_eq!(values[1], 0x34);
        assert_eq!(values[2], 0x58);
        assert_eq!(values[3], 0xFF);
        assert_eq!(values[4], 0x80);
        assert_eq!(rest.len(), 3);
        assert_eq!(rest[0], 0x99);
        assert_eq!(rest[1],0x00);
        assert_eq!(rest[2],0x42);
    }
}

#[cfg(test)]が付与されたmoduleはテスト時のみビルドされるmoduleとなります. テストコードはテストの時だけビルドされればいいので, この設定を付与したtestsというmoduleをBinaryReaderを記述したファイルを同じファイルに記述します.

use super::*; はtests moduleの外側にアクセスするためのものです. これがないと, そもそもBinaryReaderがみつかりません.

肝心のテストの中身ですが, まず8バイト分詰め込んだVec<u8>型のbufferを作ります. 要素とその数はそれくらいあればいいかなという適当なものです. 次にBinaryReaderを生成し, 読み取りたいバイト数を引数に持ち, 読み取った値と残りのバイト列のタプルを返すread()メソッドを呼び出します. あとは読み取ったバイト列と残りのバイト列が正しいか否かをassert_eq!()で個別に判定して正しさを担保します. BinaryReaderをlet mutで受け取っているのは, 先のstructの形としてcurrent_indexがread()メソッド内で変化するだろう事が予測されるので可変としています.

なんとなく, やりたい形はあっていそうなので, new()とread()を実装します.

まずはnew().

impl<'a> BinaryReader<'a> {
    fn new(buffer: &'a Vec<u8>) -> Self {
        BinaryReader {
            buffer,
            current_index: 0
        }
    }
}

なんの変哲もない感じです. struct BinaryReaderのbufferは参照なのでそのライフタイムを規定する必要があるので, 'aというライフタイム内で有効という事を明示しています. 次にread()メソッドです.

impl<'a> BinaryReader<'a> {
    fn read(&mut self, size: usize) -> (&'a [u8], &'a [u8]) {
        let index = self.current_index;
        self.current_index += size;

        (&self.buffer[index..self.current_index],
         &self.buffer[self.current_index..])
    }
}

戻り値は実質的にBinaryReaderのbufferのスライスなので, そのライフタイムを合わせています. 後はbufferのスライスを返す事で読み出した分と残りの分をタプルで返します. これで先に記述したテストは通りそうです.

ところで, このread()メソッドは引数に渡ってくる読み出しバイト数とbufferのサイズの関係に無頓着なので, うっかりbufferのサイズを超えた値を設定してしまったり, 引数の値そのものはbufferのサイズに収まっていたとしても, 現在の読み出し位置であるcurrent_indexと加算した結果, bufferのサイズを超える可能性があります. また, bufferのサイズが0の時も動作が怪しいです. なので, その部分をテストを追加しつつ調整していきます.

そういえば, 0バイトを読み出そうとした場合はどうなるのかをテストしていませんでした. 期待する動作としては読み出しは0バイトなので長さ0のスライスと読み出さなかった残りのバイト列へのスライスを返す事になるはずです. ですので, そういうテストを記述します.

#[test]
fn read_zero_byte() {
    let buffer = vec![0x02, 0x34, 0x58, 0xFF];
    let mut reader = BinaryReader::new(&buffer);
    let (values, rest) = reader.read(0);
    assert_eq!(values.len(), 0);
    assert_eq!(rest.len(), 4);
    assert_eq!(rest[0], 0x02);
    assert_eq!(rest[1],0x34);
    assert_eq!(rest[2],0x58);
    assert_eq!(rest[3],0xFF);
}

これは今のコードでもテストは通ります.

さて, BinaryReaderのbufferのサイズを超えた場合を考えます. 現状ではpanicが発生しますが, できればpanicではなくエラーを返して対応をユーザーに任せられるようにしたい処. そう考えると, read()メソッドの戻り値では表現力が足りません. そこでenum Result<T, E>の出番です. read()メソッドの戻り値をResult<T, E>で書き直します. とりあえずエラーメッセージを返せば良いかなと思うので, Eの型はStringにしています.

impl<'a> BinaryReader<'a> {
    fn read(&mut self, size: usize) -> Result<(&'a [u8], &'a [u8]), String> {
        let index = self.current_index;
        self.current_index += size;

        Ok((&self.buffer[index..self.current_index],
            &self.buffer[self.current_index..]))
    }
}

戻り値をResult<T, E>に合わせて, Ok((&[u8], &[u8]))を返すように変更します. これでは戻り値の型が変わっただけなので, current_indexや引数sizeに関する条件を追加します.

fn read(&mut self, size: usize) -> Result<(&'a [u8], &'a [u8]), String> {
    let index = self.current_index;

    let max_buffer_size = self.buffer.len();
    if index > max_buffer_size {
        return Err(format!("現在の読み出し位置({})が内部バッファのサイズ({})を超えています.",
                           index,
                           max_buffer_size));
    }

    self.current_index += size;
    if self.current_index > max_buffer_size {
        return Err(format!("引数 size({}) 分読み出すと内部バッファのサイズ({})を超えます",
                           size,
                           max_buffer_size));
    }

    Ok((&self.buffer[index..self.current_index],
        &self.buffer[self.current_index..]))
}

そういえば先に実装を書くという失態を犯してしまいました. まずはテストを書くべきでした. というわけでテストを書きます. そもそも戻り値が変わったので, 既存のテストのread()を呼び出している箇所を修正します.

#[test]
fn read_bytes() {
    let buffer = vec![0x02, 0x34, 0x58, 0xFF, 0x80, 0x99, 0x00, 0x42];
    let mut reader = BinaryReader::new(&buffer);
    let (values, rest) = reader.read(5).unwrap();

    // ...
}

#[test]
fn read_zero_byte() {
    let buffer = vec![0x02, 0x34, 0x58, 0xFF];
    let mut reader = BinaryReader::new(&buffer);
    let (values, rest) = reader.read(0).unwrap();

    // ...
}

まぁ正常系なのでunwrap()でいいでしょう(適当). 次に異常系のテストケースを書いていきます.

まずは空のbufferを持つBinaryReaderから2バイト読み出そうとした場合です.

#[test]
fn read_empty_buffer() {
    let buffer = Vec::new();
    let mut reader = BinaryReader::new(&buffer);
    let result = reader.read(2);
    assert_eq!(result,
               Err("引数 size(2) 分読み出すと内部バッファのサイズ(0)を超えます".to_string()));
}

この場合, Err(E)が返されるので, エラーメッセージが期待したものかどうかで判断します.

次はバッファを超えて読み出そうとした場合のテストケースです.

#[test]
fn read_buffer_overrun() {
    let buffer = vec![0xDE, 0xAD, 0xBE, 0xEF];
    let mut reader = BinaryReader::new(&buffer);
    let result = reader.read(5);
    assert_eq!(result,
               Err("引数 size(5) 分読み出すと内部バッファのサイズ(4)を超えます".to_string()));
}

0バイトのバッファに対して0バイト読み出そうとする場合はどうしようか迷ったのですが, 空のスライスのタプルが返っても実用上問題ないと思われるのでひとまずエラー制御はせず正常系として通すようにしました.

#[test]
fn read_empty_buffer_and_zero_byte() {
    let buffer = Vec::new();
    let mut reader = BinaryReader::new(&buffer);
    let (values, rest) = reader.read(0).unwrap();
    assert_eq!(values.len(), 0);
    assert_eq!(rest.len(), 0);
}

これで特定のバイト数分をバッファから読み取る基本的な仕組みができました. ただバイトを読み取るだけでは利便性に欠けるので, 次回はバイト列を整数や浮動小数として読み出す機構をBinaryReaderに追加してみようと思います.