use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum VbaModuleKind {
Standard,
Class,
Document,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VbaModule {
pub name: String,
pub kind: VbaModuleKind,
pub source: String,
pub bound_sheet_id: Option<u64>,
#[serde(default)]
pub prefix_bytes: Vec<u8>,
#[serde(default = "default_module_cookie")]
pub module_cookie: u16,
#[serde(default)]
pub cached_compressed_source: Option<Vec<u8>>,
}
fn default_module_cookie() -> u16 {
0xFFFF
}
impl VbaModule {
pub fn is_document(&self) -> bool {
self.kind == VbaModuleKind::Document
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VbaProject {
pub project_id: String,
pub modules: Vec<VbaModule>,
#[serde(default)]
pub raw_donor: Vec<u8>,
#[serde(default)]
pub seed_prefix_bytes: Vec<u8>,
#[serde(default = "default_module_cookie")]
pub seed_module_cookie: u16,
#[serde(default)]
pub protection_lines: Option<String>,
}
impl VbaProject {
pub fn new_empty() -> Self {
VbaProject {
project_id: new_project_guid(),
modules: Vec::new(),
raw_donor: crate::core::vba_synth::synthetic_raw_donor(),
seed_prefix_bytes: crate::core::vba_synth::synthetic_module_prefix(),
seed_module_cookie: default_module_cookie(),
protection_lines: None,
}
}
pub fn find_module(&self, name: &str) -> Option<&VbaModule> {
self.modules
.iter()
.find(|m| m.name.eq_ignore_ascii_case(name))
}
pub fn find_module_mut(&mut self, name: &str) -> Option<&mut VbaModule> {
self.modules
.iter_mut()
.find(|m| m.name.eq_ignore_ascii_case(name))
}
pub fn module_name_taken(&self, name: &str) -> bool {
self.find_module(name).is_some()
}
}
fn new_project_guid() -> String {
let hi = crate::core::engine::generate_unique_id();
let lo = crate::core::engine::generate_unique_id();
format!(
"{{{:08X}-{:04X}-{:04X}-{:04X}-{:012X}}}",
(hi >> 32) as u32,
(hi >> 16) as u16,
hi as u16,
(lo >> 48) as u16,
lo & 0xFFFF_FFFF_FFFF,
)
}
pub fn validate_vba_module_name(name: &str) -> Result<(), String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err("Module name cannot be empty".to_string());
}
if trimmed.chars().count() > 31 {
return Err(format!(
"Module name '{}' exceeds VBA's 31-character limit",
name
));
}
let first = trimmed.chars().next().unwrap();
if !first.is_alphabetic() {
return Err(format!("Module name '{}' must start with a letter", name));
}
if !trimmed.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(format!(
"Module name '{}' may only contain letters, digits, and underscores",
name
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_project() -> VbaProject {
VbaProject {
project_id: "{00000000-0000-0000-0000-000000000000}".to_string(),
modules: vec![
VbaModule {
name: "ThisWorkbook".to_string(),
kind: VbaModuleKind::Document,
source: "Attribute VB_Name = \"ThisWorkbook\"\r\n".to_string(),
bound_sheet_id: None,
prefix_bytes: vec![0xAA; 16],
module_cookie: 0xFFFF,
cached_compressed_source: None,
},
VbaModule {
name: "Module1".to_string(),
kind: VbaModuleKind::Standard,
source: "Attribute VB_Name = \"Module1\"\r\nSub Foo()\r\nEnd Sub\r\n"
.to_string(),
bound_sheet_id: None,
prefix_bytes: vec![0xBB; 16],
module_cookie: 0xFFFF,
cached_compressed_source: None,
},
],
raw_donor: Vec::new(),
seed_prefix_bytes: Vec::new(),
seed_module_cookie: 0xFFFF,
protection_lines: None,
}
}
#[test]
fn validate_name_rules() {
assert!(validate_vba_module_name("Module1").is_ok());
assert!(validate_vba_module_name("_Bad").is_err());
assert!(validate_vba_module_name("1Bad").is_err());
assert!(validate_vba_module_name("").is_err());
assert!(validate_vba_module_name("Has Space").is_err());
assert!(validate_vba_module_name("Has-Dash").is_err());
assert!(validate_vba_module_name(&"A".repeat(32)).is_err());
assert!(validate_vba_module_name(&"A".repeat(31)).is_ok());
}
#[test]
fn find_module_case_insensitive() {
let project = sample_project();
assert!(project.find_module("module1").is_some());
assert!(project.find_module("MODULE1").is_some());
assert!(project.find_module("Module2").is_none());
}
#[test]
fn module_name_taken_case_insensitive() {
let project = sample_project();
assert!(project.module_name_taken("module1"));
assert!(!project.module_name_taken("Module2"));
}
#[test]
fn set_source_leaves_prefix_bytes_untouched() {
let mut project = sample_project();
let original_prefix = project.find_module("Module1").unwrap().prefix_bytes.clone();
project.find_module_mut("Module1").unwrap().source =
"Attribute VB_Name = \"Module1\"\r\nSub Bar()\r\nEnd Sub\r\n".to_string();
assert_eq!(
project.find_module("Module1").unwrap().prefix_bytes,
original_prefix
);
}
}