From 7b28ca1d3c6f2c249bb23b349057b29115497990 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 03:22:27 -0500 Subject: [PATCH 1/7] Parse bare BT_UNK and harden function dirtree dump A type byte of 0x00 (BT_UNK with BTMT_SIZE0) is IDA's unknown type of unspecified size, which appears in real databases as function argument and return types. It was being rejected with "forbidden use of BT_UNK", which aborted type parsing for any function that used it. Treat it like BT_UNKNOWN (unknown, unspecified size) instead. Also harden dump-dirtree-funcs so a single function whose label or type fails to parse no longer panics the whole dump: per-function parse errors are tolerated and, if printing a function still fails, the entry is emitted with an inline error note rather than unwrapping. --- src/til.rs | 7 ++++--- src/tools/dump_dirtree_funcs.rs | 24 +++++++++++------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/til.rs b/src/til.rs index 4ba5e05..ed163b4 100644 --- a/src/til.rs +++ b/src/til.rs @@ -392,9 +392,10 @@ impl Basic { // InnerRef fb47f2c2-3c08-4d40-b7ab-3c7736dce31d 0x480874 BT_UNK => { let bytes = match btmt { - BTMT_SIZE0 => { - return Err(anyhow!("forbidden use of BT_UNK")) - } + // A bare `BT_UNK` (type byte 0x00) is IDA's unknown type of unspecified + // size; treat it like `BT_UNKNOWN` rather than rejecting it, since it shows + // up in real databases (e.g. as a function argument/return type). + BTMT_SIZE0 => 0, BTMT_SIZE12 => 2, // BT_UNK_WORD BTMT_SIZE48 => 8, // BT_UNK_QWORD BTMT_SIZE128 => 0, // BT_UNKNOWN diff --git a/src/tools/dump_dirtree_funcs.rs b/src/tools/dump_dirtree_funcs.rs index 0fcf7ce..943cd62 100644 --- a/src/tools/dump_dirtree_funcs.rs +++ b/src/tools/dump_dirtree_funcs.rs @@ -20,13 +20,13 @@ fn dump_inner((id0, id1, id2): Id0Id1Id2Variant) -> Result<()> { if let Some(dirtree) = id0.dirtree_function_address()? { print_dirtree( |entry| { - print_function( - &id0, - &id1, - id2.as_ref(), - Address::from_raw(*entry), - ) - .unwrap() + let address = Address::from_raw(*entry); + if let Err(err) = + print_function(&id0, &id1, id2.as_ref(), address) + { + // Don't let a single unparsable function abort the whole dump. + println!("{:#x}: ", address.into_raw()); + } }, &dirtree, ); @@ -45,14 +45,12 @@ pub fn print_function( let root_info = id0.ida_info(root_info_idx)?; let image_base = root_info.netdelta(); let info = AddressInfo::new(id0, id1, id2, image_base, address); - let name = info - .as_ref() - .and_then(|info| info.label().transpose()) - .transpose()?; + // Tolerate per-function label/type parse failures so one bad entry doesn't abort the dump; + // such a function simply prints without the failing piece. + let name = info.as_ref().and_then(|info| info.label().ok().flatten()); let ty = info .as_ref() - .and_then(|info| info.tinfo(&root_info).transpose()) - .transpose()?; + .and_then(|info| info.tinfo(&root_info).ok().flatten()); print!("{:#x}:", address.into_raw()); match (name, ty) { From 10c7fb7534ebf47acbdc0d953ccb50f5b06603e5 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 03:25:06 -0500 Subject: [PATCH 2/7] Tolerate trailing data after IDBParam outside restrictive mode Some databases carry extra bytes after the IDBParam structure. Erroring on that aborted root-info parsing (and therefore the whole import) for those files. Gate the "Data left after the IDBParam" check behind the `restrictive` feature, matching how other strictness checks are handled, so normal parsing tolerates the trailing data. --- src/id0/root_info.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/id0/root_info.rs b/src/id0/root_info.rs index 99a2623..f709642 100644 --- a/src/id0/root_info.rs +++ b/src/id0/root_info.rs @@ -275,7 +275,10 @@ impl RootInfo { match version { // TODO old version may contain extra data at the end with unknown purpose ..=699 => {} - 700.. => ensure!(input.is_empty(), "Data left after the IDBParam",), + 700.. => { + #[cfg(feature = "restrictive")] + ensure!(input.is_empty(), "Data left after the IDBParam"); + } } Ok(param) } From eab91871488e51d0fb26eb5c615cd83ec1fb32b3 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 14:37:28 -0500 Subject: [PATCH 3/7] Expose per-address string literal type Add AddressInfo::str_type, which decodes the NALT_STRTYPE altval into the character width (1/2/4 byte) and layout (zero-terminated or Pascal) IDA recorded for a string literal. This is the SDK's get_str_type and lets consumers distinguish C strings from UTF-16/UTF-32 and Pascal strings instead of assuming single-byte characters. --- doc/sdk_comp.md | 2 +- src/addr_info.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/doc/sdk_comp.md b/doc/sdk_comp.md index 1c7a585..211599b 100644 --- a/doc/sdk_comp.md +++ b/doc/sdk_comp.md @@ -900,7 +900,7 @@ nalt.hpp | 🚧 | get_xrefpos nalt.hpp | 🚧 | upd_abits nalt.hpp | 🚧 | get_aflags nalt.hpp | 🚧 | get_ind_purged -nalt.hpp | 🚧 | get_str_type +nalt.hpp | ✔️ | get_str_type | AddressInfo::str_type nalt.hpp | 🚧 | get_array_parameters nalt.hpp | 🚧 | get_switch_info nalt.hpp | 🚧 | get_custom_data_type_ids diff --git a/src/addr_info.rs b/src/addr_info.rs index 626c794..1c088f9 100644 --- a/src/addr_info.rs +++ b/src/addr_info.rs @@ -1,7 +1,8 @@ use crate::bytes_info::BytesInfo; use crate::id0::entry_iter::EntryTagContinuousSubkeys; use crate::id0::flag::nalt::x::NALT_DREF_FROM; -use crate::id0::flag::netnode::nn_res::ARRAY_SUP_TAG; +use crate::id0::flag::nalt::NALT_STRTYPE; +use crate::id0::flag::netnode::nn_res::{ARRAY_ALT_TAG, ARRAY_SUP_TAG}; use crate::id0::flag::nsup::NSUP_TYPEINFO; use crate::id0::{ get_sup_from_key, parse_maybe_cstr, ID0CStr, ID0Section, Netdelta, @@ -133,6 +134,17 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { } } + /// The string literal type IDA assigned to this address, if any. + /// + /// Decodes the `NALT_STRTYPE` altval (see `get_str_type` in `nalt.hpp`). Returns `None` for + /// addresses that are not string literals. + pub fn str_type(&self) -> Option { + let raw = + self.id0 + .sup_value(self.netnode(), NALT_STRTYPE.into(), ARRAY_ALT_TAG)?; + Some(StrType::from_code(*raw.first()?)) + } + pub fn tinfo(&self, info: &RootInfo) -> Result> { // allow if it's a struct type or a function definition match self.byte_info.byte_type() { @@ -238,3 +250,57 @@ pub fn all_address_info<'a, K: IDAKind>( #[derive(Clone, Copy, Debug)] pub struct SubtypeId(pub(crate) K::Usize); + +/// The character width of a string literal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StrWidth { + /// One byte per character (C / ASCII / UTF-8). + Byte, + /// Two bytes per character (UTF-16). + Word, + /// Four bytes per character (UTF-32). + Dword, +} + +/// The in-memory layout of a string literal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StrLayout { + /// Terminated by a zero character (C string). + TerminatedChar, + /// Length prefixed by a single byte (Pascal). + Pascal1, + /// Length prefixed by two bytes. + Pascal2, + /// Length prefixed by four bytes. + Pascal4, +} + +/// The type of a string literal IDA defined at an address, decoded from `strtype`. +/// +/// See `get_str_type` / `NALT_STRTYPE` in the IDA SDK (`nalt.hpp`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct StrType { + pub width: StrWidth, + pub layout: StrLayout, +} + +impl StrType { + /// Decode the low (type code) byte of an IDA `strtype` value. + pub fn from_code(code: u8) -> Self { + // InnerRef: nalt.hpp STRWIDTH_MASK / STRLYT_MASK / STRLYT_SHIFT. + let width = match code & 0x03 { + 1 => StrWidth::Word, + 2 => StrWidth::Dword, + // 0, and the reserved 3, are treated as single byte. + _ => StrWidth::Byte, + }; + let layout = match (code & 0xFC) >> 2 { + 1 => StrLayout::Pascal1, + 2 => StrLayout::Pascal2, + 3 => StrLayout::Pascal4, + // 0 (and any unexpected value) is a zero-terminated string. + _ => StrLayout::TerminatedChar, + }; + Self { width, layout } + } +} From dae70ea10b2c52fd5b8afb32ed3c7da2155023b8 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 14:44:02 -0500 Subject: [PATCH 4/7] Expose enum referenced by an operand Add AddressInfo::op_enum, reading the NALT_ENUM0/NALT_ENUM1 altval to return the enumeration id an instruction operand is displayed against (the SDK's op_enum / get_enum_id). This lets consumers render enum operands with the referenced enumeration instead of a bare number. --- doc/sdk_comp.md | 4 ++-- src/addr_info.rs | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/doc/sdk_comp.md b/doc/sdk_comp.md index 211599b..fb8315a 100644 --- a/doc/sdk_comp.md +++ b/doc/sdk_comp.md @@ -136,8 +136,8 @@ bytes.hpp | 🚧 | is_numop bytes.hpp | 🚧 | is_suspop bytes.hpp | 🚧 | op_adds_xrefs bytes.hpp | 🚧 | op_seg -bytes.hpp | 🚧 | op_enum -bytes.hpp | 🚧 | get_enum_id +bytes.hpp | ✔️ | op_enum | AddressInfo::op_enum +bytes.hpp | ✔️ | get_enum_id | AddressInfo::op_enum bytes.hpp | 🚧 | op_stroff bytes.hpp | 🚧 | op_based_stroff bytes.hpp | 🚧 | get_stroff_path diff --git a/src/addr_info.rs b/src/addr_info.rs index 1c088f9..bdbd645 100644 --- a/src/addr_info.rs +++ b/src/addr_info.rs @@ -1,7 +1,7 @@ use crate::bytes_info::BytesInfo; use crate::id0::entry_iter::EntryTagContinuousSubkeys; use crate::id0::flag::nalt::x::NALT_DREF_FROM; -use crate::id0::flag::nalt::NALT_STRTYPE; +use crate::id0::flag::nalt::{NALT_ENUM0, NALT_ENUM1, NALT_STRTYPE}; use crate::id0::flag::netnode::nn_res::{ARRAY_ALT_TAG, ARRAY_SUP_TAG}; use crate::id0::flag::nsup::NSUP_TYPEINFO; use crate::id0::{ @@ -11,7 +11,7 @@ use crate::id0::{ use crate::id1::{ByteDataType, ByteInfo, ByteType, ID1Section}; use crate::id2::ID2Section; use crate::til::Type; -use crate::{Address, IDAKind, IDBStr, IDBString}; +use crate::{Address, IDAKind, IDAUsize, IDBStr, IDBString}; use anyhow::{anyhow, Result}; @@ -134,6 +134,24 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { } } + /// The enumeration referenced by an operand displayed as an enum, if any. + /// + /// Reads the `NALT_ENUM0`/`NALT_ENUM1` altval (see `get_enum_id` / `op_enum` in `bytes.hpp`) + /// for operand 0 or 1, returning the referenced enumeration's id. Returns `None` when the + /// operand is not displayed as an enum (or for operands other than 0/1). + pub fn op_enum(&self, operand: u8) -> Option { + let index = match operand { + 0 => NALT_ENUM0, + 1 => NALT_ENUM1, + _ => return None, + }; + self.id0 + .altval(self.netnode(), index.into(), ARRAY_ALT_TAG) + .ok() + .flatten() + .map(|value| value.into_raw().into_u64()) + } + /// The string literal type IDA assigned to this address, if any. /// /// Decodes the `NALT_STRTYPE` altval (see `get_str_type` in `nalt.hpp`). Returns `None` for From 08cf90aa9ecb0cc70388e730b6b6e22f7016bb8b Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 16:00:46 -0500 Subject: [PATCH 5/7] Resolve enum operands to their symbolic name An enum operand's altval holds a tid, and that tid is itself a netnode whose N-tag name is the symbolic constant / type it identifies. Add ID0Section::netnode_type_name to read that name (stripping IDA's `$$ ` prefix) and AddressInfo::op_enum_name to resolve an operand's tid to it. This works whether the enumeration lives in a type library or the local types, so callers can identify exactly which enum an operand uses rather than only seeing a raw tid. --- doc/sdk_comp.md | 4 ++-- src/addr_info.rs | 19 ++++++++++++++++++- src/id0/db.rs | 12 ++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/doc/sdk_comp.md b/doc/sdk_comp.md index fb8315a..4ec295a 100644 --- a/doc/sdk_comp.md +++ b/doc/sdk_comp.md @@ -136,8 +136,8 @@ bytes.hpp | 🚧 | is_numop bytes.hpp | 🚧 | is_suspop bytes.hpp | 🚧 | op_adds_xrefs bytes.hpp | 🚧 | op_seg -bytes.hpp | ✔️ | op_enum | AddressInfo::op_enum -bytes.hpp | ✔️ | get_enum_id | AddressInfo::op_enum +bytes.hpp | ✔️ | op_enum | AddressInfo::op_enum / op_enum_name +bytes.hpp | ✔️ | get_enum_id | AddressInfo::op_enum / op_enum_name bytes.hpp | 🚧 | op_stroff bytes.hpp | 🚧 | op_based_stroff bytes.hpp | 🚧 | get_stroff_path diff --git a/src/addr_info.rs b/src/addr_info.rs index bdbd645..2d5849f 100644 --- a/src/addr_info.rs +++ b/src/addr_info.rs @@ -140,6 +140,12 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { /// for operand 0 or 1, returning the referenced enumeration's id. Returns `None` when the /// operand is not displayed as an enum (or for operands other than 0/1). pub fn op_enum(&self, operand: u8) -> Option { + self.op_enum_netnode(operand) + .map(|node| node.into_raw().into_u64()) + } + + /// The enumeration referenced by an operand, as the netnode (tid) it points at. + fn op_enum_netnode(&self, operand: u8) -> Option> { let index = match operand { 0 => NALT_ENUM0, 1 => NALT_ENUM1, @@ -149,7 +155,18 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { .altval(self.netnode(), index.into(), ARRAY_ALT_TAG) .ok() .flatten() - .map(|value| value.into_raw().into_u64()) + } + + /// The symbolic name an enum operand resolves to (an enumeration member, or the enumeration + /// itself), recovered from the referenced tid's netnode name. + /// + /// The tid is itself a netnode; its `N` name holds the symbolic constant / type name. This + /// works whether the enumeration lives in a type library or the local types. + pub fn op_enum_name(&self, operand: u8) -> Option { + let node = self.op_enum_netnode(operand)?; + self.id0 + .netnode_type_name(node) + .map(|name| IDBString::new(name.to_vec())) } /// The string literal type IDA assigned to this address, if any. diff --git a/src/id0/db.rs b/src/id0/db.rs index dedce87..3921db7 100644 --- a/src/id0/db.rs +++ b/src/id0/db.rs @@ -1298,6 +1298,18 @@ impl ID0Section { Ok(parse_maybe_cstr(value)) } + /// The name stored on a netnode (its `N` tag), with IDA's `$$ ` prefix stripped. + /// + /// Type and enum-member netnodes (the ones referenced by a `tid`) keep their name here, so + /// this resolves a `tid` to the type/member name it identifies. + pub fn netnode_type_name(&self, idx: NetnodeIdx) -> Option<&[u8]> { + let key: Vec = key_from_netnode_tag::(idx.0, b'N').collect(); + let start = self.binary_search(&key).ok()?; + let value = &self.entries[start].value; + let value = value.strip_prefix(b"$$ ").unwrap_or(&value[..]); + Some(parse_maybe_cstr(value)) + } + /// read the `$ funcords` entries of the database pub fn funcords_idx(&self) -> Result>> { self.netnode_idx_by_name("$ funcords") From a8434c9bf96468e7a6ed6a7c742d1203e644a300 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 16:10:49 -0500 Subject: [PATCH 6/7] Resolve enum operands to the exact enumeration type Add AddressInfo::op_enum_type, which maps an enum operand's member tid back to the enumeration it belongs to. An enum operand references a specific member whose netnode is allocated right after the enumeration's own netnode (members occupy enum_tid + 1 ..= enum_tid + member_count), so the member tid is matched against each enumeration's tid range. This returns the exact enumeration regardless of member-name collisions, for enumerations in a type library or the local types, rather than relying on a name match. --- doc/sdk_comp.md | 4 ++-- src/addr_info.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/doc/sdk_comp.md b/doc/sdk_comp.md index 4ec295a..99d9165 100644 --- a/doc/sdk_comp.md +++ b/doc/sdk_comp.md @@ -136,8 +136,8 @@ bytes.hpp | 🚧 | is_numop bytes.hpp | 🚧 | is_suspop bytes.hpp | 🚧 | op_adds_xrefs bytes.hpp | 🚧 | op_seg -bytes.hpp | ✔️ | op_enum | AddressInfo::op_enum / op_enum_name -bytes.hpp | ✔️ | get_enum_id | AddressInfo::op_enum / op_enum_name +bytes.hpp | ✔️ | op_enum | AddressInfo::op_enum / op_enum_name / op_enum_type +bytes.hpp | ✔️ | get_enum_id | AddressInfo::op_enum / op_enum_name / op_enum_type bytes.hpp | 🚧 | op_stroff bytes.hpp | 🚧 | op_based_stroff bytes.hpp | 🚧 | get_stroff_path diff --git a/src/addr_info.rs b/src/addr_info.rs index 2d5849f..abe2a9f 100644 --- a/src/addr_info.rs +++ b/src/addr_info.rs @@ -10,7 +10,9 @@ use crate::id0::{ }; use crate::id1::{ByteDataType, ByteInfo, ByteType, ID1Section}; use crate::id2::ID2Section; -use crate::til::Type; +use crate::til::section::TILSection; +use crate::til::r#enum::EnumMembers; +use crate::til::{TILTypeInfo, Type, TypeVariant}; use crate::{Address, IDAKind, IDAUsize, IDBStr, IDBString}; use anyhow::{anyhow, Result}; @@ -169,6 +171,47 @@ impl<'a, K: IDAKind> AddressInfo<'a, K> { .map(|name| IDBString::new(name.to_vec())) } + /// The enumeration type an operand is displayed against. + /// + /// An enum operand's tid identifies a specific enumeration member, whose netnode sits right + /// after the enumeration's own netnode (members are allocated at `enum_tid + 1 ..= + /// enum_tid + member_count`). This resolves the member tid back to the enumeration in `til` + /// by that tid range, so it returns the exact enumeration regardless of any member-name + /// collisions, working for enumerations in a type library or the local types. + pub fn op_enum_type<'t>( + &self, + operand: u8, + til: &'t TILSection, + ) -> Option<&'t TILTypeInfo> { + let member_tid = self.op_enum(operand)?; + for ty in &til.types { + let TypeVariant::Enum(en) = &ty.tinfo.type_variant else { + continue; + }; + let member_count = match &en.members { + EnumMembers::Regular(members) => members.len(), + EnumMembers::Groups(groups) => { + groups.iter().map(|g| g.sub_fields.len()).sum() + } + } as u64; + // The enumeration's netnode is named with IDA's `$$ ` type-name prefix. + let enum_name = format!("$$ {}", ty.name.as_utf8_lossy()); + let Some(enum_tid) = self + .id0 + .netnode_idx_by_name(&enum_name) + .ok() + .flatten() + .map(|node| node.into_raw().into_u64()) + else { + continue; + }; + if member_tid > enum_tid && member_tid <= enum_tid + member_count { + return Some(ty); + } + } + None + } + /// The string literal type IDA assigned to this address, if any. /// /// Decodes the `NALT_STRTYPE` altval (see `get_str_type` in `nalt.hpp`). Returns `None` for From 67503f0bb06b1d38adfc43328e01b641ba0bdc24 Mon Sep 17 00:00:00 2001 From: Chris Kader Date: Fri, 5 Jun 2026 16:45:08 -0500 Subject: [PATCH 7/7] tools: report enum-displayed operands in dump-address-info Print op_enum tid, the resolved member name and the owning enumeration (op_enum_type) for operands 0 and 1, so the address-info dump can be used to inspect which operands IDA displays against an enumeration. --- src/tools/dump_addr_info.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/tools/dump_addr_info.rs b/src/tools/dump_addr_info.rs index 8212cec..7047b38 100644 --- a/src/tools/dump_addr_info.rs +++ b/src/tools/dump_addr_info.rs @@ -1,4 +1,4 @@ -use crate::{get_id0_id1_id2_sections, Args, Id0Id1Id2Variant}; +use crate::{get_id0_id1_id2_sections, get_til_section, Args, Id0Id1Id2Variant}; use anyhow::Result; @@ -6,14 +6,18 @@ use idb_rs::addr_info::all_address_info; use idb_rs::{IDAKind, IDAVariants}; pub fn dump_addr_info(args: &Args) -> Result<()> { + let til = get_til_section(args).ok(); // parse the id0 sector/file match get_id0_id1_id2_sections(args)? { - IDAVariants::IDA32(kind) => dump_inner(kind), - IDAVariants::IDA64(kind) => dump_inner(kind), + IDAVariants::IDA32(kind) => dump_inner(kind, til.as_ref()), + IDAVariants::IDA64(kind) => dump_inner(kind, til.as_ref()), } } -fn dump_inner((id0, id1, id2): Id0Id1Id2Variant) -> Result<()> { +fn dump_inner( + (id0, id1, id2): Id0Id1Id2Variant, + til: Option<&idb_rs::til::section::TILSection>, +) -> Result<()> { // TODO create a function for that in ida_info let root_info_idx = id0.root_node()?; let root_info = id0.ida_info(root_info_idx)?; @@ -47,6 +51,18 @@ fn dump_inner((id0, id1, id2): Id0Id1Id2Variant) -> Result<()> { if let Some(tinfo) = addr_info.tinfo(&root_info)? { write!(&mut buf, " Tinfo: {tinfo:?}",)?; } + for operand in 0u8..2 { + if let Some(id) = addr_info.op_enum(operand) { + let name = addr_info.op_enum_name(operand); + let enum_ty = til.and_then(|t| addr_info.op_enum_type(operand, t)); + write!( + &mut buf, + " OpEnum[{operand}]: tid={id:#x} member={:?} enum_type={:?}", + name.as_ref().map(|n| n.as_utf8_lossy()), + enum_ty.map(|t| t.name.as_utf8_lossy()) + )?; + } + } if !buf.is_empty() { println!("{:#010x?}:{buf}", addr.into_raw()); }