From f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Wed, 25 Feb 2026 16:10:23 +0100 Subject: Feat: Refactor core download logic with concurrency and async features Implement basic unit testing Implement automatic java executable switching based on game version Split loader module into smaller modules Implement basic documentation --- src/minecraft/downloads.rs | 423 +++++++++++++++++++++++++++++++------------- src/minecraft/extraction.rs | 70 +++----- src/minecraft/launcher.rs | 262 ++++++++++++++++++++------- src/minecraft/manifests.rs | 41 +++-- 4 files changed, 549 insertions(+), 247 deletions(-) (limited to 'src/minecraft') diff --git a/src/minecraft/downloads.rs b/src/minecraft/downloads.rs index 7017d3f..8df994e 100644 --- a/src/minecraft/downloads.rs +++ b/src/minecraft/downloads.rs @@ -1,181 +1,360 @@ -use log::{debug, info}; -use reqwest::get; +use std::{ + collections::HashMap, + io::{Write, stdout}, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +use McError::Config; +use Ordering::SeqCst; +use futures::stream::{FuturesUnordered, StreamExt}; +use reqwest::Response; use serde::Deserialize; use tokio::{ - fs::{self, File, create_dir_all}, + fs::{File, create_dir_all, read_to_string, write}, io::AsyncWriteExt, + spawn, + sync::Semaphore, + task::{JoinError, JoinHandle}, }; use crate::{ - config::Config, + config::RuntimeConfig, errors::McError, - minecraft::manifests::{Library, Version}, - platform::paths, + minecraft::manifests::{AssetIndex, Library, LibraryArtifact, Version}, + platform::paths::{assets_directory, client_jar, library_file}, }; +const MAX_CONCURRENT_DOWNLOADS: usize = 100; +const PROGRESS_INITIAL_VALUE: usize = 0; +const ASSET_URL_BASE: &str = "https://resources.download.minecraft.net/"; +const MAX_PROGRESS_PERCENT: f64 = 100.0; +const PROGRESS_PRINT_WIDTH: usize = 3; +const ATOMIC_ORDERING: Ordering = SeqCst; +const DOWNLOAD_COMPLETE_MESSAGE: &str = "All downloads completed successfully!"; +const ASSET_INDEX_MISSING_ERROR: &str = "Missing asset_index for the version"; + +impl From for McError { + fn from(error: JoinError) -> Self { + Config(format!("Task panicked: {}", error)) + } +} + +/// Represents a single file to download, including the URL and destination +/// path. +#[derive(Debug, Clone)] +struct DownloadJob { + url: String, + destination_path: PathBuf, +} + +impl DownloadJob { + fn already_exists(&self) -> bool { self.destination_path.exists() } +} + +/// Represents a single asset entry in the Minecraft asset index. #[derive(Debug, Deserialize)] struct AssetObject { hash: String, - // size: u64, } +/// The Minecraft asset index manifest. #[derive(Debug, Deserialize)] struct AssetIndexManifest { - objects: std::collections::HashMap, + objects: HashMap, } -pub async fn download_all( - config: &Config, - version: &Version, +/// Download all files required to run a specific Minecraft version. +pub async fn download_all_files( + http_client: &reqwest::Client, + runtime_config: &RuntimeConfig, + version_info: &Version, ) -> Result<(), McError> { - download_client(config, version).await?; - download_libraries(config, &version.libraries).await?; - download_assets(config, version).await?; + let assets_directory_path: PathBuf = + ensure_assets_directories_exist(runtime_config).await?; + let asset_manifest: AssetIndexManifest = load_asset_index_manifest( + http_client, + &assets_directory_path, + version_info, + ) + .await?; + + let download_jobs: Vec = build_download_jobs( + runtime_config, + version_info, + &assets_directory_path, + &asset_manifest, + ); + + execute_download_jobs(http_client, download_jobs).await?; + println!("\n{}", DOWNLOAD_COMPLETE_MESSAGE); + Ok(()) } -async fn download_client( - config: &Config, - version: &Version, -) -> Result<(), McError> { - let jar_path = paths::client_jar(config, &version.id)?; - - if jar_path.exists() { - debug!("Client jar already exists: {}", jar_path.display()); - return Ok(()); - } - - info!("Downloading client {}", version.id); - download_file(&version.downloads.client.url, &jar_path).await +/// Ensure the essential assets directories exist. +async fn ensure_assets_directories_exist( + config: &RuntimeConfig, +) -> Result { + let assets_dir = assets_directory(config); + create_dir_all(assets_dir.join("objects")).await?; + create_dir_all(assets_dir.join("indexes")).await?; + Ok(assets_dir) } -async fn download_libraries( - config: &Config, - libraries: &[Library], -) -> Result<(), McError> { - for library in libraries { - if let Some(artifact) = &library.downloads.artifact { - let library_path = paths::library_file(config, &artifact.path)?; - - if !library_path.exists() { - info!("Downloading library {}", artifact.path); - download_file(&artifact.url, &library_path).await?; - } - } - - if let Some(classifiers) = &library.downloads.classifiers { - for (_, native) in classifiers { - let native_path = paths::library_file(config, &native.path)?; +/// Load the asset index manifest for the given Minecraft version. +async fn load_asset_index_manifest( + http_client: &reqwest::Client, + assets_dir: &Path, + version_info: &Version, +) -> Result { + let asset_index: &AssetIndex = version_info + .asset_index + .as_ref() + .ok_or_else(|| Config(ASSET_INDEX_MISSING_ERROR.into()))?; - if native_path.exists() { - continue; - } + let index_file_path: PathBuf = assets_dir + .join("indexes") + .join(format!("{}.json", asset_index.id)); - info!("Downloading native library {}", native.path); - download_file(&native.url, &native_path).await?; - } - } + if !index_file_path.exists() { + download_text_file(http_client, &asset_index.url, &index_file_path) + .await?; } - Ok(()) + let json_string: String = read_to_string(index_file_path).await?; + Ok(serde_json::from_str(&json_string)?) } -async fn download_asset_index( - config: &Config, - version: &Version, -) -> Result { - let assets_dir = paths::assets_dir(config); - create_dir_all(assets_dir.join("indexes")).await?; +fn build_download_jobs( + config: &RuntimeConfig, + version_info: &Version, + assets_dir: &Path, + asset_manifest: &AssetIndexManifest, +) -> Vec { + let mut jobs: Vec = Vec::new(); - let asset_index = version.asset_index.as_ref().ok_or_else(|| { - McError::Config("Missing asset_index in version.json".into()) - })?; + add_client_download_job(&mut jobs, config, version_info); + add_library_download_jobs(&mut jobs, config, version_info); + add_asset_download_jobs(&mut jobs, assets_dir, asset_manifest); - if asset_index.id == "legacy" { - return Err(McError::Config( - "Legacy assetIndex detected – pobierz właściwy version.json".into(), - )); - } + jobs +} - let index_path = assets_dir - .join("indexes") - .join(format!("{}.json", asset_index.id)); +fn add_client_download_job( + jobs: &mut Vec, + config: &RuntimeConfig, + version_info: &Version, +) { + jobs.push(DownloadJob { + url: version_info.downloads.client.url.clone(), + destination_path: client_jar(config, &version_info.id), + }); +} - if index_path.exists() { - let index_data = fs::read_to_string(&index_path).await?; - let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?; - return Ok(manifest); +fn add_library_download_jobs( + jobs: &mut Vec, + config: &RuntimeConfig, + version_info: &Version, +) { + for library in &version_info.libraries { + add_library_artifact_job(jobs, config, library); + add_library_classifier_jobs(jobs, config, library); } +} +fn add_library_artifact_job( + jobs: &mut Vec, + config: &RuntimeConfig, + library: &Library, +) { + let artifact: &LibraryArtifact = match &library.downloads.artifact { + | Some(a) => a, + | None => return, + }; + + jobs.push(DownloadJob { + url: artifact.url.clone(), + destination_path: library_file(config, &artifact.path), + }); +} - info!("Downloading asset index {}", asset_index.id); - let response = get(&asset_index.url).await?; - let manifest_text = response.text().await?; - - fs::write(&index_path, &manifest_text).await?; +fn add_library_classifier_jobs( + jobs: &mut Vec, + config: &RuntimeConfig, + library: &Library, +) { + let classifiers: &HashMap = + match &library.downloads.classifiers { + | Some(values) => values, + | None => return, + }; + + for classifier_entry in classifiers.values() { + jobs.push(DownloadJob { + url: classifier_entry.url.clone(), + destination_path: library_file(config, &classifier_entry.path), + }); + } +} - let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?; - Ok(manifest) +fn add_asset_download_jobs( + jobs: &mut Vec, + assets_dir: &Path, + asset_manifest: &AssetIndexManifest, +) { + for asset_object in asset_manifest.objects.values() { + let prefix: &str = &asset_object.hash[0..2]; + jobs.push(DownloadJob { + url: format!("{}{}/{}", ASSET_URL_BASE, prefix, asset_object.hash), + destination_path: assets_dir + .join("objects") + .join(prefix) + .join(&asset_object.hash), + }); + } } -async fn download_assets( - config: &Config, - version: &Version, +async fn execute_download_jobs( + http_client: &reqwest::Client, + download_jobs: Vec, ) -> Result<(), McError> { - let assets_dir = paths::assets_dir(config); - - create_dir_all(assets_dir.join("objects")).await?; - create_dir_all(assets_dir.join("indexes")).await?; - - let manifest = download_asset_index(config, version).await?; + let total_jobs_count: usize = download_jobs.len(); + let completed_jobs_count: Arc = + Arc::new(AtomicUsize::new(PROGRESS_INITIAL_VALUE)); + let concurrent_download_semaphore: Arc = + Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS)); + + let mut tasks: FuturesUnordered>> = + spawn_missing_download_jobs( + http_client, + download_jobs, + &completed_jobs_count, + total_jobs_count, + concurrent_download_semaphore, + ); - for (logical_path, asset) in &manifest.objects { - let subdir = &asset.hash[0..2]; - let file_path = assets_dir - .join("objects") - .join(subdir) - .join(&asset.hash); + await_download_tasks(&mut tasks).await +} - if file_path.exists() { +fn spawn_missing_download_jobs( + http_client: &reqwest::Client, + download_jobs: Vec, + completed_jobs_counter: &Arc, + total_jobs_count: usize, + concurrent_download_semaphore: Arc, +) -> FuturesUnordered>> { + let download_tasks: FuturesUnordered>> = + FuturesUnordered::new(); + + for job in download_jobs { + if job.already_exists() { + completed_jobs_counter.fetch_add(1, SeqCst); + print_download_progress(completed_jobs_counter, total_jobs_count); continue; } - let url = format!( - "https://resources.download.minecraft.net/{}/{}", - subdir, asset.hash - ); - info!("Downloading asset {} -> {}", logical_path, file_path.display()); - download_file(&url, &file_path).await?; + download_tasks.push(spawn_download_job( + http_client.clone(), + job, + concurrent_download_semaphore.clone(), + completed_jobs_counter.clone(), + total_jobs_count, + )); } - if let Some(asset) = manifest.objects.get("sounds.json") { - let file_path = assets_dir.join("indexes").join("sounds.json"); - if !file_path.exists() { - let subdir = &asset.hash[0..2]; - let url = format!( - "https://resources.download.minecraft.net/{}/{}", - subdir, asset.hash - ); - info!("Downloading sounds.json"); - download_file(&url, &file_path).await?; - } + download_tasks +} + +async fn await_download_tasks( + tasks: &mut FuturesUnordered>>, +) -> Result<(), McError> { + while let Some(task_result) = tasks.next().await { + task_result.map_err(McError::from)??; } + Ok(()) +} +fn spawn_download_job( + http_client: reqwest::Client, + download_job: DownloadJob, + concurrent_download_semaphore: Arc, + completed_jobs_count: Arc, + total_jobs_count: usize, +) -> JoinHandle> { + spawn(async move { + let _permit = concurrent_download_semaphore + .acquire_owned() + .await + .unwrap(); + download_file(&http_client, &download_job).await?; + completed_jobs_count.fetch_add(1, SeqCst); + print_download_progress(&completed_jobs_count, total_jobs_count); + Ok(()) + }) +} + +/// Print the current progress of downloads to stdout. +/// +/// # Parameters +/// - `completed_jobs_count`: Atomic counter of completed download jobs. +/// - `total_jobs_count`: Total number of jobs being processed. +fn print_download_progress( + completed_jobs_count: &AtomicUsize, + total_jobs_count: usize, +) { + let completed_jobs: usize = completed_jobs_count.load(ATOMIC_ORDERING); + let progress_percentage: f64 = ((completed_jobs as f64 + / total_jobs_count as f64) + * MAX_PROGRESS_PERCENT) + .min(MAX_PROGRESS_PERCENT); + + print!( + "\rDownloading game files: {:>width$.0}%", + progress_percentage, + width = PROGRESS_PRINT_WIDTH + ); + stdout().flush().unwrap(); +} + +async fn download_text_file( + http_client: &reqwest::Client, + file_url: &str, + destination_path: &PathBuf, +) -> Result<(), McError> { + let text_content = http_client + .get(file_url) + .send() + .await? + .error_for_status()? + .text() + .await?; + write(destination_path, text_content).await?; Ok(()) } async fn download_file( - url: &str, - path: &std::path::Path, + http_client: &reqwest::Client, + download_job: &DownloadJob, ) -> Result<(), McError> { - if let Some(parent) = path.parent() { - create_dir_all(parent).await?; + if let Some(parent_dir) = download_job.destination_path.parent() { + create_dir_all(parent_dir).await?; } - let response = get(url).await?; - let bytes = response.bytes().await?; - - let mut file = File::create(path).await?; - file.write_all(&bytes).await?; + let response: Response = http_client + .get(&download_job.url) + .send() + .await? + .error_for_status()?; + let mut byte_stream = response.bytes_stream(); + let mut file_handle: File = + File::create(&download_job.destination_path).await?; + + while let Some(chunk) = byte_stream.next().await { + file_handle.write_all(&chunk?).await?; + } Ok(()) } diff --git a/src/minecraft/extraction.rs b/src/minecraft/extraction.rs index b58fd2e..292566f 100644 --- a/src/minecraft/extraction.rs +++ b/src/minecraft/extraction.rs @@ -1,11 +1,17 @@ -use std::{fs, io, path::Path}; +use std::{ + collections::HashMap, + fs, + fs::File, + io, + path::{Path, PathBuf}, +}; -use log::info; -use zip::ZipArchive; +use zip::{read::ZipFile, ZipArchive}; use crate::{ errors::McError, - minecraft::manifests::{Library, Version}, + minecraft::manifests::{LibraryArtifact, Version}, + util::fs::library_allowed, }; pub fn extract_natives( @@ -19,8 +25,6 @@ pub fn extract_natives( .join(&version.id) .join("natives"); - info!("Extracting natives for {} into {:?}", version.id, natives_dir); - if natives_dir.exists() { fs::remove_dir_all(&natives_dir)?; } @@ -31,75 +35,51 @@ pub fn extract_natives( continue; } - let natives = match &lib.natives { + let natives: &HashMap = match &lib.natives { | Some(n) => n, | None => continue, }; - let classifier = match natives.get("linux") { + let classifier: &String = match natives.get("linux") { | Some(c) => c, | None => continue, }; - let classifiers = match &lib.downloads.classifiers { - | Some(c) => c, - | None => continue, - }; + let classifiers: &HashMap = + match &lib.downloads.classifiers { + | Some(c) => c, + | None => continue, + }; - let artifact = match classifiers.get(classifier) { + let artifact: &LibraryArtifact = match classifiers.get(classifier) { | Some(a) => a, | None => continue, }; - let jar_path = cfg + let jar_path: PathBuf = cfg .data_dir .join("minecraft") .join("libraries") .join(&artifact.path); - info!("Extracting natives from {:?}", jar_path); - extract_zip(&jar_path, &natives_dir)?; } Ok(()) } - -fn library_allowed(lib: &Library) -> bool { - let rules = match &lib.rules { - | Some(r) => r, - | None => return true, - }; - - let mut allowed = false; - - for rule in rules { - let os_match = match &rule.os { - | Some(os) => os.name == "linux", - | None => true, - }; - - if os_match { - allowed = rule.action == "allow"; - } - } - - allowed -} - fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { - let file = fs::File::open(jar_path)?; - let mut zip = ZipArchive::new(file)?; + let file: File = File::open(jar_path)?; + let mut zip: ZipArchive = ZipArchive::new(file)?; for i in 0..zip.len() { - let mut entry = zip.by_index(i)?; - let name = entry.name(); + let mut entry: ZipFile = zip.by_index(i)?; + let name: &str = entry.name(); if name.starts_with("META-INF/") { continue; } - let out_path = out_dir.join(name); + let out_path: PathBuf = out_dir.join(name); if entry.is_dir() { fs::create_dir_all(&out_path)?; @@ -110,7 +90,7 @@ fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { fs::create_dir_all(parent)?; } - let mut out_file = fs::File::create(&out_path)?; + let mut out_file: File = File::create(&out_path)?; io::copy(&mut entry, &mut out_file)?; } diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs index 2eeaf21..3cca948 100644 --- a/src/minecraft/launcher.rs +++ b/src/minecraft/launcher.rs @@ -1,64 +1,104 @@ -use std::process::Command; - -use log::{debug, info}; +use paths::{client_jar, library_file, natives_dir}; +use serde::Deserialize; +use std::path::Path; +use std::process::Output; +use std::{ + path::PathBuf, + process::{Command, ExitStatus}, +}; +use McError::Process; use crate::{ - config::Config, - errors::McError, - minecraft::manifests::{Library, Version}, - platform::paths, + config::Config, errors::McError, minecraft::manifests::Version, + platform::paths, util::fs::library_allowed, }; -fn build_classpath( - config: &Config, - version: &Version, -) -> Result { - let sep = if cfg!(windows) { ";" } else { ":" }; - let mut entries = Vec::new(); +#[derive(Debug, Clone, Deserialize)] +pub struct JavaRuntime { + pub major: u8, + pub path: PathBuf, +} + +impl JavaRuntime { + fn validate(&self) -> Result<(), McError> { + let output: Output = Command::new(&self.path) + .arg("-version") + .output() + .map_err(|e| { + McError::Runtime(format!( + "Failed to execute Java at {}: {}", + self.path.display(), + e + )) + })?; + + if !output.status.success() { + return Err(McError::Runtime(format!( + "Invalid Java binary: {}", + self.path.display() + ))); + } + + Ok(()) + } +} + +fn required_java_major(java_major: Option) -> u8 { + java_major.unwrap_or(8) +} + +fn resolve_runtime( + required: u8, + runtimes: &[JavaRuntime], +) -> Result { + let mut candidates: Vec<&JavaRuntime> = runtimes + .iter() + .filter(|r| r.major >= required) + .collect(); + + candidates.sort_by_key(|r| r.major); + + let runtime = candidates.first().ok_or_else(|| { + McError::Runtime(format!( + "No suitable Java runtime found (required: Java {})", + required + )) + })?; + + runtime.validate()?; + Ok(runtime.path.clone()) +} + +fn build_classpath(config: &Config, version: &Version) -> String { + let system_separator: &str = if cfg!(windows) { ";" } else { ":" }; + let mut entries: Vec = Vec::new(); for library in &version.libraries { if !library_allowed(library) { continue; } + if let Some(artifact) = &library.downloads.artifact { - let path = paths::library_file(config, &artifact.path)?; + let path = library_file(config, &artifact.path); entries.push(path.to_string_lossy().to_string()); } } - let client_jar = paths::client_jar(config, &version.id)?; - entries.push(client_jar.to_string_lossy().to_string()); + let client: PathBuf = client_jar(config, &version.id); + entries.push(client.to_string_lossy().to_string()); - Ok(entries.join(sep)) + entries.join(system_separator) } -pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { - let java = &config.java_path; - let classpath = build_classpath(config, version)?; - let natives_dir = paths::natives_dir(config, &version.id); - - if !natives_dir.exists() { - return Err(McError::Runtime(format!( - "Natives folder does not exist: {}", - natives_dir.display() - ))); - } - - let asset_index_id = version - .asset_index - .as_ref() - .ok_or_else(|| { - McError::Runtime("Missing assetIndex in version.json".into()) - })? - .id - .clone(); - - info!("Launching Minecraft {}", version.id); - debug!("Classpath: {}", classpath); - debug!("Natives: {}", natives_dir.display()); - debug!("Asset index: {}", asset_index_id); - - let mut cmd = Command::new(java); +fn build_command( + java_path: PathBuf, + config: &Config, + version: &Version, + natives_dir: &Path, + classpath: String, + asset_index_id: String, +) -> Command { + let mut cmd: Command = Command::new(java_path); cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) .arg(format!("-Djava.library.path={}", natives_dir.display())); @@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--version") .arg(&version.id) .arg("--gameDir") - .arg(paths::game_dir(config)) + .arg(paths::game_directory(config)) .arg("--assetsDir") - .arg(paths::assets_dir(config)) + .arg(paths::assets_directory(config)) .arg("--assetIndex") - .arg(&asset_index_id) + .arg(asset_index_id) .arg("--uuid") .arg(&config.uuid) .arg("--userProperties") @@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { .arg("--userType") .arg("legacy"); - let status = cmd.status()?; + cmd +} + +pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { + let required: u8 = required_java_major( + version + .java_version + .as_ref() + .map(|j| j.major_version), + ); + + let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?; + + let classpath: String = build_classpath(config, version); + let natives_dir: PathBuf = natives_dir(config, &version.id); + + if !natives_dir.exists() { + return Err(McError::Runtime(format!( + "Natives folder does not exist: {}", + natives_dir.display() + ))); + } + + let asset_index_id = version + .asset_index + .as_ref() + .ok_or_else(|| { + McError::Runtime("Missing assetIndex in version.json".into()) + })? + .id + .clone(); + + let mut cmd: Command = build_command( + java_path, + config, + version, + &natives_dir, + classpath, + asset_index_id, + ); + + let status: ExitStatus = cmd.status()?; if !status.success() { - return Err(McError::Process("Minecraft exited with error".into())); + return Err(Process("Minecraft exited with error".into())); } Ok(()) } -fn library_allowed(lib: &Library) -> bool { - let rules = match &lib.rules { - | Some(r) => r, - | None => return true, - }; +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn required_java_major_defaults_to_8() { + assert_eq!(required_java_major(None), 8); + } + + #[test] + fn required_java_major_uses_given_value() { + assert_eq!(required_java_major(Some(17)), 17); + } + + #[test] + fn resolve_runtime_errors_when_no_runtimes() { + let runtimes: Vec = vec![]; + assert!(resolve_runtime(17, &runtimes).is_err()); + } + + #[test] + fn resolve_runtime_picks_lowest_matching_major() { + let runtimes: Vec = vec![ + JavaRuntime { + major: 21, + path: PathBuf::from("/bin/true"), + }, + JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), + }, + ]; + + let result: PathBuf = resolve_runtime(8, &runtimes).unwrap(); + assert_eq!(result, PathBuf::from("/bin/true")); + } - let mut allowed = false; + #[test] + fn resolve_runtime_respects_required_version() { + let runtimes: Vec = vec![ + JavaRuntime { + major: 8, + path: PathBuf::from("/bin/true"), + }, + JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), + }, + ]; + + let result: PathBuf = resolve_runtime(17, &runtimes).unwrap(); + assert_eq!(result, PathBuf::from("/bin/true")); + } - for rule in rules { - let os_match = match &rule.os { - | Some(os) => os.name == "linux", - | None => true, + #[test] + fn validate_succeeds_for_true_binary() { + let runtime = JavaRuntime { + major: 17, + path: PathBuf::from("/bin/true"), }; - if os_match { - allowed = rule.action == "allow"; - } + + assert!(runtime.validate().is_ok()); } - allowed + #[test] + fn validate_fails_for_false_binary() { + let runtime = JavaRuntime { + major: 17, + path: PathBuf::from("/bin/false"), + }; + + assert!(runtime.validate().is_err()); + } } diff --git a/src/minecraft/manifests.rs b/src/minecraft/manifests.rs index 8bdec26..64e38da 100644 --- a/src/minecraft/manifests.rs +++ b/src/minecraft/manifests.rs @@ -1,11 +1,18 @@ #![allow(dead_code)] -use std::collections::HashMap; - -use reqwest; +use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; +use reqwest::get; use serde::Deserialize; +use serde_json::{from_str, Value}; +use std::collections::HashMap; +use McError::Config; -use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JavaVersionInfo { + pub component: String, + pub major_version: u8, +} #[derive(Debug, Deserialize)] pub struct Version { @@ -19,6 +26,10 @@ pub struct Version { #[serde(rename = "assetIndex")] pub asset_index: Option, + + #[serde(default)] + #[serde(rename = "javaVersion")] + pub java_version: Option, } #[derive(Debug, Deserialize)] @@ -83,15 +94,13 @@ pub struct OsRule { pub async fn load_version( cfg: &crate::config::Config, ) -> Result { - let manifest_text = reqwest::get(VERSION_MANIFEST_URL) - .await? - .text() - .await?; - let root: serde_json::Value = serde_json::from_str(&manifest_text)?; + let manifest_text = get(VERSION_MANIFEST_URL).await?.text().await?; + let root: Value = from_str(&manifest_text)?; + let version_id = if cfg.version == "latest" { root["latest"]["release"] .as_str() - .ok_or_else(|| McError::Config("missing latest.release".into()))? + .ok_or_else(|| Config("missing latest.release".into()))? .to_string() } else { cfg.version.clone() @@ -99,21 +108,19 @@ pub async fn load_version( let versions = root["versions"] .as_array() - .ok_or_else(|| McError::Config("missing versions array".into()))?; + .ok_or_else(|| Config("missing versions array".into()))?; let version_entry = versions .iter() .find(|v| v["id"].as_str() == Some(&version_id)) - .ok_or_else(|| { - McError::Config(format!("version '{}' not found", version_id)) - })?; + .ok_or_else(|| Config(format!("version '{}' not found", version_id)))?; let url = version_entry["url"] .as_str() - .ok_or_else(|| McError::Config("missing version url".into()))?; + .ok_or_else(|| Config("missing version url".into()))?; - let version_text = reqwest::get(url).await?.text().await?; - let version: Version = serde_json::from_str(&version_text)?; + let version_text = get(url).await?.text().await?; + let version: Version = from_str(&version_text)?; Ok(version) } -- cgit v1.2.3