use crate::encoding;
use crate::file::{FileError, PageReader};
use crate::storage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormObjectType {
Form,
Report,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamKind {
Blob,
TypeInfo,
PropData,
BlobDelta,
}
impl StreamKind {
fn storage_name(&self) -> &'static str {
match self {
Self::Blob => "Blob",
Self::TypeInfo => "TypeInfo",
Self::PropData => "PropData",
Self::BlobDelta => "BlobDelta",
}
}
}
#[derive(Debug, Clone)]
pub struct FormEntry {
pub name: String,
pub object_type: FormObjectType,
}
#[derive(Debug, Clone)]
pub struct FormStream {
pub name: String,
pub object_type: FormObjectType,
pub stream_kind: StreamKind,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ControlInfo {
pub name: String,
pub type_code: u16,
pub index: u32,
}
#[derive(Debug, Clone)]
pub struct FormTypeInfo {
pub form_name: String,
pub object_type: FormObjectType,
pub controls: Vec<ControlInfo>,
}
#[derive(Debug, Clone)]
pub enum BlobValue {
Bool(bool),
Short(i16),
Long(i32),
Color(u32),
Double(f64),
Guid(String),
Text(String),
Binary(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct BlobProperty {
pub prop_id: u16,
pub value: BlobValue,
}
#[derive(Debug, Clone)]
pub struct ControlProperties {
pub name: String,
pub type_code: u16,
pub properties: Vec<BlobProperty>,
}
#[derive(Debug, Clone)]
pub struct FormProperties {
pub form_name: String,
pub object_type: FormObjectType,
pub properties: Vec<BlobProperty>,
pub controls: Vec<ControlProperties>,
}
pub fn list_forms(reader: &mut PageReader) -> Result<Vec<FormEntry>, FileError> {
let entries = storage::read_storage_entries(reader)?;
if entries.is_empty() {
return Ok(Vec::new());
}
let root_id = find_root_id(&entries);
let mut result = Vec::new();
if let Some(forms_folder) = entries
.iter()
.find(|e| e.parent_id == root_id && e.name == "Forms" && storage::is_storage(e))
{
let dir = find_dir_data(&entries, forms_folder.id);
if let Some(dir_data) = dir {
let mapping = parse_dir_data(&dir_data.data)?;
for (name, _storage_num) in mapping {
result.push(FormEntry {
name,
object_type: FormObjectType::Form,
});
}
}
}
if let Some(reports_folder) = entries
.iter()
.find(|e| e.parent_id == root_id && e.name == "Reports" && storage::is_storage(e))
{
let dir = find_dir_data(&entries, reports_folder.id);
if let Some(dir_data) = dir {
let mapping = parse_dir_data(&dir_data.data)?;
for (name, _storage_num) in mapping {
result.push(FormEntry {
name,
object_type: FormObjectType::Report,
});
}
}
}
Ok(result)
}
pub fn read_form_stream(
reader: &mut PageReader,
name: &str,
stream_kind: StreamKind,
) -> Result<FormStream, FileError> {
let entries = storage::read_storage_entries(reader)?;
let (object_type, stream_data) = find_stream(&entries, name, stream_kind)?;
Ok(FormStream {
name: name.to_string(),
object_type,
stream_kind,
data: stream_data,
})
}
pub fn read_form_type_info(reader: &mut PageReader, name: &str) -> Result<FormTypeInfo, FileError> {
let entries = storage::read_storage_entries(reader)?;
let (object_type, stream_data) = find_stream(&entries, name, StreamKind::TypeInfo)?;
let controls = parse_type_info(&stream_data)?;
Ok(FormTypeInfo {
form_name: name.to_string(),
object_type,
controls,
})
}
pub fn prop_id_name(prop_id: u16) -> Option<&'static str> {
match prop_id {
0x0011 => Some("Caption"),
0x0012 => Some("ColumnWidths"),
0x0014 => Some("Name"),
0x001B => Some("ControlSource"),
0x0022 => Some("FontName"),
0x0026 => Some("Format"),
0x005B => Some("RowSource"),
0x005D => Some("RowSourceType"),
0x0068 => Some("OnKeyDown"),
0x0069 => Some("OnKeyUp"),
0x006A => Some("OnKeyPress"),
0x006B => Some("OnMouseDown"),
0x006C => Some("OnMouseUp"),
0x006D => Some("OnMouseMove"),
0x0073 => Some("OnGotFocus"),
0x0074 => Some("OnLostFocus"),
0x007E => Some("OnClick"),
0x009C => Some("RecordSource"),
0x00A0 => Some("FontName"),
0x00DE => Some("OnEnter"),
0x00DF => Some("OnExit"),
0x00E0 => Some("OnDblClick"),
0x00F5 => Some("Filter"),
0x010A => Some("LabelType"),
0x015A => Some("InputMask"),
_ => None,
}
}
pub fn control_type_name(type_code: u16) -> Option<&'static str> {
match type_code {
0x066A => Some("CheckBox"),
0x0A7A => Some("ToggleButton"),
0x0B68 => Some("CommandButton"),
0x0C64 | 0x0D64 => Some("Label"),
0x0E65 => Some("Rectangle"),
0x0F67 => Some("Image"),
0x126D => Some("TextBox"),
0x136F => Some("ComboBox"),
0x1470 => Some("SubForm"),
0x1666 => Some("Line"),
0x1898 | 0x1998 => Some("Detail"),
0x1899 => Some("FormHeader"),
0x189A => Some("FormFooter"),
0x247F => Some("EmptyCell"),
0x1B64 => Some("Label"),
0x1B65 => Some("Rectangle"),
0x1B66 => Some("Line"),
0x1B67 => Some("Image"),
0x1B68 => Some("CommandButton"),
0x1B6A => Some("CheckBox"),
0x1B6D => Some("TextBox"),
0x1B6F => Some("ComboBox"),
0x1B70 => Some("SubReport"),
0x1999 => Some("ReportHeader"),
0x199A => Some("ReportFooter"),
0x199D => Some("GroupHeader"),
0x199E => Some("GroupFooter"),
0x1F9B => Some("PageHeader"),
0x1F9C => Some("PageFooter"),
_ => None,
}
}
impl BlobProperty {
pub fn name(&self) -> Option<&'static str> {
prop_id_name(self.prop_id)
}
pub fn display_name(&self) -> String {
match self.name() {
Some(n) => n.to_string(),
None => format!("0x{:04X}", self.prop_id),
}
}
}
impl std::fmt::Display for BlobValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bool(v) => write!(f, "{}", if *v { "yes" } else { "no" }),
Self::Short(v) => write!(f, "{v}"),
Self::Long(v) => write!(f, "{v}"),
Self::Color(v) => write!(f, "#{:06X}", v & 0x00FF_FFFF),
Self::Double(v) => write!(f, "{v}"),
Self::Guid(v) => write!(f, "{v}"),
Self::Text(v) => write!(f, "{v}"),
Self::Binary(v) => write!(f, "({} bytes)", v.len()),
}
}
}
pub fn read_form_properties(
reader: &mut PageReader,
name: &str,
) -> Result<FormProperties, FileError> {
let entries = storage::read_storage_entries(reader)?;
let (object_type, blob_data) = find_stream(&entries, name, StreamKind::Blob)?;
let type_info_controls = match find_stream(&entries, name, StreamKind::TypeInfo) {
Ok((_, ti_data)) => parse_type_info(&ti_data).unwrap_or_default(),
Err(_) => Vec::new(),
};
let (form_props, control_prop_groups) = parse_blob(&blob_data)?;
let controls = control_prop_groups
.into_iter()
.enumerate()
.map(|(i, props)| {
let blob_name = props
.iter()
.find(|p| p.prop_id == 0x0014)
.and_then(|p| match &p.value {
BlobValue::Text(s) => Some(s.clone()),
_ => None,
})
.unwrap_or_else(|| format!("Control_{i}"));
let type_code = type_info_controls
.iter()
.find(|c| c.name == blob_name)
.or_else(|| type_info_controls.get(i))
.map(|c| c.type_code)
.unwrap_or(0);
ControlProperties {
name: blob_name,
type_code,
properties: props,
}
})
.collect();
Ok(FormProperties {
form_name: name.to_string(),
object_type,
properties: form_props,
controls,
})
}
fn find_stream(
entries: &[storage::StorageEntry],
name: &str,
stream_kind: StreamKind,
) -> Result<(FormObjectType, Vec<u8>), FileError> {
let root_id = find_root_id(entries);
for (folder_name, obj_type) in [
("Forms", FormObjectType::Form),
("Reports", FormObjectType::Report),
] {
if let Some(folder) = entries
.iter()
.find(|e| e.parent_id == root_id && e.name == folder_name && storage::is_storage(e))
{
if let Some(dir_data) = find_dir_data(entries, folder.id) {
let mapping = parse_dir_data(&dir_data.data)?;
if let Some((_form_name, storage_num)) = mapping.iter().find(|(n, _)| n == name) {
if let Some(form_storage) = entries.iter().find(|e| {
e.parent_id == folder.id && e.name == *storage_num && storage::is_storage(e)
}) {
let stream_name = stream_kind.storage_name();
if let Some(stream_entry) = entries.iter().find(|e| {
e.parent_id == form_storage.id
&& e.name == stream_name
&& !storage::is_storage(e)
}) {
return Ok((obj_type, stream_entry.data.clone()));
}
}
}
}
}
}
Err(FileError::FormNotFound {
name: name.to_string(),
})
}
fn find_root_id(entries: &[storage::StorageEntry]) -> i32 {
entries
.iter()
.find(|e| e.parent_id == e.id && storage::is_storage(e))
.map(|e| e.id)
.unwrap_or(1)
}
fn find_dir_data(
entries: &[storage::StorageEntry],
folder_id: i32,
) -> Option<&storage::StorageEntry> {
entries.iter().find(|e| {
e.parent_id == folder_id
&& !storage::is_storage(e)
&& (e.name == "DirData" || e.name.ends_with("DirData"))
})
}
fn parse_dir_data(data: &[u8]) -> Result<Vec<(String, String)>, FileError> {
if data.len() < 4 {
return Ok(Vec::new());
}
let mut entries = Vec::new();
let mut pos = 4;
while pos + 1 < data.len() {
if data[pos] != 0x04 {
break;
}
let declared_len = data[pos + 1] as usize;
pos += 2;
if declared_len < 4 || pos + declared_len > data.len() {
break;
}
let payload_end = pos + declared_len;
let ends_with_null =
payload_end >= 2 && data[payload_end - 2] == 0x00 && data[payload_end - 1] == 0x00;
let actual_end = if ends_with_null {
payload_end
} else {
let mut scan = pos + declared_len;
loop {
if scan + 1 >= data.len() {
break scan + 1; }
let val = u16::from_le_bytes([data[scan], data[scan + 1]]);
if val == 0x0000 {
break scan + 2; }
scan += 2;
}
};
if actual_end < pos + 4 {
pos = actual_end;
continue;
}
let name_bytes = &data[pos..actual_end - 4];
let storage_index = u16::from_le_bytes([data[actual_end - 4], data[actual_end - 3]]);
if name_bytes.is_empty() {
pos = actual_end;
continue;
}
let name =
encoding::decode_utf16le(name_bytes).map_err(|_| FileError::InvalidFormData {
reason: "invalid UTF-16LE in DirData name",
})?;
entries.push((name, storage_index.to_string()));
pos = actual_end;
}
Ok(entries)
}
const TYPEINFO_MAGIC: u32 = 0xACCD_EAF7;
fn parse_type_info(data: &[u8]) -> Result<Vec<ControlInfo>, FileError> {
if data.len() < 32 {
return Err(FileError::InvalidFormData {
reason: "TypeInfo too short for header",
});
}
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic != TYPEINFO_MAGIC {
return Err(FileError::InvalidFormData {
reason: "TypeInfo magic mismatch",
});
}
let entry_count = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
let max_entries = (data.len().saturating_sub(32)) / 8;
if entry_count > max_entries {
return Err(FileError::InvalidFormData {
reason: "TypeInfo entry count exceeds data size",
});
}
let mut controls = Vec::with_capacity(entry_count);
let mut pos = 32;
for _ in 0..entry_count {
if pos + 8 > data.len() {
break;
}
let ctrl_type = u16::from_le_bytes([data[pos], data[pos + 1]]);
let index =
u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]);
pos += 8;
let epp_start = pos;
while pos < data.len() && data[pos] != 0x00 {
pos += 1;
}
let epp_bytes = &data[epp_start..pos];
if pos < data.len() {
pos += 1; }
let real_start = pos;
while pos < data.len() && data[pos] != 0x00 {
pos += 1;
}
let real_bytes = &data[real_start..pos];
if pos < data.len() {
pos += 1; }
let name = if real_bytes.is_empty() {
decode_shift_jis(epp_bytes)
} else {
decode_shift_jis(real_bytes)
};
if ctrl_type == 0x1EFF {
continue;
}
controls.push(ControlInfo {
name,
type_code: ctrl_type,
index,
});
}
Ok(controls)
}
fn decode_shift_jis(bytes: &[u8]) -> String {
let (result, _, _) = encoding_rs::SHIFT_JIS.decode(bytes);
result.into_owned()
}
fn parse_blob(data: &[u8]) -> Result<(Vec<BlobProperty>, Vec<Vec<BlobProperty>>), FileError> {
if data.len() < 14 {
return Ok((Vec::new(), Vec::new()));
}
let mut pos = 14;
let mut all_props = Vec::new();
while pos + 14 <= data.len() {
let prop_id = u16::from_le_bytes([data[pos], data[pos + 1]]);
let type_code =
u32::from_le_bytes([data[pos + 2], data[pos + 3], data[pos + 4], data[pos + 5]]);
let _b = u32::from_le_bytes([data[pos + 6], data[pos + 7], data[pos + 8], data[pos + 9]]);
let c = u32::from_le_bytes([
data[pos + 10],
data[pos + 11],
data[pos + 12],
data[pos + 13],
]);
let data_start = pos + 14;
match type_code {
0x01 => {
if data_start + 4 > data.len() {
break;
}
let val = u32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Bool(val != 0),
});
pos += 18;
}
0x02 => {
if data_start + 5 > data.len() {
break;
}
let val = i16::from_le_bytes([data[data_start], data[data_start + 1]]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Short(val),
});
pos += 19;
}
0x03 => {
if data_start + 6 > data.len() {
break;
}
let val = i32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Long(val),
});
pos += 20;
}
0x04 => {
if data_start + 8 > data.len() {
break;
}
let val = u32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Color(val),
});
pos += 22;
}
0x08 => {
if data_start + 12 > data.len() {
break;
}
let val = f64::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
data[data_start + 4],
data[data_start + 5],
data[data_start + 6],
data[data_start + 7],
]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Double(val),
});
pos += 26;
}
0x09 => {
if data_start + 20 > data.len() {
break;
}
let guid = format_guid(&data[data_start..data_start + 16]);
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Guid(guid),
});
pos += 34;
}
0x0A | 0x0C => {
let byte_len = c as usize;
if data_start + byte_len + 4 > data.len() {
break;
}
let text_bytes = &data[data_start..data_start + byte_len];
let text = encoding::decode_utf16le(text_bytes)
.unwrap_or_else(|_| String::from_utf8_lossy(text_bytes).into_owned());
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Text(text),
});
pos += 14 + byte_len + 4;
}
0x0B => {
let byte_len = c as usize;
if data_start + byte_len + 4 > data.len() {
break;
}
let bin_data = data[data_start..data_start + byte_len].to_vec();
all_props.push(BlobProperty {
prop_id,
value: BlobValue::Binary(bin_data),
});
pos += 14 + byte_len + 4;
}
_ => {
break;
}
}
}
let control_groups = scan_control_sections(data, pos);
Ok((all_props, control_groups))
}
fn scan_control_sections(data: &[u8], start: usize) -> Vec<Vec<BlobProperty>> {
let pattern: [u8; 6] = [0x14, 0x00, 0x0A, 0x00, 0x00, 0x00];
let mut section_starts = Vec::new();
let mut search_pos = start;
while search_pos + 6 <= data.len() {
if data[search_pos..search_pos + 6] == pattern {
section_starts.push(search_pos);
search_pos += 6; } else {
search_pos += 1;
}
}
let mut control_groups = Vec::new();
for (i, &sec_start) in section_starts.iter().enumerate() {
let sec_end = section_starts.get(i + 1).copied().unwrap_or(data.len());
let props = parse_section_props(data, sec_start, sec_end);
if !props.is_empty() {
control_groups.push(props);
}
}
control_groups
}
fn parse_section_props(data: &[u8], start: usize, end: usize) -> Vec<BlobProperty> {
let mut props = Vec::new();
let mut pos = start;
while pos + 14 <= end {
let prop_id = u16::from_le_bytes([data[pos], data[pos + 1]]);
let type_code =
u32::from_le_bytes([data[pos + 2], data[pos + 3], data[pos + 4], data[pos + 5]]);
let _b = u32::from_le_bytes([data[pos + 6], data[pos + 7], data[pos + 8], data[pos + 9]]);
let c = u32::from_le_bytes([
data[pos + 10],
data[pos + 11],
data[pos + 12],
data[pos + 13],
]);
let data_start = pos + 14;
match type_code {
0x01 => {
if data_start + 4 > end {
break;
}
let val = u32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Bool(val != 0),
});
pos += 18;
}
0x02 => {
if data_start + 5 > end {
break;
}
let val = i16::from_le_bytes([data[data_start], data[data_start + 1]]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Short(val),
});
pos += 19;
}
0x03 => {
if data_start + 6 > end {
break;
}
let val = i32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Long(val),
});
pos += 20;
}
0x04 => {
if data_start + 8 > end {
break;
}
let val = u32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Color(val),
});
pos += 22;
}
0x06 => {
if data_start + 6 > end {
break;
}
let val = i32::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Long(val),
});
pos += 20;
}
0x08 => {
if data_start + 12 > end {
break;
}
let val = f64::from_le_bytes([
data[data_start],
data[data_start + 1],
data[data_start + 2],
data[data_start + 3],
data[data_start + 4],
data[data_start + 5],
data[data_start + 6],
data[data_start + 7],
]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Double(val),
});
pos += 26;
}
0x09 => {
if data_start + 20 > end {
break;
}
let guid = format_guid(&data[data_start..data_start + 16]);
props.push(BlobProperty {
prop_id,
value: BlobValue::Guid(guid),
});
pos += 34;
}
0x0A | 0x0C => {
let byte_len = c as usize;
if data_start + byte_len + 4 > end {
break;
}
let text_bytes = &data[data_start..data_start + byte_len];
let text = encoding::decode_utf16le(text_bytes)
.unwrap_or_else(|_| String::from_utf8_lossy(text_bytes).into_owned());
props.push(BlobProperty {
prop_id,
value: BlobValue::Text(text),
});
pos += 14 + byte_len + 4;
}
0x0B => {
let byte_len = c as usize;
if data_start + byte_len + 4 > end {
break;
}
let bin_data = data[data_start..data_start + byte_len].to_vec();
props.push(BlobProperty {
prop_id,
value: BlobValue::Binary(bin_data),
});
pos += 14 + byte_len + 4;
}
_ => {
break;
}
}
}
props
}
fn format_guid(bytes: &[u8]) -> String {
if bytes.len() < 16 {
return format!("({} bytes)", bytes.len());
}
let d1 = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let d2 = u16::from_le_bytes([bytes[4], bytes[5]]);
let d3 = u16::from_le_bytes([bytes[6], bytes[7]]);
format!(
"{{{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}}}",
d1,
d2,
d3,
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15]
)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir)
.join("../../testdata")
.join(relative);
if path.exists() {
Some(path)
} else {
None
}
}
macro_rules! skip_if_missing {
($path:expr) => {
match test_data_path($path) {
Some(p) => p,
None => {
eprintln!("SKIP: test data not found: {}", $path);
return;
}
}
};
}
#[test]
fn parse_dir_data_empty() {
let data = [0x00, 0x00, 0x00, 0x00];
let result = parse_dir_data(&data).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_dir_data_too_short() {
let result = parse_dir_data(&[0x00]).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_dir_data_single_entry() {
let mut data = vec![0x00, 0x00, 0x00, 0x00]; data.push(0x04); data.push(0x08); data.extend_from_slice(&[0x41, 0x00, 0x42, 0x00]);
data.extend_from_slice(&[0x05, 0x00]);
data.extend_from_slice(&[0x00, 0x00]);
let result = parse_dir_data(&data).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].0, "AB");
assert_eq!(result[0].1, "5");
}
#[test]
fn parse_type_info_too_short() {
let data = [0u8; 16];
let result = parse_type_info(&data);
assert!(result.is_err());
}
#[test]
fn parse_type_info_bad_magic() {
let mut data = [0u8; 32];
data[0] = 0xFF; let result = parse_type_info(&data);
assert!(result.is_err());
}
#[test]
fn parse_type_info_empty_entries() {
let mut data = vec![0u8; 32];
data[0..4].copy_from_slice(&TYPEINFO_MAGIC.to_le_bytes());
data[12..16].copy_from_slice(&0u32.to_le_bytes());
let result = parse_type_info(&data).unwrap();
assert!(result.is_empty());
}
#[test]
fn parse_type_info_single_entry() {
let mut data = vec![0u8; 32];
data[0..4].copy_from_slice(&TYPEINFO_MAGIC.to_le_bytes());
data[12..16].copy_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&[0x68, 0x0B]); data.extend_from_slice(&[0x00, 0x00]); data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); data.extend_from_slice(b"Btn1"); data.push(0x00); data.push(0x00);
let result = parse_type_info(&data).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "Btn1");
assert_eq!(result[0].type_code, 0x0B68);
assert_eq!(result[0].index, 0);
}
#[test]
fn list_forms_v2007() {
let path = skip_if_missing!("vbaV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let forms = list_forms(&mut reader).unwrap();
assert!(
forms.iter().any(|e| e.name == "Form1"),
"expected Form1 in form list, got: {:?}",
forms.iter().map(|e| &e.name).collect::<Vec<_>>()
);
}
#[test]
fn read_form_blob_v2007() {
let path = skip_if_missing!("vbaV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let stream = read_form_stream(&mut reader, "Form1", StreamKind::Blob).unwrap();
assert!(!stream.data.is_empty(), "Blob should not be empty");
assert_eq!(stream.object_type, FormObjectType::Form);
}
#[test]
fn read_form_type_info_v2007() {
let path = skip_if_missing!("vbaV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let type_info = read_form_type_info(&mut reader, "Form1").unwrap();
assert!(
!type_info.controls.is_empty(),
"TypeInfo should have at least one control"
);
assert_eq!(type_info.object_type, FormObjectType::Form);
}
#[test]
fn form_not_found() {
let path = skip_if_missing!("vbaV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let result = read_form_stream(&mut reader, "NoSuchForm", StreamKind::Blob);
assert!(matches!(result, Err(FileError::FormNotFound { .. })));
}
#[test]
fn no_forms_in_plain_db() {
let path = skip_if_missing!("V2003/testV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let forms = list_forms(&mut reader).unwrap();
assert!(forms.is_empty(), "expected no forms in plain test database");
}
fn make_blob(entries: &[u8]) -> Vec<u8> {
let mut data = vec![0u8; 14]; data.extend_from_slice(entries);
data
}
fn make_entry(prop_id: u16, type_code: u32, b: u32, c: u32, payload: &[u8]) -> Vec<u8> {
let mut entry = Vec::new();
entry.extend_from_slice(&prop_id.to_le_bytes());
entry.extend_from_slice(&type_code.to_le_bytes());
entry.extend_from_slice(&b.to_le_bytes());
entry.extend_from_slice(&c.to_le_bytes());
entry.extend_from_slice(payload);
entry
}
#[test]
fn parse_blob_empty() {
let data = vec![0u8; 14];
let (form_props, controls) = parse_blob(&data).unwrap();
assert!(form_props.is_empty());
assert!(controls.is_empty());
}
#[test]
fn parse_blob_too_short() {
let (form_props, controls) = parse_blob(&[0u8; 5]).unwrap();
assert!(form_props.is_empty());
assert!(controls.is_empty());
}
#[test]
fn parse_blob_bool_entry() {
let payload = [0x01, 0x00, 0x00, 0x00]; let entry = make_entry(0x0013, 0x01, 0, 0, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
assert_eq!(props[0].prop_id, 0x0013);
assert!(matches!(props[0].value, BlobValue::Bool(true)));
}
#[test]
fn parse_blob_short_entry() {
let payload = [0x2A, 0x00, 0x00, 0x00, 0x00]; let entry = make_entry(0x0098, 0x02, 0, 0, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
assert!(matches!(props[0].value, BlobValue::Short(42)));
}
#[test]
fn parse_blob_long_entry() {
let payload = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00]; let entry = make_entry(0x002A, 0x03, 0, 0, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
assert!(matches!(props[0].value, BlobValue::Long(256)));
}
#[test]
fn parse_blob_text_entry() {
let text = "AB"; let text_bytes = [0x41, 0x00, 0x42, 0x00];
let c = text_bytes.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&text_bytes);
payload.extend_from_slice(&[0x00; 4]); let entry = make_entry(0x009C, 0x0A, 0, c, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
assert_eq!(props[0].prop_id, 0x009C); match &props[0].value {
BlobValue::Text(s) => assert_eq!(s, text),
other => panic!("expected Text, got {:?}", other),
}
}
#[test]
fn parse_blob_binary_entry() {
let bin = [0xDE, 0xAD, 0xBE, 0xEF];
let c = bin.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&bin);
payload.extend_from_slice(&[0x00; 4]); let entry = make_entry(0x00BD, 0x0B, 0, c, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
match &props[0].value {
BlobValue::Binary(v) => assert_eq!(v.as_slice(), &bin),
other => panic!("expected Binary, got {:?}", other),
}
}
#[test]
fn parse_blob_multiple_entries() {
let mut entries = Vec::new();
entries.extend_from_slice(&make_entry(0x0013, 0x01, 0, 0, &[0x00; 4]));
entries.extend_from_slice(&make_entry(
0x0098,
0x02,
0,
0,
&[0x07, 0x00, 0x00, 0x00, 0x00],
));
entries.extend_from_slice(&make_entry(
0x002A,
0x03,
0,
0,
&[0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00],
));
let blob = make_blob(&entries);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 3);
assert!(matches!(props[0].value, BlobValue::Bool(false)));
assert!(matches!(props[1].value, BlobValue::Short(7)));
assert!(matches!(props[2].value, BlobValue::Long(-1)));
}
#[test]
fn parse_blob_stops_on_unknown_type() {
let mut entries = Vec::new();
entries.extend_from_slice(&make_entry(0x0013, 0x01, 0, 0, &[0x01; 4]));
entries.extend_from_slice(&make_entry(0x9999, 0xFF, 0, 0, &[0x00; 10]));
let blob = make_blob(&entries);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1, "should stop at unknown type");
}
#[test]
fn parse_blob_guid_entry() {
let guid_bytes = [
0x50, 0xA6, 0x64, 0x8D, 0xE7, 0x62, 0x03, 0x49, 0x97, 0x33, 0x0D, 0x8C, 0xE8, 0x49,
0x78, 0xBF,
];
let mut payload = Vec::new();
payload.extend_from_slice(&guid_bytes);
payload.extend_from_slice(&[0x00; 4]); let entry = make_entry(0x0178, 0x09, 0, 0, &payload);
let blob = make_blob(&entry);
let (props, _) = parse_blob(&blob).unwrap();
assert_eq!(props.len(), 1);
match &props[0].value {
BlobValue::Guid(s) => assert!(s.starts_with('{') && s.ends_with('}')),
other => panic!("expected Guid, got {:?}", other),
}
}
#[test]
fn prop_id_name_known() {
assert_eq!(prop_id_name(0x009C), Some("RecordSource"));
assert_eq!(prop_id_name(0x001B), Some("ControlSource"));
assert_eq!(prop_id_name(0x00F5), Some("Filter"));
assert_eq!(prop_id_name(0x0014), Some("Name"));
}
#[test]
fn prop_id_name_unknown() {
assert_eq!(prop_id_name(0xFFFF), None);
}
#[test]
fn blob_property_display_name() {
let known = BlobProperty {
prop_id: 0x009C,
value: BlobValue::Bool(true),
};
assert_eq!(known.display_name(), "RecordSource");
let unknown = BlobProperty {
prop_id: 0x1234,
value: BlobValue::Bool(true),
};
assert_eq!(unknown.display_name(), "0x1234");
}
#[test]
fn blob_value_display() {
assert_eq!(format!("{}", BlobValue::Bool(true)), "yes");
assert_eq!(format!("{}", BlobValue::Bool(false)), "no");
assert_eq!(format!("{}", BlobValue::Short(42)), "42");
assert_eq!(format!("{}", BlobValue::Long(-1)), "-1");
assert_eq!(format!("{}", BlobValue::Color(0x00FF0000)), "#FF0000");
assert_eq!(format!("{}", BlobValue::Text("hello".into())), "hello");
assert_eq!(format!("{}", BlobValue::Binary(vec![0; 10])), "(10 bytes)");
}
#[test]
fn read_form_properties_v2007() {
let path = skip_if_missing!("vbaV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "Form1").unwrap();
assert_eq!(props.object_type, FormObjectType::Form);
assert!(
!props.properties.is_empty(),
"expected form-level properties, got empty"
);
}
fn find_control_source(props: &FormProperties, ctrl_name: &str) -> Option<String> {
props
.controls
.iter()
.find(|c| c.name == ctrl_name)
.and_then(|c| c.properties.iter().find(|p| p.prop_id == 0x001B))
.and_then(|p| match &p.value {
BlobValue::Text(s) => Some(s.clone()),
_ => None,
})
}
#[test]
fn list_forms_form_prop_test() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let forms = list_forms(&mut reader).unwrap();
let mut names: Vec<&str> = forms.iter().map(|e| e.name.as_str()).collect();
names.sort();
assert_eq!(
names,
["F_Buttons", "F_Table0", "F_Table1", "jp_フォーム_2"]
);
assert!(forms.iter().all(|e| e.object_type == FormObjectType::Form));
}
#[test]
fn read_form_properties_empty_form() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table0").unwrap();
assert_eq!(props.form_name, "F_Table0");
assert_eq!(props.object_type, FormObjectType::Form);
assert!(
!props.properties.iter().any(|p| p.prop_id == 0x009C),
"empty form should not have RecordSource"
);
assert!(
!props.properties.iter().any(|p| p.prop_id == 0x00F5),
"empty form should not have Filter"
);
}
#[test]
fn read_form_properties_record_source_and_filter() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table1").unwrap();
let rs = props
.properties
.iter()
.find(|p| p.prop_id == 0x009C)
.expect("RecordSource should exist");
match &rs.value {
BlobValue::Text(s) => assert_eq!(s.trim(), "SELECT * FROM Table1;"),
other => panic!("expected Text, got {:?}", other),
}
let filter = props
.properties
.iter()
.find(|p| p.prop_id == 0x00F5)
.expect("Filter should exist");
match &filter.value {
BlobValue::Text(s) => assert_eq!(s, "[ID] > 0"),
other => panic!("expected Text, got {:?}", other),
}
}
#[test]
fn read_form_properties_control_source() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table1").unwrap();
assert_eq!(find_control_source(&props, "ID").as_deref(), Some("ID"));
assert_eq!(
find_control_source(&props, "ProductName").as_deref(),
Some("ProductName")
);
assert_eq!(
find_control_source(&props, "Price").as_deref(),
Some("Price")
);
assert_eq!(find_control_source(&props, "Qty").as_deref(), Some("Qty"));
}
#[test]
fn read_form_properties_calculated_field() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table1").unwrap();
assert_eq!(
find_control_source(&props, "Text_01_SubTotal").as_deref(),
Some("=[Price]*[Qty]")
);
}
#[test]
fn read_form_properties_format() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table1").unwrap();
let price = props
.controls
.iter()
.find(|c| c.name == "Price")
.expect("Price control should exist");
let fmt = price
.properties
.iter()
.find(|p| p.prop_id == 0x0026)
.expect("Format property should exist on Price");
match &fmt.value {
BlobValue::Text(s) => assert_eq!(s, "¥#,##0;-¥#,##0"),
other => panic!("expected Text, got {:?}", other),
}
}
#[test]
fn read_form_properties_japanese_form() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "jp_フォーム_2").unwrap();
assert_eq!(props.form_name, "jp_フォーム_2");
let rs = props
.properties
.iter()
.find(|p| p.prop_id == 0x009C)
.expect("RecordSource should exist");
match &rs.value {
BlobValue::Text(s) => assert_eq!(s, "jp_クエリ_02"),
other => panic!("expected Text, got {:?}", other),
}
assert_eq!(
find_control_source(&props, "商品名").as_deref(),
Some("商品名")
);
assert_eq!(find_control_source(&props, "単価").as_deref(), Some("単価"));
assert_eq!(find_control_source(&props, "個数").as_deref(), Some("個数"));
}
#[test]
fn read_form_properties_japanese_calculated_field() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "jp_フォーム_2").unwrap();
assert_eq!(
find_control_source(&props, "小計").as_deref(),
Some("=[単価]*[個数]")
);
}
#[test]
fn read_form_properties_onclick_event() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Table1").unwrap();
let btn = props
.controls
.iter()
.find(|c| c.name == "btn_msg")
.expect("btn_msg should exist");
let onclick = btn
.properties
.iter()
.find(|p| p.prop_id == 0x007E)
.expect("OnClick (0x007E) should exist on btn_msg");
match &onclick.value {
BlobValue::Text(s) => assert_eq!(s, "[Event Procedure]"),
other => panic!("expected Text, got {:?}", other),
}
}
#[test]
fn read_form_properties_japanese_onclick_event() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "jp_フォーム_2").unwrap();
let cmd = props
.controls
.iter()
.find(|c| c.name == "コマンド22")
.expect("コマンド22 should exist");
let onclick = cmd
.properties
.iter()
.find(|p| p.prop_id == 0x007E)
.expect("OnClick (0x007E) should exist on コマンド22");
match &onclick.value {
BlobValue::Text(s) => assert_eq!(s, "[Event Procedure]"),
other => panic!("expected Text, got {:?}", other),
}
}
#[test]
fn read_form_properties_all_event_types() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let props = read_form_properties(&mut reader, "F_Buttons").unwrap();
let event_cases: &[(&str, u16)] = &[
("btn_Click", 0x007E),
("btn_GotFocus", 0x0073),
("btn_LostFocus", 0x0074),
("btn_DblClick", 0x00E0),
("btn_MouseDown", 0x006B),
("btn_MouseUp", 0x006C),
("btn_MouseMove", 0x006D),
("btn_KeyDown", 0x0068),
("btn_KeyUp", 0x0069),
("btn_KeyPress", 0x006A),
("btn_Enter", 0x00DE),
("btn_Exit", 0x00DF),
];
for (btn_name, expected_prop_id) in event_cases {
let ctrl = props
.controls
.iter()
.find(|c| c.name == *btn_name)
.unwrap_or_else(|| panic!("control '{}' not found", btn_name));
let event = ctrl
.properties
.iter()
.find(|p| p.prop_id == *expected_prop_id)
.unwrap_or_else(|| {
panic!(
"prop_id 0x{:04X} not found on '{}'",
expected_prop_id, btn_name
)
});
match &event.value {
BlobValue::Text(s) => assert_eq!(
s, "[Event Procedure]",
"event value mismatch on '{}'",
btn_name
),
other => panic!("expected Text on '{}', got {:?}", btn_name, other),
}
}
}
#[test]
fn control_type_name_known_form_controls() {
let cases = [
(0x0B68, "CommandButton"),
(0x126D, "TextBox"),
(0x136F, "ComboBox"),
(0x066A, "CheckBox"),
(0x0A7A, "ToggleButton"),
(0x0E65, "Rectangle"),
(0x0F67, "Image"),
(0x1470, "SubForm"),
(0x1666, "Line"),
(0x247F, "EmptyCell"),
(0x1898, "Detail"),
(0x1998, "Detail"),
(0x1899, "FormHeader"),
(0x189A, "FormFooter"),
];
for (code, expected) in cases {
assert_eq!(
control_type_name(code),
Some(expected),
"form control 0x{:04X} should be {:?}",
code,
expected
);
}
}
#[test]
fn control_type_name_known_report_controls() {
let cases = [
(0x1B6D, "TextBox"),
(0x1B64, "Label"),
(0x1B70, "SubReport"),
(0x1B68, "CommandButton"),
(0x1B6A, "CheckBox"),
(0x1B6F, "ComboBox"),
(0x1B65, "Rectangle"),
(0x1B66, "Line"),
(0x1B67, "Image"),
(0x1999, "ReportHeader"),
(0x199A, "ReportFooter"),
(0x199D, "GroupHeader"),
(0x199E, "GroupFooter"),
(0x1F9B, "PageHeader"),
(0x1F9C, "PageFooter"),
];
for (code, expected) in cases {
assert_eq!(
control_type_name(code),
Some(expected),
"report control 0x{:04X} should be {:?}",
code,
expected
);
}
}
#[test]
fn control_type_name_unknown_returns_none() {
assert_eq!(control_type_name(0x0000), None);
assert_eq!(control_type_name(0xFFFF), None);
assert_eq!(control_type_name(0x1EFF), None);
}
#[test]
fn control_type_name_label_variants() {
assert_eq!(control_type_name(0x0C64), Some("Label"));
assert_eq!(control_type_name(0x0D64), Some("Label"));
assert_eq!(control_type_name(0x1B64), Some("Label"));
}
#[test]
fn blob_value_display_double_and_guid() {
assert_eq!(format!("{}", BlobValue::Double(1.23)), "1.23");
assert_eq!(format!("{}", BlobValue::Guid("{ABC}".into())), "{ABC}");
}
#[test]
fn parse_section_props_type_06_long() {
let mut data = Vec::new();
data.extend_from_slice(&0x0099u16.to_le_bytes());
data.extend_from_slice(&0x00000006u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&42i32.to_le_bytes());
data.extend_from_slice(&[0x00, 0x00]);
let props = parse_section_props(&data, 0, data.len());
assert_eq!(props.len(), 1);
assert_eq!(props[0].prop_id, 0x0099);
assert!(matches!(props[0].value, BlobValue::Long(42)));
}
#[test]
fn parse_section_props_type_08_double() {
let mut data = Vec::new();
data.extend_from_slice(&0x00AAu16.to_le_bytes());
data.extend_from_slice(&0x00000008u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&1.23f64.to_le_bytes());
data.extend_from_slice(&[0x00; 4]);
let props = parse_section_props(&data, 0, data.len());
assert_eq!(props.len(), 1);
assert_eq!(props[0].prop_id, 0x00AA);
match &props[0].value {
BlobValue::Double(v) => assert!((v - 1.23).abs() < f64::EPSILON),
other => panic!("expected Double, got {:?}", other),
}
}
#[test]
fn parse_section_props_type_04_color() {
let mut data = Vec::new();
data.extend_from_slice(&0x00BBu16.to_le_bytes());
data.extend_from_slice(&0x00000004u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0x00FF0000u32.to_le_bytes());
data.extend_from_slice(&[0x00; 4]);
let props = parse_section_props(&data, 0, data.len());
assert_eq!(props.len(), 1);
assert_eq!(props[0].prop_id, 0x00BB);
assert!(matches!(props[0].value, BlobValue::Color(0x00FF0000)));
}
}