Skip to main content

futu_gateway_core/
handlers_shared.rs

1// v1.4.110 P0-5 prep (T12 P1): cross-domain (trd + qot) shared decoding /
2// response building helpers — 原 `handlers/mod.rs` 11 个 pub helper + 4 个
3// 私有 supporting fn. 拆 3 crate 后位于 `futu-gateway-core`, trd / qot 子
4// crate cross-import. P3 move 之前留 `crate::handlers::*` re-export 作 facade,
5// sub-module (handlers/qot/trd/sys) 内部 use site 不动, T15 wire 才一次性 sed.
6//
7// 含:
8// - `make_success_response` / `make_error_response[_with_code]` — 标准化响应构建
9// - `decode_backend_proto` — 兼容 4-byte length-prefix
10// - `extract_repeated_field` / `extract_field5_*` — 手动 protobuf 字段提取
11// - `decode_srpc_or_direct` — SRPC envelope 双路径 fallback (v1.4.106 P2 修)
12// - `decode_cmd5121_groups` + `MultiLangName` — CMD5121 专用解码
13// - `decode_request_or_error` (pub(crate)) — handler 通用入参 decode + error 构建
14
15/// 构建成功响应(ret_type=0, s2c 为 protobuf 编码后的 bytes)
16pub fn make_success_response<M: prost::Message>(resp: &M) -> Vec<u8> {
17    prost::Message::encode_to_vec(resp)
18}
19
20/// 解码后端响应 protobuf,兼容可能存在的 4 字节长度前缀
21///
22/// 部分后端协议(如 CMD 5120/5121)的响应可能包含 OMBinSrz 4 字节大端长度前缀,
23/// 也可能是裸 protobuf。此函数先尝试裸 protobuf 解码,失败后尝试跳过前 4 字节。
24pub fn decode_backend_proto<M: prost::Message + Default>(
25    body: &[u8],
26) -> Result<M, prost::DecodeError> {
27    // 先尝试裸 protobuf 解码
28    match prost::Message::decode(body) {
29        Ok(m) => Ok(m),
30        Err(e1) => {
31            // 如果 body 长度 >= 4,尝试跳过 4 字节长度前缀
32            if body.len() >= 4 {
33                match prost::Message::decode(&body[4..]) {
34                    Ok(m) => {
35                        tracing::debug!(
36                            body_len = body.len(),
37                            "decoded backend proto after skipping 4-byte length prefix"
38                        );
39                        Ok(m)
40                    }
41                    Err(_) => Err(e1), // 返回原始错误
42                }
43            } else {
44                Err(e1)
45            }
46        }
47    }
48}
49
50/// 从 raw protobuf 中提取指定 field number 的所有 LengthDelimited 值,
51/// 然后将每个值解码为 prost::Message。
52/// 用于处理后端使用了不同于 proto 文件定义的 field number 的情况。
53pub fn extract_repeated_field<M: prost::Message + Default>(
54    body: &[u8],
55    target_field: u32,
56) -> Vec<M> {
57    let mut results = Vec::new();
58    let mut pos = 0;
59    while pos < body.len() {
60        // 解析 tag (varint)
61        let (tag, new_pos) = match decode_varint(body, pos) {
62            Some(v) => v,
63            None => break,
64        };
65        let field_number = (tag >> 3) as u32;
66        let wire_type = (tag & 0x7) as u8;
67
68        match wire_type {
69            0 => {
70                // varint — skip
71                match decode_varint(body, new_pos) {
72                    Some((_, p)) => pos = p,
73                    None => break,
74                }
75            }
76            1 => {
77                // 64-bit — skip 8 bytes
78                pos = new_pos + 8;
79            }
80            2 => {
81                // length-delimited
82                let (length, data_start) = match decode_varint(body, new_pos) {
83                    Some(v) => (v.0 as usize, v.1),
84                    None => break,
85                };
86                if data_start + length > body.len() {
87                    break;
88                }
89                if field_number == target_field
90                    && let Ok(msg) = prost::Message::decode(&body[data_start..data_start + length])
91                {
92                    results.push(msg);
93                }
94                pos = data_start + length;
95            }
96            5 => {
97                // 32-bit — skip 4 bytes
98                pos = new_pos + 4;
99            }
100            _ => break,
101        }
102    }
103    results
104}
105
106/// 从 SRPC 封装的响应体中提取 field 5 的数据并解码为指定消息类型。
107/// 后端某些命令(CMD 5120/5121)的响应被 SRPC envelope 包装,实际数据在 field 5。
108pub fn extract_field5_message<M: prost::Message + Default>(body: &[u8]) -> Option<M> {
109    extract_field5_message_with(body, |_| true)
110}
111
112/// 从 SRPC field 5 解码消息,使用 validator 验证结果。
113/// 如果 field 5 直接解码无效,还会尝试从 field 5 → field 4 提取(嵌套 SRPC)。
114pub fn extract_field5_validated<M: prost::Message + Default>(
115    body: &[u8],
116    validator: impl Fn(&M) -> bool,
117) -> Option<M> {
118    extract_field5_message_with(body, validator)
119}
120
121fn extract_field5_message_with<M: prost::Message + Default>(
122    body: &[u8],
123    validator: impl Fn(&M) -> bool,
124) -> Option<M> {
125    let field5_data = extract_raw_field(body, 5)?;
126
127    // 尝试直接解码 field 5 数据
128    if let Ok(m) = prost::Message::decode(field5_data)
129        && validator(&m)
130    {
131        return Some(m);
132    }
133
134    // field 5 内部可能还有嵌套封装(如 SRPC service envelope:field 1=version, field 2=service, field 4=data)
135    // 尝试从 field 5 的 field 4 中提取
136    if let Some(inner) = extract_raw_field(field5_data, 4)
137        && let Ok(m) = prost::Message::decode(inner)
138        && validator(&m)
139    {
140        tracing::debug!("decoded message from SRPC field 5 → inner field 4");
141        return Some(m);
142    }
143
144    None
145}
146
147/// 从 protobuf 消息中提取指定 field number 的第一个 length-delimited 数据切片
148fn extract_raw_field(body: &[u8], target_field: u32) -> Option<&[u8]> {
149    let mut pos = 0;
150    while pos < body.len() {
151        let (tag, new_pos) = decode_varint(body, pos)?;
152        let field_number = (tag >> 3) as u32;
153        let wire_type = (tag & 0x7) as u8;
154
155        match wire_type {
156            0 => {
157                let (_, p) = decode_varint(body, new_pos)?;
158                pos = p;
159            }
160            1 => {
161                pos = new_pos + 8;
162            }
163            2 => {
164                let (length, data_start) = decode_varint(body, new_pos)?;
165                let length = length as usize;
166                if data_start + length > body.len() {
167                    return None;
168                }
169                if field_number == target_field {
170                    return Some(&body[data_start..data_start + length]);
171                }
172                pos = data_start + length;
173            }
174            5 => {
175                pos = new_pos + 4;
176            }
177            _ => return None,
178        }
179    }
180    None
181}
182
183fn decode_varint(body: &[u8], start: usize) -> Option<(u64, usize)> {
184    let mut result: u64 = 0;
185    let mut shift = 0;
186    let mut pos = start;
187    loop {
188        if pos >= body.len() {
189            return None;
190        }
191        let b = body[pos];
192        pos += 1;
193        result |= ((b & 0x7f) as u64) << shift;
194        if b & 0x80 == 0 {
195            return Some((result, pos));
196        }
197        shift += 7;
198        if shift >= 64 {
199            return None;
200        }
201    }
202}
203
204/// SRPC envelope 解码失败原因(v1.4.106 codex 1110 F3 [P2] 修:不再返回 default)
205#[derive(Debug)]
206pub enum SrpcDecodeError {
207    /// body 完全无法 decode(standard prost + SRPC field 5 + 嵌套 field 4 三路径都失败)
208    AllPathsFailed { body_len: usize },
209    /// body decode 成功但 validator 不通过(例:result_code != 0 或 stock_count 不一致)
210    ValidatorRejected,
211}
212
213impl std::fmt::Display for SrpcDecodeError {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        match self {
216            Self::AllPathsFailed { body_len } => {
217                write!(
218                    f,
219                    "SRPC decode failed: all paths failed (body_len={body_len})"
220                )
221            }
222            Self::ValidatorRejected => {
223                write!(f, "SRPC decode succeeded but validator rejected response")
224            }
225        }
226    }
227}
228
229impl std::error::Error for SrpcDecodeError {}
230
231/// 统一 SRPC 封装解码:先尝试标准解码,如果结果无效则尝试 SRPC field 5 解码。
232///
233/// - `body`: 后端响应体
234/// - `validator`: 验证解码结果是否有效的闭包。返回 true 表示结果有效,直接使用;
235///   返回 false 表示结果无效,继续尝试 SRPC field 5 解码。
236///
237/// 适用于 CMD 5120/5121 等被 SRPC envelope 包装的后端命令。
238///
239/// **v1.4.106 codex 1110 F3 [P2] 修**:不再 silently return `M::default()`。
240/// caller 必须处理 `Err`,区分"空数据"和"解码失败"。default fallback 会让上层把
241/// `result_code=None + stock_list=[]` 误识别为成功空状态。返 Result 后 caller
242/// 必须显式 `match` 处理 → 如果 decode failure 应 ret_type=-1 不能 silent empty。
243pub fn decode_srpc_or_direct<M: prost::Message + Default>(
244    body: &[u8],
245    validator: impl Fn(&M) -> bool,
246) -> Result<M, SrpcDecodeError> {
247    // 1. 尝试标准解码(裸 protobuf + 跳过 4 字节前缀)
248    if let Ok(r) = decode_backend_proto::<M>(body)
249        && validator(&r)
250    {
251        return Ok(r);
252    }
253
254    // 2. 标准解码无效 → 从 SRPC envelope 的 field 5 提取(带 validator)
255    if let Some(r) = extract_field5_validated::<M>(body, &validator) {
256        tracing::debug!("decoded message from SRPC field 5");
257        return Ok(r);
258    }
259
260    // 3. 所有路径失败:区分"完全 decode 不出"vs"decode 出来但 validator 拒绝"
261    // 通过尝试一次纯 decode(不带 validator)判断
262    let raw_decoded = decode_backend_proto::<M>(body).is_ok()
263        || extract_field5_message_with(body, |_: &M| true).is_some();
264
265    if raw_decoded {
266        tracing::warn!(
267            body_len = body.len(),
268            "SRPC decode: validator rejected all candidate decodes"
269        );
270        Err(SrpcDecodeError::ValidatorRejected)
271    } else {
272        tracing::warn!(
273            body_len = body.len(),
274            "SRPC decode: all paths failed (cannot parse body as target message)"
275        );
276        Err(SrpcDecodeError::AllPathsFailed {
277            body_len: body.len(),
278        })
279    }
280}
281
282/// 多语言名称(手动解析,不通过 prost)
283#[derive(Debug, Clone)]
284pub struct MultiLangName {
285    pub language_id: i32,
286    pub name: String,
287}
288
289/// CMD5121 专用解码:SRPC field 5 包含的是 repeated GroupInfo(不是 GetGroupListResp)。
290///
291/// GroupInfo 的 field 4 (multi_lang_name) 包含后端数据导致 prost 整体解码失败,
292/// 因此 proto 中不定义 field 4。本函数先用 prost 解码 fields 1-3,
293/// 再手动从原始字节中提取 field 4 的 MultiLanguageName。
294pub fn decode_cmd5121_groups(
295    body: &[u8],
296) -> (
297    futu_backend::proto_internal::wch_lst::GetGroupListResp,
298    Vec<Vec<MultiLangName>>,
299) {
300    use futu_backend::proto_internal::wch_lst::{GetGroupListResp, GroupInfo};
301
302    // 1. 尝试标准解码
303    if let Ok(r) = decode_backend_proto::<GetGroupListResp>(body)
304        && r.result_code.is_some()
305        && !r.group_list.is_empty()
306    {
307        let empty_langs = vec![vec![]; r.group_list.len()];
308        return (r, empty_langs);
309    }
310
311    // 2. 检查 SRPC metadata (field 3) 中的 result_code
312    //    SRPC field 3 内部: field 1 = result_code (0=成功, 非0=错误)
313    let srpc_meta = extract_raw_field_bytes(body, 3);
314    let srpc_result_code = srpc_meta
315        .first()
316        .and_then(|meta| decode_srpc_result_code(meta));
317
318    if let Some(code) = srpc_result_code
319        && code != 0
320    {
321        let hex = srpc_meta_hex(&srpc_meta);
322        // Ref: FutuOpenD/Src/NNProtoCenter/Quote/NNBiz_UserSecurity.cpp:176-180.
323        // C++ only unpacks group_list when result_code is present and equals 0.
324        tracing::warn!(body_len = body.len(), result_code = code, srpc_meta_hex = %hex, "CMD5121: SRPC metadata returned error");
325        return (
326            GetGroupListResp {
327                result_code: Some(code),
328                ..Default::default()
329            },
330            vec![],
331        );
332    }
333
334    // 3. SRPC field 5 包含 repeated GroupInfo,用 extract_repeated_field 解码
335    //    (proto 中不定义 field 4,prost 自动跳过)
336    let group_list: Vec<GroupInfo> = extract_repeated_field(body, 5);
337    if group_list.is_empty() {
338        tracing::warn!(body_len = body.len(), "CMD5121 decode: no GroupInfo found");
339        return (GetGroupListResp::default(), vec![]);
340    }
341
342    // 4. 同时提取 field 5 的原始字节,从中手动解析 field 4 (multi_lang_name)
343    let raw_items: Vec<&[u8]> = extract_raw_field_bytes(body, 5);
344    let lang_list: Vec<Vec<MultiLangName>> = raw_items
345        .iter()
346        .map(|raw| extract_multi_lang_names(raw))
347        .collect();
348
349    tracing::debug!(
350        count = group_list.len(),
351        "decoded GroupInfo from SRPC field 5 with multi_lang_name"
352    );
353    let resp = GetGroupListResp {
354        result_code: Some(0),
355        group_count: Some(group_list.len() as u32),
356        group_list,
357        max_reached: None,
358    };
359    (resp, lang_list)
360}
361
362fn decode_srpc_result_code(meta: &[u8]) -> Option<i32> {
363    let mut pos = 0;
364    while pos < meta.len() {
365        let (tag, new_pos) = decode_varint(meta, pos)?;
366        let field_number = (tag >> 3) as u32;
367        let wire_type = (tag & 0x7) as u8;
368        match wire_type {
369            0 => {
370                let (val, p) = decode_varint(meta, new_pos)?;
371                if field_number == 1 {
372                    return Some(val as i32);
373                }
374                pos = p;
375            }
376            1 => {
377                pos = new_pos + 8;
378            }
379            2 => {
380                let (len, start) = decode_varint(meta, new_pos)?;
381                pos = start + len as usize;
382            }
383            5 => {
384                pos = new_pos + 4;
385            }
386            _ => break,
387        }
388    }
389    None
390}
391
392fn srpc_meta_hex(meta: &[&[u8]]) -> String {
393    meta.first()
394        .map(|m| {
395            m.iter()
396                .map(|b| format!("{b:02x}"))
397                .collect::<Vec<_>>()
398                .join(" ")
399        })
400        .unwrap_or_default()
401}
402
403/// 从 raw protobuf 中提取指定 field number 的所有 LengthDelimited 值的原始字节切片。
404fn extract_raw_field_bytes(body: &[u8], target_field: u32) -> Vec<&[u8]> {
405    let mut results = Vec::new();
406    let mut pos = 0;
407    while pos < body.len() {
408        let (tag, new_pos) = match decode_varint(body, pos) {
409            Some(v) => v,
410            None => break,
411        };
412        let field_number = (tag >> 3) as u32;
413        let wire_type = (tag & 0x7) as u8;
414
415        match wire_type {
416            0 => match decode_varint(body, new_pos) {
417                Some((_, p)) => pos = p,
418                None => break,
419            },
420            1 => {
421                pos = new_pos + 8;
422            }
423            2 => {
424                let (length, data_start) = match decode_varint(body, new_pos) {
425                    Some(v) => (v.0 as usize, v.1),
426                    None => break,
427                };
428                if data_start + length > body.len() {
429                    break;
430                }
431                if field_number == target_field {
432                    results.push(&body[data_start..data_start + length]);
433                }
434                pos = data_start + length;
435            }
436            5 => {
437                pos = new_pos + 4;
438            }
439            _ => break,
440        }
441    }
442    results
443}
444
445/// 从 GroupInfo 原始字节中手动提取 field 4 (repeated MultiLanguageName)。
446///
447/// MultiLanguageName = { optional int32 language_id = 1; optional string name = 2; }
448/// 由于后端数据可能导致 prost 解码失败,这里用容错方式手动解析。
449fn extract_multi_lang_names(group_info_bytes: &[u8]) -> Vec<MultiLangName> {
450    let mut results = Vec::new();
451
452    // 提取所有 field 4 的 length-delimited 数据
453    let raw_items = extract_raw_field_bytes(group_info_bytes, 4);
454
455    for raw in raw_items {
456        // 手动解析 MultiLanguageName: field 1 = language_id (varint), field 2 = name (string)
457        let mut language_id: i32 = 0;
458        let mut name = String::new();
459        let mut pos = 0;
460
461        while pos < raw.len() {
462            let (tag, new_pos) = match decode_varint(raw, pos) {
463                Some(v) => v,
464                None => break,
465            };
466            let field_number = (tag >> 3) as u32;
467            let wire_type = (tag & 0x7) as u8;
468
469            match wire_type {
470                0 => {
471                    // varint
472                    let (val, p) = match decode_varint(raw, new_pos) {
473                        Some(v) => v,
474                        None => break,
475                    };
476                    if field_number == 1 {
477                        language_id = val as i32;
478                    }
479                    pos = p;
480                }
481                1 => {
482                    pos = new_pos + 8;
483                }
484                2 => {
485                    let (length, data_start) = match decode_varint(raw, new_pos) {
486                        Some(v) => (v.0 as usize, v.1),
487                        None => break,
488                    };
489                    if data_start + length > raw.len() {
490                        break;
491                    }
492                    if field_number == 2 {
493                        name = String::from_utf8_lossy(&raw[data_start..data_start + length])
494                            .to_string();
495                    }
496                    pos = data_start + length;
497                }
498                5 => {
499                    pos = new_pos + 4;
500                }
501                _ => break,
502            }
503        }
504
505        if !name.is_empty() {
506            results.push(MultiLangName { language_id, name });
507        }
508    }
509
510    results
511}
512
513/// 构建错误响应
514pub fn make_error_response(ret_type: i32, msg: &str) -> Vec<u8> {
515    let resp = futu_proto::init_connect::Response {
516        ret_type,
517        ret_msg: Some(msg.to_string()),
518        err_code: None,
519        s2c: None,
520    };
521    prost::Message::encode_to_vec(&resp)
522}
523
524/// 构建标准 handler 错误响应(ret_type=-1)。
525pub fn make_standard_error_response(msg: impl AsRef<str>) -> Vec<u8> {
526    make_error_response(-1, msg.as_ref())
527}
528
529pub fn decode_request_or_error<M>(
530    body: &[u8],
531    iface: &'static str,
532    endpoint: &'static str,
533) -> Result<M, Vec<u8>>
534where
535    M: prost::Message + Default,
536{
537    <M as prost::Message>::decode(body).map_err(|err| {
538        tracing::warn!(
539            target: futu_auth::audit::TARGET,
540            iface,
541            endpoint,
542            error = %err,
543            "request body decode failed before handler dispatch"
544        );
545        make_error_response(-1, &format!("{endpoint}: 请求体解码失败"))
546    })
547}
548
549/// v1.4.34 UX-2:构建带显式 err_code 的错误响应(handler 层短路用)。
550///
551/// 常见场景:daemon 自己检测到"不需要发服务端的错"(cipher 缺失 / 参数错 / 状态不对),
552/// 直接短路给客户端清晰信号。显式 err_code 让 REST `[err_code=-401]` 前缀能标记
553/// 这是**本地短路**(而不是服务端返的),客户端据此判断该调 unlock 而不是重试。
554pub fn make_error_response_with_code(ret_type: i32, err_code: i32, msg: &str) -> Vec<u8> {
555    let resp = futu_proto::init_connect::Response {
556        ret_type,
557        ret_msg: Some(msg.to_string()),
558        err_code: Some(err_code),
559        s2c: None,
560    };
561    prost::Message::encode_to_vec(&resp)
562}
563
564#[cfg(test)]
565mod tests;