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