use miette::{IntoDiagnostic, Result, miette};
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::path::PathBuf;
use tracing::debug;
use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE, RegType};
use winreg::{RegKey, RegValue};
pub(crate) fn do_add_to_path(dirs: Vec<PathBuf>) -> Result<bool> {
let current_path = get_path_var()?;
let dirs: Vec<Vec<u16>> = dirs
.iter()
.map(|dir| OsString::from(dir).encode_wide().collect::<Vec<u16>>())
.filter(|dir| !path_contains(¤t_path, dir))
.collect();
let new_path = dirs
.iter()
.chain([¤t_path])
.fold(vec![], |acc, path| path_join(&acc, path));
if current_path == new_path {
debug!("System PATH already contains the new entries, leaving it untouched");
Ok(false)
} else {
debug!("Updating system PATH");
set_path_var(&new_path)?;
Ok(true)
}
}
fn get_path_var() -> Result<Vec<u16>> {
use std::io::{Error, ErrorKind};
RegKey::predef(HKEY_CURRENT_USER)
.open_subkey_with_flags("Environment", KEY_READ)
.and_then(|env| env.get_raw_value("PATH"))
.and_then(|value| {
winreg_ext::from_winreg_value(&value).ok_or(Error::new(
ErrorKind::InvalidData,
"The registry key `HKEY_CURRENT_USER\\Environment\\PATH` is not a string",
))
})
.or_else(|err| {
if err.kind() == ErrorKind::NotFound {
Ok(vec![])
} else {
Err(err)
}
})
.into_diagnostic()
}
fn set_path_var(new_path: &[u16]) -> Result<()> {
if new_path.is_empty() {
return Err(miette!(
"New system path is empty, this shouldn't be possible!"
));
}
RegKey::predef(HKEY_CURRENT_USER)
.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
.and_then(|environment| {
let reg_value = RegValue {
bytes: winreg_ext::to_winreg_bytes(new_path).into(),
vtype: RegType::REG_EXPAND_SZ,
};
environment.set_raw_value("PATH", ®_value)
})
.into_diagnostic()
}
fn path_contains(path: &[u16], entry: &[u16]) -> bool {
path.windows(entry.len()).any(|path| path == entry)
}
fn path_join(left: &[u16], right: &[u16]) -> Vec<u16> {
if left.is_empty() {
right.to_owned()
} else if right.is_empty() {
left.to_owned()
} else {
let sep = b';' as u16;
[left, right].join(&sep)
}
}
mod winreg_ext {
use winreg::enums::RegType;
pub fn to_winreg_bytes(val: &[u16]) -> Vec<u8> {
let mut v = val.to_owned();
v.push(0);
unsafe { std::slice::from_raw_parts(v.as_ptr().cast::<u8>(), v.len() * 2).to_vec() }
}
pub fn from_winreg_value(val: &winreg::RegValue) -> Option<Vec<u16>> {
use std::slice;
match val.vtype {
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
let mut words = unsafe {
#[allow(clippy::cast_ptr_alignment)]
slice::from_raw_parts(val.bytes.as_ptr().cast::<u16>(), val.bytes.len() / 2)
.to_owned()
};
while words.last() == Some(&0) {
words.pop();
}
Some(words)
}
_ => None,
}
}
}