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