m13o.net

2020-12-03 Thu 23:00
Rustでバイナリを読む その3   AdventCalendar2020 Rust programming

前回は数値表現の内, 32bitまでの整数について読み取る機能を作りましたので, 今回は浮動小数を読み取る仕組みの作成に取りかかろうと思います. 整数は32bit幅までとしていたので, 単精度の浮動小数についてのみ考える事とします.

例によって例の如く, 動作イメージを兼ねてバイト列から1.0を取り出すようなテストコードを書いています.

#[test]
fn read_le_f32_one() {
    let buffer = vec![/* どういうテストデータになるんだ? */];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert_eq!(value, 1.0);
}

メソッド名は整数系のものにならってread_le_f32()でいいとして, そもそもRustの単精度浮動小数を表す型であるf32における1.0を表すバイト列はどういったものになるのでしょう?

f32のドキュメントによると,

A 32-bit floating point type (specifically, the "binary32" type defined in IEEE 754-2008).

とありますので, IEEE754-2008の仕様に基づいている事がわかります.

IEEE754-2008の詳細は割愛するとして, 基本的に以下の形式をbit列に表せるよう定めています.

  • 1.0, -1.2345, 0.0などのいわゆる小数と呼ばれる数
  • -0
  • 正の無限大と負の無限大
  • 非正規化数
  • 0.0/0.0 の算術結果にあらわされるような数ではない数(NaN)

f32のような単精度浮動小数のbit表現は

  • 符号: 1bit
  • 指数部: 8bit
  • 仮数部: 23bit

となっています.

この32bit表現に対しどういうバイト列として値を表現すればテストデータ足りうるかを考えます. とはいえ, 単精度浮動小数そのものの検証としてはf32側で担保されているので, 適当な32bitのバイト列がこちらが思う通りの物として読み取れているかどうかの検証をすればBinaryReaderとしてのテストとしては十分と考えます.

なので, 正常系として, 単精度浮動小数1.0のバイト列表現がどうなっているかを考えると, IEEE754-2008の仕様により,

  • 符号: 0b0
  • 指数部: 0b01111111
  • 仮数部: 0b00000000000000000000000

となるので, これを16進数で表すと0x3f800000となります. リトルエンディアンのバイト列を考えているので, バイト列としては, [0x00, 0x00, 0x80, 0x3f]という並びになるはずです. というわけで先程のテストコードで記述できていなかった部分を埋めたテストコードがこちら.

#[test]
fn read_le_f32_one() {
    let buffer = vec![0x00, 0x00, 0x80, 0x3f, 0xFD];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert_eq!(value, 1.0);
    assert_eq!(rest.len(), 1);
    assert_eq!(rest[0], 253);
}

5バイト目はちゃんと4バイトまでしか読んでないという事を表すためだけにあるので適当な値です.

これで1.0に関するテストケースができたので, 本体であるread_le_f32()メソッドを実装しています.

fn read_le_f32(&mut self) -> Result<(f32, &'a [u8]), String> {
    match self.read(4) {
        Ok((f32_bytes, rest)) => {
            let value = f32::from_le_bytes(f32_bytes.try_into().unwrap());
            Ok((value, rest))
        }
        Err(e) => Err(e)
    }
}

やっている事は整数の時と同様なので, 代わり映えはありません.

次に正の無限大, 負の無限大についてです. これらは指数部8bitが全て1で, 仮数部は0で構成される事が規定されています. ですので, 各々の16進数表現は次のようになります.

  • 正の無限大: 0x7F800000
  • 負の無限大: 0xFF800000
#[test]
fn read_le_f32_infinity() {
    let buffer = vec![0x00, 0x00, 0x80, 0x7F, 0xFD];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert_eq!(value, f32::INFINITY);
    assert_eq!(rest.len(), 1);
    assert_eq!(rest[0], 253);
}

#[test]
fn read_le_f32_neg_infinity() {
    let buffer = vec![0x00, 0x00, 0x80, 0xFF, 0xFD];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert_eq!(value, f32::NEG_INFINITY);
    assert_eq!(rest.len(), 1);
    assert_eq!(rest[0], 253);
}

NaNについてテストケースも書いてみましょう. 指数部8bitが全て1で, 仮数部が0以外の値を取る場合がNaNであると規定されているので, これまでと同様にテストケースを書くと……

#[test]
fn read_le_f32_nan() {
    let buffer = vec![0x01, 0x00, 0x80, 0x7F, 0xFD];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert_eq!(value, f32::NAN);
    assert_eq!(rest.len(), 1);
    assert_eq!(rest[0], 253);
}

おや? テストがパスしません.

そうでした. 規格上NaNは自分自身とでさえ一致しない値なので, 読み取ったバイト列の計算結果とf32::NANとを比較しても一致する事はありません. f32には自身がNaNであるかどうかを判定するために is_nan()メソッドが実装されているので, こちらで判定するようにしましょう.

#[test]
fn read_le_f32_nan() {
    let buffer = vec![0x01, 0x00, 0x80, 0x7F, 0xFD];
    let mut reader = BinaryReader::new(&buffer);
    let (value, rest) = reader.read_le_f32().unwrap();
    assert!(value.is_nan());
    assert_eq!(rest.len(), 1);
    assert_eq!(rest[0], 253);
}

無事テストが通りました.

ここまでで, バイト列を数値表現に変換する仕組みができあがりましたので, 次回はこれを使ってより実践的な何かを構築できればと思います.