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