use std::{borrow::Cow, str};
use crate::{
addressmap::AddressMap,
error::Error,
project::{ControlEntryIterator, MethodEntry, MethodLinkIterator, PCodeMethod, VbProject},
util::read_u32_le,
vb::{
constantpool::ConstantPool,
control::Guid,
eventname,
flags::ObjectTypeFlags,
formdata::{FormControlType, FormDataParser},
functype::FuncTypDesc,
guitable::GuiTableEntry,
object::{GuidTableIter, ObjectInfo, OptionalObjectInfo, PublicObjectDescriptor},
privateobj::PrivateObjectDescriptor,
publicbytes::{ClassFormPublicBytes, PublicVarTable},
varstub::VarStubIter,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodNameResult<'a> {
NoTable,
Unnamed,
Name(&'a [u8]),
}
impl<'a> MethodNameResult<'a> {
pub fn as_bytes(&self) -> Option<&'a [u8]> {
match self {
Self::Name(n) => Some(n),
_ => None,
}
}
pub fn as_str(&self) -> Option<Cow<'a, str>> {
self.as_bytes().map(String::from_utf8_lossy)
}
}
pub fn format_signature(ftd: &FuncTypDesc<'_>, name: &str, map: &AddressMap<'_>) -> String {
let kind = ftd.kind_keyword();
let ret = ftd
.return_type()
.map(|t| format!(" As {t}"))
.unwrap_or_default();
let param_names = ftd.param_names(map);
let arg_types = ftd.arg_types();
let args = if param_names.is_empty() && ftd.arg_count() == 0 {
"()".into()
} else if param_names.is_empty() && arg_types.is_empty() {
format!("({} args)", ftd.arg_count())
} else {
let count = ftd.arg_count() as usize;
let params: Vec<String> = (0..count)
.map(|i| {
let pname = param_names
.get(i)
.filter(|n| !n.is_empty())
.map(|n| String::from_utf8_lossy(n).into_owned());
let ptype = arg_types.get(i).map(|t| format!("{t}"));
match (pname, ptype) {
(Some(n), Some(t)) => format!("{n} As {t}"),
(Some(n), None) => n,
(None, Some(t)) => format!("arg{i} As {t}"),
(None, None) => format!("arg{i}"),
}
})
.collect();
format!("({})", params.join(", "))
};
format!("{kind} {name}{args}{ret}")
}
#[derive(Debug)]
pub struct VbObject<'a, 'p> {
project: &'p VbProject<'a>,
descriptor: PublicObjectDescriptor<'a>,
info: ObjectInfo<'a>,
optional_info: Option<OptionalObjectInfo<'a>>,
private_object: Option<PrivateObjectDescriptor<'a>>,
}
impl<'a, 'p: 'a> VbObject<'a, 'p> {
pub fn parse(project: &'p VbProject<'a>, index: u16) -> Result<Self, Error> {
let map = project.address_map();
let ot = project.object_table();
if index >= ot.total_objects()? {
return Err(Error::ObjectIndexOutOfRange {
index,
total: ot.total_objects()?,
});
}
let array_offset = u32::from(index).saturating_mul(PublicObjectDescriptor::SIZE as u32);
let desc_data = map.slice_from_va(
ot.object_array_va()?.wrapping_add(array_offset),
PublicObjectDescriptor::SIZE,
)?;
let descriptor = PublicObjectDescriptor::parse(desc_data)?;
let info_data = map.slice_from_va(descriptor.object_info_va()?, ObjectInfo::SIZE)?;
let info = ObjectInfo::parse(info_data)?;
let optional_info = if descriptor.has_optional_info() {
let opt_va = descriptor
.object_info_va()?
.wrapping_add(ObjectInfo::SIZE as u32);
let constants_va = info.constants_va()?;
let has_room = constants_va == 0
|| constants_va >= opt_va.wrapping_add(OptionalObjectInfo::SIZE as u32);
if has_room {
map.slice_from_va(opt_va, OptionalObjectInfo::SIZE)
.ok()
.and_then(|d| OptionalObjectInfo::parse(d).ok())
} else {
None
}
} else {
None
};
let priv_va = info.private_object_va()?;
let private_object = if priv_va != 0 && priv_va != 0xFFFFFFFF {
map.slice_from_va(priv_va, PrivateObjectDescriptor::SIZE)
.ok()
.and_then(|d| PrivateObjectDescriptor::parse(d).ok())
} else {
None
};
Ok(Self {
project,
descriptor,
info,
optional_info,
private_object,
})
}
#[inline]
pub fn project(&self) -> &'p VbProject<'a> {
self.project
}
#[inline]
pub fn descriptor(&self) -> &PublicObjectDescriptor<'a> {
&self.descriptor
}
#[inline]
pub fn info(&self) -> &ObjectInfo<'a> {
&self.info
}
#[inline]
pub fn optional_info(&self) -> Option<&OptionalObjectInfo<'a>> {
self.optional_info.as_ref()
}
#[inline]
pub fn private_object(&self) -> Option<&PrivateObjectDescriptor<'a>> {
self.private_object.as_ref()
}
#[inline]
pub fn public_func_count(&self) -> Result<u32, Error> {
match self.private_object.as_ref() {
Some(p) => Ok(u32::from(p.func_count()?)),
None => Ok(0),
}
}
#[inline]
pub fn public_var_count(&self) -> Result<u32, Error> {
match self.private_object.as_ref() {
Some(p) => Ok(u32::from(p.var_count()?)),
None => Ok(0),
}
}
pub fn name(&self) -> Result<Cow<'a, str>, Error> {
Ok(String::from_utf8_lossy(self.name_bytes()?))
}
pub fn name_bytes(&self) -> Result<&'a [u8], Error> {
self.project
.read_string_at_va(self.descriptor.object_name_va()?)
}
pub fn object_kind(&self) -> Result<&'static str, Error> {
Ok(ObjectTypeFlags(self.descriptor.object_type_raw()?).kind_name())
}
pub fn method_name(&self, index: u16) -> Result<MethodNameResult<'a>, Error> {
let names_va = self.descriptor.method_names_va()?;
if names_va == 0 {
return Ok(MethodNameResult::NoTable);
}
let entry_va = names_va.wrapping_add(u32::from(index).saturating_mul(4));
let entry_data = self.project.address_map().slice_from_va(entry_va, 4)?;
let name_va = read_u32_le(entry_data, 0)?;
if name_va == 0 {
return Ok(MethodNameResult::Unnamed);
}
self.project
.read_string_at_va(name_va)
.map(MethodNameResult::Name)
}
#[inline]
pub fn method_count(&self) -> Result<u16, Error> {
let info_count = self.info.method_count()?;
let desc_count = self.descriptor.method_count()? as u16;
Ok(info_count.max(desc_count))
}
#[inline]
pub fn object_type_flags(&self) -> Result<ObjectTypeFlags, Error> {
Ok(ObjectTypeFlags(self.descriptor.object_type_raw()?))
}
pub fn has_pcode(&self) -> Result<bool, Error> {
match self.optional_info.as_ref() {
Some(opt) => Ok(opt.pcode_count()? > 0),
None => Ok(false),
}
}
#[inline]
pub fn control_count(&self) -> Result<u32, Error> {
match self.optional_info.as_ref() {
Some(opt) => opt.control_count(),
None => Ok(0),
}
}
pub fn controls(&self) -> Result<ControlEntryIterator<'a, 'p>, Error> {
let (controls_va, count) = match self.optional_info.as_ref() {
Some(opt) => {
let cc = opt.control_count()?;
let cv = opt.controls_va()?;
if cc > 0 && cv != 0 { (cv, cc) } else { (0, 0) }
}
None => (0, 0),
};
Ok(ControlEntryIterator::new(
self.project.address_map(),
controls_va,
count,
))
}
pub fn controls_with_form_data(
&self,
form_data: &'p FormDataParser<'a>,
) -> Result<ControlEntryIterator<'a, 'p>, Error> {
Ok(self.controls()?.with_form_data(form_data))
}
pub fn method_links(&self) -> Result<MethodLinkIterator<'a, 'p>, Error> {
let (table_va, count) = match self.optional_info.as_ref() {
Some(opt) => {
let mlc = opt.method_link_count()?;
let mlv = opt.method_link_table_va()?;
if mlc > 0 && mlv != 0 {
(mlv, u32::from(mlc))
} else {
(0, 0)
}
}
None => (0, 0),
};
Ok(MethodLinkIterator::new(
self.project.address_map(),
table_va,
count,
))
}
pub fn has_method_table(&self) -> Result<bool, Error> {
let m = self.info.methods_va()?;
let c = self.info.constants_va()?;
Ok(m != 0 && m != c)
}
pub fn methods(&self) -> Result<MethodIterator<'a, 'p>, Error> {
let total = if self.has_method_table()? {
self.method_count()?
} else {
0
};
Ok(MethodIterator {
map: self.project.address_map(),
methods_va: self.info.methods_va()?,
index: 0,
total,
})
}
pub fn pcode_methods(&self) -> Result<PCodeMethodIterator<'a, 'p>, Error> {
let total = if self.has_method_table()? {
self.method_count()?
} else {
0
};
Ok(PCodeMethodIterator {
map: self.project.address_map(),
methods_va: self.info.methods_va()?,
index: 0,
total,
})
}
pub fn class_form_public_bytes(&self) -> Option<ClassFormPublicBytes<'a>> {
if self.object_type_flags().ok()?.is_module() {
return None;
}
let pb_va = self.descriptor.public_bytes_va().ok()?;
if pb_va == 0 {
return None;
}
let data = self.project.address_map().slice_from_va(pb_va, 0x80).ok()?;
ClassFormPublicBytes::parse(data).ok()
}
pub fn public_var_table(&self) -> Option<PublicVarTable<'a>> {
if !self.object_type_flags().ok()?.is_module() {
return None;
}
let pb_va = self.descriptor.public_bytes_va().ok()?;
if pb_va == 0 {
return None;
}
let map = self.project.address_map();
let header = map.slice_from_va(pb_va, PublicVarTable::HEADER_SIZE).ok()?;
let pvt_header = PublicVarTable::parse(header).ok()?;
let full_size = pvt_header.total_size().ok()? as usize;
if full_size <= PublicVarTable::HEADER_SIZE {
return Some(pvt_header);
}
let full_data = map.slice_from_va(pb_va, full_size).ok()?;
PublicVarTable::parse(full_data).ok()
}
pub fn code_entries(
&self,
form_data: Option<&FormDataParser<'a>>,
) -> Result<Vec<CodeEntry>, Error> {
let mut entries = Vec::new();
let map = self.project.address_map();
let ftd_map = self.build_func_type_desc_map()?;
if self.has_method_table()? {
for (i, result) in self.methods()?.enumerate() {
match result? {
MethodEntry::PCode(pm) => {
let name = self.resolve_method_name(i, &ftd_map);
entries.push(CodeEntry {
va: pm.pcode_va(),
kind: CodeEntryKind::PCode,
method_index: Some(i as u16),
name,
data_const_va: Some(pm.data_const_va()),
stub_va: Some(pm.stub_va()),
pcode_size: Some(pm.proc_size()?),
});
}
MethodEntry::Native { va } => {
let name = self.resolve_method_name(i, &ftd_map);
entries.push(CodeEntry {
va,
kind: CodeEntryKind::Native,
method_index: Some(i as u16),
name,
data_const_va: None,
stub_va: None,
pcode_size: None,
});
}
_ => {}
}
}
}
for (link_idx, result) in self.method_links()?.enumerate() {
if let Ok(link) = result {
if !entries.iter().any(|e| e.va == link.code_va) {
let name = self.resolve_method_name(link_idx, &ftd_map);
entries.push(CodeEntry {
va: link.code_va,
kind: CodeEntryKind::NativeThunk,
method_index: Some(link_idx as u16),
name,
data_const_va: None,
stub_va: None,
pcode_size: None,
});
}
}
}
let controls: Vec<_> = if let Some(fd) = form_data {
self.controls_with_form_data(fd)?
.filter_map(|r| match r {
Ok(c) => Some(c),
Err(e) => {
crate::trace::warn_drop!("code_entries.controls_with_form_data", error = ?e);
None
}
})
.collect()
} else {
self.controls()?
.filter_map(|r| match r {
Ok(c) => Some(c),
Err(e) => {
crate::trace::warn_drop!("code_entries.controls", error = ?e);
None
}
})
.collect()
};
for ctrl in &controls {
let ctrl_name_cow = ctrl.name();
let ctrl_name = ctrl_name_cow.as_ref();
let ctrl_type = ctrl
.form_control_type()
.or_else(|| ctrl.class_name().and_then(FormControlType::from_class_name));
for slot in 0..ctrl.event_count()? {
if let Some(handler_va) = ctrl.event_handler_va(slot)
&& handler_va != 0
&& map.va_to_offset(handler_va).is_ok()
{
let event_name = ctrl_type
.and_then(|ct| eventname::event_name(slot, ct))
.or_else(|| eventname::standard_event_name(slot));
let label = match event_name {
Some(en) => format!("{ctrl_name}_{en}"),
None => format!("{ctrl_name}_Event{slot:02}"),
};
if !entries.iter().any(|e| e.va == handler_va) {
entries.push(CodeEntry {
va: handler_va,
kind: CodeEntryKind::EventHandler,
method_index: None,
name: Some(label),
data_const_va: None,
stub_va: None,
pcode_size: None,
});
}
}
}
}
Ok(entries)
}
fn resolve_method_name(
&self,
index: usize,
ftd_map: &[(usize, FuncTypDesc<'a>)],
) -> Option<String> {
if let Ok(result) = self.method_name(index as u16)
&& let MethodNameResult::Name(n) = result
&& let Ok(s) = str::from_utf8(n)
{
return Some(s.to_string());
}
for &(fi, ref ftd) in ftd_map {
if fi == index {
let name = format_signature(ftd, "", self.project.address_map());
let trimmed = name.trim();
if !trimmed.is_empty() && trimmed != "()" {
return Some(trimmed.to_string());
}
}
}
None
}
fn build_func_type_desc_map(&self) -> Result<Vec<(usize, FuncTypDesc<'a>)>, Error> {
let Some(priv_obj) = self.private_object() else {
return Ok(Vec::new());
};
let ftd_array_va = priv_obj.func_type_descs_va()?;
if ftd_array_va == 0 {
return Ok(Vec::new());
}
let total =
u32::from(priv_obj.func_count()?).saturating_add(u32::from(priv_obj.var_count()?));
let map = self.project.address_map();
let mut result = Vec::new();
for i in 0..total {
let ptr_va = ftd_array_va.wrapping_add(i.saturating_mul(4));
let Ok(ptr_data) = map.slice_from_va(ptr_va, 4) else {
continue;
};
let Some(ptr_bytes) = ptr_data.get(..4).and_then(|s| <[u8; 4]>::try_from(s).ok())
else {
continue;
};
let desc_va = u32::from_le_bytes(ptr_bytes);
if desc_va == 0 {
continue;
}
let Ok(desc_data) = map.slice_from_va(desc_va, 0x40) else {
continue;
};
if let Ok(ftd) = FuncTypDesc::parse_extended(desc_data) {
result.push((i as usize, ftd));
}
}
Ok(result)
}
pub fn form_data_from_gui_entry(
&self,
gui_entry: &GuiTableEntry<'a>,
) -> Option<FormDataParser<'a>> {
let va = gui_entry.form_data_va().ok()?;
let size = gui_entry.form_data_size().ok()? as usize;
if va == 0 || size == 0 {
return None;
}
let data = self.project.address_map().slice_from_va(va, size).ok()?;
FormDataParser::parse(data).ok()
}
pub fn events(
&self,
form_data: Option<&'p FormDataParser<'a>>,
) -> Result<Vec<EventBinding<'a>>, Error> {
self.events_inner(form_data, true)
}
pub fn events_all_slots(
&self,
form_data: Option<&'p FormDataParser<'a>>,
) -> Result<Vec<EventBinding<'a>>, Error> {
self.events_inner(form_data, false)
}
fn events_inner(
&self,
form_data: Option<&'p FormDataParser<'a>>,
connected_only: bool,
) -> Result<Vec<EventBinding<'a>>, Error> {
let mut bindings = Vec::new();
let controls: Vec<_> = if let Some(fd) = form_data {
self.controls_with_form_data(fd)?
.filter_map(|r| match r {
Ok(c) => Some(c),
Err(e) => {
crate::trace::warn_drop!("events.controls_with_form_data", error = ?e);
None
}
})
.collect()
} else {
self.controls()?
.filter_map(|r| match r {
Ok(c) => Some(c),
Err(e) => {
crate::trace::warn_drop!("events.controls", error = ?e);
None
}
})
.collect()
};
for ctrl in &controls {
let ctrl_type = ctrl
.form_control_type()
.or_else(|| ctrl.class_name().and_then(FormControlType::from_class_name));
let ctrl_index = ctrl.index()?;
let event_count = ctrl.event_count()?;
for slot in 0..event_count {
let handler_va = ctrl.event_handler_va(slot).unwrap_or(0);
if connected_only && handler_va == 0 {
continue;
}
let event_name = ctrl_type
.and_then(|ct| eventname::event_name(slot, ct))
.or_else(|| eventname::standard_event_name(slot));
bindings.push(EventBinding {
control_index: ctrl_index,
control_name: ctrl.name(),
control_type: ctrl_type,
event_slot: slot,
event_name,
handler_va,
});
}
}
Ok(bindings)
}
#[inline]
pub fn form_designer_data(&self, gui_entry: &GuiTableEntry<'a>) -> Option<FormDataParser<'a>> {
self.form_data_from_gui_entry(gui_entry)
}
pub fn constants_pool(&self) -> Result<ConstantPool<'a>, Error> {
Ok(ConstantPool::new(
self.project.address_map(),
self.info.constants_va()?,
))
}
pub fn object_clsid(&self) -> Option<Guid> {
self.optional_info
.as_ref()
.and_then(|opt| opt.resolve_clsid(self.project.address_map()))
}
pub fn gui_guids(&self) -> GuidTableIter<'_> {
match self.optional_info.as_ref() {
Some(opt) => opt.gui_guids(self.project.address_map()),
None => GuidTableIter::new(self.project.address_map(), 0, 0),
}
}
pub fn default_iids(&self) -> GuidTableIter<'_> {
match self.optional_info.as_ref() {
Some(opt) => opt.default_iids(self.project.address_map()),
None => GuidTableIter::new(self.project.address_map(), 0, 0),
}
}
pub fn events_iids(&self) -> GuidTableIter<'_> {
match self.optional_info.as_ref() {
Some(opt) => opt.events_iids(self.project.address_map()),
None => GuidTableIter::new(self.project.address_map(), 0, 0),
}
}
pub fn func_type_descs(&self) -> Result<FuncTypDescIter<'a, 'p>, Error> {
let (ftd_va, total) = match self.private_object.as_ref() {
Some(p) => {
let va = p.func_type_descs_va()?;
if va == 0 {
(0, 0)
} else {
let total =
u32::from(p.func_count()?).saturating_add(u32::from(p.var_count()?));
(va, total)
}
}
None => (0, 0),
};
Ok(FuncTypDescIter {
map: self.project.address_map(),
ftd_array_va: ftd_va,
index: 0,
total,
})
}
pub fn var_stubs(&self) -> Result<VarStubIter<'a>, Error> {
let (stubs_va, count) = match self.private_object.as_ref() {
Some(p) => {
let va = p.var_stubs_va()?;
let cnt = p.var_count()?;
if va != 0 && cnt > 0 {
(va, cnt)
} else {
(0, 0)
}
}
None => (0, 0),
};
Ok(VarStubIter::new(
self.project.address_map(),
stubs_va,
count,
))
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct FuncTypDescIter<'a, 'p> {
map: &'p AddressMap<'a>,
ftd_array_va: u32,
index: u32,
total: u32,
}
impl<'a, 'p> Iterator for FuncTypDescIter<'a, 'p> {
type Item = (u32, FuncTypDesc<'a>);
fn next(&mut self) -> Option<Self::Item> {
while self.index < self.total {
let i = self.index;
self.index = self.index.saturating_add(1);
let ptr_va = self.ftd_array_va.wrapping_add(i.saturating_mul(4));
let ptr_data = self.map.slice_from_va(ptr_va, 4).ok()?;
let ptr_bytes: [u8; 4] = ptr_data.get(..4).and_then(|s| s.try_into().ok())?;
let desc_va = u32::from_le_bytes(ptr_bytes);
if desc_va == 0 {
continue;
}
let desc_data = self.map.slice_from_va(desc_va, 0x40).ok()?;
if let Ok(ftd) = FuncTypDesc::parse_extended(desc_data) {
return Some((i, ftd));
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct CodeEntry {
pub va: u32,
pub kind: CodeEntryKind,
pub method_index: Option<u16>,
pub name: Option<String>,
pub data_const_va: Option<u32>,
pub stub_va: Option<u32>,
pub pcode_size: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeEntryKind {
PCode,
Native,
NativeThunk,
EventHandler,
}
#[derive(Debug, Clone)]
pub struct EventBinding<'a> {
pub control_index: u16,
pub control_name: Cow<'a, str>,
pub control_type: Option<FormControlType>,
pub event_slot: u16,
pub event_name: Option<&'static str>,
pub handler_va: u32,
}
impl<'a> EventBinding<'a> {
#[inline]
pub fn is_connected(&self) -> bool {
self.handler_va != 0
}
pub fn label(&self) -> String {
let ctrl = self.control_name.as_ref();
match self.event_name {
Some(name) => format!("{ctrl}_{name}"),
None => format!("{ctrl}_Event{:02}", self.event_slot),
}
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct MethodIterator<'a, 'p> {
map: &'p AddressMap<'a>,
methods_va: u32,
index: u16,
total: u16,
}
impl<'a, 'p> Iterator for MethodIterator<'a, 'p> {
type Item = Result<MethodEntry<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.total {
return None;
}
let i = self.index;
self.index = self.index.saturating_add(1);
Some(MethodEntry::classify(self.map, self.methods_va, i))
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct PCodeMethodIterator<'a, 'p> {
map: &'p AddressMap<'a>,
methods_va: u32,
index: u16,
total: u16,
}
impl<'a, 'p> Iterator for PCodeMethodIterator<'a, 'p> {
type Item = Result<PCodeMethod<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
while self.index < self.total {
let i = self.index;
self.index = self.index.saturating_add(1);
let entry_va = self.methods_va.wrapping_add(u32::from(i).saturating_mul(4));
let entry_data = match self.map.slice_from_va(entry_va, 4) {
Ok(d) => d,
Err(e) => return Some(Err(e)),
};
let method_va = match read_u32_le(entry_data, 0) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
if method_va == 0 || !self.map.is_va_in_image(method_va) {
continue;
}
let stub_data = match self.map.slice_from_va(method_va, 12) {
Ok(d) => d,
Err(e) => return Some(Err(e)),
};
let is_stub = stub_data.first().copied() == Some(0xBA)
|| (stub_data.first().copied() == Some(0x33)
&& stub_data.get(1).copied() == Some(0xC0)
&& stub_data.get(2).copied() == Some(0xBA));
if !is_stub {
let Some(pt_bytes) = stub_data.get(..4).and_then(|s| <[u8; 4]>::try_from(s).ok())
else {
continue;
};
let Some(ps_bytes) = stub_data
.get(8..10)
.and_then(|s| <[u8; 2]>::try_from(s).ok())
else {
continue;
};
let maybe_pt = u32::from_le_bytes(pt_bytes);
let maybe_ps = u16::from_le_bytes(ps_bytes);
if !self.map.is_va_in_image(maybe_pt) || maybe_ps == 0 || maybe_ps >= 0x8000 {
continue; }
}
return Some(PCodeMethod::parse(self.map, self.methods_va, i));
}
None
}
}