use std::{borrow::Cow, fmt};
use crate::{
error::Error,
util::{read_u16_le, read_u32_le},
vb::control::Guid,
vb::property::PropertyIter,
};
pub const FORM_DATA_MAGIC: u16 = 0xCCFF;
pub const FORM_DATA_VERSION: u16 = 0x0031;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormControlType {
PictureBox,
Label,
TextBox,
Frame,
CommandButton,
CheckBox,
OptionButton,
ComboBox,
ListBox,
HScrollBar,
VScrollBar,
Timer,
Form,
DriveListBox,
DirListBox,
FileListBox,
Menu,
MDIForm,
Shape,
Line,
Image,
Data,
OLE,
UserControl,
PropertyPage,
UserDocument,
Unknown(u8),
}
impl FormControlType {
pub fn from_u8(v: u8) -> Self {
match v {
0 => Self::PictureBox,
1 => Self::Label,
2 => Self::TextBox,
3 => Self::Frame,
4 => Self::CommandButton,
5 => Self::CheckBox,
6 => Self::OptionButton,
7 => Self::ComboBox,
8 => Self::ListBox,
9 => Self::HScrollBar,
10 => Self::VScrollBar,
11 => Self::Timer,
13 => Self::Form,
16 => Self::DriveListBox,
17 => Self::DirListBox,
18 => Self::FileListBox,
19 => Self::Menu,
20 => Self::MDIForm,
22 => Self::Shape,
23 => Self::Line,
24 => Self::Image,
37 => Self::Data,
38 => Self::OLE,
40 => Self::UserControl,
41 => Self::PropertyPage,
42 => Self::UserDocument,
n => Self::Unknown(n),
}
}
pub fn to_u8(&self) -> u8 {
match self {
Self::PictureBox => 0,
Self::Label => 1,
Self::TextBox => 2,
Self::Frame => 3,
Self::CommandButton => 4,
Self::CheckBox => 5,
Self::OptionButton => 6,
Self::ComboBox => 7,
Self::ListBox => 8,
Self::HScrollBar => 9,
Self::VScrollBar => 10,
Self::Timer => 11,
Self::Form => 13,
Self::DriveListBox => 16,
Self::DirListBox => 17,
Self::FileListBox => 18,
Self::Menu => 19,
Self::MDIForm => 20,
Self::Shape => 22,
Self::Line => 23,
Self::Image => 24,
Self::Data => 37,
Self::OLE => 38,
Self::UserControl => 40,
Self::PropertyPage => 41,
Self::UserDocument => 42,
Self::Unknown(n) => *n,
}
}
pub fn from_class_name(name: &str) -> Option<Self> {
match name {
"PictureBox" => Some(Self::PictureBox),
"Label" => Some(Self::Label),
"TextBox" => Some(Self::TextBox),
"Frame" => Some(Self::Frame),
"CommandButton" => Some(Self::CommandButton),
"CheckBox" => Some(Self::CheckBox),
"OptionButton" => Some(Self::OptionButton),
"ComboBox" => Some(Self::ComboBox),
"ListBox" => Some(Self::ListBox),
"HScrollBar" => Some(Self::HScrollBar),
"VScrollBar" => Some(Self::VScrollBar),
"Timer" => Some(Self::Timer),
"Form" => Some(Self::Form),
"DriveListBox" => Some(Self::DriveListBox),
"DirListBox" => Some(Self::DirListBox),
"FileListBox" => Some(Self::FileListBox),
"Menu" => Some(Self::Menu),
"MDIForm" => Some(Self::MDIForm),
"Shape" => Some(Self::Shape),
"Line" => Some(Self::Line),
"Image" => Some(Self::Image),
"Data" => Some(Self::Data),
"OLE" => Some(Self::OLE),
"UserControl" => Some(Self::UserControl),
"PropertyPage" => Some(Self::PropertyPage),
"UserDocument" => Some(Self::UserDocument),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::PictureBox => "PictureBox",
Self::Label => "Label",
Self::TextBox => "TextBox",
Self::Frame => "Frame",
Self::CommandButton => "CommandButton",
Self::CheckBox => "CheckBox",
Self::OptionButton => "OptionButton",
Self::ComboBox => "ComboBox",
Self::ListBox => "ListBox",
Self::HScrollBar => "HScrollBar",
Self::VScrollBar => "VScrollBar",
Self::Timer => "Timer",
Self::Form => "Form",
Self::DriveListBox => "DriveListBox",
Self::DirListBox => "DirListBox",
Self::FileListBox => "FileListBox",
Self::Menu => "Menu",
Self::MDIForm => "MDIForm",
Self::Shape => "Shape",
Self::Line => "Line",
Self::Image => "Image",
Self::Data => "Data",
Self::OLE => "OLE",
Self::UserControl => "UserControl",
Self::PropertyPage => "PropertyPage",
Self::UserDocument => "UserDocument",
Self::Unknown(_) => "Unknown",
}
}
pub fn as_str(&self) -> &'static str {
self.name()
}
pub fn is_container(&self) -> bool {
matches!(self, Self::Frame | Self::PictureBox)
}
}
impl fmt::Display for FormControlType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unknown(n) => write!(f, "Unknown({n})"),
_ => write!(f, "{}", self.name()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormMarker {
NewChild,
EndChildren,
Sibling,
FormEnd,
MenuStart,
}
impl FormMarker {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0x01 => Some(Self::NewChild),
0x02 => Some(Self::EndChildren),
0x03 => Some(Self::Sibling),
0x04 => Some(Self::FormEnd),
0x05 => Some(Self::MenuStart),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FormDataHeader<'a> {
bytes: &'a [u8],
}
impl<'a> FormDataHeader<'a> {
pub const MIN_SIZE: usize = 0x61;
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
let bytes = data.get(..Self::MIN_SIZE).ok_or(Error::TooShort {
expected: Self::MIN_SIZE,
actual: data.len(),
context: "FormDataHeader",
})?;
let magic = read_u16_le(bytes, 0x00)?;
if magic != FORM_DATA_MAGIC {
let got: [u8; 4] = bytes
.get(..4)
.and_then(|s| <[u8; 4]>::try_from(s).ok())
.unwrap_or([0; 4]);
return Err(Error::BadMagic {
expected: "CCFF (form data)",
got,
});
}
Ok(Self { bytes })
}
#[inline]
pub fn magic(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x00)
}
#[inline]
pub fn version(&self) -> Result<u16, Error> {
read_u16_le(self.bytes, 0x02)
}
#[inline]
pub fn site_flags(&self) -> u8 {
self.bytes.get(0x04).copied().unwrap_or(0)
}
pub fn form_guid(&self) -> Option<Guid> {
Guid::from_bytes(self.bytes.get(0x05..0x15)?)
}
pub fn secondary_guid(&self) -> Option<Guid> {
let data = self.bytes.get(0x15..0x25)?;
if data.iter().all(|&b| b == 0) {
return None;
}
Guid::from_bytes(data)
}
pub fn default_control_guid(&self) -> Option<Guid> {
Guid::from_bytes(self.bytes.get(0x25..0x35)?)
}
#[inline]
pub fn width(&self) -> Result<u32, Error> {
read_u32_le(self.bytes, 0x59)
}
#[inline]
pub fn height(&self) -> Result<u32, Error> {
read_u32_le(self.bytes, 0x5D)
}
}
#[derive(Clone, Debug)]
pub struct FormControlRecord<'a> {
cid: u8,
array_index: Option<u16>,
name: &'a [u8],
ctype: FormControlType,
properties: &'a [u8],
total_size: u32,
depth: u16,
offset_in_blob: u32,
properties_offset_in_blob: u32,
parent_index: Option<usize>,
}
impl<'a> FormControlRecord<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
if data.len() < 8 {
return Err(Error::TooShort {
expected: 8,
actual: data.len(),
context: "FormControlRecord",
});
}
let raw_size = read_u32_le(data, 0)?;
let has_array_index = raw_size & 0x80000000 != 0;
let total_size = raw_size & 0x7FFFFFFF;
let record = data.get(..total_size as usize).ok_or(Error::TooShort {
expected: total_size as usize,
actual: data.len(),
context: "FormControlRecord size",
})?;
if total_size < 8 {
return Err(Error::TooShort {
expected: 8,
actual: total_size as usize,
context: "FormControlRecord size",
});
}
let mut pos: usize = 4;
let cid = *record.get(pos).ok_or(Error::TooShort {
expected: pos.saturating_add(1),
actual: record.len(),
context: "FormControlRecord cId",
})?;
pos = pos.saturating_add(1);
let array_index = if has_array_index {
let idx = read_u16_le(record, pos)?;
pos = pos.saturating_add(2);
Some(idx)
} else {
None
};
let name_len = read_u16_le(record, pos)? as usize;
pos = pos.saturating_add(2);
let name_end = pos.checked_add(name_len).ok_or(Error::ArithmeticOverflow {
context: "FormControlRecord name end",
})?;
let name = record.get(pos..name_end).ok_or(Error::TooShort {
expected: name_end.saturating_add(1),
actual: record.len(),
context: "FormControlRecord name",
})?;
pos = name_end.checked_add(1).ok_or(Error::ArithmeticOverflow {
context: "FormControlRecord name terminator",
})?;
let ctype_byte = *record.get(pos).ok_or(Error::TooShort {
expected: pos.saturating_add(1),
actual: record.len(),
context: "FormControlRecord cType",
})?;
let ctype = FormControlType::from_u8(ctype_byte);
pos = pos.saturating_add(1);
let tail = record.get(pos..).unwrap_or(&[]);
let props_end = if let Some(ff_pos) = tail.iter().rposition(|&b| b == 0xFF) {
pos.saturating_add(ff_pos)
} else {
record.len()
};
let properties = record.get(pos..props_end).unwrap_or(&[]);
let properties_offset_local = pos as u32;
Ok(Self {
cid,
array_index,
name,
ctype,
properties,
total_size,
depth: 0,
offset_in_blob: 0,
properties_offset_in_blob: properties_offset_local,
parent_index: None,
})
}
#[inline]
pub fn cid(&self) -> u8 {
self.cid
}
#[inline]
pub fn array_index(&self) -> Option<u16> {
self.array_index
}
#[inline]
pub fn name(&self) -> Cow<'a, str> {
String::from_utf8_lossy(self.name)
}
#[inline]
pub fn name_bytes(&self) -> &'a [u8] {
self.name
}
#[inline]
pub fn control_type(&self) -> FormControlType {
self.ctype
}
#[inline]
pub fn raw_properties(&self) -> &'a [u8] {
self.properties
}
#[inline]
pub fn total_size(&self) -> u32 {
self.total_size
}
#[inline]
pub fn depth(&self) -> u16 {
self.depth
}
#[inline]
pub fn offset_in_blob(&self) -> u32 {
self.offset_in_blob
}
#[inline]
pub fn properties_offset_in_blob(&self) -> u32 {
self.properties_offset_in_blob
}
#[inline]
pub fn parent_index(&self) -> Option<usize> {
self.parent_index
}
pub fn properties(&self) -> PropertyIter<'a> {
PropertyIter::new(self.properties, self.ctype)
}
}
pub struct FormDataParser<'a> {
header: FormDataHeader<'a>,
controls: Vec<FormControlRecord<'a>>,
data: &'a [u8],
form_properties: &'a [u8],
}
impl<'a> FormDataParser<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
let header = FormDataHeader::parse(data)?;
let form_properties = Self::extract_form_properties(data);
let controls = Self::parse_controls(data)?;
Ok(Self {
header,
controls,
data,
form_properties,
})
}
#[inline]
pub fn header(&self) -> &FormDataHeader<'a> {
&self.header
}
#[inline]
pub fn controls(&self) -> &[FormControlRecord<'a>] {
&self.controls
}
#[inline]
pub fn raw_data(&self) -> &'a [u8] {
self.data
}
#[inline]
pub fn form_properties(&self) -> &'a [u8] {
self.form_properties
}
pub fn form_properties_decoded(&self, form_type: FormControlType) -> PropertyIter<'a> {
PropertyIter::new(self.form_properties, form_type)
}
pub fn control_by_id(&self, cid: u8) -> Option<&FormControlRecord<'a>> {
self.controls.iter().find(|c| c.cid() == cid)
}
fn extract_form_properties(data: &'a [u8]) -> &'a [u8] {
let start = FormDataHeader::MIN_SIZE;
if start >= data.len() {
return &[];
}
let mut pos = start;
while pos.saturating_add(1) < data.len() {
let cur = match data.get(pos).copied() {
Some(b) => b,
None => break,
};
let next = match data.get(pos.saturating_add(1)).copied() {
Some(b) => b,
None => break,
};
if cur == 0xFF && FormMarker::from_byte(next).is_some() {
match next {
0x01 | 0x03 | 0x05 if pos.saturating_add(6) < data.len() => {
let size = read_u32_le(data, pos.saturating_add(2))
.map(|v| v & 0x7FFFFFFF)
.unwrap_or(0);
if (8..5000).contains(&size) {
return data.get(start..pos).unwrap_or(&[]);
}
}
0x04 | 0x02 => return data.get(start..pos).unwrap_or(&[]),
_ => {}
}
}
pos = pos.saturating_add(1);
}
let end = data.len().min(start.saturating_add(256));
data.get(start..end).unwrap_or(&[])
}
fn parse_controls(data: &'a [u8]) -> Result<Vec<FormControlRecord<'a>>, Error> {
let mut controls = Vec::new();
let mut pos = FormDataHeader::MIN_SIZE;
let start = loop {
if pos.saturating_add(6) >= data.len() {
return Ok(controls); }
let cur = data.get(pos).copied().unwrap_or(0);
let next = data.get(pos.saturating_add(1)).copied().unwrap_or(0);
if cur == 0xFF && next == 0x01 {
let size = read_u32_le(data, pos.saturating_add(2))
.map(|v| v & 0x7FFFFFFF)
.unwrap_or(0);
let end_check = (size as usize).saturating_add(pos.saturating_add(2));
if (8..5000).contains(&size) && end_check <= data.len() {
break pos.saturating_add(1); }
}
if cur == 0xFF && next == 0x04 {
return Ok(controls); }
pos = pos.saturating_add(1);
};
pos = start;
let mut depth: u16 = 0;
let mut parent_stack: Vec<usize> = Vec::new();
while pos < data.len() {
let cur_byte = match data.get(pos).copied() {
Some(b) => b,
None => break,
};
let marker = match FormMarker::from_byte(cur_byte) {
Some(m) => m,
None => break,
};
pos = pos.saturating_add(1);
match marker {
FormMarker::FormEnd => break,
FormMarker::NewChild => {
if pos.saturating_add(4) > data.len() {
break;
}
let size = read_u32_le(data, pos).map(|v| v & 0x7FFFFFFF).unwrap_or(0);
let size_usize = size as usize;
let end = match pos.checked_add(size_usize) {
Some(e) if e <= data.len() => e,
_ => break,
};
if size < 8 {
break;
}
if let Some(slice) = data.get(pos..)
&& let Ok(mut record) = FormControlRecord::parse(slice)
{
record.depth = depth;
let level = depth as usize;
record.parent_index = level
.checked_sub(1)
.and_then(|parent_level| parent_stack.get(parent_level).copied());
record.offset_in_blob = pos as u32;
record.properties_offset_in_blob =
record.properties_offset_in_blob.wrapping_add(pos as u32);
let record_index = controls.len();
parent_stack.truncate(level);
parent_stack.push(record_index);
controls.push(record);
}
pos = end;
depth = depth.saturating_add(1);
}
FormMarker::Sibling | FormMarker::MenuStart => {
if pos.saturating_add(4) > data.len() {
break;
}
let size = read_u32_le(data, pos).map(|v| v & 0x7FFFFFFF).unwrap_or(0);
let size_usize = size as usize;
let end = match pos.checked_add(size_usize) {
Some(e) if e <= data.len() => e,
_ => break,
};
if size < 8 {
break;
}
if let Some(slice) = data.get(pos..)
&& let Ok(mut record) = FormControlRecord::parse(slice)
{
let record_depth = depth.saturating_sub(1);
record.depth = record_depth;
let level = record_depth as usize;
record.parent_index = level
.checked_sub(1)
.and_then(|parent_level| parent_stack.get(parent_level).copied());
record.offset_in_blob = pos as u32;
record.properties_offset_in_blob =
record.properties_offset_in_blob.wrapping_add(pos as u32);
let record_index = controls.len();
parent_stack.truncate(level);
parent_stack.push(record_index);
controls.push(record);
}
pos = end;
}
FormMarker::EndChildren => {
depth = depth.saturating_sub(1);
parent_stack.truncate(depth as usize);
}
}
}
Ok(controls)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_form_control_type_from_u8() {
assert_eq!(FormControlType::from_u8(0), FormControlType::PictureBox);
assert_eq!(FormControlType::from_u8(1), FormControlType::Label);
assert_eq!(FormControlType::from_u8(11), FormControlType::Timer);
assert_eq!(FormControlType::from_u8(13), FormControlType::Form);
assert_eq!(FormControlType::from_u8(19), FormControlType::Menu);
assert_eq!(FormControlType::from_u8(42), FormControlType::UserDocument);
assert_eq!(FormControlType::from_u8(99), FormControlType::Unknown(99));
}
#[test]
fn test_form_control_type_name() {
assert_eq!(FormControlType::Timer.name(), "Timer");
assert_eq!(FormControlType::CommandButton.name(), "CommandButton");
assert_eq!(FormControlType::Unknown(55).name(), "Unknown");
}
#[test]
fn test_form_control_type_display() {
assert_eq!(format!("{}", FormControlType::Label), "Label");
assert_eq!(format!("{}", FormControlType::Unknown(99)), "Unknown(99)");
}
#[test]
fn test_form_control_type_is_container() {
assert!(FormControlType::Frame.is_container());
assert!(FormControlType::PictureBox.is_container());
assert!(!FormControlType::Timer.is_container());
assert!(!FormControlType::Label.is_container());
}
#[test]
fn test_form_marker_from_byte() {
assert_eq!(FormMarker::from_byte(0x01), Some(FormMarker::NewChild));
assert_eq!(FormMarker::from_byte(0x02), Some(FormMarker::EndChildren));
assert_eq!(FormMarker::from_byte(0x03), Some(FormMarker::Sibling));
assert_eq!(FormMarker::from_byte(0x04), Some(FormMarker::FormEnd));
assert_eq!(FormMarker::from_byte(0x05), Some(FormMarker::MenuStart));
assert_eq!(FormMarker::from_byte(0x00), None);
assert_eq!(FormMarker::from_byte(0xFF), None);
}
#[test]
fn test_parse_timer_record() {
let record: [u8; 33] = [
0x21, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x54, 0x69, 0x6D, 0x65, 0x72, 0x31, 0x00, 0x0B, 0x02, 0x00, 0x03, 0x20, 0x4E, 0x00, 0x00, 0x07, 0x78, 0x00, 0x00, 0x00, 0x08, 0x78, 0x00, 0x00, 0x00, 0xFF, ];
let ctrl = FormControlRecord::parse(&record).unwrap();
assert_eq!(ctrl.cid(), 1);
assert_eq!(ctrl.array_index(), None);
assert_eq!(ctrl.name_bytes(), b"Timer1");
assert_eq!(ctrl.name(), "Timer1");
assert_eq!(ctrl.control_type(), FormControlType::Timer);
assert_eq!(ctrl.total_size(), 33);
assert_eq!(ctrl.raw_properties().len(), 17); }
#[test]
fn test_parse_control_array_record() {
let record: [u8; 16] = [
0x10, 0x00, 0x00, 0x80, 0x05, 0x03, 0x00, 0x03, 0x00, 0x42, 0x74, 0x6E, 0x00, 0x04, 0x02, 0xFF, ];
let ctrl = FormControlRecord::parse(&record).unwrap();
assert_eq!(ctrl.cid(), 5);
assert_eq!(ctrl.array_index(), Some(3));
assert_eq!(ctrl.name_bytes(), b"Btn");
assert_eq!(ctrl.name(), "Btn");
assert_eq!(ctrl.control_type(), FormControlType::CommandButton);
}
}