use std::fmt;
use crate::{
VbProject,
util::{read_i16_le, read_u16_le, read_u32_le},
vb::formdata::FormControlType,
vb::guitable::GuiObjectType,
};
const STD_DATA_FORMAT_MAGIC: u32 = 0x6B263850;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataFormatType {
General = 0,
Number = 1,
Currency = 2,
ShortDate = 3,
LongDate = 4,
Custom = 5,
}
impl DataFormatType {
pub fn from_u32(v: u32) -> Option<Self> {
match v {
0 => Some(Self::General),
1 => Some(Self::Number),
2 => Some(Self::Currency),
3 => Some(Self::ShortDate),
4 => Some(Self::LongDate),
5 => Some(Self::Custom),
_ => None,
}
}
}
impl fmt::Display for DataFormatType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::General => write!(f, "General"),
Self::Number => write!(f, "Number"),
Self::Currency => write!(f, "Currency"),
Self::ShortDate => write!(f, "ShortDate"),
Self::LongDate => write!(f, "LongDate"),
Self::Custom => write!(f, "Custom"),
}
}
}
#[derive(Debug, Clone)]
pub struct StdDataFormat {
pub version: u32,
pub format_type: DataFormatType,
pub format: String,
pub has_custom_values: bool,
pub first_day_of_week: Option<u32>,
pub first_week_of_year: Option<u32>,
pub blob_size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropType {
Byte,
Int16,
Long,
Str,
TagStr,
Size16,
Font,
Picture,
LongPair,
DataFormat,
Flag,
}
impl PropType {
pub fn fixed_size(&self) -> Option<usize> {
match self {
Self::Flag => Some(0),
Self::Byte => Some(1),
Self::LongPair => Some(8), Self::Int16 => Some(2),
Self::Long => Some(4),
Self::Size16 => Some(16),
Self::Font => None, Self::DataFormat => None, Self::Str | Self::TagStr | Self::Picture => None,
}
}
}
pub fn property_info(ctype: FormControlType, opcode: u8) -> Option<(&'static str, PropType)> {
generated::lookup_property(ctype.to_u8(), opcode).map(|desc| (desc.name, desc.prop_type))
}
pub fn property_descriptor(
ctype: FormControlType,
opcode: u8,
) -> Option<&'static generated::PropertyDesc> {
generated::lookup_property(ctype.to_u8(), opcode)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ControlPosition {
pub left: u32,
pub top: u32,
}
impl ControlPosition {
pub fn parse(data: &[u8]) -> Option<(Self, usize)> {
if data.len() < 8 {
return None;
}
Some((
Self {
left: read_u32_le(data, 0).ok()?,
top: read_u32_le(data, 4).ok()?,
},
8,
))
}
}
impl fmt::Display for ControlPosition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{},{}", self.left, self.top)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientRect {
pub left: u32,
pub top: u32,
pub width: u32,
pub height: u32,
}
impl ClientRect {
pub fn parse(data: &[u8]) -> Option<(Self, usize)> {
if data.len() < 16 {
return None;
}
Some((
Self {
left: read_u32_le(data, 0).ok()?,
top: read_u32_le(data, 4).ok()?,
width: read_u32_le(data, 8).ok()?,
height: read_u32_le(data, 12).ok()?,
},
16,
))
}
}
impl fmt::Display for ClientRect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{},{},{},{}",
self.left, self.top, self.width, self.height
)
}
}
#[derive(Debug, Clone)]
pub struct FontDescriptor {
pub size_pt: u32,
pub bold: bool,
pub weight: u16,
pub name: String,
}
impl FontDescriptor {
pub fn parse(data: &[u8]) -> Option<(Self, usize)> {
if data.len() < 11 {
return None;
}
let weight = read_u16_le(data, 4).ok()?;
let raw_size = read_u32_le(data, 6).ok()?;
let name_len = (*data.get(10)?) as usize;
let mut consumed: usize = 11;
let name = if name_len > 0 {
let end = consumed.checked_add(name_len)?;
if end <= data.len() {
let s = String::from_utf8_lossy(data.get(consumed..end)?).into_owned();
consumed = end;
s
} else {
String::new()
}
} else {
String::new()
};
Some((
Self {
size_pt: raw_size.checked_div(10000).unwrap_or(0),
bold: weight >= 700,
weight,
name,
},
consumed,
))
}
}
impl fmt::Display for FontDescriptor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b = if self.bold { " Bold" } else { "" };
write!(f, "({}pt{b}, \"{}\")", self.size_pt, self.name)
}
}
#[derive(Debug, Clone)]
pub struct PictureData {
pub size: u32,
pub is_bmp: bool,
pub is_default: bool,
pub bytes: Vec<u8>,
}
impl PictureData {
pub fn parse(data: &[u8]) -> Option<(Self, usize)> {
if data.len() < 4 {
return None;
}
let size = read_u32_le(data, 0).ok()?;
if size == 0xFFFFFFFF {
return Some((
Self {
size: 0,
is_bmp: false,
is_default: true,
bytes: Vec::new(),
},
4,
));
}
let total = size as usize;
let consumed = 4usize.checked_add(total)?;
if consumed > data.len() {
return None;
}
let bmp_off: usize = 12;
let is_bmp =
data.get(bmp_off) == Some(&b'B') && data.get(bmp_off.checked_add(1)?) == Some(&b'M');
Some((
Self {
size,
is_bmp,
is_default: false,
bytes: data.get(4..consumed)?.to_vec(),
},
consumed,
))
}
pub fn bytes(&self) -> Option<&[u8]> {
if self.is_default {
None
} else {
Some(&self.bytes)
}
}
}
impl fmt::Display for PictureData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_default {
write!(f, "default")
} else if self.is_bmp {
write!(f, "(BMP, {}B)", self.size)
} else {
write!(f, "({}B)", self.size)
}
}
}
impl StdDataFormat {
pub fn parse(data: &[u8]) -> Option<(Self, usize)> {
if data.len() < 0x28 {
return None;
}
if read_u32_le(data, 0).ok()? != STD_DATA_FORMAT_MAGIC {
return None;
}
let version = read_u32_le(data, 4).ok()?;
let format_type = read_u32_le(data, 8).ok()?;
let fmt_str_len = read_u32_le(data, 0x14).ok()? as usize;
let has_custom = read_u32_le(data, 0x18).ok()?;
let true_val_len = read_u32_le(data, 0x1C).ok()? as usize;
let false_val_len = read_u32_le(data, 0x20).ok()? as usize;
let null_val_len = read_u32_le(data, 0x24).ok()? as usize;
let mut off: usize = 0x28;
let fmt_byte_len = fmt_str_len.checked_mul(2)?;
let fmt_end = off.checked_add(fmt_byte_len)?;
let format = if fmt_str_len > 0 && fmt_end <= data.len() {
let utf16: Vec<u16> = (0..fmt_str_len)
.map(|j| {
let pos = off.checked_add(j.checked_mul(2)?)?;
read_u16_le(data, pos).ok()
})
.collect::<Option<Vec<_>>>()?;
off = fmt_end;
String::from_utf16_lossy(&utf16)
} else {
off = fmt_end;
String::new()
};
if has_custom != 0 {
off = off
.checked_add(0x10)?
.checked_add(true_val_len.checked_mul(2)?)?;
off = off
.checked_add(0x10)?
.checked_add(false_val_len.checked_mul(2)?)?;
off = off
.checked_add(0x10)?
.checked_add(null_val_len.checked_mul(2)?)?;
}
let first_day_of_week =
if version >= 0x60001 && off.checked_add(4).map(|e| e <= data.len()).unwrap_or(false) {
let v = read_u32_le(data, off).ok()?;
off = off.checked_add(4)?;
Some(v)
} else {
None
};
let first_week_of_year =
if version >= 0x60002 && off.checked_add(4).map(|e| e <= data.len()).unwrap_or(false) {
let v = read_u32_le(data, off).ok()?;
off = off.checked_add(4)?;
Some(v)
} else {
None
};
Some((
Self {
version,
format_type: DataFormatType::from_u32(format_type)
.unwrap_or(DataFormatType::General),
format,
has_custom_values: has_custom != 0,
first_day_of_week,
first_week_of_year,
blob_size: off as u32,
},
off,
))
}
}
impl fmt::Display for StdDataFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.format.is_empty() {
write!(f, "({}, {}B)", self.format_type, self.blob_size)
} else {
write!(
f,
"({}, \"{}\", {}B)",
self.format_type, self.format, self.blob_size
)
}
}
}
fn parse_ascii_str(data: &[u8]) -> Option<(String, usize)> {
if data.len() < 2 {
return None;
}
let len = read_u16_le(data, 0).ok()? as usize;
let end = 2usize.checked_add(len)?;
if end >= data.len() {
return None;
}
let s = String::from_utf8_lossy(data.get(2..end)?).into_owned();
Some((s, end.checked_add(1)?)) }
fn parse_utf16_str(data: &[u8]) -> Option<(String, usize)> {
if data.len() < 2 {
return None;
}
let char_count = read_u16_le(data, 0).ok()? as usize;
let byte_len = char_count.checked_mul(2)?;
let total = 2usize.checked_add(byte_len)?;
if total > data.len() {
return None;
}
let utf16: Vec<u16> = (0..char_count)
.map(|j| {
let pos = 2usize.checked_add(j.checked_mul(2)?)?;
read_u16_le(data, pos).ok()
})
.collect::<Option<Vec<_>>>()?;
let s = String::from_utf16_lossy(&utf16);
Some((s, total))
}
#[derive(Debug, Clone)]
pub enum PropertyValue {
Flag,
Byte(u8),
Int16(i16),
Long(u32),
Color(u32),
Str(String),
TagStr(String),
Position(ControlPosition),
ClientRect(ClientRect),
Font(FontDescriptor),
Picture(PictureData),
DataFormat(StdDataFormat),
}
impl PropertyValue {
pub fn as_str(&self) -> &'static str {
match self {
Self::Flag => "Flag",
Self::Byte(_) => "Byte",
Self::Int16(_) => "Int16",
Self::Long(_) => "Long",
Self::Color(_) => "Color",
Self::Str(_) => "Str",
Self::TagStr(_) => "TagStr",
Self::Position(_) => "Position",
Self::ClientRect(_) => "ClientRect",
Self::Font(_) => "Font",
Self::Picture(_) => "Picture",
Self::DataFormat(_) => "DataFormat",
}
}
pub fn picture_bytes(&self) -> Option<&[u8]> {
match self {
Self::Picture(pic) => pic.bytes(),
_ => None,
}
}
pub fn display_truncated(&self, limit: usize) -> String {
let rendered = self.to_string();
if rendered.chars().count() <= limit {
return rendered;
}
rendered.chars().take(limit).collect()
}
}
impl fmt::Display for PropertyValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Flag => Ok(()),
Self::Byte(v) => write!(f, "{v}"),
Self::Int16(v) => write!(f, "{v}"),
Self::Long(v) => write!(f, "{v}"),
Self::Color(v) => write!(f, "#{v:06X}"),
Self::Str(s) | Self::TagStr(s) => {
write!(f, "\"")?;
for ch in s.chars() {
if ch.is_control() {
write!(f, "\\x{:02X}", ch as u32)?;
} else {
write!(f, "{ch}")?;
}
}
write!(f, "\"")
}
Self::Position(p) => write!(f, "{p}"),
Self::ClientRect(r) => write!(f, "{r}"),
Self::Font(font) => write!(f, "{font}"),
Self::Picture(pic) => write!(f, "{pic}"),
Self::DataFormat(df) => write!(f, "{df}"),
}
}
}
#[derive(Debug, Clone)]
pub struct Property {
pub name: &'static str,
pub value: PropertyValue,
pub offset: usize,
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct PropertyIter<'a> {
data: &'a [u8],
pos: usize,
ctype: FormControlType,
}
impl<'a> PropertyIter<'a> {
pub fn new(data: &'a [u8], ctype: FormControlType) -> Self {
Self {
data,
pos: 0,
ctype,
}
}
fn decode_value(&mut self, ser_type: u8, callback_bytes: i8) -> Option<PropertyValue> {
let d = self.data;
let p = self.pos;
let rest = d.get(p..)?;
match ser_type {
0 => Some(PropertyValue::Flag),
1 | 18 | 26 | 33 => {
let (s, consumed) = parse_ascii_str(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::Str(s))
}
2 | 17 => {
let v = read_i16_le(d, p).ok()?;
self.pos = self.pos.checked_add(2)?;
Some(PropertyValue::Int16(v))
}
3 => {
let v = read_u32_le(d, p).ok()?;
self.pos = self.pos.checked_add(4)?;
Some(PropertyValue::Long(v))
}
4 => {
let v = *rest.first()?;
self.pos = self.pos.checked_add(1)?;
Some(PropertyValue::Byte(v))
}
5 => {
let v = read_u32_le(d, p).ok()?;
self.pos = self.pos.checked_add(4)?;
Some(PropertyValue::Color(v))
}
6 => {
let v = *rest.first()?;
self.pos = self.pos.checked_add(1)?;
if callback_bytes > 0 {
let skip = callback_bytes as usize;
let new_pos = self.pos.checked_add(skip)?;
if new_pos <= d.len() {
self.pos = new_pos;
}
}
Some(PropertyValue::Byte(v))
}
7 | 10 | 11 => {
let v = read_u32_le(d, p).ok()?;
self.pos = self.pos.checked_add(4)?;
Some(PropertyValue::Long(v))
}
8 | 9 => match callback_bytes {
4 => {
let (pos, consumed) = ControlPosition::parse(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::Position(pos))
}
12 => {
let (rect, consumed) = ClientRect::parse(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::ClientRect(rect))
}
_ => {
let v = read_u32_le(d, p).ok()?;
self.pos = self.pos.checked_add(4)?;
Some(PropertyValue::Long(v))
}
},
13 => {
let (s, consumed) = parse_utf16_str(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::TagStr(s))
}
20 => {
let (font, consumed) = FontDescriptor::parse(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::Font(font))
}
21 => {
let (pic, consumed) = PictureData::parse(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::Picture(pic))
}
22 => {
let (df, consumed) = StdDataFormat::parse(rest)?;
self.pos = self.pos.checked_add(consumed)?;
Some(PropertyValue::DataFormat(df))
}
_ => Some(PropertyValue::Flag),
}
}
}
impl<'a> Iterator for PropertyIter<'a> {
type Item = Property;
fn next(&mut self) -> Option<Property> {
let opcode = *self.data.get(self.pos)?;
if opcode == 0xFF {
return None;
}
let opcode_offset = self.pos;
if let Some(desc) = property_descriptor(self.ctype, opcode) {
self.pos = self.pos.checked_add(1)?; let value_offset = self.pos;
if desc.prop_type == PropType::Flag {
return Some(Property {
name: desc.name,
value: PropertyValue::Flag,
offset: opcode_offset,
});
}
let value = self.decode_value(desc.ser_type, desc.callback_bytes)?;
return Some(Property {
name: desc.name,
value,
offset: value_offset,
});
}
self.pos = self.pos.checked_add(1)?;
if let Some(&next) = self.data.get(self.pos)
&& (next == 0xFF || property_info(self.ctype, next).is_some())
{
let unknown_name: &'static str = Box::leak(format!("?0x{opcode:02X}").into_boxed_str());
return Some(Property {
name: unknown_name,
value: PropertyValue::Flag,
offset: opcode_offset,
});
}
None
}
}
pub fn decode_form_type(
gui_type: GuiObjectType,
form_props: &[u8],
project: &VbProject<'_>,
) -> FormControlType {
match gui_type {
GuiObjectType::PropertyPage => FormControlType::PropertyPage,
GuiObjectType::UserControl => {
if form_props.len() > 3 && form_props.first() == Some(&0x00) {
let Ok(nlen) = read_u16_le(form_props, 1) else {
return FormControlType::UserControl;
};
let nlen = nlen as usize;
let Some(name_end) = 3usize.checked_add(nlen) else {
return FormControlType::UserControl;
};
if let Some(form_name) = form_props.get(3..name_end) {
let Ok(objects) = project.objects() else {
return FormControlType::UserControl;
};
for other_obj in objects {
if let Ok(other_obj) = other_obj
&& let Ok(n) = other_obj.name_bytes()
&& n == form_name
{
let Ok(otype) = other_obj.descriptor().object_type_raw() else {
break;
};
if otype & 0x02 != 0 && otype & 0x20 == 0 {
return FormControlType::PropertyPage;
}
break;
}
}
}
}
FormControlType::UserControl
}
_ => FormControlType::Form,
}
}
pub(crate) mod generated {
include!(concat!(env!("OUT_DIR"), "/property_generated.rs"));
}