1use std::collections::BTreeMap;
35use std::sync::Mutex;
36
37use serde_json::Value as JsonValue;
38
39static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
41
42type ChangeCallback = Box<dyn Fn(&JsonValue) + Send>;
44
45static LISTENERS: Mutex<Option<BTreeMap<String, Vec<ChangeCallback>>>> = Mutex::new(None);
47
48#[derive(Debug, Clone)]
50pub struct ConfigSection {
51 pub key: String,
53 pub type_name: String,
55 pub defaults: JsonValue,
57 pub effective: JsonValue,
59}
60
61#[derive(Debug, Clone, Default)]
63struct Registry {
64 sections: BTreeMap<String, ConfigSection>,
65}
66
67pub fn register<T>(key: &str, effective: &T)
75where
76 T: serde::Serialize + Default + 'static,
77{
78 let section = ConfigSection {
79 key: key.to_string(),
80 type_name: std::any::type_name::<T>().to_string(),
81 defaults: serde_json::to_value(T::default()).unwrap_or(JsonValue::Null),
82 effective: serde_json::to_value(effective).unwrap_or(JsonValue::Null),
83 };
84
85 if let Ok(mut guard) = REGISTRY.lock() {
86 let registry = guard.get_or_insert_with(Registry::default);
87 registry.sections.insert(key.to_string(), section);
88 }
89}
90
91#[must_use]
93pub fn sections() -> Vec<ConfigSection> {
94 REGISTRY
95 .lock()
96 .ok()
97 .and_then(|guard| {
98 guard
99 .as_ref()
100 .map(|r| r.sections.values().cloned().collect())
101 })
102 .unwrap_or_default()
103}
104
105#[must_use]
113pub fn dump_effective() -> JsonValue {
114 let mut map: serde_json::Map<String, JsonValue> = sections()
115 .into_iter()
116 .map(|s| (s.key, s.effective))
117 .collect();
118 for value in map.values_mut() {
119 if let JsonValue::Object(obj) = value {
120 redact_sensitive_fields(obj);
121 }
122 }
123 JsonValue::Object(map)
124}
125
126#[must_use]
130pub fn dump_effective_unredacted() -> JsonValue {
131 let map: serde_json::Map<String, JsonValue> = sections()
132 .into_iter()
133 .map(|s| (s.key, s.effective))
134 .collect();
135 JsonValue::Object(map)
136}
137
138#[must_use]
140pub fn dump_defaults() -> JsonValue {
141 let mut map: serde_json::Map<String, JsonValue> = sections()
142 .into_iter()
143 .map(|s| (s.key, s.defaults))
144 .collect();
145 for value in map.values_mut() {
146 if let JsonValue::Object(obj) = value {
147 redact_sensitive_fields(obj);
148 }
149 }
150 JsonValue::Object(map)
151}
152
153const SENSITIVE_PATTERNS: &[&str] = &[
166 "password",
167 "secret",
168 "token",
169 "key",
170 "credential",
171 "auth",
172 "private",
173 "cert",
174 "encryption",
175 "connection_string",
176 "dsn",
177];
178
179const REDACTED: &str = "***REDACTED***";
180
181fn redact_sensitive_fields(obj: &mut serde_json::Map<String, JsonValue>) {
183 for (key, value) in obj.iter_mut() {
184 let lower = key.to_lowercase();
185 if SENSITIVE_PATTERNS.iter().any(|p| lower.contains(p)) {
186 *value = JsonValue::String(REDACTED.into());
187 continue;
188 }
189 match value {
190 JsonValue::Object(nested) => redact_sensitive_fields(nested),
191 JsonValue::Array(arr) => {
192 for item in arr.iter_mut() {
193 if let JsonValue::Object(nested) = item {
194 redact_sensitive_fields(nested);
195 }
196 }
197 }
198 _ => {}
199 }
200 }
201}
202
203#[must_use]
205pub fn is_registered(key: &str) -> bool {
206 REGISTRY
207 .lock()
208 .ok()
209 .and_then(|guard| guard.as_ref().map(|r| r.sections.contains_key(key)))
210 .unwrap_or(false)
211}
212
213#[must_use]
215pub fn get_section(key: &str) -> Option<ConfigSection> {
216 REGISTRY
217 .lock()
218 .ok()
219 .and_then(|guard| guard.as_ref().and_then(|r| r.sections.get(key).cloned()))
220}
221
222pub fn on_change(key: &str, callback: impl Fn(&JsonValue) + Send + 'static) {
230 if let Ok(mut guard) = LISTENERS.lock() {
231 let listeners = guard.get_or_insert_with(BTreeMap::new);
232 listeners
233 .entry(key.to_string())
234 .or_default()
235 .push(Box::new(callback));
236 }
237}
238
239pub fn update<T>(key: &str, effective: &T)
245where
246 T: serde::Serialize + Default + 'static,
247{
248 let effective_json = serde_json::to_value(effective).unwrap_or(JsonValue::Null);
249
250 let section = ConfigSection {
252 key: key.to_string(),
253 type_name: std::any::type_name::<T>().to_string(),
254 defaults: serde_json::to_value(T::default()).unwrap_or(JsonValue::Null),
255 effective: effective_json.clone(),
256 };
257
258 if let Ok(mut guard) = REGISTRY.lock() {
259 let registry = guard.get_or_insert_with(Registry::default);
260 registry.sections.insert(key.to_string(), section);
261 }
262
263 if let Ok(guard) = LISTENERS.lock()
265 && let Some(listeners) = &*guard
266 && let Some(callbacks) = listeners.get(key)
267 {
268 for cb in callbacks {
269 cb(&effective_json);
270 }
271 }
272}
273
274#[cfg(test)]
276pub(crate) fn clear() {
277 if let Ok(mut guard) = REGISTRY.lock() {
278 *guard = None;
279 }
280 if let Ok(mut guard) = LISTENERS.lock() {
281 *guard = None;
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use std::sync::Arc;
288 use std::sync::atomic::{AtomicU32, Ordering};
289
290 use super::*;
291
292 static TEST_LOCK: Mutex<()> = Mutex::new(());
294
295 macro_rules! serial_test {
296 () => {
297 let _guard = TEST_LOCK.lock().unwrap();
298 clear();
299 };
300 }
301
302 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
303 struct TestConfig {
304 enabled: bool,
305 threshold: f64,
306 #[serde(skip_serializing)]
307 secret_token: String,
308 }
309
310 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
311 struct SensitiveConfig {
312 host: String,
313 password: String,
314 api_token: String,
315 encryption_key: String,
316 normal_field: u32,
317 nested: NestedSensitive,
318 }
319
320 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
321 struct NestedSensitive {
322 db_password: String,
323 port: u16,
324 }
325
326 #[test]
327 fn register_and_retrieve() {
328 serial_test!();
329
330 let config = TestConfig {
331 enabled: true,
332 threshold: 0.75,
333 secret_token: "hunter2".into(),
334 };
335 register::<TestConfig>("test_module", &config);
336
337 assert!(is_registered("test_module"));
338 assert!(!is_registered("nonexistent"));
339
340 let section = get_section("test_module").unwrap();
341 assert_eq!(section.key, "test_module");
342 assert!(section.type_name.contains("TestConfig"));
343
344 assert_eq!(section.effective["enabled"], true);
345 assert_eq!(section.effective["threshold"], 0.75);
346 assert!(section.effective.get("secret_token").is_none());
348
349 assert_eq!(section.defaults["enabled"], false);
350 assert_eq!(section.defaults["threshold"], 0.0);
351 }
352
353 #[test]
354 fn sections_returns_sorted() {
355 serial_test!();
356
357 register::<TestConfig>("zebra", &TestConfig::default());
358 register::<TestConfig>("alpha", &TestConfig::default());
359 register::<TestConfig>("middle", &TestConfig::default());
360
361 let keys: Vec<String> = sections().iter().map(|s| s.key.clone()).collect();
362 assert_eq!(keys, vec!["alpha", "middle", "zebra"]);
363 }
364
365 #[test]
366 fn dump_effective_redacts_sensitive_fields() {
367 serial_test!();
368
369 let config = SensitiveConfig {
370 host: "db.example.com".into(),
371 password: "super_secret".into(),
372 api_token: "tok_abc123".into(),
373 encryption_key: "aes256key".into(),
374 normal_field: 42,
375 nested: NestedSensitive {
376 db_password: "nested_secret".into(),
377 port: 5432,
378 },
379 };
380 register::<SensitiveConfig>("db", &config);
381
382 let dump = dump_effective();
383 assert_eq!(dump["db"]["host"], "db.example.com");
385 assert_eq!(dump["db"]["normal_field"], 42);
386 assert_eq!(dump["db"]["nested"]["port"], 5432);
387
388 assert_eq!(dump["db"]["password"], REDACTED);
390 assert_eq!(dump["db"]["api_token"], REDACTED);
391 assert_eq!(dump["db"]["encryption_key"], REDACTED);
392 assert_eq!(dump["db"]["nested"]["db_password"], REDACTED);
393 }
394
395 #[test]
396 fn dump_unredacted_preserves_all_fields() {
397 serial_test!();
398
399 let config = SensitiveConfig {
400 password: "visible".into(),
401 ..Default::default()
402 };
403 register::<SensitiveConfig>("db", &config);
404
405 let dump = dump_effective_unredacted();
406 assert_eq!(dump["db"]["password"], "visible");
407 }
408
409 #[test]
410 fn dump_defaults_returns_default_values() {
411 serial_test!();
412
413 register::<TestConfig>(
414 "my_module",
415 &TestConfig {
416 enabled: true,
417 threshold: 0.9,
418 secret_token: String::new(),
419 },
420 );
421
422 let dump = dump_defaults();
423 assert_eq!(dump["my_module"]["enabled"], false);
424 assert_eq!(dump["my_module"]["threshold"], 0.0);
425 }
426
427 #[test]
428 fn re_register_overwrites() {
429 serial_test!();
430
431 let v1 = TestConfig {
432 threshold: 0.5,
433 ..Default::default()
434 };
435 register::<TestConfig>("module", &v1);
436 assert_eq!(get_section("module").unwrap().effective["threshold"], 0.5);
437
438 let v2 = TestConfig {
439 threshold: 0.9,
440 ..Default::default()
441 };
442 register::<TestConfig>("module", &v2);
443 assert_eq!(get_section("module").unwrap().effective["threshold"], 0.9);
444 }
445
446 #[test]
447 fn empty_registry() {
448 serial_test!();
449
450 assert!(sections().is_empty());
451 assert_eq!(dump_effective(), JsonValue::Object(serde_json::Map::new()));
452 assert_eq!(dump_defaults(), JsonValue::Object(serde_json::Map::new()));
453 assert!(!is_registered("anything"));
454 assert!(get_section("anything").is_none());
455 }
456
457 #[test]
460 fn on_change_fires_on_update() {
461 serial_test!();
462
463 let counter = Arc::new(AtomicU32::new(0));
464 let counter_clone = counter.clone();
465
466 on_change("my_key", move |_value| {
467 counter_clone.fetch_add(1, Ordering::Relaxed);
468 });
469
470 let config = TestConfig {
471 enabled: true,
472 ..Default::default()
473 };
474 update::<TestConfig>("my_key", &config);
475
476 assert_eq!(counter.load(Ordering::Relaxed), 1);
477
478 update::<TestConfig>("my_key", &config);
480 assert_eq!(counter.load(Ordering::Relaxed), 2);
481 }
482
483 #[test]
484 fn on_change_receives_new_value() {
485 serial_test!();
486
487 let captured = Arc::new(Mutex::new(JsonValue::Null));
488 let captured_clone = captured.clone();
489
490 on_change("watched", move |value| {
491 if let Ok(mut guard) = captured_clone.lock() {
492 *guard = value.clone();
493 }
494 });
495
496 let config = TestConfig {
497 enabled: true,
498 threshold: 0.99,
499 ..Default::default()
500 };
501 update::<TestConfig>("watched", &config);
502
503 let val = captured.lock().unwrap().clone();
504 assert_eq!(val["enabled"], true);
505 assert_eq!(val["threshold"], 0.99);
506 }
507
508 #[test]
509 fn on_change_only_fires_for_subscribed_key() {
510 serial_test!();
511
512 let counter = Arc::new(AtomicU32::new(0));
513 let counter_clone = counter.clone();
514
515 on_change("key_a", move |_| {
516 counter_clone.fetch_add(1, Ordering::Relaxed);
517 });
518
519 update::<TestConfig>("key_b", &TestConfig::default());
521 assert_eq!(counter.load(Ordering::Relaxed), 0);
522
523 update::<TestConfig>("key_a", &TestConfig::default());
525 assert_eq!(counter.load(Ordering::Relaxed), 1);
526 }
527
528 #[test]
529 fn update_also_registers() {
530 serial_test!();
531
532 assert!(!is_registered("fresh"));
533 update::<TestConfig>(
534 "fresh",
535 &TestConfig {
536 enabled: true,
537 ..Default::default()
538 },
539 );
540 assert!(is_registered("fresh"));
541 assert_eq!(get_section("fresh").unwrap().effective["enabled"], true);
542 }
543
544 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
547 struct MixedCase {
548 #[serde(rename = "Password")]
549 password_upper: String,
550 #[serde(rename = "API_TOKEN")]
551 token_upper: String,
552 #[serde(rename = "mySecret")]
553 secret_camel: String,
554 }
555
556 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
557 struct DeepNested {
558 level1: Level1,
559 }
560 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
561 struct Level1 {
562 level2: Level2,
563 name: String,
564 }
565 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
566 struct Level2 {
567 api_token: String,
568 db_password: String,
569 port: u16,
570 }
571
572 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
573 struct WithArray {
574 items: Vec<ArrayItem>,
575 }
576 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
577 struct ArrayItem {
578 name: String,
579 secret_key: String,
580 }
581
582 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
583 struct WithDefaultSecret {
584 api_token: String,
585 host: String,
586 }
587 impl Default for WithDefaultSecret {
588 fn default() -> Self {
589 Self {
590 api_token: "default-placeholder-token".into(),
591 host: "localhost".into(),
592 }
593 }
594 }
595
596 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
597 struct DoubleProtected {
598 #[serde(skip_serializing)]
599 #[allow(dead_code)]
600 hidden_secret: String,
601 visible_token: String,
602 normal: String,
603 }
604
605 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
609 struct AllSensitivePatterns {
610 my_password: String,
612 db_secret: String,
613 api_token: String,
614 encryption_key: String,
615 aws_credential: String,
616 oauth_auth_code: String,
617 private_data: String,
618 tls_cert_path: String,
619 hostname: String,
621 port: u16,
622 enabled: bool,
623 timeout_ms: u64,
624 }
625
626 #[test]
627 fn redaction_covers_all_sensitive_patterns() {
628 serial_test!();
629
630 let config = AllSensitivePatterns {
631 my_password: "pass123".into(),
632 db_secret: "sec456".into(),
633 api_token: "tok789".into(),
634 encryption_key: "key012".into(),
635 aws_credential: "cred345".into(),
636 oauth_auth_code: "auth678".into(),
637 private_data: "priv901".into(),
638 tls_cert_path: "/etc/tls/cert.pem".into(),
639 hostname: "db.prod.internal".into(),
640 port: 5432,
641 enabled: true,
642 timeout_ms: 30000,
643 };
644 register::<AllSensitivePatterns>("all_patterns", &config);
645
646 let dump = dump_effective();
647 let section = &dump["all_patterns"];
648
649 assert_eq!(section["my_password"], REDACTED, "password pattern missed");
651 assert_eq!(section["db_secret"], REDACTED, "secret pattern missed");
652 assert_eq!(section["api_token"], REDACTED, "token pattern missed");
653 assert_eq!(section["encryption_key"], REDACTED, "key pattern missed");
654 assert_eq!(
655 section["aws_credential"], REDACTED,
656 "credential pattern missed"
657 );
658 assert_eq!(section["oauth_auth_code"], REDACTED, "auth pattern missed");
659 assert_eq!(section["private_data"], REDACTED, "private pattern missed");
660 assert_eq!(section["tls_cert_path"], REDACTED, "cert pattern missed");
661
662 assert_eq!(section["hostname"], "db.prod.internal");
664 assert_eq!(section["port"], 5432);
665 assert_eq!(section["enabled"], true);
666 assert_eq!(section["timeout_ms"], 30000);
667 }
668
669 #[test]
670 fn redaction_is_case_insensitive() {
671 serial_test!();
672
673 let config = MixedCase {
674 password_upper: "visible_if_broken".into(),
675 token_upper: "visible_if_broken".into(),
676 secret_camel: "visible_if_broken".into(),
677 };
678 register::<MixedCase>("case_test", &config);
679
680 let dump = dump_effective();
681 let section = &dump["case_test"];
682
683 assert_eq!(section["Password"], REDACTED);
684 assert_eq!(section["API_TOKEN"], REDACTED);
685 assert_eq!(section["mySecret"], REDACTED);
686 }
687
688 #[test]
689 fn redaction_handles_deeply_nested_secrets() {
690 serial_test!();
691
692 let config = DeepNested {
693 level1: Level1 {
694 level2: Level2 {
695 api_token: "deep_secret_1".into(),
696 db_password: "deep_secret_2".into(),
697 port: 3306,
698 },
699 name: "safe_value".into(),
700 },
701 };
702 register::<DeepNested>("deep", &config);
703
704 let dump = dump_effective();
705 assert_eq!(dump["deep"]["level1"]["level2"]["api_token"], REDACTED);
706 assert_eq!(dump["deep"]["level1"]["level2"]["db_password"], REDACTED);
707 assert_eq!(dump["deep"]["level1"]["level2"]["port"], 3306);
708 assert_eq!(dump["deep"]["level1"]["name"], "safe_value");
709 }
710
711 #[test]
712 fn redaction_handles_arrays_with_sensitive_objects() {
713 serial_test!();
714
715 let config = WithArray {
716 items: vec![
717 ArrayItem {
718 name: "item1".into(),
719 secret_key: "sk_1".into(),
720 },
721 ArrayItem {
722 name: "item2".into(),
723 secret_key: "sk_2".into(),
724 },
725 ],
726 };
727 register::<WithArray>("array_test", &config);
728
729 let dump = dump_effective();
730 let items = dump["array_test"]["items"].as_array().unwrap();
731 for item in items {
732 assert_eq!(item["secret_key"], REDACTED);
733 assert_ne!(item["name"], REDACTED); }
735 }
736
737 #[test]
738 fn no_secret_values_in_redacted_dump_string() {
739 serial_test!();
740
741 let secrets = [
742 "hunter2",
743 "sk_live_abc123",
744 "super_s3cret!",
745 "my-private-key-data",
746 ];
747
748 let config = AllSensitivePatterns {
749 my_password: secrets[0].into(),
750 db_secret: secrets[1].into(),
751 api_token: secrets[2].into(),
752 encryption_key: secrets[3].into(),
753 ..Default::default()
754 };
755 register::<AllSensitivePatterns>("leak_check", &config);
756
757 let dump = dump_effective();
759 let dump_str = serde_json::to_string(&dump).unwrap();
760
761 for secret in &secrets {
762 assert!(
763 !dump_str.contains(secret),
764 "SECRET LEAKED in dump_effective(): '{secret}' found in output"
765 );
766 }
767 }
768
769 #[test]
770 fn defaults_dump_also_redacted() {
771 serial_test!();
772
773 register::<WithDefaultSecret>("default_secrets", &WithDefaultSecret::default());
774
775 let dump = dump_defaults();
776 assert_eq!(dump["default_secrets"]["api_token"], REDACTED);
777 assert_eq!(dump["default_secrets"]["host"], "localhost");
778 }
779
780 #[test]
781 fn skip_serializing_plus_heuristic_double_protection() {
782 serial_test!();
783
784 let config = DoubleProtected {
785 hidden_secret: "should_not_appear".into(),
786 visible_token: "should_be_redacted".into(),
787 normal: "visible".into(),
788 };
789 register::<DoubleProtected>("double", &config);
790
791 let dump = dump_effective();
792 let section = &dump["double"];
793
794 assert!(section.get("hidden_secret").is_none());
796 assert_eq!(section["visible_token"], REDACTED);
798 assert_eq!(section["normal"], "visible");
800
801 let dump_str = serde_json::to_string(&dump).unwrap();
803 assert!(!dump_str.contains("should_not_appear"));
804 assert!(!dump_str.contains("should_be_redacted"));
805 }
806
807 #[test]
810 fn multiple_listeners_on_same_key() {
811 serial_test!();
812
813 let c1 = Arc::new(AtomicU32::new(0));
814 let c2 = Arc::new(AtomicU32::new(0));
815 let c1c = c1.clone();
816 let c2c = c2.clone();
817
818 on_change("shared", move |_| {
819 c1c.fetch_add(1, Ordering::Relaxed);
820 });
821 on_change("shared", move |_| {
822 c2c.fetch_add(1, Ordering::Relaxed);
823 });
824
825 update::<TestConfig>("shared", &TestConfig::default());
826
827 assert_eq!(c1.load(Ordering::Relaxed), 1);
828 assert_eq!(c2.load(Ordering::Relaxed), 1);
829 }
830}