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}