use std::collections::BTreeMap;
use crate::{
container::Container,
demangle::symbol,
rtti::{
self,
symbols::RttiVersion,
v1::{NimKind, NimTypeFlag, NodeField},
},
util,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TypeShape {
Object,
Tuple,
Enum,
Ref,
Sequence,
Other,
}
impl TypeShape {
pub fn as_str(&self) -> &'static str {
match self {
Self::Object => "Object",
Self::Tuple => "Tuple",
Self::Enum => "Enum",
Self::Ref => "Ref",
Self::Sequence => "Sequence",
Self::Other => "Other",
}
}
fn from_kind(kind: NimKind) -> Self {
match kind {
NimKind::Object => Self::Object,
NimKind::Tuple => Self::Tuple,
NimKind::Enum => Self::Enum,
NimKind::Ref => Self::Ref,
NimKind::Sequence => Self::Sequence,
_ => Self::Other,
}
}
}
impl core::fmt::Display for TypeShape {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TypeFlags {
pub no_refs: bool,
pub acyclic: bool,
pub enum_hole: bool,
}
#[derive(Debug, Clone)]
pub struct TypeRef {
pub address: u64,
pub index: Option<usize>,
pub name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CodeRef {
pub address: u64,
pub function: Option<String>,
pub module: Option<String>,
pub symbol_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TypeField {
pub name: String,
pub offset: u64,
pub type_ref: Option<TypeRef>,
}
#[derive(Debug, Clone)]
pub struct EnumValue {
pub name: String,
pub ordinal: u64,
}
#[derive(Debug, Clone)]
pub struct NimType {
pub version: RttiVersion,
pub symbol_name: String,
pub address: u64,
pub name: Option<String>,
pub type_fragment: Option<String>,
pub shape: TypeShape,
pub size: u64,
pub align: u64,
pub depth: Option<i16>,
pub flags: TypeFlags,
pub kind: Option<NimKind>,
pub parent: Option<TypeRef>,
pub fields: Vec<TypeField>,
pub enum_values: Vec<EnumValue>,
pub display_tokens: Vec<u32>,
pub destructor: Option<CodeRef>,
pub trace_impl: Option<CodeRef>,
pub finalizer: Option<CodeRef>,
pub marker: Option<CodeRef>,
pub deepcopy: Option<CodeRef>,
pub type_info_v1: Option<TypeRef>,
pub readable: bool,
}
impl NimType {
pub fn is_readable(&self) -> bool {
self.readable
}
}
pub fn build(container: &Container<'_>) -> Vec<NimType> {
let rtti_syms = rtti::symbols::scan(container);
let mut types: Vec<NimType> = rtti_syms
.iter()
.map(|s| NimType {
version: s.version,
symbol_name: s.symbol_name.clone(),
address: s.address,
name: None,
type_fragment: s.type_fragment.clone(),
shape: TypeShape::Other,
size: 0,
align: 0,
depth: None,
flags: TypeFlags::default(),
kind: None,
parent: None,
fields: Vec::new(),
enum_values: Vec::new(),
display_tokens: Vec::new(),
destructor: None,
trace_impl: None,
finalizer: None,
marker: None,
deepcopy: None,
type_info_v1: None,
readable: false,
})
.collect();
let va_index: BTreeMap<u64, usize> = types
.iter()
.enumerate()
.map(|(i, t)| (t.address, i))
.collect();
let mut display_info: Vec<Option<(u64, i16)>> = vec![None; types.len()];
for (i, t) in types.iter_mut().enumerate() {
match t.version {
RttiVersion::V2 => {
let Some(f) = rtti::v2::read(container, t.address) else {
continue;
};
t.readable = true;
t.size = f.size;
t.align = u64::try_from(f.align).unwrap_or(0);
t.depth = Some(f.depth);
t.name = f.name;
t.flags = TypeFlags {
no_refs: false,
acyclic: f.flags & 1 != 0,
enum_hole: false,
};
t.destructor = f.destructor_addr.map(|a| resolve_code_ref(container, a));
t.trace_impl = f.trace_impl_addr.map(|a| resolve_code_ref(container, a));
t.type_info_v1 = f.type_info_v1_addr.map(|a| make_type_ref(a, &va_index));
if let Some(d) = f.display_addr {
t.shape = TypeShape::Object;
if let Some(slot) = display_info.get_mut(i) {
*slot = Some((d, f.depth));
}
}
}
RttiVersion::V1 => {
let Some(f) = rtti::v1::read(container, t.address) else {
continue;
};
t.readable = true;
t.size = f.size;
t.align = f.align;
t.kind = Some(f.kind);
t.name = f.name;
t.shape = TypeShape::from_kind(f.kind);
t.flags = flags_from_v1(&f.flags);
t.parent = f.base_addr.map(|a| make_type_ref(a, &va_index));
t.finalizer = f.finalizer_addr.map(|a| resolve_code_ref(container, a));
t.marker = f.marker_addr.map(|a| resolve_code_ref(container, a));
t.deepcopy = f.deepcopy_addr.map(|a| resolve_code_ref(container, a));
if f.kind == NimKind::Enum {
t.enum_values = node_fields_to_enum_values(&f.node_fields);
} else {
t.fields = node_fields_to_type_fields(&f.node_fields, &va_index);
}
}
}
}
let mut token_index: BTreeMap<u32, usize> = BTreeMap::new();
for (i, t) in types.iter_mut().enumerate() {
let Some((display_addr, depth)) = display_info.get(i).copied().flatten() else {
continue;
};
t.display_tokens = read_display_tokens(container, display_addr, depth);
if let Some(&own) = t.display_tokens.last() {
token_index.insert(own, i);
}
}
let parents: Vec<Option<u64>> = types
.iter()
.map(|t| {
let len = t.display_tokens.len();
if len < 2 {
return None;
}
let parent_token = t.display_tokens.get(len.wrapping_sub(2)).copied()?;
let parent_idx = token_index.get(&parent_token).copied()?;
types.get(parent_idx).map(|p| p.address)
})
.collect();
for (t, parent_addr) in types.iter_mut().zip(parents) {
if t.version == RttiVersion::V2
&& t.parent.is_none()
&& let Some(addr) = parent_addr
{
t.parent = Some(make_type_ref(addr, &va_index));
}
}
let bridge_targets: Vec<(usize, u64)> = types
.iter()
.enumerate()
.filter_map(|(i, t)| {
if t.version == RttiVersion::V2 && t.fields.is_empty() {
t.type_info_v1.as_ref().map(|r| (i, r.address))
} else {
None
}
})
.collect();
for (i, v1_addr) in bridge_targets {
let Some(f) = rtti::v1::read(container, v1_addr) else {
continue;
};
if f.node_fields.is_empty() {
continue;
}
let imported = node_fields_to_type_fields(&f.node_fields, &va_index);
if let Some(t) = types.get_mut(i) {
t.fields = imported;
}
}
let names: Vec<Option<String>> = types.iter().map(|t| t.name.clone()).collect();
for t in types.iter_mut() {
backfill_ref(&mut t.parent, &names);
backfill_ref(&mut t.type_info_v1, &names);
for field in t.fields.iter_mut() {
backfill_ref(&mut field.type_ref, &names);
}
}
types
}
fn make_type_ref(addr: u64, va_index: &BTreeMap<u64, usize>) -> TypeRef {
TypeRef {
address: addr,
index: va_index.get(&addr).copied(),
name: None,
}
}
fn backfill_ref(slot: &mut Option<TypeRef>, names: &[Option<String>]) {
if let Some(r) = slot.as_mut()
&& let Some(idx) = r.index
&& let Some(Some(name)) = names.get(idx)
{
r.name = Some(name.clone());
}
}
fn resolve_code_ref(container: &Container<'_>, addr: u64) -> CodeRef {
let Some(sym) = container.function_at_va(addr) else {
return CodeRef {
address: addr,
function: None,
module: None,
symbol_name: None,
};
};
let raw = sym.name.as_ref();
let demangled = symbol::parse(raw);
CodeRef {
address: addr,
function: demangled
.as_ref()
.map(|d| d.identifier.clone().into_owned()),
module: demangled.as_ref().map(|d| d.module.to_owned()),
symbol_name: Some(raw.to_owned()),
}
}
fn flags_from_v1(flags: &[NimTypeFlag]) -> TypeFlags {
TypeFlags {
no_refs: flags.contains(&NimTypeFlag::NoRefs),
acyclic: flags.contains(&NimTypeFlag::Acyclic),
enum_hole: flags.contains(&NimTypeFlag::EnumHole),
}
}
fn node_fields_to_type_fields(
node_fields: &[NodeField],
va_index: &BTreeMap<u64, usize>,
) -> Vec<TypeField> {
node_fields
.iter()
.map(|nf| TypeField {
name: nf.name.clone(),
offset: nf.offset,
type_ref: nf.type_addr.map(|a| make_type_ref(a, va_index)),
})
.collect()
}
fn node_fields_to_enum_values(node_fields: &[NodeField]) -> Vec<EnumValue> {
node_fields
.iter()
.map(|nf| EnumValue {
name: nf.name.clone(),
ordinal: nf.offset,
})
.collect()
}
fn read_display_tokens(container: &Container<'_>, display_addr: u64, depth: i16) -> Vec<u32> {
const MAX_DEPTH: usize = 256;
let Ok(depth_usize) = usize::try_from(depth) else {
return Vec::new();
};
let count = depth_usize.saturating_add(1).min(MAX_DEPTH);
let Some(base_off) = rtti::v2::va_to_offset(container, display_addr) else {
return Vec::new();
};
let bytes = container.bytes();
let mut tokens = Vec::with_capacity(count);
for i in 0..count {
let Some(off) = i.checked_mul(4).and_then(|d| base_off.checked_add(d)) else {
break;
};
if off.checked_add(4).is_none_or(|end| end > bytes.len()) {
break;
}
tokens.push(util::read_u32_le(bytes, off));
}
tokens
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_from_kind_maps_known_kinds() {
assert_eq!(TypeShape::from_kind(NimKind::Object), TypeShape::Object);
assert_eq!(TypeShape::from_kind(NimKind::Tuple), TypeShape::Tuple);
assert_eq!(TypeShape::from_kind(NimKind::Enum), TypeShape::Enum);
assert_eq!(TypeShape::from_kind(NimKind::Ref), TypeShape::Ref);
assert_eq!(TypeShape::from_kind(NimKind::Sequence), TypeShape::Sequence);
assert_eq!(TypeShape::from_kind(NimKind::Int), TypeShape::Other);
}
#[test]
fn shape_as_str_is_stable() {
assert_eq!(TypeShape::Object.as_str(), "Object");
assert_eq!(TypeShape::Enum.as_str(), "Enum");
assert_eq!(TypeShape::Other.to_string(), "Other");
}
#[test]
fn flags_from_v1_unifies_set() {
let f = flags_from_v1(&[NimTypeFlag::NoRefs, NimTypeFlag::Acyclic]);
assert!(f.no_refs);
assert!(f.acyclic);
assert!(!f.enum_hole);
}
#[test]
fn node_fields_become_type_fields_with_refs() {
let idx: BTreeMap<u64, usize> = [(0x1000, 0)].into_iter().collect();
let nf = vec![
NodeField {
name: "a".to_string(),
offset: 0,
type_addr: Some(0x1000),
},
NodeField {
name: "b".to_string(),
offset: 8,
type_addr: None,
},
];
let fields = node_fields_to_type_fields(&nf, &idx);
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "a");
assert_eq!(fields[0].type_ref.as_ref().unwrap().index, Some(0));
assert_eq!(fields[1].offset, 8);
assert!(fields[1].type_ref.is_none());
}
#[test]
fn enum_slots_become_values_with_ordinals() {
let nf = vec![
NodeField {
name: "ckRed".to_string(),
offset: 0,
type_addr: None,
},
NodeField {
name: "ckBlue".to_string(),
offset: 7,
type_addr: None,
},
];
let values = node_fields_to_enum_values(&nf);
assert_eq!(values.len(), 2);
assert_eq!(values[0].name, "ckRed");
assert_eq!(values[1].ordinal, 7);
}
#[test]
fn backfill_ref_fills_name_from_snapshot() {
let names = vec![Some("Foo".to_string())];
let mut slot = Some(TypeRef {
address: 0x10,
index: Some(0),
name: None,
});
backfill_ref(&mut slot, &names);
assert_eq!(slot.unwrap().name.as_deref(), Some("Foo"));
}
use crate::container::{self, Arch, Format, Section, SectionKind, Symbol, SymbolKind};
use std::borrow::Cow;
fn w64(buf: &mut [u8], off: usize, val: u64) {
buf[off..off + 8].copy_from_slice(&val.to_le_bytes());
}
fn wstr(buf: &mut [u8], off: usize, s: &str) {
buf[off..off + s.len()].copy_from_slice(s.as_bytes());
}
fn synthetic_v1_container() -> Vec<u8> {
let mut b = vec![0u8; 0x400];
w64(&mut b, 0x40, 24); w64(&mut b, 0x48, 8); b[0x50] = 17; b[0x51] = 0; w64(&mut b, 0x60, 0x100);
b[0x100] = 2; w64(&mut b, 0x120, 2); w64(&mut b, 0x128, 0x140); w64(&mut b, 0x140, 0x160); w64(&mut b, 0x148, 0x1A0);
b[0x160] = 1; w64(&mut b, 0x168, 0); w64(&mut b, 0x178, 0x300);
b[0x1A0] = 1; w64(&mut b, 0x1A8, 8); w64(&mut b, 0x1B8, 0x310);
w64(&mut b, 0x200, 1); w64(&mut b, 0x208, 1); b[0x210] = 14; w64(&mut b, 0x220, 0x240);
b[0x240] = 2; w64(&mut b, 0x260, 2); w64(&mut b, 0x268, 0x280); w64(&mut b, 0x280, 0x2A0); w64(&mut b, 0x288, 0x2C0);
b[0x2A0] = 1;
w64(&mut b, 0x2A8, 0); w64(&mut b, 0x2B8, 0x320);
b[0x2C0] = 1;
w64(&mut b, 0x2C8, 7); w64(&mut b, 0x2D8, 0x330);
wstr(&mut b, 0x300, "x");
wstr(&mut b, 0x310, "y");
wstr(&mut b, 0x320, "ckRed");
wstr(&mut b, 0x330, "ckBlue");
b
}
#[test]
fn build_recovers_v1_fields_and_enum_values() {
let bytes = synthetic_v1_container();
let section = Section {
name: ".data".to_string(),
vm_addr: 0,
vm_size: bytes.len() as u64,
data: &bytes,
kind: SectionKind::Data,
};
let symbols = vec![
Symbol {
name: Cow::Borrowed("NTIobjtype__deadbeef_"),
vm_addr: 0x40,
size: 0,
kind: SymbolKind::Object,
},
Symbol {
name: Cow::Borrowed("NTImyenum__cafef00d_"),
vm_addr: 0x200,
size: 0,
kind: SymbolKind::Object,
},
];
let container =
container::assemble(&bytes, Format::Elf, Arch::Amd64, 0, vec![section], symbols);
let types = build(&container);
assert_eq!(types.len(), 2);
let obj = types
.iter()
.find(|t| t.symbol_name.starts_with("NTIobjtype"))
.expect("object type present");
assert!(obj.is_readable());
assert_eq!(obj.shape, TypeShape::Object);
assert_eq!(obj.size, 24);
assert_eq!(obj.align, 8);
assert_eq!(obj.kind, Some(NimKind::Object));
assert_eq!(obj.fields.len(), 2);
assert_eq!(obj.fields[0].name, "x");
assert_eq!(obj.fields[0].offset, 0);
assert_eq!(obj.fields[1].name, "y");
assert_eq!(obj.fields[1].offset, 8);
assert!(obj.enum_values.is_empty());
let en = types
.iter()
.find(|t| t.symbol_name.starts_with("NTImyenum"))
.expect("enum type present");
assert!(en.is_readable());
assert_eq!(en.shape, TypeShape::Enum);
assert_eq!(en.kind, Some(NimKind::Enum));
assert!(en.fields.is_empty());
assert_eq!(en.enum_values.len(), 2);
assert_eq!(en.enum_values[0].name, "ckRed");
assert_eq!(en.enum_values[0].ordinal, 0);
assert_eq!(en.enum_values[1].name, "ckBlue");
assert_eq!(en.enum_values[1].ordinal, 7);
}
}