Skip to main content

edits/
edits.rs

1//! Write-side cursors end to end: rename, comment, patch, type-apply, and signature surgery through
2//! `at_mut`, `function_mut`, and `types_mut`.
3//!
4//! A tour of the write idioms: the read-capable cursor (read-modify-write with no re-borrow), the
5//! `&str` classifier (name vs declaration), the `Option`/`Result` shapes of the acquirers, the
6//! `TypeExpr` builder (scalar leaves, pointers, arrays, qualifiers composed off the kernel),
7//! struct-member surgery (`edit(..).member(..)`), and the type errors (`TypeDefineFailed` and the
8//! unified type-write `TypeWriteError`).
9//! Nothing is persisted: the database closes with `save = false`, and the name and bytes it touches
10//! are restored first.
11//!
12//! Run: cargo run -p idakit --example edits -- path/to/database.i64
13
14use idakit::prelude::*;
15
16fn main() -> Result<(), Box<dyn std::error::Error>> {
17    let db = std::env::args()
18        .nth(1)
19        .expect("usage: edits <path/to/database.i64>");
20
21    Ida::run(move |ida| -> Result<(), Error> {
22        ida.call(move |idb| -> Result<(), Error> {
23            idb.open(&db).call()?;
24
25            let entry = idb
26                .functions()
27                .next()
28                .expect("the database has a function")
29                .address();
30
31            name_and_comment(idb, entry)?;
32            function_types(idb, entry)?;
33            signature_surgery(idb, entry)?;
34            defining_types(idb)?;
35            editing_members(idb)?;
36            building_types(idb, entry);
37            classifier_and_errors(idb, entry);
38
39            idb.close(false);
40            println!("\nEDITS OK");
41            Ok(())
42        })?
43    })??;
44
45    Ok(())
46}
47
48/// The read-capable address cursor: rename, comment, and patch on one `at_mut`, each read back
49/// without a re-borrow. The name and bytes are restored so nothing leaks past `save = false`.
50fn name_and_comment(idb: &mut Database, ea: Address) -> Result<(), Error> {
51    println!("== at_mut: rename + comment + patch (one read-capable cursor) ==");
52
53    // `at` is the read view; `at_mut` the write cursor. Snapshot the name to put it back after.
54    let original_name = idb.at(ea).name();
55    println!("  {ea:#x} name before: {original_name:?}");
56
57    {
58        // Scope the cursor: its Drop coalesces invalidation, holding the borrow until then.
59        let mut loc = idb.at_mut(ea);
60        loc.rename("idakit_edit_demo")?;
61        loc.set_comment("renamed by the edits example", false)?;
62        loc.set_comment("shown at every xref", true)?;
63        // Read back through the SAME cursor: no drop-and-reborrow between the write and the read.
64        println!("  name after:      {:?}", loc.name());
65        println!("  regular    cmt:  {:?}", loc.comment());
66        println!("  repeatable cmt:  {:?}", loc.repeatable_comment());
67    }
68
69    // Patch a few bytes, confirm the read-back, then restore the originals.
70    let original_bytes = idb.at(ea).bytes(4);
71    let flipped: Vec<u8> = original_bytes.iter().map(|b| !b).collect();
72    {
73        let mut loc = idb.at_mut(ea);
74        loc.patch(&flipped)?;
75        println!(
76            "  patched 4 bytes, read-back matches: {}",
77            loc.bytes(4) == flipped
78        );
79        loc.patch(&original_bytes)?;
80    }
81
82    if let Some(name) = original_name {
83        idb.at_mut(ea).rename(name)?;
84    }
85    Ok(())
86}
87
88/// The noun cursor: apply a prototype through `function_mut` (an `Option`, keyed by the containing
89/// function). Shows the `Option<Result>` shape of the scoped-closure form and the `None` for an
90/// address inside no function.
91fn function_types(idb: &mut Database, ea: Address) -> Result<(), Error> {
92    println!("\n== function_mut: prototypes ==");
93    println!("  prototype before: {:?}", idb.function(ea).prototype());
94
95    // Acquire by key (a two-phase borrow keeps it a one-liner). `function_mut` is an `Option`: an
96    // address in no function yields `None`, never a cursor over nothing.
97    if let Some(mut f) = idb.function_mut(ea) {
98        f.set_type("int edits_probe(int a, int b)")?;
99    }
100    println!("  prototype after:  {:?}", idb.function(ea).prototype());
101
102    // The scoped-closure form returns `Option<Result<_>>` (None = no function, then the write's own
103    // Result). `.transpose()?` collapses both layers at once, the idiom for that shape.
104    idb.with_function_mut(ea, |f| f.set_type("void edits_probe(void)"))
105        .transpose()?;
106    println!("  prototype now:    {:?}", idb.function(ea).prototype());
107
108    // An address inside no function: the cursor is simply absent.
109    let nowhere = Address::new_const(0xffff_ffff_f000);
110    println!(
111        "  function_mut(unmapped) is_some: {}",
112        idb.function_mut(nowhere).is_some()
113    );
114
115    // For the entry address, `function_mut(ea).set_type` and `at_mut(ea).set_type` are the same
116    // apply today; `FunctionEdit` earns its own weight when signature surgery (return/arg edits)
117    // arrives.
118
119    // `clear_type` is the inverse of `set_type`; it removes the prototype and is idempotent.
120    idb.at_mut(ea).clear_type()?;
121    println!(
122        "  prototype after clear: {:?}",
123        idb.function(ea).prototype()
124    );
125    Ok(())
126}
127
128/// Signature surgery: read-modify-write one field at a time (return, an arg's type, an arg's name,
129/// an implicit `this`, the calling convention), each a typed [`Error::TypeWrite`] on failure.
130fn signature_surgery(idb: &mut Database, ea: Address) -> Result<(), Error> {
131    println!("\n== function_mut: signature surgery ==");
132
133    if let Some(mut f) = idb.function_mut(ea) {
134        f.set_type("int surgery_probe(int a, int b)")?;
135    }
136    println!("  seeded:        {:?}", idb.function(ea).prototype());
137
138    // Each verb reads the current prototype, changes one field, and re-applies.
139    if let Some(mut f) = idb.function_mut(ea) {
140        f.set_return_type(expr::char_().pointer())?; // int -> char *
141        f.set_arg_type(0, expr::decl("unsigned int"))?; // arg 0 -> unsigned int
142        f.rename_arg(1, "count")?; // arg 1 -> count
143        f.prepend_this(expr::void().pointer())?; // insert void *this
144        f.set_calling_convention(CallingConvention::Cdecl)?;
145    }
146    println!("  after surgery: {:?}", idb.function(ea).prototype());
147
148    // An out-of-range argument index is a typed error, not a panic.
149    if let Some(mut f) = idb.function_mut(ea)
150        && let Err(Error::TypeWrite { source }) = f.set_arg_type(99, expr::int32())
151    {
152        println!("  set_arg_type(99, ..) -> TypeWrite: {source}");
153    }
154    Ok(())
155}
156
157/// The capability cursor: `define` new named types, then reference one from a later declaration.
158fn defining_types(idb: &mut Database) -> Result<(), Error> {
159    println!("\n== types_mut: define ==");
160
161    idb.types_mut()
162        .define("struct edit_demo_t { int id; char *name; };")?;
163    let present = idb.named_types().any(|t| t.name() == "edit_demo_t");
164    println!("  defined edit_demo_t, present in named types: {present}");
165
166    // A declaration referencing the just-defined type resolves against the local til.
167    match idb
168        .types_mut()
169        .define("typedef struct edit_demo_t edit_demo_alias;")
170    {
171        Ok(()) => println!("  typedef alias to it: ok"),
172        Err(e) => println!("  typedef alias: {e}"),
173    }
174
175    // A malformed declaration is a typed error carrying IDA's own reason.
176    match idb.types_mut().define("struct broken { not valid") {
177        Err(Error::TypeDefineFailed { reason, .. }) => {
178            println!("  malformed define -> TypeDefineFailed: {reason}");
179        }
180        other => println!("  malformed define -> unexpected {other:?}"),
181    }
182    Ok(())
183}
184
185/// Member surgery on defined types: append/retype/rename struct fields through the `member(..)`
186/// sub-cursor (with the structured rejection when a rename collides), then add and revalue enum
187/// constants through `constant(..)`. Each edit auto-saves to the local til; nothing persists past
188/// `save = false`.
189fn editing_members(idb: &mut Database) -> Result<(), Error> {
190    use idakit::types::expr;
191
192    println!("\n== types_mut: edit members ==");
193
194    idb.types_mut()
195        .define("struct edit_member_demo { int a; int b; };")?;
196
197    idb.types_mut()
198        .edit("edit_member_demo")
199        .add_member("c", expr::int32())?;
200    idb.types_mut()
201        .edit("edit_member_demo")
202        .member("a")
203        .set_type(expr::char_())?;
204    idb.types_mut()
205        .edit("edit_member_demo")
206        .member("b")
207        .rename("beta")?;
208
209    let names: Vec<String> = idb
210        .type_named("edit_member_demo")?
211        .members()
212        .unwrap_or_default()
213        .iter()
214        .map(|m| m.name.clone())
215        .collect();
216    println!("  after add + retype + rename, members: {names:?}");
217
218    // Renaming onto an existing name surfaces the structured tinfo_code.
219    match idb
220        .types_mut()
221        .edit("edit_member_demo")
222        .member("c")
223        .rename("a")
224    {
225        Err(Error::TypeWrite {
226            source: TypeWriteError::Rejected { code, .. },
227        }) => println!("  duplicate rename -> Rejected({code})"),
228        other => println!("  duplicate rename -> unexpected {other:?}"),
229    }
230
231    // Enum constants use the same cursor: add, revalue, rename.
232    idb.types_mut()
233        .define("enum edit_enum_demo { DEMO_A = 1, DEMO_B = 2 };")?;
234    idb.types_mut()
235        .edit("edit_enum_demo")
236        .add_constant("DEMO_C", 3)?;
237    idb.types_mut()
238        .edit("edit_enum_demo")
239        .constant("DEMO_A")
240        .set_value(10)?;
241    let constants: Vec<(String, u64)> = match idb.type_named("edit_enum_demo")?.shape() {
242        TypeShape::Enum { members, .. } => {
243            members.iter().map(|m| (m.name.clone(), m.value)).collect()
244        }
245        _ => Vec::new(),
246    };
247    println!("  after add + revalue, constants: {constants:?}");
248    Ok(())
249}
250
251/// The `TypeExpr` builder: compose a recipe off the kernel (scalar-leaf roots, then the
252/// pointer/array/qualifier transforms), inspect it, and apply it through the same `set_type`. A
253/// composite lowers through the serialize-and-build facade, so the built form and its text twin
254/// reach one `tinfo`; `named(..).pointer()` over an unknown type fails at build time.
255fn building_types(idb: &mut Database, ea: Address) {
256    println!("\n== TypeExpr builder: compose + apply ==");
257
258    // Roots are free functions, transforms are methods, so a recipe reads left-to-right.
259    let uint_array = expr::uint32().array(4); // uint32[4]
260    let ptr_to_named = expr::named("edit_demo_t").pointer(); // edit_demo_t *
261    let const_ptr = expr::int32().const_().pointer(); // const int32 *
262    println!("  uint32().array(4)              -> {uint_array:<14} ({uint_array:?})");
263    println!("  named(\"edit_demo_t\").pointer() -> {ptr_to_named}");
264    println!("  int32().const_().pointer()     -> {const_ptr}");
265
266    // `deref` peels one layer, the inverse of `pointer`; qualifiers are idempotent.
267    println!(
268        "  (edit_demo_t *).deref() == named: {}",
269        ptr_to_named.clone().deref() == expr::named("edit_demo_t")
270    );
271
272    // Built recipes apply through the ordinary cursor. Whether a code entry accepts a data type is
273    // the kernel's call; the point is that the built form lowers and applies like its text twin.
274    report(
275        "set_type(uint32().array(4))",
276        idb.at_mut(ea).set_type(uint_array),
277    );
278    report(
279        "set_type(named(..).pointer())",
280        idb.at_mut(ea).set_type(ptr_to_named),
281    );
282
283    // A composite over an unknown named type builds fine, but the kernel refuses to apply it
284    // (unresolved pointee), an ApplyRejected.
285    report(
286        "set_type(named(\"no_such\").pointer())",
287        idb.at_mut(ea)
288            .set_type(expr::named("no_such_zzz").pointer()),
289    );
290
291    // A whole function prototype composes off the kernel too: a return root, then params.
292    let proto = expr::function(expr::int32())
293        .arg(expr::int32())
294        .named_arg("flags", expr::uint32())
295        .variadic()
296        .build();
297    println!("  function(int32).arg(int32).named_arg(\"flags\", uint32).variadic -> {proto}");
298    report(
299        "function_mut(entry).set_type(built prototype)",
300        idb.function_mut(ea)
301            .map_or(Ok(()), |mut f| f.set_type(proto)),
302    );
303}
304
305/// The `&str` classifier and the type-error taxonomy. The not-found and parse-failure paths fail
306/// before the kernel applies, so they never mutate; the closing `int` apply does reshape the item,
307/// which is harmless here since the database closes without saving.
308fn classifier_and_errors(idb: &mut Database, ea: Address) {
309    println!("\n== the &str classifier + type errors ==");
310
311    // `From<&str>`: a name that could exist routes by-name; a keyword or a declarator is parsed.
312    println!("  \"edit_demo_t\"   -> {:?}", TypeExpr::from("edit_demo_t"));
313    println!(
314        "  \"edit_demo_t *\" -> {:?}",
315        TypeExpr::from("edit_demo_t *")
316    );
317    println!(
318        "  \"int\"           -> {:?}  (a keyword: parsed, not looked up)",
319        TypeExpr::from("int")
320    );
321
322    // A bare unknown name takes the by-name path and reports a clean not-found, without mutating.
323    report(
324        "set_type(\"no_such_type_xyz\")",
325        idb.at_mut(ea).set_type("no_such_type_xyz"),
326    );
327
328    // An explicit `named` root forces by-name even for a keyword, so `int` is not-found here (no
329    // named type "int" exists), in contrast to the classifier, which parses a bare "int".
330    report(
331        "set_type(named(\"int\"))",
332        idb.at_mut(ea).set_type(expr::named("int")),
333    );
334
335    // A garbage declaration fails in the parser, before any apply; the reason is IDA's own.
336    report(
337        "set_type(decl(\"%%% junk %%%\"))",
338        idb.at_mut(ea).set_type(expr::decl("%%% junk %%%")),
339    );
340
341    // A builtin keyword like "int" parses and applies, rather than reporting a spurious not-found
342    // from a by-name lookup that no til would ever satisfy.
343    report("set_type(\"int\")", idb.at_mut(ea).set_type("int"));
344}
345
346/// Prints how one `set_type` call resolved, naming the error variant on failure.
347fn report(call: &str, r: Result<(), Error>) {
348    match r {
349        Ok(()) => println!("  {call} -> Ok"),
350        Err(Error::TypeWrite {
351            source: TypeWriteError::NoType { name },
352        }) => {
353            println!("  {call} -> NoType {{ name: {name:?} }}");
354        }
355        Err(Error::TypeWrite {
356            source: TypeWriteError::ParseFailed { reason, .. },
357        }) => {
358            println!("  {call} -> ParseFailed: {reason}");
359        }
360        Err(Error::TypeWrite {
361            source: TypeWriteError::ApplyRejected { reason, .. },
362        }) => {
363            println!("  {call} -> ApplyRejected: {reason}");
364        }
365        Err(e) => println!("  {call} -> {e}"),
366    }
367}