use corundum_framework_platform::package::{self, PackageKind};
use windows::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_SUCCESS, LPARAM, WPARAM};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_QUERY_VALUE, KEY_SET_VALUE, REG_EXPAND_SZ, REG_VALUE_TYPE,
RegCloseKey, RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
};
use windows::Win32::UI::WindowsAndMessaging::{
HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
};
use windows::core::w;
use super::{CliSpec, CliStatus, path_with, path_without};
use crate::error::Error;
fn dir() -> Option<String> {
let layout = package::current()?;
if layout.kind != PackageKind::Windows {
return None;
}
layout.bin_dir.to_str().map(str::to_owned)
}
pub(super) fn status(_spec: &CliSpec) -> CliStatus {
let Some(dir) = dir() else {
return CliStatus::Unsupported;
};
match Environment::open().and_then(|env| env.path()) {
Ok(Some((value, _))) if path_with(&value, &dir).is_none() => CliStatus::Installed,
Ok(_) => CliStatus::Missing,
Err(e) => {
tracing::warn!("could not read the user Path: {e}");
CliStatus::Missing
}
}
}
pub(super) fn install(_spec: &CliSpec) -> Result<(), Error> {
let dir = dir().ok_or(Error::NotPackaged)?;
let env = Environment::open()?;
let (value, ty) = env.path()?.unwrap_or((String::new(), REG_EXPAND_SZ));
if let Some(value) = path_with(&value, &dir) {
env.set_path(&value, ty)?;
announce();
}
Ok(())
}
pub(super) fn uninstall(_spec: &CliSpec) -> Result<(), Error> {
let dir = dir().ok_or(Error::NotPackaged)?;
let env = Environment::open()?;
let Some((value, ty)) = env.path()? else {
return Ok(());
};
if let Some(value) = path_without(&value, &dir) {
env.set_path(&value, ty)?;
announce();
}
Ok(())
}
struct Environment(HKEY);
impl Environment {
fn open() -> Result<Self, Error> {
let mut key = HKEY::default();
let err = unsafe {
RegOpenKeyExW(
HKEY_CURRENT_USER,
w!("Environment"),
None,
KEY_QUERY_VALUE | KEY_SET_VALUE,
&mut key,
)
};
if err != ERROR_SUCCESS {
return Err(registry_error("open HKCU\\Environment", err.0));
}
Ok(Self(key))
}
fn path(&self) -> Result<Option<(String, REG_VALUE_TYPE)>, Error> {
let mut ty = REG_VALUE_TYPE::default();
let mut size = 0u32;
let err = unsafe {
RegQueryValueExW(
self.0,
w!("Path"),
None,
Some(&mut ty),
None,
Some(&mut size),
)
};
if err == ERROR_FILE_NOT_FOUND {
return Ok(None);
}
if err != ERROR_SUCCESS {
return Err(registry_error("read the user Path", err.0));
}
let mut data = vec![0u16; (size as usize).div_ceil(2)];
let err = unsafe {
RegQueryValueExW(
self.0,
w!("Path"),
None,
Some(&mut ty),
Some(data.as_mut_ptr().cast()),
Some(&mut size),
)
};
if err != ERROR_SUCCESS {
return Err(registry_error("read the user Path", err.0));
}
data.truncate((size as usize) / 2);
while data.last() == Some(&0) {
data.pop();
}
let value = String::from_utf16(&data)
.map_err(|_| Error::CliPath("the user Path is not valid UTF-16".to_owned()))?;
Ok(Some((value, ty)))
}
fn set_path(&self, value: &str, ty: REG_VALUE_TYPE) -> Result<(), Error> {
let bytes: Vec<u8> = value
.encode_utf16()
.chain([0])
.flat_map(u16::to_le_bytes)
.collect();
let err = unsafe { RegSetValueExW(self.0, w!("Path"), None, ty, Some(&bytes)) };
if err != ERROR_SUCCESS {
return Err(registry_error("write the user Path", err.0));
}
Ok(())
}
}
impl Drop for Environment {
fn drop(&mut self) {
unsafe {
let _ = RegCloseKey(self.0);
}
}
}
fn announce() {
unsafe {
SendMessageTimeoutW(
HWND_BROADCAST,
WM_SETTINGCHANGE,
WPARAM(0),
LPARAM(w!("Environment").as_ptr() as isize),
SMTO_ABORTIFHUNG,
5000,
None,
);
}
}
fn registry_error(what: &str, code: u32) -> Error {
Error::CliPath(format!(
"could not {what}: {}",
std::io::Error::from_raw_os_error(code as i32)
))
}