use super::purification::{PurificationOptions, Purifier};
use crate::bash_parser::codegen::generate_purified_bash;
use crate::bash_parser::parser::BashParser;
use proptest::prelude::*;
#[test]
fn prop_no_injection_attacks(
var_name in "[a-z_][a-z0-9_]{0,10}",
user_input in r#"[a-z0-9 ;|&$()]{1,20}"#
) {
let bash_code = format!(
"#!/bin/bash\n{}='{}'\necho ${}",
var_name, user_input, var_name
);
if let Ok(mut parser) = BashParser::new(&bash_code) {
if let Ok(ast) = parser.parse() {
let mut purifier = Purifier::new(PurificationOptions::default());
if let Ok(purified_ast) = purifier.purify(&ast) {
let output = generate_purified_bash(&purified_ast);
let unquoted_patterns = [
format!("echo ${}", var_name), format!("echo ${{{}}}", var_name), ];
for pattern in &unquoted_patterns {
if output.contains(pattern) {
let quoted_version = format!("\"{}\"", pattern.trim_start_matches("echo "));
prop_assert!(
output.contains("ed_version) ||
output.contains(&format!("'{}'", pattern.trim_start_matches("echo "))),
"Variable expansion must be quoted to prevent injection: got unquoted {} in:\n{}",
pattern, output
);
}
}
prop_assert!(
output.contains(&format!("\"${}\"", var_name)) ||
output.contains(&format!("\"${{{}}}\"", var_name)) ||
output.contains(&format!("'${}'", var_name)) ||
output.contains(&format!("'${{{}}}'", var_name)),
"Variable expansion must be quoted, got:\n{}",
output
);
}
}
}
}
#[test]
fn prop_no_toctou_race_conditions(
file_path in r#"/[a-z]{1,10}/[a-z]{1,10}"#
) {
let bash_code = format!(
"#!/bin/bash\nif [ -f \"{}\" ]\nthen\ncat \"{}\"\nfi",
file_path, file_path
);
if let Ok(mut parser) = BashParser::new(&bash_code) {
if let Ok(ast) = parser.parse() {
let mut purifier = Purifier::new(PurificationOptions::default());
if let Ok(purified_ast) = purifier.purify(&ast) {
let output = generate_purified_bash(&purified_ast);
let report = purifier.report();
let has_check_then_use = output.contains("if [ -f") && output.contains("cat");
if has_check_then_use {
let has_toctou_warning = report.warnings.iter().any(|w|
w.to_lowercase().contains("toctou") ||
w.to_lowercase().contains("race") ||
w.to_lowercase().contains("check-then-use")
);
if !has_toctou_warning {
eprintln!("INFO: Check-then-use pattern detected but TOCTOU warnings not yet implemented");
}
}
}
}
}
}
#[test]
fn prop_no_infinite_loops(
var_name in "[a-z]",
iterations in 1u32..100
) {
let bash_code = format!(
"#!/bin/bash\n{}=0\nwhile [ \"${}\" -lt {} ]\ndo\n{}=$(({} + 1))\ndone",
var_name, var_name, iterations, var_name, var_name
);
if let Ok(mut parser) = BashParser::new(&bash_code) {
if let Ok(ast) = parser.parse() {
let mut purifier = Purifier::new(PurificationOptions::default());
if let Ok(purified_ast) = purifier.purify(&ast) {
let output = generate_purified_bash(&purified_ast);
let termination_operators = ["-lt", "-le", "-gt", "-ge", "-eq", "-ne"];
let has_termination = termination_operators.iter().any(|op| output.contains(op));
prop_assert!(
has_termination,
"Loop must have explicit termination condition with comparison operator, got:\n{}",
output
);
if bash_code.contains("while") {
prop_assert!(
output.contains("while"),
"While loop structure should be preserved, got:\n{}",
output
);
}
if bash_code.contains("while") {
prop_assert!(
output.contains("do") && output.contains("done"),
"Loop must have do/done structure, got:\n{}",
output
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_purification_determinism_simple() {
let bash_code = "#!/bin/bash\nx=42";
let mut parser1 = BashParser::new(bash_code).unwrap();
let ast1 = parser1.parse().unwrap();
let mut purifier1 = Purifier::new(PurificationOptions::default());
let purified_ast1 = purifier1.purify(&ast1).unwrap();
let output1 = generate_purified_bash(&purified_ast1);
let mut parser2 = BashParser::new(bash_code).unwrap();
let ast2 = parser2.parse().unwrap();
let mut purifier2 = Purifier::new(PurificationOptions::default());
let purified_ast2 = purifier2.purify(&ast2).unwrap();
let output2 = generate_purified_bash(&purified_ast2);
assert_eq!(output1, output2, "Purification must be deterministic");
}
#[test]
fn test_purification_idempotence_simple() {
let bash_code = "#!/bin/bash\nx=42";
let mut parser1 = BashParser::new(bash_code).unwrap();
let ast1 = parser1.parse().unwrap();
let mut purifier1 = Purifier::new(PurificationOptions::default());
let purified_ast1 = purifier1.purify(&ast1).unwrap();
let output1 = generate_purified_bash(&purified_ast1);
let mut parser2 = BashParser::new(&output1).unwrap();
let ast2 = parser2.parse().unwrap();
let mut purifier2 = Purifier::new(PurificationOptions::default());
let purified_ast2 = purifier2.purify(&ast2).unwrap();
let output2 = generate_purified_bash(&purified_ast2);
assert_eq!(output1, output2, "Purification must be idempotent");
}
#[test]
fn test_purified_output_has_posix_shebang() {
let bash_code = "#!/bin/bash\nx=42";
let mut parser = BashParser::new(bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
assert!(
output.starts_with("#!/bin/sh"),
"Purified output must have POSIX shebang, got: {}",
output.lines().next().unwrap_or("")
);
}
#[test]
fn test_variable_assignment_preserved() {
let bash_code = "#!/bin/bash\nfoo=123";
let mut parser = BashParser::new(bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
assert!(
output.contains("foo=123"),
"Variable assignment should be preserved in output:\n{}",
output
);
}
#[test]
fn test_comments_preserved() {
let bash_code = "#!/bin/bash\n# Important comment\nx=42";
let mut parser = BashParser::new(bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
assert!(
output.contains("Important comment"),
"Comment should be preserved in output:\n{}",
output
);
}
}