Skip to main content

java_manager/
search.rs

1use crate::{JavaError, JavaInfo};
2use std::collections::VecDeque;
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7const MAX_SEARCH_DEPTH: usize = 6;
8
9const JAVA_KEYWORDS: &[&str] = &["java", "jdk", "jre", "jvm", "openjdk", "graalvm"];
10
11const EXCLUDE_FOLDERS: &[&str] = &[
12    "node_modules",
13    ".git",
14    "__pycache__",
15    "vendor",
16    "cache",
17    "temp",
18    "tmp",
19    "logs",
20    "log",
21];
22
23pub fn quick_search() -> Result<Vec<JavaInfo>, JavaError> {
24    let mut results: Vec<JavaInfo> = Vec::new();
25
26    if let Ok(paths) = env::var("PATH") {
27        for path in env::split_paths(&paths) {
28            let java_exe = if cfg!(windows) { "java.exe" } else { "java" };
29            let java_path = path.join(java_exe);
30
31            if java_path.is_file()
32                && let Ok(info) = JavaInfo::new(java_path.to_string_lossy().to_string())
33            {
34                results.push(info);
35            }
36        }
37    }
38
39    Ok(results)
40}
41
42pub fn deep_search() -> Result<Vec<JavaInfo>, JavaError> {
43    #[cfg(target_os = "windows")]
44    {
45        deep_search_everything()
46    }
47
48    #[cfg(target_os = "linux")]
49    {
50        full_search()
51    }
52
53    #[cfg(not(any(target_os = "windows", target_os = "linux")))]
54    {
55        Ok(Vec::new())
56    }
57}
58
59pub fn full_search() -> Result<Vec<JavaInfo>, JavaError> {
60    let mut paths = Vec::new();
61
62    #[cfg(target_os = "windows")]
63    {
64        paths.extend(scan_registry());
65        paths.extend(scan_default_paths());
66        paths.extend(scan_microsoft_store());
67        paths.extend(scan_where_command());
68    }
69
70    #[cfg(target_os = "linux")]
71    {
72        paths.extend(scan_linux());
73    }
74
75    paths.sort();
76    paths.dedup();
77
78    Ok(paths
79        .into_iter()
80        .filter_map(|p| JavaInfo::new(p.to_string_lossy().to_string()).ok())
81        .collect())
82}
83
84#[cfg(target_os = "windows")]
85fn deep_search_everything() -> Result<Vec<JavaInfo>, JavaError> {
86    use everything_sdk::*;
87
88    let mut results: Vec<JavaInfo> = Vec::new();
89    let mut everything = global().try_lock().map_err(|_| {
90        JavaError::RuntimeError("Failed to lock Everything global state".to_string())
91    })?;
92
93    match everything.is_db_loaded() {
94        Ok(false) => {
95            return Err(JavaError::ExecuteError(
96                "Everything database is not fully loaded".to_string(),
97            ));
98        }
99        Err(EverythingError::Ipc) => {
100            return Err(JavaError::ExecuteError(
101                "Everything is not running in the background. Please start Everything.exe"
102                    .to_string(),
103            ));
104        }
105        _ => {}
106    }
107
108    let mut searcher = everything.searcher();
109    searcher.set_search("\"java.exe\" !C:\\Windows\\");
110    searcher.set_request_flags(
111        RequestFlags::EVERYTHING_REQUEST_FILE_NAME
112            | RequestFlags::EVERYTHING_REQUEST_PATH
113            | RequestFlags::EVERYTHING_REQUEST_SIZE,
114    );
115    searcher.set_sort(SortType::EVERYTHING_SORT_NAME_ASCENDING);
116
117    assert!(!searcher.get_match_case());
118
119    let query_results = searcher.query();
120
121    for item in query_results.iter() {
122        if let Ok(path) = item.filepath()
123            && let Ok(info) = JavaInfo::new(path.to_string_lossy().to_string())
124        {
125            results.push(info);
126        }
127    }
128
129    Ok(results)
130}
131
132#[cfg(target_os = "windows")]
133fn scan_registry() -> Vec<PathBuf> {
134    use winreg::RegKey;
135    use winreg::enums::*;
136
137    let mut results = Vec::new();
138
139    let javasoft_paths = [
140        r"SOFTWARE\JavaSoft\Java Development Kit",
141        r"SOFTWARE\JavaSoft\Java Runtime Environment",
142        r"SOFTWARE\WOW6432Node\JavaSoft\Java Development Kit",
143        r"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment",
144    ];
145
146    for reg_path in &javasoft_paths {
147        if let Ok(key) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(reg_path) {
148            for subkey_name in key.enum_keys().filter_map(|k| k.ok()) {
149                if let Ok(subkey) = key.open_subkey(subkey_name)
150                    && let Ok(java_home) = subkey.get_value::<String, _>("JavaHome")
151                {
152                    let java_exe = Path::new(&java_home).join("bin").join("java.exe");
153                    if java_exe.exists() {
154                        results.push(java_exe);
155                    }
156                }
157            }
158        }
159    }
160
161    let brand_paths = [
162        (r"SOFTWARE\Azul Systems\Zulu", "InstallationPath"),
163        (r"SOFTWARE\BellSoft\Liberica", "InstallationPath"),
164    ];
165
166    for (reg_path, value_name) in &brand_paths {
167        if let Ok(key) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(reg_path) {
168            for subkey_name in key.enum_keys().filter_map(|k| k.ok()) {
169                if let Ok(subkey) = key.open_subkey(subkey_name)
170                    && let Ok(install_path) = subkey.get_value::<String, _>(value_name)
171                {
172                    let java_exe = Path::new(&install_path).join("bin").join("java.exe");
173                    if java_exe.exists() {
174                        results.push(java_exe);
175                    }
176                }
177            }
178        }
179    }
180
181    results
182}
183
184#[cfg(target_os = "windows")]
185fn scan_default_paths() -> Vec<PathBuf> {
186    let mut results = Vec::new();
187    let roots = default_path_roots();
188
189    for root in roots {
190        if !root.exists() {
191            continue;
192        }
193
194        let mut queue = VecDeque::new();
195        queue.push_back((root, 0));
196
197        while let Some((current, depth)) = queue.pop_front() {
198            if depth > MAX_SEARCH_DEPTH {
199                continue;
200            }
201
202            if let Ok(entries) = fs::read_dir(&current) {
203                for entry in entries.flatten() {
204                    let path = entry.path();
205                    if !path.is_dir() {
206                        continue;
207                    }
208
209                    let java_exe = path.join("java.exe");
210                    if java_exe.exists() {
211                        results.push(java_exe);
212                        continue;
213                    }
214
215                    if should_explore_deeper(&path) {
216                        queue.push_back((path, depth + 1));
217                    }
218                }
219            }
220        }
221    }
222
223    results
224}
225
226#[cfg(target_os = "windows")]
227fn default_path_roots() -> Vec<PathBuf> {
228    let mut roots = Vec::new();
229
230    if let Some(appdata) = env::var_os("APPDATA") {
231        roots.push(Path::new(&appdata).join(".minecraft").join("runtime"));
232    }
233
234    if let Some(localappdata) = env::var_os("LOCALAPPDATA") {
235        roots.push(Path::new(&localappdata).to_path_buf());
236    }
237
238    if let Some(profile) = env::var_os("USERPROFILE") {
239        roots.push(Path::new(&profile).to_path_buf());
240    }
241
242    for drive in fixed_drives() {
243        let prog_files = Path::new(&drive).join("Program Files");
244        if prog_files.exists() {
245            roots.push(prog_files);
246        }
247
248        let prog_files_x86 = Path::new(&drive).join("Program Files (x86)");
249        if prog_files_x86.exists() {
250            roots.push(prog_files_x86);
251        }
252
253        if let Ok(entries) = fs::read_dir(&drive) {
254            for entry in entries.flatten() {
255                let dir_name = entry.file_name().to_string_lossy().to_lowercase();
256                if JAVA_KEYWORDS.iter().any(|kw| dir_name.contains(kw)) {
257                    roots.push(entry.path());
258                }
259            }
260        }
261    }
262
263    roots
264}
265
266#[cfg(target_os = "windows")]
267fn fixed_drives() -> Vec<String> {
268    let mut drives = Vec::new();
269    for letter in 'A'..='Z' {
270        let drive = format!("{letter}:\\");
271        let p = Path::new(&drive);
272        if p.exists() && fs::read_dir(p).is_ok() {
273            drives.push(drive);
274        }
275    }
276    drives
277}
278
279#[cfg(target_os = "windows")]
280fn should_explore_deeper(path: &Path) -> bool {
281    let name = match path.file_name() {
282        Some(n) => n.to_string_lossy(),
283        None => return false,
284    };
285
286    let lower = name.to_lowercase();
287
288    for ex in EXCLUDE_FOLDERS {
289        if lower.contains(ex) {
290            return false;
291        }
292    }
293
294    for kw in JAVA_KEYWORDS {
295        if lower.contains(kw) {
296            return true;
297        }
298    }
299
300    is_version_like(&name)
301}
302
303fn is_version_like(name: &str) -> bool {
304    if name.is_empty() || name.len() > 20 {
305        return false;
306    }
307
308    let has_digit = name.chars().any(|c| c.is_ascii_digit());
309    if !has_digit {
310        return false;
311    }
312
313    name.chars()
314        .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == '-')
315}
316
317#[cfg(target_os = "windows")]
318fn scan_microsoft_store() -> Vec<PathBuf> {
319    let localappdata = match env::var_os("LOCALAPPDATA") {
320        Some(v) => v,
321        None => return Vec::new(),
322    };
323
324    let base = Path::new(&localappdata)
325        .join(r"Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime");
326
327    if !base.exists() {
328        return Vec::new();
329    }
330
331    let mut results = Vec::new();
332
333    if let Ok(runtimes) = fs::read_dir(&base) {
334        for runtime in runtimes.flatten() {
335            let runtime_path = runtime.path();
336            let name = runtime_path.file_name().map(|n| n.to_string_lossy());
337            if !name
338                .as_deref()
339                .is_some_and(|n| n.starts_with("java-runtime"))
340            {
341                continue;
342            }
343
344            if let Ok(archs) = fs::read_dir(&runtime_path) {
345                for arch in archs.flatten() {
346                    let arch_path = arch.path();
347                    if !arch_path.is_dir() {
348                        continue;
349                    }
350
351                    if let Ok(versions) = fs::read_dir(&arch_path) {
352                        for version in versions.flatten() {
353                            let java_exe = version.path().join("bin").join("java.exe");
354                            if java_exe.exists() {
355                                results.push(java_exe);
356                            }
357                        }
358                    }
359                }
360            }
361        }
362    }
363
364    results
365}
366
367#[cfg(target_os = "windows")]
368fn scan_where_command() -> Vec<PathBuf> {
369    let mut results = Vec::new();
370
371    if let Ok(output) = std::process::Command::new("where").arg("java").output()
372        && output.status.success()
373    {
374        let stdout = String::from_utf8_lossy(&output.stdout);
375        for line in stdout.lines() {
376            let trimmed = line.trim();
377            if !trimmed.is_empty() {
378                let p = Path::new(trimmed);
379                if p.exists() {
380                    results.push(p.to_path_buf());
381                }
382            }
383        }
384    }
385
386    results
387}
388
389#[cfg(target_os = "linux")]
390fn scan_linux() -> Vec<PathBuf> {
391    use walkdir::WalkDir;
392
393    let mut results = Vec::new();
394
395    let mut search_dirs: Vec<PathBuf> = vec![
396        "/usr/lib/jvm".into(),
397        "/usr/java".into(),
398        "/opt".into(),
399        "/usr/local".into(),
400    ];
401
402    if let Ok(home) = env::var("HOME") {
403        let mc_runtime = Path::new(&home).join(".minecraft").join("runtime");
404        if mc_runtime.exists() {
405            search_dirs.push(mc_runtime);
406        }
407    }
408
409    for dir in search_dirs {
410        if !dir.exists() {
411            continue;
412        }
413
414        for entry in WalkDir::new(&dir)
415            .follow_links(true)
416            .into_iter()
417            .filter_map(|e| e.ok())
418        {
419            let entry_path = entry.path();
420
421            if entry_path.file_name() != Some(std::ffi::OsStr::new("java")) {
422                continue;
423            }
424            if !entry_path.is_file() {
425                continue;
426            }
427
428            if let Some(parent) = entry_path.parent() {
429                if !should_explore_linux(parent) {
430                    continue;
431                }
432            }
433
434            #[cfg(unix)]
435            {
436                use std::os::unix::fs::PermissionsExt;
437                if let Ok(metadata) = entry_path.metadata() {
438                    let permissions = metadata.permissions();
439                    if permissions.mode() & 0o111 != 0 {
440                        results.push(entry_path.to_path_buf());
441                    }
442                }
443            }
444            #[cfg(not(unix))]
445            {
446                results.push(entry_path.to_path_buf());
447            }
448        }
449    }
450
451    results
452}
453
454#[cfg(target_os = "linux")]
455fn should_explore_linux(path: &Path) -> bool {
456    let name = match path.file_name() {
457        Some(n) => n.to_string_lossy(),
458        None => return true,
459    };
460
461    let lower = name.to_lowercase();
462
463    for ex in EXCLUDE_FOLDERS {
464        if lower.contains(ex) {
465            return false;
466        }
467    }
468
469    for kw in JAVA_KEYWORDS {
470        if lower.contains(kw) {
471            return true;
472        }
473    }
474
475    is_version_like(&name)
476}