use std::collections::HashSet;
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
#[derive(Debug, Clone)]
pub(crate) struct EffectiveSearchPath {
pub combined: OsString,
pub from_process: usize,
pub from_user_registry: usize,
pub from_system_registry: usize,
}
impl EffectiveSearchPath {
#[cfg(test)]
pub(crate) fn total(&self) -> usize {
self.from_process + self.from_user_registry + self.from_system_registry
}
}
pub(crate) 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,
}
}
const MAX_REGISTRY_PATH_BYTES: usize = 64 * 1024;
const MAX_PATH_ENTRIES: usize = 256;
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,
};
absorb_registry_path_str(&raw, combined, seen)
}
pub(crate) fn absorb_registry_path_str(
raw: &str,
combined: &mut OsString,
seen: &mut HashSet<Vec<u16>>,
) -> usize {
let mut added = 0usize;
let mut entries_taken = 0usize;
for seg in raw.split(';') {
if entries_taken >= MAX_PATH_ENTRIES {
break;
}
entries_taken += 1;
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()?;
Some(cap_registry_value(v))
}
pub(crate) fn cap_registry_value(v: String) -> String {
if v.is_empty() {
return v;
}
if v.len() <= MAX_REGISTRY_PATH_BYTES {
return v;
}
match v[..MAX_REGISTRY_PATH_BYTES].rfind(';') {
Some(end) => v[..end].to_string(),
None => String::new(),
}
}
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");
}
#[test]
fn cap_registry_value_truncates_at_semicolon_boundary() {
let leading = "a".repeat(70 * 1024);
let raw = format!("{leading};C:\\fits");
let capped = cap_registry_value(raw.clone());
assert!(capped.len() <= 64 * 1024, "cap must hold");
assert!(
!capped.contains(';'),
"with no semicolon under the cap on the leading run, the tail must be dropped: {}",
capped.len()
);
}
#[test]
fn cap_registry_value_passes_small_input_through() {
let raw = "C:\\Windows;C:\\Users\\me\\.cargo\\bin".to_string();
assert_eq!(cap_registry_value(raw.clone()), raw);
}
#[test]
fn absorb_registry_path_str_caps_entry_count() {
let raw: String = (0..300)
.map(|i| format!("C:\\fake{i}"))
.collect::<Vec<_>>()
.join(";");
let mut combined = OsString::new();
let mut seen: HashSet<Vec<u16>> = HashSet::new();
let added = absorb_registry_path_str(&raw, &mut combined, &mut seen);
assert_eq!(added, 256, "must stop at MAX_PATH_ENTRIES");
}
}