mod common;
use assert2::assert;
use idakit::prelude::*;
#[test]
fn write() {
common::with_canonical_db(run);
}
fn run(idb: &mut idakit::Database) {
let address = idb.functions().next().expect("a function").address();
comment_round_trips(idb, address);
patch_round_trips(idb, address);
patch_rejects_unmapped(idb);
type_apply(idb, address);
type_define(idb);
type_build(idb, address);
type_function_build(idb, address);
type_surgery(idb, address);
type_clear(idb, address);
type_member_edit(idb);
type_enum_member_edit(idb);
type_member_ref(idb);
type_member_add_at_offset(idb);
type_member_offset_edit(idb);
type_function_edit_direct(idb, address);
type_named_arg_renders(idb, address);
type_build_failed(idb, address);
println!(
"write OK: comment round-trip, patch round-trip, unmapped patch rejected, type apply + define + build + function-build + surgery + clear + member-edit + enum-member-edit + member-ref + offset-insert + offset-edit + direct function-edit + named-arg render + build-failed"
);
}
fn comment_round_trips(idb: &mut idakit::Database, address: Address) {
let mut loc = idb.at_mut(address);
loc.set_comment("idakit regular", false)
.expect("set regular comment");
loc.set_comment("idakit repeatable", true)
.expect("set repeatable comment");
assert!(loc.comment().as_deref() == Some("idakit regular"));
assert!(loc.repeatable_comment().as_deref() == Some("idakit repeatable"));
assert!(
loc.comment() != loc.repeatable_comment(),
"regular and repeatable channels should be distinct"
);
}
fn patch_round_trips(idb: &mut idakit::Database, address: Address) {
let original = idb.at(address).bytes(4);
assert!(original.len() == 4, "need 4 readable bytes at the entry");
let flipped: Vec<u8> = original.iter().map(|b| !b).collect();
let mut loc = idb.at_mut(address);
loc.patch(&flipped).expect("patch failed");
assert!(
loc.bytes(4) == flipped,
"read-back should show patched bytes"
);
loc.patch(&original).expect("restore failed");
assert!(
loc.bytes(4) == original,
"restore should return the originals"
);
}
fn patch_rejects_unmapped(idb: &mut idakit::Database) {
let nowhere = Address::new_const(0xffff_ffff_f000);
let r = idb.at_mut(nowhere).patch(&[0x90, 0x90]);
assert!(let Err(Error::WriteRejected { op: "patch", .. }) = r);
}
fn type_apply(idb: &mut idakit::Database, address: Address) {
idb.function_mut(address)
.expect("a function at the entry")
.set_type("int idakit_probe(int a, int b)")
.expect("apply function prototype");
assert!(
idb.function(address).prototype().is_some(),
"a prototype should be set after apply"
);
let r = idb.at_mut(address).set_type("idakit_no_such_type_zzz");
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoType { .. }
}) = r
);
match idb
.at_mut(address)
.set_type(idakit::types::expr::decl("%%% not a type %%%"))
{
Err(Error::TypeWrite {
source: TypeWriteError::ParseFailed { reason, .. },
}) => {
println!("type-apply parse-error reason: {reason:?}");
}
other => panic!("garbage decl should be ParseFailed, got {other:?}"),
}
}
fn type_define(idb: &mut idakit::Database) {
idb.types_mut()
.define("struct idakit_pt { int x; int y; };")
.expect("define struct");
let names: Vec<String> = idb.named_types().map(|t| t.name()).collect();
assert!(
names.iter().any(|n| n == "idakit_pt"),
"the defined struct should appear in named types"
);
let entry = idb.functions().next().expect("a function").address();
idb.types_mut()
.define("typedef int idakit_fn_t(int arg);")
.expect("define function typedef");
idb.with_function_mut(entry, |f| f.set_type("idakit_fn_t"))
.expect("a function at the entry")
.expect("apply named function type");
assert!(
idb.function(entry).prototype().is_some(),
"a prototype should be set after applying a named function type"
);
let r = idb.with_location_mut(entry, |loc| {
loc.set_type(idakit::types::expr::decl("idakit_pt *"))
});
assert!(
!matches!(
r,
Err(Error::TypeWrite {
source: TypeWriteError::ParseFailed { .. }
})
),
"a decl referencing a defined local type must not fail parsing, got {r:?}"
);
let r = idb
.types_mut()
.define("struct idakit_broken { this is not valid");
assert!(let Err(Error::TypeDefineFailed { .. }) = r);
}
fn type_build(idb: &mut idakit::Database, address: Address) {
use idakit::types::expr;
idb.types_mut()
.define("struct idakit_built_pt { int x; int y; };")
.expect("define struct for the build path");
let built = idb
.at_mut(address)
.set_type(expr::named("idakit_built_pt").pointer());
let text = idb
.at_mut(address)
.set_type(expr::decl("idakit_built_pt *"));
assert!(
built.is_ok() == text.is_ok(),
"a built composite should agree with its text declaration: built={built:?} text={text:?}"
);
println!("type-build composite applied: {}", built.is_ok());
let r = idb
.at_mut(address)
.set_type(expr::named("idakit_no_such_built").pointer());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::ApplyRejected { .. }
}) = r
);
}
fn type_function_build(idb: &mut idakit::Database, entry: Address) {
use idakit::types::{TypeShape, expr};
idb.function_mut(entry)
.expect("a function at the entry")
.set_type(
expr::function(expr::int32())
.arg(expr::int32())
.arg(expr::decl("unsigned int")),
)
.expect("apply a built prototype");
let proto = idb
.function(entry)
.prototype_type()
.expect("walk the built prototype")
.expect("a prototype is set after the built apply");
let TypeShape::Function {
ret,
params,
varargs,
} = proto.shape()
else {
panic!(
"the built prototype should be a function, got {:?}",
proto.shape()
);
};
assert!(!*varargs, "the fixed prototype is not variadic");
assert!(params.len() == 2, "two parameters, got {}", params.len());
assert!(
proto.get(*ret).shape
== TypeShape::Int {
bytes: 4,
signed: true,
},
"the return type should be a signed 32-bit int"
);
idb.function_mut(entry)
.expect("a function at the entry")
.set_type(expr::function(expr::void()).arg(expr::int32()).variadic())
.expect("apply a variadic prototype");
let proto = idb
.function(entry)
.prototype_type()
.expect("walk the variadic prototype")
.expect("a prototype is set after the variadic apply");
assert!(let TypeShape::Function { varargs: true, .. } = proto.shape());
idb.function_mut(entry)
.expect("a function at the entry")
.set_type(
expr::function(expr::int32())
.arg(expr::int32())
.calling_convention(CallingConvention::Cdecl),
)
.expect("apply a prototype with a calling convention");
assert!(
idb.function(entry).prototype().is_some(),
"a prototype should be set after applying a cc-carrying builder"
);
}
fn type_surgery(idb: &mut idakit::Database, entry: Address) {
use idakit::types::{TypeShape, expr};
idb.function_mut(entry)
.expect("a function at the entry")
.set_type("int idakit_surgery_probe(int a, int b)")
.expect("seed a prototype to edit");
idb.function_mut(entry)
.expect("a function at the entry")
.set_return_type(expr::char_().pointer())
.expect("set the return type");
idb.function_mut(entry)
.expect("a function at the entry")
.set_arg_type(0, expr::decl("unsigned int"))
.expect("retype arg 0");
idb.function_mut(entry)
.expect("a function at the entry")
.rename_arg(1, "idakit_count")
.expect("rename arg 1");
let proto = idb
.function(entry)
.prototype_type()
.expect("walk the edited prototype")
.expect("a prototype is set");
let TypeShape::Function { ret, params, .. } = proto.shape() else {
panic!("expected a function, got {:?}", proto.shape());
};
assert!(let TypeShape::Ptr(_) = &proto.get(*ret).shape);
assert!(params.len() == 2, "still two params, got {}", params.len());
assert!(let TypeShape::Int { signed: false, .. } = &proto.get(params[0]).shape);
let text = idb.function(entry).prototype().expect("prototype text");
assert!(
text.contains("idakit_count"),
"the renamed arg should render: {text:?}"
);
let r = idb
.function_mut(entry)
.expect("a function at the entry")
.set_arg_type(9, expr::int32());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::ArgIndexOutOfRange {
index: 9,
arity: 2,
..
}
}) = r
);
idb.function_mut(entry)
.expect("a function at the entry")
.prepend_this(expr::void().pointer())
.expect("prepend a this-pointer");
let proto = idb
.function(entry)
.prototype_type()
.expect("walk after prepend_this")
.expect("a prototype is set");
let TypeShape::Function { params, .. } = proto.shape() else {
panic!("expected a function, got {:?}", proto.shape());
};
assert!(
params.len() == 3,
"the this-pointer should be prepended, got {}",
params.len()
);
let text = idb.function(entry).prototype().expect("prototype text");
assert!(
text.contains("this"),
"the this arg should render: {text:?}"
);
idb.function_mut(entry)
.expect("a function at the entry")
.set_calling_convention(CallingConvention::Cdecl)
.expect("set the calling convention");
idb.at_mut(entry).clear_type().expect("clear the prototype");
let r = idb
.function_mut(entry)
.expect("a function at the entry")
.set_return_type(expr::int32());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoPrototype { .. }
}) = r
);
}
fn type_clear(idb: &mut idakit::Database, entry: Address) {
idb.function_mut(entry)
.expect("a function at the entry")
.set_type("int idakit_clear_probe(int a)")
.expect("apply a prototype to clear");
assert!(
idb.function(entry).prototype().is_some(),
"a prototype should be set before the clear"
);
idb.at_mut(entry).clear_type().expect("clear the type");
assert!(
idb.function(entry).prototype().is_none(),
"the prototype should be gone after clear"
);
idb.at_mut(entry)
.clear_type()
.expect("a second clear is an idempotent success");
}
fn type_member_edit(idb: &mut idakit::Database) {
use idakit::types::{TypeEditCode, TypeWriteError, expr};
fn member_names(idb: &idakit::Database, ty: &str) -> Vec<String> {
let t = idb.type_named(ty).expect("resolve the type");
t.members()
.expect("a struct has members")
.iter()
.map(|m| m.name.clone())
.collect()
}
idb.types_mut()
.define("struct idakit_member_probe { int a; int b; };")
.expect("define a struct to edit");
idb.types_mut()
.edit("idakit_member_probe")
.add_member("c", expr::int32())
.expect("append member c");
assert!(
member_names(idb, "idakit_member_probe") == ["a", "b", "c"],
"c should be appended after a and b"
);
idb.types_mut()
.edit("idakit_member_probe")
.member_at(32)
.rename("beta")
.expect("rename the member at bit 32 by offset");
assert!(
member_names(idb, "idakit_member_probe") == ["a", "beta", "c"],
"the middle member should be renamed by offset"
);
idb.types_mut()
.edit("idakit_member_probe")
.member("c")
.delete()
.expect("delete c by name");
assert!(
member_names(idb, "idakit_member_probe") == ["a", "beta"],
"c should be gone, leaving a and beta"
);
let dup = idb
.types_mut()
.edit("idakit_member_probe")
.member("beta")
.rename("a");
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::Rejected {
code: TypeEditCode::DupName,
..
}
}) = dup
);
let ghost = idb
.types_mut()
.edit("idakit_member_probe")
.member("ghost")
.set_type(expr::int32());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoMember { .. }
}) = ghost
);
let no_type = idb
.types_mut()
.edit("idakit_no_such_struct")
.add_member("x", expr::int32());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoType { .. }
}) = no_type
);
idb.types_mut()
.edit("idakit_member_probe")
.member("a")
.set_type(expr::char_())
.expect("retype member a to char");
let probe = idb
.type_named("idakit_member_probe")
.expect("resolve probe");
let a = probe
.members()
.expect("a struct has members")
.iter()
.find(|m| m.name == "a")
.expect("member a");
assert!(
probe.get(a.ty).size == Some(1),
"member a should now be a one-byte char"
);
}
fn type_enum_member_edit(idb: &mut idakit::Database) {
use idakit::types::{TypeShape, TypeWriteError};
fn constants(idb: &idakit::Database, ty: &str) -> Vec<(String, u64)> {
let t = idb.type_named(ty).expect("resolve the enum");
match t.shape() {
TypeShape::Enum { members, .. } => {
members.iter().map(|m| (m.name.clone(), m.value)).collect()
}
other => panic!("expected an enum, got {other:?}"),
}
}
idb.types_mut()
.define("enum idakit_enum_probe { PROBE_A = 1, PROBE_B = 2 };")
.expect("define an enum to edit");
idb.types_mut()
.edit("idakit_enum_probe")
.add_constant("PROBE_C", 3)
.expect("add a constant");
assert!(
constants(idb, "idakit_enum_probe").contains(&("PROBE_C".to_owned(), 3)),
"PROBE_C = 3 should be added"
);
idb.types_mut()
.edit("idakit_enum_probe")
.constant("PROBE_A")
.set_value(10)
.expect("change a constant value");
assert!(
constants(idb, "idakit_enum_probe").contains(&("PROBE_A".to_owned(), 10)),
"PROBE_A should now be 10"
);
idb.types_mut()
.edit("idakit_enum_probe")
.constant("PROBE_B")
.rename("PROBE_BETA")
.expect("rename a constant");
let names: Vec<String> = constants(idb, "idakit_enum_probe")
.into_iter()
.map(|(n, _)| n)
.collect();
assert!(
names.iter().any(|n| n == "PROBE_BETA") && !names.iter().any(|n| n == "PROBE_B"),
"PROBE_B should be renamed to PROBE_BETA, got {names:?}"
);
idb.types_mut()
.edit("idakit_enum_probe")
.constant("PROBE_C")
.delete()
.expect("delete a constant");
assert!(
!constants(idb, "idakit_enum_probe")
.iter()
.any(|(n, _)| n == "PROBE_C"),
"PROBE_C should be gone"
);
let ghost = idb
.types_mut()
.edit("idakit_enum_probe")
.constant("PROBE_GHOST")
.set_value(9);
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoMember { .. }
}) = ghost
);
let no_type = idb
.types_mut()
.edit("idakit_no_such_enum")
.add_constant("X", 1);
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::NoType { .. }
}) = no_type
);
let dup = idb
.types_mut()
.edit("idakit_enum_probe")
.constant("PROBE_A")
.rename("PROBE_BETA");
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::Rejected { .. }
}) = dup
);
}
fn type_member_ref(idb: &mut idakit::Database) {
use idakit::types::{TypeWriteError, expr};
fn names(idb: &idakit::Database, ty: &str) -> Vec<String> {
idb.type_named(ty)
.expect("resolve the type")
.members()
.expect("a struct has members")
.iter()
.map(|m| m.name.clone())
.collect()
}
idb.types_mut()
.define("struct idakit_ref_probe { int a; int b; int c; };")
.expect("define a struct for the ref");
let r = idb
.types_mut()
.edit("idakit_ref_probe")
.member_ref(1)
.expect("mint a member ref");
assert!(r.index() == 1);
assert!(r.type_name() == "idakit_ref_probe");
idb.types_mut()
.edit("idakit_ref_probe")
.member("a")
.rename("alpha")
.expect("rename a");
idb.types_mut()
.edit("idakit_ref_probe")
.member_by_ref(&r)
.expect("the ref survives an unrelated rename")
.rename("beta")
.expect("rename b through the ref");
assert!(
names(idb, "idakit_ref_probe") == ["alpha", "beta", "c"],
"the ref should have renamed the middle member"
);
idb.types_mut()
.edit("idakit_ref_probe")
.add_member("d", expr::int32())
.expect("append d");
let mut types = idb.types_mut();
let mut edit = types.edit("idakit_ref_probe");
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::StaleMemberRef { .. }
}) = edit.member_by_ref(&r)
);
let oob = idb.types_mut().edit("idakit_ref_probe").member_ref(99);
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::MemberIndexOutOfRange { index: 99, .. }
}) = oob
);
idb.types_mut()
.define("struct idakit_gap_ref { int p; int q; int r; };")
.expect("define a struct for the gap case");
let gref = idb
.types_mut()
.edit("idakit_gap_ref")
.member_ref(1)
.expect("mint a ref to q");
idb.types_mut()
.edit("idakit_gap_ref")
.member("q")
.delete()
.expect("delete the middle member");
let mut types = idb.types_mut();
let mut edit = types.edit("idakit_gap_ref");
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::StaleMemberRef { .. }
}) = edit.member_by_ref(&gref)
);
}
fn type_member_add_at_offset(idb: &mut idakit::Database) {
use idakit::types::expr;
idb.types_mut()
.define("struct idakit_insert_probe { char a; int c; };")
.expect("define a struct with alignment padding after a");
let gap_start = {
let probe = idb
.type_named("idakit_insert_probe")
.expect("resolve the type");
let members = probe.members().expect("a struct has members");
let a = members.iter().find(|m| m.name == "a").expect("member a");
let c = members.iter().find(|m| m.name == "c").expect("member c");
let after_a = a.bit_offset + 8;
if c.bit_offset <= after_a {
None
} else {
Some(after_a)
}
};
let Some(gap_start) = gap_start else {
println!("skipping add_member_at: this til leaves no alignment gap after a char");
return;
};
idb.types_mut()
.edit("idakit_insert_probe")
.add_member_at(gap_start, "b", expr::char_())
.expect("insert b into the alignment gap");
let probe = idb
.type_named("idakit_insert_probe")
.expect("resolve after insert");
let landed = probe
.members()
.expect("a struct has members")
.iter()
.any(|m| m.name == "b" && m.bit_offset == gap_start);
assert!(landed, "b should be inserted at bit offset {gap_start}");
}
fn type_member_offset_edit(idb: &mut idakit::Database) {
use idakit::types::{TypeShape, expr};
idb.types_mut()
.define("struct idakit_offset_probe { int a; int b; int c; };")
.expect("define a struct to edit by offset");
idb.types_mut()
.edit("idakit_offset_probe")
.member_at(32)
.set_type(expr::decl("unsigned int"))
.expect("retype the member at bit 32");
let probe = idb
.type_named("idakit_offset_probe")
.expect("resolve the type");
let b = probe
.members()
.expect("a struct has members")
.iter()
.find(|m| m.name == "b")
.expect("member b");
assert!(let TypeShape::Int { signed: false, .. } = &probe.get(b.ty).shape);
idb.types_mut()
.edit("idakit_offset_probe")
.member_at(64)
.delete()
.expect("delete the member at bit 64");
let probe = idb
.type_named("idakit_offset_probe")
.expect("resolve after offset delete");
let names: Vec<String> = probe
.members()
.expect("a struct has members")
.iter()
.map(|m| m.name.clone())
.collect();
assert!(
!names.iter().any(|n| n == "c"),
"c should be gone after offset delete, got {names:?}"
);
}
fn type_function_edit_direct(idb: &mut idakit::Database, entry: Address) {
idb.function_mut(entry)
.expect("a function at the entry")
.set_type("int idakit_direct_probe(int a)")
.expect("seed a prototype to clear");
idb.function_mut(entry)
.expect("a function at the entry")
.clear_type()
.expect("clear_type via FunctionEdit");
assert!(
idb.function(entry).prototype().is_none(),
"the prototype should be gone after FunctionEdit::clear_type"
);
let original = idb.function(entry).name();
idb.function_mut(entry)
.expect("a function at the entry")
.rename("idakit_direct_rename_probe")
.expect("rename via FunctionEdit");
assert!(idb.function(entry).name().as_str() == "idakit_direct_rename_probe");
idb.function_mut(entry)
.expect("a function at the entry")
.rename(original.as_str())
.expect("restore the original name");
assert!(idb.function(entry).name().as_str() == original.as_str());
}
fn type_named_arg_renders(idb: &mut idakit::Database, entry: Address) {
use idakit::types::expr;
idb.function_mut(entry)
.expect("a function at the entry")
.set_type(expr::function(expr::int32()).named_arg("myparam", expr::int32()))
.expect("apply a prototype with a named arg");
let text = idb.function(entry).prototype().expect("prototype text");
assert!(
text.contains("myparam"),
"the builder-supplied param name should render: {text:?}"
);
}
fn type_build_failed(idb: &mut idakit::Database, address: Address) {
use idakit::types::expr;
let r = idb
.at_mut(address)
.set_type(expr::decl("%%% not a type %%%").pointer());
assert!(
let Err(Error::TypeWrite {
source: TypeWriteError::BuildFailed { .. }
}) = r
);
}