use crate::deobfuscation::renamer::context::{IdentifierKind, PhaseInfo, RenameContext};
pub fn build_fim_prompt(context: &RenameContext, max_phases: usize) -> (String, String) {
let kind = context.kind.unwrap_or(IdentifierKind::Method);
match kind {
IdentifierKind::Method => build_method_prompt(context, max_phases),
IdentifierKind::Field => build_field_prompt(context),
IdentifierKind::Type => build_type_prompt(context),
IdentifierKind::Parameter => build_parameter_prompt(context),
}
}
pub fn build_phase_label_prompt(phase: &PhaseInfo) -> (String, String) {
let mut prefix = String::from("<|fim_prefix|>// ");
let suffix = if phase.call_targets.is_empty() {
let mut ops_parts = Vec::new();
if let Some(ref profile) = phase.opcode_profile {
if profile.bitwise > 0 {
ops_parts.push(format!("bitwise: {}", profile.bitwise));
}
if profile.arithmetic > 0 {
ops_parts.push(format!("arithmetic: {}", profile.arithmetic));
}
if profile.array > 0 {
ops_parts.push(format!("array: {}", profile.array));
}
if profile.comparison > 0 {
ops_parts.push(format!("comparison: {}", profile.comparison));
}
}
let structure_note = phase
.structure
.as_deref()
.map(|s| format!("\n// Structure: {s}"))
.unwrap_or_default();
format!(
"\n// Ops: [{}]{structure_note}<|fim_middle|>",
ops_parts.join(", ")
)
} else {
let calls = phase.call_targets.join(", ");
format!("\n// Calls: {calls}<|fim_middle|>")
};
prefix.push_str("<|fim_suffix|>");
(prefix, suffix)
}
fn build_method_prompt(context: &RenameContext, max_phases: usize) -> (String, String) {
let mut prefix = String::new();
prefix.push_str("<|fim_prefix|>");
render_shared_context(&mut prefix, context);
if let Some(ref skeleton) = context.call_site_skeleton {
let return_type = context.dotnet_type.as_deref().unwrap_or("void");
let params = format_params(&context.parameters);
prefix.push_str(&format!("// Returns: {return_type}\n"));
prefix.push_str(&format!("public {return_type} "));
let suffix = format!("({params}) {{\n{skeleton}\n}}<|fim_middle|>");
(prefix, suffix)
} else {
let phases = truncate_phases(&context.phase_narrative, max_phases);
for (i, phase) in phases.iter().enumerate() {
prefix.push_str(&format!("// Phase {}: {}\n", i + 1, phase.label));
if !phase.call_targets.is_empty() {
let calls = phase.call_targets.join(", ");
prefix.push_str(&format!("// [calls: {calls}]\n"));
}
if let Some(ref profile) = phase.opcode_profile {
if profile.bitwise > 0 || profile.arithmetic > 0 {
let mut parts = Vec::new();
if profile.bitwise > 0 {
parts.push(format!("bitwise {}", profile.bitwise));
}
if profile.arithmetic > 0 {
parts.push(format!("arithmetic {}", profile.arithmetic));
}
if profile.array > 0 {
parts.push(format!("array {}", profile.array));
}
prefix.push_str(&format!("// [ops: {}]\n", parts.join(", ")));
}
}
}
let return_type = context.dotnet_type.as_deref().unwrap_or("void");
let params = format_params(&context.parameters);
prefix.push_str(&format!("// Returns: {return_type}\n"));
prefix.push_str(&format!("public {return_type} "));
let suffix = format!("({params}) {{\n}}<|fim_middle|>");
(prefix, suffix)
}
}
fn render_shared_context(prefix: &mut String, context: &RenameContext) {
if !context.interfaces.is_empty() {
let interfaces = format_interfaces(&context.interfaces);
prefix.push_str(&format!("// Implements: {interfaces}\n"));
}
if !context.siblings.is_empty() {
let siblings = context.siblings.join(", ");
prefix.push_str(&format!("// Sibling members: {siblings}\n"));
}
if !context.call_targets.is_empty() {
let calls = context.call_targets.join(", ");
prefix.push_str(&format!("// API calls: {calls}\n"));
}
if !context.string_literals.is_empty() {
let display: Vec<String> = context
.string_literals
.iter()
.take(5)
.map(|s| {
if s.len() > 30 {
format!("\"{}...\"", &s[..27])
} else {
format!("\"{s}\"")
}
})
.collect();
prefix.push_str(&format!("// Strings: {}\n", display.join(", ")));
}
if !context.field_accesses.is_empty() {
let fields = context.field_accesses.join(", ");
prefix.push_str(&format!("// Fields: {fields}\n"));
}
render_rejected_names(prefix, context);
render_caller_context(prefix, context);
}
fn render_rejected_names(prefix: &mut String, context: &RenameContext) {
if !context.rejected_names.is_empty() {
let rejected = context.rejected_names.join(", ");
prefix.push_str(&format!(
"// Do NOT use these names (already taken): {rejected}\n"
));
}
}
fn render_caller_context(prefix: &mut String, context: &RenameContext) {
if context.caller_context.is_empty() {
return;
}
prefix.push_str("// Called by:\n");
for caller in context.caller_context.iter().take(3) {
prefix.push_str(&format!("// {}", caller.caller_name));
let mut parts = Vec::new();
if !caller.nearby_strings.is_empty() {
let strs: Vec<String> = caller
.nearby_strings
.iter()
.take(3)
.map(|s| {
if s.len() > 40 {
format!("\"{}...\"", &s[..37])
} else {
format!("\"{s}\"")
}
})
.collect();
parts.push(format!("strings: {}", strs.join(", ")));
}
if let Some(ref usage) = caller.return_usage {
parts.push(format!("result used in {usage}"));
}
if parts.is_empty() {
prefix.push('\n');
} else {
prefix.push_str(&format!(" — {}\n", parts.join("; ")));
}
}
}
fn build_field_prompt(context: &RenameContext) -> (String, String) {
let mut prefix = String::from("<|fim_prefix|>");
if !context.siblings.is_empty() {
let used_in = context.siblings.join(", ");
prefix.push_str(&format!("// Used in: {used_in}\n"));
}
if let Some(ref dotnet_type) = context.dotnet_type {
prefix.push_str(&format!("// Type: {dotnet_type}\n"));
}
for anchor in &context.api_calls {
if let Some(pos) = anchor.argument_position {
prefix.push_str(&format!(
"// Passed to {} as arg {pos}\n",
anchor.method_name
));
}
}
render_rejected_names(&mut prefix, context);
let field_type = context.dotnet_type.as_deref().unwrap_or("object");
prefix.push_str(&format!("private {field_type} "));
let suffix = ";<|fim_middle|>".to_string();
(prefix, suffix)
}
fn build_type_prompt(context: &RenameContext) -> (String, String) {
let mut prefix = String::from("<|fim_prefix|>");
if !context.siblings.is_empty() {
let members = context.siblings.join(", ");
prefix.push_str(&format!("// Members: {members}\n"));
}
if !context.interfaces.is_empty() {
let interfaces = context.interfaces.join(", ");
prefix.push_str(&format!("// Implements: {interfaces}\n"));
}
if let Some(ref base) = context.base_class {
prefix.push_str(&format!("// Base: {base}\n"));
}
render_rejected_names(&mut prefix, context);
prefix.push_str("public class ");
let mut suffix = String::new();
let mut inherits = Vec::new();
if let Some(ref base) = context.base_class {
if base != "System.Object" {
inherits.push(base.clone());
}
}
inherits.extend(context.interfaces.iter().cloned());
if !inherits.is_empty() {
suffix.push_str(&format!(" : {}", inherits.join(", ")));
}
suffix.push_str(" {\n}");
suffix.push_str("<|fim_middle|>");
(prefix, suffix)
}
fn build_parameter_prompt(context: &RenameContext) -> (String, String) {
let mut prefix = String::from("<|fim_prefix|>");
if let Some(ref parent) = context.parent_type {
prefix.push_str(&format!("// In method: {parent}\n"));
}
for anchor in &context.api_calls {
if let Some(pos) = anchor.argument_position {
prefix.push_str(&format!(
"// Passed to {} as arg {pos}\n",
anchor.method_name
));
}
}
if !context.call_targets.is_empty() {
let calls = context.call_targets.join(", ");
prefix.push_str(&format!("// Method calls: {calls}\n"));
}
if !context.siblings.is_empty() {
let siblings = context.siblings.join(", ");
prefix.push_str(&format!("// Other params: {siblings}\n"));
}
render_rejected_names(&mut prefix, context);
let param_type = context.dotnet_type.as_deref().unwrap_or("object");
prefix.push_str(&format!("({param_type} "));
let suffix = ")<|fim_middle|>".to_string();
(prefix, suffix)
}
fn format_params(params: &[crate::deobfuscation::renamer::context::ParamInfo]) -> String {
if params.is_empty() {
return String::new();
}
params
.iter()
.enumerate()
.map(|(i, p)| {
let fallback = format!("param_{i}");
let name = p.known_name.as_deref().unwrap_or(&fallback);
format!("{} {name}", p.dotnet_type)
})
.collect::<Vec<_>>()
.join(", ")
}
fn format_interfaces(interfaces: &[String]) -> String {
interfaces.join(", ")
}
fn truncate_phases(phases: &[PhaseInfo], max_phases: usize) -> Vec<&PhaseInfo> {
if phases.len() <= max_phases {
return phases.iter().collect();
}
let half = max_phases / 2;
let mut result: Vec<&PhaseInfo> = Vec::new();
result.extend(&phases[..half]);
result.extend(&phases[phases.len() - half..]);
result
}
#[cfg(test)]
mod tests {
use crate::deobfuscation::renamer::{
context::{IdentifierKind, ParamInfo, PhaseInfo, RenameContext},
prompt::{build_fim_prompt, build_phase_label_prompt},
};
const TEST_MAX_PHASES: usize = 6;
#[test]
fn test_prompt_method_small() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
call_site_skeleton: Some(" File.WriteAllText(var_0, var_1);".to_string()),
parameters: vec![
ParamInfo {
dotnet_type: "string".to_string(),
known_name: Some("path".to_string()),
},
ParamInfo {
dotnet_type: "string".to_string(),
known_name: None,
},
],
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("<|fim_prefix|>"));
assert!(prefix.contains("Returns: void"));
assert!(suffix.contains("File.WriteAllText"));
assert!(suffix.contains("<|fim_middle|>"));
}
#[test]
fn test_prompt_method_large() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
phase_narrative: vec![
PhaseInfo {
label: "Load resource".to_string(),
call_targets: vec!["Assembly.GetManifestResourceStream".to_string()],
opcode_profile: None,
structure: None,
},
PhaseInfo {
label: "Decrypt data".to_string(),
call_targets: vec![],
opcode_profile: None,
structure: Some("loop".to_string()),
},
],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("Phase 1: Load resource"));
assert!(prefix.contains("Phase 2: Decrypt data"));
}
#[test]
fn test_prompt_method_renders_call_targets() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("byte[]".to_string()),
call_targets: vec![
"System.IO.File::ReadAllText".to_string(),
"System.Text.Encoding::GetBytes".to_string(),
],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("API calls: System.IO.File::ReadAllText"),
"Prompt should contain call targets, got:\n{prefix}"
);
assert!(
prefix.contains("System.Text.Encoding::GetBytes"),
"Prompt should contain all call targets"
);
}
#[test]
fn test_prompt_method_renders_string_literals() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
string_literals: vec!["Hello World".to_string(), "config.json".to_string()],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("Strings: \"Hello World\", \"config.json\""),
"Prompt should contain string literals, got:\n{prefix}"
);
}
#[test]
fn test_prompt_method_renders_field_accesses() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
field_accesses: vec!["Config.filePath".to_string(), "Config.timeout".to_string()],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("Fields: Config.filePath, Config.timeout"),
"Prompt should contain field accesses, got:\n{prefix}"
);
}
#[test]
fn test_prompt_method_renders_siblings() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
siblings: vec!["Initialize".to_string(), "Shutdown".to_string()],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("Sibling members: Initialize, Shutdown"),
"Prompt should contain siblings, got:\n{prefix}"
);
}
#[test]
fn test_prompt_method_small_with_full_context() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
dotnet_type: Some("void".to_string()),
call_site_skeleton: Some(" File.WriteAllText(var_0, var_1);".to_string()),
call_targets: vec!["System.IO.File::WriteAllText".to_string()],
string_literals: vec!["/tmp/output.txt".to_string()],
siblings: vec!["ReadConfig".to_string()],
parameters: vec![ParamInfo {
dotnet_type: "string".to_string(),
known_name: Some("path".to_string()),
}],
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("API calls:"), "Should have call targets");
assert!(prefix.contains("Strings:"), "Should have string literals");
assert!(prefix.contains("Sibling members:"), "Should have siblings");
assert!(
suffix.contains("File.WriteAllText"),
"Skeleton should be in suffix"
);
}
#[test]
fn test_prompt_method_empty_context() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("Returns: void"),
"Should have default return type"
);
assert!(suffix.contains("<|fim_middle|>"), "Should have FIM token");
assert!(!prefix.contains("API calls:"), "No call targets = no line");
assert!(!prefix.contains("Strings:"), "No strings = no line");
assert!(!prefix.contains("Fields:"), "No fields = no line");
assert!(!prefix.contains("Sibling"), "No siblings = no line");
}
#[test]
fn test_prompt_field() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Field),
dotnet_type: Some("System.String".to_string()),
siblings: vec!["ProcessData".to_string(), "Initialize".to_string()],
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("Used in: ProcessData, Initialize"));
assert!(prefix.contains("Type: System.String"));
assert!(suffix.contains("<|fim_middle|>"));
}
#[test]
fn test_prompt_type() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Type),
base_class: Some("System.Object".to_string()),
interfaces: vec!["IDisposable".to_string()],
siblings: vec!["ProcessData".to_string(), "Dispose".to_string()],
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("Members: ProcessData, Dispose"));
assert!(prefix.contains("Implements: IDisposable"));
assert!(suffix.contains(": IDisposable"));
}
#[test]
fn test_prompt_parameter() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Parameter),
dotnet_type: Some("string".to_string()),
parent_type: Some("WriteConfig".to_string()),
..Default::default()
};
let (prefix, suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("In method: WriteConfig"));
assert!(prefix.contains("(string "));
assert!(suffix.contains("<|fim_middle|>"));
}
#[test]
fn test_prompt_parameter_with_method_context() {
use crate::deobfuscation::renamer::context::ApiCallInfo;
let ctx = RenameContext {
kind: Some(IdentifierKind::Parameter),
dotnet_type: Some("byte[]".to_string()),
parent_type: Some("DecryptData".to_string()),
call_targets: vec!["System.Security.Cryptography.Aes::Create".to_string()],
api_calls: vec![ApiCallInfo {
method_name: "Aes::CreateDecryptor".to_string(),
argument_position: Some(0),
}],
siblings: vec!["key".to_string()],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("In method: DecryptData"),
"Should show parent method"
);
assert!(
prefix.contains("Passed to Aes::CreateDecryptor as arg 0"),
"Should show API anchor"
);
assert!(
prefix.contains("Method calls: System.Security.Cryptography.Aes::Create"),
"Should show owning method's call targets"
);
assert!(
prefix.contains("Other params: key"),
"Should show sibling params"
);
}
#[test]
fn test_phase_label_prompt_calls() {
let phase = PhaseInfo {
label: String::new(),
call_targets: vec![
"Assembly.GetManifestResourceStream".to_string(),
"BinaryReader.ReadBytes".to_string(),
],
opcode_profile: None,
structure: None,
};
let (_prefix, suffix) = build_phase_label_prompt(&phase);
assert!(suffix.contains("Assembly.GetManifestResourceStream"));
assert!(suffix.contains("BinaryReader.ReadBytes"));
}
#[test]
fn test_context_budget_truncation() {
let phases: Vec<PhaseInfo> = (0..10)
.map(|i| PhaseInfo {
label: format!("Step_{i}"),
call_targets: vec![],
opcode_profile: None,
structure: None,
})
.collect();
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
phase_narrative: phases,
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("Step_0"), "First phase should be present");
assert!(prefix.contains("Step_1"), "Second phase should be present");
assert!(prefix.contains("Step_2"), "Third phase should be present");
assert!(prefix.contains("Step_7"), "Third-to-last should be present");
assert!(prefix.contains("Step_9"), "Last phase should be present");
assert!(
!prefix.contains("Step_4"),
"Middle phase should be truncated"
);
}
#[test]
fn test_prompt_string_truncation() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
string_literals: vec![
"This is a very long string that exceeds thirty characters".to_string()
],
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(
prefix.contains("...\""),
"Long strings should be truncated with '...', got:\n{prefix}"
);
assert!(
!prefix.contains("thirty characters"),
"Truncated portion should not appear"
);
}
#[test]
fn test_prompt_string_limit() {
let ctx = RenameContext {
kind: Some(IdentifierKind::Method),
string_literals: (0..10).map(|i| format!("str_{i}")).collect(),
..Default::default()
};
let (prefix, _suffix) = build_fim_prompt(&ctx, TEST_MAX_PHASES);
assert!(prefix.contains("str_0"), "First string should be present");
assert!(prefix.contains("str_4"), "Fifth string should be present");
assert!(!prefix.contains("str_5"), "Sixth string should be excluded");
}
}