Skip to main content

zerodds_proto/
descriptor.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! A lightweight `FileDescriptorSet` AST and its decoder — the subset of
4//! `descriptor.proto` needed to build XTypes types: files, messages (incl.
5//! nested), fields (name / number / label / type / type_name) and enums.
6//!
7//! Parsed with [`crate::reader`]; every decode returns `None` on a truncated
8//! or malformed descriptor rather than panicking.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::ProtoFieldType;
14use crate::reader::{Reader, WireType};
15
16/// `FieldDescriptorProto.Label` — a field's cardinality.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Label {
19    /// proto2 `optional` (or proto3 singular).
20    Optional,
21    /// `required` (proto2 only).
22    Required,
23    /// `repeated` -> maps to `sequence<T>`.
24    Repeated,
25}
26
27impl Label {
28    fn from_wire(v: i32) -> Self {
29        match v {
30            2 => Self::Required,
31            3 => Self::Repeated,
32            _ => Self::Optional,
33        }
34    }
35}
36
37/// One field of a message (`FieldDescriptorProto`).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Field {
40    /// Field name.
41    pub name: String,
42    /// Field number -> XTypes member id.
43    pub number: i32,
44    /// Cardinality.
45    pub label: Label,
46    /// Field type; `None` if the descriptor carried an out-of-range type code.
47    pub ty: Option<ProtoFieldType>,
48    /// For message/enum/group fields: the fully-qualified type name (leading
49    /// `.`), else empty.
50    pub type_name: String,
51    /// `FieldDescriptorProto.oneof_index` — the index into the message's
52    /// `oneof_decl` list if this field is part of a `oneof`, else `None`.
53    pub oneof_index: Option<i32>,
54}
55
56/// A message type (`DescriptorProto`), possibly with nested messages/enums.
57#[derive(Debug, Clone, PartialEq, Eq, Default)]
58pub struct Message {
59    /// Message name (unqualified).
60    pub name: String,
61    /// Fields, in declaration order.
62    pub fields: Vec<Field>,
63    /// Nested message types.
64    pub nested: Vec<Message>,
65    /// Nested enum types.
66    pub enums: Vec<Enum>,
67    /// `MessageOptions.map_entry` — set on the synthetic `*Entry` message that
68    /// `protoc` generates for a `map<K,V>` field (key = field 1, value = 2).
69    pub map_entry: bool,
70    /// `oneof_decl` names, in declaration order. A field's `oneof_index` is an
71    /// index into this list.
72    pub oneofs: Vec<String>,
73}
74
75/// An enum type (`EnumDescriptorProto`).
76#[derive(Debug, Clone, PartialEq, Eq, Default)]
77pub struct Enum {
78    /// Enum name.
79    pub name: String,
80    /// `(name, number)` pairs in declaration order.
81    pub values: Vec<(String, i32)>,
82}
83
84/// A single `.proto` file (`FileDescriptorProto`).
85#[derive(Debug, Clone, PartialEq, Eq, Default)]
86pub struct File {
87    /// The `package` declaration (may be empty).
88    pub package: String,
89    /// Top-level messages.
90    pub messages: Vec<Message>,
91    /// Top-level enums.
92    pub enums: Vec<Enum>,
93}
94
95/// A `FileDescriptorSet` — the output of `protoc -o out.pb`.
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
97pub struct FileDescriptorSet {
98    /// The described files.
99    pub files: Vec<File>,
100}
101
102fn read_str(r: &mut Reader<'_>) -> Option<String> {
103    core::str::from_utf8(r.read_len_delimited()?)
104        .ok()
105        .map(String::from)
106}
107
108/// zerodds-lint: recursion-depth 64 (message nesting; bounded by .proto depth)
109fn parse_message(bytes: &[u8]) -> Option<Message> {
110    let mut m = Message::default();
111    let mut r = Reader::new(bytes);
112    while !r.at_end() {
113        let (field, wt) = r.read_tag()?;
114        match (field, wt) {
115            (1, WireType::Len) => m.name = read_str(&mut r)?, // name
116            (2, WireType::Len) => m.fields.push(parse_field(r.read_len_delimited()?)?), // field
117            (3, WireType::Len) => m.nested.push(parse_message(r.read_len_delimited()?)?), // nested_type
118            (4, WireType::Len) => m.enums.push(parse_enum(r.read_len_delimited()?)?), // enum_type
119            (7, WireType::Len) => m.map_entry = parse_message_options(r.read_len_delimited()?)?, // options
120            (8, WireType::Len) => m.oneofs.push(parse_oneof(r.read_len_delimited()?)?), // oneof_decl
121            (_, wt) => r.skip(wt)?,
122        }
123    }
124    Some(m)
125}
126
127fn parse_field(bytes: &[u8]) -> Option<Field> {
128    let mut name = String::new();
129    let mut number = 0i32;
130    let mut label = Label::Optional;
131    let mut ty = None;
132    let mut type_name = String::new();
133    let mut oneof_index = None;
134    let mut r = Reader::new(bytes);
135    while !r.at_end() {
136        let (field, wt) = r.read_tag()?;
137        match (field, wt) {
138            (1, WireType::Len) => name = read_str(&mut r)?, // name
139            (3, WireType::Varint) => number = i32::try_from(r.read_varint()?).ok()?, // number
140            (4, WireType::Varint) => {
141                label = Label::from_wire(i32::try_from(r.read_varint()?).ok()?)
142            } // label
143            (5, WireType::Varint) => {
144                ty = ProtoFieldType::from_wire(i32::try_from(r.read_varint()?).ok()?)
145            } // type
146            (6, WireType::Len) => type_name = read_str(&mut r)?, // type_name
147            (9, WireType::Varint) => oneof_index = Some(i32::try_from(r.read_varint()?).ok()?), // oneof_index
148            (_, wt) => r.skip(wt)?,
149        }
150    }
151    Some(Field {
152        name,
153        number,
154        label,
155        ty,
156        type_name,
157        oneof_index,
158    })
159}
160
161/// Parse an `OneofDescriptorProto`, returning its `name` (field 1).
162fn parse_oneof(bytes: &[u8]) -> Option<String> {
163    let mut name = String::new();
164    let mut r = Reader::new(bytes);
165    while !r.at_end() {
166        let (field, wt) = r.read_tag()?;
167        match (field, wt) {
168            (1, WireType::Len) => name = read_str(&mut r)?, // name
169            (_, wt) => r.skip(wt)?,
170        }
171    }
172    Some(name)
173}
174
175/// Parse `MessageOptions`, returning `map_entry` (field 7, bool). Other options
176/// are skipped.
177fn parse_message_options(bytes: &[u8]) -> Option<bool> {
178    let mut map_entry = false;
179    let mut r = Reader::new(bytes);
180    while !r.at_end() {
181        let (field, wt) = r.read_tag()?;
182        match (field, wt) {
183            (7, WireType::Varint) => map_entry = r.read_varint()? != 0, // map_entry
184            (_, wt) => r.skip(wt)?,
185        }
186    }
187    Some(map_entry)
188}
189
190fn parse_enum(bytes: &[u8]) -> Option<Enum> {
191    let mut e = Enum::default();
192    let mut r = Reader::new(bytes);
193    while !r.at_end() {
194        let (field, wt) = r.read_tag()?;
195        match (field, wt) {
196            (1, WireType::Len) => e.name = read_str(&mut r)?, // name
197            (2, WireType::Len) => {
198                // EnumValueDescriptorProto { name(1), number(2) }
199                let mut vr = Reader::new(r.read_len_delimited()?);
200                let mut vname = String::new();
201                let mut vnum = 0i32;
202                while !vr.at_end() {
203                    let (vf, vwt) = vr.read_tag()?;
204                    match (vf, vwt) {
205                        (1, WireType::Len) => vname = read_str(&mut vr)?,
206                        (2, WireType::Varint) => vnum = i32::try_from(vr.read_varint()?).ok()?,
207                        (_, vwt) => vr.skip(vwt)?,
208                    }
209                }
210                e.values.push((vname, vnum));
211            }
212            (_, wt) => r.skip(wt)?,
213        }
214    }
215    Some(e)
216}
217
218fn parse_file(bytes: &[u8]) -> Option<File> {
219    let mut f = File::default();
220    let mut r = Reader::new(bytes);
221    while !r.at_end() {
222        let (field, wt) = r.read_tag()?;
223        match (field, wt) {
224            (2, WireType::Len) => f.package = read_str(&mut r)?, // package
225            (4, WireType::Len) => f.messages.push(parse_message(r.read_len_delimited()?)?), // message_type
226            (5, WireType::Len) => f.enums.push(parse_enum(r.read_len_delimited()?)?), // enum_type
227            (_, wt) => r.skip(wt)?,
228        }
229    }
230    Some(f)
231}
232
233impl FileDescriptorSet {
234    /// Decode a `FileDescriptorSet` from `protoc -o out.pb` bytes. `None` on a
235    /// truncated or malformed descriptor.
236    #[must_use]
237    pub fn decode(bytes: &[u8]) -> Option<Self> {
238        let mut set = Self::default();
239        let mut r = Reader::new(bytes);
240        while !r.at_end() {
241            let (field, wt) = r.read_tag()?;
242            match (field, wt) {
243                (1, WireType::Len) => set.files.push(parse_file(r.read_len_delimited()?)?), // file
244                (_, wt) => r.skip(wt)?,
245            }
246        }
247        Some(set)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    #![allow(clippy::unwrap_used)]
254    use super::*;
255    use alloc::vec;
256
257    #[test]
258    fn parses_a_field_descriptor() {
259        // FieldDescriptorProto { name="x"(1), number=1(3), type=INT32=5(5) }
260        let bytes = [0x0a, 0x01, b'x', 0x18, 0x01, 0x28, 0x05];
261        let f = parse_field(&bytes).unwrap();
262        assert_eq!(f.name, "x");
263        assert_eq!(f.number, 1);
264        assert_eq!(f.ty, Some(ProtoFieldType::Int32));
265        assert_eq!(f.label, Label::Optional);
266    }
267
268    #[test]
269    fn parses_map_entry_option() {
270        // DescriptorProto { name="E"(1), options(7) = { map_entry=true(7) } }
271        // MessageOptions bytes: field 7 varint -> 0x38 0x01
272        let bytes = [0x0a, 0x01, b'E', 0x3a, 0x02, 0x38, 0x01];
273        let m = parse_message(&bytes).unwrap();
274        assert_eq!(m.name, "E");
275        assert!(m.map_entry);
276    }
277
278    #[test]
279    fn parses_oneof_index_and_decl() {
280        // field x: name="x"(1) number=1(3) type=INT32=5(5) oneof_index=0(9)
281        let fx = [0x0a, 0x01, b'x', 0x18, 0x01, 0x28, 0x05, 0x48, 0x00];
282        // OneofDescriptorProto { name="body"(1) }
283        let od = [0x0a, 0x04, b'b', b'o', b'd', b'y'];
284        // DescriptorProto { name="M"(1), field=fx(2), oneof_decl=od(8) }
285        let mut msg = vec![0x0a, 0x01, b'M'];
286        msg.push(0x12);
287        msg.push(fx.len() as u8);
288        msg.extend_from_slice(&fx);
289        msg.push(0x42);
290        msg.push(od.len() as u8);
291        msg.extend_from_slice(&od);
292
293        let m = parse_message(&msg).unwrap();
294        assert_eq!(m.fields[0].oneof_index, Some(0));
295        assert_eq!(m.oneofs, vec!["body".to_string()]);
296    }
297
298    #[test]
299    fn parses_a_message_with_two_fields() {
300        // field a: name="a"(1) number=1(3) type=STRING=9(5)
301        let fa = [0x0a, 0x01, b'a', 0x18, 0x01, 0x28, 0x09];
302        // field b: name="b"(1) number=2(3) type=BOOL=8(5) label=REPEATED=3(4)
303        let fb = [0x0a, 0x01, b'b', 0x18, 0x02, 0x20, 0x03, 0x28, 0x08];
304        // DescriptorProto { name="M"(1), field=fa(2), field=fb(2) }
305        let mut msg = vec![0x0a, 0x01, b'M'];
306        msg.push(0x12);
307        msg.push(fa.len() as u8);
308        msg.extend_from_slice(&fa);
309        msg.push(0x12);
310        msg.push(fb.len() as u8);
311        msg.extend_from_slice(&fb);
312
313        let m = parse_message(&msg).unwrap();
314        assert_eq!(m.name, "M");
315        assert_eq!(m.fields.len(), 2);
316        assert_eq!(m.fields[0].name, "a");
317        assert_eq!(m.fields[0].ty, Some(ProtoFieldType::String));
318        assert_eq!(m.fields[1].name, "b");
319        assert_eq!(m.fields[1].number, 2);
320        assert_eq!(m.fields[1].label, Label::Repeated);
321        assert_eq!(m.fields[1].ty, Some(ProtoFieldType::Bool));
322    }
323}