use crate::error::{Result, LateJavaCoreError};
use crate::downloader::Downloader;
use crate::utils::get_file_from_archive;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct JavaDownloaderOptions {
pub path: String,
pub java: JavaOptions,
pub intel_enabled_mac: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct JavaOptions {
pub version: Option<String>,
pub java_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftVersionJson {
pub java_version: Option<JavaVersion>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JavaVersion {
pub component: Option<String>,
pub major_version: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct JavaDownloadResult {
pub files: Vec<JavaFileItem>,
pub path: String,
pub error: Option<bool>,
pub message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct JavaFileItem {
pub path: String,
pub executable: Option<bool>,
pub sha1: Option<String>,
pub size: Option<u64>,
pub url: Option<String>,
pub r#type: Option<String>,
}
pub struct JavaDownloader {
options: JavaDownloaderOptions,
}
impl JavaDownloader {
pub fn new(options: JavaDownloaderOptions) -> Self {
Self { options }
}
pub async fn get_java_files(&self, json_version: &MinecraftVersionJson) -> Result<JavaDownloadResult> {
if self.options.java.version.is_some() {
return self.get_java_other(json_version, self.options.java.version.as_deref()).await;
}
let arch_mapping: std::collections::HashMap<&str, std::collections::HashMap<&str, &str>> = [
("windows", [("x86_64", "windows-x64"), ("x86", "windows-x86"), ("aarch64", "windows-arm64")].iter().cloned().collect()),
("macos", [("x86_64", "mac-os"), ("aarch64", if self.options.intel_enabled_mac.unwrap_or(false) { "mac-os" } else { "mac-os-arm64" })].iter().cloned().collect()),
("linux", [("x86_64", "linux"), ("x86", "linux-i386")].iter().cloned().collect()),
].iter().cloned().collect();
let os_platform = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let java_version_name = json_version.java_version
.as_ref()
.and_then(|jv| jv.component.as_deref())
.unwrap_or("jre-legacy");
let os_arch_mapping = arch_mapping.get(os_platform);
if os_arch_mapping.is_none() {
return self.get_java_other(json_version, None).await;
}
let os_arch_mapping = os_arch_mapping.unwrap();
let arch_os = os_arch_mapping.get(arch);
if arch_os.is_none() {
return self.get_java_other(json_version, None).await;
}
let arch_os = arch_os.unwrap();
let url = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
let response = reqwest::get(url).await?;
let java_versions_json: serde_json::Value = response.json().await?;
let version_name = java_versions_json[arch_os][java_version_name][0]["version"]["name"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Java("No version name found".to_string()))?;
let manifest_url = java_versions_json[arch_os][java_version_name][0]["manifest"]["url"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Java("No manifest URL found".to_string()))?;
let manifest_response = reqwest::get(manifest_url).await?;
let manifest: serde_json::Value = manifest_response.json().await?;
let manifest_entries: Vec<(String, serde_json::Value)> = manifest["files"]
.as_object()
.unwrap()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let java_exe_key = if std::env::consts::OS == "windows" {
"bin/javaw.exe"
} else {
"bin/java"
};
let java_entry = manifest_entries.iter()
.find(|(rel_path, _)| rel_path.ends_with(java_exe_key))
.ok_or_else(|| LateJavaCoreError::Java("Java executable not found in manifest".to_string()))?;
let to_delete = java_entry.0.replace(java_exe_key, "");
let mut files = Vec::new();
for (rel_path, info) in manifest_entries {
if info["type"] == "directory" {
continue;
}
if info["downloads"].is_null() {
continue;
}
files.push(JavaFileItem {
path: format!("runtime/jre-{}-{}/{}", version_name, arch_os, rel_path.replace(&to_delete, "")),
executable: info["executable"].as_bool(),
sha1: info["downloads"]["raw"]["sha1"].as_str().map(|s| s.to_string()),
size: info["downloads"]["raw"]["size"].as_u64(),
url: info["downloads"]["raw"]["url"].as_str().map(|s| s.to_string()),
r#type: Some("Java".to_string()),
});
}
let java_path = Path::new(&self.options.path)
.join(format!("runtime/jre-{}-{}", version_name, arch_os))
.join("bin")
.join(if std::env::consts::OS == "windows" { "javaw.exe" } else { "java" });
Ok(JavaDownloadResult {
files,
path: java_path.to_string_lossy().to_string(),
error: None,
message: None,
})
}
pub async fn get_java_other(&self, json_version: &MinecraftVersionJson, version_download: Option<&str>) -> Result<JavaDownloadResult> {
let (platform, arch) = self.get_platform_arch();
let major_version = version_download
.and_then(|v| v.parse().ok())
.or_else(|| json_version.java_version.as_ref().and_then(|jv| jv.major_version))
.unwrap_or(8);
let path_folder = Path::new(&self.options.path).join(format!("runtime/jre-{}", major_version));
let mut query_params = std::collections::HashMap::new();
query_params.insert("java_version", major_version.to_string());
query_params.insert("os", platform);
query_params.insert("arch", arch);
query_params.insert("archive_type", "zip");
query_params.insert("java_package_type", &self.options.java.java_type);
let query_string: String = query_params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
let java_version_url = format!("https://api.azul.com/metadata/v1/zulu/packages/?{}", query_string);
let response = reqwest::get(&java_version_url).await?;
let java_versions: serde_json::Value = response.json().await?;
let java_versions = java_versions.as_array()
.and_then(|arr| arr.first())
.ok_or_else(|| LateJavaCoreError::Java("No Java versions found for the specified parameters".to_string()))?;
let name = java_versions["name"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Java("No name in Java version response".to_string()))?;
let download_url = java_versions["download_url"]
.as_str()
.ok_or_else(|| LateJavaCoreError::Java("No download URL in Java version response".to_string()))?;
let mut java_exe_path = path_folder.join(name.replace(".zip", "")).join("bin").join("java");
if platform == "macos" {
let bin_path = path_folder.join(name.replace(".zip", "")).join("bin");
if bin_path.exists() {
if let Ok(bin_content) = std::fs::read_to_string(&bin_path) {
java_exe_path = path_folder.join(name.replace(".zip", "")).join(bin_content.trim()).join("java");
}
}
}
if !java_exe_path.exists() {
let zip_path = path_folder.join(name);
self.verify_and_download_file(&zip_path, &path_folder, name, download_url).await?;
let entries = get_file_from_archive(&zip_path.to_string_lossy(), None, None, true).await?;
for entry in entries {
if entry.name.starts_with("META-INF") {
continue;
}
let entry_path = path_folder.join(&entry.name);
if entry.is_directory {
std::fs::create_dir_all(&entry_path)?;
} else {
std::fs::write(&entry_path, entry.data)?;
}
}
if platform == "macos" {
let bin_path = path_folder.join(name.replace(".zip", "")).join("bin");
if bin_path.exists() {
if let Ok(bin_content) = std::fs::read_to_string(&bin_path) {
java_exe_path = path_folder.join(name.replace(".zip", "")).join(bin_content.trim()).join("java");
}
}
}
}
Ok(JavaDownloadResult {
files: Vec::new(),
path: java_exe_path.to_string_lossy().to_string(),
error: None,
message: None,
})
}
fn get_platform_arch(&self) -> (String, String) {
let platform_map: std::collections::HashMap<&str, &str> = [
("windows", "windows"),
("macos", "macos"),
("linux", "linux"),
].iter().cloned().collect();
let arch_map: std::collections::HashMap<&str, &str> = [
("x86_64", "x64"),
("x86", "x32"),
("aarch64", "aarch64"),
("arm", "arm"),
].iter().cloned().collect();
let current_os = std::env::consts::OS;
let current_arch = std::env::consts::ARCH;
let mapped_platform = platform_map.get(current_os).unwrap_or(¤t_os);
let mut mapped_arch = arch_map.get(current_arch).unwrap_or(¤t_arch);
if current_os == "macos" && current_arch == "aarch64" && self.options.intel_enabled_mac.unwrap_or(false) {
mapped_arch = &"x64";
}
(mapped_platform.to_string(), mapped_arch.to_string())
}
async fn verify_and_download_file(&self, file_path: &Path, path_folder: &Path, file_name: &str, url: &str) -> Result<()> {
if !file_path.exists() {
std::fs::create_dir_all(path_folder)?;
let downloader = Downloader::new();
downloader.download_file(url, &path_folder.to_string_lossy(), file_name).await?;
}
Ok(())
}
}