تمرین: تجزیه Protobuf

در این تمرین، شما یک تجزیه‌کننده برای رمزگذاری باینری پروتوباف خواهید ساخت. نگران نباشید، این کار ساده‌تر از آن است که به نظر می‌رسد! این الگو نشان‌دهنده یک الگوی رایج در تجزیه داده‌ها است که شامل عبور برش‌های داده است. داده‌های اصلی هرگز کپی نمی‌شوند.

تجزیه کامل یک پیام پروتوباف نیاز به دانستن تایپ‌های این فیلدها دارد که بر اساس شماره‌های فیلد ایندکس شده‌اند. این اطلاعات معمولاً در یک فایل proto ارائه می‌شود. در این تمرین، ما این اطلاعات را به صورت عبارات match در توابعی که برای هر فیلد فراخوانی می‌شوند، کدگذاری خواهیم کرد.

ما از پروتوباف زیر استفاده خواهیم کرد:

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

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

یک پیام پروتوباف به عنوان مجموعه‌ای از فیلدها، یکی پس از دیگری، کدگذاری می‌شود. هر فیلد به صورت یک "تگ" به همراه مقدار آن پیاده‌سازی شده است. تگ شامل شماره فیلد (مانند 2 برای فیلد id در پیام Person) و wire type است که نحوه تعیین بار را از جریان بایت مشخص می‌کند.

اعداد، از جمله تگ، با استفاده از کدگذاری با طول متغیر به نام VARINT نمایندگی می‌شوند. خوشبختانه، تابع parse_varint برای شما تعریف شده است. کد داده شده همچنین بازخوانی‌هایی برای مدیریت فیلدهای Person و PhoneNumber و تجزیه یک پیام به مجموعه‌ای از فراخوانی‌ها به آن بازخوانی‌ها را تعریف می‌کند.

برای شما باقی‌مانده است که تابع parse_field و ویژگی ProtoMessage را برای Person و PhoneNumber پیاده‌سازی کنید.

/// 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!("نوع سیم نامعتبر: {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("نامعتبر string")
    }

    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
    }
}

/// 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!("بایت کافی برای 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!("تعداد بایت‌های زیادی برای 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 Person<'a> {
    name: &'a str,
    id: u64,
    phone: Vec<PhoneNumber<'a>>,
}

// TODO: Implement ProtoMessage for Person and 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 را هنگامی که کمتر از ۴ بایت در بافر داده باقی‌مانده است، تجزیه کنید. در کد Rust معمولاً این را با استفاده از Result مدیریت می‌کنیم، اما برای سادگی در این تمرین، اگر با هرگونه خطا مواجه شویم، به جای آن که با Result برخورد کنیم، برنامه را متوقف خواهیم کرد. در روز چهارم، به بررسی دقیق‌تر مدیریت خطا در Rust خواهیم پرداخت.