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
85pub type ChangeListener = dyn Fn(&str, &str, &C5DataValue) -> () + Send + Sync;
87pub 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>, 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>, }
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
143struct 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>>>, _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>, new_value: C5DataValue, ) {
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(); job_handle_borrow_inner.take(); drop(job_handle_borrow_inner); drop(debounce_job_lock_inner); if !changes_to_process.is_empty() {
213 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 notifications_to_send.insert(changed_key.clone(), key_ancestor_path.clone());
226 }
227 }
228 }
229
230 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, change_detail.old_value.as_ref(), );
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 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 fn key_paths_with_prefix(&self, key_path: Option<&str>) -> Vec<String>;
287
288 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 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 let deserializer = C5SerdeValueDeserializer::from_c5(&direct_c5_value);
346 match Val::deserialize(deserializer) {
347 Ok(result) => return Ok(result), Err(direct_err) => {
349 if !matches!(direct_c5_value, C5DataValue::Map(_)) && !key_path.is_empty() {
355 }
360 }
363 }
364 }
365 C5DataValue::Null => {
366 }
369 }
370 }
371
372 match self._data_store.fetch_children_as_c5_value(key_path) {
375 Ok(C5DataValue::Null) => {
376 Err(ConfigError::KeyNotFound(key_path.to_string()))
381 }
382 Ok(reconstructed_c5_value) => {
383 let deserializer = C5SerdeValueDeserializer::from_c5(&reconstructed_c5_value);
385 Val::deserialize(deserializer).map_err(|e| {
386 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 ConfigError::DeserializationError {
402 key: key_path.to_string(),
403 source,
404 }
405 }
406 other_err => other_err, }
408 })
409 }
410 Err(e) => Err(e), }
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 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 }
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); match dotenvy::from_path(dotenv_path) {
663 Ok(_) => {}
664 Err(e) if e.not_found() => {} Err(e) => {
666 return Err(ConfigError::DotEnvLoadError {
667 path: dotenv_path.clone(),
668 source: e,
669 });
670 }
671 }
672 } else {
673 }
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 let old_value = data_store.get_data(key); let needs_update = match &old_value {
765 Some(ov) => ov != &value, None => true, };
768
769 if needs_update {
770 let source = ConfigSource::SetProgrammatically; let _prev_val = data_store._set_data_internal(key, value.clone(), source); change_notifier.notify_changed(key, old_value, value); }
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
798fn 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(); println!("[EnvVar] Setting '{}' from env var '{}'", c5_key, key); 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(()); }
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; }
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 let key_name = match file_name.rfind('.') {
896 Some(dot_index) => &file_name[..dot_index],
897 None => file_name, };
899
900 if file_ext == "pem" {
901 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; }
911 }
912 }
913
914 println!(
915 "[Secrets] Loading key '{}' from file {:?}",
916 key_name, entry_path
917 ); 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 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 ); 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
949pub fn read_config_data(
958 config_file_paths: &[PathBuf],
959 data_store: &C5DataStore, provided_data: &mut MultiMap<String, C5DataValue>,
961) -> Result<(), ConfigError> {
962 let mut file_config_merged: HashMap<String, C5DataValue> = HashMap::new(); let mut files_to_process: Vec<PathBuf> = Vec::new();
964 let mut file_source_map: HashMap<String, PathBuf> = HashMap::new(); 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 }
997 }
998
999 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 println!("[Config] Processing config from file {:?}", file_path);
1031
1032 for key in config_from_file.keys() {
1034 file_source_map.insert(key.clone(), file_path.clone());
1035 }
1036
1037 _take_provided_data(&mut config_from_file, provided_data);
1040
1041 _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 const PREFIX: &str = "C5_";
1068 const SEPARATOR: &str = "__";
1069 let mut env_source_flat_map: HashMap<String, ConfigSource> = HashMap::new(); 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 env_source_flat_map.insert(
1091 c5_key.clone(),
1092 ConfigSource::EnvironmentVariable(env_key_name.clone()),
1093 );
1094
1095 if let Err(e) = merge_env_var_nested(&mut file_config_merged, &c5_key, &value_str) {
1097 return Err(e); }
1099 }
1100 }
1101 let mut final_flat_map = HashMap::new();
1105 util::build_flat_map(&file_config_merged, &mut final_flat_map, String::new());
1106 for (key, value) in final_flat_map {
1110 let final_source = match env_source_flat_map.get(&key) {
1112 Some(env_source) => env_source.clone(), None => {
1114 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) }
1121 };
1122 data_store._set_data_internal(&key, value, final_source);
1124 }
1125
1126 Ok(())
1127}
1128
1129fn parse_env_var_value(value_str: &str) -> C5DataValue {
1131 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 if let Ok(i) = value_str.parse::<i64>() {
1140 return C5DataValue::Integer(i);
1141 }
1142 if let Ok(u) = value_str.parse::<u64>() {
1144 return C5DataValue::UInteger(u);
1150 }
1151 if let Ok(f) = value_str.parse::<f64>() {
1153 return C5DataValue::Float(f);
1154 }
1155 C5DataValue::String(value_str.to_string())
1157}
1158
1159fn 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; 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 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 current_level_map.insert(part.to_string(), parse_env_var_value(value_str));
1181 return Ok(()); } else {
1183 let entry = current_level_map.entry(part.to_string());
1185
1186 match entry {
1187 std::collections::hash_map::Entry::Occupied(occ_entry) => {
1188 if !matches!(occ_entry.get(), C5DataValue::Map(_)) {
1191 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 }
1199 std::collections::hash_map::Entry::Vacant(vac_entry) => {
1200 vac_entry.insert(C5DataValue::Map(HashMap::new()));
1202 }
1204 }
1205 if let Some(C5DataValue::Map(next_map)) = current_level_map.get_mut(*part) {
1211 current_level_map = next_map;
1213 } else {
1214 unreachable!(
1216 "Map for part '{}' should exist here but wasn't found or wasn't a Map",
1217 part
1218 );
1219 }
1220 } } unreachable!("Loop should handle all parts or return early");
1225}
1226
1227fn _merge(dest: &mut HashMap<String, C5DataValue>, src: &HashMap<String, C5DataValue>) {
1230 for (src_key, src_value) in src.iter() {
1231 match dest.entry(src_key.clone()) {
1233 std::collections::hash_map::Entry::Occupied(mut entry) => {
1235 let dest_val = entry.get_mut();
1237 if let (C5DataValue::Map(dest_map), C5DataValue::Map(src_map)) = (dest_val, src_value) {
1239 _merge(dest_map, src_map);
1241 } else {
1242 *entry.into_mut() = src_value.clone(); }
1247 }
1248 std::collections::hash_map::Entry::Vacant(entry) => {
1249 entry.insert(src_value.clone());
1251 }
1252 }
1253 }
1254}
1255
1256fn _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
1264fn _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 #[derive(Deserialize, Debug, PartialEq)]
1358 struct DbConfig {
1359 host: String,
1360 port: u16,
1361 user: Option<String>, #[serde(default)] 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 let (c5store, _c5store_mgr) = _create_c5store_test();
1424
1425 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"); assert_eq!(db_conf.port, 5433); assert_eq!(db_conf.user, Some("local_user".to_string())); assert_eq!(db_conf.timeout, 0); }
1440
1441 #[test]
1442 #[serial]
1443 fn test_get_into_struct_flattened() {
1444 unsafe {
1445 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"); }
1451
1452 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"); 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 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 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 env::set_var("C5_DATABASE__HOST", "env-host.com"); }
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"); assert_eq!(db_conf.port, 5433); assert_eq!(db_conf.user, Some("local_user".to_string())); assert_eq!(db_conf.timeout, 0); 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 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 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 (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 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 println!("\n--- TEST: Verifying decryption pipeline populates the store ---");
1686
1687 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 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 println!("\n--- Asserting final store state ---");
1701
1702 assert_eq!(
1704 c5store.get("database.host").unwrap(),
1705 C5DataValue::String("db.prod.com".to_string())
1706 );
1707
1708 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!(!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 #[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 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 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 let config = c5store
1771 .get_into_struct::<FullConfig>("")
1772 .expect("Deserialization failed");
1773
1774 assert_eq!(config.database.host, "db.prod.com");
1776 assert_eq!(config.database.port, 5432);
1777
1778 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 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 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 let mut options = C5StoreOptions::default();
1826 options.secret_opts.secret_key_store_configure_fn = Some(Box::new(|store| {
1827 store.set_decryptor("base64", Box::new(Base64SecretDecryptor {}));
1829 store.set_key("test_key", vec![]);
1831 }));
1832
1833 let (c5store, _mgr) =
1835 create_c5store(vec![config_path], Some(options)).expect("Store creation for test failed");
1836
1837 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 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}