c5store/
lib.rs

1#[cfg(feature = "bootstrapper")]
2pub mod bootstrapper;
3mod c5_serde;
4mod config_source;
5mod data;
6pub mod error;
7mod internal;
8pub mod providers;
9#[cfg(feature = "secrets")]
10pub mod secrets;
11#[cfg(not(feature = "secrets"))]
12pub mod secrets_dummy;
13pub mod serialization;
14pub mod telemetry;
15pub mod util;
16pub mod value;
17
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::ffi::OsStr;
21use std::fs::read_dir;
22use std::path::PathBuf;
23use std::sync::Arc;
24use std::time::Duration;
25use std::{env, fs};
26
27use c5_serde::de::C5SerdeValueDeserializer;
28use config_source::ConfigSource;
29use curve25519_parser::parse_openssl_25519_privkey;
30#[cfg(feature = "dotenv")]
31use dotenvy;
32use error::ConfigError;
33use multimap::MultiMap;
34use parking_lot::Mutex;
35use scheduled_thread_pool::{JobHandle, ScheduledThreadPool};
36use serde::de::DeserializeOwned;
37use serialization::map_from_serde_yaml_valuemap;
38#[cfg(feature = "toml")]
39use serialization::map_from_toml_value_map;
40use util::build_flat_map;
41
42use crate::data::HashsetMultiMap;
43use crate::internal::{C5DataStore, C5StoreDataValueRef, C5StoreSubscriptions};
44use crate::providers::{
45  C5ValueProvider, CONFIG_KEY_KEYNAME, CONFIG_KEY_KEYPATH, CONFIG_KEY_PROVIDER,
46};
47#[cfg(feature = "secrets")]
48use crate::secrets::SecretKeyStore;
49#[cfg(feature = "secrets")]
50use crate::secrets::systemd::SystemdCredential;
51#[cfg(feature = "secrets")]
52use crate::secrets::systemd::load_systemd_credentials;
53#[cfg(not(feature = "secrets"))]
54use crate::secrets_dummy::{SecretKeyStore, SecretKeyStoreConfiguratorFn};
55use crate::telemetry::{ConsoleLogger, Logger, StatsRecorder, StatsRecorderStub};
56use crate::value::C5DataValue;
57
58const DEFAULT_CHANGE_DELAY_PERIOD: u64 = 500;
59
60pub struct HydrateContext {
61  pub logger: Arc<dyn Logger>,
62}
63
64impl HydrateContext {
65  pub fn push_value_to_data_store(set_data_fn: &SetDataFn, key: &str, value: C5DataValue) {
66    match value {
67      C5DataValue::Map(mut value) => {
68        let mut config_data = HashMap::new();
69        build_flat_map(&mut value, &mut config_data, String::from(key));
70
71        for config_entry in config_data.into_iter() {
72          let config_entry_key = config_entry.0;
73          let config_value = config_entry.1;
74
75          set_data_fn(&config_entry_key, config_value);
76        }
77        return;
78      }
79      _ => {}
80    }
81    set_data_fn(key, value);
82  }
83}
84
85// params: notify key path, key path, value
86pub type ChangeListener = dyn Fn(&str, &str, &C5DataValue) -> () + Send + Sync;
87// params: notify key path, key path, new value, old value (Option)
88pub type DetailedChangeListener =
89  dyn Fn(&str, &str, &C5DataValue, Option<&C5DataValue>) -> () + Send + Sync;
90pub type SetDataFn = dyn Fn(&str, C5DataValue) + Send + Sync;
91#[cfg(feature = "secrets")]
92pub type SecretKeyStoreConfiguratorFn = dyn FnMut(&mut SecretKeyStore);
93
94#[cfg(feature = "secrets")]
95pub struct SecretOptions {
96  pub secret_key_path_segment: Option<String>,
97  pub secret_keys_path: Option<PathBuf>,
98  pub secret_key_store_configure_fn: Option<Box<SecretKeyStoreConfiguratorFn>>,
99  pub load_secret_keys_from_env: bool,
100  pub secret_key_env_prefix: Option<String>, // e.g., "C5_SECRETKEY_"
101  pub load_credentials_from_systemd: Vec<SystemdCredential>,
102}
103
104impl Default for SecretOptions {
105  fn default() -> Self {
106    return Self {
107      secret_key_path_segment: Some(".c5encval".to_string()),
108      secret_keys_path: None,
109      secret_key_store_configure_fn: None,
110      load_secret_keys_from_env: false,
111      secret_key_env_prefix: Some("C5_SECRETKEY_".to_string()),
112      load_credentials_from_systemd: Vec::new(),
113    };
114  }
115}
116
117#[cfg(not(feature = "secrets"))]
118#[derive(Default)]
119pub struct SecretOptions {}
120
121pub struct C5StoreOptions {
122  pub logger: Option<Arc<dyn Logger>>,
123  pub stats: Option<Arc<dyn StatsRecorder>>,
124  pub change_delay_period: Option<u64>,
125  pub secret_opts: SecretOptions,
126  #[cfg(feature = "dotenv")]
127  pub dotenv_path: Option<PathBuf>, // Path to .env file
128}
129
130impl Default for C5StoreOptions {
131  fn default() -> Self {
132    return Self {
133      logger: None,
134      stats: None,
135      change_delay_period: Some(DEFAULT_CHANGE_DELAY_PERIOD),
136      secret_opts: SecretOptions::default(),
137      #[cfg(feature = "dotenv")]
138      dotenv_path: None,
139    };
140  }
141}
142
143/// Define a struct to hold pending change info
144struct PendingChange {
145  old_value: Option<C5DataValue>,
146  new_value: C5DataValue,
147}
148
149struct ChangeNotifier {
150  debounce_job_handle: Arc<Mutex<RefCell<Option<JobHandle>>>>,
151  thread_pool: Arc<ScheduledThreadPool>,
152  delay_period: Duration,
153  pending_changes: Arc<Mutex<HashMap<String, PendingChange>>>, // Key: changed_key_path
154  _data_store: C5DataStore,
155  _subscriptions: C5StoreSubscriptions,
156}
157
158impl ChangeNotifier {
159  pub fn new(
160    delay_period: Duration,
161    data_store: C5DataStore,
162    subscriptions: C5StoreSubscriptions,
163  ) -> ChangeNotifier {
164    return ChangeNotifier {
165      debounce_job_handle: Arc::new(Mutex::new(RefCell::new(None))),
166      thread_pool: Arc::new(
167        ScheduledThreadPool::builder()
168          .num_threads(1)
169          .thread_name_pattern("c5Store_change_notifier")
170          .build(),
171      ),
172      delay_period,
173      pending_changes: Arc::new(Mutex::new(HashMap::new())),
174      _data_store: data_store,
175      _subscriptions: subscriptions,
176    };
177  }
178
179  pub fn notify_changed(
180    &self,
181    key: &str,
182    old_value: Option<C5DataValue>, // Pass owned Option<C5DataValue>
183    new_value: C5DataValue,         // Pass owned C5DataValue
184  ) {
185    let debounce_job_lock = self.debounce_job_handle.lock();
186
187    self.pending_changes.lock().insert(
188      key.to_string(),
189      PendingChange {
190        old_value,
191        new_value,
192      },
193    );
194
195    let should_schedule = debounce_job_lock.borrow().is_none();
196    if should_schedule {
197      let debounce_mut = self.debounce_job_handle.clone();
198      let pending_changes_arc = self.pending_changes.clone();
199      let subscriptions = self._subscriptions.clone();
200
201      let job = move || {
202        let changes_to_process: HashMap<String, PendingChange> =
203          pending_changes_arc.lock().drain().collect();
204
205        let debounce_job_lock_inner = debounce_mut.lock();
206        let mut job_handle_borrow_inner = debounce_job_lock_inner.borrow_mut(); // Mutable borrow here is fine
207        job_handle_borrow_inner.take(); // Clear the handle
208        drop(job_handle_borrow_inner); // Release mutable borrow
209        drop(debounce_job_lock_inner); // Release lock
210
211        // Process the collected changes
212        if !changes_to_process.is_empty() {
213          // Build map of ancestors to notify for each actual change
214          let mut notifications_to_send: HashsetMultiMap<String, String> = HashsetMultiMap::new();
215          for changed_key in changes_to_process.keys() {
216            notifications_to_send.insert(changed_key.clone(), changed_key.clone());
217            let mut key_ancestor_path = String::new();
218            for part in changed_key.split('.') {
219              if !key_ancestor_path.is_empty() {
220                key_ancestor_path.push('.');
221              }
222              key_ancestor_path.push_str(part);
223              if &key_ancestor_path != changed_key {
224                // Don't add self as ancestor for notification map
225                notifications_to_send.insert(changed_key.clone(), key_ancestor_path.clone());
226              }
227            }
228          }
229
230          // Iterate through actual changed keys and their corresponding ancestor paths to notify
231          for (changed_key, notify_paths) in notifications_to_send.iter() {
232            if let Some(change_detail) = changes_to_process.get(changed_key) {
233              for notify_path in notify_paths {
234                subscriptions.notify_value_change(
235                  notify_path,
236                  changed_key,
237                  &change_detail.new_value, // Pass reference to stored new value
238                  change_detail.old_value.as_ref(), // Pass reference to stored Option<old value>
239                );
240              }
241            }
242          }
243        }
244      };
245
246      debounce_job_lock.replace(Some(
247        self
248          .thread_pool
249          .execute_after(self.delay_period.clone(), job),
250      ));
251    }
252  }
253}
254
255pub trait C5Store {
256  fn get(&self, key_path: &str) -> Option<C5DataValue>;
257
258  fn get_ref(&self, key_path: &str) -> Option<C5StoreDataValueRef>;
259
260  fn get_into<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
261  where
262    C5DataValue: TryInto<Val, Error = ConfigError>;
263
264  fn get_into_struct<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
265  where
266    Val: DeserializeOwned;
267
268  fn exists(&self, key_path: &str) -> bool;
269
270  fn path_exists(&self, key: &str) -> bool;
271
272  //
273  // Listens to changes to the given keyPath. keyPath can be any the entire path or ancestors.
274  // By listening to an ancestor, one will receive one change event even if two children change.
275  //
276  fn subscribe(&self, key_path: &str, listener: Box<ChangeListener>);
277
278  fn subscribe_detailed(&self, key_path: &str, listener: Box<DetailedChangeListener>);
279
280  fn branch(&self, key_path: &str) -> C5StoreBranch;
281
282  //
283  // Searches for all keypaths that relative to currentKeyPath + given keyPath
284  // @return A list of Key Paths
285  //
286  fn key_paths_with_prefix(&self, key_path: Option<&str>) -> Vec<String>;
287
288  //
289  // @return null if root, prefixKey if branch
290  //
291  fn current_key_path(&self) -> &str;
292
293  fn get_source(&self, key_path: &str) -> Option<ConfigSource>;
294}
295
296#[derive(Clone)]
297pub struct C5StoreRoot {
298  _data_store: C5DataStore,
299  _subscriptions: C5StoreSubscriptions,
300}
301
302impl C5StoreRoot {
303  pub(crate) fn new(c5data_store: C5DataStore, subscriptions: C5StoreSubscriptions) -> C5StoreRoot {
304    return C5StoreRoot {
305      _data_store: c5data_store,
306      _subscriptions: subscriptions,
307    };
308  }
309}
310
311impl C5Store for C5StoreRoot {
312  fn get(&self, key_path: &str) -> Option<C5DataValue> {
313    return self._data_store.get_data(key_path);
314  }
315
316  fn get_into<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
317  where
318    C5DataValue: TryInto<Val, Error = ConfigError>,
319  {
320    self
321      ._data_store
322      .get_data(key_path)
323      .ok_or_else(|| ConfigError::KeyNotFound(key_path.to_string()))
324      .and_then(|val| val.try_into())
325  }
326
327  fn get_into_struct<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
328  where
329    Val: DeserializeOwned,
330  {
331    if let Some(direct_c5_value) = self.get(key_path) {
332      // Attempt to deserialize this direct C5DataValue
333      // We need to check if it's a Map or Array, as structs usually deserialize from these.
334      // Primitive types might deserialize if the struct is a newtype struct.
335      match direct_c5_value {
336        C5DataValue::Map(_)
337        | C5DataValue::Array(_)
338        | C5DataValue::String(_)
339        | C5DataValue::Integer(_)
340        | C5DataValue::UInteger(_)
341        | C5DataValue::Float(_)
342        | C5DataValue::Boolean(_)
343        | C5DataValue::Bytes(_) => {
344          // It's a potentially deserializable type.
345          let deserializer = C5SerdeValueDeserializer::from_c5(&direct_c5_value);
346          match Val::deserialize(deserializer) {
347            Ok(result) => return Ok(result), // Success with direct value!
348            Err(direct_err) => {
349              // It existed directly, but didn't deserialize.
350              // This *might* mean it wasn't the intended struct map,
351              // OR it could be a genuine partial map where flattened keys should complete it.
352              // Let's proceed to Strategy 2.
353              // If it's not a Map, prefix fetch is unlikely to help unless the prefix itself IS the struct.
354              if !matches!(direct_c5_value, C5DataValue::Map(_)) && !key_path.is_empty() {
355                // If the direct value wasn't a map (and not at root), deserialization likely failed
356                // because the type was wrong (e.g., trying to deserialize a struct from a C5 String).
357                // The original error `direct_err` should be informative.
358                // We still fall through to prefix fetch, as the prefix itself might contain the map.
359              }
360              // Log potential issue or decision to fallback?
361              // self._data_store._logger.debug(format!("Direct value at '{}' failed to deserialize fully ({:?}), trying prefix fetch.", key_path, direct_err));
362            }
363          }
364        }
365        C5DataValue::Null => {
366          // If direct value is Null, it won't deserialize into a typical struct.
367          // Fall through to prefix search, as children might exist.
368        }
369      }
370    }
371
372    // --- Strategy 2: Fetch children using the key as a prefix and reconstruct a C5DataValue::Map or C5DataValue::Array ---
373    // This handles flattened keys (env vars, flat files) or completes partial direct maps.
374    match self._data_store.fetch_children_as_c5_value(key_path) {
375      Ok(C5DataValue::Null) => {
376        // No direct value (handled above) and no children found via prefix.
377        // This could also mean the prefix *was* the target and we already tried and failed above.
378        // If we are here, and a direct value was found but failed to deserialize, that error might be more relevant.
379        // However, `KeyNotFound` is the common case if nothing was found at all.
380        Err(ConfigError::KeyNotFound(key_path.to_string()))
381      }
382      Ok(reconstructed_c5_value) => {
383        // Attempt to deserialize the C5DataValue reconstructed from children
384        let deserializer = C5SerdeValueDeserializer::from_c5(&reconstructed_c5_value);
385        Val::deserialize(deserializer).map_err(|e| {
386          // The error `e` here is already a ConfigError from our C5ValueDeserializer
387          // We might want to wrap it to add more context if needed, but often it's fine.
388          // Example: if `e` is TypeMismatch, we might want to add the key_path here.
389          match e {
390            ConfigError::TypeMismatch {
391              key: _,
392              expected_type,
393              found_type,
394            } => ConfigError::TypeMismatch {
395              key: key_path.to_string(),
396              expected_type,
397              found_type,
398            },
399            ConfigError::DeserializationError { key: _, source } => {
400              // Should not happen if C5ValueDeserializer is correct
401              ConfigError::DeserializationError {
402                key: key_path.to_string(),
403                source,
404              }
405            }
406            other_err => other_err, // Propagate other errors like Message, KeyNotFound (from within MapAccess etc.)
407          }
408        })
409      }
410      Err(e) => Err(e), // Propagate errors from fetch_children_as_c5_value
411    }
412  }
413
414  fn get_ref(&self, key_path: &str) -> Option<C5StoreDataValueRef> {
415    return self._data_store.get_data_ref(key_path);
416  }
417
418  fn exists(&self, key_path: &str) -> bool {
419    return self._data_store.exists(key_path);
420  }
421
422  fn path_exists(&self, key_path: &str) -> bool {
423    return self._data_store.prefix_key_exists(key_path);
424  }
425
426  fn subscribe(&self, key_path: &str, listener: Box<ChangeListener>) {
427    self._subscriptions.add(key_path, listener);
428  }
429
430  fn subscribe_detailed(&self, key_path: &str, listener: Box<DetailedChangeListener>) {
431    self._subscriptions.add_detailed(key_path, listener);
432  }
433
434  fn branch(&self, key_path: &str) -> C5StoreBranch {
435    return C5StoreBranch {
436      _root: self.clone(),
437      _key_path: key_path.to_string(),
438    };
439  }
440
441  fn key_paths_with_prefix(&self, key_path: Option<&str>) -> Vec<String> {
442    return self._data_store.keys_with_prefix(key_path);
443  }
444
445  fn current_key_path(&self) -> &str {
446    return "";
447  }
448
449  fn get_source(&self, key_path: &str) -> Option<ConfigSource> {
450    return self._data_store.get_source_info(key_path);
451  }
452}
453
454#[derive(Clone)]
455pub struct C5StoreBranch {
456  _root: C5StoreRoot,
457  _key_path: String,
458}
459
460impl C5StoreBranch {
461  fn _merge_key_path(&self, key_path: &str) -> String {
462    return self._key_path.to_string() + "." + key_path;
463  }
464}
465
466impl C5Store for C5StoreBranch {
467  fn get(&self, key_path: &str) -> Option<C5DataValue> {
468    return self._root.get(&self._merge_key_path(key_path));
469  }
470
471  fn get_into<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
472  where
473    C5DataValue: TryInto<Val, Error = ConfigError>,
474  {
475    return self._root.get_into(&self._merge_key_path(key_path));
476  }
477
478  fn get_into_struct<Val>(&self, key_path: &str) -> Result<Val, ConfigError>
479  where
480    Val: DeserializeOwned,
481  {
482    return self._root.get_into_struct(&self._merge_key_path(key_path));
483  }
484
485  fn get_ref(&self, key_path: &str) -> Option<C5StoreDataValueRef> {
486    return self._root.get_ref(&self._merge_key_path(key_path));
487  }
488
489  fn exists(&self, key_path: &str) -> bool {
490    return self._root.exists(&self._merge_key_path(key_path));
491  }
492
493  fn path_exists(&self, key_path: &str) -> bool {
494    return self._root.path_exists(&self._merge_key_path(key_path));
495  }
496
497  fn subscribe(&self, key_path: &str, listener: Box<ChangeListener>) {
498    self
499      ._root
500      .subscribe(&self._merge_key_path(key_path), listener);
501  }
502
503  fn subscribe_detailed(&self, key_path: &str, listener: Box<DetailedChangeListener>) {
504    self
505      ._root
506      .subscribe_detailed(&self._merge_key_path(key_path), listener);
507  }
508
509  fn branch(&self, key_path: &str) -> C5StoreBranch {
510    return C5StoreBranch {
511      _root: self._root.clone(),
512      _key_path: self._merge_key_path(key_path),
513    };
514  }
515
516  fn key_paths_with_prefix(&self, key_path_option: Option<&str>) -> Vec<String> {
517    return match key_path_option {
518      Some(key_path) => {
519        let merged_key_path = self._merge_key_path(key_path);
520        self._root.key_paths_with_prefix(Some(&merged_key_path))
521      }
522      None => self._root.key_paths_with_prefix(None),
523    };
524  }
525
526  fn current_key_path(&self) -> &str {
527    return &self._key_path;
528  }
529
530  fn get_source(&self, key_path: &str) -> Option<ConfigSource> {
531    self._root.get_source(&self._merge_key_path(key_path))
532  }
533}
534
535pub struct C5StoreMgr {
536  _value_providers: Arc<Mutex<HashMap<String, Box<dyn C5ValueProvider>>>>,
537  _scheduled_thread_pool: ScheduledThreadPool,
538  _scheduled_provider_job_handles: Vec<JobHandle>,
539  _data_store: C5StoreRoot,
540  _logger: Arc<dyn Logger>,
541  _stats: Arc<dyn StatsRecorder>,
542  _change_notifier: Arc<ChangeNotifier>,
543  _set_data_fn: Arc<SetDataFn>,
544  _provided_data: MultiMap<String, C5DataValue>,
545}
546
547impl C5StoreMgr {
548  fn new(
549    data_store: C5StoreRoot,
550    logger: Arc<dyn Logger>,
551    stats: Arc<dyn StatsRecorder>,
552    change_notifier: Arc<ChangeNotifier>,
553    set_data_fn: Arc<SetDataFn>,
554    provided_data: MultiMap<String, C5DataValue>,
555  ) -> C5StoreMgr {
556    return C5StoreMgr {
557      _value_providers: Arc::new(Mutex::new(HashMap::new())),
558      _scheduled_thread_pool: ScheduledThreadPool::builder()
559        .num_threads(8)
560        .thread_name_pattern("c5store_mgr")
561        .build(),
562      _scheduled_provider_job_handles: vec![],
563      _data_store: data_store,
564      _logger: logger,
565      _stats: stats,
566      _change_notifier: change_notifier,
567      _set_data_fn: set_data_fn,
568      _provided_data: provided_data,
569    };
570  }
571
572  pub fn set_value_provider<ValueProvider>(
573    &mut self,
574    name: &str,
575    mut value_provider: ValueProvider,
576    refresh_period_sec: u64,
577  ) where
578    ValueProvider: 'static + C5ValueProvider,
579  {
580    let hydrate_context = HydrateContext {
581      logger: self._logger.clone(),
582    };
583
584    let provided_data_option = self._provided_data.get_vec(name);
585
586    if provided_data_option.is_none() {
587      self._logger.warn(format!("{} value provider has no data to provide. Either remove this value provider or add configuration it must provide.", name).as_str());
588      return;
589    }
590
591    let provided_data = provided_data_option.unwrap();
592
593    for p_data in provided_data {
594      value_provider.register(p_data);
595    }
596
597    value_provider.hydrate(&*self._set_data_fn, true, &hydrate_context);
598
599    self
600      ._value_providers
601      .lock()
602      .insert(name.to_string(), Box::from(value_provider));
603
604    if refresh_period_sec > 0 {
605      // logger.debug(format!("Will refresh {} Value Provider every {} seconds.", name, refresh_period_sec));
606
607      let refresh_period_duration = Duration::from_secs(refresh_period_sec);
608
609      let value_providers_clone = self._value_providers.clone();
610      let set_data_fn = self._set_data_fn.clone();
611      let name_clone = name.to_string();
612      let job = move || {
613        let value_providers = value_providers_clone.clone();
614        let value_providers_lock = value_providers.lock();
615        let value_provider_result = value_providers_lock.get(&name_clone);
616
617        if let Some(value_provider) = value_provider_result {
618          value_provider.hydrate(&*set_data_fn, true, &hydrate_context);
619        }
620      };
621
622      let job_handle = self._scheduled_thread_pool.execute_at_fixed_rate(
623        refresh_period_duration.clone(),
624        refresh_period_duration,
625        job,
626      );
627
628      self._scheduled_provider_job_handles.push(job_handle);
629    } else {
630      // logger.debug(format!("Will not be refreshing {} Value Provider", name));
631    }
632  }
633}
634
635impl Drop for C5StoreMgr {
636  fn drop(&mut self) {
637    self._logger.info("Stopping C5StoreMgr");
638
639    while self._scheduled_provider_job_handles.len() > 0 {
640      let job_handle = self._scheduled_provider_job_handles.pop().unwrap();
641      job_handle.cancel();
642    }
643
644    self._logger.info("Stopped C5StoreMgr");
645  }
646}
647
648pub fn create_c5store(
649  config_file_paths: Vec<PathBuf>,
650  mut options_option: Option<C5StoreOptions>,
651) -> Result<(C5StoreRoot, C5StoreMgr), ConfigError> {
652  if options_option.is_none() {
653    options_option = Some(C5StoreOptions::default());
654  }
655
656  let mut options = options_option.unwrap();
657
658  #[cfg(feature = "dotenv")]
659  {
660    if let Some(dotenv_path) = &options.dotenv_path {
661      println!("[dotenv] Loading environment from {:?}", dotenv_path); // Optional log
662      match dotenvy::from_path(dotenv_path) {
663        Ok(_) => {}
664        Err(e) if e.not_found() => {} // Ignore if file not found, common case
665        Err(e) => {
666          return Err(ConfigError::DotEnvLoadError {
667            path: dotenv_path.clone(),
668            source: e,
669          });
670        }
671      }
672    } else {
673      // Maybe try loading default .env path? Or require explicit path?
674      // Let's require explicit path for now via C5StoreOptions.
675    }
676  }
677
678  #[cfg(not(feature = "secrets"))]
679  let mut secret_key_store = SecretKeyStore::default();
680
681  #[cfg(feature = "secrets")]
682  let secret_key_store = {
683    let mut secret_key_store = SecretKeyStore::new();
684
685    if let Some(ref mut configure_fn) = options.secret_opts.secret_key_store_configure_fn {
686      (configure_fn)(&mut secret_key_store);
687    }
688
689    load_secret_key_files(
690      options.secret_opts.secret_keys_path.as_ref(),
691      &mut secret_key_store,
692    )?;
693
694    if options.secret_opts.load_secret_keys_from_env {
695      let prefix = options
696        .secret_opts
697        .secret_key_env_prefix
698        .as_deref()
699        .unwrap_or("C5_SECRETKEY_");
700      load_secret_keys_from_env(prefix, &mut secret_key_store);
701    }
702
703    load_systemd_credentials(&options.secret_opts, &mut secret_key_store)?;
704
705    secret_key_store
706  };
707
708  if options.stats.is_none() {
709    options.stats = Some(Arc::new(StatsRecorderStub {}));
710  }
711
712  if options.logger.is_none() {
713    options.logger = Some(Arc::new(ConsoleLogger {}));
714  }
715
716  if options.change_delay_period.is_none() {
717    options.change_delay_period = Some(DEFAULT_CHANGE_DELAY_PERIOD);
718  }
719
720  let secret_key_store = Arc::new(secret_key_store);
721  let logger = options.logger.as_ref().unwrap().clone();
722  let stats = options.stats.as_ref().unwrap().clone();
723
724  let secret_segment = {
725    #[cfg(feature = "secrets")]
726    {
727      options
728        .secret_opts
729        .secret_key_path_segment
730        .clone()
731        .unwrap_or(".c5encval".to_string())
732    }
733    #[cfg(not(feature = "secrets"))]
734    {
735      ".c5encval".to_string()
736    }
737  };
738
739  let data_store = C5DataStore::new(
740    logger.clone(),
741    stats.clone(),
742    secret_segment,
743    secret_key_store.clone(),
744  );
745  let subscriptions = C5StoreSubscriptions::new();
746  let root = C5StoreRoot::new(data_store.clone(), subscriptions.clone());
747  let change_notifier = Arc::new(ChangeNotifier::new(
748    Duration::from_millis(options.change_delay_period.unwrap()),
749    data_store.clone(),
750    subscriptions.clone(),
751  ));
752
753  let set_data_fn = {
754    let data_store_clone = data_store.clone();
755    let change_notifier_clone = change_notifier.clone();
756
757    Arc::new(move |key: &str, value: C5DataValue| {
758      let data_store = data_store_clone.clone();
759      let change_notifier = change_notifier_clone.clone();
760
761      // Check *before* setting the data
762      let old_value = data_store.get_data(key); // Get current value
763
764      let needs_update = match &old_value {
765        Some(ov) => ov != &value, // Update if value differs
766        None => true,             // Update if key didn't exist
767      };
768
769      if needs_update {
770        // Set the data (which might decrypt secrets)
771        // Use internal setter to avoid infinite loop if set_data called set_data
772        // And pass a relevant source if possible (tricky here)
773        let source = ConfigSource::SetProgrammatically; // Or determine source if possible
774        let _prev_val = data_store._set_data_internal(key, value.clone(), source); // Use internal setter
775
776        // Notify AFTER setting the data, passing old and new values
777        change_notifier.notify_changed(key, old_value, value); // Pass owned values
778      }
779    })
780  };
781
782  let mut provided_data: MultiMap<String, C5DataValue> = MultiMap::new();
783
784  read_config_data(&config_file_paths, &data_store, &mut provided_data)?;
785
786  let c5store_mgr = C5StoreMgr::new(
787    root.clone(),
788    logger.clone(),
789    stats.clone(),
790    change_notifier.clone(),
791    set_data_fn,
792    provided_data,
793  );
794
795  return Ok((root, c5store_mgr));
796}
797
798// Helper function to read environment variables
799fn process_environment_variables(set_data_fn: Arc<SetDataFn>) {
800  const PREFIX: &str = "C5_";
801  const SEPARATOR: &str = "__";
802
803  for (key, value) in env::vars() {
804    if key.starts_with(PREFIX) {
805      let trimmed_key = key.trim_start_matches(PREFIX);
806      let c5_key = trimmed_key.replace(SEPARATOR, ".").to_lowercase(); // Convert C5_DB__HOST to db.host
807
808      // PHASE 1 CHANGE: Treat env vars as strings initially.
809      // Let get_into/get_into_struct handle final conversion.
810      // More complex parsing could be added later if needed.
811      println!("[EnvVar] Setting '{}' from env var '{}'", c5_key, key); // Optional: Add logging
812      set_data_fn(&c5_key, C5DataValue::String(value));
813    }
814  }
815}
816
817#[cfg(feature = "secrets")]
818pub fn load_secret_key_files(
819  secret_keys_path_str: Option<&PathBuf>,
820  secret_key_store: &mut SecretKeyStore,
821) -> Result<(), ConfigError> {
822  if secret_keys_path_str.is_none() {
823    return Ok(());
824  }
825
826  let skpath = secret_keys_path_str.unwrap();
827
828  if !skpath.exists() {
829    println!(
830      "[Secrets] Warning: Secret keys path {:?} does not exist.",
831      skpath
832    );
833    return Ok(()); // Don't error if path doesn't exist
834  }
835
836  if !skpath.is_dir() {
837    return Err(ConfigError::Message(format!(
838      "Secret keys path {:?} is not a directory",
839      skpath
840    )));
841  }
842
843  let files = read_dir(skpath).map_err(|e| ConfigError::IoError {
844    path: skpath.clone(),
845    source: e,
846  })?;
847
848  for dir_entry_result in files {
849    let dir_entry = dir_entry_result.map_err(|e| ConfigError::IoError {
850      path: skpath.clone(),
851      source: e,
852    })?;
853    let entry_path = dir_entry.path();
854
855    if entry_path.is_dir() {
856      continue;
857    }
858
859    let key_result = fs::read(&entry_path).map_err(|e| ConfigError::IoError {
860      path: entry_path.clone(),
861      source: e,
862    });
863    if key_result.is_err() {
864      eprintln!(
865        "[Secrets] Error reading key file {:?}: {:?}",
866        entry_path,
867        key_result.err()
868      );
869      continue; // Skip file on read error? Or return Err? Let's skip for now.
870    }
871    let mut key = key_result.unwrap();
872
873    let file_ext_os = entry_path.extension();
874    let file_name_os = entry_path.file_name();
875
876    if file_ext_os.is_none() || file_name_os.is_none() {
877      eprintln!(
878        "[Secrets] Skipping file with missing name or extension: {:?}",
879        entry_path
880      );
881      continue;
882    }
883    let file_ext = file_ext_os.unwrap().to_str().unwrap_or("");
884    let file_name = file_name_os.unwrap().to_str().unwrap_or("");
885
886    if file_name.is_empty() || file_name.len() <= file_ext.len() + 1 {
887      eprintln!(
888        "[Secrets] Skipping file with invalid name format: {:?}",
889        entry_path
890      );
891      continue;
892    }
893
894    // Robustly get key name
895    let key_name = match file_name.rfind('.') {
896      Some(dot_index) => &file_name[..dot_index],
897      None => file_name, // Should not happen if extension exists, but handle defensively
898    };
899
900    if file_ext == "pem" {
901      // Handle potential parsing errors
902      match parse_openssl_25519_privkey(&key) {
903        Ok(parsed_key) => key = parsed_key.to_bytes().to_vec(),
904        Err(e) => {
905          eprintln!(
906            "[Secrets] Error parsing PEM key file {:?}: {}",
907            entry_path, e
908          );
909          continue; // Skip invalid PEM files
910        }
911      }
912    }
913
914    println!(
915      "[Secrets] Loading key '{}' from file {:?}",
916      key_name, entry_path
917    ); // Optional log
918    secret_key_store.set_key(key_name, key);
919  }
920  Ok(())
921}
922
923#[cfg(feature = "secrets")]
924fn load_secret_keys_from_env(prefix: &str, secret_key_store: &mut SecretKeyStore) {
925  use base64::Engine;
926  for (key, value) in env::vars() {
927    if key.starts_with(prefix) {
928      let key_name = key.trim_start_matches(prefix).to_lowercase();
929      // Assume value is base64 encoded key bytes
930      match base64::engine::general_purpose::STANDARD.decode(&value) {
931        Ok(key_bytes) => {
932          println!(
933            "[Secrets] Loading key '{}' from env var '{}'",
934            key_name, key
935          ); // Optional log
936          secret_key_store.set_key(&key_name, key_bytes);
937        }
938        Err(e) => {
939          eprintln!(
940            "[Secrets] Error base64 decoding secret key from env var '{}': {}",
941            key, e
942          );
943        }
944      }
945    }
946  }
947}
948
949/// Reads configuration from specified paths (files/directories), merges them,
950/// applies environment variable overrides, separates provider configurations,
951/// and applies the final values to the store via the provided setter function.
952///
953/// Handles YAML and TOML file formats. Reads environment variables starting
954/// with "C5_" using "__" as a separator (e.g., C5_DATABASE__HOST becomes database.host).
955///
956/// Order of precedence: Environment Variables > Last File Read > First File Read.
957pub fn read_config_data(
958  config_file_paths: &[PathBuf],
959  data_store: &C5DataStore, // Expecting the internal data store
960  provided_data: &mut MultiMap<String, C5DataValue>,
961) -> Result<(), ConfigError> {
962  let mut file_config_merged: HashMap<String, C5DataValue> = HashMap::new(); // Holds NESTED structure from files
963  let mut files_to_process: Vec<PathBuf> = Vec::new();
964  let mut file_source_map: HashMap<String, PathBuf> = HashMap::new(); // Tracks top-level key source file
965
966  // --- 1. Expand directories ---
967  for path in config_file_paths {
968    if path.is_dir() {
969      match read_dir(path) {
970        Ok(entries) => {
971          let mut dir_files: Vec<PathBuf> = entries
972            .filter_map(|entry| entry.ok())
973            .map(|entry| entry.path())
974            .filter(|p| p.is_file())
975            .collect();
976          dir_files.sort();
977          files_to_process.extend(dir_files);
978        }
979        Err(e) => {
980          return Err(ConfigError::IoError {
981            path: path.clone(),
982            source: e,
983          });
984        }
985      }
986    } else if path.is_file() {
987      files_to_process.push(path.clone());
988    } else if path.exists() {
989      println!(
990        "[Config] Warning: Path {:?} exists but is not a file or directory.",
991        path
992      );
993    } else {
994      // Only warn if it *doesn't* exist
995      // println!("[Config] Info: Optional config path {:?} not found.", path);
996    }
997  }
998
999  // --- 2. Load, Merge Files, and Extract Provider Configs (ONCE) ---
1000  for file_path in &files_to_process {
1001    let extension = file_path.extension().and_then(OsStr::to_str);
1002    type ParserFn = fn(&str, &PathBuf) -> Result<HashMap<String, C5DataValue>, ConfigError>;
1003    let parser: Option<ParserFn> = match extension {
1004      Some("yaml") | Some("yml") => Some(|content, path| {
1005        serde_yaml::from_str::<HashMap<String, serde_yaml::Value>>(content)
1006          .map_err(|e| ConfigError::YamlParseError {
1007            path: path.clone(),
1008            source: e,
1009          })
1010          .map(map_from_serde_yaml_valuemap)
1011      }),
1012      #[cfg(feature = "toml")]
1013      Some("toml") => Some(|content, path| {
1014        toml::from_str::<HashMap<String, toml::Value>>(content)
1015          .map_err(|e| ConfigError::TomlParseError {
1016            path: path.clone(),
1017            source: e,
1018          })
1019          .map(map_from_toml_value_map)
1020      }),
1021      _ => None,
1022    };
1023
1024    if let Some(parse_fn) = parser {
1025      match fs::read_to_string(&file_path) {
1026        Ok(content) => {
1027          match parse_fn(&content, file_path) {
1028            Ok(mut config_from_file) => {
1029              // Make mutable
1030              println!("[Config] Processing config from file {:?}", file_path);
1031
1032              // Track file source for top-level keys BEFORE extraction/merging
1033              for key in config_from_file.keys() {
1034                file_source_map.insert(key.clone(), file_path.clone());
1035              }
1036
1037              // --- >>> Extract Provider Configs from this file's data <<< ---
1038              // Note: This modifies config_from_file IN PLACE, removing provider sections
1039              _take_provided_data(&mut config_from_file, provided_data);
1040
1041              // Merge remaining non-provider file data into the main nested accumulator
1042              _merge(&mut file_config_merged, &config_from_file);
1043            }
1044            Err(e) => return Err(e),
1045          }
1046        }
1047        Err(e) => {
1048          if e.kind() == std::io::ErrorKind::NotFound {
1049            println!(
1050              "[Config] Warning: File {:?} not found during read.",
1051              file_path
1052            );
1053          } else {
1054            return Err(ConfigError::IoError {
1055              path: file_path.clone(),
1056              source: e,
1057            });
1058          }
1059        }
1060      }
1061    }
1062  }
1063  // `file_config_merged` now holds the merged NESTED, non-provider structure from all files.
1064  // `provided_data` holds provider configs extracted from files.
1065
1066  // --- 3. Merge Environment Variables into the Nested Structure ---
1067  const PREFIX: &str = "C5_";
1068  const SEPARATOR: &str = "__";
1069  let mut env_source_flat_map: HashMap<String, ConfigSource> = HashMap::new(); // Tracks flat sources for env vars
1070
1071  for (env_key_name, value_str) in env::vars() {
1072    if env_key_name.starts_with(PREFIX) {
1073      let trimmed_key = env_key_name.trim_start_matches(PREFIX);
1074      let c5_key = trimmed_key.replace(SEPARATOR, ".").to_lowercase();
1075
1076      if c5_key.split('.').any(|part| part.is_empty()) {
1077        eprintln!(
1078          "[Config] Warning: Skipping env var '{}' due to invalid key format '{}'",
1079          env_key_name, c5_key
1080        );
1081        continue;
1082      }
1083
1084      println!(
1085        "[Config] Processing env var '{}' for key '{}'",
1086        env_key_name, c5_key
1087      );
1088
1089      // Store flat source info immediately
1090      env_source_flat_map.insert(
1091        c5_key.clone(),
1092        ConfigSource::EnvironmentVariable(env_key_name.clone()),
1093      );
1094
1095      // Use helper to merge this env var into the nested structure (`file_config_merged`)
1096      if let Err(e) = merge_env_var_nested(&mut file_config_merged, &c5_key, &value_str) {
1097        return Err(e); // Propagate conflict errors
1098      }
1099    }
1100  }
1101  // `file_config_merged` now holds the final combined NESTED structure (Files + Env Vars Merged, non-provider).
1102
1103  // --- 4. Flatten the Final Nested Structure ---
1104  let mut final_flat_map = HashMap::new();
1105  util::build_flat_map(&file_config_merged, &mut final_flat_map, String::new());
1106  // `final_flat_map` now contains all config keys (e.g., "database.host", "database.port")
1107
1108  // --- 5. Apply to Store with Correct Sources ---
1109  for (key, value) in final_flat_map {
1110    // Determine source: Check env source map first, then file source map
1111    let final_source = match env_source_flat_map.get(&key) {
1112      Some(env_source) => env_source.clone(), // Env var took precedence
1113      None => {
1114        // Must have come from a file
1115        let top_level_key = key.split('.').next().unwrap_or(&key);
1116        file_source_map
1117          .get(top_level_key)
1118          .map(|path| ConfigSource::File(path.clone()))
1119          .unwrap_or(ConfigSource::Unknown) // Fallback
1120      }
1121    };
1122    // Set the flattened key-value pair in the actual data store
1123    data_store._set_data_internal(&key, value, final_source);
1124  }
1125
1126  Ok(())
1127}
1128
1129// Helper function to attempt parsing env var strings into C5 types
1130fn parse_env_var_value(value_str: &str) -> C5DataValue {
1131  // Try bool
1132  if value_str.eq_ignore_ascii_case("true") {
1133    return C5DataValue::Boolean(true);
1134  }
1135  if value_str.eq_ignore_ascii_case("false") {
1136    return C5DataValue::Boolean(false);
1137  }
1138  // Try integer (signed first) - use i64 as base
1139  if let Ok(i) = value_str.parse::<i64>() {
1140    return C5DataValue::Integer(i);
1141  }
1142  // Try unsigned integer - use u64 as base
1143  if let Ok(u) = value_str.parse::<u64>() {
1144    // Only use UInteger if it *didn't* parse as i64 (e.g., > i64::MAX)
1145    // or perhaps prefer UInteger if non-negative? Let's stick to i64 if possible.
1146    // If parsing as i64 succeeded, we use that. If not, try u64.
1147    // A check could be added: if u <= i64::MAX as u64, return Integer(u as i64)?
1148    // For simplicity now, if it parses as u64 *after* failing i64, use UInteger.
1149    return C5DataValue::UInteger(u);
1150  }
1151  // Try float
1152  if let Ok(f) = value_str.parse::<f64>() {
1153    return C5DataValue::Float(f);
1154  }
1155  // Fallback to string
1156  C5DataValue::String(value_str.to_string())
1157}
1158
1159// Helper to merge a single environment variable into the nested structure
1160fn merge_env_var_nested(
1161  target_map: &mut HashMap<String, C5DataValue>,
1162  c5_key: &str,
1163  value_str: &str,
1164) -> Result<(), ConfigError> {
1165  let mut current_level_map = target_map; // Start with the root map
1166  let key_parts: Vec<&str> = c5_key.split('.').collect();
1167
1168  for (i, part) in key_parts.iter().enumerate() {
1169    if part.is_empty() {
1170      // Check for invalid empty parts like a..b
1171      return Err(ConfigError::Message(format!(
1172        "Invalid key format: Encountered empty segment in env var key '{}'",
1173        c5_key
1174      )));
1175    }
1176
1177    if i == key_parts.len() - 1 {
1178      // --- Last part: Insert the final value ---
1179      // `current_level_map` points to the correct parent map here.
1180      current_level_map.insert(part.to_string(), parse_env_var_value(value_str));
1181      return Ok(()); // Done
1182    } else {
1183      // --- Intermediate part: Ensure map exists and prepare descent ---
1184      let entry = current_level_map.entry(part.to_string());
1185
1186      match entry {
1187        std::collections::hash_map::Entry::Occupied(occ_entry) => {
1188          // Entry exists, check if it's a map.
1189          // We don't need to keep the borrow from occ_entry.
1190          if !matches!(occ_entry.get(), C5DataValue::Map(_)) {
1191            // Conflict: Entry exists but isn't a map
1192            return Err(ConfigError::Message(format!(
1193              "Env var key conflict: Cannot create nested structure for '{}' because part '{}' conflicts with an existing non-map value.",
1194              c5_key, part
1195            )));
1196          }
1197          // It is a map, allow occ_entry borrow to expire here.
1198        }
1199        std::collections::hash_map::Entry::Vacant(vac_entry) => {
1200          // Entry doesn't exist, insert a new map.
1201          vac_entry.insert(C5DataValue::Map(HashMap::new()));
1202          // The borrow from vac_entry expires here.
1203        }
1204      }
1205      // --- Borrow derived from `entry` ends here ---
1206
1207      // Now, we are guaranteed that current_level_map[*part] exists and is a Map.
1208      // Get the mutable reference *from current_level_map* to descend for the *next* iteration.
1209      // This borrow is valid as it's derived from `current_level_map` itself.
1210      if let Some(C5DataValue::Map(next_map)) = current_level_map.get_mut(*part) {
1211        // Update `current_level_map` to point to the nested map for the next loop iteration.
1212        current_level_map = next_map;
1213      } else {
1214        // This case should be impossible if the match logic above is correct.
1215        unreachable!(
1216          "Map for part '{}' should exist here but wasn't found or wasn't a Map",
1217          part
1218        );
1219      }
1220    } // end intermediate part
1221  } // end loop
1222
1223  // This point should be unreachable because the last part is handled inside the loop.
1224  unreachable!("Loop should handle all parts or return early");
1225}
1226
1227// Helper to recursively merge hashmaps, src overwrites dest
1228// Ensures nested maps are merged correctly.
1229fn _merge(dest: &mut HashMap<String, C5DataValue>, src: &HashMap<String, C5DataValue>) {
1230  for (src_key, src_value) in src.iter() {
1231    // Use iter()
1232    match dest.entry(src_key.clone()) {
1233      // Use entry API
1234      std::collections::hash_map::Entry::Occupied(mut entry) => {
1235        // Key exists in destination, get mutable ref to existing value
1236        let dest_val = entry.get_mut();
1237        // Check if both are maps
1238        if let (C5DataValue::Map(dest_map), C5DataValue::Map(src_map)) = (dest_val, src_value) {
1239          // Both are maps, recurse
1240          _merge(dest_map, src_map);
1241        } else {
1242          // Not both maps (or different types), source overwrites destination value
1243          // This handles cases like: dest=Map, src=String -> dest becomes String
1244          // And: dest=String, src=Map -> dest becomes Map
1245          *entry.into_mut() = src_value.clone(); // Use entry.into_mut() for direct replacement
1246        }
1247      }
1248      std::collections::hash_map::Entry::Vacant(entry) => {
1249        // Key doesn't exist in destination, insert clone from source
1250        entry.insert(src_value.clone());
1251      }
1252    }
1253  }
1254}
1255
1256// Helper to extract provider configurations (no changes needed inside, just signature)
1257fn _take_provided_data(
1258  raw_config_data: &mut HashMap<String, C5DataValue>,
1259  provided_data: &mut MultiMap<String, C5DataValue>,
1260) {
1261  _take_provided_data_helper(raw_config_data, provided_data, String::new());
1262}
1263
1264// Recursive helper for _take_provided_data (no changes needed)
1265fn _take_provided_data_helper(
1266  current_map: &mut HashMap<String, C5DataValue>,
1267  provided_data: &mut MultiMap<String, C5DataValue>,
1268  current_keypath: String,
1269) {
1270  let keys: Vec<String> = current_map.keys().cloned().collect();
1271
1272  for key in keys {
1273    let new_keypath = if current_keypath.is_empty() {
1274      key.clone()
1275    } else {
1276      format!("{}.{}", current_keypath, key)
1277    };
1278
1279    let is_provider_config = if let Some(C5DataValue::Map(data_map)) = current_map.get(&key) {
1280      data_map.contains_key(CONFIG_KEY_PROVIDER)
1281    } else {
1282      false
1283    };
1284
1285    if is_provider_config {
1286      if let Some(C5DataValue::Map(mut data_map)) = current_map.remove(&key) {
1287        data_map.insert(
1288          CONFIG_KEY_KEYPATH.to_string(),
1289          C5DataValue::String(new_keypath.clone()),
1290        );
1291        data_map.insert(
1292          CONFIG_KEY_KEYNAME.to_string(),
1293          C5DataValue::String(key.clone()),
1294        );
1295        if let Some(C5DataValue::String(provider_name)) = data_map.get(CONFIG_KEY_PROVIDER) {
1296          provided_data.insert(provider_name.clone(), C5DataValue::Map(data_map));
1297        } else {
1298          eprintln!(
1299            "[Config] Error: Provider config at '{}' has non-string value for '.provider'",
1300            new_keypath
1301          );
1302        }
1303      }
1304    } else if let Some(C5DataValue::Map(sub_map)) = current_map.get_mut(&key) {
1305      _take_provided_data_helper(sub_map, provided_data, new_keypath);
1306      if sub_map.is_empty() {
1307        current_map.remove(&key);
1308      }
1309    }
1310  }
1311}
1312
1313pub fn default_config_paths(
1314  config_dir: &str,
1315  release_env: &str,
1316  env: &str,
1317  region: &str,
1318) -> Vec<PathBuf> {
1319  let mut paths = vec![];
1320
1321  paths.push(PathBuf::from(format!("{}/common.yaml", config_dir)));
1322  paths.push(PathBuf::from(
1323    format!("{}/{}.yaml", config_dir, release_env).as_str(),
1324  ));
1325  paths.push(PathBuf::from(
1326    format!("{}/{}.yaml", config_dir, env).as_str(),
1327  ));
1328  paths.push(PathBuf::from(
1329    format!("{}/{}.yaml", config_dir, region).as_str(),
1330  ));
1331  paths.push(PathBuf::from(
1332    format!("{}/{}-{}.yaml", config_dir, env, region).as_str(),
1333  ));
1334
1335  return paths;
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340  use std::env;
1341  use std::io::Write;
1342  use std::path::PathBuf;
1343
1344  use ecies_25519::EciesX25519;
1345  use maplit::hashmap;
1346  use serde::Deserialize;
1347  use serial_test::serial;
1348  use tempfile::NamedTempFile;
1349
1350  use crate::C5Store;
1351  use crate::error::ConfigError;
1352  use crate::secrets::{Base64SecretDecryptor, EciesX25519SecretDecryptor, SecretKeyStore};
1353  use crate::value::C5DataValue;
1354  use crate::{C5StoreMgr, C5StoreOptions, SecretOptions, create_c5store, default_config_paths};
1355
1356  // Helper struct for get_into_struct tests
1357  #[derive(Deserialize, Debug, PartialEq)]
1358  struct DbConfig {
1359    host: String,
1360    port: u16,
1361    user: Option<String>, // Make fields optional if they might not exist
1362    #[serde(default)] // Example: provide default for missing fields
1363    timeout: u32,
1364  }
1365
1366  #[derive(Deserialize, Debug, PartialEq)]
1367  struct FeatureFlags {
1368    new_dashboard: bool,
1369    api_v2: bool,
1370    #[serde(default = "default_retries")]
1371    retries: u8,
1372  }
1373
1374  fn default_retries() -> u8 {
1375    3
1376  }
1377
1378  fn _create_c5store_test() -> (impl C5Store, C5StoreMgr) {
1379    let config_file_paths =
1380      default_config_paths("configs/test/config", "development", "local", "private");
1381    create_c5store(config_file_paths, None).expect("Test store creation failed")
1382  }
1383
1384  #[test]
1385  #[serial]
1386  fn test_config_contains_bill_bar_existence() {
1387    let (c5store, _c5store_mgr) = _create_c5store_test();
1388
1389    assert_eq!(c5store.exists("bill.barr"), true);
1390    assert_eq!(c5store.exists("bill"), false);
1391    assert_eq!(c5store.path_exists("bill.barr"), true);
1392    assert_eq!(c5store.path_exists("bill.barr."), false);
1393    assert_eq!(c5store.path_exists("bill"), true);
1394  }
1395
1396  #[test]
1397  #[serial]
1398  fn test_config_contains_bill_bar() {
1399    let (c5store, _c5store_mgr) = _create_c5store_test();
1400
1401    assert_eq!(
1402      c5store.get("bill.barr").unwrap(),
1403      C5DataValue::String(String::from("AG"))
1404    );
1405  }
1406
1407  #[test]
1408  #[serial]
1409  fn test_config_contains_example_test_and() {
1410    let (c5store, _c5store_mgr) = _create_c5store_test();
1411
1412    assert_eq!(
1413      c5store.get("example.test.and").unwrap(),
1414      C5DataValue::UInteger(1)
1415    );
1416    assert_eq!(c5store.get_into::<u64>("example.test.and").unwrap(), 1u64);
1417  }
1418
1419  #[test]
1420  #[serial]
1421  fn test_get_into_struct_nested() {
1422    // Uses the standard config files which have a nested structure
1423    let (c5store, _c5store_mgr) = _create_c5store_test();
1424
1425    // Assuming DbConfig struct is defined as above
1426    let db_conf_res = c5store.get_into_struct::<DbConfig>("database");
1427
1428    assert!(
1429      db_conf_res.is_ok(),
1430      "Failed to deserialize DbConfig: {:?}",
1431      db_conf_res.err()
1432    );
1433    let db_conf = db_conf_res.unwrap();
1434
1435    assert_eq!(db_conf.host, "db.local.com"); // from local.yaml
1436    assert_eq!(db_conf.port, 5433); // from local.yaml
1437    assert_eq!(db_conf.user, Some("local_user".to_string())); // from local.yaml
1438    assert_eq!(db_conf.timeout, 0); // uses serde default
1439  }
1440
1441  #[test]
1442  #[serial]
1443  fn test_get_into_struct_flattened() {
1444    unsafe {
1445      // Create a store specifically with flattened keys
1446      env::set_var("C5_FLATDB__HOST", "flat-host.com");
1447      env::set_var("C5_FLATDB__PORT", "9999");
1448      env::set_var("C5_FLATDB__USER", "flat_user");
1449      env::set_var("C5_FLATDB__TIMEOUT", "5000"); // Env vars are strings
1450    }
1451
1452    // Use an empty config file path list, relying only on env vars
1453    let (c5store, _c5store_mgr) =
1454      create_c5store(vec![], None).expect("Store creation from env failed");
1455
1456    let db_conf_res = c5store.get_into_struct::<DbConfig>("flatdb"); // Use lowercase prefix
1457
1458    assert!(
1459      db_conf_res.is_ok(),
1460      "Failed to deserialize flattened DbConfig: {:?}",
1461      db_conf_res.err()
1462    );
1463    let db_conf = db_conf_res.unwrap();
1464
1465    assert_eq!(db_conf.host, "flat-host.com");
1466    // Note: Serde handles string-to-number conversion for basic types if possible
1467    assert_eq!(db_conf.port, 9999);
1468    assert_eq!(db_conf.user, Some("flat_user".to_string()));
1469    assert_eq!(db_conf.timeout, 5000);
1470
1471    unsafe {
1472      // Clean up env vars
1473      env::remove_var("C5_FLATDB__HOST");
1474      env::remove_var("C5_FLATDB__PORT");
1475      env::remove_var("C5_FLATDB__USER");
1476      env::remove_var("C5_FLATDB__TIMEOUT");
1477    }
1478  }
1479
1480  #[test]
1481  #[serial]
1482  fn test_get_into_struct_partial_flattened() {
1483    unsafe {
1484      // Mix flattened env vars with file values
1485      env::set_var("C5_DATABASE__HOST", "env-host.com"); // Override host from file
1486    }
1487
1488    let (c5store, _c5store_mgr) = _create_c5store_test();
1489
1490    let db_conf_res = c5store.get_into_struct::<DbConfig>("database");
1491
1492    assert!(
1493      db_conf_res.is_ok(),
1494      "Failed to deserialize partially flattened DbConfig: {:?}",
1495      db_conf_res.err()
1496    );
1497    let db_conf = db_conf_res.unwrap();
1498
1499    assert_eq!(db_conf.host, "env-host.com"); // Env var overrides file
1500    assert_eq!(db_conf.port, 5433); // From local.yaml
1501    assert_eq!(db_conf.user, Some("local_user".to_string())); // From local.yaml
1502    assert_eq!(db_conf.timeout, 0); // default
1503
1504    unsafe {
1505      env::remove_var("C5_DATABASE__HOST");
1506    }
1507  }
1508
1509  #[test]
1510  #[serial]
1511  fn test_get_into_struct_array_inference() {
1512    unsafe {
1513      // Test reconstruction of arrays from numeric keys
1514      env::set_var("C5_WEB__SERVERS__0__IP", "1.1.1.1");
1515      env::set_var("C5_WEB__SERVERS__0__PORT", "80");
1516      env::set_var("C5_WEB__SERVERS__1__IP", "2.2.2.2");
1517      env::set_var("C5_WEB__SERVERS__1__PORT", "8080");
1518      env::set_var("C5_WEB__LOADBALANCER", "lb.site.com");
1519    }
1520
1521    #[derive(Deserialize, Debug, PartialEq)]
1522    struct Server {
1523      ip: String,
1524      port: u16,
1525    }
1526    #[derive(Deserialize, Debug, PartialEq)]
1527    struct WebConfig {
1528      servers: Vec<Server>,
1529      loadbalancer: String,
1530    }
1531
1532    let (c5store, _c5store_mgr) = create_c5store(vec![], None).expect("Store creation failed");
1533
1534    let web_conf_res = c5store.get_into_struct::<WebConfig>("web");
1535
1536    assert!(
1537      web_conf_res.is_ok(),
1538      "Failed to deserialize WebConfig: {:?}",
1539      web_conf_res.err()
1540    );
1541    let web_conf = web_conf_res.unwrap();
1542
1543    assert_eq!(web_conf.loadbalancer, "lb.site.com");
1544    assert_eq!(web_conf.servers.len(), 2);
1545    assert_eq!(
1546      web_conf.servers[0],
1547      Server {
1548        ip: "1.1.1.1".to_string(),
1549        port: 80
1550      }
1551    );
1552    assert_eq!(
1553      web_conf.servers[1],
1554      Server {
1555        ip: "2.2.2.2".to_string(),
1556        port: 8080
1557      }
1558    );
1559
1560    unsafe {
1561      env::remove_var("C5_WEB__SERVERS__0__IP");
1562      env::remove_var("C5_WEB__SERVERS__0__PORT");
1563      env::remove_var("C5_WEB__SERVERS__1__IP");
1564      env::remove_var("C5_WEB__SERVERS__1__PORT");
1565      env::remove_var("C5_WEB__LOADBALANCER");
1566    }
1567  }
1568
1569  #[test]
1570  #[serial]
1571  fn test_get_into_struct_key_not_found() {
1572    let (c5store, _c5store_mgr) = _create_c5store_test();
1573    let res = c5store.get_into_struct::<DbConfig>("non_existent_prefix");
1574    assert!(matches!(res, Err(ConfigError::KeyNotFound(_))));
1575  }
1576
1577  #[test]
1578  #[serial]
1579  fn test_get_into_struct_deserialization_error() {
1580    unsafe {
1581      // Set env vars that won't deserialize correctly into FeatureFlags (e.g., string for bool)
1582      env::set_var("C5_FEATURES__NEW_DASHBOARD", "maybe");
1583      env::set_var("C5_FEATURES__API_V2", "false");
1584    }
1585
1586    let (c5store, _c5store_mgr) = create_c5store(vec![], None).expect("Store creation failed");
1587
1588    let res = c5store.get_into_struct::<FeatureFlags>("features");
1589    assert!(
1590      match &res {
1591        Err(ConfigError::ConversionError { key, message }) => {
1592          // The key from C5SerdeValueDeserializer is often empty or the direct field name.
1593          // The message should be specific.
1594          (key.is_empty() || key == "features" || key == "features.new_dashboard")
1595            && message.contains("'maybe' could not be converted to boolean")
1596        }
1597        _ => false,
1598      },
1599      "Expected ConversionError for 'maybe' string with specific message, got {:?}",
1600      res
1601    );
1602
1603    unsafe {
1604      env::remove_var("C5_FEATURES__NEW_DASHBOARD");
1605      env::remove_var("C5_FEATURES__API_V2");
1606    }
1607  }
1608
1609  #[test]
1610  #[serial]
1611  #[cfg(feature = "secrets")]
1612  fn test_config_secrets_decrypt() {
1613    use crate::secrets::{Base64SecretDecryptor, EciesX25519SecretDecryptor};
1614    use ecies_25519::EciesX25519;
1615
1616    let mut config_file_paths = vec![];
1617    config_file_paths.push(PathBuf::from("configs/secret_test/secret_config.yaml"));
1618
1619    let mut config_opt = C5StoreOptions::default();
1620    config_opt.secret_opts = SecretOptions {
1621      secret_keys_path: Some(PathBuf::from("configs/secret_test/secret_keys")),
1622      secret_key_store_configure_fn: Some(Box::new(|secret_key_store: &mut SecretKeyStore| {
1623        secret_key_store.set_decryptor("base64", Box::from(Base64SecretDecryptor {}));
1624        secret_key_store.set_decryptor(
1625          "ecies_x25519",
1626          Box::from(EciesX25519SecretDecryptor::new(EciesX25519::new())),
1627        );
1628      })),
1629      load_secret_keys_from_env: false,
1630      secret_key_env_prefix: None,
1631      ..Default::default()
1632    };
1633
1634    let (c5store, _c5store_mgr) = create_c5store(config_file_paths, Some(config_opt))
1635      .expect("Secrets test store creation failed");
1636
1637    assert_eq!(
1638      c5store.get("a_secret").unwrap(),
1639      C5DataValue::Bytes("abcd".as_bytes().to_vec())
1640    );
1641    assert_eq!(
1642      c5store.get("hello_secret").unwrap(),
1643      C5DataValue::Bytes("Hello World".as_bytes().to_vec())
1644    );
1645  }
1646
1647  #[test]
1648  #[serial]
1649  #[cfg(feature = "secrets")]
1650  fn test_bad_config_secrets_decrypt() {
1651    use crate::secrets::{Base64SecretDecryptor, EciesX25519SecretDecryptor};
1652    use ecies_25519::EciesX25519;
1653
1654    let mut config_file_paths = vec![];
1655    config_file_paths.push(PathBuf::from("configs/secret_test/secret_config_bad.yaml"));
1656
1657    let mut config_opt = C5StoreOptions::default();
1658    config_opt.secret_opts = SecretOptions {
1659      secret_keys_path: Some(PathBuf::from("configs/secret_test/secret_keys")),
1660      secret_key_store_configure_fn: Some(Box::new(|secret_key_store: &mut SecretKeyStore| {
1661        secret_key_store.set_decryptor("base64", Box::from(Base64SecretDecryptor {}));
1662        secret_key_store.set_decryptor(
1663          "ecies_x25519",
1664          Box::from(EciesX25519SecretDecryptor::new(EciesX25519::new())),
1665        );
1666      })),
1667      load_secret_keys_from_env: false,
1668      secret_key_env_prefix: None,
1669      ..Default::default()
1670    };
1671
1672    let (c5store, _c5store_mgr) = create_c5store(config_file_paths, Some(config_opt))
1673      .expect("Bad secrets test store creation failed");
1674
1675    // Behavior might change with better error handling, maybe secrets just aren't loaded
1676    // Let's assume `get` still returns None if decryption failed during set_data
1677    assert_eq!(c5store.get("bad_secret"), None);
1678  }
1679
1680  #[test]
1681  #[serial]
1682  #[cfg(feature = "secrets")]
1683  fn test_decryption_pipeline_populates_store_correctly() {
1684    // --- STAGE 1: Test that the full file->decrypt->store pipeline works ---
1685    println!("\n--- TEST: Verifying decryption pipeline populates the store ---");
1686
1687    // 1. Configure the store with the real decryptor
1688    let mut options = C5StoreOptions::default();
1689    options.secret_opts.secret_key_store_configure_fn = Some(Box::new(|store| {
1690      store.set_decryptor("base64", Box::new(Base64SecretDecryptor {}));
1691      store.set_key("dummy_key", vec![1, 2, 3]);
1692    }));
1693
1694    // 2. Load the store from the correctly formatted test file
1695    let config_path = PathBuf::from("resources/test_e2e_secrets.yaml");
1696    let (c5store, _mgr) =
1697      create_c5store(vec![config_path], Some(options)).expect("Store creation failed");
1698
1699    // 3. Assert the final state of the store after decryption
1700    println!("\n--- Asserting final store state ---");
1701
1702    // Assert plaintext values were loaded
1703    assert_eq!(
1704      c5store.get("database.host").unwrap(),
1705      C5DataValue::String("db.prod.com".to_string())
1706    );
1707
1708    // Assert that the DECRYPTED values are in the store with the correct type (Bytes)
1709    assert_eq!(
1710      c5store.get("secrets.api_key").unwrap(),
1711      C5DataValue::Bytes("secret-key-123".as_bytes().to_vec())
1712    );
1713    assert_eq!(
1714      c5store.get("secrets.app_id").unwrap(),
1715      C5DataValue::Bytes(55_u32.to_be_bytes().to_vec())
1716    );
1717    assert_eq!(
1718      c5store.get("secrets.timeout").unwrap(),
1719      C5DataValue::Bytes(2.0_f64.to_be_bytes().to_vec())
1720    );
1721    assert_eq!(
1722      c5store.get("secrets.raw_key").unwrap(),
1723      C5DataValue::Bytes("byte-data".as_bytes().to_vec())
1724    );
1725
1726    // Assert that the ORIGINAL ENCRYPTED VALUES ARE GONE
1727    assert!(!c5store.exists("secrets.api_key.c5encval"));
1728
1729    println!("✅ Stage 1 Passed: Store is populated correctly from decrypted secrets.");
1730  }
1731
1732  #[test]
1733  #[serial]
1734  #[cfg(feature = "secrets")]
1735  fn test_end_to_end_deserialization_with_secrets() {
1736    use crate::secrets::Base64SecretDecryptor;
1737
1738    // --- 1. Define the Target Structs ---
1739    #[derive(Deserialize, Debug, PartialEq)]
1740    struct FullConfig {
1741      database: DatabaseConfig,
1742      secrets: SecretsConfig,
1743    }
1744    #[derive(Deserialize, Debug, PartialEq)]
1745    struct DatabaseConfig {
1746      host: String,
1747      port: u16,
1748    }
1749    #[derive(Deserialize, Debug, PartialEq)]
1750    struct SecretsConfig {
1751      api_key: String,
1752      app_id: u32,
1753      timeout: f64,
1754      raw_key: Vec<u8>,
1755    }
1756
1757    // --- 2. Configure C5StoreOptions with the REAL Base64SecretDecryptor ---
1758    let mut options = C5StoreOptions::default();
1759    options.secret_opts.secret_key_store_configure_fn = Some(Box::new(|store| {
1760      store.set_decryptor("base64", Box::new(Base64SecretDecryptor {}));
1761      store.set_key("dummy_key", vec![1, 2, 3]);
1762    }));
1763
1764    // --- 3. Load the Store from our correctly formatted test file ---
1765    let config_path = PathBuf::from("resources/test_e2e_secrets.yaml");
1766    let (c5store, _mgr) =
1767      create_c5store(vec![config_path], Some(options)).expect("Store creation failed");
1768
1769    // --- 4. Perform Deserialization and Assertions ---
1770    let config = c5store
1771      .get_into_struct::<FullConfig>("")
1772      .expect("Deserialization failed");
1773
1774    // Assert plaintext values are correct
1775    assert_eq!(config.database.host, "db.prod.com");
1776    assert_eq!(config.database.port, 5432);
1777
1778    // Assert that all secrets were decrypted and deserialized correctly
1779    assert_eq!(config.secrets.api_key, "secret-key-123");
1780    assert_eq!(config.secrets.app_id, 55);
1781    assert_eq!(config.secrets.timeout, 2.0);
1782    assert_eq!(config.secrets.raw_key, "byte-data".as_bytes());
1783  }
1784
1785  #[test]
1786  #[serial]
1787  #[cfg(feature = "secrets")]
1788  fn test_get_into_string_from_decrypted_bytes() {
1789    // --- 1. Prepare Test Configuration ---
1790    // The expected string is "Hello, Secret World!"
1791    // Its base64 representation is "SGVsbG8sIFNlY3JldCBXb3JsZCE="
1792    //
1793    // For the invalid UTF-8 test, we use the byte sequence [0xC3, 0x28],
1794    // which is an invalid 2-byte UTF-8 sequence. Its base64 is "wyg="
1795    let config_content = r#"
1796my_secret_string:
1797  ".c5encval":
1798    - "base64"
1799    - "test_key"
1800    - "SGVsbG8sIFNlY3JldCBXb3JsZCE="
1801
1802my_bad_utf8_secret:
1803  ".c5encval":
1804    - "base64"
1805    - "test_key"
1806    - "wyg="
1807"#;
1808
1809    
1810    let mut temp_config_file = tempfile::Builder::new()
1811        .prefix("c5store-test-")
1812        .suffix(".yaml")
1813        .tempfile()
1814        .unwrap();
1815    write!(temp_config_file, "{}", config_content).unwrap();
1816
1817    // Read the file's content directly from the disk to verify it.
1818    let file_path = temp_config_file.path();
1819    let content_on_disk = std::fs::read_to_string(file_path).unwrap();
1820    assert_eq!(content_on_disk, config_content, "The content on disk did not match the expected content!");
1821
1822    let config_path = temp_config_file.path().to_path_buf();
1823
1824    // --- 2. Configure C5Store for Secrets ---
1825    let mut options = C5StoreOptions::default();
1826    options.secret_opts.secret_key_store_configure_fn = Some(Box::new(|store| {
1827      // Use a simple decryptor that just decodes base64
1828      store.set_decryptor("base64", Box::new(Base64SecretDecryptor {}));
1829      // Key content doesn't matter for this decryptor, but it must exist
1830      store.set_key("test_key", vec![]);
1831    }));
1832
1833    // --- 3. Create the Store ---
1834    let (c5store, _mgr) =
1835      create_c5store(vec![config_path], Some(options)).expect("Store creation for test failed");
1836
1837    // --- 4. Test the Success Case (Valid UTF-8) ---
1838    let result = c5store.get_into::<String>("my_secret_string");
1839
1840    assert!(
1841      result.is_ok(),
1842      "get_into::<String> failed for valid UTF-8 bytes: {:?}",
1843      result.err()
1844    );
1845    let secret_string = result.unwrap();
1846    assert_eq!(secret_string, "Hello, Secret World!");
1847
1848    // --- 5. Test the Failure Case (Invalid UTF-8) ---
1849    let bad_result = c5store.get_into::<String>("my_bad_utf8_secret");
1850
1851    assert!(
1852      bad_result.is_err(),
1853      "get_into::<String> should have failed for invalid UTF-8 bytes"
1854    );
1855    assert!(
1856      matches!(bad_result, Err(ConfigError::ConversionError { .. })),
1857      "Expected a ConversionError for invalid UTF-8, but got {:?}",
1858      bad_result
1859    );
1860  }
1861}