use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "xllutils")]
#[command(about = "Utilities for Excel XLL development")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
ListExports {
path: PathBuf,
#[arg(short, long)]
ordinals: bool,
#[arg(short = 'A', long)]
addresses: bool,
},
Verify {
path: PathBuf,
#[arg(short, long)]
exports: String,
#[arg(short, long)]
strict: bool,
},
XllInfo {
path: PathBuf,
#[arg(short, long)]
json: bool,
},
#[cfg(windows)]
FindXlls,
Diff {
dll1: PathBuf,
dll2: PathBuf,
},
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::ListExports {
path,
ordinals,
addresses,
} => cmd_list_exports(path, ordinals, addresses),
Commands::Verify {
path,
exports,
strict,
} => cmd_verify(path, exports, strict),
Commands::XllInfo { path, json } => cmd_xll_info(path, json),
#[cfg(windows)]
Commands::FindXlls => cmd_find_xlls(),
Commands::Diff { dll1, dll2 } => cmd_diff(dll1, dll2),
}
}
fn cmd_list_exports(path: PathBuf, ordinals: bool, addresses: bool) -> Result<()> {
let pe = crate::pe::parse_pe_file(&path)
.with_context(|| format!("Failed to parse '{}'", path.display()))?;
println!(
"{} ({}, {} exports)",
path.display(),
pe.architecture(),
pe.exports().len()
);
for exp in pe.exports() {
let name = exp
.name
.as_deref()
.unwrap_or("<ordinal-only>");
let mut parts = vec![name.to_string()];
if ordinals {
parts.push(format!("ordinal={}", exp.ordinal));
}
if addresses {
if let Some(rva) = exp.relative_address {
parts.push(format!("rva=0x{rva:08x}"));
}
}
if exp.is_forwarded {
if let Some(fwd) = &exp.forward_to {
parts.push(format!("-> {fwd}"));
}
}
println!(" {}", parts.join(" "));
}
Ok(())
}
fn cmd_verify(path: PathBuf, exports: String, strict: bool) -> Result<()> {
let expected: Vec<&str> = exports.split(',').map(|s| s.trim()).collect();
let report = crate::exports::verify_dll_exports(&path, &expected)
.with_context(|| format!("Failed to verify '{}'", path.display()))?;
println!("Verification: {}", path.display());
println!(" Architecture: {}", report.architecture);
println!(" Total exports: {}", report.total_exports);
println!(
" Found: {} / {}",
report.found.len(),
expected.len()
);
if !report.missing.is_empty() {
println!(" Missing:");
for name in &report.missing {
println!(" - {name}");
}
}
if !report.unexpected.is_empty() {
println!(" Unexpected:");
for name in &report.unexpected {
println!(" - {name}");
}
}
if !report.complete {
bail!(
"Verification failed: {} missing export(s)",
report.missing.len()
);
}
if strict && !report.unexpected.is_empty() {
bail!(
"Strict verification failed: {} unexpected export(s)",
report.unexpected.len()
);
}
println!(" Result: OK");
Ok(())
}
fn cmd_xll_info(path: PathBuf, json: bool) -> Result<()> {
let info = crate::xll::xll_info(&path)
.with_context(|| format!("Failed to read XLL info from '{}'", path.display()))?;
if json {
#[cfg(feature = "serde")]
{
let output = serde_json::to_string_pretty(&info)
.context("Failed to serialize XLL info to JSON")?;
println!("{output}");
return Ok(());
}
#[cfg(not(feature = "serde"))]
{
bail!("JSON output requires the 'serde' feature. Rebuild with: cargo install xll-utils --features cli,serde");
}
}
println!("XLL: {}", info.name);
println!(" Path: {}", info.path.display());
println!(" Architecture: {}", info.architecture);
println!(" Export count: {}", info.export_count);
println!(" xlAutoOpen: {}", yes_no(info.has_auto_open));
println!(" xlAutoClose: {}", yes_no(info.has_auto_close));
println!(" xlAutoFree12: {}", yes_no(info.has_auto_free));
println!(
" xlAddInManagerInfo12: {}",
yes_no(info.has_addin_manager_info)
);
println!(" Exports:");
for name in &info.exports {
println!(" - {name}");
}
Ok(())
}
#[cfg(windows)]
fn cmd_find_xlls() -> Result<()> {
use crate::registry::{find_registered_xlls_in_hive, RegistryHive};
for hive in [RegistryHive::CurrentUser, RegistryHive::LocalMachine] {
let entries = find_registered_xlls_in_hive(hive)
.with_context(|| format!("Failed to query {hive} registry"))?;
if entries.is_empty() {
continue;
}
println!("{hive}:");
for entry in &entries {
println!(
" [Office {}] {} = {}",
entry.office_version,
entry.value_name,
entry.xll_path.display()
);
}
}
Ok(())
}
fn cmd_diff(dll1: PathBuf, dll2: PathBuf) -> Result<()> {
let pe1 = crate::pe::parse_pe_file(&dll1)
.with_context(|| format!("Failed to parse '{}'", dll1.display()))?;
let pe2 = crate::pe::parse_pe_file(&dll2)
.with_context(|| format!("Failed to parse '{}'", dll2.display()))?;
let names1 = pe1.export_names();
let expected: Vec<&str> = names1.iter().map(|s| s.as_str()).collect();
let diff = crate::exports::export_diff(&expected, pe2.exports());
println!(
"Comparing: {} ({}) vs {} ({})",
dll1.display(),
pe1.architecture(),
dll2.display(),
pe2.architecture()
);
println!(
" {} in first, {} in second",
pe1.exports().len(),
pe2.exports().len()
);
println!(" Common: {}", diff.common.len());
if !diff.missing.is_empty() {
println!(" Only in first ({}):", dll1.display());
for name in &diff.missing {
println!(" - {name}");
}
}
if !diff.extra.is_empty() {
println!(" Only in second ({}):", dll2.display());
for name in &diff.extra {
println!(" - {name}");
}
}
if diff.missing.is_empty() && diff.extra.is_empty() {
println!(" Result: exports are identical");
}
Ok(())
}
fn yes_no(v: bool) -> &'static str {
if v { "yes" } else { "no" }
}