m13o.net

2020-12-04 Fri 23:00
Rustでバイナリを読む その4   AdventCalendar2020 Rust programming

前回まででバイナリデータから数値表現を読み出すという処まで実装しました. その数値を読み出す処で, もしバイト列がその数値表現を満たす数よりも少なかったとしたら, どうなるのでしょうか.

例えば, 3byteしかないバイト列を引数にBinaryReaderを生成したとして, read_le_u32()メソッドを使ってu32の数値を得ようとすると, read()メソッド側がResult::Err(String)を返し, read_le_u32()もread()が返した結果と同義のErrを返すようになっています. それをテストコードで表すと以下のようになります.

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

試しに cargo test を走らせてみると, このテストはパスします. やりましたね.

ではありません.

数値取得時にエラーが発生した場合, String型のメッセージのみを受け取ってそのメッセージ文字列のみで判断するのは(しかも定数ですらない文字列リテラル), 呼び出しが深くなりエラーを別のエラーに変換してユーザーに見せるような行為をしようとした途端に破綻してしまう事が想像できます.

ですので, 今回はこのエラーをわかりやすく使い勝手良くする事にフォーカスしつつ, BinaryReaderを修正していこうと思います.

Rustにはエラーを抽象的に扱うために, std::error::Error というtraitが用意されています. このtraitはRustにおけるエラーを表す物として, 紆余曲折あって色々な変遷が歴史的にあった結果, まだstableではありませんし, Deprecatedとなっているものもあります. が, 他のstd内で実装されているError系structもこのstd::error::Error traitを実装している事と, その他のcrateがメンテナンスを止めていたりする事から, これをベースに進んでいくのであろうと思われるので, このtraitをベースに考えていこうと思います.

BinaryReaderが読み込みに失敗するケースのエラーを見返してみます.

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..],
    ))
}
  • 現在の読み出し(これから読み出そうとしている位置)がバッファの末尾を超えている場合
  • 読み出したいサイズを加味したらバッファの末尾を超えている場合

明示的にエラーとして返しているのはこの2種類ですが, どちらもこのままだとバッファオーバーランをしてしまうという事を表しています. なので, まずはそれを表すstructを作り, それにstd::error::Errorを実装します.

struct BufferOverRunError {

}

std::error::Error traitを実装する際, std::fmt::Display traitとstd::fmt::Debug traitも実装しなければなりません. ひとまずメソッドのインターフェースを揃えると以下のようになります.

use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter, Debug};

impl Display for BufferOverRunError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        unimplemented!()
    }
}

impl Debug for BufferOverRunError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        unimplemented!()
    }
}

impl Error for BufferOverRunError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        unimplemented!()
    }
}

unimplemented! マクロはそのメソッドが未実装である事を明示するためにあります.

std::fmt::Display traitは主にエンドユーザーに見せるようなエラーテキストを返す事を, std::fmt::Debug traitは主に開発者のデバッグ用途に利用する事を, それぞれ目的として実装します.

まずはstd::fmt::Display traitのfmt()メソッドを実装します. ここではエンドユーザーに見せるためのメッセージなので, 詳細に表示しすぎるのもいかがなものかと思うので, このエラーがどういうエラーなのかという事を表すメッセージのみを返すようにします. まずはテストから.

#[test]
fn buffer_over_run_error_display() {
    let error = BufferOverRunError{};
    assert_eq!(format!("{}", error), "読み出し中にエラーが発生しました");
}

こうなるように, std::fmt::Display traitのfmt()メソッドを実装すると,

impl Display for BufferOverRunError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "読み出し中にエラーが発生しました")
    }
}

このように, &mut Formatter<'_>型の引数をデスクリプタとしてwrite!マクロでメッセージを書き込みます.

これでstd::fmt::Display traitの実装はできましたが, std::fmt::Debug traitの実装についてはもう少し考慮すべき事項があります. このtraitは主に開発者がデバッグ時に原因を特定するために足掛かりとなるような情報を含めておかなければなりません. BinaryReaderのread()メソッドのケースでいえば, バッファのサイズ, 現在の読み出し位置, そして読み出したいサイズが含まれていると, 何が原因でエラーが返ったのか調べやすくなります. ですので, そういった情報をどこかから取得する必要があります. これはBufferOverRunError structにメンバーフィールドとして直接持たせるのが現状は最善と思われるので, そのようにstructの定義を変更します.

