use crate::error::Result;
use crate::msodde::field_parser::{self, DdeField};
use crate::ole::container::OleFile;
const FIELD_BEGIN: u8 = 0x13;
const FIELD_SEP: u8 = 0x14;
const FIELD_END: u8 = 0x15;
pub fn process_doc(data: &[u8]) -> Result<Vec<DdeField>> {
let mut ole = OleFile::from_bytes(data)?;
let streams = ole.list_streams();
let mut fields = Vec::new();
let target_streams: Vec<_> = streams
.iter()
.filter(|s| {
let lower = s.to_lowercase();
lower.contains("worddocument")
|| lower.contains("1table")
|| lower.contains("0table")
|| lower.contains("data")
})
.cloned()
.collect();
for stream_path in &target_streams {
if let Ok(stream_data) = ole.open_stream(stream_path) {
let extracted = extract_fields_from_binary(&stream_data);
fields.extend(extracted);
}
}
Ok(fields)
}
fn extract_fields_from_binary(data: &[u8]) -> Vec<DdeField> {
let mut fields = Vec::new();
let mut i = 0;
let len = data.len();
while i < len {
if data[i] == FIELD_BEGIN {
let mut instruction = String::new();
i += 1;
while i < len && data[i] != FIELD_SEP && data[i] != FIELD_END {
if data[i] == FIELD_BEGIN {
let mut depth = 1;
i += 1;
while i < len && depth > 0 {
if data[i] == FIELD_BEGIN {
depth += 1;
} else if data[i] == FIELD_END {
depth -= 1;
}
i += 1;
}
continue;
}
if data[i].is_ascii_graphic() || data[i] == b' ' {
instruction.push(data[i] as char);
}
i += 1;
}
while i < len && data[i] != FIELD_END {
i += 1;
}
let instruction = instruction.trim().to_string();
if field_parser::is_dde_field(&instruction)
&& let Some(dde) = field_parser::parse_dde_field(&instruction) {
fields.push(dde);
}
if let Some(decoded) = field_parser::decode_quote_field(&instruction)
&& field_parser::is_dde_field(&decoded)
&& let Some(mut dde) = field_parser::parse_dde_field(&decoded) {
dde.quote_decoded = Some(decoded);
fields.push(dde);
}
}
i += 1;
}
fields
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_fields_with_dde() {
let mut data = Vec::new();
data.push(FIELD_BEGIN);
data.extend_from_slice(b" DDEAUTO cmd.exe /c calc ");
data.push(FIELD_SEP);
data.extend_from_slice(b"result");
data.push(FIELD_END);
let fields = extract_fields_from_binary(&data);
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].source, "cmd.exe");
}
#[test]
fn test_extract_fields_no_dde() {
let mut data = Vec::new();
data.push(FIELD_BEGIN);
data.extend_from_slice(b" DATE \\@ \"yyyy-MM-dd\" ");
data.push(FIELD_END);
let fields = extract_fields_from_binary(&data);
assert!(fields.is_empty());
}
#[test]
fn test_extract_fields_empty() {
let fields = extract_fields_from_binary(&[]);
assert!(fields.is_empty());
}
#[test]
fn test_extract_fields_nested() {
let mut data = Vec::new();
data.push(FIELD_BEGIN);
data.push(FIELD_BEGIN);
data.extend_from_slice(b"PAGE");
data.push(FIELD_END);
data.extend_from_slice(b" DDEAUTO cmd.exe /c whoami ");
data.push(FIELD_END);
let fields = extract_fields_from_binary(&data);
assert_eq!(fields.len(), 1);
}
#[test]
fn test_extract_quote_encoded_non_dde() {
let mut data = Vec::new();
data.push(FIELD_BEGIN);
data.extend_from_slice(b" QUOTE 68 65 84 69 ");
data.push(FIELD_END);
let fields = extract_fields_from_binary(&data);
assert!(fields.is_empty(), "DATE is a safe field, should not trigger DDE");
}
#[test]
fn test_extract_quote_encoded_dde() {
let mut data = Vec::new();
data.push(FIELD_BEGIN);
data.extend_from_slice(b" QUOTE 68 68 69 ");
data.push(FIELD_END);
let fields = extract_fields_from_binary(&data);
assert_eq!(fields.len(), 1, "QUOTE encoding 'DDE' should be detected");
}
}