1use std::collections::{BTreeMap, BTreeSet};
8use std::fs::{File, OpenOptions};
9use std::io::{Read, Write};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13use arc_swap::ArcSwap;
14use serde::{Deserialize, Serialize};
15
16const SCHEMA_VERSION: u32 = 1;
20const MAX_FILE_BYTES: u64 = 16 * 1024 * 1024;
25
26const APP_LANGUAGE_ZH: i32 = 0;
29const APP_LANGUAGE_HK: i32 = 1;
30const APP_LANGUAGE_EN: i32 = 2;
31const APP_LANGUAGE_JA: i32 = 5;
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct CachedBrokerInfo {
36 pub name_zh_cn: String,
38 pub name_en: String,
40 pub name_tc: String,
42 pub full_name_zh_cn: String,
44 pub full_name_en: String,
46 pub full_name_tc: String,
48}
49
50impl CachedBrokerInfo {
51 #[must_use]
55 pub fn display_name(&self, app_lang: i32) -> Option<String> {
56 let (selected_abbr, selected_full) = match app_lang {
57 APP_LANGUAGE_EN | APP_LANGUAGE_JA => (&self.name_en, &self.full_name_en),
58 APP_LANGUAGE_HK => (&self.name_tc, &self.full_name_tc),
59 APP_LANGUAGE_ZH => (&self.name_zh_cn, &self.full_name_zh_cn),
60 _ => (&self.name_zh_cn, &self.full_name_zh_cn),
61 };
62 [
63 selected_abbr.as_str(),
64 self.name_en.as_str(),
65 self.name_tc.as_str(),
66 self.name_zh_cn.as_str(),
67 selected_full.as_str(),
68 self.full_name_en.as_str(),
69 self.full_name_tc.as_str(),
70 self.full_name_zh_cn.as_str(),
71 ]
72 .into_iter()
73 .find(|name| !name.is_empty())
74 .map(ToOwned::to_owned)
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
79pub enum BrokerDictionarySnapshotError {
80 #[error("broker dictionary version is invalid")]
81 InvalidVersion,
82 #[error("broker dictionary issuer id is invalid")]
83 InvalidIssuer,
84 #[error("broker dictionary issuer id is duplicated")]
85 DuplicateIssuer,
86 #[error("broker dictionary broker code is invalid")]
87 InvalidBrokerCode,
88 #[error("broker dictionary broker code is duplicated")]
89 DuplicateBrokerCode,
90 #[error("broker dictionary broker code points to a missing issuer")]
91 DanglingBrokerCode,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Default)]
95pub struct BrokerDictionarySnapshot {
96 initialized: bool,
97 backend_version: i32,
98 issuer_infos: BTreeMap<i64, CachedBrokerInfo>,
99 broker_code_to_issuer: BTreeMap<i64, i64>,
100}
101
102impl BrokerDictionarySnapshot {
103 pub fn try_from_parts(
104 backend_version: i32,
105 issuer_entries: Vec<(i64, CachedBrokerInfo)>,
106 broker_code_entries: Vec<(i64, i64)>,
107 ) -> Result<Self, BrokerDictionarySnapshotError> {
108 if backend_version < 0 {
109 return Err(BrokerDictionarySnapshotError::InvalidVersion);
110 }
111 let mut issuer_infos = BTreeMap::new();
112 for (issuer_id, info) in issuer_entries {
113 if issuer_id <= 0 {
114 return Err(BrokerDictionarySnapshotError::InvalidIssuer);
115 }
116 if issuer_infos.insert(issuer_id, info).is_some() {
117 return Err(BrokerDictionarySnapshotError::DuplicateIssuer);
118 }
119 }
120
121 let mut broker_code_to_issuer = BTreeMap::new();
122 for (broker_code, issuer_id) in broker_code_entries {
123 if broker_code <= 0 {
124 return Err(BrokerDictionarySnapshotError::InvalidBrokerCode);
125 }
126 if !issuer_infos.contains_key(&issuer_id) {
127 return Err(BrokerDictionarySnapshotError::DanglingBrokerCode);
128 }
129 if broker_code_to_issuer
130 .insert(broker_code, issuer_id)
131 .is_some()
132 {
133 return Err(BrokerDictionarySnapshotError::DuplicateBrokerCode);
134 }
135 }
136 Ok(Self {
137 initialized: true,
138 backend_version,
139 issuer_infos,
140 broker_code_to_issuer,
141 })
142 }
143
144 #[must_use]
145 pub const fn initialized(&self) -> bool {
146 self.initialized
147 }
148
149 #[must_use]
150 pub const fn backend_version(&self) -> i32 {
151 self.backend_version
152 }
153
154 #[must_use]
155 pub const fn issuer_infos(&self) -> &BTreeMap<i64, CachedBrokerInfo> {
156 &self.issuer_infos
157 }
158
159 #[must_use]
160 pub const fn broker_code_to_issuer(&self) -> &BTreeMap<i64, i64> {
161 &self.broker_code_to_issuer
162 }
163}
164
165pub struct BrokerDictionaryCache {
166 snapshot: ArcSwap<BrokerDictionarySnapshot>,
167}
168
169impl BrokerDictionaryCache {
170 #[must_use]
171 pub fn new() -> Self {
172 Self {
173 snapshot: ArcSwap::new(Arc::new(BrokerDictionarySnapshot::default())),
174 }
175 }
176
177 #[must_use]
178 pub fn snapshot(&self) -> Arc<BrokerDictionarySnapshot> {
179 self.snapshot.load_full()
180 }
181
182 pub fn replace(&self, snapshot: BrokerDictionarySnapshot) {
183 debug_assert!(snapshot.initialized());
184 self.snapshot.store(Arc::new(snapshot));
185 }
186
187 #[must_use]
188 pub fn has_last_good(&self) -> bool {
189 self.snapshot.load().initialized()
190 }
191
192 #[must_use]
193 pub fn display_name_for_issuer(&self, issuer_id: i64, app_lang: i32) -> Option<String> {
194 self.snapshot
195 .load()
196 .issuer_infos
197 .get(&issuer_id)
198 .and_then(|info| info.display_name(app_lang))
199 }
200
201 #[must_use]
202 pub fn display_name_for_broker_code(&self, broker_code: i64, app_lang: i32) -> Option<String> {
203 let snapshot = self.snapshot.load();
204 let issuer_id = snapshot.broker_code_to_issuer.get(&broker_code)?;
205 snapshot
206 .issuer_infos
207 .get(issuer_id)
208 .and_then(|info| info.display_name(app_lang))
209 }
210
211 #[must_use]
212 pub fn issuer_for_broker_code(&self, broker_code: i64) -> Option<i64> {
213 self.snapshot
214 .load()
215 .broker_code_to_issuer
216 .get(&broker_code)
217 .copied()
218 }
219
220 #[must_use]
221 pub fn contains_key(&self, issuer_id: &i64) -> bool {
222 self.snapshot.load().issuer_infos.contains_key(issuer_id)
223 }
224
225 #[must_use]
226 pub fn is_empty(&self) -> bool {
227 self.snapshot.load().issuer_infos.is_empty()
228 }
229
230 #[must_use]
231 pub fn len(&self) -> usize {
232 self.snapshot.load().issuer_infos.len()
233 }
234
235 pub fn insert(&self, issuer_id: i64, info: CachedBrokerInfo) {
238 if issuer_id <= 0 {
239 return;
240 }
241 self.snapshot.rcu(|current| {
242 let mut issuer_entries = current
243 .issuer_infos
244 .iter()
245 .map(|(id, info)| (*id, info.clone()))
246 .collect::<Vec<_>>();
247 issuer_entries.retain(|(id, _)| *id != issuer_id);
248 issuer_entries.push((issuer_id, info.clone()));
249 let mut broker_code_entries = current
250 .broker_code_to_issuer
251 .iter()
252 .map(|(code, id)| (*code, *id))
253 .collect::<Vec<_>>();
254 if !current.broker_code_to_issuer.contains_key(&issuer_id) {
255 broker_code_entries.push((issuer_id, issuer_id));
256 }
257 match BrokerDictionarySnapshot::try_from_parts(
258 current.backend_version.max(0),
259 issuer_entries,
260 broker_code_entries,
261 ) {
262 Ok(snapshot) => Arc::new(snapshot),
263 Err(_) => {
264 tracing::error!(
265 error_class = "snapshot_invariant",
266 "broker dictionary compatibility insert preserved last-good snapshot"
267 );
268 Arc::clone(current)
269 }
270 }
271 });
272 }
273}
274
275impl Default for BrokerDictionaryCache {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[derive(Debug, thiserror::Error)]
282pub enum BrokerDictionaryStoreError {
283 #[error("broker dictionary store I/O failed: {0}")]
284 Io(#[from] std::io::Error),
285 #[error("broker dictionary store JSON failed: {0}")]
286 Json(#[from] serde_json::Error),
287 #[error("broker dictionary store file is too large")]
288 TooLarge,
289 #[error("broker dictionary store schema is unsupported")]
290 UnsupportedSchema,
291 #[error("broker dictionary snapshot is uninitialized")]
292 Uninitialized,
293 #[error("broker dictionary snapshot is invalid: {0}")]
294 InvalidSnapshot(#[from] BrokerDictionarySnapshotError),
295}
296
297impl BrokerDictionaryStoreError {
298 #[must_use]
299 pub const fn class(&self) -> &'static str {
300 match self {
301 Self::Io(_) => "io",
302 Self::Json(_) => "json",
303 Self::TooLarge => "too_large",
304 Self::UnsupportedSchema => "unsupported_schema",
305 Self::Uninitialized => "uninitialized",
306 Self::InvalidSnapshot(_) => "invalid_snapshot",
307 }
308 }
309}
310
311#[derive(Debug, Serialize, Deserialize)]
312#[serde(deny_unknown_fields)]
313struct PersistedBrokerDictionary {
314 schema_version: u32,
315 backend_version: i32,
316 issuer_infos: Vec<PersistedIssuerInfo>,
317 broker_code_to_issuer: Vec<PersistedBrokerCode>,
318}
319
320#[derive(Debug, Serialize, Deserialize)]
321#[serde(deny_unknown_fields)]
322struct PersistedIssuerInfo {
323 issuer_id: i64,
324 info: CachedBrokerInfo,
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329struct PersistedBrokerCode {
330 broker_code: i64,
331 issuer_id: i64,
332}
333
334#[derive(Debug, Clone)]
335pub struct BrokerDictionaryStore {
336 path: PathBuf,
337}
338
339impl BrokerDictionaryStore {
340 #[must_use]
341 pub fn new(path: impl Into<PathBuf>) -> Self {
342 Self { path: path.into() }
343 }
344
345 #[must_use]
346 pub fn path(&self) -> &Path {
347 &self.path
348 }
349
350 pub fn load(&self) -> Result<Option<BrokerDictionarySnapshot>, BrokerDictionaryStoreError> {
351 let mut file = match OpenOptions::new().read(true).open(&self.path) {
352 Ok(file) => file,
353 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
354 Err(error) => return Err(error.into()),
355 };
356 let metadata = file.metadata()?;
357 if metadata.len() > MAX_FILE_BYTES {
358 return Err(BrokerDictionaryStoreError::TooLarge);
359 }
360 let mut bytes = Vec::with_capacity(metadata.len() as usize);
361 file.read_to_end(&mut bytes)?;
362 let persisted: PersistedBrokerDictionary = serde_json::from_slice(&bytes)?;
363 if persisted.schema_version != SCHEMA_VERSION {
364 return Err(BrokerDictionaryStoreError::UnsupportedSchema);
365 }
366 let issuer_entries = persisted
367 .issuer_infos
368 .into_iter()
369 .map(|entry| (entry.issuer_id, entry.info))
370 .collect();
371 let broker_code_entries = persisted
372 .broker_code_to_issuer
373 .into_iter()
374 .map(|entry| (entry.broker_code, entry.issuer_id))
375 .collect();
376 Ok(Some(BrokerDictionarySnapshot::try_from_parts(
377 persisted.backend_version,
378 issuer_entries,
379 broker_code_entries,
380 )?))
381 }
382
383 pub fn save(
384 &self,
385 snapshot: &BrokerDictionarySnapshot,
386 ) -> Result<(), BrokerDictionaryStoreError> {
387 self.prepare_save(snapshot)?.commit()
388 }
389
390 pub fn prepare_save(
395 &self,
396 snapshot: &BrokerDictionarySnapshot,
397 ) -> Result<PreparedBrokerDictionarySave, BrokerDictionaryStoreError> {
398 self.prepare_save_inner(snapshot, || {})
399 }
400
401 #[cfg(any(test, feature = "test-util"))]
406 pub fn prepare_save_with_hook(
407 &self,
408 snapshot: &BrokerDictionarySnapshot,
409 during_write: impl FnOnce(),
410 ) -> Result<PreparedBrokerDictionarySave, BrokerDictionaryStoreError> {
411 self.prepare_save_inner(snapshot, during_write)
412 }
413
414 fn prepare_save_inner(
415 &self,
416 snapshot: &BrokerDictionarySnapshot,
417 during_write: impl FnOnce(),
418 ) -> Result<PreparedBrokerDictionarySave, BrokerDictionaryStoreError> {
419 if !snapshot.initialized() {
420 return Err(BrokerDictionaryStoreError::Uninitialized);
421 }
422 let explicit_parent = self
423 .path
424 .parent()
425 .filter(|path| !path.as_os_str().is_empty());
426 let parent = explicit_parent.unwrap_or_else(|| Path::new("."));
427 if let Some(explicit_parent) = explicit_parent {
428 std::fs::create_dir_all(explicit_parent)?;
429 }
430 let persisted = PersistedBrokerDictionary {
431 schema_version: SCHEMA_VERSION,
432 backend_version: snapshot.backend_version,
433 issuer_infos: snapshot
434 .issuer_infos
435 .iter()
436 .map(|(issuer_id, info)| PersistedIssuerInfo {
437 issuer_id: *issuer_id,
438 info: info.clone(),
439 })
440 .collect(),
441 broker_code_to_issuer: snapshot
442 .broker_code_to_issuer
443 .iter()
444 .map(|(broker_code, issuer_id)| PersistedBrokerCode {
445 broker_code: *broker_code,
446 issuer_id: *issuer_id,
447 })
448 .collect(),
449 };
450 let bytes = serde_json::to_vec(&persisted)?;
451 let stem = self
452 .path
453 .file_name()
454 .and_then(|name| name.to_str())
455 .unwrap_or("broker-dictionary-v1.json");
456 let mut collisions = BTreeSet::new();
457 let mut during_write = Some(during_write);
462 for suffix in 0..16_u8 {
463 let temporary = parent.join(format!(".{stem}.{}.{suffix}.tmp", std::process::id()));
464 let mut options = OpenOptions::new();
465 options.write(true).create_new(true);
466 #[cfg(unix)]
467 {
468 use std::os::unix::fs::OpenOptionsExt as _;
469 options.mode(0o600);
470 }
471 let mut file = match options.open(&temporary) {
472 Ok(file) => file,
473 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
474 collisions.insert(suffix);
475 continue;
476 }
477 Err(error) => return Err(error.into()),
478 };
479 if let Err(error) = set_file_private(&temporary) {
480 let _ = std::fs::remove_file(&temporary);
481 return Err(error.into());
482 }
483 if let Some(during_write) = during_write.take() {
484 during_write();
485 }
486 if let Err(error) = write_prepared(&mut file, &bytes) {
487 let _ = std::fs::remove_file(&temporary);
488 return Err(error.into());
489 }
490 return Ok(PreparedBrokerDictionarySave {
491 temporary,
492 destination: self.path.clone(),
493 parent: parent.to_path_buf(),
494 committed: false,
495 });
496 }
497 Err(std::io::Error::other(format!(
498 "broker dictionary temp names unavailable: {} collisions",
499 collisions.len()
500 ))
501 .into())
502 }
503}
504
505pub struct PreparedBrokerDictionarySave {
506 temporary: PathBuf,
507 destination: PathBuf,
508 parent: PathBuf,
509 committed: bool,
510}
511
512impl PreparedBrokerDictionarySave {
513 pub fn commit(mut self) -> Result<(), BrokerDictionaryStoreError> {
518 atomic_replace(&self.temporary, &self.destination)?;
519 self.committed = true;
520 set_file_private(&self.destination)?;
521 sync_directory(&self.parent)?;
522 Ok(())
523 }
524}
525
526impl Drop for PreparedBrokerDictionarySave {
527 fn drop(&mut self) {
528 if !self.committed {
529 let _ = std::fs::remove_file(&self.temporary);
530 }
531 }
532}
533
534#[must_use]
535pub fn default_broker_dictionary_path_from_home(home: &Path) -> PathBuf {
536 home.join(".futu-opend-rs")
537 .join("broker-dictionary-v1.json")
538}
539
540#[must_use]
541pub fn default_broker_dictionary_store() -> Option<BrokerDictionaryStore> {
542 std::env::var_os("HOME")
543 .or_else(|| std::env::var_os("USERPROFILE"))
544 .map(PathBuf::from)
545 .map(|home| BrokerDictionaryStore::new(default_broker_dictionary_path_from_home(&home)))
546}
547
548fn write_prepared(file: &mut File, bytes: &[u8]) -> std::io::Result<()> {
549 file.write_all(bytes)?;
550 file.sync_all()
551}
552
553fn atomic_replace(temporary: &Path, destination: &Path) -> std::io::Result<()> {
554 std::fs::rename(temporary, destination)
559}
560
561#[cfg(unix)]
562fn set_file_private(path: &Path) -> std::io::Result<()> {
563 use std::os::unix::fs::PermissionsExt;
564 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
565}
566
567#[cfg(not(unix))]
568fn set_file_private(_path: &Path) -> std::io::Result<()> {
569 Ok(())
570}
571
572#[cfg(not(windows))]
573fn sync_directory(path: &Path) -> std::io::Result<()> {
574 File::open(path)?.sync_all()
575}
576
577#[cfg(windows)]
578fn sync_directory(_path: &Path) -> std::io::Result<()> {
579 Ok(())
582}