use std::borrow::Cow;
use crate::{
addressmap::AddressMap,
entrypoint,
error::Error,
project::{CodeEntryKind, PCodeMethod, VbObject},
util::read_cstr,
vb::{
external::{ExternalComponentIter, ExternalTableEntry},
formdata::FormDataParser,
guitable::{GuiTableEntry, GuiTableIter},
header::VbHeader,
objecttable::ObjectTable,
projectdata::ProjectData,
},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EntrypointKind {
PCodeStub,
NativeProc,
NativeThunk,
EventHandler,
SubMain,
}
impl From<CodeEntryKind> for EntrypointKind {
fn from(k: CodeEntryKind) -> Self {
match k {
CodeEntryKind::PCode => Self::PCodeStub,
CodeEntryKind::Native => Self::NativeProc,
CodeEntryKind::NativeThunk => Self::NativeThunk,
CodeEntryKind::EventHandler => Self::EventHandler,
}
}
}
#[derive(Debug, Clone)]
pub struct CodeEntrypoint<'a> {
pub va: u32,
pub kind: EntrypointKind,
pub name_hint: Cow<'a, str>,
pub object_index: Option<u16>,
pub method_index: Option<u16>,
pub is_pcode: bool,
pub data_const_va: Option<u32>,
pub stub_va: Option<u32>,
pub pcode_size: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticKind {
AbsentOptional,
Quirk,
Malformed,
}
#[derive(Debug, Clone)]
pub struct ParseDiagnostic {
pub kind: DiagnosticKind,
pub severity: DiagnosticSeverity,
pub object_index: Option<u16>,
pub site: &'static str,
pub message: Cow<'static, str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CompilationMode {
Pcode,
Native,
Mixed,
}
#[derive(Debug)]
pub struct VbProject<'a> {
map: AddressMap<'a>,
vb_header_va: u32,
vb_header: VbHeader<'a>,
project_data: ProjectData<'a>,
object_table: ObjectTable<'a>,
}
impl<'a> VbProject<'a> {
pub fn from_bytes(file: &'a [u8]) -> Result<Self, Error> {
let pe = goblin::pe::PE::parse(file).map_err(|e| Error::UnrecognizedFormat {
reason: format!("goblin: {e}"),
})?;
Self::from_goblin(file, &pe)
}
pub fn from_goblin(file: &'a [u8], pe: &goblin::pe::PE<'_>) -> Result<Self, Error> {
let map = AddressMap::from_goblin(file, pe)?;
let entry_rva = pe
.header
.optional_header
.as_ref()
.ok_or(Error::TooShort {
expected: 1,
actual: 0,
context: "PE optional header",
})?
.standard_fields
.address_of_entry_point;
let vb_header_va = entrypoint::extract_vb_header_va(&map, entry_rva).or_else(|_| {
entrypoint::extract_vb_header_va_from_exports(&map, &pe.exports)
.ok_or(Error::NotRecognized)
})?;
let vb_header_data = map
.slice_from_va(vb_header_va, VbHeader::SIZE)
.map_err(|_| Error::TruncatedContainer {
context: "VbHeader",
})?;
let vb_header = VbHeader::parse(vb_header_data).map_err(|_| Error::TruncatedContainer {
context: "VbHeader",
})?;
let pd_va = vb_header
.project_data_va()
.map_err(|_| Error::TruncatedContainer {
context: "VbHeader.project_data_va",
})?;
let pd_data =
map.slice_from_va(pd_va, ProjectData::SIZE)
.map_err(|_| Error::TruncatedContainer {
context: "ProjectData",
})?;
let project_data = ProjectData::parse(pd_data).map_err(|_| Error::TruncatedContainer {
context: "ProjectData",
})?;
let ot_va = project_data
.object_table_va()
.map_err(|_| Error::TruncatedContainer {
context: "ProjectData.object_table_va",
})?;
let ot_data =
map.slice_from_va(ot_va, ObjectTable::SIZE)
.map_err(|_| Error::TruncatedContainer {
context: "ObjectTable",
})?;
let object_table = ObjectTable::parse(ot_data).map_err(|_| Error::TruncatedContainer {
context: "ObjectTable",
})?;
Ok(Self {
map,
vb_header_va,
vb_header,
project_data,
object_table,
})
}
#[inline]
pub fn vb_header_va(&self) -> u32 {
self.vb_header_va
}
#[inline]
pub fn vb_header(&self) -> &VbHeader<'a> {
&self.vb_header
}
#[inline]
pub fn project_data(&self) -> &ProjectData<'a> {
&self.project_data
}
#[inline]
pub fn object_table(&self) -> &ObjectTable<'a> {
&self.object_table
}
#[inline]
pub fn address_map(&self) -> &AddressMap<'a> {
&self.map
}
#[inline]
pub fn va_to_rva(&self, va: u32) -> Option<u64> {
va.checked_sub(self.map.image_base()).map(u64::from)
}
#[inline]
pub fn pcode_method_rva(&self, method: &PCodeMethod<'_>) -> Option<u64> {
self.va_to_rva(method.stub_va())
}
#[inline]
pub fn code_entrypoint_rva(&self, entrypoint: &CodeEntrypoint<'_>) -> Option<u64> {
self.va_to_rva(entrypoint.va)
}
#[inline]
pub fn is_pcode(&self) -> Result<bool, Error> {
self.project_data.is_pcode()
}
pub fn compilation_mode(&self) -> Result<CompilationMode, Error> {
let project_pcode = self.is_pcode()?;
let mut any_pcode = false;
for obj in self.objects()? {
let obj = obj?;
if obj.has_pcode()? {
any_pcode = true;
break;
}
}
Ok(match (project_pcode, any_pcode) {
(true, true) => CompilationMode::Pcode,
(false, false) => CompilationMode::Native,
_ => CompilationMode::Mixed,
})
}
pub fn project_name(&self) -> Result<Cow<'a, str>, Error> {
Ok(String::from_utf8_lossy(self.project_name_bytes()?))
}
pub fn project_name_bytes(&self) -> Result<&'a [u8], Error> {
self.read_string_at_va(self.object_table.project_name_va()?)
}
#[inline]
pub fn object_count(&self) -> Result<u16, Error> {
self.object_table.total_objects()
}
pub fn objects(&self) -> Result<ObjectIterator<'a, '_>, Error> {
Ok(ObjectIterator {
project: self,
index: 0,
total: self.object_table.total_objects()?,
})
}
pub fn externals(&self) -> Result<ExternalIterator<'a, '_>, Error> {
Ok(ExternalIterator {
map: &self.map,
table_va: self.project_data.external_table_va()?,
index: 0,
total: self.project_data.external_count()?,
})
}
pub fn components(&self) -> Result<ExternalComponentIter<'a>, Error> {
let table_va = self.vb_header.external_table_va()?;
let count = self.vb_header.external_count()?;
if table_va == 0 || count == 0 {
return Ok(ExternalComponentIter::new(&[], 0));
}
let max_size = (count as usize).saturating_mul(0x400); let data = self.map.slice_from_va(table_va, max_size).unwrap_or(&[]);
Ok(ExternalComponentIter::new(data, count))
}
pub fn gui_entries(&self) -> Result<GuiTableIter<'_>, Error> {
Ok(GuiTableIter::new(
&self.map,
self.vb_header.gui_table_va()?,
self.vb_header.form_count()?,
))
}
pub fn diagnostics(&self) -> Result<Vec<ParseDiagnostic>, Error> {
let mut out: Vec<ParseDiagnostic> = Vec::new();
let sub_main = self.vb_header.sub_main_va()?;
if sub_main == 0 {
out.push(ParseDiagnostic {
kind: DiagnosticKind::AbsentOptional,
severity: DiagnosticSeverity::Info,
object_index: None,
site: "sub_main",
message: Cow::Borrowed(
"no Sub Main entry point declared (VbHeader.sub_main_va == 0)",
),
});
} else if !self.map.is_va_in_image(sub_main) {
out.push(ParseDiagnostic {
kind: DiagnosticKind::Quirk,
severity: DiagnosticSeverity::Warning,
object_index: None,
site: "sub_main",
message: Cow::Borrowed(
"Sub Main VA is non-zero but does not resolve inside the PE image",
),
});
}
if let Ok(mode) = self.compilation_mode()
&& mode == CompilationMode::Mixed
{
out.push(ParseDiagnostic {
kind: DiagnosticKind::Quirk,
severity: DiagnosticSeverity::Warning,
object_index: None,
site: "compilation_mode",
message: Cow::Borrowed(
"project flag and per-object scan disagree — treat each object's has_pcode() as authoritative",
),
});
}
for (i, obj_result) in self.objects()?.enumerate() {
let obj = obj_result?;
let object_index = u16::try_from(i).ok();
let kind = obj.object_kind()?;
if obj.optional_info().is_none() && kind != "Module" {
out.push(ParseDiagnostic {
kind: DiagnosticKind::AbsentOptional,
severity: DiagnosticSeverity::Warning,
object_index,
site: "OptionalObjectInfo",
message: Cow::Borrowed(
"OptionalObjectInfo missing for non-module object — controls/event sinks unavailable",
),
});
}
if obj.private_object().is_none() && kind == "Class" {
out.push(ParseDiagnostic {
kind: DiagnosticKind::AbsentOptional,
severity: DiagnosticSeverity::Warning,
object_index,
site: "PrivateObjectDescriptor",
message: Cow::Borrowed(
"PrivateObjectDescriptor missing for class object — function type descriptors unavailable",
),
});
}
if !obj.has_method_table()? {
let info = obj.info();
let methods_va = info.methods_va()?;
let constants_va = info.constants_va()?;
if methods_va != 0 && methods_va == constants_va {
out.push(ParseDiagnostic {
kind: DiagnosticKind::Quirk,
severity: DiagnosticSeverity::Info,
object_index,
site: "method_table",
message: Cow::Borrowed(
"methods_va == constants_va — no method dispatch table for this object",
),
});
}
}
}
Ok(out)
}
pub fn code_entrypoints(&self) -> Result<Vec<CodeEntrypoint<'a>>, Error> {
let mut out: Vec<CodeEntrypoint<'a>> = Vec::new();
for (obj_index, obj_result) in self.objects()?.enumerate() {
let obj = obj_result?;
let object_index = u16::try_from(obj_index).ok();
for entry in obj.code_entries(None)? {
out.push(CodeEntrypoint {
va: entry.va,
kind: EntrypointKind::from(entry.kind),
name_hint: entry.name.map(Cow::Owned).unwrap_or(Cow::Borrowed("")),
object_index,
method_index: entry.method_index,
is_pcode: matches!(entry.kind, CodeEntryKind::PCode),
data_const_va: entry.data_const_va,
stub_va: entry.stub_va,
pcode_size: entry.pcode_size,
});
}
}
let sub_main = self.vb_header.sub_main_va()?;
if sub_main != 0 && self.map.is_va_in_image(sub_main) {
out.push(CodeEntrypoint {
va: sub_main,
kind: EntrypointKind::SubMain,
name_hint: Cow::Borrowed("Sub Main"),
object_index: None,
method_index: None,
is_pcode: false,
data_const_va: None,
stub_va: None,
pcode_size: None,
});
}
Ok(out)
}
pub fn gui_entries_with_form_data(&self) -> Result<GuiEntriesWithFormData<'a, '_>, Error> {
Ok(GuiEntriesWithFormData {
inner: self.gui_entries()?,
map: &self.map,
})
}
pub fn read_string_at_va(&self, va: u32) -> Result<&'a [u8], Error> {
if va == 0 {
return Ok(b"");
}
let offset = self.map.va_to_offset(va)?;
read_cstr(self.map.file(), offset)
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct ExternalIterator<'a, 'p> {
map: &'p AddressMap<'a>,
table_va: u32,
index: u32,
total: u32,
}
impl<'a, 'p> Iterator for ExternalIterator<'a, 'p> {
type Item = Result<ExternalTableEntry<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.total || self.table_va == 0 {
return None;
}
let offset = self.index.saturating_mul(ExternalTableEntry::SIZE as u32);
let entry_va = self.table_va.wrapping_add(offset);
self.index = self.index.saturating_add(1);
let data = match self.map.slice_from_va(entry_va, ExternalTableEntry::SIZE) {
Ok(d) => d,
Err(e) => return Some(Err(e)),
};
Some(ExternalTableEntry::parse(data))
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct ObjectIterator<'a, 'p> {
project: &'p VbProject<'a>,
index: u16,
total: u16,
}
impl<'a, 'p: 'a> Iterator for ObjectIterator<'a, 'p> {
type Item = Result<VbObject<'a, 'p>, 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(VbObject::parse(self.project, i))
}
}
pub struct GuiEntryWithFormData<'a> {
pub entry: GuiTableEntry<'a>,
pub form_data: Option<FormDataParser<'a>>,
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct GuiEntriesWithFormData<'a, 'p> {
inner: GuiTableIter<'p>,
map: &'p AddressMap<'a>,
}
impl<'a, 'p: 'a> Iterator for GuiEntriesWithFormData<'a, 'p> {
type Item = Result<GuiEntryWithFormData<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
let entry = self.inner.next()?;
let form_data = match (entry.form_data_va(), entry.form_data_size()) {
(Ok(va), Ok(size)) if va != 0 && size != 0 => {
match self.map.slice_from_va(va, size as usize) {
Ok(data) => FormDataParser::parse(data).ok(),
Err(_) => None,
}
}
_ => None,
};
Some(Ok(GuiEntryWithFormData { entry, form_data }))
}
}