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