use crate::types::{DynamicExecTemplate, SysModulesInjection};
pub fn detect_sys_modules_injection(content: &str) -> Vec<SysModulesInjection> {
use regex::Regex;
let mut injections = Vec::new();
let re = Regex::new(r#"sys\.modules\[['"]([^'"]+)['"]\]\s*=\s*(\w+)"#).unwrap();
let re_dunder = Regex::new(r#"sys\.modules\[__name__\]\s*=\s*(\w+)"#).unwrap();
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
for caps in re.captures_iter(line) {
injections.push(SysModulesInjection {
module_name: caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
line: line_num,
value: caps
.get(2)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
});
}
for caps in re_dunder.captures_iter(line) {
injections.push(SysModulesInjection {
module_name: "__name__".to_string(),
line: line_num,
value: caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
});
}
}
injections
}
pub(super) fn detect_dynamic_exec_templates(content: &str) -> Vec<DynamicExecTemplate> {
let mut templates = Vec::new();
let exec_patterns = ["exec(", "eval(", "compile("];
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
for pattern in &exec_patterns {
if let Some(start_idx) = line.find(pattern) {
let after_call = &line[start_idx + pattern.len()..];
let has_old_style = after_call.contains("%s")
|| after_call.contains("%d")
|| after_call.contains("%r")
|| after_call.contains("%(");
let has_new_style = after_call.contains("{}")
|| after_call.contains("{0}")
|| (after_call.contains('{') && after_call.contains('}'));
if has_old_style || has_new_style {
let call_type = pattern.trim_end_matches('(').to_string();
let mut generated_prefixes = Vec::new();
if let Some(def_idx) = after_call.find("def ") {
let after_def = &after_call[def_idx + 4..];
if let Some(end) = after_def.find('%').or_else(|| after_def.find('{')) {
let prefix = after_def[..end].trim();
if !prefix.is_empty()
&& prefix.chars().all(|c| c.is_alphanumeric() || c == '_')
{
generated_prefixes.push(prefix.to_string());
}
}
}
if let Some(class_idx) = after_call.find("class ") {
let after_class = &after_call[class_idx + 6..];
if let Some(end) = after_class.find('%').or_else(|| after_class.find('{')) {
let prefix = after_class[..end].trim();
if !prefix.is_empty()
&& prefix.chars().all(|c| c.is_alphanumeric() || c == '_')
{
generated_prefixes.push(prefix.to_string());
}
}
}
templates.push(DynamicExecTemplate {
template: after_call.chars().take(100).collect(), generated_prefixes,
line: line_num,
call_type,
});
}
}
}
}
templates
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_exec_with_old_style_format() {
let content = r#"
exec("def get%s(self): return self._%s" % (name, name))
"#;
let templates = detect_dynamic_exec_templates(content);
assert_eq!(templates.len(), 1);
assert_eq!(templates[0].call_type, "exec");
assert!(templates[0].generated_prefixes.contains(&"get".to_string()));
}
#[test]
fn detects_eval_with_new_style_format() {
let content = r#"
result = eval("{}({})".format(func_name, args))
"#;
let templates = detect_dynamic_exec_templates(content);
assert_eq!(templates.len(), 1);
assert_eq!(templates[0].call_type, "eval");
}
#[test]
fn detects_compile_with_format() {
let content = r#"
code = compile("class {}(Base): pass".format(name), "<dynamic>", "exec")
"#;
let templates = detect_dynamic_exec_templates(content);
assert_eq!(templates.len(), 1);
assert_eq!(templates[0].call_type, "compile");
}
#[test]
fn no_detection_without_format_string() {
let content = r#"
exec(some_code)
eval("1 + 2")
"#;
let templates = detect_dynamic_exec_templates(content);
assert!(templates.is_empty() || templates.iter().all(|t| !t.template.contains("%s")));
}
#[test]
fn detects_sys_modules_single_quote() {
let content = r#"
import sys
sys.modules['compat'] = compat_module
"#;
let injections = detect_sys_modules_injection(content);
assert_eq!(injections.len(), 1);
assert_eq!(injections[0].module_name, "compat");
assert_eq!(injections[0].value, "compat_module");
}
#[test]
fn detects_sys_modules_double_quote() {
let content = r#"
sys.modules["shim"] = wrapper
"#;
let injections = detect_sys_modules_injection(content);
assert_eq!(injections.len(), 1);
assert_eq!(injections[0].module_name, "shim");
assert_eq!(injections[0].value, "wrapper");
}
#[test]
fn detects_sys_modules_dunder_name() {
let content = r#"
sys.modules[__name__] = ModuleWrapper(sys.modules[__name__])
"#;
let injections = detect_sys_modules_injection(content);
assert_eq!(injections.len(), 1);
assert_eq!(injections[0].module_name, "__name__");
}
#[test]
fn no_sys_modules_detection_for_read() {
let content = r#"
mod = sys.modules['foo'] # reading, not writing
"#;
let injections = detect_sys_modules_injection(content);
assert!(injections.is_empty());
}
}