use std::fmt;
use idakit_sys as sys;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::VariantArray;
use crate::Database;
use crate::address::Address;
#[bon::bon]
impl Database {
#[inline]
#[must_use]
#[doc(alias("xrefblk_t", "first_to"))]
pub fn xrefs_to(&self, address: Address) -> Xrefs {
self.xrefs_to_with(address).call()
}
#[inline]
#[must_use]
#[doc(alias("xrefblk_t", "first_from"))]
pub fn xrefs_from(&self, address: Address) -> Xrefs {
self.xrefs_from_with(address).call()
}
#[builder]
#[doc(alias("xrefblk_t", "first_to"))]
pub fn xrefs_to_with(
&self,
#[builder(start_fn)] address: Address,
#[builder(default = false)] flow: bool,
) -> Xrefs {
Xrefs::new(self.xrefs_build(address, true, flow))
}
#[builder]
#[doc(alias("xrefblk_t", "first_from"))]
pub fn xrefs_from_with(
&self,
#[builder(start_fn)] address: Address,
#[builder(default = false)] flow: bool,
) -> Xrefs {
Xrefs::new(self.xrefs_build(address, false, flow))
}
#[inline]
#[must_use]
pub fn has_external_refs(&self, address: Address) -> bool {
self.xref_has_external_refs(address)
}
#[inline]
#[must_use]
pub fn has_jump_or_flow_xref(&self, address: Address) -> bool {
self.xref_has_jump_or_flow_xref(address)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[doc(alias("xrefblk_t"))]
pub struct Xref {
pub from: Address,
pub to: Address,
pub kind: XrefKind,
pub origin: XrefOrigin,
}
impl Xref {
#[inline]
#[must_use]
pub(crate) fn from_raw(from: u64, to: u64, ty: u8, iscode: u8, user: u8) -> Option<Self> {
let from = Address::try_new(from)?;
let to = Address::try_new(to)?;
let kind = if iscode != 0 {
XrefKind::Code(CodeXref::try_from(ty).ok()?)
} else {
XrefKind::Data(DataXref::try_from(ty).ok()?)
};
let origin = if user != 0 {
XrefOrigin::User
} else {
XrefOrigin::Analysis
};
Some(Self {
from,
to,
kind,
origin,
})
}
#[inline]
#[must_use]
pub const fn is_code(&self) -> bool {
matches!(self.kind, XrefKind::Code(_))
}
#[inline]
#[must_use]
pub const fn is_user(&self) -> bool {
matches!(self.origin, XrefOrigin::User)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[doc(alias("XREF_USER"))]
pub enum XrefOrigin {
Analysis,
User,
}
#[doc(alias("first_to", "next_to", "first_from", "next_from"))]
pub struct Xrefs {
recs: std::vec::IntoIter<sys::XrefRec>,
}
impl Xrefs {
#[inline]
pub(crate) fn new(recs: Vec<sys::XrefRec>) -> Self {
Self {
recs: recs.into_iter(),
}
}
}
impl Iterator for Xrefs {
type Item = Xref;
#[inline]
fn next(&mut self) -> Option<Xref> {
self.recs.by_ref().find_map(|r| {
Xref::from_raw(
r.from,
r.to,
r.type_ as u8,
u8::from(r.iscode),
u8::from(r.user),
)
})
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.recs.size_hint().1)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum XrefKind {
Code(CodeXref),
Data(DataXref),
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
TryFromPrimitive,
IntoPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u8)]
#[doc(alias("cref_t"))]
pub enum CodeXref {
#[doc(alias("fl_U"))]
Unknown = 0,
#[doc(alias("fl_CF"))]
CallFar = 16,
#[doc(alias("fl_CN"))]
CallNear = 17,
#[doc(alias("fl_JF"))]
JumpFar = 18,
#[doc(alias("fl_JN"))]
JumpNear = 19,
#[doc(alias("fl_USobsolete"))]
UserObsolete = 20,
#[doc(alias("fl_F"))]
Flow = 21,
}
impl fmt::Display for CodeXref {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Unknown => "unknown",
Self::CallFar => "far call",
Self::CallNear => "near call",
Self::JumpFar => "far jump",
Self::JumpNear => "near jump",
Self::UserObsolete => "user-specified (obsolete)",
Self::Flow => "ordinary flow",
})
}
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
TryFromPrimitive,
IntoPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u8)]
#[doc(alias("dref_t"))]
pub enum DataXref {
#[doc(alias("dr_U"))]
Unknown = 0,
#[doc(alias("dr_O"))]
Offset = 1,
#[doc(alias("dr_W"))]
Write = 2,
#[doc(alias("dr_R"))]
Read = 3,
#[doc(alias("dr_T"))]
Text = 4,
#[doc(alias("dr_I"))]
Informational = 5,
#[doc(alias("dr_S"))]
Symbolic = 6,
}
impl fmt::Display for DataXref {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Unknown => "unknown",
Self::Offset => "offset",
Self::Write => "write",
Self::Read => "read",
Self::Text => "text",
Self::Informational => "informational",
Self::Symbolic => "symbolic",
})
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use idakit_sys as sys;
use rstest::rstest;
use super::*;
#[rstest]
#[case::call_near(17, 1, XrefKind::Code(CodeXref::CallNear))]
#[case::jump_near(19, 1, XrefKind::Code(CodeXref::JumpNear))]
#[case::flow(21, 1, XrefKind::Code(CodeXref::Flow))]
#[case::data_write(2, 0, XrefKind::Data(DataXref::Write))]
#[case::data_read(3, 0, XrefKind::Data(DataXref::Read))]
#[case::symbolic(6, 0, XrefKind::Data(DataXref::Symbolic))]
fn classifies_by_type_byte(#[case] ty: u8, #[case] iscode: u8, #[case] expect: XrefKind) {
let x = Xref::from_raw(0x1000, 0x2000, ty, iscode, 0).expect("valid edge");
assert!(x.kind == expect);
assert!(x.is_code() == matches!(expect, XrefKind::Code(_)));
assert!(x.from.get() == 0x1000);
assert!(x.to.get() == 0x2000);
}
#[rstest]
#[case::analysis(0, XrefOrigin::Analysis, false)]
#[case::user(1, XrefOrigin::User, true)]
fn origin_reflects_user_flag(
#[case] user: u8,
#[case] origin: XrefOrigin,
#[case] is_user: bool,
) {
let x = Xref::from_raw(0x1000, 0x2000, 3, 0, user).expect("valid edge");
assert!(x.origin == origin);
assert!(x.is_user() == is_user);
}
#[rstest]
#[case::unmapped_code(99, 1)]
#[case::unmapped_data(99, 0)]
fn unmapped_type_is_skipped(#[case] ty: u8, #[case] iscode: u8) {
assert!(Xref::from_raw(0x1000, 0x2000, ty, iscode, 0).is_none());
}
#[test]
fn xref_type_ids_align_with_the_facade() {
fn check<T: Copy + fmt::Debug + Into<u8>>(name: &str, variants: &[T], ids: &[u8]) {
assert!(
ids.len() == variants.len(),
"{name}: facade lists {} ids for {} variants",
ids.len(),
variants.len()
);
for (i, &v) in variants.iter().enumerate() {
let raw: u8 = v.into();
assert!(
ids[i] == raw,
"{name} {v:?}: facade code {} != discriminant {raw}",
ids[i]
);
}
}
check("CodeXref", CodeXref::VARIANTS, &sys::cref_type_ids());
check("DataXref", DataXref::VARIANTS, &sys::dref_type_ids());
}
#[test]
fn every_variant_round_trips() {
for &c in CodeXref::VARIANTS {
assert!(CodeXref::try_from(u8::from(c)).ok() == Some(c));
}
for &d in DataXref::VARIANTS {
assert!(DataXref::try_from(u8::from(d)).ok() == Some(d));
}
}
#[test]
fn badaddr_endpoint_is_rejected() {
let bad = sys::BADADDR;
assert!(Xref::from_raw(bad, 0x2000, 3, 0, 0).is_none());
assert!(Xref::from_raw(0x1000, bad, 3, 0, 0).is_none());
}
#[test]
fn xref_serde_round_trips() {
let x = Xref {
from: Address::try_new(0x1000).expect("nonzero"),
to: Address::try_new(0x2000).expect("nonzero"),
kind: XrefKind::Code(CodeXref::CallNear),
origin: XrefOrigin::User,
};
let json = serde_json::to_string(&x).expect("serializable");
assert!(serde_json::from_str::<Xref>(&json).expect("deserializable") == x);
}
#[test]
fn xref_kind_data_serde_round_trips() {
let kind = XrefKind::Data(DataXref::Symbolic);
let json = serde_json::to_string(&kind).expect("serializable");
assert!(serde_json::from_str::<XrefKind>(&json).expect("deserializable") == kind);
}
#[test]
fn xrefs_size_hint_reports_the_record_count() {
let rec = |to: u64| sys::XrefRec {
from: 0,
to,
type_: 0,
iscode: false,
user: false,
};
let xrefs = Xrefs::new(vec![rec(1), rec(2), rec(3)]);
assert!(xrefs.size_hint() == (0, Some(3)));
}
#[rstest]
#[case::unknown(CodeXref::Unknown, "unknown")]
#[case::call_near(CodeXref::CallNear, "near call")]
#[case::flow(CodeXref::Flow, "ordinary flow")]
fn code_xref_display(#[case] value: CodeXref, #[case] expect: &str) {
assert!(value.to_string() == expect);
}
#[rstest]
#[case::unknown(DataXref::Unknown, "unknown")]
#[case::write(DataXref::Write, "write")]
#[case::symbolic(DataXref::Symbolic, "symbolic")]
fn data_xref_display(#[case] value: DataXref, #[case] expect: &str) {
assert!(value.to_string() == expect);
}
}