use std::fmt;
use std::ops::Range;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{Deserialize, Serialize};
use strum::VariantArray;
use idakit_sys as sys;
use crate::Database;
use crate::address::Address;
use crate::arena::{Arena, Idx};
use crate::error::{Error, Result};
#[doc(alias("qbasic_block_t"))]
pub type BasicBlockId = Idx<BasicBlock>;
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
TryFromPrimitive,
IntoPrimitive,
VariantArray,
Serialize,
Deserialize,
)]
#[repr(u8)]
#[doc(alias("fc_block_type_t"))]
pub enum BasicBlockKind {
#[doc(alias("fcb_normal"))]
Normal = 0,
#[doc(alias("fcb_indjump"))]
IndirectJump = 1,
#[doc(alias("fcb_ret"))]
Return = 2,
#[doc(alias("fcb_cndret"))]
CondReturn = 3,
#[doc(alias("fcb_noret"))]
NoReturn = 4,
#[doc(alias("fcb_error"))]
Error = 7,
}
impl BasicBlockKind {
#[inline]
#[must_use]
pub fn is_return(self) -> bool {
matches!(self, Self::Return | Self::CondReturn)
}
#[inline]
#[must_use]
pub fn is_noreturn(self) -> bool {
matches!(self, Self::NoReturn)
}
}
impl fmt::Display for BasicBlockKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normal => f.write_str("normal"),
Self::IndirectJump => f.write_str("indirect jump"),
Self::Return => f.write_str("return"),
Self::CondReturn => f.write_str("conditional return"),
Self::NoReturn => f.write_str("no-return call"),
Self::Error => f.write_str("error"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[doc(alias("fcb_enoret", "fcb_extern"))]
pub struct ExternalExit {
pub target: Address,
pub noreturn: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[doc(alias("qbasic_block_t"))]
pub struct BasicBlock {
range: Range<Address>,
kind: BasicBlockKind,
succ: Vec<BasicBlockId>,
pred: Vec<BasicBlockId>,
exits: Vec<ExternalExit>,
}
impl BasicBlock {
#[inline]
#[must_use]
pub fn range(&self) -> Range<Address> {
self.range.clone()
}
#[inline]
#[must_use]
pub fn start(&self) -> Address {
self.range.start
}
#[inline]
#[must_use]
pub fn end(&self) -> Address {
self.range.end
}
#[inline]
#[must_use]
pub fn kind(&self) -> BasicBlockKind {
self.kind
}
#[inline]
#[must_use]
pub fn successors(&self) -> &[BasicBlockId] {
&self.succ
}
#[inline]
#[must_use]
pub fn predecessors(&self) -> &[BasicBlockId] {
&self.pred
}
#[inline]
#[must_use]
pub fn exits(&self) -> &[ExternalExit] {
&self.exits
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[doc(alias("qflow_chart_t"))]
pub struct FlowChart {
blocks: Arena<BasicBlock>,
entry: BasicBlockId,
function: Address,
}
impl FlowChart {
#[inline]
#[must_use]
pub fn function(&self) -> Address {
self.function
}
#[inline]
#[must_use]
pub fn entry(&self) -> BasicBlockId {
self.entry
}
#[inline]
#[must_use]
pub fn block(&self, id: BasicBlockId) -> &BasicBlock {
&self.blocks[id]
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn blocks(&self) -> impl ExactSizeIterator<Item = (BasicBlockId, &BasicBlock)> {
self.blocks.iter()
}
#[must_use]
pub fn block_at(&self, address: Address) -> Option<BasicBlockId> {
self.blocks
.iter()
.find_map(|(id, b)| (b.range.start <= address && address < b.range.end).then_some(id))
}
}
impl Database {
#[doc(alias("qflow_chart_t"))]
pub fn flowchart(&self, address: Address) -> Result<FlowChart> {
self.build_flowchart(address, sys::FlowChartFlags::empty())
}
#[expect(
clippy::unused_self,
reason = "&self is the kernel-thread/live-database proof token, not instance state"
)]
pub(crate) fn build_flowchart(
&self,
address: Address,
flags: sys::FlowChartFlags,
) -> Result<FlowChart> {
crate::claim::ensure_kernel_thread();
let chart = sys::cfg_build(address.get(), flags.bits()).map_err(|_| Error::NoFunction {
address: address.get(),
})?;
let blocks = extract(&chart)?;
let function = blocks.iter().next().map_or(address, |(_, b)| b.start());
Ok(FlowChart {
blocks,
entry: BasicBlockId::from_raw(0),
function,
})
}
}
pub(crate) fn flowchart_flags(
call_ends: bool,
externals: bool,
predecessors: bool,
) -> sys::FlowChartFlags {
let mut flags = sys::FlowChartFlags::empty();
if call_ends {
flags |= sys::FlowChartFlags::CALL_ENDS;
}
if !externals {
flags |= sys::FlowChartFlags::NOEXT;
}
if !predecessors {
flags |= sys::FlowChartFlags::NOPREDS;
}
flags
}
const FCB_ENORET: u8 = 5;
fn extract(chart: &sys::FlowChart) -> Result<Arena<BasicBlock>> {
let nproper = sys::cfg_nproper(chart);
let mut blocks = Arena::new();
for i in 0..nproper {
let info = sys::cfg_block(chart, i).expect("cfg_block within nproper");
let raw = info.kind as u8;
let kind = BasicBlockKind::try_from(raw).map_err(|_| Error::UnknownBlockKind {
block: info.start,
raw,
})?;
let (succ, exits) = successors(chart, i, nproper);
blocks.alloc(BasicBlock {
range: block_range(info.start, info.end),
kind,
succ,
pred: predecessors(chart, i, nproper),
exits,
});
}
Ok(blocks)
}
fn successors(
chart: &sys::FlowChart,
n: usize,
nproper: usize,
) -> (Vec<BasicBlockId>, Vec<ExternalExit>) {
let mut succ = Vec::new();
let mut exits = Vec::new();
for j in sys::cfg_succs(chart, n).expect("cfg_succs within nproper") {
if (j as usize) < nproper {
succ.push(BasicBlockId::from_raw(j));
} else {
let info = sys::cfg_block(chart, j as usize).expect("cfg_block for external stub");
exits.push(ExternalExit {
target: Address::try_new(info.start).expect("external stub start is BADADDR"),
noreturn: info.kind as u8 == FCB_ENORET,
});
}
}
(succ, exits)
}
fn predecessors(chart: &sys::FlowChart, n: usize, nproper: usize) -> Vec<BasicBlockId> {
sys::cfg_preds(chart, n)
.expect("cfg_preds within nproper")
.into_iter()
.filter(|&j| (j as usize) < nproper)
.map(BasicBlockId::from_raw)
.collect()
}
fn block_range(start: u64, end: u64) -> Range<Address> {
let start = Address::try_new(start).expect("flow-chart block start is BADADDR");
let end = Address::try_new(end).expect("flow-chart block end is BADADDR");
start..end
}
#[cfg(test)]
mod tests {
use assert2::assert;
use idakit_sys as sys;
use rstest::rstest;
use super::*;
const fn assert_send<T: Send>() {}
const _: () = assert_send::<FlowChart>();
#[rstest]
#[case(BasicBlockKind::Normal, 0)]
#[case(BasicBlockKind::IndirectJump, 1)]
#[case(BasicBlockKind::Return, 2)]
#[case(BasicBlockKind::CondReturn, 3)]
#[case(BasicBlockKind::NoReturn, 4)]
#[case(BasicBlockKind::Error, 7)]
fn block_kind_raw_matches_sdk(#[case] kind: BasicBlockKind, #[case] raw: u8) {
assert!(u8::from(kind) == raw);
assert!(BasicBlockKind::try_from(raw).ok() == Some(kind));
}
#[rstest]
#[case(5)]
#[case(6)]
#[case(8)]
#[case(200)]
#[case(0xff)]
fn unmodeled_block_kinds_are_rejected(#[case] raw: u8) {
assert!(BasicBlockKind::try_from(raw).is_err());
}
#[rstest]
#[case(BasicBlockKind::Return, true, false)]
#[case(BasicBlockKind::CondReturn, true, false)]
#[case(BasicBlockKind::NoReturn, false, true)]
#[case(BasicBlockKind::Normal, false, false)]
#[case(BasicBlockKind::IndirectJump, false, false)]
#[case(BasicBlockKind::Error, false, false)]
fn block_kind_predicates(#[case] kind: BasicBlockKind, #[case] ret: bool, #[case] noret: bool) {
assert!(kind.is_return() == ret);
assert!(kind.is_noreturn() == noret);
}
#[test]
fn block_kind_ids_align_with_the_facade() {
let ids = sys::block_kind_ids();
assert!(
ids.len() == BasicBlockKind::VARIANTS.len(),
"facade lists {} ids for {} variants",
ids.len(),
BasicBlockKind::VARIANTS.len()
);
for (i, &kind) in BasicBlockKind::VARIANTS.iter().enumerate() {
assert!(
ids[i] == u8::from(kind),
"block kind {kind:?}: facade fc_block_type_t {} != discriminant {}",
ids[i],
u8::from(kind)
);
}
}
#[test]
fn every_variant_round_trips() {
for &kind in BasicBlockKind::VARIANTS {
assert!(BasicBlockKind::try_from(u8::from(kind)).ok() == Some(kind));
}
}
#[test]
fn cfg_flags_compose() {
assert!(flowchart_flags(false, true, true).is_empty());
assert!(flowchart_flags(true, true, true) == sys::FlowChartFlags::CALL_ENDS);
assert!(flowchart_flags(false, false, true) == sys::FlowChartFlags::NOEXT);
assert!(flowchart_flags(false, true, false) == sys::FlowChartFlags::NOPREDS);
assert!(
flowchart_flags(true, false, false)
== sys::FlowChartFlags::CALL_ENDS
| sys::FlowChartFlags::NOEXT
| sys::FlowChartFlags::NOPREDS
);
}
fn sample_chart() -> FlowChart {
let mut blocks = Arena::new();
let entry = blocks.alloc(BasicBlock {
range: Address::try_new(0x1000).unwrap()..Address::try_new(0x1010).unwrap(),
kind: BasicBlockKind::Normal,
succ: vec![],
pred: vec![],
exits: vec![ExternalExit {
target: Address::try_new(0x2000).unwrap(),
noreturn: true,
}],
});
FlowChart {
blocks,
entry,
function: Address::try_new(0x1000).unwrap(),
}
}
#[test]
fn flowchart_clone_and_eq() {
let chart = sample_chart();
let cloned = chart.clone();
assert!(cloned == chart);
}
#[test]
fn is_empty_reflects_the_arena() {
let empty = FlowChart {
blocks: Arena::new(),
entry: BasicBlockId::from_raw(0),
function: Address::try_new(0x1000).unwrap(),
};
assert!(empty.is_empty());
assert!(!sample_chart().is_empty());
}
#[test]
fn exits_returns_the_real_data() {
let chart = sample_chart();
let exits = chart.block(chart.entry()).exits();
assert!(exits.len() == 1);
assert!(exits[0].target == Address::try_new(0x2000).unwrap());
assert!(exits[0].noreturn);
}
#[test]
fn block_at_end_is_exclusive() {
let chart = sample_chart();
let entry = chart.entry();
assert!(chart.block_at(Address::try_new(0x1000).unwrap()) == Some(entry));
assert!(chart.block_at(Address::try_new(0x100f).unwrap()) == Some(entry));
assert!(chart.block_at(Address::try_new(0x1010).unwrap()).is_none());
}
#[test]
fn flowchart_serde_round_trip() {
let chart = sample_chart();
let json = serde_json::to_string(&chart).unwrap();
let round_tripped: FlowChart = serde_json::from_str(&json).unwrap();
assert!(round_tripped == chart);
}
#[rstest]
#[case(BasicBlockKind::Normal, "normal")]
#[case(BasicBlockKind::IndirectJump, "indirect jump")]
#[case(BasicBlockKind::Return, "return")]
#[case(BasicBlockKind::CondReturn, "conditional return")]
#[case(BasicBlockKind::NoReturn, "no-return call")]
#[case(BasicBlockKind::Error, "error")]
fn block_kind_display(#[case] kind: BasicBlockKind, #[case] expected: &str) {
assert!(kind.to_string() == expected);
}
#[test]
fn external_exit_serde_round_trip() {
let exit = ExternalExit {
target: Address::try_new(0x4000).unwrap(),
noreturn: false,
};
let json = serde_json::to_string(&exit).unwrap();
let round_tripped: ExternalExit = serde_json::from_str(&json).unwrap();
assert!(round_tripped == exit);
}
}