use crate::config::new_kopi_config;
use crate::error::{KopiError, Result};
use crate::models::distribution::Distribution;
use crate::storage::{InstalledJdk, JdkRepository};
use crate::version::VersionRequest;
use std::env;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::str::FromStr;
pub mod discovery;
pub mod installer;
pub mod security;
pub mod tools;
use crate::error::format_error_with_color;
use crate::installation::AutoInstaller;
use crate::version::resolver::VersionResolver;
use security::SecurityValidator;
pub fn run(_args: Vec<String>) -> Result<i32> {
run_shim()?;
Ok(0)
}
pub fn run_shim() -> Result<()> {
let start = std::time::Instant::now();
let config = new_kopi_config()?;
let security_validator = SecurityValidator::new(&config);
let tool_name = get_tool_name()?;
log::debug!("Shim invoked as: {tool_name}");
security_validator.validate_tool(&tool_name)?;
let resolver = VersionResolver::new(&config);
let (version_request, version_source) = match resolver.resolve_version() {
Ok((req, source)) => (req, source),
Err(e @ KopiError::NoLocalVersion { .. }) => {
eprintln!(
"{}",
format_error_with_color(&e, std::io::stderr().is_terminal())
);
std::process::exit(crate::error::get_exit_code(&e));
}
Err(e) => return Err(e),
};
log::debug!("Resolved version: {version_request:?} from {version_source:?}");
security_validator.validate_version(&version_request.version_pattern)?;
if let Some(dist) = &version_request.distribution {
security_validator.validate_version(dist)?;
}
let repository = JdkRepository::new(&config);
let installed_jdk = match find_jdk_installation(&repository, &version_request) {
Ok(jdk) => jdk,
Err(mut err) => {
if let KopiError::JdkNotInstalled {
jdk_spec,
auto_install_enabled: enabled,
..
} = &mut err
{
let auto_installer = AutoInstaller::new(&config, false);
let auto_install_enabled = auto_installer.should_auto_install();
*enabled = auto_install_enabled;
if auto_install_enabled {
let version_spec = if let Some(dist) = &version_request.distribution {
format!("{}@{}", dist, version_request.version_pattern)
} else {
version_request.version_pattern.clone()
};
let should_install = match auto_installer.prompt_user(&version_spec) {
Ok(approved) => approved,
Err(e) => {
eprintln!(
"{}",
format_error_with_color(&e, std::io::stderr().is_terminal())
);
false
}
};
if should_install {
match auto_installer.install_jdk(&version_request) {
Ok(()) => {
match find_jdk_installation(&repository, &version_request) {
Ok(jdk) => jdk,
Err(_) => {
let error = KopiError::JdkNotInstalled {
jdk_spec: jdk_spec.clone(),
version: Some(version_request.version_pattern.clone()),
distribution: version_request.distribution.clone(),
auto_install_enabled,
auto_install_failed: Some(
"Installation succeeded but JDK still not found"
.to_string(),
),
user_declined: false,
install_in_progress: false,
};
eprintln!(
"{}",
format_error_with_color(
&error,
std::io::stderr().is_terminal()
)
);
std::process::exit(crate::error::get_exit_code(&error));
}
}
}
Err(e) => {
if let KopiError::KopiNotFound { .. } = &e {
eprintln!(
"{}",
format_error_with_color(
&e,
std::io::stderr().is_terminal()
)
);
std::process::exit(crate::error::get_exit_code(&e));
}
let error = KopiError::JdkNotInstalled {
jdk_spec: jdk_spec.clone(),
version: Some(version_request.version_pattern.clone()),
distribution: version_request.distribution.clone(),
auto_install_enabled,
auto_install_failed: Some(e.to_string()),
user_declined: false,
install_in_progress: false,
};
eprintln!(
"{}",
format_error_with_color(
&error,
std::io::stderr().is_terminal()
)
);
std::process::exit(crate::error::get_exit_code(&error));
}
}
} else {
let error = KopiError::JdkNotInstalled {
jdk_spec: jdk_spec.clone(),
version: Some(version_request.version_pattern.clone()),
distribution: version_request.distribution.clone(),
auto_install_enabled,
auto_install_failed: None,
user_declined: true,
install_in_progress: false,
};
eprintln!(
"{}",
format_error_with_color(&error, std::io::stderr().is_terminal())
);
std::process::exit(crate::error::get_exit_code(&error));
}
} else {
eprintln!(
"{}",
format_error_with_color(&err, std::io::stderr().is_terminal())
);
std::process::exit(crate::error::get_exit_code(&err));
}
} else {
return Err(err);
}
}
};
log::debug!(
"Found JDK: {} {} at {:?}",
installed_jdk.distribution,
installed_jdk.version,
installed_jdk.path
);
let tool_path = build_tool_path(&installed_jdk, &tool_name)?;
log::debug!("Tool path: {tool_path:?}");
let args: Vec<OsString> = env::args_os().skip(1).collect();
security_validator.validate_path(&tool_path)?;
security_validator.check_permissions(&tool_path)?;
let elapsed = start.elapsed();
log::debug!("Shim resolution completed in {elapsed:?}");
let err = crate::platform::process::exec_replace(&tool_path, args);
Err(KopiError::SystemError(format!(
"Failed to execute {tool_path:?}: {err}"
)))
}
fn get_tool_name() -> Result<String> {
let arg0 = env::args_os()
.next()
.ok_or_else(|| KopiError::SystemError("No argv[0] found".to_string()))?;
let path = PathBuf::from(arg0);
let tool_name = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| KopiError::SystemError("Invalid tool name in argv[0]".to_string()))?;
Ok(tool_name.to_string())
}
fn find_jdk_installation(
repository: &JdkRepository,
version_request: &VersionRequest,
) -> Result<InstalledJdk> {
log::debug!("Finding JDK for version request: {version_request:?}");
let distribution = if let Some(dist_name) = &version_request.distribution {
Distribution::from_str(dist_name)?
} else {
Distribution::Temurin
};
log::debug!("Using distribution: {}", distribution.id());
let installed_jdks = repository.list_installed_jdks()?;
log::debug!("Found {} installed JDKs", installed_jdks.len());
for jdk in installed_jdks {
log::debug!(
"Checking JDK: distribution={}, version={} against request: distribution={}, \
version={}",
jdk.distribution,
jdk.version,
distribution.id(),
version_request.version_pattern
);
if jdk.distribution.to_lowercase() == distribution.id() {
let matches = jdk
.version
.matches_pattern(&version_request.version_pattern);
log::debug!(
"Version matching: installed {} matches pattern {}? {}",
jdk.version,
version_request.version_pattern,
matches
);
if matches {
return Ok(jdk);
}
}
}
Err(KopiError::JdkNotInstalled {
jdk_spec: format!("{}@{}", distribution.id(), version_request.version_pattern),
version: Some(version_request.version_pattern.clone()),
distribution: Some(distribution.id().to_string()),
auto_install_enabled: false, auto_install_failed: None,
user_declined: false,
install_in_progress: false,
})
}
fn build_tool_path(installed_jdk: &InstalledJdk, tool_name: &str) -> Result<PathBuf> {
let bin_dir = installed_jdk.resolve_bin_path()?;
let tool_filename = if crate::platform::executable_extension().is_empty() {
tool_name.to_string()
} else {
format!("{}{}", tool_name, crate::platform::executable_extension())
};
let tool_path = bin_dir.join(tool_filename);
if !tool_path.exists() {
#[cfg(not(test))]
{
let mut available_tools = Vec::new();
if bin_dir.exists()
&& let Ok(entries) = std::fs::read_dir(&bin_dir)
{
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
let tool_name_clean = if cfg!(windows) && name.ends_with(".exe") {
&name[..name.len() - 4]
} else {
name
};
if entry.metadata().map(|m| m.is_file()).unwrap_or(false) {
available_tools.push(tool_name_clean.to_string());
}
}
}
}
available_tools.sort();
let error = KopiError::ToolNotFound {
tool: tool_name.to_string(),
jdk_path: installed_jdk
.path
.to_str()
.unwrap_or("<invalid path>")
.to_string(),
available_tools,
};
eprintln!(
"{}",
format_error_with_color(&error, std::io::stderr().is_terminal())
);
std::process::exit(crate::error::get_exit_code(&error));
}
#[cfg(test)]
return Err(KopiError::SystemError(format!(
"Tool '{tool_name}' not found in JDK at {:?}",
installed_jdk.path
)));
}
Ok(tool_path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KopiConfig;
use crate::paths::install;
use crate::version::Version;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_get_tool_name() {
}
#[test]
fn test_build_tool_path_unix() {
#[cfg(not(target_os = "windows"))]
{
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("test-jdk");
let bin_dir = install::bin_directory(&jdk_path);
fs::create_dir_all(&bin_dir).unwrap();
let java_path = bin_dir.join("java");
fs::write(&java_path, "").unwrap();
let installed_jdk =
InstalledJdk::new("test".to_string(), Version::new(21, 0, 1), jdk_path, false);
let result = build_tool_path(&installed_jdk, "java").unwrap();
assert_eq!(result, java_path);
}
}
#[test]
fn test_build_tool_path_windows() {
#[cfg(target_os = "windows")]
{
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("test-jdk");
let bin_dir = install::bin_directory(&jdk_path);
fs::create_dir_all(&bin_dir).unwrap();
let java_path = bin_dir.join("java.exe");
fs::write(&java_path, "").unwrap();
let installed_jdk =
InstalledJdk::new("test".to_string(), Version::new(21, 0, 1), jdk_path, false);
let result = build_tool_path(&installed_jdk, "java").unwrap();
assert_eq!(result, java_path);
}
}
#[test]
fn test_build_tool_path_not_found() {
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("test-jdk");
let bin_dir = install::bin_directory(&jdk_path);
fs::create_dir_all(&bin_dir).unwrap();
let installed_jdk =
InstalledJdk::new("test".to_string(), Version::new(21, 0, 1), jdk_path, false);
let result = build_tool_path(&installed_jdk, "nonexistent");
assert!(result.is_err());
}
#[test]
fn test_find_jdk_installation_found() {
let temp_dir = TempDir::new().unwrap();
install::ensure_installations_root(temp_dir.path()).unwrap();
let jdk_path = install::installation_directory(temp_dir.path(), "temurin-21.0.1");
fs::create_dir_all(&jdk_path).unwrap();
let _version_request = VersionRequest::new("21".to_string())
.unwrap()
.with_distribution("temurin".to_string());
}
#[test]
fn test_find_jdk_installation_not_found() {
unsafe {
std::env::remove_var("KOPI_AUTO_INSTALL");
std::env::remove_var("KOPI_AUTO_INSTALL__ENABLED");
std::env::remove_var("KOPI_AUTO_INSTALL__PROMPT");
std::env::remove_var("KOPI_AUTO_INSTALL__TIMEOUT_SECS");
}
let temp_dir = TempDir::new().unwrap();
let config = KopiConfig::new(temp_dir.path().to_path_buf()).unwrap();
let repository = JdkRepository::new(&config);
let version_request = VersionRequest::new("99".to_string())
.unwrap()
.with_distribution("nonexistent".to_string());
let result = find_jdk_installation(&repository, &version_request);
assert!(result.is_err());
assert!(matches!(result, Err(KopiError::JdkNotInstalled { .. })));
}
#[test]
fn test_version_matching_logic() {
let installed = Version::from_str("17.0.15").unwrap();
let requested = Version::from_str("17").unwrap();
assert!(installed.matches_pattern(&requested.to_string()));
assert!(Version::from_str("21.0.1").unwrap().matches_pattern("21"));
assert!(
Version::from_str("11.0.21+9")
.unwrap()
.matches_pattern("11")
);
assert!(
Version::from_str("17.0.15")
.unwrap()
.matches_pattern("17.0")
);
assert!(!Version::from_str("17.0.15").unwrap().matches_pattern("18"));
assert!(
!Version::from_str("17.0.15")
.unwrap()
.matches_pattern("17.0.16")
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_build_tool_path_with_bundle_structure() {
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("temurin-21.0.1");
let bundle_home = install::bundle_java_home(&jdk_path);
let bundle_bin_dir = install::bin_directory(&bundle_home);
fs::create_dir_all(&bundle_bin_dir).unwrap();
let java_path = bundle_bin_dir.join("java");
fs::write(&java_path, "").unwrap();
let installed_jdk = InstalledJdk::new(
"temurin".to_string(),
Version::new(21, 0, 1),
jdk_path,
false,
);
let result = build_tool_path(&installed_jdk, "java").unwrap();
assert_eq!(result, java_path);
}
#[test]
#[cfg(target_os = "macos")]
fn test_build_tool_path_with_direct_structure() {
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("liberica-21.0.1");
let bin_dir = install::bin_directory(&jdk_path);
fs::create_dir_all(&bin_dir).unwrap();
let java_path = bin_dir.join("java");
fs::write(&java_path, "").unwrap();
let installed_jdk = InstalledJdk::new(
"liberica".to_string(),
Version::new(21, 0, 1),
jdk_path,
false,
);
let result = build_tool_path(&installed_jdk, "java").unwrap();
assert_eq!(result, java_path);
}
#[test]
fn test_build_tool_path_with_missing_bin() {
let temp_dir = TempDir::new().unwrap();
let jdk_path = temp_dir.path().join("broken-jdk");
fs::create_dir_all(&jdk_path).unwrap();
let installed_jdk = InstalledJdk::new(
"broken".to_string(),
Version::new(21, 0, 1),
jdk_path,
false,
);
let result = build_tool_path(&installed_jdk, "java");
assert!(result.is_err());
}
#[test]
fn test_find_jdk_installation_returns_installed_jdk() {
unsafe {
std::env::remove_var("KOPI_AUTO_INSTALL");
std::env::remove_var("KOPI_AUTO_INSTALL__ENABLED");
std::env::remove_var("KOPI_AUTO_INSTALL__PROMPT");
std::env::remove_var("KOPI_AUTO_INSTALL__TIMEOUT_SECS");
}
let temp_dir = TempDir::new().unwrap();
let config = KopiConfig::new(temp_dir.path().to_path_buf()).unwrap();
let repository = JdkRepository::new(&config);
install::ensure_installations_root(temp_dir.path()).unwrap();
let jdk_path = install::installation_directory(temp_dir.path(), "temurin-21.0.1");
fs::create_dir_all(&jdk_path).unwrap();
let version_request = VersionRequest::new("21".to_string())
.unwrap()
.with_distribution("temurin".to_string());
let bin_dir = install::bin_directory(&jdk_path);
fs::create_dir_all(&bin_dir).unwrap();
let result = find_jdk_installation(&repository, &version_request);
match result {
Ok(jdk) => {
assert_eq!(jdk.distribution, "temurin");
assert_eq!(jdk.version.to_string(), "21.0.1");
assert_eq!(jdk.path, jdk_path);
}
Err(e) => panic!("Expected to find JDK but got error: {e:?}"),
}
}
}