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 macro_rules! serial_test {
310 () => {
311 let _guard = TEST_LOCK.lock().unwrap();
312 clear();
313 };
314 }
315
316 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
317 struct TestConfig {
318 enabled: bool,
319 threshold: f64,
320 #[serde(skip_serializing)]
321 secret_token: String,
322 }
323
324 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
325 struct SensitiveConfig {
326 host: String,
327 password: String,
328 api_token: String,
329 encryption_key: String,
330 normal_field: u32,
331 nested: NestedSensitive,
332 }
333
334 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
335 struct NestedSensitive {
336 db_password: String,
337 port: u16,
338 }
339
340 #[test]
341 fn register_and_retrieve() {
342 serial_test!();
343
344 let config = TestConfig {
345 enabled: true,
346 threshold: 0.75,
347 secret_token: "hunter2".into(),
348 };
349 register::<TestConfig>("test_module", &config);
350
351 assert!(is_registered("test_module"));
352 assert!(!is_registered("nonexistent"));
353
354 let section = get_section("test_module").unwrap();
355 assert_eq!(section.key, "test_module");
356 assert!(section.type_name.contains("TestConfig"));
357
358 assert_eq!(section.effective["enabled"], true);
359 assert_eq!(section.effective["threshold"], 0.75);
360 assert!(section.effective.get("secret_token").is_none());
362
363 assert_eq!(section.defaults["enabled"], false);
364 assert_eq!(section.defaults["threshold"], 0.0);
365 }
366
367 #[test]
368 fn sections_returns_sorted() {
369 serial_test!();
370
371 register::<TestConfig>("zebra", &TestConfig::default());
372 register::<TestConfig>("alpha", &TestConfig::default());
373 register::<TestConfig>("middle", &TestConfig::default());
374
375 let keys: Vec<String> = sections().iter().map(|s| s.key.clone()).collect();
376 assert_eq!(keys, vec!["alpha", "middle", "zebra"]);
377 }
378
379 #[test]
380 fn dump_effective_redacts_sensitive_fields() {
381 serial_test!();
382
383 let config = SensitiveConfig {
384 host: "db.example.com".into(),
385 password: "super_secret".into(),
386 api_token: "tok_abc123".into(),
387 encryption_key: "aes256key".into(),
388 normal_field: 42,
389 nested: NestedSensitive {
390 db_password: "nested_secret".into(),
391 port: 5432,
392 },
393 };
394 register::<SensitiveConfig>("db", &config);
395
396 let dump = dump_effective();
397 assert_eq!(dump["db"]["host"], "db.example.com");
399 assert_eq!(dump["db"]["normal_field"], 42);
400 assert_eq!(dump["db"]["nested"]["port"], 5432);
401
402 assert_eq!(dump["db"]["password"], REDACTED);
404 assert_eq!(dump["db"]["api_token"], REDACTED);
405 assert_eq!(dump["db"]["encryption_key"], REDACTED);
406 assert_eq!(dump["db"]["nested"]["db_password"], REDACTED);
407 }
408
409 #[cfg(feature = "dangerous-diagnostics")]
410 #[test]
411 fn dump_unredacted_preserves_all_fields() {
412 serial_test!();
413
414 let config = SensitiveConfig {
415 password: "visible".into(),
416 ..Default::default()
417 };
418 register::<SensitiveConfig>("db", &config);
419
420 let dump = dump_effective_unredacted();
421 assert_eq!(dump["db"]["password"], "visible");
422 }
423
424 #[test]
425 fn dump_defaults_returns_default_values() {
426 serial_test!();
427
428 register::<TestConfig>(
429 "my_module",
430 &TestConfig {
431 enabled: true,
432 threshold: 0.9,
433 secret_token: String::new(),
434 },
435 );
436
437 let dump = dump_defaults();
438 assert_eq!(dump["my_module"]["enabled"], false);
439 assert_eq!(dump["my_module"]["threshold"], 0.0);
440 }
441
442 #[test]
443 fn re_register_overwrites() {
444 serial_test!();
445
446 let v1 = TestConfig {
447 threshold: 0.5,
448 ..Default::default()
449 };
450 register::<TestConfig>("module", &v1);
451 assert_eq!(get_section("module").unwrap().effective["threshold"], 0.5);
452
453 let v2 = TestConfig {
454 threshold: 0.9,
455 ..Default::default()
456 };
457 register::<TestConfig>("module", &v2);
458 assert_eq!(get_section("module").unwrap().effective["threshold"], 0.9);
459 }
460
461 #[test]
462 fn empty_registry() {
463 serial_test!();
464
465 assert!(sections().is_empty());
466 assert_eq!(dump_effective(), JsonValue::Object(serde_json::Map::new()));
467 assert_eq!(dump_defaults(), JsonValue::Object(serde_json::Map::new()));
468 assert!(!is_registered("anything"));
469 assert!(get_section("anything").is_none());
470 }
471
472 #[test]
475 fn on_change_fires_on_update() {
476 serial_test!();
477
478 let counter = Arc::new(AtomicU32::new(0));
479 let counter_clone = counter.clone();
480
481 on_change("my_key", move |_value| {
482 counter_clone.fetch_add(1, Ordering::Relaxed);
483 });
484
485 let config = TestConfig {
486 enabled: true,
487 ..Default::default()
488 };
489 update::<TestConfig>("my_key", &config);
490
491 assert_eq!(counter.load(Ordering::Relaxed), 1);
492
493 update::<TestConfig>("my_key", &config);
495 assert_eq!(counter.load(Ordering::Relaxed), 2);
496 }
497
498 #[test]
499 fn on_change_receives_new_value() {
500 serial_test!();
501
502 let captured = Arc::new(Mutex::new(JsonValue::Null));
503 let captured_clone = captured.clone();
504
505 on_change("watched", move |value| {
506 if let Ok(mut guard) = captured_clone.lock() {
507 *guard = value.clone();
508 }
509 });
510
511 let config = TestConfig {
512 enabled: true,
513 threshold: 0.99,
514 ..Default::default()
515 };
516 update::<TestConfig>("watched", &config);
517
518 let val = captured.lock().unwrap().clone();
519 assert_eq!(val["enabled"], true);
520 assert_eq!(val["threshold"], 0.99);
521 }
522
523 #[test]
524 fn on_change_only_fires_for_subscribed_key() {
525 serial_test!();
526
527 let counter = Arc::new(AtomicU32::new(0));
528 let counter_clone = counter.clone();
529
530 on_change("key_a", move |_| {
531 counter_clone.fetch_add(1, Ordering::Relaxed);
532 });
533
534 update::<TestConfig>("key_b", &TestConfig::default());
536 assert_eq!(counter.load(Ordering::Relaxed), 0);
537
538 update::<TestConfig>("key_a", &TestConfig::default());
540 assert_eq!(counter.load(Ordering::Relaxed), 1);
541 }
542
543 #[test]
544 fn update_also_registers() {
545 serial_test!();
546
547 assert!(!is_registered("fresh"));
548 update::<TestConfig>(
549 "fresh",
550 &TestConfig {
551 enabled: true,
552 ..Default::default()
553 },
554 );
555 assert!(is_registered("fresh"));
556 assert_eq!(get_section("fresh").unwrap().effective["enabled"], true);
557 }
558
559 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
562 struct MixedCase {
563 #[serde(rename = "Password")]
564 password_upper: String,
565 #[serde(rename = "API_TOKEN")]
566 token_upper: String,
567 #[serde(rename = "mySecret")]
568 secret_camel: String,
569 }
570
571 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
572 struct DeepNested {
573 level1: Level1,
574 }
575 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
576 struct Level1 {
577 level2: Level2,
578 name: String,
579 }
580 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
581 struct Level2 {
582 api_token: String,
583 db_password: String,
584 port: u16,
585 }
586
587 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
588 struct WithArray {
589 items: Vec<ArrayItem>,
590 }
591 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
592 struct ArrayItem {
593 name: String,
594 secret_key: String,
595 }
596
597 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
598 struct WithDefaultSecret {
599 api_token: String,
600 host: String,
601 }
602 impl Default for WithDefaultSecret {
603 fn default() -> Self {
604 Self {
605 api_token: "default-placeholder-token".into(),
606 host: "localhost".into(),
607 }
608 }
609 }
610
611 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
612 struct DoubleProtected {
613 #[serde(skip_serializing)]
614 #[allow(dead_code)]
615 hidden_secret: String,
616 visible_token: String,
617 normal: String,
618 }
619
620 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
624 struct AllSensitivePatterns {
625 my_password: String,
627 db_secret: String,
628 api_token: String,
629 encryption_key: String,
630 aws_credential: String,
631 oauth_auth_code: String,
632 private_data: String,
633 tls_cert_path: String,
634 hostname: String,
636 port: u16,
637 enabled: bool,
638 timeout_ms: u64,
639 }
640
641 #[test]
642 fn redaction_covers_all_sensitive_patterns() {
643 serial_test!();
644
645 let config = AllSensitivePatterns {
646 my_password: "pass123".into(),
647 db_secret: "sec456".into(),
648 api_token: "tok789".into(),
649 encryption_key: "key012".into(),
650 aws_credential: "cred345".into(),
651 oauth_auth_code: "auth678".into(),
652 private_data: "priv901".into(),
653 tls_cert_path: "/etc/tls/cert.pem".into(),
654 hostname: "db.prod.internal".into(),
655 port: 5432,
656 enabled: true,
657 timeout_ms: 30000,
658 };
659 register::<AllSensitivePatterns>("all_patterns", &config);
660
661 let dump = dump_effective();
662 let section = &dump["all_patterns"];
663
664 assert_eq!(section["my_password"], REDACTED, "password pattern missed");
666 assert_eq!(section["db_secret"], REDACTED, "secret pattern missed");
667 assert_eq!(section["api_token"], REDACTED, "token pattern missed");
668 assert_eq!(section["encryption_key"], REDACTED, "key pattern missed");
669 assert_eq!(
670 section["aws_credential"], REDACTED,
671 "credential pattern missed"
672 );
673 assert_eq!(section["oauth_auth_code"], REDACTED, "auth pattern missed");
674 assert_eq!(section["private_data"], REDACTED, "private pattern missed");
675 assert_eq!(section["tls_cert_path"], REDACTED, "cert pattern missed");
676
677 assert_eq!(section["hostname"], "db.prod.internal");
679 assert_eq!(section["port"], 5432);
680 assert_eq!(section["enabled"], true);
681 assert_eq!(section["timeout_ms"], 30000);
682 }
683
684 #[test]
685 fn redaction_is_case_insensitive() {
686 serial_test!();
687
688 let config = MixedCase {
689 password_upper: "visible_if_broken".into(),
690 token_upper: "visible_if_broken".into(),
691 secret_camel: "visible_if_broken".into(),
692 };
693 register::<MixedCase>("case_test", &config);
694
695 let dump = dump_effective();
696 let section = &dump["case_test"];
697
698 assert_eq!(section["Password"], REDACTED);
699 assert_eq!(section["API_TOKEN"], REDACTED);
700 assert_eq!(section["mySecret"], REDACTED);
701 }
702
703 #[test]
704 fn redaction_handles_deeply_nested_secrets() {
705 serial_test!();
706
707 let config = DeepNested {
708 level1: Level1 {
709 level2: Level2 {
710 api_token: "deep_secret_1".into(),
711 db_password: "deep_secret_2".into(),
712 port: 3306,
713 },
714 name: "safe_value".into(),
715 },
716 };
717 register::<DeepNested>("deep", &config);
718
719 let dump = dump_effective();
720 assert_eq!(dump["deep"]["level1"]["level2"]["api_token"], REDACTED);
721 assert_eq!(dump["deep"]["level1"]["level2"]["db_password"], REDACTED);
722 assert_eq!(dump["deep"]["level1"]["level2"]["port"], 3306);
723 assert_eq!(dump["deep"]["level1"]["name"], "safe_value");
724 }
725
726 #[test]
727 fn redaction_handles_arrays_with_sensitive_objects() {
728 serial_test!();
729
730 let config = WithArray {
731 items: vec![
732 ArrayItem {
733 name: "item1".into(),
734 secret_key: "sk_1".into(),
735 },
736 ArrayItem {
737 name: "item2".into(),
738 secret_key: "sk_2".into(),
739 },
740 ],
741 };
742 register::<WithArray>("array_test", &config);
743
744 let dump = dump_effective();
745 let items = dump["array_test"]["items"].as_array().unwrap();
746 for item in items {
747 assert_eq!(item["secret_key"], REDACTED);
748 assert_ne!(item["name"], REDACTED); }
750 }
751
752 #[test]
753 fn no_secret_values_in_redacted_dump_string() {
754 serial_test!();
755
756 let secrets = [
757 "hunter2",
758 "sk_live_abc123",
759 "super_s3cret!",
760 "my-private-key-data",
761 ];
762
763 let config = AllSensitivePatterns {
764 my_password: secrets[0].into(),
765 db_secret: secrets[1].into(),
766 api_token: secrets[2].into(),
767 encryption_key: secrets[3].into(),
768 ..Default::default()
769 };
770 register::<AllSensitivePatterns>("leak_check", &config);
771
772 let dump = dump_effective();
774 let dump_str = serde_json::to_string(&dump).unwrap();
775
776 for secret in &secrets {
777 assert!(
778 !dump_str.contains(secret),
779 "SECRET LEAKED in dump_effective(): '{secret}' found in output"
780 );
781 }
782 }
783
784 #[test]
785 fn defaults_dump_also_redacted() {
786 serial_test!();
787
788 register::<WithDefaultSecret>("default_secrets", &WithDefaultSecret::default());
789
790 let dump = dump_defaults();
791 assert_eq!(dump["default_secrets"]["api_token"], REDACTED);
792 assert_eq!(dump["default_secrets"]["host"], "localhost");
793 }
794
795 #[test]
796 fn skip_serializing_plus_heuristic_double_protection() {
797 serial_test!();
798
799 let config = DoubleProtected {
800 hidden_secret: "should_not_appear".into(),
801 visible_token: "should_be_redacted".into(),
802 normal: "visible".into(),
803 };
804 register::<DoubleProtected>("double", &config);
805
806 let dump = dump_effective();
807 let section = &dump["double"];
808
809 assert!(section.get("hidden_secret").is_none());
811 assert_eq!(section["visible_token"], REDACTED);
813 assert_eq!(section["normal"], "visible");
815
816 let dump_str = serde_json::to_string(&dump).unwrap();
818 assert!(!dump_str.contains("should_not_appear"));
819 assert!(!dump_str.contains("should_be_redacted"));
820 }
821
822 #[test]
825 fn multiple_listeners_on_same_key() {
826 serial_test!();
827
828 let c1 = Arc::new(AtomicU32::new(0));
829 let c2 = Arc::new(AtomicU32::new(0));
830 let c1c = c1.clone();
831 let c2c = c2.clone();
832
833 on_change("shared", move |_| {
834 c1c.fetch_add(1, Ordering::Relaxed);
835 });
836 on_change("shared", move |_| {
837 c2c.fetch_add(1, Ordering::Relaxed);
838 });
839
840 update::<TestConfig>("shared", &TestConfig::default());
841
842 assert_eq!(c1.load(Ordering::Relaxed), 1);
843 assert_eq!(c2.load(Ordering::Relaxed), 1);
844 }
845}