Skip to main content

futu_backend/conn_ip/
store.rs

1use std::path::{Path, PathBuf};
2
3use futu_core::conn_ip::{
4    CPP_MAX_PERSISTED_CONN_IP_ADDRESSES, ConnIpAddress, ConnIpCatalog, ConnIpSnapshot,
5    RestoredConnIpCatalogFacts, restore_conn_ip_catalog_like_cpp,
6};
7use serde::{Deserialize, Serialize};
8
9const STORE_SCHEMA_VERSION: u32 = 1;
10
11#[derive(Debug, Clone)]
12pub struct ConnIpCatalogStore {
13    root: PathBuf,
14}
15
16impl ConnIpCatalogStore {
17    #[must_use]
18    pub fn from_root(root: PathBuf) -> Self {
19        Self { root }
20    }
21
22    pub fn production() -> Result<Self, ConnIpCatalogStoreError> {
23        crate::auth::try_futu_opend_dir()
24            .map(Self::from_root)
25            .map_err(|error| ConnIpCatalogStoreError::Directory(error.to_string()))
26    }
27
28    pub fn load(
29        &self,
30        conn_identity: u32,
31    ) -> Result<Option<ConnIpCatalog>, ConnIpCatalogStoreError> {
32        load_conn_ip_catalog_from_path(&self.path(conn_identity), conn_identity)
33    }
34
35    pub fn save(&self, catalog: &ConnIpCatalog) -> Result<(), ConnIpCatalogStoreError> {
36        save_conn_ip_catalog_to_path(&self.path(catalog.snapshot.conn_identity), catalog)
37    }
38
39    fn path(&self, conn_identity: u32) -> PathBuf {
40        self.root.join(format!("conn-info-{conn_identity}.json"))
41    }
42}
43
44#[derive(Debug)]
45pub enum ConnIpCatalogStoreError {
46    Directory(String),
47    Read(std::io::Error),
48    Decode(serde_json::Error),
49    UnsupportedSchema(u32),
50    IdentityMismatch { expected: u32, actual: u32 },
51    OversizedCatalog(usize),
52    InvalidCatalog,
53    Encode(serde_json::Error),
54    Write(std::io::Error),
55}
56
57impl std::fmt::Display for ConnIpCatalogStoreError {
58    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::Directory(message) => write!(formatter, "store directory unavailable: {message}"),
61            Self::Read(error) => write!(formatter, "read ConnIP catalog: {error}"),
62            Self::Decode(error) => write!(formatter, "decode ConnIP catalog: {error}"),
63            Self::UnsupportedSchema(version) => {
64                write!(formatter, "unsupported ConnIP catalog schema {version}")
65            }
66            Self::IdentityMismatch { expected, actual } => write!(
67                formatter,
68                "ConnIP catalog identity mismatch: expected {expected}, got {actual}"
69            ),
70            Self::OversizedCatalog(size) => write!(
71                formatter,
72                "ConnIP catalog has {size} addresses; max is {CPP_MAX_PERSISTED_CONN_IP_ADDRESSES}"
73            ),
74            Self::InvalidCatalog => write!(formatter, "invalid persisted ConnIP catalog"),
75            Self::Encode(error) => write!(formatter, "encode ConnIP catalog: {error}"),
76            Self::Write(error) => write!(formatter, "write ConnIP catalog: {error}"),
77        }
78    }
79}
80
81impl std::error::Error for ConnIpCatalogStoreError {}
82
83#[derive(Debug, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85struct StoredConnIpCatalog {
86    schema_version: u32,
87    conn_identity: u32,
88    addresses: Vec<StoredConnIpAddress>,
89    anti_ddos_ip: Option<String>,
90    condition_flag: i32,
91    previous_endpoint: Option<String>,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96struct StoredConnIpAddress {
97    ip: String,
98    port: u16,
99    region: u32,
100    simplified_description: String,
101    traditional_description: String,
102    english_description: String,
103    enable_backup_port: bool,
104    backup_port: u16,
105    condition_flag: i32,
106}
107
108impl From<&ConnIpAddress> for StoredConnIpAddress {
109    fn from(address: &ConnIpAddress) -> Self {
110        Self {
111            ip: address.ip.clone(),
112            port: address.port,
113            region: address.region,
114            simplified_description: address.simplified_description.clone(),
115            traditional_description: address.traditional_description.clone(),
116            english_description: address.english_description.clone(),
117            enable_backup_port: address.enable_backup_port,
118            backup_port: address.backup_port,
119            condition_flag: address.condition_flag,
120        }
121    }
122}
123
124impl From<StoredConnIpAddress> for ConnIpAddress {
125    fn from(address: StoredConnIpAddress) -> Self {
126        Self {
127            ip: address.ip,
128            port: address.port,
129            region: address.region,
130            simplified_description: address.simplified_description,
131            traditional_description: address.traditional_description,
132            english_description: address.english_description,
133            enable_backup_port: address.enable_backup_port,
134            backup_port: address.backup_port,
135            condition_flag: address.condition_flag,
136        }
137    }
138}
139
140fn conn_ip_catalog_path(conn_identity: u32) -> Result<PathBuf, ConnIpCatalogStoreError> {
141    Ok(ConnIpCatalogStore::production()?.path(conn_identity))
142}
143
144pub fn load_conn_ip_catalog(
145    conn_identity: u32,
146) -> Result<Option<ConnIpCatalog>, ConnIpCatalogStoreError> {
147    load_conn_ip_catalog_from_path(&conn_ip_catalog_path(conn_identity)?, conn_identity)
148}
149
150pub(super) fn load_conn_ip_catalog_from_path(
151    path: &Path,
152    expected_conn_identity: u32,
153) -> Result<Option<ConnIpCatalog>, ConnIpCatalogStoreError> {
154    let bytes = match std::fs::read(path) {
155        Ok(bytes) => bytes,
156        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
157        Err(error) => return Err(ConnIpCatalogStoreError::Read(error)),
158    };
159    let stored: StoredConnIpCatalog =
160        serde_json::from_slice(&bytes).map_err(ConnIpCatalogStoreError::Decode)?;
161    if stored.schema_version != STORE_SCHEMA_VERSION {
162        return Err(ConnIpCatalogStoreError::UnsupportedSchema(
163            stored.schema_version,
164        ));
165    }
166    if stored.conn_identity != expected_conn_identity {
167        return Err(ConnIpCatalogStoreError::IdentityMismatch {
168            expected: expected_conn_identity,
169            actual: stored.conn_identity,
170        });
171    }
172    if stored.addresses.len() > CPP_MAX_PERSISTED_CONN_IP_ADDRESSES {
173        return Err(ConnIpCatalogStoreError::OversizedCatalog(
174            stored.addresses.len(),
175        ));
176    }
177    let snapshot = ConnIpSnapshot {
178        conn_identity: stored.conn_identity,
179        addresses: stored.addresses.into_iter().map(Into::into).collect(),
180        anti_ddos_ip: stored.anti_ddos_ip,
181        condition_flag: stored.condition_flag,
182    };
183    restore_conn_ip_catalog_like_cpp(RestoredConnIpCatalogFacts {
184        expected_conn_identity,
185        snapshot,
186        previous_endpoint: stored.previous_endpoint,
187    })
188    .map(Some)
189    .ok_or(ConnIpCatalogStoreError::InvalidCatalog)
190}
191
192pub fn save_conn_ip_catalog(catalog: &ConnIpCatalog) -> Result<(), ConnIpCatalogStoreError> {
193    save_conn_ip_catalog_to_path(
194        &conn_ip_catalog_path(catalog.snapshot.conn_identity)?,
195        catalog,
196    )
197}
198
199pub(super) fn save_conn_ip_catalog_to_path(
200    path: &Path,
201    catalog: &ConnIpCatalog,
202) -> Result<(), ConnIpCatalogStoreError> {
203    let stored = StoredConnIpCatalog {
204        schema_version: STORE_SCHEMA_VERSION,
205        conn_identity: catalog.snapshot.conn_identity,
206        addresses: catalog
207            .snapshot
208            .addresses
209            .iter()
210            .map(StoredConnIpAddress::from)
211            .collect(),
212        anti_ddos_ip: catalog.snapshot.anti_ddos_ip.clone(),
213        condition_flag: catalog.snapshot.condition_flag,
214        previous_endpoint: catalog.previous_endpoint.clone(),
215    };
216    let bytes = serde_json::to_vec(&stored).map_err(ConnIpCatalogStoreError::Encode)?;
217    crate::auth::write_secret_file(path, &bytes).map_err(ConnIpCatalogStoreError::Write)
218}