native/src/path_control.rs
Rama main · réplica de lectura del repositorio original.
//! Per-user PATH registration for the installed Origin Speak manager.
//!
//! Windows updates only the exact Origin Speak manager directory in
//! `HKCU\Environment\Path`. Unix installs the manager in `~/.local/bin` and
//! reports whether that directory is already visible without editing shell rc files.
use std::path::Path;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PathRegistration {
pub on_path: bool,
pub changed: bool,
}
#[cfg(target_os = "windows")]
mod imp {
use super::PathRegistration;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows::Win32::Foundation::{
ERROR_FILE_NOT_FOUND, ERROR_SUCCESS, LPARAM, WIN32_ERROR, WPARAM,
};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE, REG_EXPAND_SZ, REG_SZ, REG_VALUE_TYPE,
RegCloseKey, RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
};
use windows::Win32::UI::WindowsAndMessaging::{
HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
};
use windows::core::{Error as WindowsError, HRESULT, PCWSTR};
const ENVIRONMENT_KEY: &str = r"Environment";
const PATH_VALUE: &str = "Path";
struct RegistryKey(HKEY);
impl Drop for RegistryKey {
fn drop(&mut self) {
unsafe {
let _ = RegCloseKey(self.0);
}
}
}
pub fn status(root: &Path) -> Result<PathRegistration, String> {
crate::management::validate_manager_root_ownership(root)?;
let (value, _) = read_path()?;
Ok(PathRegistration {
on_path: contains_root(&value, root)?,
changed: false,
})
}
pub fn ensure(root: &Path) -> Result<PathRegistration, String> {
crate::management::validate_manager_root_ownership(root)?;
let root = path_string(root)?;
let (value, value_type) = read_path()?;
if contains_normalized(&value, &root) {
return Ok(PathRegistration {
on_path: true,
changed: false,
});
}
let next = if value.trim().is_empty() {
root
} else if value.ends_with(';') {
format!("{value}{root}")
} else {
format!("{value};{root}")
};
write_path(&next, value_type)?;
broadcast_environment_change();
Ok(PathRegistration {
on_path: true,
changed: true,
})
}
pub fn remove(root: &Path) -> Result<PathRegistration, String> {
let root = path_string(root)?;
let (value, value_type) = read_path()?;
let mut removed = false;
let kept = value
.split(';')
.filter(|entry| {
let matched = normalize(entry) == normalize(&root);
removed |= matched;
!matched
})
.collect::<Vec<_>>()
.join(";");
if removed {
write_path(&kept, value_type)?;
broadcast_environment_change();
}
Ok(PathRegistration {
on_path: false,
changed: removed,
})
}
fn contains_root(value: &str, root: &Path) -> Result<bool, String> {
Ok(contains_normalized(value, &path_string(root)?))
}
fn contains_normalized(value: &str, root: &str) -> bool {
let root = normalize(root);
value.split(';').any(|entry| normalize(entry) == root)
}
fn normalize(value: &str) -> String {
value
.trim()
.trim_matches('"')
.trim_end_matches(['\\', '/'])
.to_ascii_lowercase()
}
fn path_string(path: &Path) -> Result<String, String> {
let value = path
.to_str()
.ok_or_else(|| "Origin Speak manager path is not valid Unicode".to_string())?;
if value.contains(';') {
return Err("Origin Speak manager path contains an unsupported semicolon".to_string());
}
Ok(value.to_string())
}
fn read_path() -> Result<(String, REG_VALUE_TYPE), String> {
let key = open_environment_key(KEY_READ)?;
let value_name = wide(PATH_VALUE);
let mut value_type = REG_EXPAND_SZ;
let mut byte_len = 0_u32;
let status = unsafe {
RegQueryValueExW(
key.0,
PCWSTR(value_name.as_ptr()),
None,
Some(&mut value_type),
None,
Some(&mut byte_len),
)
};
if status == ERROR_FILE_NOT_FOUND {
return Ok((String::new(), REG_EXPAND_SZ));
}
check(status, "query per-user PATH")?;
if value_type != REG_SZ && value_type != REG_EXPAND_SZ {
return Err("per-user PATH has an unsupported registry value type".to_string());
}
if byte_len == 0 {
return Ok((String::new(), value_type));
}
let mut bytes = vec![0_u8; byte_len as usize];
let status = unsafe {
RegQueryValueExW(
key.0,
PCWSTR(value_name.as_ptr()),
None,
Some(&mut value_type),
Some(bytes.as_mut_ptr()),
Some(&mut byte_len),
)
};
check(status, "read per-user PATH")?;
let units = bytes[..byte_len as usize]
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.take_while(|unit| *unit != 0)
.collect::<Vec<_>>();
let value =
String::from_utf16(&units).map_err(|error| format!("decode per-user PATH: {error}"))?;
Ok((value, value_type))
}
fn write_path(value: &str, value_type: REG_VALUE_TYPE) -> Result<(), String> {
let key = open_environment_key(KEY_WRITE)?;
let value_name = wide(PATH_VALUE);
let wide_value = wide(value);
let bytes = wide_value
.iter()
.flat_map(|unit| unit.to_le_bytes())
.collect::<Vec<_>>();
let status = unsafe {
RegSetValueExW(
key.0,
PCWSTR(value_name.as_ptr()),
None,
value_type,
Some(&bytes),
)
};
check(status, "write per-user PATH")
}
fn open_environment_key(
access: windows::Win32::System::Registry::REG_SAM_FLAGS,
) -> Result<RegistryKey, String> {
let subkey = wide(ENVIRONMENT_KEY);
let mut key = HKEY::default();
let status = unsafe {
RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR(subkey.as_ptr()),
None,
access,
&mut key,
)
};
check(status, "open per-user Environment registry key")?;
Ok(RegistryKey(key))
}
fn broadcast_environment_change() {
let message = wide("Environment");
let mut result = 0_usize;
unsafe {
let _ = SendMessageTimeoutW(
HWND_BROADCAST,
WM_SETTINGCHANGE,
WPARAM(0),
LPARAM(message.as_ptr() as isize),
SMTO_ABORTIFHUNG,
1500,
Some(&mut result),
);
}
}
fn wide(value: &str) -> Vec<u16> {
std::ffi::OsStr::new(value)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn check(status: WIN32_ERROR, operation: &str) -> Result<(), String> {
if status == ERROR_SUCCESS {
return Ok(());
}
let error = WindowsError::from_hresult(HRESULT::from_win32(status.0));
Err(format!("{operation}: {error}"))
}
}
#[cfg(unix)]
mod imp {
use super::PathRegistration;
use std::path::Path;
pub fn status(root: &Path) -> Result<PathRegistration, String> {
let on_path = std::env::var_os("PATH")
.map(|value| std::env::split_paths(&value).any(|entry| entry == root))
.unwrap_or(false);
Ok(PathRegistration {
on_path,
changed: false,
})
}
pub fn ensure(root: &Path) -> Result<PathRegistration, String> {
status(root)
}
pub fn remove(root: &Path) -> Result<PathRegistration, String> {
status(root).map(|_| PathRegistration {
on_path: false,
changed: false,
})
}
}
#[cfg(not(any(target_os = "windows", unix)))]
mod imp {
use super::PathRegistration;
use std::path::Path;
pub fn status(_: &Path) -> Result<PathRegistration, String> {
Err("manager PATH discovery is unsupported on this platform".to_string())
}
pub fn ensure(_: &Path) -> Result<PathRegistration, String> {
Err("manager PATH registration is unsupported on this platform".to_string())
}
pub fn remove(_: &Path) -> Result<PathRegistration, String> {
Err("manager PATH registration is unsupported on this platform".to_string())
}
}
pub fn status(root: &Path) -> Result<PathRegistration, String> {
imp::status(root)
}
pub fn ensure(root: &Path) -> Result<PathRegistration, String> {
imp::ensure(root)
}
pub fn remove(root: &Path) -> Result<PathRegistration, String> {
imp::remove(root)
}