use crate::{
error::Error,
util::read_u16_le,
vb::{control::Guid, controlprop::ControlPropertyIter, external::VbBaseType},
};
#[derive(Clone, Copy, Debug)]
pub struct PublicVarTable<'a> {
bytes: &'a [u8],
var_count: u16,
}
impl<'a> PublicVarTable<'a> {
pub const HEADER_SIZE: usize = 12;
pub const ENTRY_SIZE: usize = 4;
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
if data.len() < Self::HEADER_SIZE {
return Err(Error::TooShort {
expected: Self::HEADER_SIZE,
actual: data.len(),
context: "PublicVarTable header",
});
}
let var_count = read_u16_le(data, 0x06)?;
let available_entries = (data.len().saturating_sub(Self::HEADER_SIZE)) / Self::ENTRY_SIZE;
let effective_count = var_count.min(available_entries as u16);
Ok(Self {
bytes: data,
var_count: effective_count,
})
}
#[inline]
pub fn total_size(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x00)
}
#[inline]
pub fn extra_count(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x04)
}
#[inline]
pub fn data_frame_size(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x02)
}
#[inline]
pub fn var_count(&self) -> u16 {
self.var_count
}
pub fn var(&self, index: u16) -> Option<PublicVarEntry> {
if index >= self.var_count {
return None;
}
let offset =
Self::HEADER_SIZE.checked_add((index as usize).checked_mul(Self::ENTRY_SIZE)?)?;
let end = offset.checked_add(Self::ENTRY_SIZE)?;
if end > self.bytes.len() {
return None;
}
Some(PublicVarEntry {
frame_offset: read_u16_le(self.bytes, offset).ok()?,
type_code: read_u16_le(self.bytes, offset.checked_add(2)?).ok()?,
})
}
pub fn vars(&self) -> PublicVarIter<'_> {
PublicVarIter {
table: self,
index: 0,
}
}
pub fn valid_vars(&self) -> impl Iterator<Item = PublicVarEntry> + '_ {
self.vars().filter(|e| e.is_valid())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicVarEntry {
pub frame_offset: u16,
pub type_code: u16,
}
impl PublicVarEntry {
pub fn base_type(&self) -> VbBaseType {
match self.type_code & 0xFF {
0x01 | 0x0C => VbBaseType::Variant,
0x09 => VbBaseType::Object,
other => VbBaseType::from_raw(other as u8),
}
}
pub fn type_name(&self) -> &'static str {
self.base_type().name()
}
#[inline]
pub fn type_flags(&self) -> u8 {
(self.type_code >> 8) as u8
}
pub fn is_valid(&self) -> bool {
if self.frame_offset == 0 && self.type_code == 0 {
return false;
}
let base = self.type_code & 0xFF;
let known_type = matches!(
base,
0x01 | 0x02
| 0x03
| 0x04
| 0x05
| 0x06
| 0x07
| 0x08
| 0x09
| 0x0B
| 0x0C
| 0x0D
| 0x11
);
known_type && self.frame_offset < 0x1000
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct PublicVarIter<'a> {
table: &'a PublicVarTable<'a>,
index: u16,
}
impl Iterator for PublicVarIter<'_> {
type Item = PublicVarEntry;
fn next(&mut self) -> Option<Self::Item> {
let entry = self.table.var(self.index)?;
self.index = self.index.saturating_add(1);
Some(entry)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ClassFormPublicBytes<'a> {
bytes: &'a [u8],
}
impl<'a> ClassFormPublicBytes<'a> {
pub const MIN_SIZE: usize = 0x0C;
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
if data.len() < Self::MIN_SIZE {
return Err(Error::TooShort {
expected: Self::MIN_SIZE,
actual: data.len(),
context: "ClassFormPublicBytes",
});
}
Ok(Self { bytes: data })
}
#[inline]
pub fn data_size(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x00)
}
#[inline]
pub fn instance_size(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x02)
}
#[inline]
pub fn property_count(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x04)
}
#[inline]
pub fn control_count(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x06)
}
#[inline]
pub fn has_controls(&self) -> bool {
self.control_count().is_ok_and(|c| c > 0)
}
pub fn entry_data(&self) -> &'a [u8] {
self.bytes.get(0x0C..).unwrap_or(&[])
}
pub fn default_iid(&self) -> Option<Guid> {
if self.has_controls() {
return None;
}
Guid::from_bytes(self.bytes.get(0x0C..0x1C)?)
}
pub fn events_iid(&self) -> Option<Guid> {
if self.has_controls() {
return None;
}
Guid::from_bytes(self.bytes.get(0x1C..0x2C)?)
}
pub fn control_entries(&self) -> ControlPropertyIter<'a> {
ControlPropertyIter::new(self.entry_data(), self.control_count().unwrap_or(0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vb::controlprop::ControlPropertyType;
const MOD_VARIAVEIS: [u8; 78] = [
0x4E, 0x00, 0x48, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x04, 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0x00, 0x0C, 0x00, 0x01, 0x00, 0x10, 0x00,
0x01, 0x00, 0x14, 0x00, 0x01, 0x00, 0x18, 0x00, 0x01, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x20,
0x00, 0x01, 0x00, 0x24, 0x00, 0x01, 0x00, 0x28, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x01, 0x00,
0x30, 0x00, 0x01, 0x00, 0x3C, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
0x00, 0x01, 0x00,
];
const MOD_UTIL: [u8; 16] = [
0x10, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
0x00,
];
#[test]
fn test_parse_mod_variaveis() {
let table = PublicVarTable::parse(&MOD_VARIAVEIS).unwrap();
assert_eq!(table.total_size().unwrap(), 0x4E);
assert_eq!(table.var_count(), 15);
let v0 = table.var(0).unwrap();
assert_eq!(v0.frame_offset, 0x0000);
assert_eq!(v0.type_code, 0x0001);
assert_eq!(v0.type_name(), "Variant");
let v1 = table.var(1).unwrap();
assert_eq!(v1.frame_offset, 0x0004);
assert_eq!(v1.type_code, 0x0001);
assert_eq!(table.vars().count(), 15);
}
#[test]
fn test_parse_mod_util() {
let table = PublicVarTable::parse(&MOD_UTIL).unwrap();
assert_eq!(table.total_size().unwrap(), 0x10);
assert_eq!(table.var_count(), 1);
let v0 = table.var(0).unwrap();
assert_eq!(v0.frame_offset, 0x0000);
assert_eq!(v0.type_code, 0x0003);
assert_eq!(v0.type_name(), "Long");
assert_eq!(v0.type_flags(), 0);
}
#[test]
fn test_var_out_of_range() {
let table = PublicVarTable::parse(&MOD_UTIL).unwrap();
assert!(table.var(1).is_none());
}
#[test]
fn test_parse_too_short() {
assert!(PublicVarTable::parse(&[0; 7]).is_err());
}
#[test]
fn test_type_names() {
let entry = PublicVarEntry {
frame_offset: 0,
type_code: 0x0003,
};
assert_eq!(entry.type_name(), "Long");
let entry = PublicVarEntry {
frame_offset: 0,
type_code: 0x0105,
};
assert_eq!(entry.type_name(), "Double");
assert_eq!(entry.type_flags(), 0x01);
}
const FORM1_PUBLIC_BYTES: [u8; 0x40] = [
0x0C, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x3D, 0xFB, 0xFC, 0xFA, 0xA0, 0x68, 0x10, 0xA7, 0x38, 0x08, 0x00, 0x2B, 0x33, 0x71, 0xB5, 0x22, 0x3D, 0xFB, 0xFC, 0xFA, 0xA0, 0x68,
0x10, 0xA7, 0x38, 0x08, 0x00, 0x2B, 0x33, 0x71, 0xB5, 0x02, 0x00, 0x00,
0x00, 0x68, 0x2F, 0x40, 0x00, 0x78, 0x2F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0x4F, 0xAD, 0x33, ];
const CLS_CRC32_PUBLIC_BYTES: [u8; 0x18] = [
0x38, 0x00, 0x60, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x05, 0x00, 0x5C, 0x00, 0x55, 0x00, 0x00, 0x00, 0x65, 0x00, ];
#[test]
fn test_form_no_controls() {
let cfpb = ClassFormPublicBytes::parse(&FORM1_PUBLIC_BYTES).unwrap();
assert_eq!(cfpb.data_size().unwrap(), 0x0C);
assert_eq!(cfpb.instance_size().unwrap(), 0x44);
assert_eq!(cfpb.property_count().unwrap(), 0);
assert_eq!(cfpb.control_count().unwrap(), 0);
assert!(!cfpb.has_controls());
assert!(cfpb.default_iid().is_some());
assert!(cfpb.events_iid().is_some());
assert_ne!(cfpb.default_iid(), cfpb.events_iid());
}
#[test]
fn test_class_with_controls() {
let cfpb = ClassFormPublicBytes::parse(&CLS_CRC32_PUBLIC_BYTES).unwrap();
assert_eq!(cfpb.data_size().unwrap(), 0x38);
assert_eq!(cfpb.instance_size().unwrap(), 0x60);
assert_eq!(cfpb.property_count().unwrap(), 1);
assert_eq!(cfpb.control_count().unwrap(), 1);
assert!(cfpb.has_controls());
assert!(cfpb.default_iid().is_none());
assert!(cfpb.events_iid().is_none());
assert!(!cfpb.entry_data().is_empty());
}
#[test]
fn test_class_control_entries() {
let cfpb = ClassFormPublicBytes::parse(&CLS_CRC32_PUBLIC_BYTES).unwrap();
let entries: Vec<_> = cfpb.control_entries().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].frame_offset().unwrap(), 0x38);
assert_eq!(entries[0].property_type(), ControlPropertyType::SafeArray);
assert_eq!(entries[0].flags(), 0x00);
}
#[test]
fn test_form_no_control_entries() {
let cfpb = ClassFormPublicBytes::parse(&FORM1_PUBLIC_BYTES).unwrap();
assert_eq!(cfpb.control_entries().count(), 0);
}
#[test]
fn test_class_form_too_short() {
assert!(ClassFormPublicBytes::parse(&[0; 0x0B]).is_err());
}
}