diff options
| author | 2026-01-14 16:37:37 +0800 | |
|---|---|---|
| committer | 2026-01-14 16:38:35 +0800 | |
| commit | d7f9f1a10b619e0e19134dead91048cbe3c3d9ab (patch) | |
| tree | 9826f8a1d6220f84d818c92730f76c3b4edacedc /src-tauri | |
| parent | e861e52d43e93478690c3a20903639f7773b8ce8 (diff) | |
| download | DropOut-d7f9f1a10b619e0e19134dead91048cbe3c3d9ab.tar.gz DropOut-d7f9f1a10b619e0e19134dead91048cbe3c3d9ab.zip | |
feat: implement Maven coordinate parsing and URL construction utilities
Diffstat (limited to 'src-tauri')
| -rw-r--r-- | src-tauri/src/core/manifest.rs | 154 | ||||
| -rw-r--r-- | src-tauri/src/core/maven.rs | 254 |
2 files changed, 407 insertions, 1 deletions
diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs index 11ebc5a..2fea811 100644 --- a/src-tauri/src/core/manifest.rs +++ b/src-tauri/src/core/manifest.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; use std::error::Error; +use std::path::PathBuf; + +use crate::core::game_version::GameVersion; #[derive(Debug, Deserialize, Serialize)] pub struct VersionManifest { @@ -24,8 +27,157 @@ pub struct Version { pub release_time: String, } -pub async fn fetch_version_manifest() -> Result<VersionManifest, Box<dyn Error>> { +pub async fn fetch_version_manifest() -> Result<VersionManifest, Box<dyn Error + Send + Sync>> { let url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; let resp = reqwest::get(url).await?.json::<VersionManifest>().await?; Ok(resp) } + +/// Load a version JSON from the local versions directory. +/// +/// This is used for loading both vanilla and modded versions that have been +/// previously downloaded or installed. +/// +/// # Arguments +/// * `game_dir` - The .minecraft directory path +/// * `version_id` - The version ID to load +/// +/// # Returns +/// The parsed `GameVersion` if found, or an error if not found. +pub async fn load_local_version( + game_dir: &PathBuf, + version_id: &str, +) -> Result<GameVersion, Box<dyn Error + Send + Sync>> { + let json_path = game_dir + .join("versions") + .join(version_id) + .join(format!("{}.json", version_id)); + + if !json_path.exists() { + return Err(format!("Version {} not found locally", version_id).into()); + } + + let content = tokio::fs::read_to_string(&json_path).await?; + let version: GameVersion = serde_json::from_str(&content)?; + Ok(version) +} + +/// Fetch a version JSON from Mojang's servers. +/// +/// # Arguments +/// * `version_id` - The version ID to fetch +/// +/// # Returns +/// The parsed `GameVersion` from Mojang's API. +pub async fn fetch_vanilla_version( + version_id: &str, +) -> Result<GameVersion, Box<dyn Error + Send + Sync>> { + // First, get the manifest to find the version URL + let manifest = fetch_version_manifest().await?; + + let version_entry = manifest + .versions + .iter() + .find(|v| v.id == version_id) + .ok_or_else(|| format!("Version {} not found in manifest", version_id))?; + + // Fetch the actual version JSON + let resp = reqwest::get(&version_entry.url) + .await? + .json::<GameVersion>() + .await?; + + Ok(resp) +} + +/// Load a version, checking local first, then fetching from remote if needed. +/// +/// For modded versions (those with `inheritsFrom`), this will also resolve +/// the inheritance chain. +/// +/// # Arguments +/// * `game_dir` - The .minecraft directory path +/// * `version_id` - The version ID to load +/// +/// # Returns +/// A fully resolved `GameVersion` ready for launching. +pub async fn load_version( + game_dir: &PathBuf, + version_id: &str, +) -> Result<GameVersion, Box<dyn Error + Send + Sync>> { + // Try loading from local first + let mut version = match load_local_version(game_dir, version_id).await { + Ok(v) => v, + Err(_) => { + // Not found locally, try fetching from Mojang + fetch_vanilla_version(version_id).await? + } + }; + + // If this version inherits from another, resolve the inheritance iteratively + while let Some(parent_id) = version.inherits_from.clone() { + // Load the parent version + let parent = match load_local_version(game_dir, &parent_id).await { + Ok(v) => v, + Err(_) => fetch_vanilla_version(&parent_id).await?, + }; + + // Merge child into parent + version = crate::core::version_merge::merge_versions(version, parent); + } + + Ok(version) +} + +/// Save a version JSON to the local versions directory. +/// +/// # Arguments +/// * `game_dir` - The .minecraft directory path +/// * `version` - The version to save +/// +/// # Returns +/// The path where the JSON was saved. +pub async fn save_local_version( + game_dir: &PathBuf, + version: &GameVersion, +) -> Result<PathBuf, Box<dyn Error + Send + Sync>> { + let version_dir = game_dir.join("versions").join(&version.id); + tokio::fs::create_dir_all(&version_dir).await?; + + let json_path = version_dir.join(format!("{}.json", version.id)); + let content = serde_json::to_string_pretty(version)?; + tokio::fs::write(&json_path, content).await?; + + Ok(json_path) +} + +/// List all locally installed versions. +/// +/// # Arguments +/// * `game_dir` - The .minecraft directory path +/// +/// # Returns +/// A list of version IDs found in the versions directory. +pub async fn list_local_versions( + game_dir: &PathBuf, +) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> { + let versions_dir = game_dir.join("versions"); + let mut versions = Vec::new(); + + if !versions_dir.exists() { + return Ok(versions); + } + + let mut entries = tokio::fs::read_dir(&versions_dir).await?; + while let Some(entry) = entries.next_entry().await? { + if entry.file_type().await?.is_dir() { + let name = entry.file_name().to_string_lossy().to_string(); + let json_path = entry.path().join(format!("{}.json", name)); + if json_path.exists() { + versions.push(name); + } + } + } + + Ok(versions) +} 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<String>, + 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<Self> { + // 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<String> { + // 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<PathBuf> { + 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)); + } +} |