diff options
| author | Filip Wandzio <contact@philw.dev> | 2026-01-22 23:14:08 +0100 |
|---|---|---|
| committer | Filip Wandzio <contact@philw.dev> | 2026-01-22 23:14:08 +0100 |
| commit | 72ddd7b7704f2087a52c9c0552446682918c513b (patch) | |
| tree | e5134f215ea82c1fc8eda17b34e426a7b1dfafc6 /src | |
| download | dml-72ddd7b7704f2087a52c9c0552446682918c513b.tar.gz dml-72ddd7b7704f2087a52c9c0552446682918c513b.zip | |
Implement basic game files download logic
Implement core clap arguments
Respect XDG_BASE_DIR
Currently library extraction is broken because it assumes every instace has it's own library folder. This should be refactored so instances share libraries
Signed-off-by: Filip Wandzio <contact@philw.dev>
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/loader.rs | 70 | ||||
| -rw-r--r-- | src/config/mod.rs | 3 | ||||
| -rw-r--r-- | src/constants.rs | 13 | ||||
| -rw-r--r-- | src/errors.rs | 34 | ||||
| -rw-r--r-- | src/main.rs | 60 | ||||
| -rw-r--r-- | src/minecraft/downloads.rs | 66 | ||||
| -rw-r--r-- | src/minecraft/extraction.rs | 10 | ||||
| -rw-r--r-- | src/minecraft/launcher.rs | 73 | ||||
| -rw-r--r-- | src/minecraft/manifests.rs | 80 | ||||
| -rw-r--r-- | src/minecraft/mod.rs | 4 | ||||
| -rw-r--r-- | src/platform/mod.rs | 1 | ||||
| -rw-r--r-- | src/platform/paths.rs | 48 | ||||
| -rw-r--r-- | src/util/fs.rs | 12 | ||||
| -rw-r--r-- | src/util/mod.rs | 2 | ||||
| -rw-r--r-- | src/util/sha1.rs | 14 |
15 files changed, 490 insertions, 0 deletions
diff --git a/src/config/loader.rs b/src/config/loader.rs new file mode 100644 index 0000000..81a4351 --- /dev/null +++ b/src/config/loader.rs | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | use std::{env, path::PathBuf}; | ||
| 2 | |||
| 3 | use serde::Deserialize; | ||
| 4 | |||
| 5 | use crate::{constants::*, errors::McError}; | ||
| 6 | |||
| 7 | #[allow(dead_code)] | ||
| 8 | #[derive(Debug, Deserialize)] | ||
| 9 | pub struct Config { | ||
| 10 | pub username: String, | ||
| 11 | pub uuid: String, | ||
| 12 | pub version: String, | ||
| 13 | pub java_path: String, | ||
| 14 | pub max_memory_mb: u32, | ||
| 15 | pub data_dir: PathBuf, | ||
| 16 | pub cache_dir: PathBuf, | ||
| 17 | pub config_dir: PathBuf, | ||
| 18 | #[serde(default)] | ||
| 19 | pub jvm_args: Vec<String>, | ||
| 20 | } | ||
| 21 | |||
| 22 | impl Config { | ||
| 23 | pub fn load() -> Result<Self, McError> { | ||
| 24 | let cfg_path = default_config_path()?; | ||
| 25 | let mut cfg: Config = if cfg_path.exists() { | ||
| 26 | let txt = std::fs::read_to_string(&cfg_path)?; | ||
| 27 | toml::from_str(&txt).map_err(|e| McError::Config(e.to_string()))? | ||
| 28 | } else { | ||
| 29 | Self::default() | ||
| 30 | }; | ||
| 31 | |||
| 32 | if let Ok(v) = env::var("MC_USERNAME") { | ||
| 33 | cfg.username = v; | ||
| 34 | } | ||
| 35 | if let Ok(v) = env::var("MC_VERSION") { | ||
| 36 | cfg.version = v; | ||
| 37 | } | ||
| 38 | if let Ok(v) = env::var("MC_JAVA_PATH") { | ||
| 39 | cfg.java_path = v; | ||
| 40 | } | ||
| 41 | if let Ok(v) = env::var("MC_MAX_MEMORY_MB") { | ||
| 42 | cfg.max_memory_mb = v.parse().unwrap_or(cfg.max_memory_mb); | ||
| 43 | } | ||
| 44 | |||
| 45 | Ok(cfg) | ||
| 46 | } | ||
| 47 | |||
| 48 | fn default() -> Self { | ||
| 49 | let base = | ||
| 50 | directories::ProjectDirs::from("com", "example", "mccl").expect("platform dirs"); | ||
| 51 | |||
| 52 | Self { | ||
| 53 | username: "Player".into(), | ||
| 54 | uuid: uuid::Uuid::new_v4().to_string(), | ||
| 55 | version: DEFAULT_VERSION.into(), | ||
| 56 | java_path: DEFAULT_JAVA_PATH.into(), | ||
| 57 | max_memory_mb: DEFAULT_MAX_MEMORY_MB, | ||
| 58 | data_dir: base.data_dir().into(), | ||
| 59 | cache_dir: base.cache_dir().into(), | ||
| 60 | config_dir: base.config_dir().into(), | ||
| 61 | jvm_args: vec![], | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | fn default_config_path() -> Result<PathBuf, McError> { | ||
| 67 | let base = directories::ProjectDirs::from("com", "example", "mccl") | ||
| 68 | .ok_or_else(|| McError::Config("cannot determine config dir".into()))?; | ||
| 69 | Ok(base.config_dir().join("config.toml")) | ||
| 70 | } | ||
diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..c5fc004 --- /dev/null +++ b/src/config/mod.rs | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | pub mod loader; | ||
| 2 | |||
| 3 | pub use loader::Config; | ||
diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..52833b2 --- /dev/null +++ b/src/constants.rs | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | |||
| 3 | use std::time::Duration; | ||
| 4 | |||
| 5 | pub const VERSION_MANIFEST_URL: &str = | ||
| 6 | "https://launchermeta.mojang.com/mc/game/version_manifest.json"; | ||
| 7 | |||
| 8 | pub const DOWNLOAD_RETRIES: usize = 3; | ||
| 9 | pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); | ||
| 10 | |||
| 11 | pub const DEFAULT_MAX_MEMORY_MB: u32 = 2048; | ||
| 12 | pub const DEFAULT_JAVA_PATH: &str = "java"; | ||
| 13 | pub const DEFAULT_VERSION: &str = "latest"; | ||
diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..c167733 --- /dev/null +++ b/src/errors.rs | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | use std::{fmt, io}; | ||
| 2 | |||
| 3 | #[allow(dead_code)] | ||
| 4 | #[derive(Debug)] | ||
| 5 | pub enum McError { | ||
| 6 | Io(io::Error), | ||
| 7 | Http(reqwest::Error), | ||
| 8 | Json(serde_json::Error), | ||
| 9 | Zip(zip::result::ZipError), | ||
| 10 | Config(String), | ||
| 11 | ShaMismatch(String), | ||
| 12 | Process(String), | ||
| 13 | Runtime(String), // ← NEW | ||
| 14 | } | ||
| 15 | |||
| 16 | impl fmt::Display for McError { | ||
| 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } | ||
| 18 | } | ||
| 19 | |||
| 20 | impl From<io::Error> for McError { | ||
| 21 | fn from(e: io::Error) -> Self { Self::Io(e) } | ||
| 22 | } | ||
| 23 | |||
| 24 | impl From<reqwest::Error> for McError { | ||
| 25 | fn from(e: reqwest::Error) -> Self { Self::Http(e) } | ||
| 26 | } | ||
| 27 | |||
| 28 | impl From<serde_json::Error> for McError { | ||
| 29 | fn from(e: serde_json::Error) -> Self { Self::Json(e) } | ||
| 30 | } | ||
| 31 | |||
| 32 | impl From<zip::result::ZipError> for McError { | ||
| 33 | fn from(e: zip::result::ZipError) -> Self { Self::Zip(e) } | ||
| 34 | } | ||
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8a01b9f --- /dev/null +++ b/src/main.rs | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | mod constants; | ||
| 2 | mod errors; | ||
| 3 | |||
| 4 | mod config; | ||
| 5 | mod minecraft; | ||
| 6 | mod platform; | ||
| 7 | mod util; | ||
| 8 | |||
| 9 | use clap::Parser; | ||
| 10 | use config::Config; | ||
| 11 | use errors::McError; | ||
| 12 | use log::{debug, info}; | ||
| 13 | |||
| 14 | #[derive(Parser, Debug)] | ||
| 15 | #[command(author, about, disable_version_flag = true)] | ||
| 16 | struct Cli { | ||
| 17 | #[arg(long)] | ||
| 18 | version: Option<String>, | ||
| 19 | |||
| 20 | #[arg(long)] | ||
| 21 | username: Option<String>, | ||
| 22 | |||
| 23 | #[arg(long, num_args(0..), allow_hyphen_values = true)] | ||
| 24 | jvm_args: Vec<String>, | ||
| 25 | } | ||
| 26 | |||
| 27 | #[tokio::main] | ||
| 28 | async fn main() -> Result<(), McError> { | ||
| 29 | dotenvy::dotenv().ok(); | ||
| 30 | env_logger::init(); | ||
| 31 | |||
| 32 | let cli = Cli::parse(); | ||
| 33 | let mut config = Config::load()?; | ||
| 34 | |||
| 35 | if let Some(v) = cli.version { | ||
| 36 | config.version = v; | ||
| 37 | } | ||
| 38 | |||
| 39 | if let Some(u) = cli.username { | ||
| 40 | config.username = u; | ||
| 41 | } | ||
| 42 | if !cli.jvm_args.is_empty() { | ||
| 43 | config.jvm_args = cli.jvm_args; | ||
| 44 | } | ||
| 45 | |||
| 46 | info!("Final config after CLI overrides: {:?}", config); | ||
| 47 | |||
| 48 | platform::paths::ensure_dirs(&config)?; | ||
| 49 | info!("Using Minecraft version {}", config.version); | ||
| 50 | |||
| 51 | let version = minecraft::manifests::load_version(&config).await?; | ||
| 52 | info!("Loaded version manifest for: {}", version.id); | ||
| 53 | debug!("Main class: {}", version.main_class); | ||
| 54 | |||
| 55 | minecraft::downloads::download_all(&config, &version).await?; | ||
| 56 | minecraft::extraction::extract_natives(&config, &version)?; | ||
| 57 | minecraft::launcher::launch(&config, &version)?; | ||
| 58 | |||
| 59 | Ok(()) | ||
| 60 | } | ||
diff --git a/src/minecraft/downloads.rs b/src/minecraft/downloads.rs new file mode 100644 index 0000000..5be5a05 --- /dev/null +++ b/src/minecraft/downloads.rs | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | use log::{debug, info}; | ||
| 2 | use tokio::{fs, io::AsyncWriteExt}; | ||
| 3 | |||
| 4 | use crate::{ | ||
| 5 | config::Config, | ||
| 6 | errors::McError, | ||
| 7 | minecraft::manifests::{Library, Version}, | ||
| 8 | platform::paths, | ||
| 9 | }; | ||
| 10 | |||
| 11 | /// Download everything required to launch: | ||
| 12 | /// - client jar | ||
| 13 | /// - libraries | ||
| 14 | pub async fn download_all(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 15 | download_client(config, version).await?; | ||
| 16 | download_libraries(config, &version.libraries).await?; | ||
| 17 | Ok(()) | ||
| 18 | } | ||
| 19 | |||
| 20 | async fn download_client(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 21 | let jar_path = paths::client_jar(config, &version.id)?; | ||
| 22 | |||
| 23 | if jar_path.exists() { | ||
| 24 | debug!("Client jar already exists"); | ||
| 25 | return Ok(()); | ||
| 26 | } | ||
| 27 | |||
| 28 | info!("Downloading client {}", version.id); | ||
| 29 | |||
| 30 | download_file(&version.downloads.client.url, &jar_path).await | ||
| 31 | } | ||
| 32 | |||
| 33 | async fn download_libraries(config: &Config, libraries: &[Library]) -> Result<(), McError> { | ||
| 34 | for lib in libraries { | ||
| 35 | let Some(artifact) = &lib.downloads.artifact else { | ||
| 36 | continue; | ||
| 37 | }; | ||
| 38 | |||
| 39 | let lib_path = paths::library_file(config, &artifact.path)?; | ||
| 40 | |||
| 41 | if lib_path.exists() { | ||
| 42 | continue; | ||
| 43 | } | ||
| 44 | |||
| 45 | info!("Downloading library {}", artifact.path); | ||
| 46 | download_file(&artifact.url, &lib_path).await?; | ||
| 47 | } | ||
| 48 | |||
| 49 | Ok(()) | ||
| 50 | } | ||
| 51 | |||
| 52 | /* ---------------- helper ---------------- */ | ||
| 53 | |||
| 54 | async fn download_file(url: &str, path: &std::path::Path) -> Result<(), McError> { | ||
| 55 | if let Some(parent) = path.parent() { | ||
| 56 | fs::create_dir_all(parent).await?; | ||
| 57 | } | ||
| 58 | |||
| 59 | let response = reqwest::get(url).await?; | ||
| 60 | let bytes = response.bytes().await?; | ||
| 61 | |||
| 62 | let mut file = fs::File::create(path).await?; | ||
| 63 | file.write_all(&bytes).await?; | ||
| 64 | |||
| 65 | Ok(()) | ||
| 66 | } | ||
diff --git a/src/minecraft/extraction.rs b/src/minecraft/extraction.rs new file mode 100644 index 0000000..5175ee0 --- /dev/null +++ b/src/minecraft/extraction.rs | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | use log::info; | ||
| 2 | |||
| 3 | use crate::errors::McError; | ||
| 4 | pub fn extract_natives( | ||
| 5 | _cfg: &crate::config::Config, | ||
| 6 | version: &crate::minecraft::manifests::Version, | ||
| 7 | ) -> Result<(), McError> { | ||
| 8 | info!("Extracting natives for {}", version.id); | ||
| 9 | Ok(()) | ||
| 10 | } | ||
diff --git a/src/minecraft/launcher.rs b/src/minecraft/launcher.rs new file mode 100644 index 0000000..f7e3ecc --- /dev/null +++ b/src/minecraft/launcher.rs | |||
| @@ -0,0 +1,73 @@ | |||
| 1 | use std::process::Command; | ||
| 2 | |||
| 3 | use log::{debug, info}; | ||
| 4 | |||
| 5 | use crate::{config::Config, errors::McError, minecraft::manifests::Version, platform::paths}; | ||
| 6 | |||
| 7 | /// Build the full classpath | ||
| 8 | fn build_classpath(config: &Config, version: &Version) -> Result<String, McError> { | ||
| 9 | let sep = if cfg!(windows) { ";" } else { ":" }; | ||
| 10 | let mut entries = Vec::new(); | ||
| 11 | |||
| 12 | for lib in &version.libraries { | ||
| 13 | if let Some(artifact) = &lib.downloads.artifact { | ||
| 14 | let path = paths::library_file(config, &artifact.path)?; | ||
| 15 | entries.push(path.to_string_lossy().to_string()); | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | let client_jar = paths::client_jar(config, &version.id)?; | ||
| 20 | entries.push(client_jar.to_string_lossy().to_string()); | ||
| 21 | Ok(entries.join(sep)) | ||
| 22 | } | ||
| 23 | |||
| 24 | /// Launch Minecraft | ||
| 25 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 26 | let java = &config.java_path; | ||
| 27 | let classpath = build_classpath(config, version)?; | ||
| 28 | let natives_dir = paths::natives_dir(config, &version.id); | ||
| 29 | |||
| 30 | if !natives_dir.exists() { | ||
| 31 | return Err(McError::Runtime(format!( | ||
| 32 | "Natives folder does not exist: {}", | ||
| 33 | natives_dir.display() | ||
| 34 | ))); | ||
| 35 | } | ||
| 36 | |||
| 37 | info!("Launching Minecraft {}", version.id); | ||
| 38 | debug!("Classpath: {}", classpath); | ||
| 39 | debug!("Natives: {}", natives_dir.display()); | ||
| 40 | |||
| 41 | let status = Command::new(java) | ||
| 42 | .arg(format!("-Xmx{}M", config.max_memory_mb)) | ||
| 43 | .arg(format!("-Djava.library.path={}", natives_dir.display())) | ||
| 44 | .arg("-cp") | ||
| 45 | .arg(classpath) | ||
| 46 | .arg(&version.main_class) | ||
| 47 | .arg("--username") | ||
| 48 | .arg(&config.username) | ||
| 49 | .arg("--version") | ||
| 50 | .arg(&version.id) | ||
| 51 | .arg("--gameDir") | ||
| 52 | .arg(paths::minecraft_root(config)) | ||
| 53 | .arg("--assetsDir") | ||
| 54 | .arg(paths::minecraft_root(config).join("assets")) | ||
| 55 | .arg("--assetIndex") | ||
| 56 | .arg(&version.id) | ||
| 57 | .arg("--uuid") | ||
| 58 | .arg(&config.uuid) | ||
| 59 | .arg("--userProperties") | ||
| 60 | .arg("{}") | ||
| 61 | .arg("--accessToken") | ||
| 62 | .arg("0") | ||
| 63 | .arg("--userType") | ||
| 64 | .arg("legacy") | ||
| 65 | .args(&config.jvm_args) | ||
| 66 | .status()?; | ||
| 67 | |||
| 68 | if !status.success() { | ||
| 69 | return Err(McError::Process("Minecraft exited with error".into())); | ||
| 70 | } | ||
| 71 | |||
| 72 | Ok(()) | ||
| 73 | } | ||
diff --git a/src/minecraft/manifests.rs b/src/minecraft/manifests.rs new file mode 100644 index 0000000..3cc59af --- /dev/null +++ b/src/minecraft/manifests.rs | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | use reqwest; | ||
| 3 | use serde::Deserialize; | ||
| 4 | |||
| 5 | use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; | ||
| 6 | |||
| 7 | #[derive(Debug, Deserialize)] | ||
| 8 | pub struct Version { | ||
| 9 | pub id: String, | ||
| 10 | |||
| 11 | #[serde(rename = "mainClass")] | ||
| 12 | pub main_class: String, | ||
| 13 | |||
| 14 | pub downloads: Downloads, | ||
| 15 | pub libraries: Vec<Library>, | ||
| 16 | } | ||
| 17 | |||
| 18 | #[derive(Debug, Deserialize)] | ||
| 19 | pub struct Downloads { | ||
| 20 | pub client: DownloadInfo, | ||
| 21 | } | ||
| 22 | |||
| 23 | #[derive(Debug, Deserialize)] | ||
| 24 | pub struct DownloadInfo { | ||
| 25 | pub url: String, | ||
| 26 | pub sha1: String, | ||
| 27 | pub size: u64, | ||
| 28 | } | ||
| 29 | |||
| 30 | #[derive(Debug, Deserialize)] | ||
| 31 | pub struct Library { | ||
| 32 | pub downloads: LibraryDownloads, | ||
| 33 | } | ||
| 34 | |||
| 35 | #[derive(Debug, Deserialize)] | ||
| 36 | pub struct LibraryDownloads { | ||
| 37 | pub artifact: Option<LibraryArtifact>, | ||
| 38 | } | ||
| 39 | |||
| 40 | #[derive(Debug, Deserialize)] | ||
| 41 | pub struct LibraryArtifact { | ||
| 42 | pub path: String, | ||
| 43 | pub url: String, | ||
| 44 | pub sha1: String, | ||
| 45 | pub size: u64, | ||
| 46 | } | ||
| 47 | |||
| 48 | pub async fn load_version(cfg: &crate::config::Config) -> Result<Version, McError> { | ||
| 49 | let manifest_text = reqwest::get(VERSION_MANIFEST_URL) | ||
| 50 | .await? | ||
| 51 | .text() | ||
| 52 | .await?; | ||
| 53 | let root: serde_json::Value = serde_json::from_str(&manifest_text)?; | ||
| 54 | let version_id = if cfg.version == "latest" { | ||
| 55 | root["latest"]["release"] | ||
| 56 | .as_str() | ||
| 57 | .ok_or_else(|| McError::Config("missing latest.release".into()))? | ||
| 58 | .to_string() | ||
| 59 | } else { | ||
| 60 | cfg.version.clone() | ||
| 61 | }; | ||
| 62 | |||
| 63 | let versions = root["versions"] | ||
| 64 | .as_array() | ||
| 65 | .ok_or_else(|| McError::Config("missing versions array".into()))?; | ||
| 66 | |||
| 67 | let version_entry = versions | ||
| 68 | .iter() | ||
| 69 | .find(|v| v["id"].as_str() == Some(&version_id)) | ||
| 70 | .ok_or_else(|| McError::Config(format!("version '{}' not found", version_id)))?; | ||
| 71 | |||
| 72 | let url = version_entry["url"] | ||
| 73 | .as_str() | ||
| 74 | .ok_or_else(|| McError::Config("missing version url".into()))?; | ||
| 75 | |||
| 76 | let version_text = reqwest::get(url).await?.text().await?; | ||
| 77 | let version: Version = serde_json::from_str(&version_text)?; | ||
| 78 | |||
| 79 | Ok(version) | ||
| 80 | } | ||
diff --git a/src/minecraft/mod.rs b/src/minecraft/mod.rs new file mode 100644 index 0000000..f1ce1f0 --- /dev/null +++ b/src/minecraft/mod.rs | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | pub mod downloads; | ||
| 2 | pub mod extraction; | ||
| 3 | pub mod launcher; | ||
| 4 | pub mod manifests; | ||
diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..8118b29 --- /dev/null +++ b/src/platform/mod.rs | |||
| @@ -0,0 +1 @@ | |||
| pub mod paths; | |||
diff --git a/src/platform/paths.rs b/src/platform/paths.rs new file mode 100644 index 0000000..47aae9a --- /dev/null +++ b/src/platform/paths.rs | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | use std::{fs, path::PathBuf}; | ||
| 2 | |||
| 3 | use directories::ProjectDirs; | ||
| 4 | |||
| 5 | use crate::{config::Config, errors::McError}; | ||
| 6 | |||
| 7 | fn project_dirs() -> ProjectDirs { | ||
| 8 | ProjectDirs::from("com", "dml", "dml").expect("failed to determine project directories") | ||
| 9 | } | ||
| 10 | |||
| 11 | /// Root Minecraft directory | ||
| 12 | pub fn minecraft_root(_cfg: &Config) -> PathBuf { project_dirs().data_dir().join("minecraft") } | ||
| 13 | |||
| 14 | /* ---------------- setup ---------------- */ | ||
| 15 | |||
| 16 | pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> { | ||
| 17 | let root = minecraft_root(cfg); | ||
| 18 | |||
| 19 | fs::create_dir_all(root.join("versions"))?; | ||
| 20 | fs::create_dir_all(root.join("libraries"))?; | ||
| 21 | fs::create_dir_all(root.join("assets"))?; | ||
| 22 | |||
| 23 | Ok(()) | ||
| 24 | } | ||
| 25 | |||
| 26 | /* ---------------- versions ---------------- */ | ||
| 27 | |||
| 28 | pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { | ||
| 29 | minecraft_root(cfg).join("versions").join(version) | ||
| 30 | } | ||
| 31 | |||
| 32 | pub fn client_jar(cfg: &Config, version: &str) -> Result<PathBuf, McError> { | ||
| 33 | Ok(version_dir(cfg, version).join(format!("{}.jar", version))) | ||
| 34 | } | ||
| 35 | |||
| 36 | /* ---------------- libraries ---------------- */ | ||
| 37 | |||
| 38 | pub fn library_file(cfg: &Config, rel_path: &str) -> Result<PathBuf, McError> { | ||
| 39 | Ok(minecraft_root(cfg) | ||
| 40 | .join("libraries") | ||
| 41 | .join(rel_path)) | ||
| 42 | } | ||
| 43 | |||
| 44 | /* ---------------- natives ---------------- */ | ||
| 45 | |||
| 46 | pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { | ||
| 47 | version_dir(cfg, version).join("natives") | ||
| 48 | } | ||
diff --git a/src/util/fs.rs b/src/util/fs.rs new file mode 100644 index 0000000..b86c0d7 --- /dev/null +++ b/src/util/fs.rs | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | |||
| 3 | use std::path::Path; | ||
| 4 | |||
| 5 | use crate::errors::McError; | ||
| 6 | |||
| 7 | pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { | ||
| 8 | if path.exists() { | ||
| 9 | tokio::fs::remove_file(path).await?; | ||
| 10 | } | ||
| 11 | Ok(()) | ||
| 12 | } | ||
diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..8176b9b --- /dev/null +++ b/src/util/mod.rs | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | pub mod fs; | ||
| 2 | pub mod sha1; | ||
diff --git a/src/util/sha1.rs b/src/util/sha1.rs new file mode 100644 index 0000000..c5f1021 --- /dev/null +++ b/src/util/sha1.rs | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | #![allow(dead_code)] | ||
| 2 | |||
| 3 | use std::path::Path; | ||
| 4 | |||
| 5 | use sha1::{Digest, Sha1}; | ||
| 6 | |||
| 7 | use crate::errors::McError; | ||
| 8 | |||
| 9 | pub async fn sha1_hex(path: &Path) -> Result<String, McError> { | ||
| 10 | let data = tokio::fs::read(path).await?; | ||
| 11 | let mut hasher = Sha1::new(); | ||
| 12 | hasher.update(&data); | ||
| 13 | Ok(format!("{:x}", hasher.finalize())) | ||
| 14 | } | ||
