use std::ffi::c_int;
use std::ops::Range;
use num_enum::{IntoPrimitive, TryFromPrimitive};
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,
)]
#[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)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[doc(alias("fcb_enoret", "fcb_extern"))]
pub struct ExternalExit {
pub target: Address,
pub noreturn: bool,
}
#[derive(Clone, Debug)]
#[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)]
#[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()
}
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, 0)
}
pub(crate) fn build_flowchart(&self, address: Address, flags: c_int) -> Result<FlowChart> {
let chart = sys::cfg_build(address.get(), flags).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) -> c_int {
let mut flags = 0;
if call_ends {
flags |= sys::FC_CALL_ENDS;
}
if !externals {
flags |= sys::FC_NOEXT;
}
if !predecessors {
flags |= sys::FC_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 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 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) == 0);
assert!(flowchart_flags(true, true, true) == sys::FC_CALL_ENDS);
assert!(flowchart_flags(false, false, true) == sys::FC_NOEXT);
assert!(flowchart_flags(false, true, false) == sys::FC_NOPREDS);
assert!(
flowchart_flags(true, false, false)
== sys::FC_CALL_ENDS | sys::FC_NOEXT | sys::FC_NOPREDS
);
}
}