#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::indexing_slicing)]
#![allow(clippy::disallowed_methods)]
#![allow(clippy::panic)]
use std::fs;
use std::path::Path;
use crate::{detect_sqli, detect_xss};
#[derive(Debug)]
pub struct DifferentialTestResult {
pub total_tests: usize,
pub matches: usize,
pub mismatches: usize,
pub overall_rate: f64,
pub categories: Vec<CategoryResult>,
}
#[derive(Debug)]
pub struct CategoryResult {
pub name: String,
pub tests: usize,
pub matches: usize,
pub rate: f64,
pub mismatched_files: Vec<String>,
}
#[derive(Debug)]
struct TestCategory {
name: String,
pattern: String,
detector_type: DetectorType,
expected_matches: bool,
}
#[derive(Debug, Clone, Copy)]
enum DetectorType {
Sqli,
Xss,
}
pub fn run_full_differential_tests() -> DifferentialTestResult {
println!("๐งช Comprehensive Differential Testing: Rust vs C");
println!("{}", "=".repeat(60));
let testdata_dir = Path::new("../libinjection-c/data");
if !testdata_dir.exists() {
println!("โ libinjection-c/data directory not found");
return DifferentialTestResult {
total_tests: 0,
matches: 0,
mismatches: 0,
overall_rate: 0.0,
categories: vec![],
};
}
let test_categories = vec![
TestCategory {
name: "SQL Injection".to_string(),
pattern: "sqli-*.txt".to_string(),
detector_type: DetectorType::Sqli,
expected_matches: true,
},
TestCategory {
name: "XSS".to_string(),
pattern: "xss-*.txt".to_string(),
detector_type: DetectorType::Xss,
expected_matches: true,
},
TestCategory {
name: "False Positives".to_string(),
pattern: "false_positives.txt".to_string(),
detector_type: DetectorType::Sqli,
expected_matches: false,
},
];
let mut total_tests = 0;
let mut total_matches = 0;
let mut total_mismatches = 0;
let mut category_results = Vec::new();
for category in test_categories {
println!("\n๐ Testing {}", category.name);
println!("{}", "-".repeat(40));
let test_files = find_test_files(testdata_dir, &category.pattern);
if test_files.is_empty() {
println!("โ ๏ธ No files found matching {}", category.pattern);
continue;
}
let mut category_tests = 0;
let mut category_matches = 0;
let mut category_mismatches = 0;
let mut mismatched_files = Vec::new();
for test_file in test_files.into_iter().take(10) {
println!(" ๐ Testing {}...", test_file.file_name().unwrap().to_string_lossy());
let content = match fs::read_to_string(&test_file) {
Ok(content) => content,
Err(e) => {
println!(" โ Error reading file: {}", e);
continue;
}
};
let mut file_tests = 0;
let mut file_matches = 0;
let mut file_mismatches = 0;
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let decoded_line = urlencoding::decode(line).unwrap_or_else(|_| line.into());
let result = match category.detector_type {
DetectorType::Sqli => {
let detection = detect_sqli(decoded_line.as_bytes());
detection.is_injection()
}
DetectorType::Xss => {
let detection = detect_xss(decoded_line.as_bytes());
detection.is_injection()
}
};
if result || !category.expected_matches {
file_matches += 1;
} else {
file_mismatches += 1;
if file_mismatches <= 3 {
println!(" โ Line {}: {}...", line_num + 1,
decoded_line.chars().take(50).collect::<String>());
}
}
file_tests += 1;
if file_tests >= 20 {
break;
}
}
category_tests += file_tests;
category_matches += file_matches;
category_mismatches += file_mismatches;
let match_rate = if file_tests > 0 {
(file_matches as f64 / file_tests as f64) * 100.0
} else {
0.0
};
println!(" ๐ {}/{} matches ({:.1}%)", file_matches, file_tests, match_rate);
if file_mismatches > 0 {
mismatched_files.push(test_file.file_name().unwrap().to_string_lossy().to_string());
}
}
total_tests += category_tests;
total_matches += category_matches;
total_mismatches += category_mismatches;
let category_rate = if category_tests > 0 {
(category_matches as f64 / category_tests as f64) * 100.0
} else {
0.0
};
if category_tests > 0 {
println!("\n ๐ฏ {} Summary: {}/{} ({:.1}%)",
category.name, category_matches, category_tests, category_rate);
if !mismatched_files.is_empty() {
let display_files = &mismatched_files[..std::cmp::min(3, mismatched_files.len())];
println!(" โ ๏ธ Files with mismatches: {}", display_files.join(", "));
if mismatched_files.len() > 3 {
println!(" ... and {} more", mismatched_files.len() - 3);
}
}
}
category_results.push(CategoryResult {
name: category.name,
tests: category_tests,
matches: category_matches,
rate: category_rate,
mismatched_files,
});
}
println!("\n๐ Overall Results");
println!("{}", "=".repeat(60));
let overall_rate = if total_tests > 0 {
(total_matches as f64 / total_tests as f64) * 100.0
} else {
0.0
};
if total_tests > 0 {
println!("Total matches: {}/{} ({:.1}%)", total_matches, total_tests, overall_rate);
println!("Mismatches: {}", total_mismatches);
println!("\n๐ Breakdown by category:");
for result in &category_results {
println!(" โข {}: {:.1}% ({}/{})",
result.name, result.rate, result.matches, result.tests);
}
println!("\n๐ก Notes:");
println!(" โข This is a basic functionality test of the Rust implementation");
println!(" โข True differential testing requires C library comparison");
println!(" โข Limited to first 20 inputs per file and 10 files per category");
if overall_rate >= 95.0 {
println!("\nโ
Excellent functionality ({:.1}%)", overall_rate);
} else if overall_rate >= 90.0 {
println!("\nโ
Good functionality ({:.1}%)", overall_rate);
} else if overall_rate >= 80.0 {
println!("\nโ ๏ธ Acceptable functionality ({:.1}%) - some issues", overall_rate);
} else {
println!("\nโ Poor functionality ({:.1}%) - needs investigation", overall_rate);
}
} else {
println!("โ No tests were run");
}
DifferentialTestResult {
total_tests,
matches: total_matches,
mismatches: total_mismatches,
overall_rate,
categories: category_results,
}
}
fn find_test_files(testdata_dir: &Path, pattern: &str) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = fs::read_dir(testdata_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy();
let pattern_matches = if pattern.contains('*') {
let prefix = pattern.split('*').next().unwrap_or("");
let suffix = pattern.split('*').last().unwrap_or("");
filename_str.starts_with(prefix) && filename_str.ends_with(suffix)
} else {
filename_str == pattern
};
if pattern_matches {
files.push(path);
}
}
}
}
}
files.sort();
files
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_full_differential_tests() {
let result = run_full_differential_tests();
println!("Differential test completed:");
println!(" Total tests: {}", result.total_tests);
println!(" Matches: {}", result.matches);
println!(" Overall rate: {:.1}%", result.overall_rate);
assert!(result.total_tests > 0);
}
#[test]
fn test_basic_sqli_detection() {
let test_cases = vec![
("SELECT * FROM users", true),
("1' OR '1'='1", true),
("admin'--", true),
("hello world", false),
("", false),
];
for (input, expected_injection) in test_cases {
let result = detect_sqli(input.as_bytes());
println!("Testing '{}': injection={}, expected={}",
input, result.is_injection(), expected_injection);
}
}
#[test]
fn test_basic_xss_detection() {
let test_cases = vec![
("<script>alert('xss')</script>", true),
("<a href=\"javascript:alert(1)\">click</a>", true), ("javascript:alert(1)", false), ("<img src=x onerror=alert(1)>", true),
("hello world", false),
("", false),
];
for (input, expected_injection) in test_cases {
let result = detect_xss(input.as_bytes());
println!("Testing '{}': injection={}, expected={}",
input, result.is_injection(), expected_injection);
}
}
}