Skip to main content

oxihuman_core/
grpc_codec.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! gRPC framing codec stub.
6
7/// A gRPC frame header (5 bytes: 1 compression flag + 4-byte length).
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct GrpcFrameHeader {
10    /// 0 = not compressed, 1 = compressed.
11    pub compressed: bool,
12    /// Message length in bytes.
13    pub message_len: u32,
14}
15
16/// gRPC framing error.
17#[derive(Debug, Clone, PartialEq)]
18pub enum GrpcError {
19    InsufficientData,
20    InvalidCompressionFlag(u8),
21    MessageTruncated { expected: u32, got: usize },
22}
23
24impl std::fmt::Display for GrpcError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::InsufficientData => write!(f, "insufficient data for gRPC frame"),
28            Self::InvalidCompressionFlag(b) => write!(f, "invalid gRPC compression flag: {b}"),
29            Self::MessageTruncated { expected, got } => {
30                write!(
31                    f,
32                    "gRPC message truncated: expected {expected} bytes, got {got}"
33                )
34            }
35        }
36    }
37}
38
39/// Encode a gRPC frame: 5-byte header + message bytes.
40pub fn encode_frame(data: &[u8], compressed: bool, buf: &mut Vec<u8>) {
41    buf.push(if compressed { 1 } else { 0 });
42    buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
43    buf.extend_from_slice(data);
44}
45
46/// Decode a gRPC frame header from the first 5 bytes.
47pub fn decode_frame_header(buf: &[u8]) -> Result<GrpcFrameHeader, GrpcError> {
48    if buf.len() < 5 {
49        return Err(GrpcError::InsufficientData);
50    }
51    let flag = buf[0];
52    if flag > 1 {
53        return Err(GrpcError::InvalidCompressionFlag(flag));
54    }
55    let len_bytes: [u8; 4] = buf[1..5].try_into().unwrap_or_default();
56    Ok(GrpcFrameHeader {
57        compressed: flag == 1,
58        message_len: u32::from_be_bytes(len_bytes),
59    })
60}
61
62/// Decode a complete gRPC frame, returning the message bytes.
63pub fn decode_frame(buf: &[u8]) -> Result<(GrpcFrameHeader, &[u8]), GrpcError> {
64    let header = decode_frame_header(buf)?;
65    let msg_len = header.message_len as usize;
66    if buf.len() < 5 + msg_len {
67        return Err(GrpcError::MessageTruncated {
68            expected: header.message_len,
69            got: buf.len().saturating_sub(5),
70        });
71    }
72    Ok((header, &buf[5..5 + msg_len]))
73}
74
75/// Return the total framed length of a message.
76pub fn framed_length(msg_len: usize) -> usize {
77    5 + msg_len
78}
79
80/// Return `true` if the buffer contains a complete gRPC frame.
81pub fn is_complete_frame(buf: &[u8]) -> bool {
82    if buf.len() < 5 {
83        return false;
84    }
85    let len_bytes: [u8; 4] = buf[1..5].try_into().unwrap_or([0; 4]);
86    let msg_len = u32::from_be_bytes(len_bytes) as usize;
87    buf.len() >= 5 + msg_len
88}
89
90/// Split a byte slice into multiple gRPC frames.
91pub fn split_frames(mut buf: &[u8]) -> Vec<Vec<u8>> {
92    let mut frames = vec![];
93    while buf.len() >= 5 {
94        if let Ok((header, msg)) = decode_frame(buf) {
95            frames.push(msg.to_vec());
96            let consumed = 5 + header.message_len as usize;
97            if consumed > buf.len() {
98                break;
99            }
100            buf = &buf[consumed..];
101        } else {
102            break;
103        }
104    }
105    frames
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_encode_frame_length() {
114        /* encoded frame is header (5) + data */
115        let mut buf = vec![];
116        encode_frame(b"hello", false, &mut buf);
117        assert_eq!(buf.len(), 10);
118    }
119
120    #[test]
121    fn test_decode_header() {
122        /* header decoded correctly */
123        let mut buf = vec![];
124        encode_frame(b"test", false, &mut buf);
125        let h = decode_frame_header(&buf).expect("should succeed");
126        assert!(!h.compressed);
127        assert_eq!(h.message_len, 4);
128    }
129
130    #[test]
131    fn test_decode_complete_frame() {
132        /* full frame decodes message bytes */
133        let mut buf = vec![];
134        encode_frame(b"abc", false, &mut buf);
135        let (_, msg) = decode_frame(&buf).expect("should succeed");
136        assert_eq!(msg, b"abc");
137    }
138
139    #[test]
140    fn test_insufficient_data() {
141        /* short buffer returns error */
142        assert!(decode_frame_header(&[0, 0, 0]).is_err());
143    }
144
145    #[test]
146    fn test_is_complete_frame_true() {
147        /* complete frame detected */
148        let mut buf = vec![];
149        encode_frame(&[1, 2], false, &mut buf);
150        assert!(is_complete_frame(&buf));
151    }
152
153    #[test]
154    fn test_is_complete_frame_false() {
155        /* truncated frame not complete */
156        assert!(!is_complete_frame(&[0, 0, 0, 0, 5]));
157    }
158
159    #[test]
160    fn test_framed_length() {
161        /* framed_length adds 5 bytes */
162        assert_eq!(framed_length(10), 15);
163    }
164
165    #[test]
166    fn test_split_frames() {
167        /* two back-to-back frames split correctly */
168        let mut buf = vec![];
169        encode_frame(b"A", false, &mut buf);
170        encode_frame(b"BB", false, &mut buf);
171        let frames = split_frames(&buf);
172        assert_eq!(frames.len(), 2);
173    }
174
175    #[test]
176    fn test_compressed_flag() {
177        /* compressed flag set correctly */
178        let mut buf = vec![];
179        encode_frame(b"x", true, &mut buf);
180        let h = decode_frame_header(&buf).expect("should succeed");
181        assert!(h.compressed);
182    }
183
184    #[test]
185    fn test_invalid_compression_flag() {
186        /* flag > 1 is invalid */
187        let buf = [2u8, 0, 0, 0, 0];
188        assert!(decode_frame_header(&buf).is_err());
189    }
190}