演習: Protobufの解析

この演習では、protobuf バイナリ エンコード用のパーサーを作成します。見かけよりも簡単ですので、心配はいりません。これは、データのスライスを渡す一般的な解析パターンを示しています。基になるデータ自体がコピーされることはありません。

protobuf メッセージを完全に解析するには、フィールド番号でインデックス付けされたフィールドの型を知る必要があります。これは通常、proto ファイルで提供されます。この演習では、フィールドごとに呼び出される関数の match ステートメントに、その情報をエンコードします。

次の proto を使用します。

message PhoneNumber {
  optional string number = 1;
  optional string type = 2;
}

message Person {
  optional string name = 1;
  optional int32 id = 2;
  repeated PhoneNumber phones = 3;
}

proto メッセージは、連続するフィールドとしてエンコードされます。それぞれが後ろに値を伴う「タグ」として実装されます。タグにはフィールド番号(例: Person メッセージの id フィールドには 2)と、バイト ストリームからペイロードがどのように決定されるかを定義するワイヤータイプが含まれます。

タグを含む整数は、VARINT と呼ばれる可変長エンコードで表されます。幸いにも、parse_varint は以下ですでに定義されています。また、このコードでは、Person フィールドと PhoneNumber フィールドを処理し、メッセージを解析してこれらのコールバックに対する一連の呼び出しに変換するコールバックも定義しています。

残る作業は、parse_field 関数と、Person および PhoneNumberProtoMessage トレイトを実装するだけです。

/// ワイヤー上で見えるワイヤータイプ。
enum WireType {
    /// Varint WireType は、値が単一の VARINT であることを示します。
    Varint,
    /// The I64 WireType indicates that the value is precisely 8 bytes in
    /// little-endian order containing a 64-bit signed integer or double type.
    //I64,  -- not needed for this exercise
    /// The Len WireType indicates that the value is a length represented as a
    /// VARINT followed by exactly that number of bytes.
    Len,
    // The I32 WireType indicates that the value is precisely 4 bytes in
    // little-endian order containing a 32-bit signed integer or float type.
    //I32,  -- not needed for this exercise
}

#[derive(Debug)]
/// ワイヤータイプに基づいて型指定されたフィールドの値。
enum FieldValue<'a> {
    Varint(u64),
    //I64(i64)、  -- この演習では不要
    Len(&'a [u8]),
    //I32(i32),  -- not needed for this exercise
}

#[derive(Debug)]
/// フィールド番号とその値を含むフィールド。
struct Field<'a> {
    field_num: u64,
    value: FieldValue<'a>,
}

trait ProtoMessage<'a>: Default {
    fn add_field(&mut self, field: Field<'a>);
}

impl From<u64> for WireType {
    fn from(value: u64) -> Self {
        match value {
            0 => WireType::Varint,
            //1 => WireType::I64、  -- この演習では不要
            2 => WireType::Len,
            //5 => WireType::I32,  -- not needed for this exercise
            _ => panic!("Invalid wire type: {value}"),
        }
    }
}

impl<'a> FieldValue<'a> {
    fn as_str(&self) -> &'a str {
        let FieldValue::Len(data) = self else {
            panic!("Expected string to be a `Len` field");
        };
        std::str::from_utf8(data).expect("Invalid string")
    }

    fn as_bytes(&self) -> &'a [u8] {
        let FieldValue::Len(data) = self else {
            panic!("Expected bytes to be a `Len` field");
        };
        data
    }

    fn as_u64(&self) -> u64 {
        let FieldValue::Varint(value) = self else {
            panic!("Expected `u64` to be a `Varint` field");
        };
        *value
    }
}

/// VARINT を解析し、解析した値と残りのバイトを返します。
fn parse_varint(data: &[u8]) -> (u64, &[u8]) {
    for i in 0..7 {
        let Some(b) = data.get(i) else {
            panic!("Not enough bytes for varint");
        };
        if b & 0x80 == 0 {
            // これは VARINT の最後のバイトであるため、
            // u64 に変換して返します。
            let mut value = 0u64;
            for b in data[..=i].iter().rev() {
                value = (value << 7) | (b & 0x7f) as u64;
            }
            return (value, &data[i + 1..]);
        }
    }

    // 7 バイトを超える値は無効です。
    panic!("Too many bytes for varint");
}

/// タグをフィールド番号と WireType に変換します。
fn unpack_tag(tag: u64) -> (u64, WireType) {
    let field_num = tag >> 3;
    let wire_type = WireType::from(tag & 0x7);
    (field_num, wire_type)
}


/// フィールドを解析して残りのバイトを返します。
fn parse_field(data: &[u8]) -> (Field, &[u8]) {
    let (tag, remainder) = parse_varint(data);
    let (field_num, wire_type) = unpack_tag(tag);
    let (fieldvalue, remainder) = match wire_type {
        _ => todo!("ワイヤータイプに応じて、フィールドを構築し、必要な量のバイトを消費します。")
    };
    todo!("フィールドと、未消費のバイトを返します。")
}

/// 指定されたデータ内のメッセージを解析し、メッセージのフィールドごとに
/// `T::add_field` を呼び出します。
///
/// 入力全体が消費されます。
fn parse_message<'a, T: ProtoMessage<'a>>(mut data: &'a [u8]) -> T {
    let mut result = T::default();
    while !data.is_empty() {
        let parsed = parse_field(data);
        result.add_field(parsed.0);
        data = parsed.1;
    }
    result
}

#[derive(Debug, Default)]
struct PhoneNumber<'a> {
    number: &'a str,
    type_: &'a str,
}

#[derive(Debug, Default)]
struct Person<'a> {
    name: &'a str,
    id: u64,
    phone: Vec<PhoneNumber<'a>>,
}

// TODO: Person と PhoneNumber の ProtoMessage を実装します。

fn main() {
    let person: Person = parse_message(&[
        0x0a, 0x07, 0x6d, 0x61, 0x78, 0x77, 0x65, 0x6c, 0x6c, 0x10, 0x2a, 0x1a,
        0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x30, 0x32, 0x2d, 0x35, 0x35, 0x35,
        0x2d, 0x31, 0x32, 0x31, 0x32, 0x12, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x1a,
        0x18, 0x0a, 0x0e, 0x2b, 0x31, 0x38, 0x30, 0x30, 0x2d, 0x38, 0x36, 0x37,
        0x2d, 0x35, 0x33, 0x30, 0x38, 0x12, 0x06, 0x6d, 0x6f, 0x62, 0x69, 0x6c,
        0x65,
    ]);
    println!("{:#?}", person);
}
This slide and its sub-slides should take about 30 minutes.
  • In this exercise there are various cases where protobuf parsing might fail, e.g. if you try to parse an i32 when there are fewer than 4 bytes left in the data buffer. In normal Rust code we'd handle this with the Result enum, but for simplicity in this exercise we panic if any errors are encountered. On day 4 we'll cover error handling in Rust in more detail.