use std::collections::HashSet;
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
#[derive(Debug, Clone)]
pub struct EffectiveSearchPath {
pub combined: OsString,
pub from_process: usize,
pub from_user_registry: usize,
pub from_system_registry: usize,
}
impl EffectiveSearchPath {
#[cfg(test)]
pub fn total(&self) -> usize {
self.from_process + self.from_user_registry + self.from_system_registry
}
}
pub fn effective_search_path() -> EffectiveSearchPath {
let mut combined = OsString::new();
let mut seen: HashSet<Vec<u16>> = HashSet::new();
let mut from_process = 0usize;
if let Some(p) = std::env::var_os("PATH") {
for seg in split_path_env(&p) {
if push_dedup(&seg, &mut combined, &mut seen) {
from_process += 1;
}
}
}
let from_user_registry = absorb_registry_path(
winreg::enums::HKEY_CURRENT_USER,
"Environment",
&mut combined,
&mut seen,
);
let from_system_registry = absorb_registry_path(
winreg::enums::HKEY_LOCAL_MACHINE,
r"System\CurrentControlSet\Control\Session Manager\Environment",
&mut combined,
&mut seen,
);
EffectiveSearchPath {
combined,
from_process,
from_user_registry,
from_system_registry,
}
}
fn absorb_registry_path(
hive: winreg::HKEY,
subkey: &str,
combined: &mut OsString,
seen: &mut HashSet<Vec<u16>>,
) -> usize {
let raw = match read_registry_path(hive, subkey) {
Some(v) => v,
None => return 0,
};
let mut added = 0usize;
for seg in raw.split(';') {
let expanded = expand_env_vars(seg);
if push_dedup(&expanded, combined, seen) {
added += 1;
}
}
added
}
fn read_registry_path(hive: winreg::HKEY, subkey: &str) -> Option<String> {
let key = winreg::RegKey::predef(hive).open_subkey(subkey).ok()?;
let v: String = key.get_value("Path").ok()?;
if v.is_empty() { None } else { Some(v) }
}
fn push_dedup(seg: &OsStr, dst: &mut OsString, seen: &mut HashSet<Vec<u16>>) -> bool {
if seg.is_empty() {
return false;
}
let key: Vec<u16> = seg
.encode_wide()
.map(|c| if c < 128 { (c as u8).to_ascii_lowercase() as u16 } else { c })
.collect();
if seen.insert(key) {
if !dst.is_empty() {
dst.push(";");
}
dst.push(seg);
true
} else {
false
}
}
fn split_path_env(p: &OsStr) -> Vec<OsString> {
let wide: Vec<u16> = p.encode_wide().collect();
let mut out = Vec::new();
let mut start = 0;
for (i, w) in wide.iter().enumerate() {
if *w == b';' as u16 {
out.push(OsString::from_wide(&wide[start..i]));
start = i + 1;
}
}
out.push(OsString::from_wide(&wide[start..]));
out
}
fn expand_env_vars(s: &str) -> OsString {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(start) = rest.find('%') {
out.push_str(&rest[..start]);
let after = &rest[start + 1..];
if let Some(end) = after.find('%') {
let var = &after[..end];
if let Ok(val) = std::env::var(var) {
out.push_str(&val);
} else {
out.push('%');
out.push_str(var);
out.push('%');
}
rest = &after[end + 1..];
} else {
out.push('%');
rest = after;
break;
}
}
out.push_str(rest);
OsString::from(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_dedup_skips_empty() {
let mut combined = OsString::new();
let mut seen = HashSet::new();
assert!(!push_dedup(OsStr::new(""), &mut combined, &mut seen));
assert!(combined.is_empty());
}
#[test]
fn push_dedup_is_case_insensitive() {
let mut combined = OsString::new();
let mut seen = HashSet::new();
assert!(push_dedup(OsStr::new(r"C:\Foo\Bar"), &mut combined, &mut seen));
assert!(!push_dedup(OsStr::new(r"c:\foo\bar"), &mut combined, &mut seen));
assert_eq!(combined, OsString::from(r"C:\Foo\Bar"));
}
#[test]
fn expand_env_vars_expands_known_and_preserves_unknown() {
unsafe { std::env::set_var("__RUNEX_TEST_VAR", "REPLACED"); }
let r = expand_env_vars(r"%__RUNEX_TEST_VAR%\sub");
unsafe { std::env::remove_var("__RUNEX_TEST_VAR"); }
assert_eq!(r, OsString::from(r"REPLACED\sub"));
let r2 = expand_env_vars(r"%__RUNEX_DEFINITELY_NOT_SET_VAR%\x");
assert_eq!(r2, OsString::from(r"%__RUNEX_DEFINITELY_NOT_SET_VAR%\x"));
}
#[test]
fn effective_search_path_runs_and_counts_consistently() {
let p = effective_search_path();
assert_eq!(
p.from_process + p.from_user_registry + p.from_system_registry,
p.total()
);
assert!(p.total() > 0, "effective_search_path should never be empty in practice");
}
}