1use once_cell::sync::Lazy;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12use std::sync::Mutex;
13use tracing::error;
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum ErrorMode {
19 Production,
21 Development,
23 Testing,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ErrorLevel {
30 Public,
32 Internal,
34 Debug,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ErrorContext {
41 FileOpen,
43 FileRead,
45 FileWrite,
47 NetworkRequest,
49 XmlParsing,
51 XmlBuilding,
53 SecurityValidation,
55 EntityClassification,
57 PathValidation,
59 MemoryAllocation,
61 DatabaseConnection,
63 Authentication,
65 Authorization,
67}
68
69pub trait SecureError: fmt::Display + fmt::Debug {
71 fn public_message(&self) -> String;
73
74 fn internal_message(&self) -> String;
76
77 fn debug_message(&self) -> String;
79
80 fn error_level(&self) -> ErrorLevel;
82
83 fn error_context(&self) -> ErrorContext;
85
86 fn error_id(&self) -> String {
88 Uuid::new_v4().to_string()
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct RedactionRule {
95 pub name: String,
97 pub pattern: Regex,
99 pub replacement: String,
101 pub production: bool,
103 pub development: bool,
105 pub testing: bool,
107}
108
109impl RedactionRule {
110 pub fn new(
112 name: &str,
113 pattern: &str,
114 replacement: &str,
115 production: bool,
116 development: bool,
117 testing: bool,
118 ) -> Result<Self, regex::Error> {
119 Ok(RedactionRule {
120 name: name.to_string(),
121 pattern: Regex::new(pattern)?,
122 replacement: replacement.to_string(),
123 production,
124 development,
125 testing,
126 })
127 }
128
129 pub fn applies_to_mode(&self, mode: ErrorMode) -> bool {
131 match mode {
132 ErrorMode::Production => self.production,
133 ErrorMode::Development => self.development,
134 ErrorMode::Testing => self.testing,
135 }
136 }
137
138 pub fn apply(&self, message: &str) -> String {
140 self.pattern
141 .replace_all(message, self.replacement.as_str())
142 .to_string()
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct SanitizerConfig {
149 pub mode: ErrorMode,
151 pub generate_correlation_ids: bool,
153 pub log_internal_details: bool,
155 pub max_message_length: usize,
157 pub include_error_codes: bool,
159}
160
161impl Default for SanitizerConfig {
162 fn default() -> Self {
163 SanitizerConfig {
164 mode: if cfg!(debug_assertions) {
165 ErrorMode::Development
166 } else {
167 ErrorMode::Production
168 },
169 generate_correlation_ids: true,
170 log_internal_details: true,
171 max_message_length: 256,
172 include_error_codes: true,
173 }
174 }
175}
176
177pub struct ErrorSanitizer {
179 config: SanitizerConfig,
180 redaction_rules: Vec<RedactionRule>,
181 error_code_map: HashMap<ErrorContext, &'static str>,
182 correlation_store: HashMap<String, String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SanitizedError {
188 pub correlation_id: String,
190 pub message: String,
192 pub code: Option<String>,
194 pub context: Option<String>,
196}
197
198impl fmt::Display for SanitizedError {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 if let Some(code) = &self.code {
201 write!(f, "[{}] {}", code, self.message)?;
202 } else {
203 write!(f, "{}", self.message)?;
204 }
205
206 if let Some(context) = &self.context {
207 write!(f, " ({})", context)?;
208 }
209
210 write!(f, " [ID: {}]", &self.correlation_id[0..8])
211 }
212}
213
214static DEFAULT_REDACTION_RULES: Lazy<Vec<RedactionRule>> = Lazy::new(|| {
216 let mut rules = Vec::new();
217
218 if let Ok(rule) = RedactionRule::new(
220 "filesystem_paths",
221 r"(/[^/\s]+)+(/[^/\s]*\.[^/\s]+)?|([A-Z]:\\[^\\]+\\[^\\]*)",
222 "<file path>",
223 true, false, false, ) {
227 rules.push(rule);
228 }
229
230 if let Ok(rule) = RedactionRule::new(
232 "filesystem_paths_dev",
233 r"(/[^/\s]+)+/([^/\s]*\.[^/\s]+)|([A-Z]:\\[^\\]+\\[^\\]*)\\([^\\]*)",
234 "<path>/$2$4",
235 false, true, false, ) {
239 rules.push(rule);
240 }
241
242 if let Ok(rule) = RedactionRule::new(
244 "ip_addresses",
245 r"\b(?:\d{1,3}\.){3}\d{1,3}\b|\b[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}\b",
246 "<ip address>",
247 true, true, false, ) {
251 rules.push(rule);
252 }
253
254 if let Ok(rule) = RedactionRule::new(
256 "hostnames",
257 r"https?://[^\s/$.?#].[^\s]*|[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:[/\s]|$)",
258 "<hostname>",
259 true, true, false, ) {
263 rules.push(rule);
264 }
265
266 if let Ok(rule) = RedactionRule::new(
268 "memory_addresses",
269 r"0x[0-9a-fA-F]+|[0-9a-fA-F]{8,16}",
270 "<memory address>",
271 true, true, false, ) {
275 rules.push(rule);
276 }
277
278 if let Ok(rule) = RedactionRule::new(
280 "stack_traces",
281 r"at [^:]+:\d+:\d+|in `[^`]+`",
282 "<stack trace>",
283 true, false, false, ) {
287 rules.push(rule);
288 }
289
290 if let Ok(rule) = RedactionRule::new(
292 "api_keys",
293 r#"(?i)(api_?key|token|secret|password|auth)[\s]*[:=][\s]*"?([a-zA-Z0-9\-_]{16,})"?"#,
294 "$1=<redacted>",
295 true, true, true, ) {
299 rules.push(rule);
300 }
301
302 if let Ok(rule) = RedactionRule::new(
304 "user_paths",
305 r"/Users/[^/\s]+|/home/[^/\s]+|C:\\Users\\[^\\\\]+",
306 "<user directory>",
307 true, true, false, ) {
311 rules.push(rule);
312 }
313
314 if let Ok(rule) = RedactionRule::new(
316 "db_connections",
317 r"(?i)(mysql|postgres|mongodb)://[^@\s]+@[^/\s]+/[^\s]*",
318 "$1://<connection>",
319 true, true, true, ) {
323 rules.push(rule);
324 }
325
326 rules
327});
328
329impl ErrorSanitizer {
330 pub fn new() -> Self {
332 Self::with_config(SanitizerConfig::default())
333 }
334
335 pub fn with_config(config: SanitizerConfig) -> Self {
337 let error_code_map = Self::create_error_code_map();
338
339 ErrorSanitizer {
340 config,
341 redaction_rules: DEFAULT_REDACTION_RULES.clone(),
342 error_code_map,
343 correlation_store: HashMap::new(),
344 }
345 }
346
347 pub fn add_redaction_rule(&mut self, rule: RedactionRule) {
349 self.redaction_rules.push(rule);
350 }
351
352 pub fn sanitize<E>(&mut self, error: E, context: ErrorContext) -> SanitizedError
354 where
355 E: std::error::Error + fmt::Display + fmt::Debug,
356 {
357 let correlation_id = if self.config.generate_correlation_ids {
358 Uuid::new_v4().to_string()
359 } else {
360 "none".to_string()
361 };
362
363 let raw_message = error.to_string();
365 let debug_message = format!("{:?}", error);
366
367 if self.config.log_internal_details {
369 error!(
370 correlation_id = %correlation_id,
371 context = ?context,
372 raw_message = %raw_message,
373 debug_info = %debug_message,
374 "Internal error details"
375 );
376
377 if self.config.generate_correlation_ids {
379 self.correlation_store.insert(
380 correlation_id.clone(),
381 format!(
382 "Context: {:?}, Error: {}, Debug: {}",
383 context, raw_message, debug_message
384 ),
385 );
386 }
387 }
388
389 let sanitized_message = self.apply_sanitization(&raw_message, context);
391
392 let final_message = if sanitized_message.len() > self.config.max_message_length {
394 format!(
395 "{}...",
396 &sanitized_message[0..self.config.max_message_length.saturating_sub(3)]
397 )
398 } else {
399 sanitized_message
400 };
401
402 let error_code = if self.config.include_error_codes {
404 self.error_code_map.get(&context).map(|&s| s.to_string())
405 } else {
406 None
407 };
408
409 SanitizedError {
410 correlation_id,
411 message: final_message,
412 code: error_code,
413 context: Some(self.get_safe_context_description(context)),
414 }
415 }
416
417 fn apply_sanitization(&self, message: &str, context: ErrorContext) -> String {
419 let mut sanitized = message.to_string();
420
421 sanitized = self.apply_context_specific_sanitization(sanitized, context);
423
424 for rule in &self.redaction_rules {
426 if rule.applies_to_mode(self.config.mode) {
427 sanitized = rule.apply(&sanitized);
428 }
429 }
430
431 sanitized
432 }
433
434 fn apply_context_specific_sanitization(
436 &self,
437 message: String,
438 context: ErrorContext,
439 ) -> String {
440 match (context, self.config.mode) {
441 (
442 ErrorContext::FileOpen | ErrorContext::FileRead | ErrorContext::FileWrite,
443 ErrorMode::Production,
444 ) => "File operation failed".to_string(),
445 (
446 ErrorContext::FileOpen | ErrorContext::FileRead | ErrorContext::FileWrite,
447 ErrorMode::Development,
448 ) => {
449 let operation = match context {
451 ErrorContext::FileOpen => "open",
452 ErrorContext::FileRead => "read",
453 ErrorContext::FileWrite => "write",
454 _ => "access",
455 };
456 format!("Failed to {} file", operation)
457 }
458 (ErrorContext::NetworkRequest, ErrorMode::Production) => {
459 "Network operation failed".to_string()
460 }
461 (ErrorContext::XmlParsing, ErrorMode::Production) => {
462 "Invalid XML structure".to_string()
463 }
464 (ErrorContext::XmlBuilding, ErrorMode::Production) => {
465 "XML generation failed".to_string()
466 }
467 (ErrorContext::SecurityValidation, ErrorMode::Production) => {
468 "Security validation failed".to_string()
469 }
470 (ErrorContext::EntityClassification, ErrorMode::Production) => {
471 "Entity validation failed".to_string()
472 }
473 (ErrorContext::PathValidation, ErrorMode::Production) => {
474 "Path validation failed".to_string()
475 }
476 (ErrorContext::MemoryAllocation, ErrorMode::Production) => {
477 "Memory allocation failed".to_string()
478 }
479 (ErrorContext::DatabaseConnection, ErrorMode::Production) => {
480 "Database connection failed".to_string()
481 }
482 (ErrorContext::Authentication, ErrorMode::Production) => {
483 "Authentication failed".to_string()
484 }
485 (ErrorContext::Authorization, ErrorMode::Production) => "Access denied".to_string(),
486 _ => message,
488 }
489 }
490
491 fn create_error_code_map() -> HashMap<ErrorContext, &'static str> {
493 let mut map = HashMap::new();
494 map.insert(ErrorContext::FileOpen, "E1001");
495 map.insert(ErrorContext::FileRead, "E1002");
496 map.insert(ErrorContext::FileWrite, "E1003");
497 map.insert(ErrorContext::NetworkRequest, "E2001");
498 map.insert(ErrorContext::XmlParsing, "E3001");
499 map.insert(ErrorContext::XmlBuilding, "E3002");
500 map.insert(ErrorContext::SecurityValidation, "E4001");
501 map.insert(ErrorContext::EntityClassification, "E4002");
502 map.insert(ErrorContext::PathValidation, "E4003");
503 map.insert(ErrorContext::MemoryAllocation, "E5001");
504 map.insert(ErrorContext::DatabaseConnection, "E6001");
505 map.insert(ErrorContext::Authentication, "E7001");
506 map.insert(ErrorContext::Authorization, "E7002");
507 map
508 }
509
510 fn get_safe_context_description(&self, context: ErrorContext) -> String {
512 match context {
513 ErrorContext::FileOpen => "file access".to_string(),
514 ErrorContext::FileRead => "file reading".to_string(),
515 ErrorContext::FileWrite => "file writing".to_string(),
516 ErrorContext::NetworkRequest => "network operation".to_string(),
517 ErrorContext::XmlParsing => "XML parsing".to_string(),
518 ErrorContext::XmlBuilding => "XML generation".to_string(),
519 ErrorContext::SecurityValidation => "security check".to_string(),
520 ErrorContext::EntityClassification => "entity validation".to_string(),
521 ErrorContext::PathValidation => "path validation".to_string(),
522 ErrorContext::MemoryAllocation => "memory management".to_string(),
523 ErrorContext::DatabaseConnection => "database access".to_string(),
524 ErrorContext::Authentication => "authentication".to_string(),
525 ErrorContext::Authorization => "authorization".to_string(),
526 }
527 }
528
529 pub fn get_error_details(&self, correlation_id: &str) -> Option<&String> {
531 self.correlation_store.get(correlation_id)
532 }
533
534 pub fn clear_error_store(&mut self) {
536 self.correlation_store.clear();
537 }
538
539 pub fn get_statistics(&self) -> SanitizerStatistics {
541 SanitizerStatistics {
542 mode: self.config.mode,
543 active_rules: self
544 .redaction_rules
545 .iter()
546 .filter(|r| r.applies_to_mode(self.config.mode))
547 .count(),
548 stored_errors: self.correlation_store.len(),
549 }
550 }
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct SanitizerStatistics {
556 pub mode: ErrorMode,
558 pub active_rules: usize,
560 pub stored_errors: usize,
562}
563
564impl Default for ErrorSanitizer {
565 fn default() -> Self {
566 Self::new()
567 }
568}
569
570impl ErrorSanitizer {
572 pub fn sanitize_io_error<E>(&mut self, error: E, context: ErrorContext) -> SanitizedError
574 where
575 E: std::error::Error + fmt::Display + fmt::Debug,
576 {
577 self.sanitize(error, context)
578 }
579
580 pub fn sanitize_parse_error<E>(&mut self, error: E) -> SanitizedError
582 where
583 E: std::error::Error + fmt::Display + fmt::Debug,
584 {
585 self.sanitize(error, ErrorContext::XmlParsing)
586 }
587
588 pub fn sanitize_build_error<E>(&mut self, error: E) -> SanitizedError
590 where
591 E: std::error::Error + fmt::Display + fmt::Debug,
592 {
593 self.sanitize(error, ErrorContext::XmlBuilding)
594 }
595
596 pub fn sanitize_security_error<E>(&mut self, error: E) -> SanitizedError
598 where
599 E: std::error::Error + fmt::Display + fmt::Debug,
600 {
601 self.sanitize(error, ErrorContext::SecurityValidation)
602 }
603}
604
605static GLOBAL_SANITIZER: Lazy<Mutex<ErrorSanitizer>> =
607 Lazy::new(|| Mutex::new(ErrorSanitizer::with_config(SanitizerConfig::default())));
608
609pub fn init_global_sanitizer(config: SanitizerConfig) {
611 *GLOBAL_SANITIZER.lock().unwrap() = ErrorSanitizer::with_config(config);
613}
614
615pub fn with_global_sanitizer<F, R>(f: F) -> R
617where
618 F: FnOnce(&mut ErrorSanitizer) -> R,
619{
620 let mut sanitizer = GLOBAL_SANITIZER.lock().unwrap();
621 f(&mut *sanitizer)
622}
623
624pub fn sanitize_error<E>(error: E, context: ErrorContext) -> SanitizedError
626where
627 E: std::error::Error + fmt::Display + fmt::Debug,
628{
629 with_global_sanitizer(|sanitizer| sanitizer.sanitize(error, context))
630}
631
632pub fn sanitize_io_error<E>(error: E, context: ErrorContext) -> SanitizedError
634where
635 E: std::error::Error + fmt::Display + fmt::Debug,
636{
637 with_global_sanitizer(|sanitizer| sanitizer.sanitize_io_error(error, context))
638}
639
640pub fn sanitize_parse_error<E>(error: E) -> SanitizedError
642where
643 E: std::error::Error + fmt::Display + fmt::Debug,
644{
645 with_global_sanitizer(|sanitizer| sanitizer.sanitize_parse_error(error))
646}
647
648pub fn sanitize_build_error<E>(error: E) -> SanitizedError
650where
651 E: std::error::Error + fmt::Display + fmt::Debug,
652{
653 with_global_sanitizer(|sanitizer| sanitizer.sanitize_build_error(error))
654}
655
656pub fn sanitize_security_error<E>(error: E) -> SanitizedError
658where
659 E: std::error::Error + fmt::Display + fmt::Debug,
660{
661 with_global_sanitizer(|sanitizer| sanitizer.sanitize_security_error(error))
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use std::io::{Error, ErrorKind};
668
669 #[test]
670 fn test_secure_error_trait() {
671 struct TestError {
672 message: String,
673 context: ErrorContext,
674 }
675
676 impl fmt::Display for TestError {
677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678 write!(f, "{}", self.message)
679 }
680 }
681
682 impl fmt::Debug for TestError {
683 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684 write!(
685 f,
686 "TestError {{ message: {:?}, context: {:?} }}",
687 self.message, self.context
688 )
689 }
690 }
691
692 impl std::error::Error for TestError {}
693
694 impl SecureError for TestError {
695 fn public_message(&self) -> String {
696 "Operation failed".to_string()
697 }
698
699 fn internal_message(&self) -> String {
700 self.message.clone()
701 }
702
703 fn debug_message(&self) -> String {
704 format!("{:?}", self)
705 }
706
707 fn error_level(&self) -> ErrorLevel {
708 ErrorLevel::Internal
709 }
710
711 fn error_context(&self) -> ErrorContext {
712 self.context
713 }
714 }
715
716 let error = TestError {
717 message: "Detailed error with /path/to/file.txt".to_string(),
718 context: ErrorContext::FileRead,
719 };
720
721 assert_eq!(error.public_message(), "Operation failed");
722 assert!(error.internal_message().contains("/path/to/file.txt"));
723 assert_eq!(error.error_level(), ErrorLevel::Internal);
724 assert_eq!(error.error_context(), ErrorContext::FileRead);
725 }
726
727 #[test]
728 fn test_redaction_rules() {
729 let rule = RedactionRule::new(
730 "test_paths",
731 r"/[^/\s]+/[^/\s]+",
732 "<redacted path>",
733 true,
734 true,
735 false,
736 )
737 .unwrap();
738
739 let message = "Failed to open /home/user/secret.txt";
740 let redacted = rule.apply(message);
741 assert_eq!(redacted, "Failed to open <redacted path>/secret.txt");
742
743 assert!(rule.applies_to_mode(ErrorMode::Production));
744 assert!(rule.applies_to_mode(ErrorMode::Development));
745 assert!(!rule.applies_to_mode(ErrorMode::Testing));
746 }
747
748 #[test]
749 fn test_error_sanitizer_production_mode() {
750 let config = SanitizerConfig {
751 mode: ErrorMode::Production,
752 generate_correlation_ids: true,
753 log_internal_details: false, max_message_length: 100,
755 include_error_codes: true,
756 };
757
758 let mut sanitizer = ErrorSanitizer::with_config(config);
759 let io_error = Error::new(
760 ErrorKind::NotFound,
761 "File not found: /home/user/secrets.txt",
762 );
763
764 let sanitized = sanitizer.sanitize_io_error(io_error, ErrorContext::FileOpen);
765
766 assert_eq!(sanitized.message, "File operation failed");
767 assert_eq!(sanitized.code, Some("E1001".to_string()));
768 assert!(sanitized.context.is_some());
769 assert!(!sanitized.correlation_id.is_empty());
770 }
771
772 #[test]
773 fn test_error_sanitizer_development_mode() {
774 let config = SanitizerConfig {
775 mode: ErrorMode::Development,
776 generate_correlation_ids: true,
777 log_internal_details: false,
778 max_message_length: 200,
779 include_error_codes: true,
780 };
781
782 let mut sanitizer = ErrorSanitizer::with_config(config);
783 let io_error = Error::new(
784 ErrorKind::PermissionDenied,
785 "Permission denied: /etc/shadow",
786 );
787
788 let sanitized = sanitizer.sanitize_io_error(io_error, ErrorContext::FileRead);
789
790 assert!(sanitized.message.contains("file"));
792 assert_eq!(sanitized.code, Some("E1002".to_string()));
793 assert!(sanitized.context.is_some());
794 }
795
796 #[test]
797 fn test_path_redaction() {
798 let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
799 mode: ErrorMode::Production,
800 ..SanitizerConfig::default()
801 });
802
803 let error = Error::new(
804 ErrorKind::NotFound,
805 "Cannot find /Users/john/Documents/secret.pdf",
806 );
807 let sanitized = sanitizer.sanitize_io_error(error, ErrorContext::FileOpen);
808
809 assert_eq!(sanitized.message, "File operation failed");
811 }
812
813 #[test]
814 fn test_ip_address_redaction() {
815 let rule = RedactionRule::new(
816 "test_ips",
817 r"\b(?:\d{1,3}\.){3}\d{1,3}\b",
818 "<ip>",
819 true,
820 true,
821 true,
822 )
823 .unwrap();
824
825 let message = "Connection failed to 192.168.1.1:8080";
826 let redacted = rule.apply(message);
827 assert_eq!(redacted, "Connection failed to <ip>:8080");
828 }
829
830 #[test]
831 fn test_memory_address_redaction() {
832 let rule = RedactionRule::new(
833 "test_memory",
834 r"0x[0-9a-fA-F]+",
835 "<addr>",
836 true,
837 true,
838 false,
839 )
840 .unwrap();
841
842 let message = "Segfault at address 0x7fff5fbff000";
843 let redacted = rule.apply(message);
844 assert_eq!(redacted, "Segfault at address <addr>");
845 }
846
847 #[test]
848 fn test_api_key_redaction() {
849 let rule = RedactionRule::new(
850 "test_keys",
851 r#"(?i)(api_?key|token)[\s]*[:=][\s]*"?[a-zA-Z0-9\-_]{16,}"?"#,
852 "$1=<redacted>",
853 true,
854 true,
855 true,
856 )
857 .unwrap();
858
859 let message = r#"Authentication failed: api_key="sk_test_123456789abcdefghij""#;
860 let redacted = rule.apply(message);
861 assert!(redacted.contains("api_key=<redacted>"));
862 assert!(!redacted.contains("sk_test_123456789abcdefghij"));
863 }
864
865 #[test]
866 fn test_context_specific_sanitization() {
867 let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
868 mode: ErrorMode::Production,
869 ..SanitizerConfig::default()
870 });
871
872 let contexts = vec![
874 (ErrorContext::XmlParsing, "Invalid XML structure"),
875 (ErrorContext::XmlBuilding, "XML generation failed"),
876 (
877 ErrorContext::SecurityValidation,
878 "Security validation failed",
879 ),
880 (ErrorContext::Authentication, "Authentication failed"),
881 (ErrorContext::Authorization, "Access denied"),
882 ];
883
884 for (context, expected) in contexts {
885 let error = Error::new(
886 ErrorKind::InvalidInput,
887 "Detailed error message with /path/to/file.txt",
888 );
889 let sanitized = sanitizer.sanitize_io_error(error, context);
890 assert_eq!(sanitized.message, expected);
891 }
892 }
893
894 #[test]
895 fn test_message_length_truncation() {
896 let config = SanitizerConfig {
897 mode: ErrorMode::Testing, max_message_length: 20,
899 ..SanitizerConfig::default()
900 };
901
902 let mut sanitizer = ErrorSanitizer::with_config(config);
903 let long_error = Error::new(
904 ErrorKind::Other,
905 "This is a very long error message that should be truncated.",
906 );
907
908 let sanitized = sanitizer.sanitize_io_error(long_error, ErrorContext::FileRead);
909 assert!(sanitized.message.len() <= 20);
910 assert!(sanitized.message.ends_with("..."));
911 }
912
913 #[test]
914 fn test_correlation_id_generation() {
915 let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
916 generate_correlation_ids: true,
917 ..SanitizerConfig::default()
918 });
919
920 let error1 = Error::new(ErrorKind::NotFound, "Error 1");
921 let error2 = Error::new(ErrorKind::NotFound, "Error 2");
922
923 let sanitized1 = sanitizer.sanitize_io_error(error1, ErrorContext::FileOpen);
924 let sanitized2 = sanitizer.sanitize_io_error(error2, ErrorContext::FileOpen);
925
926 assert_ne!(sanitized1.correlation_id, sanitized2.correlation_id);
927 assert!(!sanitized1.correlation_id.is_empty());
928 assert!(!sanitized2.correlation_id.is_empty());
929 }
930
931 #[test]
932 fn test_error_codes() {
933 let sanitizer = ErrorSanitizer::new();
934 let stats = sanitizer.get_statistics();
935
936 assert_eq!(
937 stats.mode,
938 if cfg!(debug_assertions) {
939 ErrorMode::Development
940 } else {
941 ErrorMode::Production
942 }
943 );
944 assert!(stats.active_rules > 0);
945 assert_eq!(stats.stored_errors, 0);
946 }
947
948 #[test]
949 fn test_global_sanitizer() {
950 let error = Error::new(
951 ErrorKind::PermissionDenied,
952 "Access denied to /secret/file.txt",
953 );
954 let sanitized = sanitize_io_error(error, ErrorContext::FileRead);
955
956 assert!(!sanitized.correlation_id.is_empty());
957 assert!(!sanitized.message.is_empty());
958 assert!(sanitized.code.is_some());
959 }
960}