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(¤t) {
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}