use log::debug;
use std::collections::HashMap;
use std::path::PathBuf;
use walkdir::WalkDir;
use windows_registry::{CURRENT_USER, LOCAL_MACHINE};
#[derive(Debug, Clone)]
pub struct Candidate {
pub path: PathBuf,
#[doc(hidden)]
pub _source: String,
}
use crate::invariant_ppt::assert_invariant;
pub fn discover_candidates() -> HashMap<String, Vec<Candidate>> {
let mut map: HashMap<String, Vec<Candidate>> = HashMap::new();
scan_registry_uninstall(&mut map);
scan_common_locations(&mut map);
scan_existing_path(&mut map);
assert_invariant(
!map.is_empty(),
"Discovery phase yielded zero candidates. System appears to be empty or unreadable.",
Some("Discovery"),
);
for key in map.keys() {
assert_invariant(
key == &key.to_lowercase(),
&format!("Discovery key '{}' is not lowercase", key),
Some("Discovery"),
);
}
map
}
fn scan_registry_uninstall(map: &mut HashMap<String, Vec<Candidate>>) {
let key_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
let hives = [
(CURRENT_USER, "HKCU_Uninstall"),
(LOCAL_MACHINE, "HKLM_Uninstall"),
];
for (hive, source_label) in hives {
if let Ok(uninstall_key) = hive.open(key_path) {
for subkey_name in uninstall_key.keys().into_iter().flatten() {
if let Ok(subkey) = uninstall_key.open(&subkey_name) {
if let Some(install_loc) = subkey
.get_string("InstallLocation")
.ok()
.filter(|s| !s.is_empty())
{
let path = PathBuf::from(&install_loc);
let bin_path = path.join("bin");
if bin_path.exists() {
add_dir_candidates(map, &bin_path, source_label);
} else if path.exists() {
add_dir_candidates(map, &path, source_label);
}
}
}
}
}
}
}
fn scan_common_locations(map: &mut HashMap<String, Vec<Candidate>>) {
if let Some(user_profile) = directories::UserDirs::new() {
let home = user_profile.home_dir();
let cargo_bin = home.join(".cargo").join("bin");
if cargo_bin.exists() {
add_dir_candidates(map, &cargo_bin, "cargo");
}
let local_bin = home.join(".local").join("bin");
if local_bin.exists() {
add_dir_candidates(map, &local_bin, "local_bin");
}
let scoop_shims = home.join("scoop").join("shims");
if scoop_shims.exists() {
add_dir_candidates(map, &scoop_shims, "scoop");
}
let python_base = home
.join("AppData")
.join("Local")
.join("Programs")
.join("Python");
let entries = if python_base.exists() {
std::fs::read_dir(&python_base).ok()
} else {
None
};
if let Some(entries) = entries {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with("Python3") {
add_dir_candidates(map, &path, "python");
let scripts = path.join("Scripts");
if scripts.exists() {
add_dir_candidates(map, &scripts, "python_scripts");
}
}
}
}
}
}
let drives: Vec<String> = ('A'..='Z')
.map(|d| format!("{}:", d))
.filter(|d| std::path::Path::new(&format!("{}\\", d)).exists())
.collect();
for drive in drives {
let drive_path = PathBuf::from(drive);
if let Ok(entries) = std::fs::read_dir(&drive_path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with("Python3") {
add_dir_candidates(map, &path, "python_system");
let scripts = path.join("Scripts");
if scripts.exists() {
add_dir_candidates(map, &scripts, "python_scripts");
}
}
}
}
}
for pf in ["Program Files", "Program Files (x86)"] {
let pf_path = drive_path.join(pf);
if let Ok(entries) = std::fs::read_dir(&pf_path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with("Python3") {
add_dir_candidates(map, &path, "python_system");
let scripts = path.join("Scripts");
if scripts.exists() {
add_dir_candidates(map, &scripts, "python_scripts");
}
}
}
}
}
}
}
}
fn scan_existing_path(map: &mut HashMap<String, Vec<Candidate>>) {
if let Ok(path_var) = std::env::var("PATH") {
for part in path_var.split(';') {
if part.is_empty() {
continue;
}
let path = PathBuf::from(part);
if path.exists() {
add_dir_candidates(map, &path, "existing_path");
}
}
}
}
fn add_dir_candidates(map: &mut HashMap<String, Vec<Candidate>>, dir: &PathBuf, source: &str) {
debug!("Scanning directory: {:?}", dir);
let walker = WalkDir::new(dir).max_depth(1);
for entry in walker.into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if !path.is_file() {
continue;
}
if let (Some(stem), Some(ext)) = (path.file_stem(), path.extension()) {
let ext_str = ext.to_string_lossy().to_lowercase();
if ext_str == "exe" || ext_str == "cmd" || ext_str == "bat" || ext_str == "com" {
let cmd_name = stem.to_string_lossy().to_lowercase();
map.entry(cmd_name).or_default().push(Candidate {
path: dir.to_path_buf(), _source: source.to_string(),
});
}
}
}
}