struct BufferOverRunError {
    buffer_size: usize,
    index: usize,
    read_size: usize
}

buffer_sizeがバッファ全体のサイズ, indexが現在の読み出し位置, read_sizeが読み出すバイト数をそれぞれ表します. この情報を含んだメッセージをstd::fmt::Debug traitのfmt()メソッドで返すようにします.

impl Debug for BufferOverRunError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f,
               "バッファオーバーラン バッファサイズ {}, 読み出し位置 {}, 読み出しバイト数 {}",
               self.buffer_size,
               self.index,
               self.read_size)
    }
}

無機質なメッセージにはなっていますが, 開発者向けですし, これくらいでいいでしょう(エラーメッセージって考えるの結構大変ですよね……). ああっと, テストを書いていません. 書きましょう.

#[test]
fn buffer_over_run_error_debug() {
    let error = BufferOverRunError{
        buffer_size: 4,
        index: 3,
        read_size: 42
    };
    assert_eq!(format!("{:?}", error), "バッファオーバーラン バッファサイズ 4, 読み出し位置 3, 読み出しバイト数 42");
}

基本的にはstd::fmt::Display traitのfmt()メソッドと同じなのですが, format!マクロに渡している時のフォーマッタの指定の仕方が異なっている事に注意です. {}だとstd::fmt::Display traitのfmt()メソッドが, {:?}だとstd::fmt::Debug traitのfmt()メソッドが内部的に呼び出されます.

ところで, これをビルドすると, buffer_over_run_error_display()テストでコンパイルエラーが発生します. BufferOverRunError structにメンバーフィールドを追加しているので, 生成時にメンバーフィールドを初期化してあげる必要がありました. 初期化値はなんでもいいのですが, ひとまずはbuffer_over_run_error_debug()と揃えておきましょう.

#[test]
fn buffer_over_run_error_display() {
    let error = BufferOverRunError{
        buffer_size: 4,
        index: 3,
        read_size: 42
    };
    assert_eq!(format!("{}", error), "読み出し中にエラーが発生しました");
}

最後に, std::error::Error traitのsource()メソッドを実装します. が, このメソッド, このエラーが発生する原因となったstd::error::Error traitを実装しているものを返す物となっているのですが, 今回のケースですと元となるエラーが想定されていません. ですので, 今haNoneを返すだけにしておきましょう. まずはテスト.

#[test]
fn buffer_over_run_error_source() {
    let error = BufferOverRunError {
        buffer_size: 4,
        index: 3,
        read_size: 42
    };
    assert!(error.source().is_none())
}

生成してsource()メソッドの戻り値がNoneかどうかを調べるだけの簡単なお仕事. 実装も以下のように本当にNoneを返すだけです.

impl Error for BufferOverRunError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

これでBufferOverRunError structが一定の形になったので, 実際にBinaryReaderに組み込んでみましょう.

BinaryReaderのread()メソッドのエラーケースを検証しているテストは, read_empty_buffer()とread_bufer_overrun()なので, この2つのテストケースをBufferOverRunError前提のものとして改修しましょう.

ながめていると, 最後のassert_eq!マクロの第二引数を書き換えるだけで終わらせたい気持ちがでてきます. 以下のような形が望ましそうです.

#[test]
fn read_empty_buffer() {
    let buffer = Vec::new();
    let mut reader = BinaryReader::new(&buffer);
    let result = reader.read(2);
    assert_eq!(
        result,
        Err(BufferOverRunError {
            buffer_size: 0,
            index: 0,
            read_size: 2,
        })
    );
}

#[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(BufferOverRunError {
            buffer_size: 4,
            index: 0,
            read_size: 5,
        })
    );
}

この形にするためにはまず, read()メソッドの戻り値 std::result::Result<T, E>のEの型をStringからBufferOverRunErrorに変える必要があります.

fn read(&mut self, size: usize) -> Result<(&'a [u8], &'a [u8]), BufferOverRunError> {
    //...
}

戻り値の型が変わったので, エラー時に返す値の型をBUfferOverRunErrorに修正します.

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

    let max_buffer_size = self.buffer.len();
    if index > max_buffer_size {
        return Err(BufferOverRunError {
            buffer_size: max_buffer_size,
            index,
            read_size: size
        });
    }

    self.current_index += size;
    if self.current_index > max_buffer_size {
        return Err(BufferOverRunError {
            buffer_size: max_buffer_size,
            index,
            read_size: size,
        });
    }

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

