Skip to main content

chipi_core/backend/
ida.rs

1//! IDA Pro processor module backend.
2//!
3//! Generates a Python (IDAPython) processor module for IDA Pro 9.x
4//! from a validated `.chipi` definition.
5
6use std::collections::HashMap;
7
8use crate::codegen_ida;
9use crate::config::GenTarget;
10use crate::tree;
11use crate::types::ValidatedDef;
12
13use super::{CodegenBackend, CodegenError, FlowConfig, OperandKind};
14
15/// The IDA Pro code generation backend.
16pub struct IdaBackend;
17
18/// Parsed IDA-specific configuration from `lang_options`.
19pub struct IdaOptions {
20    pub processor_name: String,
21    pub processor_long_name: String,
22    pub processor_id: u64,
23    pub register_names: Vec<String>,
24    pub segment_registers: Vec<String>,
25    pub address_size: u32,
26    /// Bytes per addressable unit. Word-addressed architectures (like GC DSP)
27    /// use 2, meaning raw address values must be multiplied by this to get
28    /// IDA byte addresses.
29    pub bytes_per_unit: u32,
30    pub flags: Vec<String>,
31    pub operand_types: HashMap<String, OperandKind>,
32    /// Maps type alias names to display prefixes (e.g., "gpr" -> "r").
33    pub display_prefixes: HashMap<String, String>,
34    pub flow: FlowConfig,
35}
36
37impl CodegenBackend for IdaBackend {
38    fn lang(&self) -> &str {
39        "ida"
40    }
41
42    fn validate_lang_options(&self, options: &toml::Value) -> Result<(), Vec<String>> {
43        let table = match options {
44            toml::Value::Table(t) => t,
45            _ => return Ok(()),
46        };
47
48        let mut errors = Vec::new();
49
50        // Required fields
51        if !table.contains_key("processor_name") {
52            errors.push("missing required lang_option: processor_name".to_string());
53        }
54        if !table.contains_key("processor_long_name") {
55            errors.push("missing required lang_option: processor_long_name".to_string());
56        }
57        if !table.contains_key("processor_id") {
58            errors.push("missing required lang_option: processor_id".to_string());
59        }
60        if !table.contains_key("register_names") {
61            errors.push("missing required lang_option: register_names".to_string());
62        }
63
64        // Validate known keys
65        let known_keys = [
66            "processor_name",
67            "processor_long_name",
68            "processor_id",
69            "register_names",
70            "segment_registers",
71            "address_size",
72            "bytes_per_unit",
73            "flags",
74            "operand_types",
75            "display_prefixes",
76            "flow",
77        ];
78        for key in table.keys() {
79            if !known_keys.contains(&key.as_str()) {
80                errors.push(format!("unknown lang_option: {}", key));
81            }
82        }
83
84        if errors.is_empty() {
85            Ok(())
86        } else {
87            Err(errors)
88        }
89    }
90
91    fn generate(&self, ir: &ValidatedDef, config: &GenTarget) -> Result<String, CodegenError> {
92        let tree = tree::build_tree(ir);
93        let opts = parse_ida_options(config).map_err(|e| CodegenError::Internal(e))?;
94        Ok(codegen_ida::generate_ida_code(
95            ir,
96            &tree,
97            &opts,
98            &config.type_map,
99        ))
100    }
101
102    fn formatter_command(&self) -> Option<&[&str]> {
103        Some(&["ruff", "format"])
104    }
105}
106
107/// Parse `lang_options` and `type_map` into `IdaOptions`.
108fn parse_ida_options(config: &GenTarget) -> Result<IdaOptions, String> {
109    let table = match &config.lang_options {
110        Some(toml::Value::Table(t)) => t.clone(),
111        Some(_) => return Err("lang_options must be a table".to_string()),
112        None => return Err("IDA backend requires lang_options configuration".to_string()),
113    };
114
115    let processor_name = table
116        .get("processor_name")
117        .and_then(|v| v.as_str())
118        .ok_or("processor_name must be a string")?
119        .to_string();
120
121    let processor_long_name = table
122        .get("processor_long_name")
123        .and_then(|v| v.as_str())
124        .ok_or("processor_long_name must be a string")?
125        .to_string();
126
127    let processor_id = table
128        .get("processor_id")
129        .and_then(|v| v.as_integer())
130        .ok_or("processor_id must be an integer")? as u64;
131
132    let register_names = table
133        .get("register_names")
134        .and_then(|v| v.as_array())
135        .ok_or("register_names must be an array")?
136        .iter()
137        .map(|v| {
138            v.as_str()
139                .ok_or("register_names entries must be strings")
140                .map(|s| s.to_string())
141        })
142        .collect::<Result<Vec<String>, _>>()?;
143
144    if register_names.is_empty() {
145        return Err("register_names must not be empty".to_string());
146    }
147
148    let segment_registers = if let Some(sr) = table.get("segment_registers") {
149        sr.as_array()
150            .ok_or("segment_registers must be an array")?
151            .iter()
152            .map(|v| {
153                v.as_str()
154                    .ok_or("segment_registers entries must be strings")
155                    .map(|s| s.to_string())
156            })
157            .collect::<Result<Vec<String>, _>>()?
158    } else {
159        // Default: last 2 registers are CS, DS
160        let len = register_names.len();
161        if len >= 2 {
162            vec![
163                register_names[len - 2].clone(),
164                register_names[len - 1].clone(),
165            ]
166        } else {
167            register_names.clone()
168        }
169    };
170
171    let address_size = table
172        .get("address_size")
173        .and_then(|v| v.as_integer())
174        .unwrap_or(32) as u32;
175
176    let bytes_per_unit = table
177        .get("bytes_per_unit")
178        .and_then(|v| v.as_integer())
179        .unwrap_or(1) as u32;
180
181    let flags = if let Some(f) = table.get("flags") {
182        f.as_array()
183            .ok_or("flags must be an array")?
184            .iter()
185            .map(|v| {
186                v.as_str()
187                    .ok_or("flags entries must be strings")
188                    .map(|s| s.to_string())
189            })
190            .collect::<Result<Vec<String>, _>>()?
191    } else {
192        Vec::new()
193    };
194
195    let operand_types = if let Some(ot) = table.get("operand_types") {
196        let ot_table = ot.as_table().ok_or("operand_types must be a table")?;
197        let mut map = HashMap::new();
198        for (key, val) in ot_table {
199            let kind_str = val.as_str().ok_or("operand_types values must be strings")?;
200            let kind = match kind_str {
201                "register" | "reg" => OperandKind::Register,
202                "immediate" | "imm" => OperandKind::Immediate,
203                "address" | "addr" => OperandKind::Address,
204                "memory" | "mem" => OperandKind::Memory,
205                _ => {
206                    return Err(format!(
207                        "unknown operand type '{}' for field '{}'",
208                        kind_str, key
209                    ));
210                }
211            };
212            map.insert(key.clone(), kind);
213        }
214        map
215    } else {
216        HashMap::new()
217    };
218
219    let display_prefixes = if let Some(dp) = table.get("display_prefixes") {
220        let dp_table = dp.as_table().ok_or("display_prefixes must be a table")?;
221        let mut map = HashMap::new();
222        for (key, val) in dp_table {
223            let prefix = val
224                .as_str()
225                .ok_or("display_prefixes values must be strings")?;
226            map.insert(key.clone(), prefix.to_string());
227        }
228        map
229    } else {
230        HashMap::new()
231    };
232
233    let flow = if let Some(f) = table.get("flow") {
234        let f_table = f.as_table().ok_or("flow must be a table")?;
235        FlowConfig {
236            calls: parse_string_array(f_table, "calls")?,
237            branches: parse_string_array(f_table, "branches")?,
238            unconditional_branches: parse_string_array(f_table, "unconditional_branches")?,
239            returns: parse_string_array(f_table, "returns")?,
240            stops: parse_string_array(f_table, "stops")?,
241        }
242    } else {
243        FlowConfig::default()
244    };
245
246    Ok(IdaOptions {
247        processor_name,
248        processor_long_name,
249        processor_id,
250        register_names,
251        segment_registers,
252        address_size,
253        bytes_per_unit,
254        flags,
255        operand_types,
256        display_prefixes,
257        flow,
258    })
259}
260
261fn parse_string_array(
262    table: &toml::map::Map<String, toml::Value>,
263    key: &str,
264) -> Result<Vec<String>, String> {
265    if let Some(v) = table.get(key) {
266        v.as_array()
267            .ok_or(format!("{} must be an array", key))?
268            .iter()
269            .map(|v| {
270                v.as_str()
271                    .ok_or(format!("{} entries must be strings", key))
272                    .map(|s| s.to_string())
273            })
274            .collect()
275    } else {
276        Ok(Vec::new())
277    }
278}