use crate::path_entry::PathEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
Process,
User,
Machine,
}
#[derive(Debug)]
pub struct PathRead {
pub entries: Vec<PathEntry>,
pub warning: Option<String>,
}
pub fn read_path(target: Target) -> PathRead {
match target {
Target::Process => PathRead {
entries: split_into_entries(&std::env::var("PATH").unwrap_or_default()),
warning: None,
},
Target::User => read_registry(target),
Target::Machine => read_registry(target),
}
}
pub(crate) fn split_into_entries(s: &str) -> Vec<PathEntry> {
let sep = if cfg!(windows) { ';' } else { ':' };
s.split(sep)
.filter(|x| !x.is_empty())
.map(|raw| PathEntry::from_raw(raw, |v| std::env::var(v).ok()))
.collect()
}
#[cfg(windows)]
fn read_registry(target: Target) -> PathRead {
use winreg::RegKey;
use winreg::enums::*;
let (root, subkey) = match target {
Target::User => (RegKey::predef(HKEY_CURRENT_USER), "Environment"),
Target::Machine => (
RegKey::predef(HKEY_LOCAL_MACHINE),
r"System\CurrentControlSet\Control\Session Manager\Environment",
),
Target::Process => unreachable!(),
};
let key = match root.open_subkey(subkey) {
Ok(k) => k,
Err(e) => {
return PathRead {
entries: Vec::new(),
warning: Some(format!("could not open registry key: {e}")),
};
}
};
let raw_value = match key.get_raw_value("Path") {
Ok(v) => v,
Err(e) => {
return PathRead {
entries: Vec::new(),
warning: Some(format!("could not read Path value: {e}")),
};
}
};
match decode_reg_string(&raw_value) {
Ok(raw_string) => PathRead {
entries: split_into_entries(&raw_string),
warning: None,
},
Err(reason) => PathRead {
entries: Vec::new(),
warning: Some(format!("registry Path is not a valid string ({reason})")),
},
}
}
#[cfg(not(windows))]
fn read_registry(target: Target) -> PathRead {
let label = match target {
Target::User => "user",
Target::Machine => "machine",
Target::Process => unreachable!(),
};
PathRead {
entries: split_into_entries(&std::env::var("PATH").unwrap_or_default()),
warning: Some(format!(
"--target {label} is Windows-only; falling back to process PATH"
)),
}
}
#[cfg(windows)]
pub(crate) fn decode_reg_string(v: &winreg::RegValue) -> Result<String, &'static str> {
use winreg::enums::RegType;
match v.vtype {
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
if v.bytes.len() % 2 != 0 {
return Err("UTF-16 byte stream has odd length");
}
let units: Vec<u16> = v
.bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let trimmed: &[u16] = match units.iter().position(|&u| u == 0) {
Some(idx) => &units[..idx],
None => &units[..],
};
Ok(String::from_utf16_lossy(trimmed))
}
_ => Err("unexpected registry value type"),
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
use winreg::RegValue;
use winreg::enums::RegType;
fn reg_value(s: &str, vtype: RegType) -> RegValue {
let mut units: Vec<u16> = s.encode_utf16().collect();
units.push(0);
let bytes: Vec<u8> = units.iter().flat_map(|u| u.to_le_bytes()).collect();
RegValue { bytes, vtype }
}
#[test]
fn decode_reg_string_keeps_percent_var_for_reg_expand_sz() {
let v = reg_value(
r"%LocalAppData%\Microsoft\WindowsApps",
RegType::REG_EXPAND_SZ,
);
let decoded = decode_reg_string(&v).expect("REG_EXPAND_SZ decode");
assert_eq!(decoded, r"%LocalAppData%\Microsoft\WindowsApps");
}
#[test]
fn decode_reg_string_handles_reg_sz_literal() {
let v = reg_value(r"C:\Program Files\PowerShell\7", RegType::REG_SZ);
let decoded = decode_reg_string(&v).expect("REG_SZ decode");
assert_eq!(decoded, r"C:\Program Files\PowerShell\7");
}
#[test]
fn decode_reg_string_rejects_unsupported_reg_type() {
let v = RegValue {
bytes: vec![0, 0, 0, 0],
vtype: RegType::REG_DWORD,
};
let err = decode_reg_string(&v).unwrap_err();
assert!(err.contains("unexpected"), "err was: {err}");
}
#[test]
fn decode_reg_string_rejects_odd_byte_length() {
let v = RegValue {
bytes: vec![b'A', b'B', b'C'],
vtype: RegType::REG_SZ,
};
let err = decode_reg_string(&v).unwrap_err();
assert!(err.contains("odd length"), "err was: {err}");
}
}