pub mod properties;
use std::fmt::Display;
use crate::{
errors::{ClassError, ParserContext},
files::{
class::properties::{
ClassHeader, ClassProperties, DataBindingBehavior, DataSourceBehavior, FileUsage,
MtsStatus, Persistence,
},
common::{extract_attributes, extract_version},
},
io::SourceFile,
lexer::tokenize,
parsers::{
cst::{parse, serialize_cst},
SyntaxKind,
},
ConcreteSyntaxTree, ParseResult,
};
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub struct ClassFile {
pub header: ClassHeader,
#[serde(serialize_with = "serialize_cst")]
pub cst: ConcreteSyntaxTree,
}
impl Display for ClassFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "VB6 Class File: {}", self.header.attributes.name)
}
}
impl ClassFile {
#[must_use]
pub fn parse(source_file: &SourceFile) -> ParseResult<'_, Self> {
let mut input = source_file.source_stream();
let mut ctx = ParserContext::new(input.file_name(), input.contents);
let token_stream_result = tokenize(&mut input);
let (token_stream_opt, token_failures) = token_stream_result.unpack();
ctx.extend_errors(token_failures);
let Some(token_stream) = token_stream_opt else {
return ParseResult::new(None, ctx.into_errors());
};
let cst = parse(token_stream);
let Some(version) = extract_version(&cst) else {
ctx.error(input.span_here(), ClassError::VersionKeywordMissing);
return ParseResult::new(None, ctx.into_errors());
};
let properties = extract_properties(&cst);
let attributes = extract_attributes(&cst);
let header = ClassHeader {
version,
properties,
attributes,
};
let filtered_cst = cst.without_kinds(&[
SyntaxKind::VersionStatement,
SyntaxKind::PropertiesBlock,
SyntaxKind::AttributeStatement,
]);
ParseResult::new(
Some(ClassFile {
header,
cst: filtered_cst,
}),
ctx.into_errors(),
)
}
}
fn extract_properties(cst: &ConcreteSyntaxTree) -> ClassProperties {
let mut multi_use = FileUsage::MultiUse;
let mut persistable = Persistence::NotPersistable;
let mut data_binding_behavior = DataBindingBehavior::None;
let mut data_source_behavior = DataSourceBehavior::None;
let mut mts_transaction_mode = MtsStatus::NotAnMTSObject;
let properties_blocks: Vec<_> = cst
.children()
.into_iter()
.filter(|c| c.kind() == SyntaxKind::PropertiesBlock)
.collect();
if properties_blocks.is_empty() {
return ClassProperties::default();
}
let properties_block = &properties_blocks[0];
let property_nodes: Vec<_> = properties_block
.children()
.iter()
.filter(|c| c.kind() == SyntaxKind::Property)
.collect();
for prop_node in property_nodes {
let mut key = String::new();
let mut value = String::new();
let mut found_equals = false;
for child in prop_node.children() {
if !child.is_token() {
continue;
}
match child.kind() {
SyntaxKind::PropertyKey => {
if let Some(first_child) = child.children().first() {
key = first_child.text().trim().to_string();
}
}
SyntaxKind::EqualityOperator => {
found_equals = true;
}
SyntaxKind::PropertyValue => {
if found_equals {
for val_child in child.children() {
if val_child.is_token() {
match val_child.kind() {
SyntaxKind::IntegerLiteral | SyntaxKind::LongLiteral => {
value.push_str(val_child.text().trim());
}
SyntaxKind::SubtractionOperator => {
value.push('-');
}
_ => {}
}
}
}
}
}
_ => {}
}
}
if !key.is_empty() && !value.is_empty() {
match key.as_str() {
"MultiUse" => {
multi_use = if value == "-1" {
FileUsage::MultiUse
} else {
FileUsage::SingleUse
};
}
"Persistable" => {
persistable = if value == "-1" {
Persistence::Persistable
} else {
Persistence::NotPersistable
};
}
"DataBindingBehavior" => {
data_binding_behavior = match value.as_str() {
"1" => DataBindingBehavior::Simple,
"2" => DataBindingBehavior::Complex,
_ => DataBindingBehavior::None,
};
}
"DataSourceBehavior" => {
data_source_behavior = match value.as_str() {
"1" => DataSourceBehavior::DataSource,
_ => DataSourceBehavior::None,
};
}
"MTSTransactionMode" => {
mts_transaction_mode = match value.as_str() {
"1" => MtsStatus::NoTransactions,
"2" => MtsStatus::RequiresTransaction,
"3" => MtsStatus::UsesTransaction,
"4" => MtsStatus::RequiresNewTransaction,
_ => MtsStatus::NotAnMTSObject,
};
}
_ => {}
}
}
}
ClassProperties {
multi_use,
persistable,
data_binding_behavior,
data_source_behavior,
mts_transaction_mode,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::SourceFile;
#[test]
fn class_file_valid() {
let class_bytes = r#"VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = \"Something\"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Attribute VB_Ext_KEY = \"SavedWithClassBuilder6\" ,\"Yes\"
Attribute VB_Ext_KEY = \"Saved\" ,\"False\"
Option Explicit
"#;
let result = SourceFile::decode_with_replacement("test.cls", class_bytes.as_bytes());
let source_file = match result {
Ok(source_file) => source_file,
Err(e) => panic!("Failed to decode source file 'test.cls': {e:?}"),
};
let result = ClassFile::parse(&source_file);
if result.has_failures() {
for failure in result.failures() {
failure.print();
}
panic!("Class parse had failures");
}
assert!(result.has_result());
}
#[test]
fn class_file_invalid() {
let class_bytes = b"VERSION 1.0 CLASS\r
BEGIN\r
MultiUse = -1 'True\r
Persistable = 0 'NotPersistable\r
DataBindingBehavior = 0 'vbNone\r
DataSourceBehavior = 0 'vbNone\r
MTSTransactionMode = 0 'NotAnMTSObject\r
END\r
Attribute VB_Name = \"Something\"\r
Attribute VB_GlobalNameSpace = False\r
Attribute VB_Creatable = True\r
Attribute VB_PredeclaredId = False\r
Attribute VB_Exposed = False\r
Attribute VB_Description = \"Description text\"\r
\r
Option Explicit\r";
let result = SourceFile::decode_with_replacement("test.cls", class_bytes);
let source_file = match result {
Ok(source_file) => source_file,
Err(e) => panic!("Failed to decode source file 'test.cls': {e:?}"),
};
let result = ClassFile::parse(&source_file);
assert!(result.has_failures());
}
#[test]
fn class_header_valid() {
let input = b"VERSION 1.0 CLASS\r
BEGIN\r
MultiUse = -1 'True\r
Persistable = 0 'NotPersistable\r
DataBindingBehavior = 0 'vbNone\r
DataSourceBehavior = 0 'vbNone\r
MTSTransactionMode = 0 'NotAnMTSObject\r
END\r
Attribute VB_Name = \"Something\"\r
Attribute VB_GlobalNameSpace = False\r
Attribute VB_Creatable = True\r
Attribute VB_PredeclaredId = False\r
Attribute VB_Exposed = False";
let sourcefile = SourceFile::decode_with_replacement("test.cls", input)
.expect("Unabled to decode source file with replacement.");
let result = ClassFile::parse(&sourcefile);
assert!(result.has_result());
}
#[test]
fn version_valid() {
let class_bytes = b"VERSION 1.0 CLASS\r\n";
let result = SourceFile::decode_with_replacement("test.cls", class_bytes);
let source_file = match result {
Ok(source_file) => source_file,
Err(e) => panic!("Failed to decode source file 'test.cls': {e:?}"),
};
let mut source_stream = source_file.source_stream();
let (token_stream_opt, _failures) = tokenize(&mut source_stream).unpack();
let token_stream = token_stream_opt.expect("Failed to tokenize the input.");
let cst = parse(token_stream);
let version = extract_version(&cst);
assert!(version.is_some());
let version = version.expect("Version should be present if it's Some.");
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
}
#[test]
fn version_invalid() {
let class_bytes = b"VERION 1.0 CLASS";
let result = SourceFile::decode_with_replacement("test.cls", class_bytes);
let source_file = match result {
Ok(source_file) => source_file,
Err(e) => panic!("Failed to decode source file 'test.cls': {e:?}"),
};
let mut source_stream = source_file.source_stream();
let (token_stream_opt, _failures) = tokenize(&mut source_stream).unpack();
let token_stream = token_stream_opt.expect("Failed to tokenize the input.");
let cst = parse(token_stream);
let version = extract_version(&cst);
assert!(version.is_none());
}
}