Skip to main content

memscope_rs/analysis/safety/
analyzer.rs

1use crate::analysis::safety::engine::RiskAssessmentEngine;
2use crate::analysis::safety::types::*;
3use crate::analysis::unsafe_ffi_tracker::{RiskLevel, SafetyViolation, StackFrame};
4use crate::capture::types::{AllocationInfo, TrackingError, TrackingResult};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10#[derive(Debug, Clone)]
11pub struct SafetyAnalysisConfig {
12    pub detailed_risk_assessment: bool,
13    pub enable_passport_tracking: bool,
14    pub min_risk_level: RiskLevel,
15    pub max_reports: usize,
16    pub enable_dynamic_violations: bool,
17    pub strict_mutex_handling: bool,
18    pub max_mutex_poison_retries: usize,
19}
20
21impl Default for SafetyAnalysisConfig {
22    fn default() -> Self {
23        Self {
24            detailed_risk_assessment: true,
25            enable_passport_tracking: true,
26            min_risk_level: RiskLevel::Low,
27            max_reports: 1000,
28            enable_dynamic_violations: true,
29            strict_mutex_handling: false,
30            max_mutex_poison_retries: 3,
31        }
32    }
33}
34
35#[derive(Debug, Clone, Default)]
36struct CircuitBreaker {
37    poison_count: usize,
38    last_poison_time: Option<u64>,
39    is_open: bool,
40}
41
42impl CircuitBreaker {
43    fn record_poison(&mut self, max_retries: usize) {
44        self.poison_count += 1;
45        self.last_poison_time = Some(get_current_timestamp());
46
47        if self.poison_count >= max_retries {
48            self.is_open = true;
49        }
50    }
51
52    fn is_tripped(&self) -> bool {
53        self.is_open
54    }
55
56    fn reset(&mut self) {
57        self.poison_count = 0;
58        self.last_poison_time = None;
59        self.is_open = false;
60    }
61
62    fn poison_count(&self) -> usize {
63        self.poison_count
64    }
65
66    #[allow(dead_code)]
67    fn last_poison_time(&self) -> Option<u64> {
68        self.last_poison_time
69    }
70}
71
72fn get_current_timestamp() -> u64 {
73    match SystemTime::now().duration_since(UNIX_EPOCH) {
74        Ok(duration) => duration.as_secs(),
75        Err(e) => {
76            tracing::error!(
77                "System clock error when getting timestamp: {}. Using 0 as timestamp.",
78                e
79            );
80            0
81        }
82    }
83}
84
85fn get_current_timestamp_nanos() -> u128 {
86    match SystemTime::now().duration_since(UNIX_EPOCH) {
87        Ok(duration) => duration.as_nanos(),
88        Err(e) => {
89            tracing::error!(
90                "System clock error when getting timestamp in nanos: {}. Using 0 as timestamp.",
91                e
92            );
93            0
94        }
95    }
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct SafetyAnalysisStats {
100    pub total_reports: usize,
101    pub reports_by_risk_level: HashMap<String, usize>,
102    pub total_passports: usize,
103    pub passports_by_status: HashMap<String, usize>,
104    pub dynamic_violations: usize,
105    pub analysis_start_time: u64,
106}
107
108pub struct SafetyAnalyzer {
109    unsafe_reports: Arc<Mutex<HashMap<String, UnsafeReport>>>,
110    memory_passports: Arc<Mutex<HashMap<usize, MemoryPassport>>>,
111    risk_engine: RiskAssessmentEngine,
112    config: SafetyAnalysisConfig,
113    stats: Arc<Mutex<SafetyAnalysisStats>>,
114    reports_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
115    passports_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
116    stats_circuit_breaker: Arc<Mutex<CircuitBreaker>>,
117}
118
119impl SafetyAnalyzer {
120    pub fn new(config: SafetyAnalysisConfig) -> Self {
121        tracing::info!("🔒 Initializing Safety Analyzer");
122        tracing::info!(
123            "   • Detailed risk assessment: {}",
124            config.detailed_risk_assessment
125        );
126        tracing::info!(
127            "   • Passport tracking: {}",
128            config.enable_passport_tracking
129        );
130        tracing::info!("   • Min risk level: {:?}", config.min_risk_level);
131
132        Self {
133            unsafe_reports: Arc::new(Mutex::new(HashMap::new())),
134            memory_passports: Arc::new(Mutex::new(HashMap::new())),
135            risk_engine: RiskAssessmentEngine::new(),
136            config,
137            stats: Arc::new(Mutex::new(SafetyAnalysisStats {
138                analysis_start_time: get_current_timestamp(),
139                ..Default::default()
140            })),
141            reports_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
142            passports_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
143            stats_circuit_breaker: Arc::new(Mutex::new(CircuitBreaker::default())),
144        }
145    }
146
147    fn lock_circuit_breaker<'a>(
148        breaker: &'a Arc<Mutex<CircuitBreaker>>,
149        name: &str,
150    ) -> TrackingResult<std::sync::MutexGuard<'a, CircuitBreaker>> {
151        breaker.lock().map_err(|e| {
152            let error_msg = format!("Mutex poisoned in {}: {}", name, e);
153            tracing::error!("{}", error_msg);
154            TrackingError::LockError(error_msg)
155        })
156    }
157
158    fn lock_reports(
159        &self,
160    ) -> TrackingResult<std::sync::MutexGuard<'_, HashMap<String, UnsafeReport>>> {
161        let circuit_breaker =
162            Self::lock_circuit_breaker(&self.reports_circuit_breaker, "reports_circuit_breaker")?;
163
164        if circuit_breaker.is_tripped() {
165            return Err(TrackingError::LockError(
166                "Circuit breaker tripped for unsafe_reports: too many mutex poison events"
167                    .to_string(),
168            ));
169        }
170
171        drop(circuit_breaker);
172
173        match self.unsafe_reports.lock() {
174            Ok(guard) => {
175                if let Ok(mut cb) = Self::lock_circuit_breaker(
176                    &self.reports_circuit_breaker,
177                    "reports_circuit_breaker",
178                ) {
179                    cb.reset();
180                }
181                Ok(guard)
182            }
183            Err(e) => {
184                let error_msg = format!("Mutex poisoned in unsafe_reports: {}", e);
185                tracing::error!("{}", error_msg);
186
187                if let Ok(mut circuit_breaker) = Self::lock_circuit_breaker(
188                    &self.reports_circuit_breaker,
189                    "reports_circuit_breaker",
190                ) {
191                    circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
192
193                    if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
194                        tracing::error!(
195                            "Circuit breaker tripped for unsafe_reports after {} poison events",
196                            circuit_breaker.poison_count()
197                        );
198                        return Err(TrackingError::LockError(error_msg));
199                    } else {
200                        tracing::warn!(
201                            "Recovering from mutex poison in unsafe_reports (attempt {}/{})",
202                            circuit_breaker.poison_count(),
203                            self.config.max_mutex_poison_retries
204                        );
205                    }
206                }
207
208                Ok(e.into_inner())
209            }
210        }
211    }
212
213    fn lock_passports(
214        &self,
215    ) -> TrackingResult<std::sync::MutexGuard<'_, HashMap<usize, MemoryPassport>>> {
216        let circuit_breaker = Self::lock_circuit_breaker(
217            &self.passports_circuit_breaker,
218            "passports_circuit_breaker",
219        )?;
220
221        if circuit_breaker.is_tripped() {
222            return Err(TrackingError::LockError(
223                "Circuit breaker tripped for memory_passports: too many mutex poison events"
224                    .to_string(),
225            ));
226        }
227
228        drop(circuit_breaker);
229
230        match self.memory_passports.lock() {
231            Ok(guard) => {
232                if let Ok(mut cb) = Self::lock_circuit_breaker(
233                    &self.passports_circuit_breaker,
234                    "passports_circuit_breaker",
235                ) {
236                    cb.reset();
237                }
238                Ok(guard)
239            }
240            Err(e) => {
241                let error_msg = format!("Mutex poisoned in memory_passports: {}", e);
242                tracing::error!("{}", error_msg);
243
244                if let Ok(mut circuit_breaker) = Self::lock_circuit_breaker(
245                    &self.passports_circuit_breaker,
246                    "passports_circuit_breaker",
247                ) {
248                    circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
249
250                    if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
251                        tracing::error!(
252                            "Circuit breaker tripped for memory_passports after {} poison events",
253                            circuit_breaker.poison_count()
254                        );
255                        return Err(TrackingError::LockError(error_msg));
256                    } else {
257                        tracing::warn!(
258                            "Recovering from mutex poison in memory_passports (attempt {}/{})",
259                            circuit_breaker.poison_count(),
260                            self.config.max_mutex_poison_retries
261                        );
262                    }
263                }
264
265                Ok(e.into_inner())
266            }
267        }
268    }
269
270    fn lock_stats(&self) -> TrackingResult<std::sync::MutexGuard<'_, SafetyAnalysisStats>> {
271        let circuit_breaker =
272            Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")?;
273
274        if circuit_breaker.is_tripped() {
275            return Err(TrackingError::LockError(
276                "Circuit breaker tripped for stats: too many mutex poison events".to_string(),
277            ));
278        }
279
280        drop(circuit_breaker);
281
282        match self.stats.lock() {
283            Ok(guard) => {
284                if let Ok(mut cb) =
285                    Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")
286                {
287                    cb.reset();
288                }
289                Ok(guard)
290            }
291            Err(e) => {
292                let error_msg = format!("Mutex poisoned in stats: {}", e);
293                tracing::error!("{}", error_msg);
294
295                if let Ok(mut circuit_breaker) =
296                    Self::lock_circuit_breaker(&self.stats_circuit_breaker, "stats_circuit_breaker")
297                {
298                    circuit_breaker.record_poison(self.config.max_mutex_poison_retries);
299
300                    if self.config.strict_mutex_handling || circuit_breaker.is_tripped() {
301                        tracing::error!(
302                            "Circuit breaker tripped for stats after {} poison events",
303                            circuit_breaker.poison_count()
304                        );
305                        return Err(TrackingError::LockError(error_msg));
306                    } else {
307                        tracing::warn!(
308                            "Recovering from mutex poison in stats (attempt {}/{})",
309                            circuit_breaker.poison_count(),
310                            self.config.max_mutex_poison_retries
311                        );
312                    }
313                }
314
315                Ok(e.into_inner())
316            }
317        }
318    }
319
320    pub fn generate_unsafe_report(
321        &self,
322        source: UnsafeSource,
323        allocations: &[AllocationInfo],
324        violations: &[SafetyViolation],
325    ) -> TrackingResult<String> {
326        let report_id = self.generate_report_id(&source);
327
328        tracing::info!("🔍 Generating unsafe report: {}", report_id);
329
330        let memory_context = self.create_memory_context(allocations);
331        let call_stack = self.capture_call_stack()?;
332
333        let risk_assessment = if self.config.detailed_risk_assessment {
334            self.risk_engine
335                .assess_risk(&source, &memory_context, &call_stack)
336        } else {
337            self.create_basic_risk_assessment(&source)
338        };
339
340        if !self.should_generate_report(&risk_assessment.risk_level) {
341            return Ok(report_id);
342        }
343
344        let dynamic_violations = self.convert_safety_violations(violations);
345
346        let related_passports = if self.config.enable_passport_tracking {
347            self.find_related_passports(&source, allocations)
348        } else {
349            Vec::new()
350        };
351
352        let report = UnsafeReport {
353            report_id: report_id.clone(),
354            source,
355            risk_assessment: risk_assessment.clone(),
356            dynamic_violations,
357            related_passports,
358            memory_context,
359            generated_at: get_current_timestamp(),
360        };
361
362        let mut reports = self.lock_reports()?;
363        if reports.len() >= self.config.max_reports {
364            if let Some(oldest_id) = reports.keys().next().cloned() {
365                reports.remove(&oldest_id);
366            }
367        }
368        reports.insert(report_id.clone(), report);
369
370        self.update_stats(&report_id, &risk_assessment.risk_level);
371
372        tracing::info!(
373            "✅ Generated unsafe report: {} (risk: {:?})",
374            report_id,
375            risk_assessment.risk_level
376        );
377
378        Ok(report_id)
379    }
380
381    pub fn create_memory_passport(
382        &self,
383        allocation_ptr: usize,
384        size_bytes: usize,
385        initial_event: PassportEventType,
386    ) -> TrackingResult<String> {
387        if !self.config.enable_passport_tracking {
388            return Ok(String::new());
389        }
390
391        let passport_id = format!(
392            "passport_{:x}_{}",
393            allocation_ptr,
394            get_current_timestamp_nanos()
395        );
396
397        let call_stack = self.capture_call_stack()?;
398        let current_time = get_current_timestamp();
399
400        let initial_passport_event = PassportEvent {
401            event_type: initial_event,
402            timestamp: current_time,
403            context: "SafetyAnalyzer".to_string(),
404            call_stack,
405            metadata: HashMap::new(),
406        };
407
408        let memory_context = MemoryContext {
409            total_allocated: size_bytes,
410            active_allocations: 1,
411            memory_pressure: MemoryPressureLevel::Low,
412            allocation_patterns: Vec::new(),
413        };
414
415        let source = UnsafeSource::RawPointer {
416            operation: "passport_creation".to_string(),
417            location: format!("0x{allocation_ptr:x}"),
418        };
419
420        let risk_assessment = self.risk_engine.assess_risk(&source, &memory_context, &[]);
421
422        let passport = MemoryPassport {
423            passport_id: passport_id.clone(),
424            allocation_ptr,
425            size_bytes,
426            status_at_shutdown: PassportStatus::Unknown,
427            lifecycle_events: vec![initial_passport_event],
428            risk_assessment,
429            created_at: current_time,
430            updated_at: current_time,
431        };
432
433        let mut passports = self.lock_passports()?;
434        passports.insert(allocation_ptr, passport);
435
436        let mut stats = self.lock_stats()?;
437        stats.total_passports += 1;
438
439        tracing::info!(
440            "📋 Created memory passport: {} for 0x{:x}",
441            passport_id,
442            allocation_ptr
443        );
444
445        Ok(passport_id)
446    }
447
448    pub fn record_passport_event(
449        &self,
450        allocation_ptr: usize,
451        event_type: PassportEventType,
452        context: String,
453    ) -> TrackingResult<()> {
454        if !self.config.enable_passport_tracking {
455            return Ok(());
456        }
457
458        let call_stack = self.capture_call_stack()?;
459        let current_time = get_current_timestamp();
460
461        let event = PassportEvent {
462            event_type,
463            timestamp: current_time,
464            context,
465            call_stack,
466            metadata: HashMap::new(),
467        };
468
469        let mut passports = self.lock_passports()?;
470        if let Some(passport) = passports.get_mut(&allocation_ptr) {
471            passport.lifecycle_events.push(event);
472            passport.updated_at = current_time;
473
474            tracing::info!("📝 Recorded passport event for 0x{:x}", allocation_ptr);
475        }
476
477        Ok(())
478    }
479
480    pub fn finalize_passports_at_shutdown(&self) -> Vec<String> {
481        let mut leaked_passports = Vec::new();
482
483        let mut passports = match self.lock_passports() {
484            Ok(guard) => guard,
485            Err(e) => {
486                tracing::error!("Failed to lock passports during finalization: {}", e);
487                return leaked_passports;
488            }
489        };
490
491        for (ptr, passport) in passports.iter_mut() {
492            let final_status = self.determine_final_passport_status(&passport.lifecycle_events);
493            passport.status_at_shutdown = final_status.clone();
494
495            if matches!(final_status, PassportStatus::InForeignCustody) {
496                leaked_passports.push(passport.passport_id.clone());
497                tracing::warn!(
498                    "🚨 Memory leak detected: passport {} (0x{:x}) in foreign custody",
499                    passport.passport_id,
500                    ptr
501                );
502            }
503        }
504
505        let status_counts: Vec<String> = passports
506            .values()
507            .map(|p| format!("{:?}", p.status_at_shutdown))
508            .collect();
509
510        drop(passports);
511
512        let mut stats = match self.lock_stats() {
513            Ok(guard) => guard,
514            Err(e) => {
515                tracing::error!("Failed to lock stats during finalization: {}", e);
516                return leaked_passports;
517            }
518        };
519        for status_key in status_counts {
520            *stats.passports_by_status.entry(status_key).or_insert(0) += 1;
521        }
522
523        tracing::info!(
524            "🏁 Finalized {} passports, {} leaks detected",
525            self.get_passport_count(),
526            leaked_passports.len()
527        );
528
529        leaked_passports
530    }
531
532    pub fn get_unsafe_reports(&self) -> HashMap<String, UnsafeReport> {
533        match self.lock_reports() {
534            Ok(guard) => guard.clone(),
535            Err(e) => {
536                tracing::error!("Failed to get unsafe reports: {}", e);
537                HashMap::new()
538            }
539        }
540    }
541
542    pub fn get_memory_passports(&self) -> HashMap<usize, MemoryPassport> {
543        match self.lock_passports() {
544            Ok(guard) => guard.clone(),
545            Err(e) => {
546                tracing::error!("Failed to get memory passports: {}", e);
547                HashMap::new()
548            }
549        }
550    }
551
552    pub fn get_stats(&self) -> SafetyAnalysisStats {
553        match self.lock_stats() {
554            Ok(guard) => guard.clone(),
555            Err(e) => {
556                tracing::error!("Failed to get stats: {}", e);
557                SafetyAnalysisStats::default()
558            }
559        }
560    }
561
562    fn generate_report_id(&self, source: &UnsafeSource) -> String {
563        let timestamp = get_current_timestamp_nanos();
564
565        let source_type = match source {
566            UnsafeSource::UnsafeBlock { .. } => "UB",
567            UnsafeSource::FfiFunction { .. } => "FFI",
568            UnsafeSource::RawPointer { .. } => "PTR",
569            UnsafeSource::Transmute { .. } => "TX",
570        };
571
572        format!("UNSAFE-{}-{}", source_type, timestamp % 1000000)
573    }
574
575    fn create_memory_context(&self, allocations: &[AllocationInfo]) -> MemoryContext {
576        let total_allocated = allocations.iter().map(|a| a.size).sum();
577        let active_allocations = allocations
578            .iter()
579            .filter(|a| a.timestamp_dealloc.is_none())
580            .count();
581
582        let memory_pressure = if total_allocated > 1024 * 1024 * 1024 {
583            MemoryPressureLevel::Critical
584        } else if total_allocated > 512 * 1024 * 1024 {
585            MemoryPressureLevel::High
586        } else if total_allocated > 256 * 1024 * 1024 {
587            MemoryPressureLevel::Medium
588        } else {
589            MemoryPressureLevel::Low
590        };
591
592        MemoryContext {
593            total_allocated,
594            active_allocations,
595            memory_pressure,
596            allocation_patterns: Vec::new(),
597        }
598    }
599
600    fn capture_call_stack(&self) -> TrackingResult<Vec<StackFrame>> {
601        Ok(vec![StackFrame {
602            function_name: "safety_analyzer".to_string(),
603            file_name: Some("src/analysis/safety_analyzer.rs".to_string()),
604            line_number: Some(1),
605            is_unsafe: false,
606        }])
607    }
608
609    fn create_basic_risk_assessment(&self, source: &UnsafeSource) -> RiskAssessment {
610        let (risk_level, risk_score) = match source {
611            UnsafeSource::UnsafeBlock { .. } => (RiskLevel::Medium, 50.0),
612            UnsafeSource::FfiFunction { .. } => (RiskLevel::Medium, 45.0),
613            UnsafeSource::RawPointer { .. } => (RiskLevel::High, 70.0),
614            UnsafeSource::Transmute { .. } => (RiskLevel::High, 65.0),
615        };
616
617        RiskAssessment {
618            risk_level,
619            risk_score,
620            risk_factors: Vec::new(),
621            confidence_score: 0.5,
622            mitigation_suggestions: vec!["Review unsafe operation for safety".to_string()],
623            assessment_timestamp: get_current_timestamp(),
624        }
625    }
626
627    fn should_generate_report(&self, risk_level: &RiskLevel) -> bool {
628        match (&self.config.min_risk_level, risk_level) {
629            (RiskLevel::Low, _) => true,
630            (RiskLevel::Medium, RiskLevel::Low) => false,
631            (RiskLevel::Medium, _) => true,
632            (RiskLevel::High, RiskLevel::Low | RiskLevel::Medium) => false,
633            (RiskLevel::High, _) => true,
634            (RiskLevel::Critical, RiskLevel::Critical) => true,
635            (RiskLevel::Critical, _) => false,
636        }
637    }
638
639    fn convert_safety_violations(&self, violations: &[SafetyViolation]) -> Vec<DynamicViolation> {
640        violations
641            .iter()
642            .map(|v| match v {
643                SafetyViolation::DoubleFree { timestamp, .. } => DynamicViolation {
644                    violation_type: ViolationType::DoubleFree,
645                    memory_address: 0,
646                    memory_size: 0,
647                    detected_at: (*timestamp as u64),
648                    call_stack: Vec::new(),
649                    severity: RiskLevel::Critical,
650                    context: "Double free detected: memory was freed twice".to_string(),
651                },
652                SafetyViolation::InvalidFree {
653                    attempted_pointer,
654                    timestamp,
655                    ..
656                } => DynamicViolation {
657                    violation_type: ViolationType::InvalidAccess,
658                    memory_address: *attempted_pointer,
659                    memory_size: 0,
660                    detected_at: (*timestamp as u64),
661                    call_stack: Vec::new(),
662                    severity: RiskLevel::High,
663                    context: format!(
664                        "Invalid free attempted at address 0x{:x}",
665                        attempted_pointer
666                    ),
667                },
668                SafetyViolation::PotentialLeak {
669                    allocation_timestamp,
670                    leak_detection_timestamp,
671                    ..
672                } => DynamicViolation {
673                    violation_type: ViolationType::MemoryLeak,
674                    memory_address: 0,
675                    memory_size: 0,
676                    detected_at: (*leak_detection_timestamp as u64),
677                    call_stack: Vec::new(),
678                    severity: RiskLevel::Medium,
679                    context: format!(
680                        "Potential memory leak detected (allocated at timestamp {})",
681                        allocation_timestamp
682                    ),
683                },
684                SafetyViolation::CrossBoundaryRisk {
685                    risk_level,
686                    description,
687                    ..
688                } => DynamicViolation {
689                    violation_type: ViolationType::FfiBoundaryViolation,
690                    memory_address: 0,
691                    memory_size: 0,
692                    detected_at: get_current_timestamp(),
693                    call_stack: Vec::new(),
694                    severity: risk_level.clone(),
695                    context: description.clone(),
696                },
697            })
698            .collect()
699    }
700
701    fn find_related_passports(
702        &self,
703        _source: &UnsafeSource,
704        _allocations: &[AllocationInfo],
705    ) -> Vec<String> {
706        Vec::new()
707    }
708
709    fn update_stats(&self, _report_id: &str, risk_level: &RiskLevel) {
710        match self.lock_stats() {
711            Ok(mut stats) => {
712                stats.total_reports += 1;
713                let risk_key = format!("{risk_level:?}");
714                *stats.reports_by_risk_level.entry(risk_key).or_insert(0) += 1;
715            }
716            Err(e) => {
717                tracing::error!("Failed to update stats: {}", e);
718            }
719        }
720    }
721
722    fn determine_final_passport_status(&self, events: &[PassportEvent]) -> PassportStatus {
723        let mut has_handover = false;
724        let mut has_reclaim = false;
725        let mut has_foreign_free = false;
726
727        for event in events {
728            match event.event_type {
729                PassportEventType::HandoverToFfi => has_handover = true,
730                PassportEventType::ReclaimedByRust => has_reclaim = true,
731                PassportEventType::FreedByForeign => has_foreign_free = true,
732                _ => {}
733            }
734        }
735
736        if has_handover && !has_reclaim && !has_foreign_free {
737            PassportStatus::InForeignCustody
738        } else if has_foreign_free {
739            PassportStatus::FreedByForeign
740        } else if has_reclaim {
741            PassportStatus::ReclaimedByRust
742        } else if has_handover {
743            PassportStatus::HandoverToFfi
744        } else {
745            PassportStatus::FreedByRust
746        }
747    }
748
749    fn get_passport_count(&self) -> usize {
750        self.memory_passports.lock().map(|p| p.len()).unwrap_or(0)
751    }
752}
753
754impl Default for SafetyAnalyzer {
755    fn default() -> Self {
756        Self::new(SafetyAnalysisConfig::default())
757    }
758}
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763
764    /// Objective: Verify SafetyAnalyzer creation with default config
765    /// Invariants: Default config should have detailed_risk_assessment enabled
766    #[test]
767    fn test_safety_analyzer_default() {
768        let analyzer = SafetyAnalyzer::default();
769        let stats = analyzer.get_stats();
770        assert_eq!(
771            stats.total_reports, 0,
772            "New analyzer should have zero reports"
773        );
774        assert_eq!(
775            stats.total_passports, 0,
776            "New analyzer should have zero passports"
777        );
778    }
779
780    /// Objective: Verify SafetyAnalyzer creation with custom config
781    /// Invariants: Custom config values should be respected
782    #[test]
783    fn test_safety_analyzer_custom_config() {
784        let config = SafetyAnalysisConfig {
785            detailed_risk_assessment: false,
786            enable_passport_tracking: false,
787            min_risk_level: RiskLevel::High,
788            max_reports: 100,
789            enable_dynamic_violations: false,
790            ..Default::default()
791        };
792        let analyzer = SafetyAnalyzer::new(config);
793        let stats = analyzer.get_stats();
794        assert_eq!(
795            stats.total_reports, 0,
796            "Custom config analyzer should start with zero reports"
797        );
798    }
799
800    /// Objective: Verify generate_unsafe_report for UnsafeBlock source
801    /// Invariants: Should generate report with correct source type
802    #[test]
803    fn test_generate_unsafe_report_unsafe_block() {
804        let analyzer = SafetyAnalyzer::default();
805        let source = UnsafeSource::UnsafeBlock {
806            location: "test.rs:10".to_string(),
807            function: "test_fn".to_string(),
808            file_path: Some("test.rs".to_string()),
809            line_number: Some(10),
810        };
811
812        let result = analyzer.generate_unsafe_report(source, &[], &[]);
813        assert!(result.is_ok(), "Should generate report successfully");
814        let report_id = result.unwrap();
815        assert!(
816            report_id.starts_with("UNSAFE-UB-"),
817            "Report ID should start with UNSAFE-UB-"
818        );
819    }
820
821    /// Objective: Verify generate_unsafe_report for FfiFunction source
822    /// Invariants: Should generate report with FFI prefix and correct FFI context
823    #[test]
824    fn test_generate_unsafe_report_ffi() {
825        let analyzer = SafetyAnalyzer::default();
826        let source = UnsafeSource::FfiFunction {
827            library: "libc".to_string(),
828            function: "malloc".to_string(),
829            call_site: "test.rs:20".to_string(),
830        };
831
832        let result = analyzer.generate_unsafe_report(source, &[], &[]);
833        assert!(result.is_ok(), "Should generate FFI report successfully");
834        let report_id = result.unwrap();
835        assert!(
836            report_id.starts_with("UNSAFE-FFI-"),
837            "FFI report ID should start with UNSAFE-FFI-"
838        );
839
840        let reports = analyzer.get_unsafe_reports();
841        let report = reports
842            .get(&report_id)
843            .expect("Report should exist in reports map");
844
845        match &report.source {
846            UnsafeSource::FfiFunction {
847                library,
848                function,
849                call_site,
850            } => {
851                assert_eq!(
852                    library, "libc",
853                    "FFI report should contain correct library name"
854                );
855                assert_eq!(
856                    function, "malloc",
857                    "FFI report should contain correct function name"
858                );
859                assert_eq!(
860                    call_site, "test.rs:20",
861                    "FFI report should contain correct call site"
862                );
863            }
864            _ => panic!("Report source should be FfiFunction variant"),
865        }
866    }
867
868    /// Objective: Verify generate_unsafe_report for RawPointer source
869    /// Invariants: Should generate report with PTR prefix
870    #[test]
871    fn test_generate_unsafe_report_raw_pointer() {
872        let analyzer = SafetyAnalyzer::default();
873        let source = UnsafeSource::RawPointer {
874            operation: "dereference".to_string(),
875            location: "0x1000".to_string(),
876        };
877
878        let result = analyzer.generate_unsafe_report(source, &[], &[]);
879        assert!(
880            result.is_ok(),
881            "Should generate raw pointer report successfully"
882        );
883        let report_id = result.unwrap();
884        assert!(
885            report_id.starts_with("UNSAFE-PTR-"),
886            "PTR report ID should start with UNSAFE-PTR-"
887        );
888    }
889
890    /// Objective: Verify generate_unsafe_report for Transmute source
891    /// Invariants: Should generate report with TX prefix
892    #[test]
893    fn test_generate_unsafe_report_transmute() {
894        let analyzer = SafetyAnalyzer::default();
895        let source = UnsafeSource::Transmute {
896            from_type: "u8".to_string(),
897            to_type: "i8".to_string(),
898            location: "test.rs:30".to_string(),
899        };
900
901        let result = analyzer.generate_unsafe_report(source, &[], &[]);
902        assert!(
903            result.is_ok(),
904            "Should generate transmute report successfully"
905        );
906        let report_id = result.unwrap();
907        assert!(
908            report_id.starts_with("UNSAFE-TX-"),
909            "TX report ID should start with UNSAFE-TX-"
910        );
911    }
912
913    /// Objective: Verify create_memory_passport functionality
914    /// Invariants: Should create passport with correct initial state
915    #[test]
916    fn test_create_memory_passport() {
917        let analyzer = SafetyAnalyzer::default();
918        let result =
919            analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
920        assert!(result.is_ok(), "Should create passport successfully");
921        let passport_id = result.unwrap();
922        assert!(
923            passport_id.starts_with("passport_"),
924            "Passport ID should start with passport_"
925        );
926
927        let stats = analyzer.get_stats();
928        assert_eq!(
929            stats.total_passports, 1,
930            "Should have one passport after creation"
931        );
932    }
933
934    /// Objective: Verify passport tracking disabled behavior
935    /// Invariants: Should return empty string when tracking disabled
936    #[test]
937    fn test_passport_tracking_disabled() {
938        let config = SafetyAnalysisConfig {
939            enable_passport_tracking: false,
940            ..Default::default()
941        };
942        let analyzer = SafetyAnalyzer::new(config);
943        let result =
944            analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
945        assert!(result.is_ok(), "Should return Ok even when disabled");
946        assert!(
947            result.unwrap().is_empty(),
948            "Should return empty string when disabled"
949        );
950    }
951
952    /// Objective: Verify record_passport_event functionality
953    /// Invariants: Should record event on existing passport
954    #[test]
955    fn test_record_passport_event() {
956        let analyzer = SafetyAnalyzer::default();
957        analyzer
958            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
959            .unwrap();
960
961        let result = analyzer.record_passport_event(
962            0x1000,
963            PassportEventType::HandoverToFfi,
964            "test_context".to_string(),
965        );
966        assert!(result.is_ok(), "Should record event successfully");
967
968        let passports = analyzer.get_memory_passports();
969        assert!(passports.contains_key(&0x1000), "Passport should exist");
970        let passport = passports.get(&0x1000).unwrap();
971        assert_eq!(passport.lifecycle_events.len(), 2, "Should have two events");
972    }
973
974    /// Objective: Verify finalize_passports_at_shutdown detects leaks
975    /// Invariants: Should detect passports in foreign custody
976    #[test]
977    fn test_finalize_passports_leak_detection() {
978        let analyzer = SafetyAnalyzer::default();
979        analyzer
980            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
981            .unwrap();
982        analyzer
983            .record_passport_event(
984                0x1000,
985                PassportEventType::HandoverToFfi,
986                "ffi_transfer".to_string(),
987            )
988            .unwrap();
989
990        let leaks = analyzer.finalize_passports_at_shutdown();
991        assert_eq!(
992            leaks.len(),
993            1,
994            "Should detect one leak for passport in foreign custody"
995        );
996    }
997
998    /// Objective: Verify finalize_passports_at_shutdown for freed passports
999    /// Invariants: Should not detect leaks for properly freed passports
1000    #[test]
1001    fn test_finalize_passports_no_leak() {
1002        let analyzer = SafetyAnalyzer::default();
1003        analyzer
1004            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1005            .unwrap();
1006        analyzer
1007            .record_passport_event(
1008                0x1000,
1009                PassportEventType::FreedByForeign,
1010                "freed".to_string(),
1011            )
1012            .unwrap();
1013
1014        let leaks = analyzer.finalize_passports_at_shutdown();
1015        assert!(
1016            leaks.is_empty(),
1017            "Should not detect leak for freed passport"
1018        );
1019    }
1020
1021    /// Objective: Verify get_unsafe_reports returns all reports
1022    /// Invariants: Should return all generated reports
1023    #[test]
1024    fn test_get_unsafe_reports() {
1025        let analyzer = SafetyAnalyzer::default();
1026        let source = UnsafeSource::UnsafeBlock {
1027            location: "test.rs".to_string(),
1028            function: "test".to_string(),
1029            file_path: None,
1030            line_number: None,
1031        };
1032        analyzer
1033            .generate_unsafe_report(source.clone(), &[], &[])
1034            .unwrap();
1035        analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1036
1037        let reports = analyzer.get_unsafe_reports();
1038        assert_eq!(reports.len(), 2, "Should have two reports");
1039    }
1040
1041    /// Objective: Verify min_risk_level filtering
1042    /// Invariants: Should not generate report below min risk level
1043    #[test]
1044    fn test_min_risk_level_filtering() {
1045        let config = SafetyAnalysisConfig {
1046            min_risk_level: RiskLevel::Critical,
1047            ..Default::default()
1048        };
1049        let analyzer = SafetyAnalyzer::new(config);
1050        let source = UnsafeSource::UnsafeBlock {
1051            location: "test.rs".to_string(),
1052            function: "test".to_string(),
1053            file_path: None,
1054            line_number: None,
1055        };
1056        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1057        assert!(result.is_ok(), "Should return Ok even when filtered");
1058    }
1059
1060    /// Objective: Verify stats update after report generation
1061    /// Invariants: Stats should reflect generated reports
1062    #[test]
1063    fn test_stats_update() {
1064        let analyzer = SafetyAnalyzer::default();
1065        let source = UnsafeSource::RawPointer {
1066            operation: "test".to_string(),
1067            location: "test".to_string(),
1068        };
1069        analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1070
1071        let stats = analyzer.get_stats();
1072        assert_eq!(stats.total_reports, 1, "Stats should show one report");
1073        assert!(
1074            !stats.reports_by_risk_level.is_empty(),
1075            "Should have risk level breakdown"
1076        );
1077    }
1078
1079    /// Objective: Verify max_reports limit enforcement
1080    /// Invariants: Should remove oldest report when limit exceeded
1081    #[test]
1082    fn test_max_reports_limit() {
1083        let config = SafetyAnalysisConfig {
1084            max_reports: 2,
1085            ..Default::default()
1086        };
1087        let analyzer = SafetyAnalyzer::new(config);
1088        let source = UnsafeSource::UnsafeBlock {
1089            location: "test.rs".to_string(),
1090            function: "test".to_string(),
1091            file_path: None,
1092            line_number: None,
1093        };
1094
1095        analyzer
1096            .generate_unsafe_report(source.clone(), &[], &[])
1097            .unwrap();
1098        analyzer
1099            .generate_unsafe_report(source.clone(), &[], &[])
1100            .unwrap();
1101        analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1102
1103        let reports = analyzer.get_unsafe_reports();
1104        assert!(reports.len() <= 2, "Should not exceed max_reports limit");
1105    }
1106
1107    /// Objective: Verify SafetyAnalysisConfig default values
1108    /// Invariants: Default should have sensible values
1109    #[test]
1110    fn test_safety_config_default() {
1111        let config = SafetyAnalysisConfig::default();
1112        assert!(
1113            config.detailed_risk_assessment,
1114            "Detailed risk assessment should be enabled"
1115        );
1116        assert!(
1117            config.enable_passport_tracking,
1118            "Passport tracking should be enabled"
1119        );
1120        assert_eq!(config.max_reports, 1000, "Max reports should be 1000");
1121    }
1122
1123    /// Objective: Verify RiskLevel ordering
1124    /// Invariants: Critical should be highest, Low should be lowest
1125    #[test]
1126    fn test_risk_level_ordering() {
1127        assert!(matches!(RiskLevel::Low, RiskLevel::Low));
1128        assert!(matches!(RiskLevel::Medium, RiskLevel::Medium));
1129        assert!(matches!(RiskLevel::High, RiskLevel::High));
1130        assert!(matches!(RiskLevel::Critical, RiskLevel::Critical));
1131    }
1132
1133    /// Objective: Verify PassportStatus variants
1134    /// Invariants: All variants should be distinct
1135    #[test]
1136    fn test_passport_status_variants() {
1137        let statuses = vec![
1138            PassportStatus::FreedByRust,
1139            PassportStatus::HandoverToFfi,
1140            PassportStatus::FreedByForeign,
1141            PassportStatus::ReclaimedByRust,
1142            PassportStatus::InForeignCustody,
1143            PassportStatus::Unknown,
1144        ];
1145
1146        for status in &statuses {
1147            let debug_str = format!("{status:?}");
1148            assert!(
1149                !debug_str.is_empty(),
1150                "Status should have debug representation"
1151            );
1152        }
1153    }
1154
1155    /// Objective: Verify PassportEventType variants
1156    /// Invariants: All event types should be distinct
1157    #[test]
1158    fn test_passport_event_type_variants() {
1159        let event_types = vec![
1160            PassportEventType::AllocatedInRust,
1161            PassportEventType::HandoverToFfi,
1162            PassportEventType::FreedByForeign,
1163            PassportEventType::ReclaimedByRust,
1164            PassportEventType::BoundaryAccess,
1165            PassportEventType::OwnershipTransfer,
1166        ];
1167
1168        for event_type in &event_types {
1169            let debug_str = format!("{event_type:?}");
1170            assert!(
1171                !debug_str.is_empty(),
1172                "Event type should have debug representation"
1173            );
1174        }
1175    }
1176
1177    /// Objective: Verify record_passport_event for non-existent passport
1178    /// Invariants: Should return Ok even when passport doesn't exist
1179    #[test]
1180    fn test_record_passport_event_non_existent() {
1181        let analyzer = SafetyAnalyzer::default();
1182        let result = analyzer.record_passport_event(
1183            0x9999,
1184            PassportEventType::HandoverToFfi,
1185            "test_context".to_string(),
1186        );
1187        assert!(
1188            result.is_ok(),
1189            "Should return Ok even for non-existent passport"
1190        );
1191
1192        let passports = analyzer.get_memory_passports();
1193        assert!(
1194            !passports.contains_key(&0x9999),
1195            "Non-existent passport should not be created"
1196        );
1197    }
1198
1199    /// Objective: Verify generate_unsafe_report with allocations
1200    /// Invariants: Should handle allocations correctly in memory context
1201    #[test]
1202    fn test_generate_unsafe_report_with_allocations() {
1203        let analyzer = SafetyAnalyzer::default();
1204        let source = UnsafeSource::RawPointer {
1205            operation: "test".to_string(),
1206            location: "test.rs".to_string(),
1207        };
1208
1209        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1210        assert!(
1211            result.is_ok(),
1212            "Should generate report with empty allocations"
1213        );
1214    }
1215
1216    /// Objective: Verify generate_unsafe_report with safety violations
1217    /// Invariants: Should convert violations to dynamic violations
1218    #[test]
1219    fn test_generate_unsafe_report_with_violations() {
1220        let analyzer = SafetyAnalyzer::default();
1221        let source = UnsafeSource::UnsafeBlock {
1222            location: "test.rs".to_string(),
1223            function: "test".to_string(),
1224            file_path: None,
1225            line_number: None,
1226        };
1227
1228        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1229        assert!(
1230            result.is_ok(),
1231            "Should generate report with empty violations"
1232        );
1233
1234        let report_id = result.unwrap();
1235        let reports = analyzer.get_unsafe_reports();
1236        let report = reports.get(&report_id).expect("Report should exist");
1237        assert_eq!(
1238            report.dynamic_violations.len(),
1239            0,
1240            "Should have no violations when empty"
1241        );
1242    }
1243
1244    /// Objective: Verify determine_final_passport_status for various scenarios
1245    /// Invariants: Should correctly determine passport status based on events
1246    #[test]
1247    fn test_determine_final_passport_status_scenarios() {
1248        let analyzer = SafetyAnalyzer::default();
1249
1250        let events_handover_only = vec![PassportEvent {
1251            event_type: PassportEventType::HandoverToFfi,
1252            timestamp: 1000,
1253            context: "test".to_string(),
1254            call_stack: vec![],
1255            metadata: HashMap::new(),
1256        }];
1257        let status = analyzer.determine_final_passport_status(&events_handover_only);
1258        assert!(
1259            matches!(status, PassportStatus::InForeignCustody),
1260            "Handover without reclaim or foreign free should be InForeignCustody"
1261        );
1262
1263        let events_reclaimed = vec![
1264            PassportEvent {
1265                event_type: PassportEventType::HandoverToFfi,
1266                timestamp: 1000,
1267                context: "test".to_string(),
1268                call_stack: vec![],
1269                metadata: HashMap::new(),
1270            },
1271            PassportEvent {
1272                event_type: PassportEventType::ReclaimedByRust,
1273                timestamp: 2000,
1274                context: "test".to_string(),
1275                call_stack: vec![],
1276                metadata: HashMap::new(),
1277            },
1278        ];
1279        let status = analyzer.determine_final_passport_status(&events_reclaimed);
1280        assert!(
1281            matches!(status, PassportStatus::ReclaimedByRust),
1282            "Reclaimed after handover should be ReclaimedByRust"
1283        );
1284
1285        let events_freed_by_foreign = vec![
1286            PassportEvent {
1287                event_type: PassportEventType::HandoverToFfi,
1288                timestamp: 1000,
1289                context: "test".to_string(),
1290                call_stack: vec![],
1291                metadata: HashMap::new(),
1292            },
1293            PassportEvent {
1294                event_type: PassportEventType::FreedByForeign,
1295                timestamp: 2000,
1296                context: "test".to_string(),
1297                call_stack: vec![],
1298                metadata: HashMap::new(),
1299            },
1300        ];
1301        let status = analyzer.determine_final_passport_status(&events_freed_by_foreign);
1302        assert!(
1303            matches!(status, PassportStatus::FreedByForeign),
1304            "Freed by foreign should be FreedByForeign"
1305        );
1306
1307        let events_no_handover = vec![PassportEvent {
1308            event_type: PassportEventType::AllocatedInRust,
1309            timestamp: 1000,
1310            context: "test".to_string(),
1311            call_stack: vec![],
1312            metadata: HashMap::new(),
1313        }];
1314        let status = analyzer.determine_final_passport_status(&events_no_handover);
1315        assert!(
1316            matches!(status, PassportStatus::FreedByRust),
1317            "No handover should be FreedByRust"
1318        );
1319    }
1320
1321    /// Objective: Verify create_basic_risk_assessment functionality
1322    /// Invariants: Should create basic assessment when detailed_risk_assessment is disabled
1323    #[test]
1324    fn test_create_basic_risk_assessment() {
1325        let config = SafetyAnalysisConfig {
1326            detailed_risk_assessment: false,
1327            ..Default::default()
1328        };
1329        let analyzer = SafetyAnalyzer::new(config);
1330
1331        let source = UnsafeSource::RawPointer {
1332            operation: "test".to_string(),
1333            location: "test.rs".to_string(),
1334        };
1335        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1336        assert!(
1337            result.is_ok(),
1338            "Should generate report with basic assessment"
1339        );
1340
1341        let report_id = result.unwrap();
1342        let reports = analyzer.get_unsafe_reports();
1343        let report = reports.get(&report_id).expect("Report should exist");
1344        assert!(
1345            report.risk_assessment.risk_score > 0.0,
1346            "Basic assessment should have risk score"
1347        );
1348    }
1349
1350    /// Objective: Verify should_generate_report filtering logic
1351    /// Invariants: Should filter reports based on min_risk_level
1352    #[test]
1353    fn test_should_generate_report_filtering() {
1354        let config = SafetyAnalysisConfig {
1355            min_risk_level: RiskLevel::High,
1356            ..Default::default()
1357        };
1358        let analyzer = SafetyAnalyzer::new(config);
1359
1360        let source = UnsafeSource::UnsafeBlock {
1361            location: "test.rs".to_string(),
1362            function: "test".to_string(),
1363            file_path: None,
1364            line_number: None,
1365        };
1366        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1367        assert!(result.is_ok(), "Should return Ok even when filtered");
1368
1369        let reports = analyzer.get_unsafe_reports();
1370        assert!(
1371            reports.is_empty(),
1372            "Report should be filtered out when below min risk level"
1373        );
1374    }
1375
1376    /// Objective: Verify memory pressure level calculation
1377    /// Invariants: Should correctly calculate memory pressure based on allocations
1378    #[test]
1379    fn test_memory_pressure_levels() {
1380        let analyzer = SafetyAnalyzer::default();
1381
1382        let source = UnsafeSource::UnsafeBlock {
1383            location: "test.rs".to_string(),
1384            function: "test".to_string(),
1385            file_path: None,
1386            line_number: None,
1387        };
1388        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1389        assert!(result.is_ok(), "Should handle empty allocations");
1390    }
1391
1392    /// Objective: Verify passport tracking with multiple events
1393    /// Invariants: Should correctly track multiple passport events
1394    #[test]
1395    fn test_passport_multiple_events() {
1396        let analyzer = SafetyAnalyzer::default();
1397
1398        analyzer
1399            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1400            .unwrap();
1401
1402        analyzer
1403            .record_passport_event(
1404                0x1000,
1405                PassportEventType::HandoverToFfi,
1406                "transfer_to_ffi".to_string(),
1407            )
1408            .unwrap();
1409
1410        analyzer
1411            .record_passport_event(
1412                0x1000,
1413                PassportEventType::BoundaryAccess,
1414                "ffi_access".to_string(),
1415            )
1416            .unwrap();
1417
1418        analyzer
1419            .record_passport_event(
1420                0x1000,
1421                PassportEventType::ReclaimedByRust,
1422                "reclaimed".to_string(),
1423            )
1424            .unwrap();
1425
1426        let passports = analyzer.get_memory_passports();
1427        let passport = passports.get(&0x1000).expect("Passport should exist");
1428        assert_eq!(
1429            passport.lifecycle_events.len(),
1430            4,
1431            "Should have four events"
1432        );
1433    }
1434
1435    /// Objective: Verify finalize_passports_at_shutdown with mixed statuses
1436    /// Invariants: Should correctly categorize passports by final status
1437    #[test]
1438    fn test_finalize_passports_mixed_statuses() {
1439        let analyzer = SafetyAnalyzer::default();
1440
1441        analyzer
1442            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1443            .unwrap();
1444        analyzer
1445            .record_passport_event(
1446                0x1000,
1447                PassportEventType::HandoverToFfi,
1448                "leaked".to_string(),
1449            )
1450            .unwrap();
1451
1452        analyzer
1453            .create_memory_passport(0x2000, 2048, PassportEventType::AllocatedInRust)
1454            .unwrap();
1455        analyzer
1456            .record_passport_event(
1457                0x2000,
1458                PassportEventType::FreedByForeign,
1459                "freed".to_string(),
1460            )
1461            .unwrap();
1462
1463        let leaks = analyzer.finalize_passports_at_shutdown();
1464        assert_eq!(leaks.len(), 1, "Should detect one leak");
1465
1466        let stats = analyzer.get_stats();
1467        assert!(
1468            stats.passports_by_status.contains_key("InForeignCustody"),
1469            "Stats should include InForeignCustody status"
1470        );
1471        assert!(
1472            stats.passports_by_status.contains_key("FreedByForeign"),
1473            "Stats should include FreedByForeign status"
1474        );
1475    }
1476
1477    /// Objective: Verify enable_dynamic_violations configuration
1478    /// Invariants: Should respect dynamic_violations config setting
1479    #[test]
1480    fn test_enable_dynamic_violations_config() {
1481        let config = SafetyAnalysisConfig {
1482            enable_dynamic_violations: false,
1483            ..Default::default()
1484        };
1485        let analyzer = SafetyAnalyzer::new(config);
1486
1487        let source = UnsafeSource::UnsafeBlock {
1488            location: "test.rs".to_string(),
1489            function: "test".to_string(),
1490            file_path: None,
1491            line_number: None,
1492        };
1493        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1494        assert!(
1495            result.is_ok(),
1496            "Should generate report with dynamic violations disabled"
1497        );
1498    }
1499
1500    /// Objective: Verify all UnsafeSource variants generate unique report IDs
1501    /// Invariants: Each source type should generate distinct report ID prefix
1502    #[test]
1503    fn test_all_unsafe_source_variants() {
1504        let analyzer = SafetyAnalyzer::default();
1505
1506        let sources = vec![
1507            UnsafeSource::UnsafeBlock {
1508                location: "test.rs".to_string(),
1509                function: "test".to_string(),
1510                file_path: None,
1511                line_number: None,
1512            },
1513            UnsafeSource::FfiFunction {
1514                library: "libc".to_string(),
1515                function: "malloc".to_string(),
1516                call_site: "test.rs".to_string(),
1517            },
1518            UnsafeSource::RawPointer {
1519                operation: "test".to_string(),
1520                location: "test.rs".to_string(),
1521            },
1522            UnsafeSource::Transmute {
1523                from_type: "u8".to_string(),
1524                to_type: "i8".to_string(),
1525                location: "test.rs".to_string(),
1526            },
1527        ];
1528
1529        let mut report_ids = Vec::new();
1530        for source in sources {
1531            let result = analyzer.generate_unsafe_report(source, &[], &[]);
1532            assert!(
1533                result.is_ok(),
1534                "Should generate report for all source types"
1535            );
1536            report_ids.push(result.unwrap());
1537        }
1538
1539        assert_eq!(report_ids.len(), 4, "Should have generated 4 reports");
1540    }
1541
1542    /// Objective: Verify determine_final_passport_status with conflicting events
1543    /// Invariants: Should handle reclaim + foreign_free scenario correctly
1544    #[test]
1545    fn test_determine_final_passport_status_conflicting_events() {
1546        let analyzer = SafetyAnalyzer::default();
1547
1548        let events_conflict = vec![
1549            PassportEvent {
1550                event_type: PassportEventType::HandoverToFfi,
1551                timestamp: 1000,
1552                context: "test".to_string(),
1553                call_stack: vec![],
1554                metadata: HashMap::new(),
1555            },
1556            PassportEvent {
1557                event_type: PassportEventType::ReclaimedByRust,
1558                timestamp: 2000,
1559                context: "test".to_string(),
1560                call_stack: vec![],
1561                metadata: HashMap::new(),
1562            },
1563            PassportEvent {
1564                event_type: PassportEventType::FreedByForeign,
1565                timestamp: 3000,
1566                context: "test".to_string(),
1567                call_stack: vec![],
1568                metadata: HashMap::new(),
1569            },
1570        ];
1571        let status = analyzer.determine_final_passport_status(&events_conflict);
1572        assert!(
1573            matches!(status, PassportStatus::FreedByForeign),
1574            "When both reclaim and foreign_free exist, should prioritize foreign_free"
1575        );
1576    }
1577
1578    /// Objective: Verify passport creation with zero size
1579    /// Invariants: Should handle zero size allocation gracefully
1580    #[test]
1581    fn test_create_memory_passport_zero_size() {
1582        let analyzer = SafetyAnalyzer::default();
1583        let result = analyzer.create_memory_passport(0x1000, 0, PassportEventType::AllocatedInRust);
1584        assert!(result.is_ok(), "Should create passport with zero size");
1585
1586        let passports = analyzer.get_memory_passports();
1587        let passport = passports.get(&0x1000).expect("Passport should exist");
1588        assert_eq!(passport.size_bytes, 0, "Passport should have zero size");
1589    }
1590
1591    /// Objective: Verify passport creation with null pointer
1592    /// Invariants: Should handle null pointer (0x0) allocation
1593    #[test]
1594    fn test_create_memory_passport_null_pointer() {
1595        let analyzer = SafetyAnalyzer::default();
1596        let result = analyzer.create_memory_passport(0x0, 1024, PassportEventType::AllocatedInRust);
1597        assert!(result.is_ok(), "Should create passport with null pointer");
1598
1599        let passports = analyzer.get_memory_passports();
1600        assert!(
1601            passports.contains_key(&0x0),
1602            "Passport with null pointer should exist"
1603        );
1604    }
1605
1606    /// Objective: Verify multiple passports with same pointer (potential bug)
1607    /// Invariants: Should overwrite previous passport with same pointer
1608    #[test]
1609    fn test_create_memory_passport_duplicate_pointer() {
1610        let analyzer = SafetyAnalyzer::default();
1611
1612        analyzer
1613            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1614            .unwrap();
1615
1616        analyzer
1617            .create_memory_passport(0x1000, 2048, PassportEventType::AllocatedInRust)
1618            .unwrap();
1619
1620        let passports = analyzer.get_memory_passports();
1621        assert_eq!(
1622            passports.len(),
1623            1,
1624            "Duplicate pointer should overwrite previous passport"
1625        );
1626
1627        let passport = passports.get(&0x1000).expect("Passport should exist");
1628        assert_eq!(
1629            passport.size_bytes, 2048,
1630            "Should have size from second creation"
1631        );
1632
1633        let stats = analyzer.get_stats();
1634        assert_eq!(
1635            stats.total_passports, 2,
1636            "Stats should count both creation attempts"
1637        );
1638    }
1639
1640    /// Objective: Verify max_reports limit with exact boundary
1641    /// Invariants: Should handle exactly max_reports count
1642    #[test]
1643    fn test_max_reports_exact_boundary() {
1644        let config = SafetyAnalysisConfig {
1645            max_reports: 3,
1646            ..Default::default()
1647        };
1648        let analyzer = SafetyAnalyzer::new(config);
1649        let source = UnsafeSource::UnsafeBlock {
1650            location: "test.rs".to_string(),
1651            function: "test".to_string(),
1652            file_path: None,
1653            line_number: None,
1654        };
1655
1656        for _ in 0..3 {
1657            analyzer
1658                .generate_unsafe_report(source.clone(), &[], &[])
1659                .unwrap();
1660        }
1661
1662        let reports = analyzer.get_unsafe_reports();
1663        assert_eq!(reports.len(), 3, "Should have exactly max_reports count");
1664
1665        analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1666
1667        let reports = analyzer.get_unsafe_reports();
1668        assert!(
1669            reports.len() <= 3,
1670            "Should not exceed max_reports after adding one more"
1671        );
1672    }
1673
1674    /// Objective: Verify report generation with all risk levels using basic assessment
1675    /// Invariants: Should correctly filter based on min_risk_level when using basic assessment
1676    #[test]
1677    fn test_risk_level_filtering_comprehensive() {
1678        let test_cases = vec![
1679            (RiskLevel::Low, 4),
1680            (RiskLevel::Medium, 4),
1681            (RiskLevel::High, 2),
1682            (RiskLevel::Critical, 0),
1683        ];
1684
1685        for (min_level, expected_count) in test_cases {
1686            let config = SafetyAnalysisConfig {
1687                min_risk_level: min_level.clone(),
1688                detailed_risk_assessment: false,
1689                ..Default::default()
1690            };
1691            let analyzer = SafetyAnalyzer::new(config);
1692
1693            let sources = vec![
1694                UnsafeSource::UnsafeBlock {
1695                    location: "test.rs".to_string(),
1696                    function: "test".to_string(),
1697                    file_path: None,
1698                    line_number: None,
1699                },
1700                UnsafeSource::FfiFunction {
1701                    library: "libc".to_string(),
1702                    function: "malloc".to_string(),
1703                    call_site: "test.rs".to_string(),
1704                },
1705                UnsafeSource::RawPointer {
1706                    operation: "test".to_string(),
1707                    location: "test.rs".to_string(),
1708                },
1709                UnsafeSource::Transmute {
1710                    from_type: "u8".to_string(),
1711                    to_type: "i8".to_string(),
1712                    location: "test.rs".to_string(),
1713                },
1714            ];
1715
1716            for source in sources.into_iter() {
1717                analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1718            }
1719
1720            let reports = analyzer.get_unsafe_reports();
1721            let actual_count = reports.len();
1722
1723            assert_eq!(
1724                actual_count, expected_count,
1725                "For min_level {:?}, expected {} reports but got {}",
1726                min_level, expected_count, actual_count
1727            );
1728        }
1729    }
1730
1731    /// Objective: Verify risk assessment engine behavior with no matching factors
1732    /// Invariants: Should assign Medium risk when no risk factors match (conservative approach)
1733    #[test]
1734    fn test_risk_assessment_no_matching_factors() {
1735        let config = SafetyAnalysisConfig {
1736            detailed_risk_assessment: true,
1737            min_risk_level: RiskLevel::Low,
1738            ..Default::default()
1739        };
1740        let analyzer = SafetyAnalyzer::new(config);
1741
1742        let source = UnsafeSource::UnsafeBlock {
1743            location: "safe_location.rs".to_string(),
1744            function: "safe_function".to_string(),
1745            file_path: None,
1746            line_number: None,
1747        };
1748
1749        let result = analyzer.generate_unsafe_report(source, &[], &[]);
1750        assert!(result.is_ok(), "Should generate report");
1751
1752        let reports = analyzer.get_unsafe_reports();
1753        assert_eq!(reports.len(), 1, "Should have one report");
1754
1755        let report = reports.values().next().expect("Report should exist");
1756        assert!(
1757            matches!(report.risk_assessment.risk_level, RiskLevel::Low),
1758            "Risk level should be Low when no risk factors match (empty risk factors indicate low risk)"
1759        );
1760    }
1761
1762    /// Objective: Verify passport event recording with empty context
1763    /// Invariants: Should handle empty context string
1764    #[test]
1765    fn test_record_passport_event_empty_context() {
1766        let analyzer = SafetyAnalyzer::default();
1767
1768        analyzer
1769            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1770            .unwrap();
1771
1772        let result =
1773            analyzer.record_passport_event(0x1000, PassportEventType::HandoverToFfi, String::new());
1774        assert!(result.is_ok(), "Should record event with empty context");
1775
1776        let passports = analyzer.get_memory_passports();
1777        let passport = passports.get(&0x1000).expect("Passport should exist");
1778        assert_eq!(passport.lifecycle_events.len(), 2, "Should have two events");
1779    }
1780
1781    /// Objective: Verify stats consistency after multiple operations
1782    /// Invariants: Stats should accurately reflect all operations
1783    #[test]
1784    fn test_stats_consistency() {
1785        let analyzer = SafetyAnalyzer::default();
1786
1787        let initial_stats = analyzer.get_stats();
1788        assert_eq!(initial_stats.total_reports, 0);
1789        assert_eq!(initial_stats.total_passports, 0);
1790
1791        analyzer
1792            .generate_unsafe_report(
1793                UnsafeSource::UnsafeBlock {
1794                    location: "test.rs".to_string(),
1795                    function: "test".to_string(),
1796                    file_path: None,
1797                    line_number: None,
1798                },
1799                &[],
1800                &[],
1801            )
1802            .unwrap();
1803
1804        analyzer
1805            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1806            .unwrap();
1807
1808        let stats = analyzer.get_stats();
1809        assert_eq!(stats.total_reports, 1, "Should have 1 report");
1810        assert_eq!(stats.total_passports, 1, "Should have 1 passport");
1811        assert!(
1812            !stats.reports_by_risk_level.is_empty(),
1813            "Should have risk level breakdown"
1814        );
1815    }
1816
1817    /// Objective: Verify passport lifecycle with all event types
1818    /// Invariants: Should handle all PassportEventType variants
1819    #[test]
1820    fn test_passport_lifecycle_all_event_types() {
1821        let analyzer = SafetyAnalyzer::default();
1822
1823        analyzer
1824            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
1825            .unwrap();
1826
1827        let event_types = vec![
1828            PassportEventType::HandoverToFfi,
1829            PassportEventType::BoundaryAccess,
1830            PassportEventType::OwnershipTransfer,
1831            PassportEventType::ReclaimedByRust,
1832        ];
1833
1834        for event_type in event_types {
1835            let event_type_str = format!("{:?}", event_type);
1836            let result = analyzer.record_passport_event(0x1000, event_type, "test".to_string());
1837            assert!(
1838                result.is_ok(),
1839                "Should record event type {}",
1840                event_type_str
1841            );
1842        }
1843
1844        let passports = analyzer.get_memory_passports();
1845        let passport = passports.get(&0x1000).expect("Passport should exist");
1846        assert_eq!(
1847            passport.lifecycle_events.len(),
1848            5,
1849            "Should have initial event plus 4 recorded events"
1850        );
1851    }
1852
1853    /// Objective: Verify report ID uniqueness with rapid generation
1854    /// Invariants: Each report should have unique ID even when generated rapidly
1855    #[test]
1856    fn test_report_id_uniqueness() {
1857        let analyzer = SafetyAnalyzer::default();
1858        let source = UnsafeSource::UnsafeBlock {
1859            location: "test.rs".to_string(),
1860            function: "test".to_string(),
1861            file_path: None,
1862            line_number: None,
1863        };
1864
1865        let mut report_ids = std::collections::HashSet::new();
1866        for _ in 0..100 {
1867            let report_id = analyzer
1868                .generate_unsafe_report(source.clone(), &[], &[])
1869                .unwrap();
1870            assert!(report_ids.insert(report_id), "Report ID should be unique");
1871        }
1872
1873        assert_eq!(report_ids.len(), 100, "Should have 100 unique report IDs");
1874    }
1875
1876    /// Objective: Verify finalize_passports_at_shutdown with empty state
1877    /// Invariants: Should handle empty passport map gracefully
1878    #[test]
1879    fn test_finalize_passports_empty_state() {
1880        let analyzer = SafetyAnalyzer::default();
1881        let leaks = analyzer.finalize_passports_at_shutdown();
1882        assert!(leaks.is_empty(), "Should have no leaks with empty state");
1883
1884        let stats = analyzer.get_stats();
1885        assert!(
1886            stats.passports_by_status.is_empty(),
1887            "Should have no passport status stats"
1888        );
1889    }
1890
1891    /// Objective: Verify should_generate_report for all risk level combinations
1892    /// Invariants: Should correctly filter based on all possible combinations
1893    #[test]
1894    fn test_should_generate_report_all_combinations() {
1895        let test_cases = vec![
1896            (RiskLevel::Low, RiskLevel::Low, true),
1897            (RiskLevel::Low, RiskLevel::Medium, true),
1898            (RiskLevel::Low, RiskLevel::High, true),
1899            (RiskLevel::Low, RiskLevel::Critical, true),
1900            (RiskLevel::Medium, RiskLevel::Low, false),
1901            (RiskLevel::Medium, RiskLevel::Medium, true),
1902            (RiskLevel::Medium, RiskLevel::High, true),
1903            (RiskLevel::Medium, RiskLevel::Critical, true),
1904            (RiskLevel::High, RiskLevel::Low, false),
1905            (RiskLevel::High, RiskLevel::Medium, false),
1906            (RiskLevel::High, RiskLevel::High, true),
1907            (RiskLevel::High, RiskLevel::Critical, true),
1908            (RiskLevel::Critical, RiskLevel::Low, false),
1909            (RiskLevel::Critical, RiskLevel::Medium, false),
1910            (RiskLevel::Critical, RiskLevel::High, false),
1911            (RiskLevel::Critical, RiskLevel::Critical, true),
1912        ];
1913
1914        for (min_level, report_level, expected) in test_cases {
1915            let config = SafetyAnalysisConfig {
1916                min_risk_level: min_level.clone(),
1917                ..Default::default()
1918            };
1919            let analyzer = SafetyAnalyzer::new(config);
1920            let result = analyzer.should_generate_report(&report_level);
1921            assert_eq!(
1922                result, expected,
1923                "should_generate_report({:?}, {:?}) should be {}",
1924                min_level, report_level, expected
1925            );
1926        }
1927    }
1928
1929    /// Objective: Verify create_basic_risk_assessment for all source types
1930    /// Invariants: Each source type should have appropriate risk level and score
1931    #[test]
1932    fn test_create_basic_risk_assessment_all_sources() {
1933        let config = SafetyAnalysisConfig {
1934            detailed_risk_assessment: false,
1935            ..Default::default()
1936        };
1937        let analyzer = SafetyAnalyzer::new(config);
1938
1939        let test_cases = vec![
1940            (
1941                UnsafeSource::UnsafeBlock {
1942                    location: "test.rs".to_string(),
1943                    function: "test".to_string(),
1944                    file_path: None,
1945                    line_number: None,
1946                },
1947                RiskLevel::Medium,
1948                50.0,
1949            ),
1950            (
1951                UnsafeSource::FfiFunction {
1952                    library: "libc".to_string(),
1953                    function: "malloc".to_string(),
1954                    call_site: "test.rs".to_string(),
1955                },
1956                RiskLevel::Medium,
1957                45.0,
1958            ),
1959            (
1960                UnsafeSource::RawPointer {
1961                    operation: "dereference".to_string(),
1962                    location: "test.rs".to_string(),
1963                },
1964                RiskLevel::High,
1965                70.0,
1966            ),
1967            (
1968                UnsafeSource::Transmute {
1969                    from_type: "u8".to_string(),
1970                    to_type: "i8".to_string(),
1971                    location: "test.rs".to_string(),
1972                },
1973                RiskLevel::High,
1974                65.0,
1975            ),
1976        ];
1977
1978        for (source, expected_level, expected_score) in test_cases {
1979            let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
1980            let reports = analyzer.get_unsafe_reports();
1981            let report = reports.get(&report_id).expect("Report should exist");
1982
1983            assert_eq!(
1984                report.risk_assessment.risk_level, expected_level,
1985                "Risk level should match for source"
1986            );
1987            assert_eq!(
1988                report.risk_assessment.risk_score, expected_score,
1989                "Risk score should match for source"
1990            );
1991        }
1992    }
1993
1994    /// Objective: Verify report generation with multiple reports at max limit
1995    /// Invariants: Should correctly handle max_reports boundary
1996    #[test]
1997    fn test_max_reports_overflow() {
1998        let config = SafetyAnalysisConfig {
1999            max_reports: 5,
2000            ..Default::default()
2001        };
2002        let analyzer = SafetyAnalyzer::new(config);
2003        let source = UnsafeSource::UnsafeBlock {
2004            location: "test.rs".to_string(),
2005            function: "test".to_string(),
2006            file_path: None,
2007            line_number: None,
2008        };
2009
2010        for i in 0..10 {
2011            let result = analyzer.generate_unsafe_report(source.clone(), &[], &[]);
2012            assert!(result.is_ok(), "Should generate report {}", i);
2013        }
2014
2015        let reports = analyzer.get_unsafe_reports();
2016        assert!(reports.len() <= 5, "Should not exceed max_reports limit");
2017    }
2018
2019    /// Objective: Verify passport creation with very large pointer
2020    /// Invariants: Should handle large pointer values
2021    #[test]
2022    fn test_create_memory_passport_large_pointer() {
2023        let analyzer = SafetyAnalyzer::default();
2024        let large_ptr = usize::MAX;
2025        let result =
2026            analyzer.create_memory_passport(large_ptr, 1024, PassportEventType::AllocatedInRust);
2027        assert!(result.is_ok(), "Should create passport with large pointer");
2028
2029        let passports = analyzer.get_memory_passports();
2030        assert!(
2031            passports.contains_key(&large_ptr),
2032            "Passport with large pointer should exist"
2033        );
2034    }
2035
2036    /// Objective: Verify passport creation with very large size
2037    /// Invariants: Should handle large size values
2038    #[test]
2039    fn test_create_memory_passport_large_size() {
2040        let analyzer = SafetyAnalyzer::default();
2041        let large_size = usize::MAX;
2042        let result =
2043            analyzer.create_memory_passport(0x1000, large_size, PassportEventType::AllocatedInRust);
2044        assert!(result.is_ok(), "Should create passport with large size");
2045
2046        let passports = analyzer.get_memory_passports();
2047        let passport = passports.get(&0x1000).expect("Passport should exist");
2048        assert_eq!(
2049            passport.size_bytes, large_size,
2050            "Passport should have large size"
2051        );
2052    }
2053
2054    /// Objective: Verify multiple passport events in sequence
2055    /// Invariants: Should correctly track all events in order
2056    #[test]
2057    fn test_passport_event_sequence() {
2058        let analyzer = SafetyAnalyzer::default();
2059
2060        analyzer
2061            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2062            .unwrap();
2063
2064        let events = vec![
2065            PassportEventType::BoundaryAccess,
2066            PassportEventType::OwnershipTransfer,
2067            PassportEventType::HandoverToFfi,
2068            PassportEventType::BoundaryAccess,
2069            PassportEventType::FreedByForeign,
2070        ];
2071
2072        for event_type in events {
2073            analyzer
2074                .record_passport_event(0x1000, event_type, "test".to_string())
2075                .unwrap();
2076        }
2077
2078        let passports = analyzer.get_memory_passports();
2079        let passport = passports.get(&0x1000).expect("Passport should exist");
2080        assert_eq!(
2081            passport.lifecycle_events.len(),
2082            6,
2083            "Should have initial event plus 5 recorded events"
2084        );
2085    }
2086
2087    /// Objective: Verify generate_report_id format for all source types
2088    /// Invariants: Report ID should have correct prefix for each source type
2089    #[test]
2090    fn test_generate_report_id_format() {
2091        let analyzer = SafetyAnalyzer::default();
2092
2093        let sources = vec![
2094            (
2095                UnsafeSource::UnsafeBlock {
2096                    location: "test.rs".to_string(),
2097                    function: "test".to_string(),
2098                    file_path: None,
2099                    line_number: None,
2100                },
2101                "UNSAFE-UB-",
2102            ),
2103            (
2104                UnsafeSource::FfiFunction {
2105                    library: "libc".to_string(),
2106                    function: "malloc".to_string(),
2107                    call_site: "test.rs".to_string(),
2108                },
2109                "UNSAFE-FFI-",
2110            ),
2111            (
2112                UnsafeSource::RawPointer {
2113                    operation: "test".to_string(),
2114                    location: "test.rs".to_string(),
2115                },
2116                "UNSAFE-PTR-",
2117            ),
2118            (
2119                UnsafeSource::Transmute {
2120                    from_type: "u8".to_string(),
2121                    to_type: "i8".to_string(),
2122                    location: "test.rs".to_string(),
2123                },
2124                "UNSAFE-TX-",
2125            ),
2126        ];
2127
2128        for (source, expected_prefix) in sources {
2129            let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2130            assert!(
2131                report_id.starts_with(expected_prefix),
2132                "Report ID should start with {}",
2133                expected_prefix
2134            );
2135        }
2136    }
2137
2138    /// Objective: Verify stats update for different risk levels
2139    /// Invariants: Stats should correctly track reports by risk level
2140    #[test]
2141    fn test_stats_by_risk_level() {
2142        let analyzer = SafetyAnalyzer::default();
2143
2144        let sources = vec![
2145            UnsafeSource::RawPointer {
2146                operation: "test".to_string(),
2147                location: "test.rs".to_string(),
2148            },
2149            UnsafeSource::Transmute {
2150                from_type: "u8".to_string(),
2151                to_type: "i8".to_string(),
2152                location: "test.rs".to_string(),
2153            },
2154            UnsafeSource::UnsafeBlock {
2155                location: "test.rs".to_string(),
2156                function: "test".to_string(),
2157                file_path: None,
2158                line_number: None,
2159            },
2160        ];
2161
2162        for source in sources {
2163            analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2164        }
2165
2166        let stats = analyzer.get_stats();
2167        assert_eq!(stats.total_reports, 3, "Should have 3 reports");
2168        assert!(
2169            stats.reports_by_risk_level.contains_key("Low"),
2170            "Should have Low risk level reports"
2171        );
2172    }
2173
2174    /// Objective: Verify passport status determination for edge cases
2175    /// Invariants: Should correctly determine status for edge case event combinations
2176    #[test]
2177    fn test_determine_final_passport_status_edge_cases() {
2178        let analyzer = SafetyAnalyzer::default();
2179
2180        let events_only_reclaim = vec![PassportEvent {
2181            event_type: PassportEventType::ReclaimedByRust,
2182            timestamp: 1000,
2183            context: "test".to_string(),
2184            call_stack: vec![],
2185            metadata: HashMap::new(),
2186        }];
2187        let status = analyzer.determine_final_passport_status(&events_only_reclaim);
2188        assert!(
2189            matches!(status, PassportStatus::ReclaimedByRust),
2190            "Only reclaim event should result in ReclaimedByRust"
2191        );
2192
2193        let events_only_foreign_free = vec![PassportEvent {
2194            event_type: PassportEventType::FreedByForeign,
2195            timestamp: 1000,
2196            context: "test".to_string(),
2197            call_stack: vec![],
2198            metadata: HashMap::new(),
2199        }];
2200        let status = analyzer.determine_final_passport_status(&events_only_foreign_free);
2201        assert!(
2202            matches!(status, PassportStatus::FreedByForeign),
2203            "Only foreign free event should result in FreedByForeign"
2204        );
2205    }
2206
2207    /// Objective: Verify analyzer with all config options disabled
2208    /// Invariants: Should handle disabled features gracefully
2209    #[test]
2210    fn test_analyzer_all_features_disabled() {
2211        let config = SafetyAnalysisConfig {
2212            detailed_risk_assessment: false,
2213            enable_passport_tracking: false,
2214            min_risk_level: RiskLevel::Low,
2215            max_reports: 10,
2216            enable_dynamic_violations: false,
2217            ..Default::default()
2218        };
2219        let analyzer = SafetyAnalyzer::new(config);
2220
2221        let source = UnsafeSource::UnsafeBlock {
2222            location: "test.rs".to_string(),
2223            function: "test".to_string(),
2224            file_path: None,
2225            line_number: None,
2226        };
2227
2228        let result = analyzer.generate_unsafe_report(source, &[], &[]);
2229        assert!(
2230            result.is_ok(),
2231            "Should generate report with all features disabled"
2232        );
2233
2234        let passport_result =
2235            analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2236        assert!(
2237            passport_result.is_ok(),
2238            "Should return Ok when passport tracking disabled"
2239        );
2240        assert!(
2241            passport_result.unwrap().is_empty(),
2242            "Should return empty string when passport tracking disabled"
2243        );
2244    }
2245
2246    /// Objective: Verify report source information preservation
2247    /// Invariants: Report should preserve all source information
2248    #[test]
2249    fn test_report_source_preservation() {
2250        let analyzer = SafetyAnalyzer::default();
2251
2252        let source = UnsafeSource::UnsafeBlock {
2253            location: "src/test.rs:42".to_string(),
2254            function: "test_function".to_string(),
2255            file_path: Some("src/test.rs".to_string()),
2256            line_number: Some(42),
2257        };
2258
2259        let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2260        let reports = analyzer.get_unsafe_reports();
2261        let report = reports.get(&report_id).expect("Report should exist");
2262
2263        match &report.source {
2264            UnsafeSource::UnsafeBlock {
2265                location,
2266                function,
2267                file_path,
2268                line_number,
2269            } => {
2270                assert_eq!(location, "src/test.rs:42");
2271                assert_eq!(function, "test_function");
2272                assert_eq!(file_path, &Some("src/test.rs".to_string()));
2273                assert_eq!(line_number, &Some(42));
2274            }
2275            _ => panic!("Report source should be UnsafeBlock"),
2276        }
2277    }
2278
2279    /// Objective: Verify strict mutex handling mode returns errors
2280    /// Invariants: When strict_mutex_handling is enabled, mutex poison should propagate errors
2281    #[test]
2282    fn test_strict_mutex_handling_mode() {
2283        let config = SafetyAnalysisConfig {
2284            strict_mutex_handling: true,
2285            ..Default::default()
2286        };
2287        let analyzer = SafetyAnalyzer::new(config);
2288
2289        let source = UnsafeSource::UnsafeBlock {
2290            location: "test.rs".to_string(),
2291            function: "test".to_string(),
2292            file_path: None,
2293            line_number: None,
2294        };
2295
2296        let result = analyzer.generate_unsafe_report(source, &[], &[]);
2297        assert!(
2298            result.is_ok(),
2299            "Should generate report successfully in normal case"
2300        );
2301
2302        let passport_result =
2303            analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2304        assert!(
2305            passport_result.is_ok(),
2306            "Should create passport successfully in normal case"
2307        );
2308    }
2309
2310    /// Objective: Verify lenient mutex handling mode recovers gracefully
2311    /// Invariants: When strict_mutex_handling is disabled, mutex poison should recover data
2312    #[test]
2313    fn test_lenient_mutex_handling_mode() {
2314        let config = SafetyAnalysisConfig {
2315            strict_mutex_handling: false,
2316            ..Default::default()
2317        };
2318        let analyzer = SafetyAnalyzer::new(config);
2319
2320        let source = UnsafeSource::UnsafeBlock {
2321            location: "test.rs".to_string(),
2322            function: "test".to_string(),
2323            file_path: None,
2324            line_number: None,
2325        };
2326
2327        let result = analyzer.generate_unsafe_report(source, &[], &[]);
2328        assert!(
2329            result.is_ok(),
2330            "Should generate report successfully in lenient mode"
2331        );
2332
2333        let passport_result =
2334            analyzer.create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust);
2335        assert!(
2336            passport_result.is_ok(),
2337            "Should create passport successfully in lenient mode"
2338        );
2339    }
2340
2341    /// Objective: Verify config option for strict mutex handling
2342    /// Invariants: Config should correctly control mutex handling behavior
2343    #[test]
2344    fn test_mutex_handling_config_option() {
2345        let strict_config = SafetyAnalysisConfig {
2346            strict_mutex_handling: true,
2347            ..Default::default()
2348        };
2349        let strict_analyzer = SafetyAnalyzer::new(strict_config);
2350        assert!(
2351            strict_analyzer.config.strict_mutex_handling,
2352            "Strict mode should be enabled"
2353        );
2354
2355        let lenient_config = SafetyAnalysisConfig {
2356            strict_mutex_handling: false,
2357            ..Default::default()
2358        };
2359        let lenient_analyzer = SafetyAnalyzer::new(lenient_config);
2360        assert!(
2361            !lenient_analyzer.config.strict_mutex_handling,
2362            "Strict mode should be disabled"
2363        );
2364    }
2365
2366    /// Objective: Verify error handling in getter methods
2367    /// Invariants: Getter methods should handle mutex errors gracefully
2368    #[test]
2369    fn test_getter_methods_error_handling() {
2370        let analyzer = SafetyAnalyzer::default();
2371
2372        let reports = analyzer.get_unsafe_reports();
2373        assert!(reports.is_empty(), "Should return empty map on success");
2374
2375        let passports = analyzer.get_memory_passports();
2376        assert!(passports.is_empty(), "Should return empty map on success");
2377
2378        let stats = analyzer.get_stats();
2379        assert_eq!(
2380            stats.total_reports, 0,
2381            "Should return default stats on success"
2382        );
2383    }
2384
2385    /// Objective: Verify convert_safety_violations handles DoubleFree correctly
2386    /// Invariants: DoubleFree should convert to DynamicViolation with Critical severity
2387    #[test]
2388    fn test_convert_safety_violation_double_free() {
2389        use crate::core::CallStackRef;
2390
2391        let analyzer = SafetyAnalyzer::default();
2392
2393        let call_stack = CallStackRef::new(1, Some(1));
2394        let violations = vec![SafetyViolation::DoubleFree {
2395            first_free_stack: call_stack.clone(),
2396            second_free_stack: call_stack.clone(),
2397            timestamp: 1000,
2398        }];
2399
2400        let source = UnsafeSource::RawPointer {
2401            operation: "test".to_string(),
2402            location: "test.rs".to_string(),
2403        };
2404
2405        let report_id = analyzer
2406            .generate_unsafe_report(source, &[], &violations)
2407            .unwrap();
2408        let reports = analyzer.get_unsafe_reports();
2409        let report = reports.get(&report_id).expect("Report should exist");
2410
2411        assert_eq!(
2412            report.dynamic_violations.len(),
2413            1,
2414            "Should have one dynamic violation"
2415        );
2416        let dv = &report.dynamic_violations[0];
2417        assert!(
2418            matches!(dv.violation_type, ViolationType::DoubleFree),
2419            "Violation type should be DoubleFree"
2420        );
2421        assert!(
2422            matches!(dv.severity, RiskLevel::Critical),
2423            "DoubleFree should have Critical severity"
2424        );
2425    }
2426
2427    /// Objective: Verify convert_safety_violations handles InvalidFree correctly
2428    /// Invariants: InvalidFree should convert to DynamicViolation with High severity
2429    #[test]
2430    fn test_convert_safety_violation_invalid_free() {
2431        use crate::core::CallStackRef;
2432
2433        let analyzer = SafetyAnalyzer::default();
2434
2435        let call_stack = CallStackRef::new(2, Some(1));
2436        let violations = vec![SafetyViolation::InvalidFree {
2437            attempted_pointer: 0x2000,
2438            stack: call_stack,
2439            timestamp: 2000,
2440        }];
2441
2442        let source = UnsafeSource::RawPointer {
2443            operation: "test".to_string(),
2444            location: "test.rs".to_string(),
2445        };
2446
2447        let report_id = analyzer
2448            .generate_unsafe_report(source, &[], &violations)
2449            .unwrap();
2450        let reports = analyzer.get_unsafe_reports();
2451        let report = reports.get(&report_id).expect("Report should exist");
2452
2453        let dv = &report.dynamic_violations[0];
2454        assert!(
2455            matches!(dv.violation_type, ViolationType::InvalidAccess),
2456            "Violation type should be InvalidAccess"
2457        );
2458        assert_eq!(
2459            dv.memory_address, 0x2000,
2460            "Memory address should match attempted pointer"
2461        );
2462        assert!(
2463            matches!(dv.severity, RiskLevel::High),
2464            "InvalidFree should have High severity"
2465        );
2466    }
2467
2468    /// Objective: Verify convert_safety_violations handles PotentialLeak correctly
2469    /// Invariants: PotentialLeak should convert to DynamicViolation with Medium severity
2470    #[test]
2471    fn test_convert_safety_violation_potential_leak() {
2472        use crate::core::CallStackRef;
2473
2474        let analyzer = SafetyAnalyzer::default();
2475
2476        let call_stack = CallStackRef::new(3, Some(1));
2477        let violations = vec![SafetyViolation::PotentialLeak {
2478            allocation_stack: call_stack,
2479            allocation_timestamp: 1000,
2480            leak_detection_timestamp: 5000,
2481        }];
2482
2483        let source = UnsafeSource::RawPointer {
2484            operation: "test".to_string(),
2485            location: "test.rs".to_string(),
2486        };
2487
2488        let report_id = analyzer
2489            .generate_unsafe_report(source, &[], &violations)
2490            .unwrap();
2491        let reports = analyzer.get_unsafe_reports();
2492        let report = reports.get(&report_id).expect("Report should exist");
2493
2494        let dv = &report.dynamic_violations[0];
2495        assert!(
2496            matches!(dv.violation_type, ViolationType::MemoryLeak),
2497            "Violation type should be MemoryLeak"
2498        );
2499        assert!(
2500            matches!(dv.severity, RiskLevel::Medium),
2501            "PotentialLeak should have Medium severity"
2502        );
2503        assert_eq!(
2504            dv.detected_at, 5000,
2505            "Detected at should match leak_detection_timestamp"
2506        );
2507    }
2508
2509    /// Objective: Verify convert_safety_violations handles CrossBoundaryRisk correctly
2510    /// Invariants: CrossBoundaryRisk should preserve risk level from original violation
2511    #[test]
2512    fn test_convert_safety_violation_cross_boundary() {
2513        use crate::core::CallStackRef;
2514
2515        let analyzer = SafetyAnalyzer::default();
2516
2517        let call_stack = CallStackRef::new(4, Some(1));
2518        let violations = vec![SafetyViolation::CrossBoundaryRisk {
2519            risk_level: RiskLevel::High,
2520            description: "FFI boundary violation".to_string(),
2521            stack: call_stack,
2522        }];
2523
2524        let source = UnsafeSource::FfiFunction {
2525            library: "libc".to_string(),
2526            function: "malloc".to_string(),
2527            call_site: "test.rs".to_string(),
2528        };
2529
2530        let report_id = analyzer
2531            .generate_unsafe_report(source, &[], &violations)
2532            .unwrap();
2533        let reports = analyzer.get_unsafe_reports();
2534        let report = reports.get(&report_id).expect("Report should exist");
2535
2536        let dv = &report.dynamic_violations[0];
2537        assert!(
2538            matches!(dv.violation_type, ViolationType::FfiBoundaryViolation),
2539            "Violation type should be FfiBoundaryViolation"
2540        );
2541        assert!(
2542            matches!(dv.severity, RiskLevel::High),
2543            "Severity should match original risk level"
2544        );
2545        assert_eq!(
2546            dv.context, "FFI boundary violation",
2547            "Context should match description"
2548        );
2549    }
2550
2551    /// Objective: Verify convert_safety_violations handles multiple violations
2552    /// Invariants: All violations should be converted correctly
2553    #[test]
2554    fn test_convert_multiple_safety_violations() {
2555        use crate::core::CallStackRef;
2556
2557        let analyzer = SafetyAnalyzer::default();
2558
2559        let call_stack = CallStackRef::new(5, Some(1));
2560        let violations = vec![
2561            SafetyViolation::DoubleFree {
2562                first_free_stack: call_stack.clone(),
2563                second_free_stack: call_stack.clone(),
2564                timestamp: 1000,
2565            },
2566            SafetyViolation::InvalidFree {
2567                attempted_pointer: 0x2000,
2568                stack: call_stack.clone(),
2569                timestamp: 2000,
2570            },
2571            SafetyViolation::PotentialLeak {
2572                allocation_stack: call_stack,
2573                allocation_timestamp: 1000,
2574                leak_detection_timestamp: 5000,
2575            },
2576        ];
2577
2578        let source = UnsafeSource::UnsafeBlock {
2579            location: "test.rs".to_string(),
2580            function: "test".to_string(),
2581            file_path: None,
2582            line_number: None,
2583        };
2584
2585        let report_id = analyzer
2586            .generate_unsafe_report(source, &[], &violations)
2587            .unwrap();
2588        let reports = analyzer.get_unsafe_reports();
2589        let report = reports.get(&report_id).expect("Report should exist");
2590
2591        assert_eq!(
2592            report.dynamic_violations.len(),
2593            3,
2594            "Should have three dynamic violations"
2595        );
2596    }
2597
2598    /// Objective: Verify memory context creation with empty allocations
2599    /// Invariants: Memory context should handle empty allocations correctly
2600    #[test]
2601    fn test_create_memory_context_empty() {
2602        let analyzer = SafetyAnalyzer::default();
2603
2604        let source = UnsafeSource::RawPointer {
2605            operation: "test".to_string(),
2606            location: "test.rs".to_string(),
2607        };
2608
2609        let report_id = analyzer.generate_unsafe_report(source, &[], &[]).unwrap();
2610        let reports = analyzer.get_unsafe_reports();
2611        let report = reports.get(&report_id).expect("Report should exist");
2612
2613        assert_eq!(
2614            report.memory_context.total_allocated, 0,
2615            "Total allocated should be 0 for empty allocations"
2616        );
2617        assert_eq!(
2618            report.memory_context.active_allocations, 0,
2619            "Active allocations should be 0 for empty allocations"
2620        );
2621    }
2622
2623    /// Objective: Verify CircuitBreaker trip behavior
2624    /// Invariants: CircuitBreaker should trip after max_retries poison events
2625    #[test]
2626    fn test_circuit_breaker_trip_threshold() {
2627        let mut breaker = CircuitBreaker::default();
2628
2629        assert!(!breaker.is_tripped(), "Should not be tripped initially");
2630
2631        breaker.record_poison(3);
2632        assert_eq!(breaker.poison_count(), 1);
2633        assert!(!breaker.is_tripped(), "Should not trip after 1 event");
2634
2635        breaker.record_poison(3);
2636        assert_eq!(breaker.poison_count(), 2);
2637        assert!(!breaker.is_tripped(), "Should not trip after 2 events");
2638
2639        breaker.record_poison(3);
2640        assert_eq!(breaker.poison_count(), 3);
2641        assert!(
2642            breaker.is_tripped(),
2643            "Should trip after reaching max_retries"
2644        );
2645    }
2646
2647    /// Objective: Verify CircuitBreaker reset functionality
2648    /// Invariants: Reset should clear all state
2649    #[test]
2650    fn test_circuit_breaker_reset() {
2651        let mut breaker = CircuitBreaker::default();
2652
2653        breaker.record_poison(3);
2654        breaker.record_poison(3);
2655        breaker.record_poison(3);
2656
2657        assert!(breaker.is_tripped(), "Should be tripped");
2658
2659        breaker.reset();
2660
2661        assert!(!breaker.is_tripped(), "Should not be tripped after reset");
2662        assert_eq!(breaker.poison_count(), 0, "Poison count should be 0");
2663        assert!(
2664            breaker.last_poison_time().is_none(),
2665            "Last poison time should be None"
2666        );
2667    }
2668
2669    /// Objective: Verify CircuitBreaker with different max_retries values
2670    /// Invariants: Should respect different threshold values
2671    #[test]
2672    fn test_circuit_breaker_different_thresholds() {
2673        let mut breaker1 = CircuitBreaker::default();
2674        breaker1.record_poison(1);
2675        assert!(
2676            breaker1.is_tripped(),
2677            "Should trip immediately with max_retries=1"
2678        );
2679
2680        let mut breaker5 = CircuitBreaker::default();
2681        for _ in 0..4 {
2682            breaker5.record_poison(5);
2683        }
2684        assert!(
2685            !breaker5.is_tripped(),
2686            "Should not trip before reaching threshold"
2687        );
2688        breaker5.record_poison(5);
2689        assert!(breaker5.is_tripped(), "Should trip at exactly max_retries");
2690    }
2691
2692    /// Objective: Verify get_current_timestamp returns valid value
2693    /// Invariants: Timestamp should be positive and reasonable
2694    #[test]
2695    fn test_get_current_timestamp() {
2696        let ts = get_current_timestamp();
2697        assert!(ts > 0, "Timestamp should be positive");
2698        assert!(
2699            ts > 1700000000,
2700            "Timestamp should be after 2023 (reasonable value)"
2701        );
2702    }
2703
2704    /// Objective: Verify get_current_timestamp_nanos returns valid value
2705    /// Invariants: Nanos timestamp should be greater than seconds timestamp
2706    #[test]
2707    fn test_get_current_timestamp_nanos() {
2708        let ts_nanos = get_current_timestamp_nanos();
2709        let ts_secs = get_current_timestamp();
2710
2711        assert!(ts_nanos > 0, "Nanos timestamp should be positive");
2712        assert!(
2713            ts_nanos >= ts_secs as u128,
2714            "Nanos should be >= seconds timestamp"
2715        );
2716    }
2717
2718    /// Objective: Verify record_passport_event when tracking is disabled
2719    /// Invariants: Should return Ok(()) immediately without modifying state
2720    #[test]
2721    fn test_record_passport_event_tracking_disabled() {
2722        let config = SafetyAnalysisConfig {
2723            enable_passport_tracking: false,
2724            ..Default::default()
2725        };
2726        let analyzer = SafetyAnalyzer::new(config);
2727
2728        let result = analyzer.record_passport_event(
2729            0x1000,
2730            PassportEventType::HandoverToFfi,
2731            "test".to_string(),
2732        );
2733
2734        assert!(result.is_ok(), "Should return Ok when tracking disabled");
2735    }
2736
2737    /// Objective: Verify generate_unsafe_report with passport tracking enabled
2738    /// Invariants: Should handle passport tracking correctly
2739    #[test]
2740    fn test_generate_report_with_passport_tracking() {
2741        let analyzer = SafetyAnalyzer::default();
2742
2743        analyzer
2744            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2745            .unwrap();
2746
2747        let source = UnsafeSource::RawPointer {
2748            operation: "test".to_string(),
2749            location: "test.rs".to_string(),
2750        };
2751
2752        let result = analyzer.generate_unsafe_report(source, &[], &[]);
2753        assert!(
2754            result.is_ok(),
2755            "Should generate report with passport tracking"
2756        );
2757    }
2758
2759    /// Objective: Verify SafetyAnalysisStats serialization
2760    /// Invariants: Stats should serialize and deserialize correctly
2761    #[test]
2762    fn test_safety_analysis_stats_serialization() {
2763        let stats = SafetyAnalysisStats {
2764            total_reports: 10,
2765            reports_by_risk_level: HashMap::from([("Low".to_string(), 5), ("High".to_string(), 5)]),
2766            total_passports: 3,
2767            passports_by_status: HashMap::from([("Active".to_string(), 3)]),
2768            dynamic_violations: 2,
2769            analysis_start_time: 1000,
2770        };
2771
2772        let json = serde_json::to_string(&stats).expect("Should serialize");
2773        let deserialized: SafetyAnalysisStats =
2774            serde_json::from_str(&json).expect("Should deserialize");
2775
2776        assert_eq!(deserialized.total_reports, 10, "Total reports should match");
2777        assert_eq!(
2778            deserialized.total_passports, 3,
2779            "Total passports should match"
2780        );
2781    }
2782
2783    /// Objective: Verify SafetyAnalysisConfig clone functionality
2784    /// Invariants: Cloned config should have identical values
2785    #[test]
2786    fn test_safety_config_clone() {
2787        let config = SafetyAnalysisConfig {
2788            detailed_risk_assessment: false,
2789            enable_passport_tracking: false,
2790            min_risk_level: RiskLevel::High,
2791            max_reports: 500,
2792            enable_dynamic_violations: false,
2793            strict_mutex_handling: true,
2794            max_mutex_poison_retries: 5,
2795        };
2796
2797        let cloned = config.clone();
2798
2799        assert!(
2800            !cloned.detailed_risk_assessment,
2801            "Cloned detailed_risk_assessment should match"
2802        );
2803        assert!(
2804            !cloned.enable_passport_tracking,
2805            "Cloned enable_passport_tracking should match"
2806        );
2807        assert_eq!(cloned.max_reports, 500, "Cloned max_reports should match");
2808        assert_eq!(
2809            cloned.max_mutex_poison_retries, 5,
2810            "Cloned max_mutex_poison_retries should match"
2811        );
2812    }
2813
2814    /// Objective: Verify finalize_passports_at_shutdown handles lock failure
2815    /// Invariants: Should return empty vec when lock fails (graceful degradation)
2816    #[test]
2817    fn test_finalize_passports_graceful_degradation() {
2818        let analyzer = SafetyAnalyzer::default();
2819
2820        analyzer
2821            .create_memory_passport(0x1000, 1024, PassportEventType::AllocatedInRust)
2822            .unwrap();
2823
2824        let leaks = analyzer.finalize_passports_at_shutdown();
2825        assert!(
2826            leaks.is_empty(),
2827            "Should have no leaks when passport is not in foreign custody"
2828        );
2829    }
2830}