use std::path::PathBuf;
use windows::core::HSTRING;
use windows::core::PWSTR;
use windows::Win32::Foundation::{
ERROR_FILE_NOT_FOUND, ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS, WIN32_ERROR,
};
use windows::Win32::System::Registry::{
RegCloseKey, RegDeleteValueW, RegEnumValueW, RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE, KEY_READ, KEY_WRITE, REG_SZ,
REG_SAM_FLAGS, REG_VALUE_TYPE,
};
use crate::error::{Error, Result};
pub const OFFICE_VERSIONS: &[&str] = &["16.0", "15.0", "14.0"];
const OPTIONS_SUBKEY_TEMPLATE: &str = r"Software\Microsoft\Office\{}\Excel\Options";
const INSTALL_ROOT_TEMPLATE: &str = r"Software\Microsoft\Office\{}\Excel\InstallRoot";
struct RegKey(HKEY);
impl Drop for RegKey {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe {
let _ = RegCloseKey(self.0);
}
}
}
}
fn options_subkey(version: &str) -> String {
OPTIONS_SUBKEY_TEMPLATE.replace("{}", version)
}
fn install_root_subkey(version: &str) -> String {
INSTALL_ROOT_TEMPLATE.replace("{}", version)
}
fn win_err(code: WIN32_ERROR, context: &str) -> Error {
Error::Registry(format!("{context} (error code {0})", code.0))
}
fn open_key(root: HKEY, subkey: &str, access: u32) -> Result<Option<RegKey>> {
let subkey_h = HSTRING::from(subkey);
let mut result_key = HKEY::default();
let status = unsafe {
RegOpenKeyExW(
root,
&subkey_h,
0, REG_SAM_FLAGS(access),
&mut result_key,
)
};
if status == ERROR_SUCCESS {
Ok(Some(RegKey(result_key)))
} else if status == ERROR_FILE_NOT_FOUND {
Ok(None)
} else {
Err(win_err(status, &format!("RegOpenKeyExW({subkey})")))
}
}
fn query_string(key: HKEY, value_name: &str) -> Result<Option<String>> {
let name_h = HSTRING::from(value_name);
let mut value_type = REG_VALUE_TYPE::default();
let mut size_bytes: u32 = 0;
let status = unsafe {
RegQueryValueExW(
key,
&name_h,
None,
Some(&mut value_type),
None,
Some(&mut size_bytes),
)
};
if status == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
if status != ERROR_SUCCESS && status != ERROR_MORE_DATA {
return Err(win_err(status, &format!("RegQueryValueExW({value_name}) size")));
}
if value_type != REG_SZ {
return Err(Error::Registry(format!(
"Expected REG_SZ for '{value_name}', got type {}",
value_type.0
)));
}
let len_u16 = (size_bytes as usize) / 2;
let mut buf: Vec<u16> = vec![0u16; len_u16];
let status = unsafe {
RegQueryValueExW(
key,
&name_h,
None,
Some(&mut value_type),
Some(buf.as_mut_ptr().cast()),
Some(&mut size_bytes),
)
};
if status != ERROR_SUCCESS {
return Err(win_err(status, &format!("RegQueryValueExW({value_name}) data")));
}
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
Ok(Some(String::from_utf16_lossy(&buf[..end])))
}
fn set_string(key: HKEY, value_name: &str, data: &str) -> Result<()> {
let name_h = HSTRING::from(value_name);
let data_h = HSTRING::from(data);
let wide: &[u16] = data_h.as_wide();
let mut wide_with_null: Vec<u16> = wide.to_vec();
wide_with_null.push(0);
let byte_len = wide_with_null.len() * 2;
let bytes: &[u8] =
unsafe { std::slice::from_raw_parts(wide_with_null.as_ptr().cast(), byte_len) };
let status = unsafe { RegSetValueExW(key, &name_h, 0, REG_SZ, Some(bytes)) };
if status != ERROR_SUCCESS {
return Err(win_err(status, &format!("RegSetValueExW({value_name})")));
}
Ok(())
}
fn delete_value(key: HKEY, value_name: &str) -> Result<bool> {
let name_h = HSTRING::from(value_name);
let status = unsafe { RegDeleteValueW(key, &name_h) };
if status == ERROR_SUCCESS {
Ok(true)
} else if status == ERROR_FILE_NOT_FOUND {
Ok(false)
} else {
Err(win_err(status, &format!("RegDeleteValueW({value_name})")))
}
}
fn enumerate_string_values(key: HKEY) -> Result<Vec<(String, String)>> {
let mut results = Vec::new();
let mut index: u32 = 0;
let mut name_buf: Vec<u16> = vec![0u16; 256];
let mut data_buf: Vec<u16> = vec![0u16; 512];
loop {
let mut name_len = name_buf.len() as u32; let mut data_len = (data_buf.len() * 2) as u32; let mut value_type: u32 = 0;
let status = unsafe {
RegEnumValueW(
key,
index,
PWSTR(name_buf.as_mut_ptr()),
&mut name_len,
None,
Some(&mut value_type),
Some(data_buf.as_mut_ptr().cast()),
Some(&mut data_len),
)
};
if status == ERROR_NO_MORE_ITEMS {
break;
}
if status == ERROR_MORE_DATA {
name_buf.resize(name_buf.len() * 2, 0);
data_buf.resize(data_buf.len() * 2, 0);
continue;
}
if status != ERROR_SUCCESS {
return Err(win_err(status, "RegEnumValueW"));
}
if REG_VALUE_TYPE(value_type) == REG_SZ {
let name = String::from_utf16_lossy(&name_buf[..name_len as usize]);
let data_u16_len = (data_len as usize) / 2;
let end = data_buf[..data_u16_len]
.iter()
.position(|&c| c == 0)
.unwrap_or(data_u16_len);
let data = String::from_utf16_lossy(&data_buf[..end]);
results.push((name, data));
}
index += 1;
}
Ok(results)
}
fn parse_open_value(value: &str) -> Option<PathBuf> {
let trimmed = value.trim();
let path_str = if let Some(rest) = trimmed.strip_prefix("/R") {
rest.trim().trim_matches('"')
} else {
trimmed.trim_matches('"')
};
if path_str.is_empty() {
None
} else {
Some(PathBuf::from(path_str))
}
}
fn is_open_value_name(name: &str) -> bool {
let upper = name.to_uppercase();
upper == "OPEN" || (upper.starts_with("OPEN") && upper[4..].parse::<u32>().is_ok())
}
fn open_value_name(index: usize) -> String {
if index == 0 {
"OPEN".to_string()
} else {
format!("OPEN{index}")
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RegisteredXll {
pub hive: RegistryHive,
pub office_version: String,
pub value_name: String,
pub raw_value: String,
pub xll_path: PathBuf,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RegistryHive {
CurrentUser,
LocalMachine,
}
impl std::fmt::Display for RegistryHive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistryHive::CurrentUser => write!(f, "HKCU"),
RegistryHive::LocalMachine => write!(f, "HKLM"),
}
}
}
impl RegistryHive {
fn hkey(self) -> HKEY {
match self {
RegistryHive::CurrentUser => HKEY_CURRENT_USER,
RegistryHive::LocalMachine => HKEY_LOCAL_MACHINE,
}
}
}
pub fn detect_office_versions() -> Result<Vec<(RegistryHive, String)>> {
let mut found = Vec::new();
for &version in OFFICE_VERSIONS {
let subkey = options_subkey(version);
for &hive in &[RegistryHive::CurrentUser, RegistryHive::LocalMachine] {
if let Some(_key) = open_key(hive.hkey(), &subkey, KEY_QUERY_VALUE.0)? {
found.push((hive, version.to_string()));
}
}
}
Ok(found)
}
pub fn find_registered_xlls() -> Result<Vec<RegisteredXll>> {
find_registered_xlls_in_hive(RegistryHive::CurrentUser)
}
pub fn find_registered_xlls_in_hive(hive: RegistryHive) -> Result<Vec<RegisteredXll>> {
let mut results = Vec::new();
for &version in OFFICE_VERSIONS {
let subkey = options_subkey(version);
let key = match open_key(hive.hkey(), &subkey, KEY_READ.0)? {
Some(k) => k,
None => continue,
};
let values = enumerate_string_values(key.0)?;
for (name, data) in values {
if is_open_value_name(&name) {
if let Some(path) = parse_open_value(&data) {
results.push(RegisteredXll {
hive,
office_version: version.to_string(),
value_name: name,
raw_value: data,
xll_path: path,
});
}
}
}
}
Ok(results)
}
pub fn find_registered_xlls_for_version(
hive: RegistryHive,
version: &str,
) -> Result<Vec<RegisteredXll>> {
let subkey = options_subkey(version);
let key = match open_key(hive.hkey(), &subkey, KEY_READ.0)? {
Some(k) => k,
None => return Ok(Vec::new()),
};
let values = enumerate_string_values(key.0)?;
let mut results = Vec::new();
for (name, data) in values {
let upper = name.to_uppercase();
if upper == "OPEN" || (upper.starts_with("OPEN") && upper[4..].parse::<u32>().is_ok()) {
if let Some(path) = parse_open_value(&data) {
results.push(RegisteredXll {
hive,
office_version: version.to_string(),
value_name: name,
raw_value: data,
xll_path: path,
});
}
}
}
Ok(results)
}
pub fn register_xll(version: &str, xll_path: &std::path::Path) -> Result<String> {
let subkey = options_subkey(version);
let key = open_key(HKEY_CURRENT_USER, &subkey, KEY_READ.0 | KEY_WRITE.0)?
.ok_or_else(|| {
Error::Registry(format!(
"Office {version} Options key not found: {subkey}"
))
})?;
let existing = enumerate_string_values(key.0)?;
let target_lower = xll_path.to_string_lossy().to_lowercase();
for (name, data) in &existing {
if is_open_value_name(name) {
if let Some(path) = parse_open_value(data) {
if path.to_string_lossy().to_lowercase() == target_lower {
return Ok(name.clone());
}
}
}
}
let mut used_indices: Vec<usize> = Vec::new();
for (name, _) in &existing {
let upper = name.to_uppercase();
if upper == "OPEN" {
used_indices.push(0);
} else if let Some(suffix) = upper.strip_prefix("OPEN") {
if let Ok(n) = suffix.parse::<usize>() {
used_indices.push(n);
}
}
}
let next_index = if used_indices.is_empty() {
0
} else {
used_indices.sort_unstable();
let mut idx = 0usize;
for &used in &used_indices {
if used != idx {
break;
}
idx = used + 1;
}
idx
};
let value_name = open_value_name(next_index);
let path_str = xll_path.to_string_lossy();
let value_data = format!("/R \"{path_str}\"");
set_string(key.0, &value_name, &value_data)?;
Ok(value_name)
}
pub fn unregister_xll(version: &str, xll_path: &std::path::Path) -> Result<bool> {
let subkey = options_subkey(version);
let key = match open_key(HKEY_CURRENT_USER, &subkey, KEY_READ.0 | KEY_WRITE.0)? {
Some(k) => k,
None => return Ok(false),
};
let values = enumerate_string_values(key.0)?;
let target = xll_path.to_string_lossy().to_lowercase();
let mut open_entries: Vec<(String, String)> = Vec::new();
let mut found_index: Option<usize> = None;
for (name, data) in &values {
if is_open_value_name(name) {
if found_index.is_none() {
if let Some(path) = parse_open_value(data) {
if path.to_string_lossy().to_lowercase() == target {
found_index = Some(open_entries.len());
}
}
}
open_entries.push((name.clone(), data.clone()));
}
}
let remove_idx = match found_index {
Some(idx) => idx,
None => return Ok(false),
};
for (name, _) in &open_entries {
delete_value(key.0, name)?;
}
open_entries.remove(remove_idx);
for (i, (_, data)) in open_entries.iter().enumerate() {
set_string(key.0, &open_value_name(i), data)?;
}
Ok(true)
}
pub fn query_excel_install_path(version: &str) -> Result<Option<PathBuf>> {
let subkey = install_root_subkey(version);
if let Some(key) = open_key(HKEY_LOCAL_MACHINE, &subkey, KEY_QUERY_VALUE.0)? {
if let Some(path_str) = query_string(key.0, "Path")? {
return Ok(Some(PathBuf::from(path_str)));
}
}
if let Some(key) = open_key(HKEY_CURRENT_USER, &subkey, KEY_QUERY_VALUE.0)? {
if let Some(path_str) = query_string(key.0, "Path")? {
return Ok(Some(PathBuf::from(path_str)));
}
}
Ok(None)
}
pub fn detect_excel_path() -> Result<Option<(String, PathBuf)>> {
for &version in OFFICE_VERSIONS {
if let Some(path) = query_excel_install_path(version)? {
return Ok(Some((version.to_string(), path)));
}
}
Ok(None)
}
pub fn is_xll_registered(
hive: RegistryHive,
version: &str,
xll_path: &std::path::Path,
) -> Result<bool> {
let entries = find_registered_xlls_for_version(hive, version)?;
let target = xll_path.to_string_lossy().to_lowercase();
Ok(entries
.iter()
.any(|e| e.xll_path.to_string_lossy().to_lowercase() == target))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_options_subkey() {
assert_eq!(
options_subkey("16.0"),
r"Software\Microsoft\Office\16.0\Excel\Options"
);
assert_eq!(
options_subkey("14.0"),
r"Software\Microsoft\Office\14.0\Excel\Options"
);
}
#[test]
fn test_install_root_subkey() {
assert_eq!(
install_root_subkey("16.0"),
r"Software\Microsoft\Office\16.0\Excel\InstallRoot"
);
}
#[test]
fn test_parse_open_value_standard() {
let v = parse_open_value(r#"/R "C:\addins\my.xll""#);
assert_eq!(v, Some(PathBuf::from(r"C:\addins\my.xll")));
}
#[test]
fn test_parse_open_value_no_quotes() {
let v = parse_open_value(r"/R C:\addins\my.xll");
assert_eq!(v, Some(PathBuf::from(r"C:\addins\my.xll")));
}
#[test]
fn test_parse_open_value_bare_path() {
let v = parse_open_value(r#""C:\addins\my.xll""#);
assert_eq!(v, Some(PathBuf::from(r"C:\addins\my.xll")));
}
#[test]
fn test_parse_open_value_empty() {
assert_eq!(parse_open_value(""), None);
assert_eq!(parse_open_value("/R "), None);
assert_eq!(parse_open_value(r#"/R """#), None);
}
#[test]
fn test_open_value_name() {
assert_eq!(open_value_name(0), "OPEN");
assert_eq!(open_value_name(1), "OPEN1");
assert_eq!(open_value_name(2), "OPEN2");
assert_eq!(open_value_name(10), "OPEN10");
}
#[test]
fn test_registry_hive_display() {
assert_eq!(format!("{}", RegistryHive::CurrentUser), "HKCU");
assert_eq!(format!("{}", RegistryHive::LocalMachine), "HKLM");
}
#[test]
fn test_is_open_value_name() {
assert!(is_open_value_name("OPEN"));
assert!(is_open_value_name("OPEN1"));
assert!(is_open_value_name("OPEN2"));
assert!(is_open_value_name("OPEN10"));
assert!(is_open_value_name("open")); assert!(is_open_value_name("Open1"));
assert!(!is_open_value_name("OPENED"));
assert!(!is_open_value_name("OPENx"));
assert!(!is_open_value_name(""));
assert!(!is_open_value_name("CLOSE"));
}
}