Skip to main content

futu_codec/
header.rs

1use bytes::{Buf, BufMut, BytesMut};
2
3/// **Stable API** — API 协议帧头大小 (44 bytes)。
4///
5/// 对应 C++ `APIProtoHeader` 结构体:
6/// - szHeaderFlag[2]:  "FT" magic bytes
7/// - nProtoID:         u32 LE, 协议 ID
8/// - nProtoFmtType:    u8, 0=Protobuf, 1=JSON
9/// - nProtoVer:        u8, 协议版本
10/// - nSerialNo:        u32 LE, 序列号
11/// - nBodyLen:         u32 LE, body 长度
12/// - arrBodySHA1[20]:  body 的 SHA1 哈希
13/// - arrReserved[8]:   保留字段
14pub const HEADER_SIZE: usize = 44;
15
16/// **Internal** — Magic bytes "FT",仅 `header.rs` 内部 encode/decode 用。
17///
18/// v1.4.89 API audit 确认无跨 crate 使用(`grep MAGIC` 返 0 match in
19/// downstream crates),gate 为 `pub(crate)` 防意外外部依赖。
20pub(crate) const MAGIC: [u8; 2] = *b"FT";
21
22/// **Stable API** — 协议格式类型(Protobuf=0 / JSON=1)。
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[repr(u8)]
25#[non_exhaustive]
26pub enum ProtoFmtType {
27    Protobuf = 0,
28    Json = 1,
29}
30
31impl TryFrom<u8> for ProtoFmtType {
32    type Error = u8;
33
34    fn try_from(value: u8) -> Result<Self, u8> {
35        match value {
36            0 => Ok(Self::Protobuf),
37            1 => Ok(Self::Json),
38            other => Err(other),
39        }
40    }
41}
42
43/// **Stable API** — FutuOpenD API 协议帧头(44 字节,手工 encode/decode)。
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct FutuHeader {
46    pub proto_id: u32,
47    pub proto_fmt_type: ProtoFmtType,
48    pub proto_ver: u8,
49    pub serial_no: u32,
50    pub body_len: u32,
51    pub body_sha1: [u8; 20],
52}
53
54impl FutuHeader {
55    /// **Stable API** — 从 `BytesMut` 中解析帧头(不消费 bytes,仅 peek)。
56    ///
57    /// 返回 `None` 如果数据不足 [`HEADER_SIZE`] 字节。
58    /// 返回 `Err` 如果 magic bytes 不匹配或格式类型无效。
59    pub fn peek(src: &BytesMut) -> Result<Option<Self>, futu_core::error::FutuError> {
60        Self::peek_slice(src.as_ref())
61    }
62
63    /// Parse a header from any borrowed byte slice without copying it.
64    ///
65    /// This is the WebSocket/datagram counterpart to [`Self::peek`]; the
66    /// `BytesMut` entrypoint remains stable for Tokio codec callers.
67    pub fn peek_slice(src: &[u8]) -> Result<Option<Self>, futu_core::error::FutuError> {
68        if src.len() < HEADER_SIZE {
69            return Ok(None);
70        }
71
72        // 校验 magic bytes
73        if src[0] != MAGIC[0] || src[1] != MAGIC[1] {
74            return Err(futu_core::error::FutuError::InvalidHeader);
75        }
76
77        let proto_id = u32::from_le_bytes([src[2], src[3], src[4], src[5]]);
78        let proto_fmt_type = ProtoFmtType::try_from(src[6]).map_err(|v| {
79            futu_core::error::FutuError::Codec(format!("unknown proto fmt type: {v}"))
80        })?;
81        let proto_ver = src[7];
82        let serial_no = u32::from_le_bytes([src[8], src[9], src[10], src[11]]);
83        let body_len = u32::from_le_bytes([src[12], src[13], src[14], src[15]]);
84
85        let mut body_sha1 = [0u8; 20];
86        body_sha1.copy_from_slice(&src[16..36]);
87
88        Ok(Some(Self {
89            proto_id,
90            proto_fmt_type,
91            proto_ver,
92            serial_no,
93            body_len,
94            body_sha1,
95        }))
96    }
97
98    /// **Stable API** — 从 `BytesMut` 中解析并消费帧头(成功时 advance `HEADER_SIZE` bytes)。
99    pub fn decode(src: &mut BytesMut) -> Result<Option<Self>, futu_core::error::FutuError> {
100        let header = Self::peek(src)?;
101        if header.is_some() {
102            src.advance(HEADER_SIZE);
103        }
104        Ok(header)
105    }
106
107    /// **Stable API** — 将帧头写入 `BytesMut`(44 字节,LE 序 + MAGIC + SHA1 + reserved)。
108    pub fn encode(&self, dst: &mut BytesMut) {
109        dst.reserve(HEADER_SIZE);
110        dst.put_slice(&MAGIC);
111        dst.put_u32_le(self.proto_id);
112        dst.put_u8(self.proto_fmt_type as u8);
113        dst.put_u8(self.proto_ver);
114        dst.put_u32_le(self.serial_no);
115        dst.put_u32_le(self.body_len);
116        dst.put_slice(&self.body_sha1);
117        dst.put_slice(&[0u8; 8]); // reserved
118    }
119}
120
121#[cfg(test)]
122mod tests;