xll-utils 0.1.0

PE/COFF parsing and export verification utilities for Excel XLL development
Documentation
//! Command-line interface for xll-utils.
//!
//! Provides subcommands for listing exports, verifying DLLs,
//! extracting XLL metadata, and more. Requires the `cli` feature.

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 {
    /// List all exports from a DLL/XLL file
    ListExports {
        /// Path to the DLL/XLL file
        path: PathBuf,
        /// Show ordinals
        #[arg(short, long)]
        ordinals: bool,
        /// Show relative addresses
        #[arg(short = 'A', long)]
        addresses: bool,
    },
    /// Verify DLL exports against an expected list
    Verify {
        /// Path to the DLL/XLL file
        path: PathBuf,
        /// Expected export names (comma-separated)
        #[arg(short, long)]
        exports: String,
        /// Fail on any unexpected exports
        #[arg(short, long)]
        strict: bool,
    },
    /// Show XLL file information
    XllInfo {
        /// Path to the XLL file
        path: PathBuf,
        /// Output as JSON (requires serde feature)
        #[arg(short, long)]
        json: bool,
    },
    /// Find registered XLLs in Windows Registry
    #[cfg(windows)]
    FindXlls,
    /// Compare exports between two DLL/XLL files
    Diff {
        /// First DLL path
        dll1: PathBuf,
        /// Second DLL path
        dll2: PathBuf,
    },
}

/// Run the CLI application.
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" }
}