Вправа: Розбір 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 кодується як серія полів, що йдуть одне за одним. Кожне з них реалізовано у вигляді "тегу", за яким слідує значення. Тег містить номер поля (наприклад, 2 для поля id у повідомленні Person) і тип передачі, який визначає спосіб визначення корисного навантаження з потоку байт.

Цілі числа, включаючи тег, подаються у кодуванні змінної довжини, яке називається VARINT. На щастя, нижче визначено parse_varint для вас. Наведений код також визначає виклики для обробки полів Person і PhoneNumber, а також для розбору повідомлення на серію викликів цих зворотних викликів.

Вам залишається реалізувати функцію parse_field та трейт ProtoMessage для Person та PhoneNumber.

/// wire type як він приходить по дроту.
enum WireType {
    /// Varint WireType вказує на те, що значення є одним VARINT.
    Varint,
    /// I64 WireType вказує на те, що значення має точно 8 байт у little-endian
    /// порядку та містить 64-бітне ціле зі знаком або тип з плаваючою комою подвійної точності.
//I64,  -- не потрібно для цієї вправи
    /// Len WireType вказує на те, що значення є довжиною, представленою у вигляді 
    /// VARINT за яким слідує рівно стільки байт.
    Len,
    // Тип WireType I32 вказує на те, що значення - це рівно 4 байти в
    // little-endian порядку, що містять 32-бітне ціле число зі знаком або тип з плаваючою комою.
    //I32,  -- не потрібно для цієї вправи
}

#[derive(Debug)]
/// Значення поля, введене на основі wire type.
enum FieldValue<'a> {
    Varint(u64),
    //I64(i64),  -- не потрібно для цієї вправи
    Len(&'a [u8]),
    //I32(i32),  -- не потрібно для цієї вправи
}

#[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,  -- не потрібно для цієї вправи
            _ => panic!("Неправильний wire type: {value}"),
        }
    }
}

impl<'a> FieldValue<'a> {
    fn as_str(&self) -> &'a str {
        let FieldValue::Len(data) = self else {
            panic!("Очікуваний рядок має бути полем `Len`");
        };
        std::str::from_utf8(data).expect("Неправильний рядок")
    }

    fn as_bytes(&self) -> &'a [u8] {
        let FieldValue::Len(data) = self else {
            panic!("Очікувані байти мають бути полем `Len`");
        };
        data
    }

    fn as_u64(&self) -> u64 {
        let FieldValue::Varint(value) = self else {
            panic!("Очікувалося, що `u64` буде полем `Varint`");
        };
        *value
    }
}

/// Розбір VARINT з поверненням розібраного значення та решти байтів.
fn parse_varint(data: &[u8]) -> (u64, &[u8]) {
    for i in 0..7 {
        let Some(b) = data.get(i) else {
            panic!("Недостатньо байт для 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!("Забагато байт для 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!("На основі wire type побудуйте Field, використовуючи стільки байт, скільки потрібно.")
    };
    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 Person<'a> {
    name: &'a str,
    id: u64,
    phone: Vec<PhoneNumber<'a>>,
}

// TODO: Реалізувати ProtoMessage для Person та PhoneNumber.

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.
  • У цій вправі існують різні випадки, коли розбір protobuf може не спрацювати, наприклад, якщо ви спробуєте розібрати i32, коли у буфері даних залишилося менше 4 байт. У звичайному Rust-коді ми б впоралися з цим за допомогою переліку Result, але для простоти у цій вправі ми панікуємо, якщо виникають помилки. На четвертий день ми розглянемо обробку помилок у Rust більш детально.