Exercício: Análise de Protobuf

Neste exercício, você construirá um analisador (parser) para a codificação binária de protobuf. Não se preocupe, é mais simples do que parece! Isso ilustra um padrão de análise comum, passando slices de dados. Os próprios dados subjacentes nunca são copiados.

Analisar (parse) completamente uma mensagem protobuf requer conhecer os tipos dos campos, indexados por seus números de campo. Isso é normalmente fornecido em um arquivo proto. Neste exercício, codificaremos essas informações em declarações match em funções que são chamadas para cada campo.

Usaremos o seguinte 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;
}

Uma mensagem proto é codificada como uma série de campos, um após o outro. Cada um é implementado como uma "tag" seguida pelo valor. A tag contém um número de campo (por exemplo, 2 para o campo id de uma mensagem Person) e um tipo de fio (wire type) definindo como a carga útil deve ser determinada a partir do fluxo (stream) de bytes.

Números inteiros, incluindo a tag, são representados com uma codificação de comprimento variável chamada VARINT. Felizmente, parse_varint é definido para você abaixo. O código fornecido também define callbacks para lidar com campos Person e PhoneNumber, e para analisar uma mensagem em uma série de chamadas para esses callbacks.

O que resta para você é implementar a função parse_field e o trait ProtoMessage para Person e PhoneNumber.

/// Um wire type como visto no wire.
enum WireType {
    /// O Varint WireType indica que o valor é um único VARINT.
    Varint,
    //I64,  -- não é necessário para este exercício
    /// O Len WireType indica que o valor é um comprimento representado como um
    /// VARINT seguido exatamente por esse número de bytes.
    Len,
    /// O I32 WireType indica que o valor é precisamente 4 bytes em
    /// ordem little-endian contendo um inteiro de 32 bits com sinal.
    I32,
}

#[derive(Debug)]
/// O valor de um campo, digitado com base no wire type.
enum FieldValue<'a> {
    Varint(u64),
    //I64(i64),  -- não é necessário para este exercício
    Len(&'a [u8]),
    I32(i32),
}

#[derive(Debug)]
/// Um campo, contendo o número do campo e seu valor.
struct Field<'a> {
    field_num: u64,
    value: FieldValue<'a>,
}

trait ProtoMessage<'a>: Default + 'a {
    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,  -- não é necessário para este exercício
            2 => WireType::Len,
            5 => WireType::I32,
            _ => panic!("Wire-type inválido: {value}"),
        }
    }
}

impl<'a> FieldValue<'a> {
    fn as_string(&self) -> &'a str {
        let FieldValue::Len(data) = self else {
            panic!("Esperava-se que a string fosse um campo `Len`");
        };
        std::str::from_utf8(data).expect("String inválida")
    }

    fn as_bytes(&self) -> &'a [u8] {
        let FieldValue::Len(data) = self else {
            panic!("Esperava-se que os bytes fossem um campo `Len`");
        };
        data
    }

    fn as_u64(&self) -> u64 {
        let FieldValue::Varint(value) = self else {
            panic!("Esperava-se que `u64` fosse um campo `Varint");
        };
        *value
    }

    #[allow(dead_code)]
    fn as_i32(&self) -> i32 {
        let FieldValue::I32(value) = self else {
            panic!("Esperava-se que `i32` fosse um campo `I32");
        };
        *value
    }
}

/// Analise (_parse_) um VARINT, retornando o valor analisado e os bytes restantes.
fn parse_varint(data: &[u8]) -> (u64, &[u8]) {
    for i in 0..7 {
        let Some(b) = data.get(i) else {
            panic!("Não há bytes suficientes para o varint");
        };
        if b & 0x80 == 0 {
            // Este é o último byte do VARINT, então converta-o para
            // um u64 e retorne-o.
            let mut value = 0u64;
            for b in data[..=i].iter().rev() {
                value = (value << 7) | (b & 0x7f) as u64;
            }
            return (value, &data[i + 1..]);
        }
    }

    // Mais de 7 bytes é inválido.
    panic!("Bytes demais para varint");
}

/// Converta uma tag em um número de campo e um WireType.
fn unpack_tag(tag: u64) -> (u64, WireType) {
    let field_num = tag >> 3;
    let wire_type = WireType::from(tag & 0x7);
    (field_num, wire_type)
}


/// Analise (_parse_) um campo, retornando os bytes restantes
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!("Com base no wire type, construa um Field, consumindo quantos bytes forem necessários.")
    };
    todo!("Retorne o campo e quaisquer bytes não consumidos.")
}

/// Analise (_parse_) uma mensagem nos dados fornecidos, chamando `T::add_field` para cada campo na
/// mensagem.
///
/// Todo o input é consumido.
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: Implemente ProtoMessage para Person e 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.
  • Neste exercício, há vários casos em que a análise de protobuf pode falhar, por exemplo, se você tentar analisar um i32 quando houver menos de 4 bytes restantes no buffer de dados. Em código Rust normal, lidaríamos com isso com o enum Result, mas para simplicidade neste exercício, lançamos um pânico se ocorrerem erros. No dia 4, abordaremos o tratamento de erros em Rust com mais detalhes.