演習: 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 メッセージは、フィールドが 1 つずつ順番に並んだものとしてエンコードされます。各フィールドは 「tag」とそれに続く値で実装されます。tag にはフィールド番号 (たとえば、Person メッセージの id フィールドなら 2)と、バイトストリームから ペイロードをどのように決定すべきかを定義するワイヤー型が含まれます。これらは 1 つの整数にまとめられ、以下の unpack_tag でデコードされます。
Varint
tag を含む整数は、VARINT と呼ばれる可変長エンコーディングで表現されます。 幸い、parse_varint は以下で定義済みです。
ワイヤー型
Proto ではいくつかのワイヤー型が定義されていますが、この演習で使うのはそのうち 2 つだけです。
Varint ワイヤー型には 1 つの varint が含まれ、Person.id のような int32 型の proto 値をエンコードするために使われます。
Len ワイヤー型には、varint で表現された長さと、それに続くそのバイト数の ペイロードが含まれます。これは、Person.name のような string 型の proto 値を エンコードするために使われます。また、Person.phones のようにサブメッセージを 含む proto 値のエンコードにも使われ、その場合ペイロードにはサブメッセージの エンコーディングが含まれます。
演習
与えられたコードでは、Person と PhoneNumber のフィールドを処理するコールバックと、 メッセージをパースしてそれらのコールバック呼び出し列に変換する処理も定義されています。
あなたに残されているのは、parse_field 関数と、Person および PhoneNumber に対する ProtoMessage トレイトを実装することです。
// Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 /// A wire type as seen on the wire. enum WireType { /// The Varint WireType indicates the value is a single 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)] /// A field's value, typed based on the wire type. enum FieldValue<'a> { Varint(u64), //I64(i64), -- not needed for this exercise Len(&'a [u8]), //I32(i32), -- not needed for this exercise } #[derive(Debug)] /// A field, containing the field number and its value. 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, -- not needed for this exercise 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 } } /// Parse a VARINT, returning the parsed value and the remaining bytes. 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 { // This is the last byte of the VARINT, so convert it to // a u64 and return it. let mut value = 0u64; for b in data[..=i].iter().rev() { value = (value << 7) | (b & 0x7f) as u64; } return (value, &data[i + 1..]); } } // More than 7 bytes is invalid. panic!("Too many bytes for varint"); } /// Convert a tag into a field number and a WireType. fn unpack_tag(tag: u64) -> (u64, WireType) { let field_num = tag >> 3; let wire_type = WireType::from(tag & 0x7); (field_num, wire_type) } /// Parse a field, returning the remaining bytes 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!("フィールドと、未消費のバイトを返します。") } /// Parse a message in the given data, calling `T::add_field` for each field in /// the message. /// /// The entire input is consumed. 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 を実装する。 #[test] fn test_id() { let person_id: Person = parse_message(&[0x10, 0x2a]); assert_eq!(person_id, Person { name: "", id: 42, phone: vec![] }); } #[test] fn test_name() { let person_name: Person = parse_message(&[ 0x0a, 0x0e, 0x62, 0x65, 0x61, 0x75, 0x74, 0x69, 0x66, 0x75, 0x6c, 0x20, 0x6e, 0x61, 0x6d, 0x65, ]); assert_eq!(person_name, Person { name: "beautiful name", id: 0, phone: vec![] }); } #[test] fn test_just_person() { let person_name_id: Person = parse_message(&[0x0a, 0x04, 0x45, 0x76, 0x61, 0x6e, 0x10, 0x16]); assert_eq!(person_name_id, Person { name: "Evan", id: 22, phone: vec![] }); } #[test] fn test_phone() { let phone: Person = parse_message(&[ 0x0a, 0x00, 0x10, 0x00, 0x1a, 0x16, 0x0a, 0x0e, 0x2b, 0x31, 0x32, 0x33, 0x34, 0x2d, 0x37, 0x37, 0x37, 0x2d, 0x39, 0x30, 0x39, 0x30, 0x12, 0x04, 0x68, 0x6f, 0x6d, 0x65, ]); assert_eq!( phone, Person { name: "", id: 0, phone: vec![PhoneNumber { number: "+1234-777-9090", type_: "home" },], } ); } // Put that all together into a single parse. #[test] fn test_full_person() { 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, ]); assert_eq!( person, Person { name: "maxwell", id: 42, phone: vec![ PhoneNumber { number: "+1202-555-1212", type_: "home" }, PhoneNumber { number: "+1800-867-5308", type_: "mobile" }, ] } ); }
- この演習では、protobuf のパースが失敗する可能性のあるケースがさまざまにあります。 たとえば、データバッファに残っているバイト数が 4 未満なのに
i32をパースしようとする 場合です。通常の Rust コードであればこれをResult列挙型で処理しますが、 この演習では簡単にするため、エラーが発生した場合は panic します。4 日目には、 Rust のエラーハンドリングをさらに詳しく扱います。