1#![warn(missing_docs)]
32#![warn(clippy::all)]
33#![deny(unsafe_code)]
34
35pub mod diff_test;
36pub mod lock_verify;
37
38use anyhow::{Context, Result};
39use syn::{visit::Visit, Block, Expr, ExprUnsafe, ItemFn};
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct UnsafeBlock {
44 pub line: usize,
46 pub confidence: u8,
48 pub pattern: UnsafePattern,
50 pub suggestion: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum UnsafePattern {
57 RawPointerDeref,
59 Transmute,
61 Assembly,
63 FfiCall,
65 UnionAccess,
67 MutableStatic,
69 Other,
71}
72
73#[derive(Debug, Clone)]
75pub struct UnsafeAuditReport {
76 pub total_lines: usize,
78 pub unsafe_lines: usize,
80 pub unsafe_density_percent: f64,
82 pub unsafe_blocks: Vec<UnsafeBlock>,
84 pub average_confidence: f64,
86}
87
88impl UnsafeAuditReport {
89 pub fn new(total_lines: usize, unsafe_lines: usize, unsafe_blocks: Vec<UnsafeBlock>) -> Self {
91 let unsafe_density_percent =
92 if total_lines > 0 { (unsafe_lines as f64 / total_lines as f64) * 100.0 } else { 0.0 };
93
94 let average_confidence = if !unsafe_blocks.is_empty() {
95 unsafe_blocks.iter().map(|b| b.confidence as f64).sum::<f64>()
96 / unsafe_blocks.len() as f64
97 } else {
98 0.0
99 };
100
101 Self {
102 total_lines,
103 unsafe_lines,
104 unsafe_density_percent,
105 unsafe_blocks,
106 average_confidence,
107 }
108 }
109
110 pub fn meets_density_target(&self) -> bool {
112 self.unsafe_density_percent < 5.0
113 }
114
115 pub fn high_confidence_blocks(&self) -> Vec<&UnsafeBlock> {
117 self.unsafe_blocks.iter().filter(|b| b.confidence >= 70).collect()
118 }
119}
120
121pub struct UnsafeAuditor {
123 unsafe_blocks: Vec<UnsafeBlock>,
124 total_lines: usize,
125 unsafe_lines: usize,
126 source_code: String,
127}
128
129impl UnsafeAuditor {
130 pub fn new() -> Self {
132 Self {
133 unsafe_blocks: Vec::new(),
134 total_lines: 0,
135 unsafe_lines: 0,
136 source_code: String::new(),
137 }
138 }
139
140 pub fn audit(&mut self, rust_code: &str) -> Result<UnsafeAuditReport> {
142 self.source_code = rust_code.to_string();
144
145 self.total_lines = rust_code.lines().count();
147
148 let syntax_tree = syn::parse_file(rust_code).context("Failed to parse Rust code")?;
150
151 self.visit_file(&syntax_tree);
153
154 Ok(UnsafeAuditReport::new(self.total_lines, self.unsafe_lines, self.unsafe_blocks.clone()))
155 }
156
157 fn analyze_unsafe_block(&self, unsafe_block: &ExprUnsafe) -> (UnsafePattern, u8, String) {
159 let block_str = quote::quote!(#unsafe_block).to_string();
161
162 let (pattern, confidence, suggestion) = if block_str.contains("std :: ptr ::")
164 || block_str.contains("* ptr")
165 || block_str.contains("null_mut")
166 || block_str.contains("null()")
167 {
168 (
169 UnsafePattern::RawPointerDeref,
170 85,
171 "Consider using Box<T>, &T, or &mut T with proper lifetimes".to_string(),
172 )
173 } else if block_str.contains("transmute") {
174 (
175 UnsafePattern::Transmute,
176 40,
177 "Consider safe alternatives like From/Into traits or checked conversions"
178 .to_string(),
179 )
180 } else if block_str.contains("asm!") || block_str.contains("global_asm!") {
181 (
182 UnsafePattern::Assembly,
183 15,
184 "No safe alternative - inline assembly required for platform-specific operations"
185 .to_string(),
186 )
187 } else if block_str.contains("extern") {
188 (
189 UnsafePattern::FfiCall,
190 30,
191 "Consider creating a safe wrapper around FFI calls".to_string(),
192 )
193 } else {
194 (
195 UnsafePattern::Other,
196 50,
197 "Review if this unsafe block can be eliminated or replaced with safe alternatives"
198 .to_string(),
199 )
200 };
201
202 (pattern, confidence, suggestion)
203 }
204
205 fn count_block_lines(&self, block: &Block) -> usize {
207 block.stmts.len() + 2
209 }
210}
211
212impl Default for UnsafeAuditor {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl<'ast> Visit<'ast> for UnsafeAuditor {
219 fn visit_expr(&mut self, expr: &'ast Expr) {
221 if let Expr::Unsafe(unsafe_expr) = expr {
222 let (pattern, confidence, suggestion) = self.analyze_unsafe_block(unsafe_expr);
224
225 let block_lines = self.count_block_lines(&unsafe_expr.block);
227 self.unsafe_lines += block_lines;
228
229 let line = 0; self.unsafe_blocks.push(UnsafeBlock { line, confidence, pattern, suggestion });
233 }
234
235 syn::visit::visit_expr(self, expr);
237 }
238
239 fn visit_item_fn(&mut self, func: &'ast ItemFn) {
241 if func.sig.unsafety.is_some() {
243 let body_lines = self.count_block_lines(&func.block);
245 self.unsafe_lines += body_lines;
246
247 self.unsafe_blocks.push(UnsafeBlock {
248 line: 0,
249 confidence: 60,
250 pattern: UnsafePattern::Other,
251 suggestion: "Unsafe function - review if entire function needs to be unsafe or just specific blocks".to_string(),
252 });
253 }
254
255 syn::visit::visit_item_fn(self, func);
257 }
258}
259
260pub fn audit_rust_code(rust_code: &str) -> Result<UnsafeAuditReport> {
272 let mut auditor = UnsafeAuditor::new();
273 auditor.audit(rust_code)
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct CompilationError {
279 pub code: Option<String>,
281 pub message: String,
283}
284
285#[derive(Debug, Clone)]
287pub struct CompilationResult {
288 pub success: bool,
290 pub errors: Vec<CompilationError>,
292 pub warnings: Vec<String>,
294}
295
296pub fn verify_compilation(rust_code: &str) -> Result<CompilationResult> {
310 use std::process::Command;
311 use std::time::{SystemTime, UNIX_EPOCH};
312
313 let unique_id = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
314 let temp_dir = std::env::temp_dir();
315 let temp_file = temp_dir.join(format!("decy_verify_{}.rs", unique_id));
316 let temp_output = temp_dir.join(format!("decy_verify_{}.rmeta", unique_id));
317
318 std::fs::write(&temp_file, rust_code)
319 .context("Failed to write temp file for compilation check")?;
320
321 let output = Command::new("rustc")
322 .arg("--emit=metadata")
323 .arg("--edition=2021")
324 .arg("-o")
325 .arg(&temp_output)
326 .arg(&temp_file)
327 .output()
328 .context("Failed to run rustc — is it installed?")?;
329
330 let _ = std::fs::remove_file(&temp_file);
332 let _ = std::fs::remove_file(&temp_output);
333
334 let stderr = String::from_utf8_lossy(&output.stderr);
335
336 if output.status.success() {
337 let warnings: Vec<String> =
338 stderr.lines().filter(|l| l.contains("warning")).map(|l| l.to_string()).collect();
339 Ok(CompilationResult { success: true, errors: vec![], warnings })
340 } else {
341 let mut errors = Vec::new();
342 for line in stderr.lines() {
343 if line.starts_with("error") {
344 let code = line
346 .find('[')
347 .and_then(|start| line.find(']').map(|end| (start, end)))
348 .map(|(start, end)| line[start + 1..end].to_string());
349 errors.push(CompilationError { code, message: line.to_string() });
350 }
351 }
352 Ok(CompilationResult { success: false, errors, warnings: vec![] })
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
364 fn test_no_unsafe_blocks() {
365 let code = r#"
367 fn safe_function() {
368 let x = 42;
369 println!("{}", x);
370 }
371 "#;
372
373 let report = audit_rust_code(code).expect("Audit failed");
374 assert_eq!(report.unsafe_blocks.len(), 0);
375 assert_eq!(report.unsafe_lines, 0);
376 assert!(report.meets_density_target());
377 }
378
379 #[test]
380 fn test_single_unsafe_block() {
381 let code = r#"
383 fn with_unsafe() {
384 unsafe {
385 let ptr = std::ptr::null_mut::<i32>();
386 *ptr = 42;
387 }
388 }
389 "#;
390
391 let report = audit_rust_code(code).expect("Audit failed");
392 assert_eq!(report.unsafe_blocks.len(), 1, "Should detect one unsafe block");
393 assert!(report.unsafe_lines > 0, "Should count unsafe lines");
394 }
395
396 #[test]
397 fn test_multiple_unsafe_blocks() {
398 let code = r#"
400 fn multiple_unsafe() {
401 unsafe {
402 let ptr1 = std::ptr::null_mut::<i32>();
403 }
404
405 let safe_code = 42;
406
407 unsafe {
408 let ptr2 = std::ptr::null_mut::<f64>();
409 }
410 }
411 "#;
412
413 let report = audit_rust_code(code).expect("Audit failed");
414 assert_eq!(report.unsafe_blocks.len(), 2, "Should detect two unsafe blocks");
415 }
416
417 #[test]
418 fn test_unsafe_density_calculation() {
419 let code = r#"
421fn example() {
422 let x = 1;
423 let y = 2;
424 unsafe {
425 let ptr = std::ptr::null_mut::<i32>();
426 }
427 let z = 3;
428}
429"#;
430 let report = audit_rust_code(code).expect("Audit failed");
431
432 assert!(report.unsafe_density_percent > 20.0);
435 assert!(report.unsafe_density_percent < 50.0);
436 }
437
438 #[test]
439 fn test_nested_unsafe_blocks() {
440 let code = r#"
442 fn nested() {
443 unsafe {
444 let ptr = std::ptr::null_mut::<i32>();
445 unsafe {
446 *ptr = 42;
447 }
448 }
449 }
450 "#;
451
452 let report = audit_rust_code(code).expect("Audit failed");
453 assert!(!report.unsafe_blocks.is_empty(), "Should detect unsafe blocks");
455 }
456
457 #[test]
458 fn test_unsafe_in_different_items() {
459 let code = r#"
461 fn func1() {
462 unsafe { let x = 1; }
463 }
464
465 fn func2() {
466 unsafe { let y = 2; }
467 }
468
469 impl MyStruct {
470 fn method(&self) {
471 unsafe { let z = 3; }
472 }
473 }
474
475 struct MyStruct;
476 "#;
477
478 let report = audit_rust_code(code).expect("Audit failed");
479 assert_eq!(report.unsafe_blocks.len(), 3, "Should detect unsafe in all items");
480 }
481
482 #[test]
483 fn test_confidence_scoring() {
484 let code = r#"
486 fn with_pointer() {
487 unsafe {
488 let ptr = std::ptr::null_mut::<i32>();
489 *ptr = 42;
490 }
491 }
492 "#;
493
494 let report = audit_rust_code(code).expect("Audit failed");
495 assert_eq!(report.unsafe_blocks.len(), 1);
496
497 let block = &report.unsafe_blocks[0];
498 assert!(block.confidence > 0, "Should have non-zero confidence");
499 assert!(block.confidence <= 100, "Confidence should be 0-100");
500 }
501
502 #[test]
503 fn test_pattern_detection_raw_pointer() {
504 let code = r#"
506 fn deref_pointer() {
507 unsafe {
508 let ptr = std::ptr::null_mut::<i32>();
509 *ptr = 42;
510 }
511 }
512 "#;
513
514 let report = audit_rust_code(code).expect("Audit failed");
515 assert_eq!(report.unsafe_blocks.len(), 1);
516 assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::RawPointerDeref);
517 }
518
519 #[test]
520 fn test_suggestion_generation() {
521 let code = r#"
523 fn with_unsafe() {
524 unsafe {
525 let ptr = std::ptr::null_mut::<i32>();
526 }
527 }
528 "#;
529
530 let report = audit_rust_code(code).expect("Audit failed");
531 assert_eq!(report.unsafe_blocks.len(), 1);
532 assert!(!report.unsafe_blocks[0].suggestion.is_empty(), "Should provide a suggestion");
533 }
534
535 #[test]
536 fn test_high_confidence_blocks() {
537 let code = r#"
539 fn example() {
540 unsafe { let x = 1; }
541 unsafe { let y = 2; }
542 }
543 "#;
544
545 let report = audit_rust_code(code).expect("Audit failed");
546 let high_conf = report.high_confidence_blocks();
549 assert!(high_conf.len() <= report.unsafe_blocks.len());
550 }
551
552 #[test]
553 fn test_average_confidence() {
554 let code = r#"
556 fn example() {
557 unsafe { let x = 1; }
558 }
559 "#;
560
561 let report = audit_rust_code(code).expect("Audit failed");
562 assert!(report.average_confidence >= 0.0);
563 assert!(report.average_confidence <= 100.0);
564 }
565
566 #[test]
567 fn test_empty_code() {
568 let code = "";
570 let report = audit_rust_code(code).expect("Audit failed");
571 assert_eq!(report.unsafe_blocks.len(), 0);
572 assert_eq!(report.total_lines, 0);
573 }
574
575 #[test]
576 fn test_invalid_rust_code() {
577 let code = "fn incomplete(";
579 let result = audit_rust_code(code);
580 assert!(result.is_err(), "Should return error for invalid code");
581 }
582
583 #[test]
584 fn test_unsafe_fn() {
585 let code = r#"
587 unsafe fn dangerous_function() {
588 let x = 42;
589 }
590 "#;
591
592 let report = audit_rust_code(code).expect("Audit failed");
593 assert!(!report.unsafe_blocks.is_empty() || report.unsafe_lines > 0);
595 }
596
597 #[test]
598 fn test_verify_compilation_valid_code() {
599 let result = verify_compilation("fn main() {}").expect("rustc failed to run");
600 assert!(result.success, "Valid Rust should compile");
601 assert!(result.errors.is_empty());
602 }
603
604 #[test]
605 fn test_verify_compilation_type_error() {
606 let result =
607 verify_compilation("fn main() { let x: i32 = \"bad\"; }").expect("rustc failed to run");
608 assert!(!result.success, "Type error should fail compilation");
609 assert!(!result.errors.is_empty(), "Should have at least one error");
610 let has_e0308 = result.errors.iter().any(|e| e.code.as_deref() == Some("E0308"));
612 assert!(has_e0308, "Should contain E0308 error code");
613 }
614
615 #[test]
616 fn test_verify_compilation_missing_function() {
617 let result =
618 verify_compilation("fn main() { undefined_function(); }").expect("rustc failed to run");
619 assert!(!result.success);
620 assert!(!result.errors.is_empty());
621 }
622
623 #[test]
628 fn test_pattern_detection_transmute() {
629 let code = r#"
630 fn with_transmute() {
631 unsafe {
632 let x: u32 = std::mem::transmute(1.0f32);
633 }
634 }
635 "#;
636 let report = audit_rust_code(code).expect("Audit failed");
637 assert_eq!(report.unsafe_blocks.len(), 1);
638 assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::Transmute);
639 assert_eq!(report.unsafe_blocks[0].confidence, 40);
640 assert!(report.unsafe_blocks[0].suggestion.contains("From/Into"));
641 }
642
643 #[test]
644 fn test_pattern_detection_assembly() {
645 let code = r#"
648 fn with_asm() {
649 unsafe {
650 std::arch::asm!("nop");
651 }
652 }
653 "#;
654 let report = audit_rust_code(code).expect("Audit failed");
655 assert_eq!(report.unsafe_blocks.len(), 1);
656 let pattern = &report.unsafe_blocks[0].pattern;
659 assert!(
660 *pattern == UnsafePattern::Assembly || *pattern == UnsafePattern::Other,
661 "Expected Assembly or Other, got {:?}",
662 pattern
663 );
664 }
665
666 #[test]
667 fn test_pattern_detection_ffi() {
668 let code = r#"
669 fn with_ffi() {
670 unsafe {
671 extern "C" {
672 fn puts(s: *const u8) -> i32;
673 }
674 }
675 }
676 "#;
677 let report = audit_rust_code(code).expect("Audit failed");
678 assert_eq!(report.unsafe_blocks.len(), 1);
679 assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::FfiCall);
680 assert_eq!(report.unsafe_blocks[0].confidence, 30);
681 assert!(report.unsafe_blocks[0].suggestion.contains("safe wrapper"));
682 }
683
684 #[test]
685 fn test_pattern_detection_other() {
686 let code = r#"
687 fn with_other_unsafe() {
688 unsafe {
689 let v: Vec<i32> = Vec::new();
690 let _ = v.len();
691 }
692 }
693 "#;
694 let report = audit_rust_code(code).expect("Audit failed");
695 assert_eq!(report.unsafe_blocks.len(), 1);
696 assert_eq!(report.unsafe_blocks[0].pattern, UnsafePattern::Other);
697 assert_eq!(report.unsafe_blocks[0].confidence, 50);
698 assert!(report.unsafe_blocks[0].suggestion.contains("Review"));
699 }
700
701 #[test]
706 fn test_meets_density_target_low_density() {
707 let report = UnsafeAuditReport::new(100, 3, vec![]);
708 assert!(report.meets_density_target());
709 assert!(report.unsafe_density_percent < 5.0);
710 }
711
712 #[test]
713 fn test_meets_density_target_high_density() {
714 let report = UnsafeAuditReport::new(100, 10, vec![]);
715 assert!(!report.meets_density_target());
716 assert!(report.unsafe_density_percent >= 5.0);
717 }
718
719 #[test]
720 fn test_meets_density_target_zero_lines() {
721 let report = UnsafeAuditReport::new(0, 0, vec![]);
722 assert!(report.meets_density_target());
723 assert!((report.unsafe_density_percent - 0.0).abs() < 0.001);
724 }
725
726 #[test]
727 fn test_high_confidence_blocks_filtering() {
728 let blocks = vec![
729 UnsafeBlock {
730 line: 1,
731 confidence: 85,
732 pattern: UnsafePattern::RawPointerDeref,
733 suggestion: "Use Box".to_string(),
734 },
735 UnsafeBlock {
736 line: 10,
737 confidence: 40,
738 pattern: UnsafePattern::Transmute,
739 suggestion: "Use From".to_string(),
740 },
741 UnsafeBlock {
742 line: 20,
743 confidence: 70,
744 pattern: UnsafePattern::Other,
745 suggestion: "Review".to_string(),
746 },
747 ];
748 let report = UnsafeAuditReport::new(100, 10, blocks);
749 let high = report.high_confidence_blocks();
750 assert_eq!(high.len(), 2); assert!(high.iter().all(|b| b.confidence >= 70));
752 }
753
754 #[test]
755 fn test_high_confidence_blocks_empty() {
756 let report = UnsafeAuditReport::new(100, 0, vec![]);
757 assert!(report.high_confidence_blocks().is_empty());
758 }
759
760 #[test]
765 fn test_unsafe_auditor_default() {
766 let auditor = UnsafeAuditor::default();
767 assert_eq!(auditor.total_lines, 0);
768 assert_eq!(auditor.unsafe_lines, 0);
769 assert!(auditor.unsafe_blocks.is_empty());
770 }
771
772 #[test]
773 fn test_unsafe_fn_detection_body_lines() {
774 let code = r#"
775 unsafe fn dangerous() {
776 let x = 1;
777 let y = 2;
778 let z = 3;
779 }
780 "#;
781 let report = audit_rust_code(code).expect("Audit failed");
782 assert!(!report.unsafe_blocks.is_empty());
783 let block = &report.unsafe_blocks[0];
784 assert_eq!(block.confidence, 60);
785 assert_eq!(block.pattern, UnsafePattern::Other);
786 assert!(block.suggestion.contains("Unsafe function"));
787 }
788
789 #[test]
794 fn test_verify_compilation_with_warnings() {
795 let code = "#[warn(unused_variables)] fn main() { let x = 42; }";
796 let result = verify_compilation(code).expect("rustc failed to run");
797 assert!(result.success);
798 }
800
801 #[test]
802 fn test_verify_compilation_multiple_errors() {
803 let code = "fn main() { undefined_a(); undefined_b(); }";
804 let result = verify_compilation(code).expect("rustc failed to run");
805 assert!(!result.success);
806 assert!(!result.errors.is_empty());
807 }
808
809 #[test]
810 fn test_compilation_error_without_code() {
811 let code = "this is not valid rust at all";
813 let result = verify_compilation(code).expect("rustc failed to run");
814 assert!(!result.success);
815 assert!(!result.errors.is_empty());
817 }
818
819 #[test]
820 fn test_average_confidence_calculation() {
821 let blocks = vec![
822 UnsafeBlock {
823 line: 0,
824 confidence: 80,
825 pattern: UnsafePattern::RawPointerDeref,
826 suggestion: "s".to_string(),
827 },
828 UnsafeBlock {
829 line: 0,
830 confidence: 40,
831 pattern: UnsafePattern::Transmute,
832 suggestion: "s".to_string(),
833 },
834 ];
835 let report = UnsafeAuditReport::new(100, 10, blocks);
836 assert!((report.average_confidence - 60.0).abs() < 0.001);
837 }
838
839 #[test]
840 fn test_average_confidence_no_blocks() {
841 let report = UnsafeAuditReport::new(100, 0, vec![]);
842 assert!((report.average_confidence - 0.0).abs() < 0.001);
843 }
844}
845
846#[cfg(test)]
847#[path = "lock_verify_tests.rs"]
848mod lock_verify_tests;