Skip to main content

tonic_debug/
inspect.rs

1//! Human-readable protobuf wire format inspection.
2//!
3//! Since we don't have access to `.proto` schemas at runtime, this module
4//! decodes raw protobuf bytes using the wire format specification. Each field
5//! is displayed with its field number, wire type, and value — giving developers
6//! meaningful insight into what is being sent over the wire.
7
8use bytes::Buf;
9use std::fmt;
10
11/// Wire types as defined by the protobuf encoding specification.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum WireType {
14    /// Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
15    Varint,
16    /// 64-bit (fixed64, sfixed64, double)
17    Bit64,
18    /// Length-delimited (string, bytes, embedded messages, packed repeated fields)
19    LengthDelimited,
20    /// Start group (deprecated)
21    StartGroup,
22    /// End group (deprecated)
23    EndGroup,
24    /// 32-bit (fixed32, sfixed32, float)
25    Bit32,
26    /// Unknown wire type
27    Unknown(u32),
28}
29
30impl fmt::Display for WireType {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            WireType::Varint => write!(f, "varint"),
34            WireType::Bit64 => write!(f, "64-bit"),
35            WireType::LengthDelimited => write!(f, "length-delimited"),
36            WireType::StartGroup => write!(f, "start-group"),
37            WireType::EndGroup => write!(f, "end-group"),
38            WireType::Bit32 => write!(f, "32-bit"),
39            WireType::Unknown(v) => write!(f, "unknown({})", v),
40        }
41    }
42}
43
44impl From<u32> for WireType {
45    fn from(value: u32) -> Self {
46        match value {
47            0 => WireType::Varint,
48            1 => WireType::Bit64,
49            2 => WireType::LengthDelimited,
50            3 => WireType::StartGroup,
51            4 => WireType::EndGroup,
52            5 => WireType::Bit32,
53            v => WireType::Unknown(v),
54        }
55    }
56}
57
58/// A single decoded protobuf field.
59#[derive(Debug, Clone, PartialEq)]
60pub struct ProtoField {
61    /// The field number from the protobuf tag.
62    pub field_number: u32,
63    /// The wire type of this field.
64    pub wire_type: WireType,
65    /// The decoded value.
66    pub value: ProtoValue,
67}
68
69/// Represents a decoded protobuf value.
70#[derive(Debug, Clone, PartialEq)]
71pub enum ProtoValue {
72    /// A varint-encoded integer.
73    Varint(u64),
74    /// A 32-bit fixed value.
75    Fixed32(u32),
76    /// A 64-bit fixed value.
77    Fixed64(u64),
78    /// Length-delimited bytes — may be a string, nested message, or raw bytes.
79    Bytes(Vec<u8>),
80    /// Nested message fields (when bytes successfully decode as protobuf).
81    Nested(Vec<ProtoField>),
82}
83
84impl fmt::Display for ProtoValue {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            ProtoValue::Varint(v) => write!(f, "{}", v),
88            ProtoValue::Fixed32(v) => write!(f, "0x{:08x}", v),
89            ProtoValue::Fixed64(v) => write!(f, "0x{:016x}", v),
90            ProtoValue::Bytes(b) => {
91                // Try to display as UTF-8 string first
92                if let Ok(s) = std::str::from_utf8(b) {
93                    if s.chars()
94                        .all(|c| !c.is_control() || c == '\n' || c == '\r' || c == '\t')
95                    {
96                        return write!(f, "\"{}\"", s);
97                    }
98                }
99                // Fall back to hex representation
100                write!(f, "0x")?;
101                for byte in b {
102                    write!(f, "{:02x}", byte)?;
103                }
104                Ok(())
105            }
106            ProtoValue::Nested(fields) => {
107                write!(f, "{{ ")?;
108                for (i, field) in fields.iter().enumerate() {
109                    if i > 0 {
110                        write!(f, ", ")?;
111                    }
112                    write!(f, "{}", field)?;
113                }
114                write!(f, " }}")
115            }
116        }
117    }
118}
119
120impl fmt::Display for ProtoField {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(
123            f,
124            "field {} ({}): {}",
125            self.field_number, self.wire_type, self.value
126        )
127    }
128}
129
130/// Decode a varint from the buffer, returning the value and the number of bytes consumed.
131fn decode_varint(buf: &[u8]) -> Option<(u64, usize)> {
132    let mut value: u64 = 0;
133    let mut shift = 0;
134    for (i, &byte) in buf.iter().enumerate() {
135        if shift >= 64 {
136            return None;
137        }
138        value |= ((byte & 0x7F) as u64) << shift;
139        shift += 7;
140        if byte & 0x80 == 0 {
141            return Some((value, i + 1));
142        }
143    }
144    None
145}
146
147/// Attempt to decode raw bytes as protobuf wire format fields.
148///
149/// Returns `None` if the bytes do not look like valid protobuf.
150pub fn decode_protobuf(data: &[u8]) -> Option<Vec<ProtoField>> {
151    let mut fields = Vec::new();
152    let mut pos = 0;
153
154    while pos < data.len() {
155        // Decode the tag (field_number << 3 | wire_type)
156        let (tag, tag_len) = decode_varint(&data[pos..])?;
157        pos += tag_len;
158
159        let wire_type_raw = (tag & 0x07) as u32;
160        let field_number = (tag >> 3) as u32;
161
162        // Field number 0 is invalid
163        if field_number == 0 {
164            return None;
165        }
166
167        let wire_type = WireType::from(wire_type_raw);
168
169        let value = match wire_type {
170            WireType::Varint => {
171                let (v, v_len) = decode_varint(&data[pos..])?;
172                pos += v_len;
173                ProtoValue::Varint(v)
174            }
175            WireType::Bit64 => {
176                if pos + 8 > data.len() {
177                    return None;
178                }
179                let mut buf = &data[pos..pos + 8];
180                let v = buf.get_u64_le();
181                pos += 8;
182                ProtoValue::Fixed64(v)
183            }
184            WireType::LengthDelimited => {
185                let (len, len_bytes) = decode_varint(&data[pos..])?;
186                pos += len_bytes;
187                let len = len as usize;
188                if pos + len > data.len() {
189                    return None;
190                }
191                let payload = &data[pos..pos + len];
192                pos += len;
193
194                // Try to recursively decode as a nested protobuf message
195                if let Some(nested) = decode_protobuf(payload) {
196                    if !nested.is_empty() {
197                        ProtoValue::Nested(nested)
198                    } else {
199                        ProtoValue::Bytes(payload.to_vec())
200                    }
201                } else {
202                    ProtoValue::Bytes(payload.to_vec())
203                }
204            }
205            WireType::Bit32 => {
206                if pos + 4 > data.len() {
207                    return None;
208                }
209                let mut buf = &data[pos..pos + 4];
210                let v = buf.get_u32_le();
211                pos += 4;
212                ProtoValue::Fixed32(v)
213            }
214            WireType::StartGroup | WireType::EndGroup => {
215                // Deprecated wire types — skip
216                return None;
217            }
218            WireType::Unknown(_) => {
219                return None;
220            }
221        };
222
223        fields.push(ProtoField {
224            field_number,
225            wire_type,
226            value,
227        });
228    }
229
230    Some(fields)
231}
232
233/// A decoded gRPC frame.
234#[derive(Debug)]
235pub struct GrpcFrame {
236    /// Whether the frame is compressed.
237    pub compressed: bool,
238    /// The raw payload bytes.
239    pub payload: Vec<u8>,
240    /// Decoded protobuf fields (if decoding succeeded).
241    pub decoded_fields: Option<Vec<ProtoField>>,
242}
243
244impl fmt::Display for GrpcFrame {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        write!(
247            f,
248            "gRPC frame (compressed={}, {} bytes)",
249            self.compressed,
250            self.payload.len()
251        )?;
252        if let Some(ref fields) = self.decoded_fields {
253            for field in fields {
254                write!(f, "\n  {}", field)?;
255            }
256        } else {
257            write!(f, "\n  <raw bytes: {} bytes>", self.payload.len())?;
258        }
259        Ok(())
260    }
261}
262
263/// Parse gRPC-encoded data into frames.
264///
265/// gRPC uses a length-prefixed framing format:
266/// - 1 byte: compressed flag (0 = uncompressed, 1 = compressed)
267/// - 4 bytes: message length (big-endian)
268/// - N bytes: message payload
269pub fn parse_grpc_frames(data: &[u8]) -> Vec<GrpcFrame> {
270    let mut frames = Vec::new();
271    let mut pos = 0;
272
273    while pos + 5 <= data.len() {
274        let compressed = data[pos] != 0;
275        pos += 1;
276
277        let length =
278            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
279        pos += 4;
280
281        if pos + length > data.len() {
282            break;
283        }
284
285        let payload = data[pos..pos + length].to_vec();
286        pos += length;
287
288        let decoded_fields = if !compressed {
289            decode_protobuf(&payload)
290        } else {
291            None
292        };
293
294        frames.push(GrpcFrame {
295            compressed,
296            payload,
297            decoded_fields,
298        });
299    }
300
301    frames
302}
303
304/// Format a byte slice as a hex dump suitable for debug logging.
305pub fn hex_dump(data: &[u8], max_bytes: usize) -> String {
306    let truncated = data.len() > max_bytes;
307    let display = &data[..data.len().min(max_bytes)];
308
309    let mut result = String::new();
310    for (i, chunk) in display.chunks(16).enumerate() {
311        if i > 0 {
312            result.push('\n');
313        }
314        result.push_str(&format!("  {:04x}: ", i * 16));
315        for (j, byte) in chunk.iter().enumerate() {
316            if j == 8 {
317                result.push(' ');
318            }
319            result.push_str(&format!("{:02x} ", byte));
320        }
321        // Pad remaining space
322        let remaining = 16 - chunk.len();
323        for j in 0..remaining {
324            if chunk.len() + j == 8 {
325                result.push(' ');
326            }
327            result.push_str("   ");
328        }
329        result.push_str(" |");
330        for byte in chunk {
331            if byte.is_ascii_graphic() || *byte == b' ' {
332                result.push(*byte as char);
333            } else {
334                result.push('.');
335            }
336        }
337        result.push('|');
338    }
339
340    if truncated {
341        result.push_str(&format!(
342            "\n  ... ({} bytes truncated)",
343            data.len() - max_bytes
344        ));
345    }
346
347    result
348}
349
350/// Format gRPC request/response data into a human-readable string.
351pub fn format_grpc_message(data: &[u8]) -> String {
352    let frames = parse_grpc_frames(data);
353    if frames.is_empty() {
354        return format!("  <no valid gRPC frames, {} raw bytes>", data.len());
355    }
356
357    let mut result = String::new();
358    for (i, frame) in frames.iter().enumerate() {
359        if i > 0 {
360            result.push('\n');
361        }
362        result.push_str(&format!("{}", frame));
363    }
364    result
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_decode_varint() {
373        // Single byte varint: 1
374        assert_eq!(decode_varint(&[0x01]), Some((1, 1)));
375        // Single byte varint: 127
376        assert_eq!(decode_varint(&[0x7F]), Some((127, 1)));
377        // Two byte varint: 128
378        assert_eq!(decode_varint(&[0x80, 0x01]), Some((128, 2)));
379        // Two byte varint: 300
380        assert_eq!(decode_varint(&[0xAC, 0x02]), Some((300, 2)));
381    }
382
383    #[test]
384    fn test_decode_simple_protobuf() {
385        // Field 1, varint, value 150 (encoded as: 08 96 01)
386        let data = vec![0x08, 0x96, 0x01];
387        let fields = decode_protobuf(&data).unwrap();
388        assert_eq!(fields.len(), 1);
389        assert_eq!(fields[0].field_number, 1);
390        assert_eq!(fields[0].wire_type, WireType::Varint);
391        match &fields[0].value {
392            ProtoValue::Varint(v) => assert_eq!(*v, 150),
393            _ => panic!("Expected varint"),
394        }
395    }
396
397    #[test]
398    fn test_decode_string_field() {
399        // Field 2, length-delimited, value "testing" (encoded as: 12 07 74 65 73 74 69 6e 67)
400        let data = vec![0x12, 0x07, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67];
401        let fields = decode_protobuf(&data).unwrap();
402        assert_eq!(fields.len(), 1);
403        assert_eq!(fields[0].field_number, 2);
404        assert_eq!(fields[0].wire_type, WireType::LengthDelimited);
405        match &fields[0].value {
406            ProtoValue::Bytes(b) => assert_eq!(b, b"testing"),
407            _ => panic!("Expected bytes"),
408        }
409    }
410
411    #[test]
412    fn test_parse_grpc_frame() {
413        // gRPC frame: uncompressed, 3-byte payload (field 1, varint 150)
414        let mut data = vec![0x00]; // compressed flag = false
415        data.extend_from_slice(&3u32.to_be_bytes()); // length = 3
416        data.extend_from_slice(&[0x08, 0x96, 0x01]); // protobuf payload
417
418        let frames = parse_grpc_frames(&data);
419        assert_eq!(frames.len(), 1);
420        assert!(!frames[0].compressed);
421        assert!(frames[0].decoded_fields.is_some());
422        let fields = frames[0].decoded_fields.as_ref().unwrap();
423        assert_eq!(fields.len(), 1);
424        assert_eq!(fields[0].field_number, 1);
425    }
426
427    #[test]
428    fn test_invalid_protobuf() {
429        // Random bytes that don't form valid protobuf
430        let data = vec![
431            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
432        ];
433        assert!(decode_protobuf(&data).is_none());
434    }
435
436    #[test]
437    fn test_hex_dump() {
438        let data = b"Hello, World!";
439        let dump = hex_dump(data, 256);
440        assert!(dump.contains("48 65 6c 6c"));
441        assert!(dump.contains("|Hello, World!|"));
442    }
443
444    #[test]
445    fn test_empty_data() {
446        let fields = decode_protobuf(&[]);
447        assert_eq!(fields, Some(vec![]));
448    }
449
450    #[test]
451    fn test_wire_type_display() {
452        assert_eq!(format!("{}", WireType::Varint), "varint");
453        assert_eq!(format!("{}", WireType::LengthDelimited), "length-delimited");
454        assert_eq!(format!("{}", WireType::Unknown(99)), "unknown(99)");
455    }
456}