Skip to main content

futu_backend/stock_list/
sync.rs

1//! CMD6746 single-page transport/protobuf adapter.
2
3use std::io::Read as IoRead;
4
5use bytes::Bytes;
6use flate2::read::GzDecoder;
7use futu_command_spec::StaticDataReadOperation;
8use futu_core::error::{FutuError, Result};
9use futu_domain_static_data::{
10    StockListServerChecksumPair, StockListServerChecksums, StockListSyncPageFacts,
11    ensure_stock_list_sync_backend_success,
12};
13use prost::Message;
14
15use crate::conn::BackendConn;
16use crate::proto_internal::{ftcmd6741, stock_list_sync_svr};
17
18use super::{StockInfo, parse::parse_stock_item};
19
20#[derive(Debug, Clone)]
21pub struct StockListPage {
22    pub items: Vec<StockInfo>,
23    pub facts: StockListSyncPageFacts,
24    pub all_count: u32,
25}
26
27pub async fn fetch_stock_list_page(
28    backend: &BackendConn,
29    request_version: u64,
30) -> Result<StockListPage> {
31    let req = stock_list_sync_svr::StockListReq {
32        stock_list_version: request_version,
33        if_req_all: 0,
34        special_version: Some(1),
35    };
36    let resp = crate::command_runtime::execute_static_data_read(
37        backend,
38        StaticDataReadOperation::StockListSync,
39        Bytes::from(req.encode_to_vec()),
40    )
41    .await?;
42    let decompressed = decompress_stock_list_body(&resp.body)?;
43    let parsed: stock_list_sync_svr::StockListRsp = Message::decode(decompressed.as_slice())
44        .map_err(|e| FutuError::Codec(format!("CMD6746 decode failed: {e}")))?;
45
46    if let Err(err) = ensure_stock_list_sync_backend_success(parsed.result) {
47        return Err(FutuError::Codec(format!(
48            "CMD6746 error: result={}",
49            match err {
50                futu_domain_static_data::StockListSyncBackendError::Result(result) => result,
51            }
52        )));
53    }
54
55    let items = parsed
56        .arry_items
57        .iter()
58        .map(parse_stock_item)
59        .collect::<Vec<_>>();
60    let facts = StockListSyncPageFacts {
61        item_count: items.len(),
62        array_max_version: parsed.array_max_version,
63        if_all_rsp: parsed.if_all_rsp,
64        next_request_interval_secs: parsed.next_request_interval,
65        server_checksum: Some(StockListServerChecksums {
66            environment_id: parsed.stock_list_id.unwrap_or(0),
67            #[allow(deprecated)]
68            legacy_id: parsed.id_check_sum.unwrap_or(0),
69            #[allow(deprecated)]
70            legacy_seq: parsed.seq_check_sum.unwrap_or(0),
71            stock_count: parsed.stock_count.unwrap_or(0),
72            id_v2: parsed
73                .id_check_sum_v2
74                .as_ref()
75                .map(checksum_pair_from_proto),
76            seq_v2: parsed
77                .seq_check_sum_v2
78                .as_ref()
79                .map(checksum_pair_from_proto),
80        }),
81    };
82
83    log_legacy_checksums(&parsed);
84    Ok(StockListPage {
85        items,
86        facts,
87        all_count: parsed.all_count.unwrap_or(0),
88    })
89}
90
91fn checksum_pair_from_proto(value: &ftcmd6741::CheckSum) -> StockListServerChecksumPair {
92    StockListServerChecksumPair {
93        low: value.low.unwrap_or(0),
94        high: value.high.unwrap_or(0),
95    }
96}
97
98fn log_legacy_checksums(parsed: &stock_list_sync_svr::StockListRsp) {
99    // Ref: SecListDBUpdater.cpp:191-225,330-421. V2 is authoritative when
100    // both halves are present; old u64 fields remain diagnostic compatibility.
101    #[allow(deprecated)]
102    if let (Some(server_id_sum), Some(server_seq_sum)) = (parsed.id_check_sum, parsed.seq_check_sum)
103        && (server_id_sum > 0 || server_seq_sum > 0)
104    {
105        tracing::debug!(server_id_sum, server_seq_sum, "server checksums received");
106    }
107    if let (Some(id_v2), Some(seq_v2)) = (&parsed.id_check_sum_v2, &parsed.seq_check_sum_v2) {
108        tracing::debug!(
109            id_low = id_v2.low,
110            id_high = id_v2.high,
111            seq_low = seq_v2.low,
112            seq_high = seq_v2.high,
113            "server v2 checksums received"
114        );
115    }
116}
117
118fn decompress_stock_list_body(data: &[u8]) -> Result<Vec<u8>> {
119    if data.is_empty() {
120        return Ok(Vec::new());
121    }
122
123    let mut gzip = GzDecoder::new(data);
124    let mut decompressed = Vec::new();
125    if gzip.read_to_end(&mut decompressed).is_ok() {
126        return Ok(decompressed);
127    }
128
129    let mut zlib = flate2::read::ZlibDecoder::new(data);
130    let mut decompressed = Vec::new();
131    if zlib.read_to_end(&mut decompressed).is_ok() {
132        return Ok(decompressed);
133    }
134
135    tracing::debug!(
136        len = data.len(),
137        "stock-list data not compressed, using raw body"
138    );
139    Ok(data.to_vec())
140}