良い感じになった気がしてきたのでコンパイルしてテストが通るか確認してみましょう. ……盛大にコンパイルエラーが発生しました. そうでした. read()メソッドの戻り値を変えたので, read()メソッドを呼び出している他のメソッドも修正する必要があります. インターフェースを変えるとこういう事故が発生するので, インターフェースは最初に良く考えましょうという偉大な先達たちの教えに脱帽しつつ, コンパイルエラーとなる数値を読み出すメソッドたちを修正していきましょう.

fn read_u8(&mut self) -> Result<(u8, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_i8(&mut self) -> Result<(i8, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_u16(&mut self) -> Result<(u16, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_i16(&mut self) -> Result<(i16, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_u32(&mut self) -> Result<(u32, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_i32(&mut self) -> Result<(i32, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_u24(&mut self) -> Result<(u32, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_i24(&mut self) -> Result<(i32, &'a [u8]), BufferOverRunError> {
    // ...
}

fn read_le_f32(&mut self) -> Result<(f32, &'a [u8]), BufferOverRunError> {
    // ...
}

また, この修正により read_le_u32_not_enough_byte_count() テストもErr(E)の型が変更された事に起因してコンパイルが通らなくなっているので, 以下のようにBufferOverRunErrorを見るように修正します.

#[test]
fn read_le_u32_not_enough_byte_count() {
    let buffer = vec![0x80, 0x00, 0xF0];
    let mut reader = BinaryReader::new(&buffer);
    let result = reader.read_le_u32();
    assert_eq!(
        result,
        Err(BufferOverRunError {
            buffer_size: 3,
            index: 0,
            read_size: 4
        })
    );
}

さぁこれでコンパイルも無事通……りません. Err(E)を検査するテストケースで軒並コンパイルエラーです.

error[E0369]: binary operation `==` cannot be applied to type `std::result::Result<(&[u8], &[u8]), BufferOverRunError>`

なるほど. BufferOverRunErrorは比較演算子を実装していないからそもそも比較できないという事ですね. 実装しましょう. といっても, Rustはありがたい事にこういうよくある物の実装の手間を省いてくれるためのシンタックスを用意してくれているので, それに乗っかっていきましょう. 具体的にはBufferOverRunError structを定義している箇所で以下のような#[derive]アトリビュートを付与します.

#[derive(Eq, PartialEq)]
struct BufferOverRunError {
    buffer_size: usize,
    index: usize,
    read_size: usize
}

こうする事で, BufferOverRunError structに対してEq, PartialEq traitの標準的な実装を自動で提供してくれるようになります. 便利!

ここまで実装した上で, cargo testを実行してみましょう.

running 27 tests
test tests::buffer_over_run_error_debug ... ok
test tests::buffer_over_run_error_display ... ok
test tests::buffer_over_run_error_source ... ok
test tests::read_buffer_overrun ... ok
test tests::read_bytes ... ok
test tests::read_empty_buffer ... ok
test tests::read_empty_buffer_and_zero_byte ... ok
test tests::read_i8 ... ok
test tests::read_le_f32_infinity ... ok
test tests::read_le_f32_nan ... ok
test tests::read_le_f32_neg_infinity ... ok
test tests::read_le_f32_one ... ok
test tests::read_le_i16_minus_one ... ok
test tests::read_le_i16_signed_max ... ok
test tests::read_le_i16_signed_min ... ok
test tests::read_le_i24_minus_one ... ok
test tests::read_le_i24_signed_max ... ok
test tests::read_le_i24_signed_min ... ok
test tests::read_le_i32_minus_one ... ok
test tests::read_le_i32_signed_min ... ok
test tests::read_le_i32_signed_max ... ok
test tests::read_le_u16 ... ok
test tests::read_le_u24 ... ok
test tests::read_le_u32 ... ok
test tests::read_le_u32_not_enough_byte_count ... ok
test tests::read_u8 ... ok
test tests::read_zero_byte ... ok

test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

やったぜ. std::error::Errorの実装方法も初歩的な事はこれでできるようになったので, また一つRustライフが捗るようになりました.