From d7f9f1a10b619e0e19134dead91048cbe3c3d9ab Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Wed, 14 Jan 2026 16:37:37 +0800 Subject: feat: implement Maven coordinate parsing and URL construction utilities --- src-tauri/src/core/maven.rs | 254 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src-tauri/src/core/maven.rs (limited to 'src-tauri/src/core/maven.rs') diff --git a/src-tauri/src/core/maven.rs b/src-tauri/src/core/maven.rs new file mode 100644 index 0000000..a930e05 --- /dev/null +++ b/src-tauri/src/core/maven.rs @@ -0,0 +1,254 @@ +//! Maven coordinate parsing and URL construction utilities. +//! +//! Mod loaders like Fabric and Forge specify libraries using Maven coordinates +//! (e.g., `net.fabricmc:fabric-loader:0.14.21`) instead of direct download URLs. +//! This module provides utilities to parse these coordinates and construct +//! download URLs for various Maven repositories. + +use std::path::PathBuf; + +/// Known Maven repository URLs for mod loaders +pub const MAVEN_CENTRAL: &str = "https://repo1.maven.org/maven2/"; +pub const FABRIC_MAVEN: &str = "https://maven.fabricmc.net/"; +pub const FORGE_MAVEN: &str = "https://maven.minecraftforge.net/"; +pub const MOJANG_LIBRARIES: &str = "https://libraries.minecraft.net/"; + +/// Represents a parsed Maven coordinate. +/// +/// Maven coordinates follow the format: `group:artifact:version[:classifier][@extension]` +/// Examples: +/// - `net.fabricmc:fabric-loader:0.14.21` +/// - `org.lwjgl:lwjgl:3.3.1:natives-linux` +/// - `com.example:artifact:1.0@zip` +#[derive(Debug, Clone, PartialEq)] +pub struct MavenCoordinate { + pub group: String, + pub artifact: String, + pub version: String, + pub classifier: Option, + pub extension: String, +} + +impl MavenCoordinate { + /// Parse a Maven coordinate string. + /// + /// # Arguments + /// * `coord` - A string in the format `group:artifact:version[:classifier][@extension]` + /// + /// # Returns + /// * `Some(MavenCoordinate)` if parsing succeeds + /// * `None` if the format is invalid + /// + /// # Examples + /// ``` + /// let coord = MavenCoordinate::parse("net.fabricmc:fabric-loader:0.14.21").unwrap(); + /// assert_eq!(coord.group, "net.fabricmc"); + /// assert_eq!(coord.artifact, "fabric-loader"); + /// assert_eq!(coord.version, "0.14.21"); + /// ``` + pub fn parse(coord: &str) -> Option { + // Handle extension suffix (e.g., @zip) + let (coord_part, extension) = if let Some(at_idx) = coord.rfind('@') { + let ext = &coord[at_idx + 1..]; + let base = &coord[..at_idx]; + (base, ext.to_string()) + } else { + (coord, "jar".to_string()) + }; + + let parts: Vec<&str> = coord_part.split(':').collect(); + + match parts.len() { + 3 => Some(MavenCoordinate { + group: parts[0].to_string(), + artifact: parts[1].to_string(), + version: parts[2].to_string(), + classifier: None, + extension, + }), + 4 => Some(MavenCoordinate { + group: parts[0].to_string(), + artifact: parts[1].to_string(), + version: parts[2].to_string(), + classifier: Some(parts[3].to_string()), + extension, + }), + _ => None, + } + } + + /// Get the relative path for this artifact in a Maven repository. + /// + /// # Returns + /// The path as `group/artifact/version/artifact-version[-classifier].extension` + /// + /// # Examples + /// ``` + /// let coord = MavenCoordinate::parse("net.fabricmc:fabric-loader:0.14.21").unwrap(); + /// assert_eq!(coord.to_path(), "net/fabricmc/fabric-loader/0.14.21/fabric-loader-0.14.21.jar"); + /// ``` + pub fn to_path(&self) -> String { + let group_path = self.group.replace('.', "/"); + let filename = match &self.classifier { + Some(classifier) => { + format!( + "{}-{}-{}.{}", + self.artifact, self.version, classifier, self.extension + ) + } + None => { + format!("{}-{}.{}", self.artifact, self.version, self.extension) + } + }; + + format!("{}/{}/{}/{}", group_path, self.artifact, self.version, filename) + } + + /// Get the local file path for storing this artifact. + /// + /// # Arguments + /// * `libraries_dir` - The base libraries directory + /// + /// # Returns + /// The full path where the library should be stored + pub fn to_local_path(&self, libraries_dir: &PathBuf) -> PathBuf { + let rel_path = self.to_path(); + libraries_dir.join(rel_path.replace('/', std::path::MAIN_SEPARATOR_STR)) + } + + /// Construct the full download URL for this artifact. + /// + /// # Arguments + /// * `base_url` - The Maven repository base URL (e.g., `https://maven.fabricmc.net/`) + /// + /// # Returns + /// The full URL to download the artifact + pub fn to_url(&self, base_url: &str) -> String { + let base = base_url.trim_end_matches('/'); + format!("{}/{}", base, self.to_path()) + } +} + +/// Resolve the download URL for a library. +/// +/// This function handles both: +/// 1. Libraries with explicit download URLs (vanilla Minecraft) +/// 2. Libraries with only Maven coordinates (Fabric/Forge) +/// +/// # Arguments +/// * `name` - The Maven coordinate string +/// * `explicit_url` - An explicit download URL if provided in the library JSON +/// * `maven_url` - A custom Maven repository URL from the library JSON +/// +/// # Returns +/// The resolved download URL +pub fn resolve_library_url(name: &str, explicit_url: Option<&str>, maven_url: Option<&str>) -> Option { + // If there's an explicit URL, use it + if let Some(url) = explicit_url { + return Some(url.to_string()); + } + + // Parse the Maven coordinate + let coord = MavenCoordinate::parse(name)?; + + // Determine the base Maven URL + let base_url = maven_url.unwrap_or_else(|| { + // Guess the repository based on group + if coord.group.starts_with("net.fabricmc") { + FABRIC_MAVEN + } else if coord.group.starts_with("net.minecraftforge") || coord.group.starts_with("cpw.mods") { + FORGE_MAVEN + } else { + MOJANG_LIBRARIES + } + }); + + Some(coord.to_url(base_url)) +} + +/// Get the local storage path for a library. +/// +/// # Arguments +/// * `name` - The Maven coordinate string +/// * `libraries_dir` - The base libraries directory +/// +/// # Returns +/// The path where the library should be stored +pub fn get_library_path(name: &str, libraries_dir: &PathBuf) -> Option { + let coord = MavenCoordinate::parse(name)?; + Some(coord.to_local_path(libraries_dir)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_simple_coordinate() { + let coord = MavenCoordinate::parse("net.fabricmc:fabric-loader:0.14.21").unwrap(); + assert_eq!(coord.group, "net.fabricmc"); + assert_eq!(coord.artifact, "fabric-loader"); + assert_eq!(coord.version, "0.14.21"); + assert_eq!(coord.classifier, None); + assert_eq!(coord.extension, "jar"); + } + + #[test] + fn test_parse_with_classifier() { + let coord = MavenCoordinate::parse("org.lwjgl:lwjgl:3.3.1:natives-linux").unwrap(); + assert_eq!(coord.group, "org.lwjgl"); + assert_eq!(coord.artifact, "lwjgl"); + assert_eq!(coord.version, "3.3.1"); + assert_eq!(coord.classifier, Some("natives-linux".to_string())); + assert_eq!(coord.extension, "jar"); + } + + #[test] + fn test_parse_with_extension() { + let coord = MavenCoordinate::parse("com.example:artifact:1.0@zip").unwrap(); + assert_eq!(coord.extension, "zip"); + } + + #[test] + fn test_to_path() { + let coord = MavenCoordinate::parse("net.fabricmc:fabric-loader:0.14.21").unwrap(); + assert_eq!( + coord.to_path(), + "net/fabricmc/fabric-loader/0.14.21/fabric-loader-0.14.21.jar" + ); + } + + #[test] + fn test_to_path_with_classifier() { + let coord = MavenCoordinate::parse("org.lwjgl:lwjgl:3.3.1:natives-linux").unwrap(); + assert_eq!( + coord.to_path(), + "org/lwjgl/lwjgl/3.3.1/lwjgl-3.3.1-natives-linux.jar" + ); + } + + #[test] + fn test_to_url() { + let coord = MavenCoordinate::parse("net.fabricmc:fabric-loader:0.14.21").unwrap(); + assert_eq!( + coord.to_url(FABRIC_MAVEN), + "https://maven.fabricmc.net/net/fabricmc/fabric-loader/0.14.21/fabric-loader-0.14.21.jar" + ); + } + + #[test] + fn test_resolve_library_url_explicit() { + let url = resolve_library_url( + "net.fabricmc:fabric-loader:0.14.21", + Some("https://example.com/lib.jar"), + None, + ); + assert_eq!(url, Some("https://example.com/lib.jar".to_string())); + } + + #[test] + fn test_resolve_library_url_fabric() { + let url = resolve_library_url("net.fabricmc:fabric-loader:0.14.21", None, None); + assert!(url.unwrap().starts_with(FABRIC_MAVEN)); + } +} -- cgit v1.2.3-70-g09d2 From 9193112aca842dbe4d723aa865a7a30f3bcdb691 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Wed, 14 Jan 2026 16:44:56 +0800 Subject: refactor: clean up formatting and improve readability in core modules --- src-tauri/src/core/fabric.rs | 11 ++++++++--- src-tauri/src/core/forge.rs | 13 ++++++++----- src-tauri/src/core/manifest.rs | 6 +++--- src-tauri/src/core/maven.rs | 15 ++++++++++++--- src-tauri/src/main.rs | 35 ++++++++++++++++------------------- 5 files changed, 47 insertions(+), 33 deletions(-) (limited to 'src-tauri/src/core/maven.rs') diff --git a/src-tauri/src/core/fabric.rs b/src-tauri/src/core/fabric.rs index 8fc960f..fd38f41 100644 --- a/src-tauri/src/core/fabric.rs +++ b/src-tauri/src/core/fabric.rs @@ -89,7 +89,8 @@ pub struct InstalledFabricVersion { /// /// # Returns /// A list of game versions that have Fabric intermediary mappings available. -pub async fn fetch_supported_game_versions() -> Result, Box> { +pub async fn fetch_supported_game_versions( +) -> Result, Box> { let url = format!("{}/versions/game", FABRIC_META_URL); let resp = reqwest::get(&url) .await? @@ -102,7 +103,8 @@ pub async fn fetch_supported_game_versions() -> Result, B /// /// # Returns /// A list of all Fabric loader versions, ordered by build number (newest first). -pub async fn fetch_loader_versions() -> Result, Box> { +pub async fn fetch_loader_versions( +) -> Result, Box> { let url = format!("{}/versions/loader", FABRIC_META_URL); let resp = reqwest::get(&url) .await? @@ -145,7 +147,10 @@ pub async fn fetch_version_profile( "{}/versions/loader/{}/{}/profile/json", FABRIC_META_URL, game_version, loader_version ); - let resp = reqwest::get(&url).await?.json::().await?; + let resp = reqwest::get(&url) + .await? + .json::() + .await?; Ok(resp) } diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs index 7b951ff..0f17bcc 100644 --- a/src-tauri/src/core/forge.rs +++ b/src-tauri/src/core/forge.rs @@ -48,7 +48,7 @@ pub struct InstalledForgeVersion { /// A list of Minecraft version strings that have Forge available. pub async fn fetch_supported_game_versions() -> Result, Box> { let promos = fetch_promotions().await?; - + let mut versions: Vec = promos .promos .keys() @@ -62,12 +62,12 @@ pub async fn fetch_supported_game_versions() -> Result, Box Result> { let version_id = generate_version_id(game_version, forge_version); - + // Create basic version JSON structure // Note: This is a simplified version. Full Forge installation requires // downloading the installer and running processors. @@ -206,7 +206,10 @@ fn create_forge_version_json( vec![ create_library_entry(&forge_maven_coord, Some(FORGE_MAVEN_URL)), create_library_entry( - &format!("net.minecraftforge:forge:{}-{}:universal", game_version, forge_version), + &format!( + "net.minecraftforge:forge:{}-{}:universal", + game_version, forge_version + ), Some(FORGE_MAVEN_URL), ), ], diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs index 2fea811..bae87c9 100644 --- a/src-tauri/src/core/manifest.rs +++ b/src-tauri/src/core/manifest.rs @@ -74,7 +74,7 @@ pub async fn fetch_vanilla_version( ) -> Result> { // First, get the manifest to find the version URL let manifest = fetch_version_manifest().await?; - + let version_entry = manifest .versions .iter() @@ -86,7 +86,7 @@ pub async fn fetch_vanilla_version( .await? .json::() .await?; - + Ok(resp) } @@ -121,7 +121,7 @@ pub async fn load_version( Ok(v) => v, Err(_) => fetch_vanilla_version(&parent_id).await?, }; - + // Merge child into parent version = crate::core::version_merge::merge_versions(version, parent); } diff --git a/src-tauri/src/core/maven.rs b/src-tauri/src/core/maven.rs index a930e05..8c89768 100644 --- a/src-tauri/src/core/maven.rs +++ b/src-tauri/src/core/maven.rs @@ -101,7 +101,10 @@ impl MavenCoordinate { } }; - format!("{}/{}/{}/{}", group_path, self.artifact, self.version, filename) + format!( + "{}/{}/{}/{}", + group_path, self.artifact, self.version, filename + ) } /// Get the local file path for storing this artifact. @@ -142,7 +145,11 @@ impl MavenCoordinate { /// /// # Returns /// The resolved download URL -pub fn resolve_library_url(name: &str, explicit_url: Option<&str>, maven_url: Option<&str>) -> Option { +pub fn resolve_library_url( + name: &str, + explicit_url: Option<&str>, + maven_url: Option<&str>, +) -> Option { // If there's an explicit URL, use it if let Some(url) = explicit_url { return Some(url.to_string()); @@ -156,7 +163,9 @@ pub fn resolve_library_url(name: &str, explicit_url: Option<&str>, maven_url: Op // Guess the repository based on group if coord.group.starts_with("net.fabricmc") { FABRIC_MAVEN - } else if coord.group.starts_with("net.minecraftforge") || coord.group.starts_with("cpw.mods") { + } else if coord.group.starts_with("net.minecraftforge") + || coord.group.starts_with("cpw.mods") + { FORGE_MAVEN } else { MOJANG_LIBRARIES diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2261273..ba16f7a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -41,7 +41,7 @@ impl MsRefreshTokenState { } /// Check if a string contains unresolved placeholders in the form ${...} -/// +/// /// After the replacement phase, if a string still contains ${...}, it means /// that placeholder variable was not found in the replacements map and is /// therefore unresolved. We should skip adding such arguments to avoid @@ -119,11 +119,11 @@ async fn start_game( window, format!("Loading version details for {}...", version_id) ); - + let version_details = core::manifest::load_version(&game_dir, &version_id) .await .map_err(|e| e.to_string())?; - + emit_log!( window, format!( @@ -145,7 +145,9 @@ async fn start_game( // --- Client Jar --- // Get downloads from version_details (may be inherited) - let downloads = version_details.downloads.as_ref() + let downloads = version_details + .downloads + .as_ref() .ok_or("Version has no downloads information")?; let client_jar = &downloads.client; let mut client_path = game_dir.join("versions"); @@ -222,12 +224,11 @@ async fn start_game( } else { // 3. Library without explicit downloads (mod loader libraries) // Use Maven coordinate resolution - if let Some(url) = core::maven::resolve_library_url( - &lib.name, - None, - lib.url.as_deref(), - ) { - if let Some(lib_path) = core::maven::get_library_path(&lib.name, &libraries_dir) { + if let Some(url) = + core::maven::resolve_library_url(&lib.name, None, lib.url.as_deref()) + { + if let Some(lib_path) = core::maven::get_library_path(&lib.name, &libraries_dir) + { download_tasks.push(core::downloader::DownloadTask { url, path: lib_path, @@ -246,7 +247,9 @@ async fn start_game( let indexes_dir = assets_dir.join("indexes"); // Get asset index (may be inherited from parent) - let asset_index = version_details.asset_index.as_ref() + let asset_index = version_details + .asset_index + .as_ref() .ok_or("Version has no asset index information")?; // Download Asset Index JSON @@ -262,10 +265,7 @@ async fn start_game( .await .map_err(|e| e.to_string())? } else { - println!( - "Downloading asset index from {}", - asset_index.url - ); + println!("Downloading asset index from {}", asset_index.url); let content = reqwest::get(&asset_index.url) .await .map_err(|e| e.to_string())? @@ -426,10 +426,7 @@ async fn start_game( replacements.insert("${version_name}", version_id.clone()); replacements.insert("${game_directory}", game_dir.to_string_lossy().to_string()); replacements.insert("${assets_root}", assets_dir.to_string_lossy().to_string()); - replacements.insert( - "${assets_index_name}", - asset_index.id.clone(), - ); + replacements.insert("${assets_index_name}", asset_index.id.clone()); replacements.insert("${auth_uuid}", account.uuid()); replacements.insert("${auth_access_token}", account.access_token()); replacements.insert("${user_type}", "mojang".to_string()); -- cgit v1.2.3-70-g09d2