use std::io::{Cursor, Read};
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::error::{Error, Result};
use crate::msodde::field_parser::DdeField;
pub fn process_xlsx(data: &[u8]) -> Result<Vec<DdeField>> {
let cursor = Cursor::new(data);
let mut archive = zip::ZipArchive::new(cursor)
.map_err(|e| Error::InvalidOoxml(format!("Invalid ZIP: {e}")))?;
let mut fields = Vec::new();
let ext_link_parts: Vec<String> = (0..archive.len())
.filter_map(|i| {
archive.by_index(i).ok().and_then(|e| {
let name = e.name().to_string();
let lower = name.to_lowercase();
if lower.contains("externallinks/externallink") && lower.ends_with(".xml") {
Some(name)
} else {
None
}
})
})
.collect();
for part_name in &ext_link_parts {
let mut xml_data = Vec::new();
if let Ok(mut entry) = archive.by_name(part_name) {
entry.read_to_end(&mut xml_data)?;
}
if xml_data.is_empty() {
continue;
}
let part_fields = extract_dde_links(&xml_data, part_name)?;
fields.extend(part_fields);
}
Ok(fields)
}
fn extract_dde_links(xml_data: &[u8], source_part: &str) -> Result<Vec<DdeField>> {
let mut reader = Reader::from_reader(Cursor::new(xml_data));
reader.config_mut().trim_text(true);
let mut fields = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let local_name =
String::from_utf8_lossy(e.local_name().as_ref()).to_string();
if local_name == "ddeLink" {
let mut service = String::new();
let mut topic = String::new();
for attr in e.attributes().flatten() {
let key = String::from_utf8_lossy(attr.key.local_name().as_ref())
.to_string();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key.as_str() {
"ddeService" => service = value,
"ddeTopic" => topic = value,
_ => {}
}
}
if !service.is_empty() {
fields.push(DdeField {
field_instruction: format!(
"ddeLink service={} topic={} ({})",
service, topic, source_part
),
command: format!("{} {}", service, topic),
source: service,
quote_decoded: None,
});
}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(Error::XmlParsing(format!(
"Error parsing externalLink XML: {e}"
)));
}
_ => {}
}
buf.clear();
}
Ok(fields)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_dde_link() {
let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<ddeLink ddeService="cmd" ddeTopic="/c calc"/>
</externalLink>"#;
let fields = extract_dde_links(xml, "test.xml").unwrap();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].source, "cmd");
assert!(fields[0].command.contains("/c calc"));
}
#[test]
fn test_extract_no_dde_link() {
let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<externalBook/>
</externalLink>"#;
let fields = extract_dde_links(xml, "test.xml").unwrap();
assert!(fields.is_empty());
}
#[test]
fn test_extract_multiple_dde_links() {
let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<ddeLink ddeService="Excel" ddeTopic="Sheet1"/>
<ddeLink ddeService="cmd" ddeTopic="/c whoami"/>
</externalLink>"#;
let fields = extract_dde_links(xml, "test.xml").unwrap();
assert_eq!(fields.len(), 2);
}
#[test]
fn test_extract_empty_service() {
let xml = br#"<?xml version="1.0"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<ddeLink ddeTopic="test"/>
</externalLink>"#;
let fields = extract_dde_links(xml, "test.xml").unwrap();
assert!(fields.is_empty(), "Empty service should be skipped");
}
}