use std::collections::hash_map::DefaultHasher;
use std::ffi::c_int;
use std::fmt;
use std::hash::{Hash, Hasher};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use snafu::Snafu;
use strum::VariantArray;
use idakit_sys as sys;
use crate::Database;
use crate::error::{Error, Result};
use crate::ffi::{nul_checked, reason_or};
use crate::types::diff::AggregateKind;
use crate::types::{TypeExpr, ValueRepr};
const DEFMASK64: u64 = u64::MAX;
const ETF_COMPATIBLE: u32 = 0x0000_0008;
const ETF_FORCENAME: u32 = 0x0000_0020;
const BTF_STRUCT: u32 = 0x0D;
const BTF_UNION: u32 = 0x1D;
const BTF_ENUM: u32 = 0x2D;
const fn decl_type_of(kind: AggregateKind) -> u32 {
match kind {
AggregateKind::Struct => BTF_STRUCT,
AggregateKind::Union => BTF_UNION,
AggregateKind::Enum => BTF_ENUM,
}
}
impl Database {
#[inline]
#[must_use]
pub fn types_mut(&mut self) -> TypesMut<'_> {
TypesMut { db: self }
}
}
pub struct TypesMut<'db> {
db: &'db mut Database,
}
impl TypesMut<'_> {
#[doc(alias("parse_decls"))]
pub fn define(&mut self, decl: impl AsRef<str>) -> Result<()> {
let decl = decl.as_ref();
let result = self.db.define_type(nul_checked(decl, "decl")?);
if result.code == 0 {
Ok(())
} else {
Err(Error::TypeDefineFailed {
decl: decl.to_owned(),
reason: reason_or(&result.reason, "the declaration is not valid"),
})
}
}
#[doc(alias("del_named_type"))]
pub fn delete(&mut self, name: impl AsRef<str>) -> Result<()> {
let name = name.as_ref();
let result = self.db.delete_type(nul_checked(name, "name")?);
edit_result(result.code, &result.reason, name, None)
}
#[doc(alias("rename_type"))]
pub fn rename(&mut self, name: impl AsRef<str>, new_name: impl AsRef<str>) -> Result<()> {
let name = name.as_ref();
let new_name = nul_checked(new_name.as_ref(), "new name")?;
let result = self.db.rename_type(nul_checked(name, "name")?, new_name);
edit_result(result.code, &result.reason, name, None)
}
#[doc(alias("create_forward_decl"))]
pub fn forward_declare(&mut self, name: impl AsRef<str>, kind: AggregateKind) -> Result<()> {
let name = name.as_ref();
let result = self
.db
.forward_declare_type(nul_checked(name, "name")?, decl_type_of(kind));
edit_result(result.code, &result.reason, name, None)
}
#[inline]
#[must_use]
pub fn edit(&mut self, name: impl Into<String>) -> TypeEdit<'_> {
TypeEdit {
db: self.db,
name: name.into(),
}
}
}
impl fmt::Debug for TypesMut<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypesMut").finish_non_exhaustive()
}
}
pub struct TypeEdit<'db> {
db: &'db mut Database,
name: String,
}
impl TypeEdit<'_> {
#[doc(alias("add_udm"))]
pub fn add_member(&mut self, name: impl AsRef<str>, ty: impl Into<TypeExpr>) -> Result<()> {
self.add_member_impl(name.as_ref(), &ty.into(), sys::MEMBER_APPEND)
}
#[doc(alias("add_udm"))]
pub fn add_member_at(
&mut self,
bit_offset: u64,
name: impl AsRef<str>,
ty: impl Into<TypeExpr>,
) -> Result<()> {
self.add_member_impl(name.as_ref(), &ty.into(), bit_offset)
}
fn add_member_impl(&mut self, name: &str, ty: &TypeExpr, member_bit: u64) -> Result<()> {
let recipe = ty.checked_serialize()?;
let type_name = self.name.clone();
let result = self.db.udt_add_member(
nul_checked(&type_name, "type name")?,
nul_checked(name, "member name")?,
&recipe,
member_bit,
);
edit_result(result.code, &result.reason, &type_name, None)
}
#[inline]
#[must_use]
pub fn member(&mut self, name: impl Into<String>) -> MemberEdit<'_> {
let type_name = self.name.clone();
MemberEdit {
db: &mut *self.db,
type_name,
key: MemberKey::Name(name.into()),
}
}
#[inline]
#[must_use]
pub fn member_at(&mut self, bit_offset: u64) -> MemberEdit<'_> {
let type_name = self.name.clone();
MemberEdit {
db: &mut *self.db,
type_name,
key: MemberKey::Offset(bit_offset),
}
}
#[doc(alias("add_edm"))]
pub fn add_constant(&mut self, name: impl AsRef<str>, value: u64) -> Result<()> {
self.add_member_at_mask(name.as_ref(), value, DEFMASK64, 0)
}
#[doc(alias("add_edm"))]
pub fn add_constant_forced(&mut self, name: impl AsRef<str>, value: u64) -> Result<()> {
self.add_member_at_mask(name.as_ref(), value, DEFMASK64, ETF_FORCENAME)
}
#[doc(alias("add_edm"))]
pub fn add_flag(&mut self, name: impl AsRef<str>, value: u64, mask: u64) -> Result<()> {
self.add_member_at_mask(name.as_ref(), value, mask, 0)
}
#[doc(alias("add_edm"))]
pub fn add_flag_forced(&mut self, name: impl AsRef<str>, value: u64, mask: u64) -> Result<()> {
self.add_member_at_mask(name.as_ref(), value, mask, ETF_FORCENAME)
}
fn add_member_at_mask(
&mut self,
name: &str,
value: u64,
mask: u64,
etf_flags: u32,
) -> Result<()> {
let type_name = self.name.clone();
let result = self.db.enum_add_member(
nul_checked(&type_name, "type name")?,
nul_checked(name, "constant name")?,
value,
mask,
etf_flags,
);
edit_result(result.code, &result.reason, &type_name, None)
}
#[doc(alias("set_enum_is_bitmask"))]
pub fn set_bitmask(&mut self, on: bool) -> Result<()> {
let type_name = self.name.clone();
let result = self
.db
.enum_set_bitmask(nul_checked(&type_name, "type name")?, on);
edit_result(result.code, &result.reason, &type_name, None)
}
#[doc(alias("set_enum_repr"))]
pub fn set_repr(&mut self, repr: ValueRepr) -> Result<()> {
let type_name = self.name.clone();
let result = self.db.enum_set_repr(
nul_checked(&type_name, "type name")?,
u32::from(repr.format),
repr.signed,
repr.leading_zeros,
);
edit_result(result.code, &result.reason, &type_name, None)
}
pub fn set_enum_width(&mut self, nbytes: i32) -> Result<()> {
let type_name = self.name.clone();
let result = self
.db
.enum_set_width(nul_checked(&type_name, "type name")?, nbytes);
edit_result(result.code, &result.reason, &type_name, None)
}
#[doc(alias("del_edm_by_value"))]
pub fn delete_constant_by_value(&mut self, value: u64) -> Result<()> {
let type_name = self.name.clone();
let result = self
.db
.enum_del_member_by_value(nul_checked(&type_name, "type name")?, value);
edit_result(result.code, &result.reason, &type_name, None)
}
#[inline]
#[must_use]
pub fn constant(&mut self, name: impl Into<String>) -> ConstantEdit<'_> {
let type_name = self.name.clone();
ConstantEdit {
db: &mut *self.db,
type_name,
name: name.into(),
}
}
pub fn member_ref(&self, index: usize) -> Result<MemberRef> {
let (count, generation, _) = self.read_layout(index)?;
if index >= count {
return Err(TypeWriteError::MemberIndexOutOfRange {
type_name: self.name.clone(),
index,
count,
}
.into());
}
Ok(MemberRef {
type_name: self.name.clone(),
index,
generation,
})
}
pub fn member_by_ref(&mut self, member: &MemberRef) -> Result<MemberEdit<'_>> {
let name = self.name.clone();
let (count, generation, key) = self.read_layout(member.index)?;
let stale =
member.type_name != name || member.generation != generation || member.index >= count;
match key {
Some(key) if !stale => Ok(MemberEdit {
db: &mut *self.db,
type_name: name,
key,
}),
_ => Err(TypeWriteError::StaleMemberRef { type_name: name }.into()),
}
}
fn read_layout(&self, index: usize) -> Result<(usize, u64, Option<MemberKey>)> {
let ty = self
.db
.type_named(&self.name)
.map_err(|_| TypeWriteError::NoType {
name: self.name.clone(),
})?;
let members = ty.members().ok_or_else(|| TypeWriteError::NoType {
name: self.name.clone(),
})?;
let mut hasher = DefaultHasher::new();
members.len().hash(&mut hasher);
for member in members {
member.bit_offset.hash(&mut hasher);
let tv = ty.get(member.ty);
tv.size.hash(&mut hasher);
std::mem::discriminant(&tv.shape).hash(&mut hasher);
}
let key = members.get(index).map(|member| {
if member.name.is_empty() {
MemberKey::Offset(member.bit_offset)
} else {
MemberKey::Name(member.name.clone())
}
});
Ok((members.len(), hasher.finish(), key))
}
}
impl fmt::Debug for TypeEdit<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypeEdit")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MemberRef {
type_name: String,
index: usize,
generation: u64,
}
impl MemberRef {
#[inline]
#[must_use]
pub const fn index(&self) -> usize {
self.index
}
#[inline]
#[must_use]
pub fn type_name(&self) -> &str {
&self.type_name
}
}
pub struct MemberEdit<'db> {
db: &'db mut Database,
type_name: String,
key: MemberKey,
}
impl MemberEdit<'_> {
#[doc(alias("set_udm_type"))]
pub fn set_type(&mut self, ty: impl Into<TypeExpr>) -> Result<()> {
self.set_type_with_flags(ty, 0)
}
#[doc(alias("set_udm_type"))]
pub fn set_type_compatible(&mut self, ty: impl Into<TypeExpr>) -> Result<()> {
self.set_type_with_flags(ty, ETF_COMPATIBLE)
}
fn set_type_with_flags(&mut self, ty: impl Into<TypeExpr>, etf_flags: u32) -> Result<()> {
let recipe = ty.into().checked_serialize()?;
let result = self
.dispatch(|db, tp, mp, bit| db.udt_set_member_type(tp, mp, bit, &recipe, etf_flags))?;
edit_result(
result.code,
&result.reason,
&self.type_name,
Some(&self.key),
)
}
#[doc(alias("rename_udm"))]
pub fn rename(&mut self, new_name: impl AsRef<str>) -> Result<()> {
let new_name = nul_checked(new_name.as_ref(), "new member name")?;
let result =
self.dispatch(|db, tp, mp, bit| db.udt_rename_member(tp, mp, bit, new_name))?;
edit_result(
result.code,
&result.reason,
&self.type_name,
Some(&self.key),
)
}
#[doc(alias("set_udm_cmt"))]
pub fn comment(&mut self, text: impl AsRef<str>) -> Result<()> {
let text = nul_checked(text.as_ref(), "comment")?;
let result =
self.dispatch(|db, tp, mp, bit| db.udt_set_member_comment(tp, mp, bit, text))?;
edit_result(
result.code,
&result.reason,
&self.type_name,
Some(&self.key),
)
}
#[doc(alias("set_udm_repr"))]
pub fn set_repr(&mut self, repr: ValueRepr) -> Result<()> {
let result = self.dispatch(|db, tp, mp, bit| {
db.udt_set_member_repr(
tp,
mp,
bit,
u32::from(repr.format),
repr.signed,
repr.leading_zeros,
)
})?;
edit_result(
result.code,
&result.reason,
&self.type_name,
Some(&self.key),
)
}
#[doc(alias("del_udm"))]
pub fn delete(&mut self) -> Result<()> {
let result = self.dispatch(Database::udt_del_member)?;
edit_result(
result.code,
&result.reason,
&self.type_name,
Some(&self.key),
)
}
fn dispatch(
&mut self,
f: impl FnOnce(&mut Database, &str, &str, u64) -> sys::TypeWriteResult,
) -> Result<sys::TypeWriteResult> {
let type_name = nul_checked(&self.type_name, "type name")?;
let (member, bit) = match &self.key {
MemberKey::Name(n) => (nul_checked(n, "member name")?, 0),
MemberKey::Offset(o) => ("", *o),
};
Ok(f(&mut *self.db, type_name, member, bit))
}
}
impl fmt::Debug for MemberEdit<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemberEdit")
.field("type_name", &self.type_name)
.field("key", &self.key)
.finish_non_exhaustive()
}
}
pub struct ConstantEdit<'db> {
db: &'db mut Database,
type_name: String,
name: String,
}
impl ConstantEdit<'_> {
#[doc(alias("edit_edm"))]
pub fn set_value(&mut self, value: u64) -> Result<()> {
let result = self.dispatch(|db, tp, np| db.enum_set_member_value(tp, np, value))?;
self.result(result.code, &result.reason)
}
#[doc(alias("rename_edm"))]
pub fn rename(&mut self, new_name: impl AsRef<str>) -> Result<()> {
self.rename_with_flags(new_name.as_ref(), 0)
}
#[doc(alias("rename_edm"))]
pub fn rename_forced(&mut self, new_name: impl AsRef<str>) -> Result<()> {
self.rename_with_flags(new_name.as_ref(), ETF_FORCENAME)
}
fn rename_with_flags(&mut self, new_name: &str, etf_flags: u32) -> Result<()> {
let new_name = nul_checked(new_name, "new constant name")?;
let result =
self.dispatch(|db, tp, np| db.enum_rename_member(tp, np, new_name, etf_flags))?;
self.result(result.code, &result.reason)
}
#[doc(alias("del_edm"))]
pub fn delete(&mut self) -> Result<()> {
let result = self.dispatch(Database::enum_del_member)?;
self.result(result.code, &result.reason)
}
fn dispatch(
&mut self,
f: impl FnOnce(&mut Database, &str, &str) -> sys::TypeWriteResult,
) -> Result<sys::TypeWriteResult> {
let type_name = nul_checked(&self.type_name, "type name")?;
let name = nul_checked(&self.name, "constant name")?;
Ok(f(&mut *self.db, type_name, name))
}
fn result(&self, code: c_int, reason: &str) -> Result<()> {
edit_result(
code,
reason,
&self.type_name,
Some(&MemberKey::Name(self.name.clone())),
)
}
}
impl fmt::Debug for ConstantEdit<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConstantEdit")
.field("type_name", &self.type_name)
.field("name", &self.name)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum MemberKey {
Name(String),
Offset(u64),
}
impl fmt::Display for MemberKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Name(n) => write!(f, "named {n:?}"),
Self::Offset(o) => write!(f, "at bit offset {o}"),
}
}
}
fn edit_result(code: c_int, reason: &str, type_name: &str, key: Option<&MemberKey>) -> Result<()> {
match code {
0 => Ok(()),
sys::TEDIT_NO_TYPE => Err(TypeWriteError::NoType {
name: type_name.to_owned(),
}
.into()),
sys::TEDIT_NO_MEMBER => Err(TypeWriteError::NoMember {
type_name: type_name.to_owned(),
key: key.map(MemberKey::to_string).unwrap_or_default(),
}
.into()),
sys::TEDIT_BUILD => Err(TypeWriteError::BuildFailed {
reason: reason_or(
reason,
"an unknown named type or invalid declaration within it",
),
}
.into()),
n => match TypeEditCode::try_from(n) {
Ok(code) => Err(TypeWriteError::Rejected {
type_name: type_name.to_owned(),
code,
}
.into()),
Err(_) => Err(TypeWriteError::UnknownCode {
type_name: type_name.to_owned(),
code: n,
}
.into()),
},
}
}
#[derive(Debug, Snafu, PartialEq, Eq)]
#[snafu(visibility(pub(crate)))]
pub enum TypeWriteError {
#[snafu(display("no type named {name:?} in the local type library"))]
NoType {
name: String,
},
#[snafu(display("no member {key} in type {type_name:?}"))]
NoMember {
type_name: String,
key: String,
},
#[snafu(display(
"member index {index} out of range ({count} member(s)) in type {type_name:?}"
))]
MemberIndexOutOfRange {
type_name: String,
index: usize,
count: usize,
},
#[snafu(display(
"parameter index {index} out of range ({arity} parameter(s)) at {address:#x}"
))]
ArgIndexOutOfRange {
address: u64,
index: usize,
arity: usize,
},
#[snafu(display(
"member reference into {type_name:?} is stale (the layout changed since it was minted)"
))]
StaleMemberRef {
type_name: String,
},
#[snafu(display("no function prototype to edit at {address:#x}"))]
NoPrototype {
address: u64,
},
#[snafu(display("could not parse type declaration {decl:?}: {reason}"))]
ParseFailed {
decl: String,
reason: String,
},
#[snafu(display("could not build the type: {reason}"))]
BuildFailed {
reason: String,
},
#[snafu(display("editing {type_name:?} was rejected: {code}"))]
Rejected {
type_name: String,
code: TypeEditCode,
},
#[snafu(display("could not apply type at {address:#x}: {reason}"))]
ApplyRejected {
address: u64,
reason: String,
},
#[snafu(display("editing {type_name:?} returned an unmodeled type-edit code {code}"))]
UnknownCode {
type_name: String,
code: i32,
},
}
#[derive(
Clone, Copy, PartialEq, Eq, Hash, Debug, TryFromPrimitive, IntoPrimitive, VariantArray,
)]
#[repr(i32)]
#[doc(alias("tinfo_code_t"))]
pub enum TypeEditCode {
Ok = 0,
SaveError = -1,
Serialize = -2,
BadName = -3,
BadArg = -4,
BadType = -5,
BadSize = -6,
BadIndex = -7,
BadArray = -8,
BadBitfield = -9,
BadOffset = -10,
BadUnionVar = -11,
BadVarLast = -12,
Overlap = -13,
BadSubtype = -14,
BadValue = -15,
NoBitmask = -16,
BadBitmask = -17,
BadMaskValue = -18,
BadRepr = -19,
GroupNotEmpty = -20,
DupName = -21,
UnionBitfield = -22,
BadTah = -23,
BadBase = -24,
BadGap = -25,
Nested = -26,
NotCompatible = -27,
BadLayout = -28,
BadGroups = -29,
BadSerial = -30,
AlienName = -31,
Stock = -32,
EnumSize = -33,
NotImplemented = -34,
TypeWorse = -35,
BadFixedSize = -36,
StructSize = -37,
NotFound = -38,
}
impl TypeEditCode {
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::Ok => "ok",
Self::SaveError => "failed to save",
Self::Serialize => "failed to serialize",
Self::BadName => "the name is not acceptable",
Self::BadArg => "bad argument",
Self::BadType => "bad type",
Self::BadSize => "bad size",
Self::BadIndex => "bad index",
Self::BadArray => "arrays are forbidden as function arguments",
Self::BadBitfield => "bitfields are forbidden as function arguments",
Self::BadOffset => "bad member offset",
Self::BadUnionVar => "unions cannot have variable-sized members",
Self::BadVarLast => "a variable-sized member must be the last member",
Self::Overlap => "the member overlaps with members that cannot be deleted",
Self::BadValue => "the value is not acceptable",
Self::NoBitmask => "the bitmask was not found",
Self::BadBitmask => "bad enum member mask",
Self::BadMaskValue => "bad bitmask and value combination",
Self::BadRepr => "bad or incompatible field representation",
Self::GroupNotEmpty => "could not delete the group mask of a non-empty group",
Self::DupName => "duplicate name",
Self::UnionBitfield => "unions cannot have bitfields",
Self::BadTah => "bad type-attribute bits",
Self::BadBase => "bad base class",
Self::BadGap => "bad gap",
Self::BadSubtype | Self::Nested => "recursive structure nesting is forbidden",
Self::NotCompatible => "the new type is not compatible with the old type",
Self::BadLayout => "failed to calculate the structure/union layout",
Self::BadGroups => "bad group sizes for a bitmask enum",
Self::BadSerial => "the enum value has too many serials",
Self::AlienName => "the enum member name is used in another enum",
Self::Stock => "stock type info cannot be modified",
Self::EnumSize => "bad enum size",
Self::NotImplemented => "not implemented",
Self::TypeWorse => "the new type is worse than the old type",
Self::BadFixedSize => "cannot extend the struct beyond its fixed size",
Self::StructSize => "bad fixed structure size",
Self::NotFound => "the member was not found",
}
}
}
impl fmt::Display for TypeEditCode {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message())
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use idakit_sys as sys;
use rstest::rstest;
use super::*;
#[test]
fn message_and_display_report_the_code() {
for &code in TypeEditCode::VARIANTS {
assert!(!code.message().is_empty(), "{code:?} has an empty message");
}
assert!(TypeEditCode::Ok.message() == "ok");
assert!(TypeEditCode::NotFound.message() == "the member was not found");
assert!(format!("{}", TypeEditCode::AlienName) == TypeEditCode::AlienName.message());
assert!(!format!("{}", TypeEditCode::Ok).is_empty());
}
#[test]
fn tinfo_code_ids_align_with_the_facade() {
let ids = sys::tinfo_code_ids();
assert!(
ids.len() == TypeEditCode::VARIANTS.len(),
"facade lists {} ids for {} variants",
ids.len(),
TypeEditCode::VARIANTS.len()
);
for (i, &code) in TypeEditCode::VARIANTS.iter().enumerate() {
assert!(
ids[i] == i32::from(code),
"type edit code {code:?}: facade TERR_ {} != discriminant {}",
ids[i],
i32::from(code)
);
}
}
#[test]
fn type_edit_code_round_trips() {
for &v in TypeEditCode::VARIANTS {
let raw = i32::from(v);
assert!(TypeEditCode::try_from(raw) == Ok(v));
}
}
#[rstest]
#[case(TypeEditCode::Ok, 0)]
#[case(TypeEditCode::DupName, -21)]
#[case(TypeEditCode::Stock, -32)]
#[case(TypeEditCode::NotFound, -38)]
fn type_edit_code_pins_terr_values(#[case] code: TypeEditCode, #[case] expected: i32) {
assert!(i32::from(code) == expected);
}
#[test]
fn type_edit_code_rejects_unknown() {
assert!(TypeEditCode::try_from(-39).is_err());
assert!(TypeEditCode::try_from(1).is_err());
}
#[test]
fn member_ref_ord_sorts_by_type_then_index_then_generation() {
let a = MemberRef {
type_name: "Pt".to_owned(),
index: 0,
generation: 1,
};
let b = MemberRef {
type_name: "Pt".to_owned(),
index: 1,
generation: 0,
};
let c = MemberRef {
type_name: "Zz".to_owned(),
index: 0,
generation: 0,
};
assert!(a < b);
assert!(b < c);
assert!(a < c);
}
#[test]
fn member_key_renders() {
assert!(MemberKey::Name("hp".to_owned()).to_string() == "named \"hp\"");
assert!(MemberKey::Offset(64).to_string() == "at bit offset 64");
}
}