diff options
| author | Filip Wandzio <contact@philw.dev> | 2026-02-25 16:10:23 +0100 |
|---|---|---|
| committer | Filip Wandzio <contact@philw.dev> | 2026-02-25 16:10:23 +0100 |
| commit | f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a (patch) | |
| tree | c96432be342b02bc0409e5b78b6b5d54afcc7cd6 | |
| parent | 2e10b0713f5369f489d2ababd70108cc359c5d2d (diff) | |
| download | dml-f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a.tar.gz dml-f7b4b643ebc52a4d72d90d9adbdddc9aa0721e4a.zip | |
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
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | .run/Run dml.run.xml | 20 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | src/config/file.rs | 26 | ||||
| -rw-r--r-- | src/config/loader.rs | 129 | ||||
| -rw-r--r-- | src/config/mod.rs | 17 | ||||
| -rw-r--r-- | src/config/runtime.rs | 95 | ||||
| -rw-r--r-- | src/constants.rs | 110 | ||||
| -rw-r--r-- | src/errors.rs | 320 | ||||
| -rw-r--r-- | src/main.rs | 105 | ||||
| -rw-r--r-- | src/minecraft/downloads.rs | 423 | ||||
| -rw-r--r-- | src/minecraft/extraction.rs | 70 | ||||
| -rw-r--r-- | src/minecraft/launcher.rs | 262 | ||||
| -rw-r--r-- | src/minecraft/manifests.rs | 41 | ||||
| -rw-r--r-- | src/platform/paths.rs | 270 | ||||
| -rw-r--r-- | src/util/fs.rs | 26 | ||||
| -rw-r--r-- | src/util/sha1.rs | 7 |
18 files changed, 1560 insertions, 377 deletions
| @@ -7,3 +7,4 @@ Cargo.lock | |||
| 7 | *.pdb | 7 | *.pdb |
| 8 | rust-project.json | 8 | rust-project.json |
| 9 | *.png | 9 | *.png |
| 10 | .idea | ||
diff --git a/.run/Run dml.run.xml b/.run/Run dml.run.xml new file mode 100644 index 0000000..dd66d28 --- /dev/null +++ b/.run/Run dml.run.xml | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | <component name="ProjectRunConfigurationManager"> | ||
| 2 | <configuration default="false" name="Run dml" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> | ||
| 3 | <option name="buildProfileId" value="dev" /> | ||
| 4 | <option name="command" value="run --package dml --bin dml" /> | ||
| 5 | <option name="workingDirectory" value="file://$PROJECT_DIR$" /> | ||
| 6 | <envs /> | ||
| 7 | <option name="emulateTerminal" value="true" /> | ||
| 8 | <option name="channel" value="STABLE" /> | ||
| 9 | <option name="requiredFeatures" value="true" /> | ||
| 10 | <option name="allFeatures" value="true" /> | ||
| 11 | <option name="withSudo" value="true" /> | ||
| 12 | <option name="buildTarget" value="REMOTE" /> | ||
| 13 | <option name="backtrace" value="SHORT" /> | ||
| 14 | <option name="isRedirectInput" value="false" /> | ||
| 15 | <option name="redirectInputPath" value="" /> | ||
| 16 | <method v="2"> | ||
| 17 | <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> | ||
| 18 | </method> | ||
| 19 | </configuration> | ||
| 20 | </component> \ No newline at end of file | ||
| @@ -8,10 +8,8 @@ clap = { version = "4.5.54", features = ["derive"] } | |||
| 8 | directories = "6.0.0" | 8 | directories = "6.0.0" |
| 9 | dotenvy = "0.15.7" | 9 | dotenvy = "0.15.7" |
| 10 | env_logger = "0.11.8" | 10 | env_logger = "0.11.8" |
| 11 | futures-util = "0.3.31" | 11 | futures = "0.3.32" |
| 12 | indicatif = "0.18.3" | ||
| 13 | log = "0.4.29" | 12 | log = "0.4.29" |
| 14 | rayon = "1.11.0" | ||
| 15 | reqwest = { version = "0.13.1", features = ["json", "stream"] } | 13 | reqwest = { version = "0.13.1", features = ["json", "stream"] } |
| 16 | serde = { version = "1.0.228", features = ["derive"] } | 14 | serde = { version = "1.0.228", features = ["derive"] } |
| 17 | serde_json = "1.0.149" | 15 | serde_json = "1.0.149" |
| @@ -1,14 +1,13 @@ | |||
| 1 | # Very simple minecraft launcher (early stage of development). Made to explore concurrency/async in rust. | 1 | # Very simple minecraft launcher (early stage of development). Made to explore concurrency/async in rust. And because I don't like the default launcher. |
| 2 | 2 | ||
| 3 | ## Available features | 3 | ## Available features |
| 4 | 4 | ||
| 5 | - [x] playing any version > 1.10 | 5 | - [x] playing any version >1.7 and <1.15 |
| 6 | 6 | ||
| 7 | ## Planned features | 7 | ## Planned features |
| 8 | 8 | ||
| 9 | - [] "modern" minecraft | 9 | - [ ] Concurrent asset downloading |
| 10 | 10 | ||
| 11 | - [] modloaders | 11 | - [ ] modloaders |
| 12 | |||
| 13 | - [] Concurrent asset downloading | ||
| 14 | 12 | ||
| 13 | - [ ] "modern" minecraft | ||
diff --git a/src/config/file.rs b/src/config/file.rs new file mode 100644 index 0000000..17f2cb2 --- /dev/null +++ b/src/config/file.rs | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use serde::Deserialize; | ||
| 4 | |||
| 5 | use crate::minecraft::launcher::JavaRuntime; | ||
| 6 | |||
| 7 | #[derive(Debug, Deserialize)] | ||
| 8 | pub struct FileConfig { | ||
| 9 | pub username: String, | ||
| 10 | pub uuid: String, | ||
| 11 | pub version: String, | ||
| 12 | pub max_memory_mb: u32, | ||
| 13 | |||
| 14 | #[serde(default)] | ||
| 15 | pub jvm_args: Vec<String>, | ||
| 16 | |||
| 17 | #[serde(default)] | ||
| 18 | pub runtimes: Vec<JavaRuntime>, | ||
| 19 | |||
| 20 | #[serde(default)] | ||
| 21 | pub java_path: String, | ||
| 22 | |||
| 23 | pub data_dir: PathBuf, | ||
| 24 | // pub cache_dir: PathBuf, | ||
| 25 | // pub config_dir: PathBuf, | ||
| 26 | } | ||
diff --git a/src/config/loader.rs b/src/config/loader.rs index d4b142e..47514e5 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs | |||
| @@ -1,65 +1,90 @@ | |||
| 1 | use std::{env::var, fs::read_to_string, path::PathBuf}; | 1 | use std::{env, fs::read_to_string, path::PathBuf}; |
| 2 | |||
| 3 | use directories::ProjectDirs; | 2 | use directories::ProjectDirs; |
| 4 | use serde::Deserialize; | 3 | use uuid::Uuid; |
| 5 | 4 | ||
| 5 | use super::{file::FileConfig, runtime::RuntimeConfig}; | ||
| 6 | use crate::{constants::*, errors::McError}; | 6 | use crate::{constants::*, errors::McError}; |
| 7 | #[allow(dead_code)] | 7 | |
| 8 | #[derive(Debug, Deserialize)] | 8 | pub struct ConfigLoader; |
| 9 | pub struct Config { | 9 | |
| 10 | pub username: String, | 10 | impl ConfigLoader { |
| 11 | pub uuid: String, | 11 | pub fn load(cfg_file: Option<&PathBuf>) -> Result<RuntimeConfig, McError> { |
| 12 | pub version: String, | 12 | let path = match cfg_file { |
| 13 | pub java_path: String, | 13 | | Some(p) => p.clone(), |
| 14 | pub max_memory_mb: u32, | 14 | | None => Self::default_config_path()?, |
| 15 | pub data_dir: PathBuf, | 15 | }; |
| 16 | pub cache_dir: PathBuf, | 16 | |
| 17 | pub config_dir: PathBuf, | 17 | let mut file_cfg = if path.exists() { |
| 18 | #[serde(default)] | 18 | Self::read_file(&path)? |
| 19 | pub jvm_args: Vec<String>, | ||
| 20 | } | ||
| 21 | impl Config { | ||
| 22 | pub fn load() -> Result<Self, McError> { | ||
| 23 | let cfg_path = default_config_path()?; | ||
| 24 | let mut cfg: Config = if cfg_path.exists() { | ||
| 25 | let txt = read_to_string(&cfg_path)?; | ||
| 26 | toml::from_str(&txt).map_err(|e| McError::Config(e.to_string()))? | ||
| 27 | } else { | 19 | } else { |
| 28 | Self::default() | 20 | Self::default_file_config()? |
| 29 | }; | 21 | }; |
| 30 | if let Ok(v) = var("MC_USERNAME") { | 22 | |
| 31 | cfg.username = v; | 23 | Self::apply_env_overrides(&mut file_cfg); |
| 32 | } | 24 | |
| 33 | if let Ok(v) = var("MC_VERSION") { | 25 | Ok(RuntimeConfig::from_file(file_cfg)) |
| 34 | cfg.version = v; | 26 | } |
| 35 | } | 27 | |
| 36 | if let Ok(v) = var("MC_JAVA_PATH") { | 28 | fn read_file(path: &PathBuf) -> Result<FileConfig, McError> { |
| 37 | cfg.java_path = v; | 29 | let content = read_to_string(path).map_err(|e| { |
| 38 | } | 30 | McError::Config(format!( |
| 39 | if let Ok(v) = var("MC_MAX_MEMORY_MB") { | 31 | "Failed to read config file {}: {}", |
| 40 | cfg.max_memory_mb = v.parse().unwrap_or(cfg.max_memory_mb); | 32 | path.display(), |
| 41 | } | 33 | e |
| 42 | Ok(cfg) | 34 | )) |
| 35 | })?; | ||
| 36 | |||
| 37 | toml::from_str(&content).map_err(|e| { | ||
| 38 | McError::Config(format!("Failed to parse config file: {}", e)) | ||
| 39 | }) | ||
| 43 | } | 40 | } |
| 44 | 41 | ||
| 45 | fn default() -> Self { | 42 | fn apply_env_overrides(cfg: &mut FileConfig) { |
| 46 | let base = | 43 | cfg.username = env::var(ENV_USERNAME) |
| 47 | ProjectDirs::from("com", "example", "dml").expect("platform dirs"); | 44 | .unwrap_or_else(|_| DEFAULT_USERNAME.to_string()); |
| 48 | Self { | 45 | |
| 49 | username: "Player".into(), | 46 | cfg.version = env::var(ENV_VERSION) |
| 50 | uuid: uuid::Uuid::new_v4().to_string(), | 47 | .unwrap_or_else(|_| DEFAULT_VERSION.to_string()); |
| 48 | |||
| 49 | cfg.java_path = env::var(ENV_JAVA_PATH) | ||
| 50 | .unwrap_or_else(|_| DEFAULT_JAVA_PATH.to_string()); | ||
| 51 | |||
| 52 | cfg.max_memory_mb = env::var(ENV_MAX_MEMORY_MB) | ||
| 53 | .ok() | ||
| 54 | .and_then(|v| v.parse().ok()) | ||
| 55 | .unwrap_or(DEFAULT_MAX_MEMORY_MB); | ||
| 56 | } | ||
| 57 | |||
| 58 | fn default_config_path() -> Result<PathBuf, McError> { | ||
| 59 | let base = ProjectDirs::from( | ||
| 60 | DEFAULT_COMPANY, | ||
| 61 | DEFAULT_PROJECT_GROUP, | ||
| 62 | DEFAULT_PROJECT_NAME, | ||
| 63 | ) | ||
| 64 | .ok_or_else(|| McError::Config(DEFAULT_ERR_PLATFORM_DIR.into()))?; | ||
| 65 | |||
| 66 | Ok(base.config_dir().join(DEFAULT_CONFIG_FILENAME)) | ||
| 67 | } | ||
| 68 | |||
| 69 | fn default_file_config() -> Result<FileConfig, McError> { | ||
| 70 | let base = ProjectDirs::from( | ||
| 71 | DEFAULT_COMPANY, | ||
| 72 | DEFAULT_PROJECT_GROUP, | ||
| 73 | DEFAULT_PROJECT_NAME, | ||
| 74 | ) | ||
| 75 | .ok_or_else(|| McError::Config(DEFAULT_ERR_PLATFORM_DIR.into()))?; | ||
| 76 | |||
| 77 | Ok(FileConfig { | ||
| 78 | username: DEFAULT_USERNAME.into(), | ||
| 79 | uuid: Uuid::new_v4().to_string(), | ||
| 51 | version: DEFAULT_VERSION.into(), | 80 | version: DEFAULT_VERSION.into(), |
| 52 | java_path: DEFAULT_JAVA_PATH.into(), | ||
| 53 | max_memory_mb: DEFAULT_MAX_MEMORY_MB, | 81 | max_memory_mb: DEFAULT_MAX_MEMORY_MB, |
| 82 | java_path: DEFAULT_JAVA_PATH.into(), | ||
| 54 | data_dir: base.data_dir().into(), | 83 | data_dir: base.data_dir().into(), |
| 55 | cache_dir: base.cache_dir().into(), | 84 | // cache_dir: base.cache_dir().into(), |
| 56 | config_dir: base.config_dir().into(), | 85 | // config_dir: base.config_dir().into(), |
| 57 | jvm_args: vec![], | 86 | jvm_args: vec![], |
| 58 | } | 87 | runtimes: vec![], |
| 88 | }) | ||
| 59 | } | 89 | } |
| 60 | } | 90 | } |
| 61 | fn default_config_path() -> Result<PathBuf, McError> { | ||
| 62 | let base = ProjectDirs::from("com", "example", "dml") | ||
| 63 | .ok_or_else(|| McError::Config("cannot determine config dir".into()))?; | ||
| 64 | Ok(base.config_dir().join("config.toml")) | ||
| 65 | } | ||
diff --git a/src/config/mod.rs b/src/config/mod.rs index 066154a..a375dda 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs | |||
| @@ -1,12 +1,11 @@ | |||
| 1 | //! Configuration module for the DML launcher. | 1 | //! Configuration module for the DML launcher. |
| 2 | //! | ||
| 3 | //! This module contains submodules and helpers for loading, parsing, and | ||
| 4 | //! managing configuration files and settings. It abstracts the details | ||
| 5 | //! of where configuration is stored and how it is represented in memory. | ||
| 6 | //! | ||
| 7 | //! # Submodules | ||
| 8 | //! - `loader`: Functions to load and parse configuration from disk or | ||
| 9 | //! environment variables. | ||
| 10 | 2 | ||
| 3 | pub mod file; | ||
| 11 | pub mod loader; | 4 | pub mod loader; |
| 12 | pub use loader::Config; | 5 | pub mod runtime; |
| 6 | |||
| 7 | pub use loader::ConfigLoader; | ||
| 8 | pub use runtime::RuntimeConfig; | ||
| 9 | |||
| 10 | /// Backwards-compatibility alias so existing code can keep using `Config` | ||
| 11 | pub type Config = RuntimeConfig; | ||
diff --git a/src/config/runtime.rs b/src/config/runtime.rs new file mode 100644 index 0000000..46857f0 --- /dev/null +++ b/src/config/runtime.rs | |||
| @@ -0,0 +1,95 @@ | |||
| 1 | use std::path::PathBuf; | ||
| 2 | |||
| 3 | use super::file::FileConfig; | ||
| 4 | use crate::{constants::*, minecraft::launcher::JavaRuntime}; | ||
| 5 | |||
| 6 | /// Configuration for the runtime environment used to launch Minecraft. | ||
| 7 | /// | ||
| 8 | /// The `RuntimeConfig` struct holds the configuration settings required to | ||
| 9 | /// launch Minecraft with a specific Java runtime. This includes details about | ||
| 10 | /// the user, the Minecraft version, the available Java runtimes, and JVM options. | ||
| 11 | /// | ||
| 12 | /// The `RuntimeConfig` struct is typically used by the launcher to store and | ||
| 13 | /// manage the necessary settings for running the game. It also supports | ||
| 14 | /// dynamic configuration by allowing the addition of Java runtimes and the | ||
| 15 | /// specification of system paths and arguments. | ||
| 16 | #[derive(Debug)] | ||
| 17 | #[derive(Default)] | ||
| 18 | pub struct RuntimeConfig { | ||
| 19 | /// The username of the player running Minecraft. | ||
| 20 | pub username: String, | ||
| 21 | |||
| 22 | /// The UUID (unique identifier) of the user. | ||
| 23 | pub uuid: String, | ||
| 24 | |||
| 25 | /// The version of Minecraft that the user is running. | ||
| 26 | pub version: String, | ||
| 27 | |||
| 28 | /// The maximum amount of memory (in megabytes) allocated to the Java process. | ||
| 29 | /// This setting determines how much memory the Minecraft instance can use. | ||
| 30 | pub max_memory_mb: u32, | ||
| 31 | |||
| 32 | /// A list of arguments to pass to the JVM when launching Minecraft. | ||
| 33 | /// These arguments can be used to customize the JVM's behavior, such as | ||
| 34 | /// memory settings, garbage collection options, etc. | ||
| 35 | pub jvm_args: Vec<String>, | ||
| 36 | |||
| 37 | /// A list of Java runtime environments available for Minecraft. | ||
| 38 | /// If no specific Java runtime is provided, the default one is used. | ||
| 39 | pub runtimes: Vec<JavaRuntime>, | ||
| 40 | |||
| 41 | /// The directory where Minecraft data (e.g., worlds, logs, configs) is stored. | ||
| 42 | /// This path is critical for loading and saving game data. | ||
| 43 | pub data_dir: PathBuf, | ||
| 44 | } | ||
| 45 | |||
| 46 | impl RuntimeConfig { | ||
| 47 | /// Creates a new `RuntimeConfig` instance from a `FileConfig`. | ||
| 48 | /// | ||
| 49 | /// This method takes a `FileConfig` object (typically loaded from a configuration file) | ||
| 50 | /// and constructs a `RuntimeConfig` instance. It ensures that if no runtimes are defined | ||
| 51 | /// but a Java path is provided, a default Java runtime is created based on the specified path. | ||
| 52 | /// | ||
| 53 | /// # Parameters | ||
| 54 | /// - `file`: A `FileConfig` instance containing initial configuration values for the | ||
| 55 | /// Minecraft runtime. This includes user details, Minecraft version, JVM arguments, | ||
| 56 | /// and the path to Java (if specified). | ||
| 57 | /// | ||
| 58 | /// # Returns | ||
| 59 | /// - A `RuntimeConfig` instance initialized with the values from `file`. If no runtimes | ||
| 60 | /// were provided in `file` but a Java path was given, the method will add a default Java runtime. | ||
| 61 | /// | ||
| 62 | /// # Example | ||
| 63 | /// ```rust | ||
| 64 | /// let file_config = FileConfig { | ||
| 65 | /// username: "Player1".into(), | ||
| 66 | /// uuid: "1234-5678-9101".into(), | ||
| 67 | /// version: "1.18.2".into(), | ||
| 68 | /// max_memory_mb: 2048, | ||
| 69 | /// jvm_args: vec!["-Xmx2G".into()], | ||
| 70 | /// runtimes: Vec::new(), | ||
| 71 | /// java_path: "/path/to/java".into(), | ||
| 72 | /// data_dir: "/path/to/minecraft/data".into(), | ||
| 73 | /// }; | ||
| 74 | /// | ||
| 75 | /// let runtime_config = RuntimeConfig::from_file(file_config); | ||
| 76 | /// ``` | ||
| 77 | pub fn from_file(mut file: FileConfig) -> Self { | ||
| 78 | if file.runtimes.is_empty() && !file.java_path.is_empty() { | ||
| 79 | file.runtimes.push(JavaRuntime { | ||
| 80 | major: DEFAULT_JAVA_MAJOR, | ||
| 81 | path: PathBuf::from(&file.java_path), | ||
| 82 | }); | ||
| 83 | } | ||
| 84 | |||
| 85 | Self { | ||
| 86 | username: file.username, | ||
| 87 | uuid: file.uuid, | ||
| 88 | version: file.version, | ||
| 89 | max_memory_mb: file.max_memory_mb, | ||
| 90 | jvm_args: file.jvm_args, | ||
| 91 | runtimes: file.runtimes, | ||
| 92 | data_dir: file.data_dir, | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
diff --git a/src/constants.rs b/src/constants.rs index 3234fe6..72de325 100644 --- a/src/constants.rs +++ b/src/constants.rs | |||
| @@ -1,13 +1,123 @@ | |||
| 1 | #![allow(dead_code)] | 1 | #![allow(dead_code)] |
| 2 | 2 | ||
| 3 | //! Constants module for the DML Launcher. | ||
| 4 | //! | ||
| 5 | //! This module defines immutable configuration values and defaults | ||
| 6 | //! used throughout the launcher. These constants provide a single | ||
| 7 | //! source of truth for URLs, download behavior, memory settings, | ||
| 8 | //! JVM defaults, download concurrency, directory names, and version selection. | ||
| 9 | //! | ||
| 10 | //! Centralizing these constants ensures consistency across the | ||
| 11 | //! application and simplifies maintenance when defaults need to change. | ||
| 12 | |||
| 3 | use std::time::Duration; | 13 | use std::time::Duration; |
| 4 | 14 | ||
| 15 | /// URL to the Minecraft version manifest. | ||
| 16 | /// | ||
| 17 | /// This manifest contains metadata for all available Minecraft | ||
| 18 | /// releases, including release dates, type (release, snapshot), | ||
| 19 | /// and download URLs for the client and server distributions. | ||
| 5 | pub const VERSION_MANIFEST_URL: &str = | 20 | pub const VERSION_MANIFEST_URL: &str = |
| 6 | "https://launchermeta.mojang.com/mc/game/version_manifest.json"; | 21 | "https://launchermeta.mojang.com/mc/game/version_manifest.json"; |
| 7 | 22 | ||
| 23 | /// Number of times to retry a failed download. | ||
| 24 | /// | ||
| 25 | /// Used by asset and library download routines to tolerate | ||
| 26 | /// transient network failures. Downloads will be retried | ||
| 27 | /// up to this number of attempts before returning an error. | ||
| 8 | pub const DOWNLOAD_RETRIES: usize = 3; | 28 | pub const DOWNLOAD_RETRIES: usize = 3; |
| 29 | |||
| 30 | /// Duration to wait between download retries. | ||
| 31 | /// | ||
| 32 | /// Provides fixed delay for network resilience when fetching | ||
| 33 | /// game assets. Specified in milliseconds. | ||
| 9 | pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); | 34 | pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); |
| 10 | 35 | ||
| 36 | /// Default maximum memory allocation for the Minecraft JVM process in | ||
| 37 | /// megabytes. | ||
| 38 | /// | ||
| 39 | /// If the user does not specify memory settings via configuration | ||
| 40 | /// or JVM arguments, this value will be applied as the default heap size. | ||
| 11 | pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048; | 41 | pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048; |
| 42 | |||
| 43 | /// Default path to the Java executable. | ||
| 44 | /// | ||
| 45 | /// Used when launching the Minecraft client. Can be overridden | ||
| 46 | /// via configuration or environment variables if a custom Java | ||
| 47 | /// installation is required. | ||
| 12 | pub const DEFAULT_JAVA_PATH: &str = "java"; | 48 | pub const DEFAULT_JAVA_PATH: &str = "java"; |
| 49 | |||
| 50 | /// Default Minecraft version to launch when no specific version is provided. | ||
| 51 | /// | ||
| 52 | /// The launcher will resolve "latest" to the most recent stable release | ||
| 53 | /// according to the version manifest. | ||
| 13 | pub const DEFAULT_VERSION: &str = "latest"; | 54 | pub const DEFAULT_VERSION: &str = "latest"; |
| 55 | |||
| 56 | /// Default Java major version for launching Minecraft. | ||
| 57 | /// | ||
| 58 | /// This value is used when generating the JVM runtime configuration | ||
| 59 | /// for the launcher. | ||
| 60 | pub const DEFAULT_JAVA_MAJOR: u8 = 8; | ||
| 61 | |||
| 62 | /// Default number of concurrent downloads for libraries. | ||
| 63 | /// | ||
| 64 | /// Limits the number of simultaneous HTTP requests when fetching | ||
| 65 | /// Minecraft libraries to avoid saturating network or file handles. | ||
| 66 | pub const DEFAULT_LIBRARY_CONCURRENCY: usize = 20; | ||
| 67 | |||
| 68 | /// Default number of concurrent downloads for assets. | ||
| 69 | /// | ||
| 70 | /// Limits the number of simultaneous HTTP requests when fetching | ||
| 71 | /// Minecraft assets to optimize throughput without overloading the system. | ||
| 72 | pub const DEFAULT_ASSET_CONCURRENCY: usize = 20; | ||
| 73 | |||
| 74 | /// Default launcher cache directory name. | ||
| 75 | /// | ||
| 76 | /// Used as the base subdirectory for downloaded assets, libraries, | ||
| 77 | /// versions, and configuration. | ||
| 78 | pub const DEFAULT_LAUNCHER_DIR: &str = "dml"; | ||
| 79 | |||
| 80 | /// Default environment variable names for overriding configuration. | ||
| 81 | pub const ENV_USERNAME: &str = "MC_USERNAME"; | ||
| 82 | pub const ENV_VERSION: &str = "MC_VERSION"; | ||
| 83 | pub const ENV_JAVA_PATH: &str = "MC_JAVA_PATH"; | ||
| 84 | pub const ENV_MAX_MEMORY_MB: &str = "MC_MAX_MEMORY_MB"; | ||
| 85 | |||
| 86 | /// Default project/platform constants for `directories::ProjectDirs`. | ||
| 87 | pub const DEFAULT_COMPANY: &str = "com"; | ||
| 88 | pub const DEFAULT_PROJECT_GROUP: &str = "example"; | ||
| 89 | pub const DEFAULT_PROJECT_NAME: &str = "dml"; | ||
| 90 | |||
| 91 | /// Default error messages for common failures. | ||
| 92 | pub const DEFAULT_ERR_PLATFORM_DIR: &str = "cannot determine config dir"; | ||
| 93 | pub const DEFAULT_ERR_CREATE_DIR: &str = "failed to create directory"; | ||
| 94 | |||
| 95 | /// Default configuration filename within the launcher directory. | ||
| 96 | pub const DEFAULT_CONFIG_FILENAME: &str = "config.toml"; | ||
| 97 | |||
| 98 | /// Default username if none is provided by environment or config. | ||
| 99 | pub const DEFAULT_USERNAME: &str = "phil"; | ||
| 100 | |||
| 101 | /// Default subdirectory names within the launcher directory. | ||
| 102 | pub mod directory { | ||
| 103 | /// Directory for Minecraft versions. | ||
| 104 | pub const VERSIONS: &str = "versions"; | ||
| 105 | |||
| 106 | /// Directory for downloaded libraries. | ||
| 107 | pub const LIBRARIES: &str = "libraries"; | ||
| 108 | |||
| 109 | /// Directory for Minecraft assets. | ||
| 110 | pub const ASSETS: &str = "assets"; | ||
| 111 | |||
| 112 | /// Subdirectory for asset indexes. | ||
| 113 | pub const INDEXES: &str = "indexes"; | ||
| 114 | |||
| 115 | /// Subdirectory for asset objects. | ||
| 116 | pub const OBJECTS: &str = "objects"; | ||
| 117 | |||
| 118 | /// Directory for native libraries. | ||
| 119 | pub const NATIVES: &str = "natives"; | ||
| 120 | |||
| 121 | /// Directory for saved worlds. | ||
| 122 | pub const SAVES: &str = "saves"; | ||
| 123 | } | ||
diff --git a/src/errors.rs b/src/errors.rs index b98ae2d..bb9c985 100644 --- a/src/errors.rs +++ b/src/errors.rs | |||
| @@ -1,45 +1,337 @@ | |||
| 1 | use std::{fmt, io}; | 1 | use std::{fmt, io}; |
| 2 | 2 | ||
| 3 | use fmt::{Display, Formatter, Result}; | 3 | use fmt::{Display, Formatter, Result}; |
| 4 | use io::Error; | ||
| 4 | use zip::result::ZipError; | 5 | use zip::result::ZipError; |
| 5 | 6 | ||
| 6 | /// Represents all possible errors that can occur in the DML launcher. | 7 | /// Central error type for the DML launcher. |
| 7 | /// | 8 | /// |
| 8 | /// This enum centralizes error handling for the entire application, | 9 | /// # Overview |
| 9 | /// wrapping various underlying error types from I/O, HTTP requests, | 10 | /// |
| 10 | /// JSON parsing, ZIP extraction, configuration issues, and runtime errors. | 11 | /// `McError` provides a unified error abstraction for the entire |
| 12 | /// application. Instead of exposing multiple unrelated error types | ||
| 13 | /// across modules, all recoverable failures are represented through | ||
| 14 | /// this single enum. | ||
| 15 | /// | ||
| 16 | /// This design offers several advantages: | ||
| 17 | /// | ||
| 18 | /// - Simplified function signatures (`Result<T, McError>`) | ||
| 19 | /// - Seamless propagation using the `?` operator | ||
| 20 | /// - Centralized error categorization | ||
| 21 | /// - Clear separation between infrastructure errors and logical failures | ||
| 22 | /// | ||
| 23 | /// # Error Categories | ||
| 24 | /// | ||
| 25 | /// The variants fall into two broad categories: | ||
| 26 | /// | ||
| 27 | /// 1. Wrapped external errors These variants encapsulate errors originating | ||
| 28 | /// from third-party libraries or the standard library. | ||
| 29 | /// | ||
| 30 | /// 2. Application-defined errors These variants represent logical or | ||
| 31 | /// domain-specific failures unique to the launcher. | ||
| 32 | /// | ||
| 33 | /// All variants are intentionally explicit to preserve failure context | ||
| 34 | /// and avoid opaque error strings. | ||
| 35 | /// | ||
| 36 | /// # Example | ||
| 37 | /// | ||
| 38 | /// ```ignore | ||
| 39 | /// fn load_config(path: &str) -> Result<Config, McError> { | ||
| 40 | /// let data = read_to_string(path)?; | ||
| 41 | /// let config = from_str(&data)?; | ||
| 42 | /// Ok(config) | ||
| 43 | /// } | ||
| 44 | /// ``` | ||
| 45 | /// | ||
| 46 | /// In this example, both `io::Error` and `serde_json::Error` | ||
| 47 | /// automatically convert into `McError` via the provided `From` | ||
| 48 | /// implementations. | ||
| 11 | #[allow(dead_code)] | 49 | #[allow(dead_code)] |
| 12 | #[derive(Debug)] | 50 | #[derive(Debug)] |
| 13 | pub enum McError { | 51 | pub enum McError { |
| 14 | Io(io::Error), | 52 | /// Errors originating from the standard I/O subsystem. |
| 53 | /// | ||
| 54 | /// Typical causes: | ||
| 55 | /// - File system access failures | ||
| 56 | /// - Permission errors | ||
| 57 | /// - Missing files or directories | ||
| 58 | /// - Broken pipes | ||
| 59 | /// | ||
| 60 | /// Wraps: `std::io::Error` | ||
| 61 | Io(Error), | ||
| 62 | |||
| 63 | /// Errors occurring during HTTP communication. | ||
| 64 | /// | ||
| 65 | /// Typical causes: | ||
| 66 | /// - Network connectivity failures | ||
| 67 | /// - Timeouts | ||
| 68 | /// - TLS negotiation errors | ||
| 69 | /// - Invalid HTTP responses | ||
| 70 | /// | ||
| 71 | /// Wraps: `reqwest::Error` | ||
| 15 | Http(reqwest::Error), | 72 | Http(reqwest::Error), |
| 73 | |||
| 74 | /// Errors encountered while parsing or serializing JSON. | ||
| 75 | /// | ||
| 76 | /// Typical causes: | ||
| 77 | /// - Malformed configuration files | ||
| 78 | /// - Invalid API responses | ||
| 79 | /// - Schema mismatches | ||
| 80 | /// | ||
| 81 | /// Wraps: `serde_json::Error` | ||
| 16 | Json(serde_json::Error), | 82 | Json(serde_json::Error), |
| 83 | |||
| 84 | /// Errors related to ZIP archive handling. | ||
| 85 | /// | ||
| 86 | /// Typical causes: | ||
| 87 | /// - Corrupted archives | ||
| 88 | /// - Unsupported compression methods | ||
| 89 | /// - Extraction failures | ||
| 90 | /// | ||
| 91 | /// Wraps: `zip::result::ZipError` | ||
| 17 | Zip(ZipError), | 92 | Zip(ZipError), |
| 93 | |||
| 94 | /// Configuration-related failures detected by the application. | ||
| 95 | /// | ||
| 96 | /// This variant is used when configuration data is syntactically | ||
| 97 | /// valid but semantically incorrect or incomplete. | ||
| 98 | /// | ||
| 99 | /// The contained string should describe the specific issue. | ||
| 18 | Config(String), | 100 | Config(String), |
| 101 | |||
| 102 | /// Indicates a checksum verification failure. | ||
| 103 | /// | ||
| 104 | /// This is typically used when validating downloaded files | ||
| 105 | /// against an expected SHA hash. A mismatch suggests corruption, | ||
| 106 | /// incomplete transfer, or tampering. | ||
| 107 | /// | ||
| 108 | /// The contained string should identify the failing resource. | ||
| 19 | ShaMismatch(String), | 109 | ShaMismatch(String), |
| 110 | |||
| 111 | /// Represents a failure when spawning or interacting with | ||
| 112 | /// an external process. | ||
| 113 | /// | ||
| 114 | /// Typical causes: | ||
| 115 | /// - Executable not found | ||
| 116 | /// - Non-zero exit status | ||
| 117 | /// - Failed argument construction | ||
| 118 | /// | ||
| 119 | /// The contained string provides additional diagnostic detail. | ||
| 20 | Process(String), | 120 | Process(String), |
| 121 | |||
| 122 | /// A general-purpose runtime error. | ||
| 123 | /// | ||
| 124 | /// This variant is intended for unexpected logical failures | ||
| 125 | /// that do not fall into other specific categories. | ||
| 126 | /// | ||
| 127 | /// It should not be used as a catch-all replacement for | ||
| 128 | /// more precise error variants. | ||
| 21 | Runtime(String), | 129 | Runtime(String), |
| 22 | } | 130 | } |
| 23 | 131 | ||
| 132 | /// User-facing formatting implementation. | ||
| 133 | /// | ||
| 134 | /// This implementation of the `Display` trait delegates to the `Debug` representation of the enum, | ||
| 135 | /// providing a full structural view of the error. This includes all internal details such as the | ||
| 136 | /// variant type and wrapped error information, which is particularly useful during development and debugging. | ||
| 137 | /// | ||
| 138 | /// The `Debug` representation might be too verbose for production use, where more concise, user-friendly | ||
| 139 | /// error messages would be beneficial. A future enhancement may be to implement a custom `Display` formatting | ||
| 140 | /// for each error variant, providing a cleaner, more readable message for end users./// User-facing formatting implementation. | ||
| 141 | /// | ||
| 142 | /// Currently delegates to the `Debug` representation of the enum. | ||
| 143 | /// This preserves full structural information and wrapped error | ||
| 144 | /// details, which is useful during development and debugging. | ||
| 145 | /// | ||
| 146 | /// For production environments, a more refined implementation | ||
| 147 | /// may format each variant explicitly to produce cleaner, | ||
| 148 | /// user-oriented error messages. | ||
| 24 | impl Display for McError { | 149 | impl Display for McError { |
| 25 | /// Formats the error for user-friendly display. | 150 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { |
| 26 | /// | 151 | write!(f, "{:?}", self) |
| 27 | /// Currently, it uses the `Debug` format for simplicity. | 152 | } |
| 28 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "{:?}", self) } | ||
| 29 | } | 153 | } |
| 30 | 154 | ||
| 31 | impl From<io::Error> for McError { | 155 | /// Enables conversion from `std::io::Error` to `McError`. |
| 32 | fn from(e: io::Error) -> Self { Self::Io(e) } | 156 | /// |
| 157 | /// This implementation allows any `std::io::Error` (e.g., file I/O errors) to be automatically converted into | ||
| 158 | /// the `McError::Io` variant. This conversion is crucial when using the `?` operator within functions returning | ||
| 159 | /// `Result<T, McError>`, as it ensures that I/O errors are properly handled and propagated as part of the unified | ||
| 160 | /// `McError` error type. | ||
| 161 | /// | ||
| 162 | /// This reduces the need for manual conversions and simplifies error handling in functions that involve I/O operations. | ||
| 163 | impl From<Error> for McError { | ||
| 164 | fn from(e: Error) -> Self { | ||
| 165 | Self::Io(e) | ||
| 166 | } | ||
| 33 | } | 167 | } |
| 34 | 168 | ||
| 169 | /// Enables conversion from `reqwest::Error` to `McError`. | ||
| 170 | /// | ||
| 171 | /// This implementation allows `reqwest::Error` (such as errors from HTTP requests) to be seamlessly converted | ||
| 172 | /// into the `McError::Http` variant. This is particularly useful for error handling in functions that involve HTTP | ||
| 173 | /// requests, as it allows HTTP-related failures to propagate naturally into the `McError` type and be handled | ||
| 174 | /// consistently with other error types. | ||
| 175 | /// | ||
| 176 | /// This conversion makes it easy to work with `reqwest` errors without worrying about mixing error types in the | ||
| 177 | /// function signature. | ||
| 35 | impl From<reqwest::Error> for McError { | 178 | impl From<reqwest::Error> for McError { |
| 36 | fn from(e: reqwest::Error) -> Self { Self::Http(e) } | 179 | fn from(e: reqwest::Error) -> Self { |
| 180 | Self::Http(e) | ||
| 181 | } | ||
| 37 | } | 182 | } |
| 38 | 183 | ||
| 184 | /// Enables conversion from `serde_json::Error` to `McError`. | ||
| 185 | /// | ||
| 186 | /// This implementation simplifies handling JSON parsing and serialization errors by converting any `serde_json::Error` | ||
| 187 | /// (e.g., invalid JSON format, deserialization failure) into the `McError::Json` variant. This allows JSON errors | ||
| 188 | /// to be handled in the same way as other error types, enabling uniform error management across the application. | ||
| 189 | /// | ||
| 190 | /// By using this conversion, developers don't have to manually map JSON-related errors to a custom error type, | ||
| 191 | /// reducing boilerplate and improving readability. | ||
| 39 | impl From<serde_json::Error> for McError { | 192 | impl From<serde_json::Error> for McError { |
| 40 | fn from(e: serde_json::Error) -> Self { Self::Json(e) } | 193 | fn from(e: serde_json::Error) -> Self { |
| 194 | Self::Json(e) | ||
| 195 | } | ||
| 41 | } | 196 | } |
| 42 | 197 | ||
| 198 | /// Enables conversion from `ZipError` to `McError`. | ||
| 199 | /// | ||
| 200 | /// This implementation allows errors from ZIP archive operations (such as invalid or corrupted archives) to be | ||
| 201 | /// converted into the `McError::Zip` variant. This makes it easy to integrate ZIP archive operations into the | ||
| 202 | /// `McError` error handling model, ensuring consistency in how errors are propagated across the application. | ||
| 203 | /// | ||
| 204 | /// This conversion allows the use of the `?` operator for ZIP-related errors, making error handling in ZIP operations | ||
| 205 | /// as seamless as with other error types. | ||
| 43 | impl From<ZipError> for McError { | 206 | impl From<ZipError> for McError { |
| 44 | fn from(e: ZipError) -> Self { Self::Zip(e) } | 207 | fn from(e: ZipError) -> Self { |
| 208 | Self::Zip(e) | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | /// Tests the `Display` trait implementation for each `McError` variant. | ||
| 213 | /// | ||
| 214 | /// This test ensures that the `Display` formatting for all `McError` variants results in a non-empty string. | ||
| 215 | /// It verifies that each variant can be correctly converted to a string, even though the current implementation | ||
| 216 | /// delegates to `Debug`. This test is crucial for confirming that the `Display` implementation is functioning | ||
| 217 | /// as expected for all error variants. | ||
| 218 | #[cfg(test)] | ||
| 219 | mod tests { | ||
| 220 | use super::*; | ||
| 221 | use io::ErrorKind::Other; | ||
| 222 | use reqwest::Client; | ||
| 223 | use serde_json::{from_str, Value}; | ||
| 224 | use std::io; | ||
| 225 | use std::io::Error; | ||
| 226 | use McError::{Config, Io, Json, Process, Runtime, ShaMismatch, Zip}; | ||
| 227 | use ZipError::InvalidArchive; | ||
| 228 | |||
| 229 | fn create_error_variants() -> Vec<McError> { | ||
| 230 | vec![ | ||
| 231 | Io(Error::new(Other, "io")), | ||
| 232 | Json(from_str::<Value>("not json").unwrap_err()), | ||
| 233 | Zip(InvalidArchive("zip".into())), | ||
| 234 | Config("config".into()), | ||
| 235 | ShaMismatch("sha".into()), | ||
| 236 | Process("process".into()), | ||
| 237 | Runtime("runtime".into()), | ||
| 238 | ] | ||
| 239 | } | ||
| 240 | |||
| 241 | #[test] | ||
| 242 | fn display_formats_error_variants() { | ||
| 243 | let error_variants = create_error_variants(); | ||
| 244 | |||
| 245 | for error in error_variants { | ||
| 246 | let display_str = format!("{}", error); | ||
| 247 | assert!(!display_str.is_empty()); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | /// Tests the conversion from `std::io::Error` to `McError::Io`. | ||
| 252 | /// | ||
| 253 | /// This test ensures that an I/O error of type `std::io::Error` is correctly converted to the `McError::Io` | ||
| 254 | /// variant. The test checks that the string representation of the I/O error matches the expected value and | ||
| 255 | /// verifies that the conversion process is seamless. This test is essential for confirming the correct behavior | ||
| 256 | /// of the `From<Error>` implementation for I/O errors. | ||
| 257 | #[test] | ||
| 258 | fn from_io_error() { | ||
| 259 | let io_error: Error = Error::new(Other, "oops"); | ||
| 260 | let mc_error: McError = io_error.into(); | ||
| 261 | |||
| 262 | match mc_error { | ||
| 263 | | Io(e) => assert_eq!(e.to_string(), "oops"), | ||
| 264 | | _ => panic!("Expected McError::Io, but got: {:?}", mc_error), | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | /// Tests the conversion from `serde_json::Error` to `McError::Json`. | ||
| 269 | /// | ||
| 270 | /// This test ensures that a `serde_json::Error`, which occurs when JSON parsing or serialization fails, | ||
| 271 | /// is correctly converted into the `McError::Json` variant. The test checks that the error is appropriately | ||
| 272 | /// transformed and can be matched against the `McError::Json` variant. | ||
| 273 | #[test] | ||
| 274 | fn from_json_error() { | ||
| 275 | let json_error: serde_json::Error = | ||
| 276 | from_str::<Value>("not json").unwrap_err(); | ||
| 277 | let mc_error: McError = json_error.into(); | ||
| 278 | |||
| 279 | assert!(matches!(mc_error, Json(_))); | ||
| 280 | } | ||
| 281 | |||
| 282 | /// Tests the conversion from `ZipError` to `McError::Zip`. | ||
| 283 | /// | ||
| 284 | /// This test ensures that a `ZipError`, typically arising from invalid ZIP archive operations, | ||
| 285 | /// is converted to the `McError::Zip` variant. This ensures that ZIP-related errors are handled | ||
| 286 | /// in the same way as other error types, maintaining consistency in error propagation across the application. | ||
| 287 | #[test] | ||
| 288 | fn from_zip_error() { | ||
| 289 | let zip_error = InvalidArchive("bad zip".into()); | ||
| 290 | let mc_error: McError = zip_error.into(); | ||
| 291 | |||
| 292 | assert!(matches!(mc_error, Zip(_))); | ||
| 293 | } | ||
| 294 | |||
| 295 | /// Tests the conversion from `reqwest::Error` to `McError::Http`. | ||
| 296 | /// | ||
| 297 | /// This test ensures that HTTP-related errors from `reqwest` (e.g., failed requests, connection issues) | ||
| 298 | /// are correctly converted into the `McError::Http` variant. It confirms that HTTP-related failures | ||
| 299 | /// are properly integrated into the unified `McError` error type and handled in the same way as other errors. | ||
| 300 | #[tokio::test] | ||
| 301 | async fn from_reqwest_error() { | ||
| 302 | let client: Client = Client::new(); | ||
| 303 | let request_error: reqwest::Error = client | ||
| 304 | .get("http://127.0.0.1:0") | ||
| 305 | .send() | ||
| 306 | .await | ||
| 307 | .unwrap_err(); | ||
| 308 | |||
| 309 | let mc_error: McError = request_error.into(); | ||
| 310 | |||
| 311 | assert!( | ||
| 312 | matches!(mc_error, McError::Http(_)), | ||
| 313 | "Expected McError::Http, got: {:?}", | ||
| 314 | mc_error | ||
| 315 | ); | ||
| 316 | } | ||
| 317 | |||
| 318 | /// Tests that `Config`, `ShaMismatch`, `Process`, and `Runtime` variants are formatted consistently. | ||
| 319 | /// | ||
| 320 | /// This test checks that the `Display` and `Debug` formats are the same for the `Config`, `ShaMismatch`, | ||
| 321 | /// `Process`, and `Runtime` variants of `McError`. The test ensures that these variants are correctly | ||
| 322 | /// formatted both for user-facing display (`Display`) and for debugging purposes (`Debug`), | ||
| 323 | /// confirming consistency between the two formats. | ||
| 324 | #[test] | ||
| 325 | fn config_sha_process_runtime_variants() { | ||
| 326 | let error_variants = vec![ | ||
| 327 | Config("bad config".into()), | ||
| 328 | ShaMismatch("bad sha".into()), | ||
| 329 | Process("failed process".into()), | ||
| 330 | Runtime("runtime error".into()), | ||
| 331 | ]; | ||
| 332 | |||
| 333 | for error in error_variants { | ||
| 334 | assert_eq!(format!("{}", error), format!("{:?}", error)); | ||
| 335 | } | ||
| 336 | } | ||
| 45 | } | 337 | } |
diff --git a/src/main.rs b/src/main.rs index e229a3e..4b32c51 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -1,47 +1,116 @@ | |||
| 1 | mod constants; | 1 | //! Main module for the DML Launcher. |
| 2 | mod errors; | 2 | //! |
| 3 | //! This module contains the entry point and orchestrates the full lifecycle | ||
| 4 | //! of the launcher. It coordinates configuration loading, environment setup, | ||
| 5 | //! version management, asset downloading, native extraction, and client launch. | ||
| 6 | //! | ||
| 7 | //! # Workflow | ||
| 8 | //! | ||
| 9 | //! 1. Load environment variables from .env. | ||
| 10 | //! 2. Initialize logging with the warning level. | ||
| 11 | //! 3. Parse command-line arguments using clap. | ||
| 12 | //! 4. Load the persistent configuration file. | ||
| 13 | //! 5. Apply command-line overrides to configuration values. | ||
| 14 | //! 6. Ensure all necessary directories exist. | ||
| 15 | //! 7. Load the version manifest for the selected Minecraft version. | ||
| 16 | //! 8. Download required assets including libraries and client binaries. | ||
| 17 | //! 9. Extract native libraries for the current platform. | ||
| 18 | //! 10. Launch the Minecraft client with the configured JVM arguments and | ||
| 19 | //! session information. | ||
| 20 | //! | ||
| 21 | //! # Error Handling | ||
| 22 | //! | ||
| 23 | //! All operations that can fail propagate errors through the | ||
| 24 | //! McError type. This centralizes error management and | ||
| 25 | //! simplifies the use of the ? operator throughout the launcher. | ||
| 26 | //! | ||
| 27 | //! # Asynchronous Operations | ||
| 28 | //! | ||
| 29 | //! HTTP requests and asset downloads are performed asynchronously using | ||
| 30 | //! tokio to maximize efficiency and reduce blocking. Local file | ||
| 31 | //! operations and the client launch remain synchronous to maintain | ||
| 32 | //! consistency and avoid race conditions. | ||
| 3 | 33 | ||
| 4 | mod config; | 34 | mod config; |
| 35 | mod constants; | ||
| 36 | mod errors; | ||
| 5 | mod minecraft; | 37 | mod minecraft; |
| 6 | mod platform; | 38 | mod platform; |
| 7 | mod util; | 39 | mod util; |
| 8 | 40 | ||
| 9 | use clap::Parser; | 41 | use clap::Parser; |
| 10 | use config::Config; | ||
| 11 | use dotenvy::dotenv; | 42 | use dotenvy::dotenv; |
| 12 | use errors::McError; | 43 | use errors::McError; |
| 13 | use log::{debug, info}; | 44 | use log::LevelFilter::Warn; |
| 45 | use reqwest::Client; | ||
| 46 | use platform::paths::ensure_directories; | ||
| 14 | 47 | ||
| 15 | use crate::minecraft::{ | 48 | use crate::{ |
| 16 | downloads::download_all, extraction::extract_natives, launcher::launch, | 49 | config::ConfigLoader, |
| 17 | manifests, | 50 | minecraft::{ |
| 51 | downloads::download_all_files, extraction::extract_natives, launcher::launch, | ||
| 52 | manifests::load_version, | ||
| 53 | }, | ||
| 18 | }; | 54 | }; |
| 55 | use crate::config::RuntimeConfig; | ||
| 56 | use crate::minecraft::manifests::Version; | ||
| 19 | 57 | ||
| 58 | /// Command-line interface definition. | ||
| 59 | /// | ||
| 60 | /// Provides user-configurable options for the launcher, including | ||
| 61 | /// specifying a Minecraft version, a username, and additional | ||
| 62 | /// JVM arguments. Utilizes clap for argument parsing and validation. | ||
| 20 | #[derive(Parser, Debug)] | 63 | #[derive(Parser, Debug)] |
| 21 | #[command(author, about, disable_version_flag = true)] | 64 | #[command(author, about, disable_version_flag = true)] |
| 22 | struct Cli { | 65 | struct Cli { |
| 66 | /// Optional Minecraft version to launch. | ||
| 23 | #[arg(long)] | 67 | #[arg(long)] |
| 24 | version: Option<String>, | 68 | version: Option<String>, |
| 25 | 69 | ||
| 70 | /// Optional username for the Minecraft session. | ||
| 26 | #[arg(long)] | 71 | #[arg(long)] |
| 27 | username: Option<String>, | 72 | username: Option<String>, |
| 28 | 73 | ||
| 74 | /// Optional JVM arguments to pass to the Minecraft process. | ||
| 75 | /// | ||
| 76 | /// Supports multiple values and allows arguments that begin with hyphens. | ||
| 29 | #[arg(long, num_args(0..), allow_hyphen_values = true)] | 77 | #[arg(long, num_args(0..), allow_hyphen_values = true)] |
| 30 | jvm_args: Vec<String>, | 78 | jvm_args: Vec<String>, |
| 31 | } | 79 | } |
| 32 | 80 | ||
| 81 | /// Launcher entry point. | ||
| 82 | /// | ||
| 83 | /// This asynchronous function orchestrates the entire launch process, | ||
| 84 | /// handling configuration, directory setup, asset management, native | ||
| 85 | /// extraction, and client execution. Returns a Result<(), McError> | ||
| 86 | /// to capture and propagate all errors encountered during execution. | ||
| 87 | /// | ||
| 88 | /// # Steps | ||
| 89 | /// | ||
| 90 | /// 1. Load environment variables from .env. | ||
| 91 | /// 2. Initialize logging with a warning level filter. | ||
| 92 | /// 3. Parse CLI arguments and merge them into the configuration. | ||
| 93 | /// 4. Ensure required directories exist for game data and assets. | ||
| 94 | /// 5. Load the manifest for the specified Minecraft version. | ||
| 95 | /// 6. Build an HTTP client with HTTP/2 support for asset downloads. | ||
| 96 | /// 7. Download all required assets including libraries and client binaries. | ||
| 97 | /// 8. Extract platform-specific native libraries. | ||
| 98 | /// 9. Launch the Minecraft client using the configured JVM arguments and | ||
| 99 | /// session. | ||
| 33 | #[tokio::main] | 100 | #[tokio::main] |
| 34 | async fn main() -> Result<(), McError> { | 101 | async fn main() -> Result<(), McError> { |
| 35 | dotenv().ok(); | 102 | dotenv().ok(); |
| 36 | env_logger::init(); | ||
| 37 | 103 | ||
| 38 | let cli = Cli::parse(); | 104 | env_logger::Builder::new() |
| 39 | let mut config = Config::load()?; | 105 | .filter_level(Warn) |
| 106 | .init(); | ||
| 40 | 107 | ||
| 108 | let cli: Cli = Cli::parse(); | ||
| 109 | let mut config: RuntimeConfig = ConfigLoader::load(None)?; | ||
| 110 | |||
| 41 | if let Some(v) = cli.version { | 111 | if let Some(v) = cli.version { |
| 42 | config.version = v; | 112 | config.version = v; |
| 43 | } | 113 | } |
| 44 | |||
| 45 | if let Some(u) = cli.username { | 114 | if let Some(u) = cli.username { |
| 46 | config.username = u; | 115 | config.username = u; |
| 47 | } | 116 | } |
| @@ -49,16 +118,14 @@ async fn main() -> Result<(), McError> { | |||
| 49 | config.jvm_args = cli.jvm_args; | 118 | config.jvm_args = cli.jvm_args; |
| 50 | } | 119 | } |
| 51 | 120 | ||
| 52 | info!("Final config after CLI overrides: {:?}", config); | 121 | ensure_directories(&config)?; |
| 53 | |||
| 54 | platform::paths::ensure_dirs(&config)?; | ||
| 55 | info!("Using Minecraft version {}", config.version); | ||
| 56 | 122 | ||
| 57 | let version = manifests::load_version(&config).await?; | 123 | let version: Version = load_version(&config).await?; |
| 58 | info!("Loaded version manifest for: {}", version.id); | 124 | let client: Client = Client::builder() |
| 59 | debug!("Main class: {}", version.main_class); | 125 | .http2_prior_knowledge() |
| 126 | .build()?; | ||
| 60 | 127 | ||
| 61 | download_all(&config, &version).await?; | 128 | download_all_files(&client, &config, &version).await?; |
| 62 | extract_natives(&config, &version)?; | 129 | extract_natives(&config, &version)?; |
| 63 | launch(&config, &version)?; | 130 | launch(&config, &version)?; |
| 64 | 131 | ||
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 @@ | |||
| 1 | use log::{debug, info}; | 1 | use std::{ |
| 2 | use reqwest::get; | 2 | collections::HashMap, |
| 3 | io::{Write, stdout}, | ||
| 4 | path::{Path, PathBuf}, | ||
| 5 | sync::{ | ||
| 6 | Arc, | ||
| 7 | atomic::{AtomicUsize, Ordering}, | ||
| 8 | }, | ||
| 9 | }; | ||
| 10 | |||
| 11 | use McError::Config; | ||
| 12 | use Ordering::SeqCst; | ||
| 13 | use futures::stream::{FuturesUnordered, StreamExt}; | ||
| 14 | use reqwest::Response; | ||
| 3 | use serde::Deserialize; | 15 | use serde::Deserialize; |
| 4 | use tokio::{ | 16 | use tokio::{ |
| 5 | fs::{self, File, create_dir_all}, | 17 | fs::{File, create_dir_all, read_to_string, write}, |
| 6 | io::AsyncWriteExt, | 18 | io::AsyncWriteExt, |
| 19 | spawn, | ||
| 20 | sync::Semaphore, | ||
| 21 | task::{JoinError, JoinHandle}, | ||
| 7 | }; | 22 | }; |
| 8 | 23 | ||
| 9 | use crate::{ | 24 | use crate::{ |
| 10 | config::Config, | 25 | config::RuntimeConfig, |
| 11 | errors::McError, | 26 | errors::McError, |
| 12 | minecraft::manifests::{Library, Version}, | 27 | minecraft::manifests::{AssetIndex, Library, LibraryArtifact, Version}, |
| 13 | platform::paths, | 28 | platform::paths::{assets_directory, client_jar, library_file}, |
| 14 | }; | 29 | }; |
| 15 | 30 | ||
| 31 | const MAX_CONCURRENT_DOWNLOADS: usize = 100; | ||
| 32 | const PROGRESS_INITIAL_VALUE: usize = 0; | ||
| 33 | const ASSET_URL_BASE: &str = "https://resources.download.minecraft.net/"; | ||
| 34 | const MAX_PROGRESS_PERCENT: f64 = 100.0; | ||
| 35 | const PROGRESS_PRINT_WIDTH: usize = 3; | ||
| 36 | const ATOMIC_ORDERING: Ordering = SeqCst; | ||
| 37 | const DOWNLOAD_COMPLETE_MESSAGE: &str = "All downloads completed successfully!"; | ||
| 38 | const ASSET_INDEX_MISSING_ERROR: &str = "Missing asset_index for the version"; | ||
| 39 | |||
| 40 | impl From<JoinError> for McError { | ||
| 41 | fn from(error: JoinError) -> Self { | ||
| 42 | Config(format!("Task panicked: {}", error)) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | /// Represents a single file to download, including the URL and destination | ||
| 47 | /// path. | ||
| 48 | #[derive(Debug, Clone)] | ||
| 49 | struct DownloadJob { | ||
| 50 | url: String, | ||
| 51 | destination_path: PathBuf, | ||
| 52 | } | ||
| 53 | |||
| 54 | impl DownloadJob { | ||
| 55 | fn already_exists(&self) -> bool { self.destination_path.exists() } | ||
| 56 | } | ||
| 57 | |||
| 58 | /// Represents a single asset entry in the Minecraft asset index. | ||
| 16 | #[derive(Debug, Deserialize)] | 59 | #[derive(Debug, Deserialize)] |
| 17 | struct AssetObject { | 60 | struct AssetObject { |
| 18 | hash: String, | 61 | hash: String, |
| 19 | // size: u64, | ||
| 20 | } | 62 | } |
| 21 | 63 | ||
| 64 | /// The Minecraft asset index manifest. | ||
| 22 | #[derive(Debug, Deserialize)] | 65 | #[derive(Debug, Deserialize)] |
| 23 | struct AssetIndexManifest { | 66 | struct AssetIndexManifest { |
| 24 | objects: std::collections::HashMap<String, AssetObject>, | 67 | objects: HashMap<String, AssetObject>, |
| 25 | } | 68 | } |
| 26 | 69 | ||
| 27 | pub async fn download_all( | 70 | /// Download all files required to run a specific Minecraft version. |
| 28 | config: &Config, | 71 | pub async fn download_all_files( |
| 29 | version: &Version, | 72 | http_client: &reqwest::Client, |
| 73 | runtime_config: &RuntimeConfig, | ||
| 74 | version_info: &Version, | ||
| 30 | ) -> Result<(), McError> { | 75 | ) -> Result<(), McError> { |
| 31 | download_client(config, version).await?; | 76 | let assets_directory_path: PathBuf = |
| 32 | download_libraries(config, &version.libraries).await?; | 77 | ensure_assets_directories_exist(runtime_config).await?; |
| 33 | download_assets(config, version).await?; | 78 | let asset_manifest: AssetIndexManifest = load_asset_index_manifest( |
| 79 | http_client, | ||
| 80 | &assets_directory_path, | ||
| 81 | version_info, | ||
| 82 | ) | ||
| 83 | .await?; | ||
| 84 | |||
| 85 | let download_jobs: Vec<DownloadJob> = build_download_jobs( | ||
| 86 | runtime_config, | ||
| 87 | version_info, | ||
| 88 | &assets_directory_path, | ||
| 89 | &asset_manifest, | ||
| 90 | ); | ||
| 91 | |||
| 92 | execute_download_jobs(http_client, download_jobs).await?; | ||
| 93 | println!("\n{}", DOWNLOAD_COMPLETE_MESSAGE); | ||
| 94 | |||
| 34 | Ok(()) | 95 | Ok(()) |
| 35 | } | 96 | } |
| 36 | 97 | ||
| 37 | async fn download_client( | 98 | /// Ensure the essential assets directories exist. |
| 38 | config: &Config, | 99 | async fn ensure_assets_directories_exist( |
| 39 | version: &Version, | 100 | config: &RuntimeConfig, |
| 40 | ) -> Result<(), McError> { | 101 | ) -> Result<PathBuf, McError> { |
| 41 | let jar_path = paths::client_jar(config, &version.id)?; | 102 | let assets_dir = assets_directory(config); |
| 42 | 103 | create_dir_all(assets_dir.join("objects")).await?; | |
| 43 | if jar_path.exists() { | 104 | create_dir_all(assets_dir.join("indexes")).await?; |
| 44 | debug!("Client jar already exists: {}", jar_path.display()); | 105 | Ok(assets_dir) |
| 45 | return Ok(()); | ||
| 46 | } | ||
| 47 | |||
| 48 | info!("Downloading client {}", version.id); | ||
| 49 | download_file(&version.downloads.client.url, &jar_path).await | ||
| 50 | } | 106 | } |
| 51 | 107 | ||
| 52 | async fn download_libraries( | 108 | /// Load the asset index manifest for the given Minecraft version. |
| 53 | config: &Config, | 109 | async fn load_asset_index_manifest( |
| 54 | libraries: &[Library], | 110 | http_client: &reqwest::Client, |
| 55 | ) -> Result<(), McError> { | 111 | assets_dir: &Path, |
| 56 | for library in libraries { | 112 | version_info: &Version, |
| 57 | if let Some(artifact) = &library.downloads.artifact { | 113 | ) -> Result<AssetIndexManifest, McError> { |
| 58 | let library_path = paths::library_file(config, &artifact.path)?; | 114 | let asset_index: &AssetIndex = version_info |
| 59 | 115 | .asset_index | |
| 60 | if !library_path.exists() { | 116 | .as_ref() |
| 61 | info!("Downloading library {}", artifact.path); | 117 | .ok_or_else(|| Config(ASSET_INDEX_MISSING_ERROR.into()))?; |
| 62 | download_file(&artifact.url, &library_path).await?; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | if let Some(classifiers) = &library.downloads.classifiers { | ||
| 67 | for (_, native) in classifiers { | ||
| 68 | let native_path = paths::library_file(config, &native.path)?; | ||
| 69 | 118 | ||
| 70 | if native_path.exists() { | 119 | let index_file_path: PathBuf = assets_dir |
| 71 | continue; | 120 | .join("indexes") |
| 72 | } | 121 | .join(format!("{}.json", asset_index.id)); |
| 73 | 122 | ||
| 74 | info!("Downloading native library {}", native.path); | 123 | if !index_file_path.exists() { |
| 75 | download_file(&native.url, &native_path).await?; | 124 | download_text_file(http_client, &asset_index.url, &index_file_path) |
| 76 | } | 125 | .await?; |
| 77 | } | ||
| 78 | } | 126 | } |
| 79 | 127 | ||
| 80 | Ok(()) | 128 | let json_string: String = read_to_string(index_file_path).await?; |
| 129 | Ok(serde_json::from_str(&json_string)?) | ||
| 81 | } | 130 | } |
| 82 | 131 | ||
| 83 | async fn download_asset_index( | 132 | fn build_download_jobs( |
| 84 | config: &Config, | 133 | config: &RuntimeConfig, |
| 85 | version: &Version, | 134 | version_info: &Version, |
| 86 | ) -> Result<AssetIndexManifest, McError> { | 135 | assets_dir: &Path, |
| 87 | let assets_dir = paths::assets_dir(config); | 136 | asset_manifest: &AssetIndexManifest, |
| 88 | create_dir_all(assets_dir.join("indexes")).await?; | 137 | ) -> Vec<DownloadJob> { |
| 138 | let mut jobs: Vec<DownloadJob> = Vec::new(); | ||
| 89 | 139 | ||
| 90 | let asset_index = version.asset_index.as_ref().ok_or_else(|| { | 140 | add_client_download_job(&mut jobs, config, version_info); |
| 91 | McError::Config("Missing asset_index in version.json".into()) | 141 | add_library_download_jobs(&mut jobs, config, version_info); |
| 92 | })?; | 142 | add_asset_download_jobs(&mut jobs, assets_dir, asset_manifest); |
| 93 | 143 | ||
| 94 | if asset_index.id == "legacy" { | 144 | jobs |
| 95 | return Err(McError::Config( | 145 | } |
| 96 | "Legacy assetIndex detected – pobierz właściwy version.json".into(), | ||
| 97 | )); | ||
| 98 | } | ||
| 99 | 146 | ||
| 100 | let index_path = assets_dir | 147 | fn add_client_download_job( |
| 101 | .join("indexes") | 148 | jobs: &mut Vec<DownloadJob>, |
| 102 | .join(format!("{}.json", asset_index.id)); | 149 | config: &RuntimeConfig, |
| 150 | version_info: &Version, | ||
| 151 | ) { | ||
| 152 | jobs.push(DownloadJob { | ||
| 153 | url: version_info.downloads.client.url.clone(), | ||
| 154 | destination_path: client_jar(config, &version_info.id), | ||
| 155 | }); | ||
| 156 | } | ||
| 103 | 157 | ||
| 104 | if index_path.exists() { | 158 | fn add_library_download_jobs( |
| 105 | let index_data = fs::read_to_string(&index_path).await?; | 159 | jobs: &mut Vec<DownloadJob>, |
| 106 | let manifest: AssetIndexManifest = serde_json::from_str(&index_data)?; | 160 | config: &RuntimeConfig, |
| 107 | return Ok(manifest); | 161 | version_info: &Version, |
| 162 | ) { | ||
| 163 | for library in &version_info.libraries { | ||
| 164 | add_library_artifact_job(jobs, config, library); | ||
| 165 | add_library_classifier_jobs(jobs, config, library); | ||
| 108 | } | 166 | } |
| 167 | } | ||
| 168 | fn add_library_artifact_job( | ||
| 169 | jobs: &mut Vec<DownloadJob>, | ||
| 170 | config: &RuntimeConfig, | ||
| 171 | library: &Library, | ||
| 172 | ) { | ||
| 173 | let artifact: &LibraryArtifact = match &library.downloads.artifact { | ||
| 174 | | Some(a) => a, | ||
| 175 | | None => return, | ||
| 176 | }; | ||
| 177 | |||
| 178 | jobs.push(DownloadJob { | ||
| 179 | url: artifact.url.clone(), | ||
| 180 | destination_path: library_file(config, &artifact.path), | ||
| 181 | }); | ||
| 182 | } | ||
| 109 | 183 | ||
| 110 | info!("Downloading asset index {}", asset_index.id); | 184 | fn add_library_classifier_jobs( |
| 111 | let response = get(&asset_index.url).await?; | 185 | jobs: &mut Vec<DownloadJob>, |
| 112 | let manifest_text = response.text().await?; | 186 | config: &RuntimeConfig, |
| 113 | 187 | library: &Library, | |
| 114 | fs::write(&index_path, &manifest_text).await?; | 188 | ) { |
| 189 | let classifiers: &HashMap<String, LibraryArtifact> = | ||
| 190 | match &library.downloads.classifiers { | ||
| 191 | | Some(values) => values, | ||
| 192 | | None => return, | ||
| 193 | }; | ||
| 194 | |||
| 195 | for classifier_entry in classifiers.values() { | ||
| 196 | jobs.push(DownloadJob { | ||
| 197 | url: classifier_entry.url.clone(), | ||
| 198 | destination_path: library_file(config, &classifier_entry.path), | ||
| 199 | }); | ||
| 200 | } | ||
| 201 | } | ||
| 115 | 202 | ||
| 116 | let manifest: AssetIndexManifest = serde_json::from_str(&manifest_text)?; | 203 | fn add_asset_download_jobs( |
| 117 | Ok(manifest) | 204 | jobs: &mut Vec<DownloadJob>, |
| 205 | assets_dir: &Path, | ||
| 206 | asset_manifest: &AssetIndexManifest, | ||
| 207 | ) { | ||
| 208 | for asset_object in asset_manifest.objects.values() { | ||
| 209 | let prefix: &str = &asset_object.hash[0..2]; | ||
| 210 | jobs.push(DownloadJob { | ||
| 211 | url: format!("{}{}/{}", ASSET_URL_BASE, prefix, asset_object.hash), | ||
| 212 | destination_path: assets_dir | ||
| 213 | .join("objects") | ||
| 214 | .join(prefix) | ||
| 215 | .join(&asset_object.hash), | ||
| 216 | }); | ||
| 217 | } | ||
| 118 | } | 218 | } |
| 119 | 219 | ||
| 120 | async fn download_assets( | 220 | async fn execute_download_jobs( |
| 121 | config: &Config, | 221 | http_client: &reqwest::Client, |
| 122 | version: &Version, | 222 | download_jobs: Vec<DownloadJob>, |
| 123 | ) -> Result<(), McError> { | 223 | ) -> Result<(), McError> { |
| 124 | let assets_dir = paths::assets_dir(config); | 224 | let total_jobs_count: usize = download_jobs.len(); |
| 125 | 225 | let completed_jobs_count: Arc<AtomicUsize> = | |
| 126 | create_dir_all(assets_dir.join("objects")).await?; | 226 | Arc::new(AtomicUsize::new(PROGRESS_INITIAL_VALUE)); |
| 127 | create_dir_all(assets_dir.join("indexes")).await?; | 227 | let concurrent_download_semaphore: Arc<Semaphore> = |
| 128 | 228 | Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS)); | |
| 129 | let manifest = download_asset_index(config, version).await?; | 229 | |
| 230 | let mut tasks: FuturesUnordered<JoinHandle<Result<(), McError>>> = | ||
| 231 | spawn_missing_download_jobs( | ||
| 232 | http_client, | ||
| 233 | download_jobs, | ||
| 234 | &completed_jobs_count, | ||
| 235 | total_jobs_count, | ||
| 236 | concurrent_download_semaphore, | ||
| 237 | ); | ||
| 130 | 238 | ||
| 131 | for (logical_path, asset) in &manifest.objects { | 239 | await_download_tasks(&mut tasks).await |
| 132 | let subdir = &asset.hash[0..2]; | 240 | } |
| 133 | let file_path = assets_dir | ||
| 134 | .join("objects") | ||
| 135 | .join(subdir) | ||
| 136 | .join(&asset.hash); | ||
| 137 | 241 | ||
| 138 | if file_path.exists() { | 242 | fn spawn_missing_download_jobs( |
| 243 | http_client: &reqwest::Client, | ||
| 244 | download_jobs: Vec<DownloadJob>, | ||
| 245 | completed_jobs_counter: &Arc<AtomicUsize>, | ||
| 246 | total_jobs_count: usize, | ||
| 247 | concurrent_download_semaphore: Arc<Semaphore>, | ||
| 248 | ) -> FuturesUnordered<JoinHandle<Result<(), McError>>> { | ||
| 249 | let download_tasks: FuturesUnordered<JoinHandle<Result<(), McError>>> = | ||
| 250 | FuturesUnordered::new(); | ||
| 251 | |||
| 252 | for job in download_jobs { | ||
| 253 | if job.already_exists() { | ||
| 254 | completed_jobs_counter.fetch_add(1, SeqCst); | ||
| 255 | print_download_progress(completed_jobs_counter, total_jobs_count); | ||
| 139 | continue; | 256 | continue; |
| 140 | } | 257 | } |
| 141 | 258 | ||
| 142 | let url = format!( | 259 | download_tasks.push(spawn_download_job( |
| 143 | "https://resources.download.minecraft.net/{}/{}", | 260 | http_client.clone(), |
| 144 | subdir, asset.hash | 261 | job, |
| 145 | ); | 262 | concurrent_download_semaphore.clone(), |
| 146 | info!("Downloading asset {} -> {}", logical_path, file_path.display()); | 263 | completed_jobs_counter.clone(), |
| 147 | download_file(&url, &file_path).await?; | 264 | total_jobs_count, |
| 265 | )); | ||
| 148 | } | 266 | } |
| 149 | 267 | ||
| 150 | if let Some(asset) = manifest.objects.get("sounds.json") { | 268 | download_tasks |
| 151 | let file_path = assets_dir.join("indexes").join("sounds.json"); | 269 | } |
| 152 | if !file_path.exists() { | 270 | |
| 153 | let subdir = &asset.hash[0..2]; | 271 | async fn await_download_tasks( |
| 154 | let url = format!( | 272 | tasks: &mut FuturesUnordered<JoinHandle<Result<(), McError>>>, |
| 155 | "https://resources.download.minecraft.net/{}/{}", | 273 | ) -> Result<(), McError> { |
| 156 | subdir, asset.hash | 274 | while let Some(task_result) = tasks.next().await { |
| 157 | ); | 275 | task_result.map_err(McError::from)??; |
| 158 | info!("Downloading sounds.json"); | ||
| 159 | download_file(&url, &file_path).await?; | ||
| 160 | } | ||
| 161 | } | 276 | } |
| 277 | Ok(()) | ||
| 278 | } | ||
| 162 | 279 | ||
| 280 | fn spawn_download_job( | ||
| 281 | http_client: reqwest::Client, | ||
| 282 | download_job: DownloadJob, | ||
| 283 | concurrent_download_semaphore: Arc<Semaphore>, | ||
| 284 | completed_jobs_count: Arc<AtomicUsize>, | ||
| 285 | total_jobs_count: usize, | ||
| 286 | ) -> JoinHandle<Result<(), McError>> { | ||
| 287 | spawn(async move { | ||
| 288 | let _permit = concurrent_download_semaphore | ||
| 289 | .acquire_owned() | ||
| 290 | .await | ||
| 291 | .unwrap(); | ||
| 292 | download_file(&http_client, &download_job).await?; | ||
| 293 | completed_jobs_count.fetch_add(1, SeqCst); | ||
| 294 | print_download_progress(&completed_jobs_count, total_jobs_count); | ||
| 295 | Ok(()) | ||
| 296 | }) | ||
| 297 | } | ||
| 298 | |||
| 299 | /// Print the current progress of downloads to stdout. | ||
| 300 | /// | ||
| 301 | /// # Parameters | ||
| 302 | /// - `completed_jobs_count`: Atomic counter of completed download jobs. | ||
| 303 | /// - `total_jobs_count`: Total number of jobs being processed. | ||
| 304 | fn print_download_progress( | ||
| 305 | completed_jobs_count: &AtomicUsize, | ||
| 306 | total_jobs_count: usize, | ||
| 307 | ) { | ||
| 308 | let completed_jobs: usize = completed_jobs_count.load(ATOMIC_ORDERING); | ||
| 309 | let progress_percentage: f64 = ((completed_jobs as f64 | ||
| 310 | / total_jobs_count as f64) | ||
| 311 | * MAX_PROGRESS_PERCENT) | ||
| 312 | .min(MAX_PROGRESS_PERCENT); | ||
| 313 | |||
| 314 | print!( | ||
| 315 | "\rDownloading game files: {:>width$.0}%", | ||
| 316 | progress_percentage, | ||
| 317 | width = PROGRESS_PRINT_WIDTH | ||
| 318 | ); | ||
| 319 | stdout().flush().unwrap(); | ||
| 320 | } | ||
| 321 | |||
| 322 | async fn download_text_file( | ||
| 323 | http_client: &reqwest::Client, | ||
| 324 | file_url: &str, | ||
| 325 | destination_path: &PathBuf, | ||
| 326 | ) -> Result<(), McError> { | ||
| 327 | let text_content = http_client | ||
| 328 | .get(file_url) | ||
| 329 | .send() | ||
| 330 | .await? | ||
| 331 | .error_for_status()? | ||
| 332 | .text() | ||
| 333 | .await?; | ||
| 334 | write(destination_path, text_content).await?; | ||
| 163 | Ok(()) | 335 | Ok(()) |
| 164 | } | 336 | } |
| 165 | 337 | ||
| 166 | async fn download_file( | 338 | async fn download_file( |
| 167 | url: &str, | 339 | http_client: &reqwest::Client, |
| 168 | path: &std::path::Path, | 340 | download_job: &DownloadJob, |
| 169 | ) -> Result<(), McError> { | 341 | ) -> Result<(), McError> { |
| 170 | if let Some(parent) = path.parent() { | 342 | if let Some(parent_dir) = download_job.destination_path.parent() { |
| 171 | create_dir_all(parent).await?; | 343 | create_dir_all(parent_dir).await?; |
| 172 | } | 344 | } |
| 173 | 345 | ||
| 174 | let response = get(url).await?; | 346 | let response: Response = http_client |
| 175 | let bytes = response.bytes().await?; | 347 | .get(&download_job.url) |
| 176 | 348 | .send() | |
| 177 | let mut file = File::create(path).await?; | 349 | .await? |
| 178 | file.write_all(&bytes).await?; | 350 | .error_for_status()?; |
| 351 | let mut byte_stream = response.bytes_stream(); | ||
| 352 | let mut file_handle: File = | ||
| 353 | File::create(&download_job.destination_path).await?; | ||
| 354 | |||
| 355 | while let Some(chunk) = byte_stream.next().await { | ||
| 356 | file_handle.write_all(&chunk?).await?; | ||
| 357 | } | ||
| 179 | 358 | ||
| 180 | Ok(()) | 359 | Ok(()) |
| 181 | } | 360 | } |
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 @@ | |||
| 1 | use std::{fs, io, path::Path}; | 1 | use std::{ |
| 2 | collections::HashMap, | ||
| 3 | fs, | ||
| 4 | fs::File, | ||
| 5 | io, | ||
| 6 | path::{Path, PathBuf}, | ||
| 7 | }; | ||
| 2 | 8 | ||
| 3 | use log::info; | 9 | use zip::{read::ZipFile, ZipArchive}; |
| 4 | use zip::ZipArchive; | ||
| 5 | 10 | ||
| 6 | use crate::{ | 11 | use crate::{ |
| 7 | errors::McError, | 12 | errors::McError, |
| 8 | minecraft::manifests::{Library, Version}, | 13 | minecraft::manifests::{LibraryArtifact, Version}, |
| 14 | util::fs::library_allowed, | ||
| 9 | }; | 15 | }; |
| 10 | 16 | ||
| 11 | pub fn extract_natives( | 17 | pub fn extract_natives( |
| @@ -19,8 +25,6 @@ pub fn extract_natives( | |||
| 19 | .join(&version.id) | 25 | .join(&version.id) |
| 20 | .join("natives"); | 26 | .join("natives"); |
| 21 | 27 | ||
| 22 | info!("Extracting natives for {} into {:?}", version.id, natives_dir); | ||
| 23 | |||
| 24 | if natives_dir.exists() { | 28 | if natives_dir.exists() { |
| 25 | fs::remove_dir_all(&natives_dir)?; | 29 | fs::remove_dir_all(&natives_dir)?; |
| 26 | } | 30 | } |
| @@ -31,75 +35,51 @@ pub fn extract_natives( | |||
| 31 | continue; | 35 | continue; |
| 32 | } | 36 | } |
| 33 | 37 | ||
| 34 | let natives = match &lib.natives { | 38 | let natives: &HashMap<String, String> = match &lib.natives { |
| 35 | | Some(n) => n, | 39 | | Some(n) => n, |
| 36 | | None => continue, | 40 | | None => continue, |
| 37 | }; | 41 | }; |
| 38 | 42 | ||
| 39 | let classifier = match natives.get("linux") { | 43 | let classifier: &String = match natives.get("linux") { |
| 40 | | Some(c) => c, | 44 | | Some(c) => c, |
| 41 | | None => continue, | 45 | | None => continue, |
| 42 | }; | 46 | }; |
| 43 | 47 | ||
| 44 | let classifiers = match &lib.downloads.classifiers { | 48 | let classifiers: &HashMap<String, LibraryArtifact> = |
| 45 | | Some(c) => c, | 49 | match &lib.downloads.classifiers { |
| 46 | | None => continue, | 50 | | Some(c) => c, |
| 47 | }; | 51 | | None => continue, |
| 52 | }; | ||
| 48 | 53 | ||
| 49 | let artifact = match classifiers.get(classifier) { | 54 | let artifact: &LibraryArtifact = match classifiers.get(classifier) { |
| 50 | | Some(a) => a, | 55 | | Some(a) => a, |
| 51 | | None => continue, | 56 | | None => continue, |
| 52 | }; | 57 | }; |
| 53 | 58 | ||
| 54 | let jar_path = cfg | 59 | let jar_path: PathBuf = cfg |
| 55 | .data_dir | 60 | .data_dir |
| 56 | .join("minecraft") | 61 | .join("minecraft") |
| 57 | .join("libraries") | 62 | .join("libraries") |
| 58 | .join(&artifact.path); | 63 | .join(&artifact.path); |
| 59 | 64 | ||
| 60 | info!("Extracting natives from {:?}", jar_path); | ||
| 61 | |||
| 62 | extract_zip(&jar_path, &natives_dir)?; | 65 | extract_zip(&jar_path, &natives_dir)?; |
| 63 | } | 66 | } |
| 64 | 67 | ||
| 65 | Ok(()) | 68 | Ok(()) |
| 66 | } | 69 | } |
| 67 | |||
| 68 | fn library_allowed(lib: &Library) -> bool { | ||
| 69 | let rules = match &lib.rules { | ||
| 70 | | Some(r) => r, | ||
| 71 | | None => return true, | ||
| 72 | }; | ||
| 73 | |||
| 74 | let mut allowed = false; | ||
| 75 | |||
| 76 | for rule in rules { | ||
| 77 | let os_match = match &rule.os { | ||
| 78 | | Some(os) => os.name == "linux", | ||
| 79 | | None => true, | ||
| 80 | }; | ||
| 81 | |||
| 82 | if os_match { | ||
| 83 | allowed = rule.action == "allow"; | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | allowed | ||
| 88 | } | ||
| 89 | |||
| 90 | fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { | 70 | fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { |
| 91 | let file = fs::File::open(jar_path)?; | 71 | let file: File = File::open(jar_path)?; |
| 92 | let mut zip = ZipArchive::new(file)?; | 72 | let mut zip: ZipArchive<File> = ZipArchive::new(file)?; |
| 93 | 73 | ||
| 94 | for i in 0..zip.len() { | 74 | for i in 0..zip.len() { |
| 95 | let mut entry = zip.by_index(i)?; | 75 | let mut entry: ZipFile<File> = zip.by_index(i)?; |
| 96 | let name = entry.name(); | 76 | let name: &str = entry.name(); |
| 97 | 77 | ||
| 98 | if name.starts_with("META-INF/") { | 78 | if name.starts_with("META-INF/") { |
| 99 | continue; | 79 | continue; |
| 100 | } | 80 | } |
| 101 | 81 | ||
| 102 | let out_path = out_dir.join(name); | 82 | let out_path: PathBuf = out_dir.join(name); |
| 103 | 83 | ||
| 104 | if entry.is_dir() { | 84 | if entry.is_dir() { |
| 105 | fs::create_dir_all(&out_path)?; | 85 | fs::create_dir_all(&out_path)?; |
| @@ -110,7 +90,7 @@ fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { | |||
| 110 | fs::create_dir_all(parent)?; | 90 | fs::create_dir_all(parent)?; |
| 111 | } | 91 | } |
| 112 | 92 | ||
| 113 | let mut out_file = fs::File::create(&out_path)?; | 93 | let mut out_file: File = File::create(&out_path)?; |
| 114 | io::copy(&mut entry, &mut out_file)?; | 94 | io::copy(&mut entry, &mut out_file)?; |
| 115 | } | 95 | } |
| 116 | 96 | ||
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 @@ | |||
| 1 | use std::process::Command; | 1 | use paths::{client_jar, library_file, natives_dir}; |
| 2 | 2 | use serde::Deserialize; | |
| 3 | use log::{debug, info}; | 3 | use std::path::Path; |
| 4 | use std::process::Output; | ||
| 5 | use std::{ | ||
| 6 | path::PathBuf, | ||
| 7 | process::{Command, ExitStatus}, | ||
| 8 | }; | ||
| 9 | use McError::Process; | ||
| 4 | 10 | ||
| 5 | use crate::{ | 11 | use crate::{ |
| 6 | config::Config, | 12 | config::Config, errors::McError, minecraft::manifests::Version, |
| 7 | errors::McError, | 13 | platform::paths, util::fs::library_allowed, |
| 8 | minecraft::manifests::{Library, Version}, | ||
| 9 | platform::paths, | ||
| 10 | }; | 14 | }; |
| 11 | 15 | ||
| 12 | fn build_classpath( | 16 | #[derive(Debug, Clone, Deserialize)] |
| 13 | config: &Config, | 17 | pub struct JavaRuntime { |
| 14 | version: &Version, | 18 | pub major: u8, |
| 15 | ) -> Result<String, McError> { | 19 | pub path: PathBuf, |
| 16 | let sep = if cfg!(windows) { ";" } else { ":" }; | 20 | } |
| 17 | let mut entries = Vec::new(); | 21 | |
| 22 | impl JavaRuntime { | ||
| 23 | fn validate(&self) -> Result<(), McError> { | ||
| 24 | let output: Output = Command::new(&self.path) | ||
| 25 | .arg("-version") | ||
| 26 | .output() | ||
| 27 | .map_err(|e| { | ||
| 28 | McError::Runtime(format!( | ||
| 29 | "Failed to execute Java at {}: {}", | ||
| 30 | self.path.display(), | ||
| 31 | e | ||
| 32 | )) | ||
| 33 | })?; | ||
| 34 | |||
| 35 | if !output.status.success() { | ||
| 36 | return Err(McError::Runtime(format!( | ||
| 37 | "Invalid Java binary: {}", | ||
| 38 | self.path.display() | ||
| 39 | ))); | ||
| 40 | } | ||
| 41 | |||
| 42 | Ok(()) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | fn required_java_major(java_major: Option<u8>) -> u8 { | ||
| 47 | java_major.unwrap_or(8) | ||
| 48 | } | ||
| 49 | |||
| 50 | fn resolve_runtime( | ||
| 51 | required: u8, | ||
| 52 | runtimes: &[JavaRuntime], | ||
| 53 | ) -> Result<PathBuf, McError> { | ||
| 54 | let mut candidates: Vec<&JavaRuntime> = runtimes | ||
| 55 | .iter() | ||
| 56 | .filter(|r| r.major >= required) | ||
| 57 | .collect(); | ||
| 58 | |||
| 59 | candidates.sort_by_key(|r| r.major); | ||
| 60 | |||
| 61 | let runtime = candidates.first().ok_or_else(|| { | ||
| 62 | McError::Runtime(format!( | ||
| 63 | "No suitable Java runtime found (required: Java {})", | ||
| 64 | required | ||
| 65 | )) | ||
| 66 | })?; | ||
| 67 | |||
| 68 | runtime.validate()?; | ||
| 69 | Ok(runtime.path.clone()) | ||
| 70 | } | ||
| 71 | |||
| 72 | fn build_classpath(config: &Config, version: &Version) -> String { | ||
| 73 | let system_separator: &str = if cfg!(windows) { ";" } else { ":" }; | ||
| 74 | let mut entries: Vec<String> = Vec::new(); | ||
| 18 | 75 | ||
| 19 | for library in &version.libraries { | 76 | for library in &version.libraries { |
| 20 | if !library_allowed(library) { | 77 | if !library_allowed(library) { |
| 21 | continue; | 78 | continue; |
| 22 | } | 79 | } |
| 80 | |||
| 23 | if let Some(artifact) = &library.downloads.artifact { | 81 | if let Some(artifact) = &library.downloads.artifact { |
| 24 | let path = paths::library_file(config, &artifact.path)?; | 82 | let path = library_file(config, &artifact.path); |
| 25 | entries.push(path.to_string_lossy().to_string()); | 83 | entries.push(path.to_string_lossy().to_string()); |
| 26 | } | 84 | } |
| 27 | } | 85 | } |
| 28 | 86 | ||
| 29 | let client_jar = paths::client_jar(config, &version.id)?; | 87 | let client: PathBuf = client_jar(config, &version.id); |
| 30 | entries.push(client_jar.to_string_lossy().to_string()); | 88 | entries.push(client.to_string_lossy().to_string()); |
| 31 | 89 | ||
| 32 | Ok(entries.join(sep)) | 90 | entries.join(system_separator) |
| 33 | } | 91 | } |
| 34 | 92 | ||
| 35 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | 93 | fn build_command( |
| 36 | let java = &config.java_path; | 94 | java_path: PathBuf, |
| 37 | let classpath = build_classpath(config, version)?; | 95 | config: &Config, |
| 38 | let natives_dir = paths::natives_dir(config, &version.id); | 96 | version: &Version, |
| 39 | 97 | natives_dir: &Path, | |
| 40 | if !natives_dir.exists() { | 98 | classpath: String, |
| 41 | return Err(McError::Runtime(format!( | 99 | asset_index_id: String, |
| 42 | "Natives folder does not exist: {}", | 100 | ) -> Command { |
| 43 | natives_dir.display() | 101 | let mut cmd: Command = Command::new(java_path); |
| 44 | ))); | ||
| 45 | } | ||
| 46 | |||
| 47 | let asset_index_id = version | ||
| 48 | .asset_index | ||
| 49 | .as_ref() | ||
| 50 | .ok_or_else(|| { | ||
| 51 | McError::Runtime("Missing assetIndex in version.json".into()) | ||
| 52 | })? | ||
| 53 | .id | ||
| 54 | .clone(); | ||
| 55 | |||
| 56 | info!("Launching Minecraft {}", version.id); | ||
| 57 | debug!("Classpath: {}", classpath); | ||
| 58 | debug!("Natives: {}", natives_dir.display()); | ||
| 59 | debug!("Asset index: {}", asset_index_id); | ||
| 60 | |||
| 61 | let mut cmd = Command::new(java); | ||
| 62 | 102 | ||
| 63 | cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) | 103 | cmd.arg(format!("-Xmx{}M", config.max_memory_mb)) |
| 64 | .arg(format!("-Djava.library.path={}", natives_dir.display())); | 104 | .arg(format!("-Djava.library.path={}", natives_dir.display())); |
| @@ -76,11 +116,11 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | |||
| 76 | .arg("--version") | 116 | .arg("--version") |
| 77 | .arg(&version.id) | 117 | .arg(&version.id) |
| 78 | .arg("--gameDir") | 118 | .arg("--gameDir") |
| 79 | .arg(paths::game_dir(config)) | 119 | .arg(paths::game_directory(config)) |
| 80 | .arg("--assetsDir") | 120 | .arg("--assetsDir") |
| 81 | .arg(paths::assets_dir(config)) | 121 | .arg(paths::assets_directory(config)) |
| 82 | .arg("--assetIndex") | 122 | .arg("--assetIndex") |
| 83 | .arg(&asset_index_id) | 123 | .arg(asset_index_id) |
| 84 | .arg("--uuid") | 124 | .arg("--uuid") |
| 85 | .arg(&config.uuid) | 125 | .arg(&config.uuid) |
| 86 | .arg("--userProperties") | 126 | .arg("--userProperties") |
| @@ -90,32 +130,128 @@ pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | |||
| 90 | .arg("--userType") | 130 | .arg("--userType") |
| 91 | .arg("legacy"); | 131 | .arg("legacy"); |
| 92 | 132 | ||
| 93 | let status = cmd.status()?; | 133 | cmd |
| 134 | } | ||
| 135 | |||
| 136 | pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { | ||
| 137 | let required: u8 = required_java_major( | ||
| 138 | version | ||
| 139 | .java_version | ||
| 140 | .as_ref() | ||
| 141 | .map(|j| j.major_version), | ||
| 142 | ); | ||
| 143 | |||
| 144 | let java_path: PathBuf = resolve_runtime(required, &config.runtimes)?; | ||
| 145 | |||
| 146 | let classpath: String = build_classpath(config, version); | ||
| 147 | let natives_dir: PathBuf = natives_dir(config, &version.id); | ||
| 148 | |||
| 149 | if !natives_dir.exists() { | ||
| 150 | return Err(McError::Runtime(format!( | ||
| 151 | "Natives folder does not exist: {}", | ||
| 152 | natives_dir.display() | ||
| 153 | ))); | ||
| 154 | } | ||
| 155 | |||
| 156 | let asset_index_id = version | ||
| 157 | .asset_index | ||
| 158 | .as_ref() | ||
| 159 | .ok_or_else(|| { | ||
| 160 | McError::Runtime("Missing assetIndex in version.json".into()) | ||
| 161 | })? | ||
| 162 | .id | ||
| 163 | .clone(); | ||
| 164 | |||
| 165 | let mut cmd: Command = build_command( | ||
| 166 | java_path, | ||
| 167 | config, | ||
| 168 | version, | ||
| 169 | &natives_dir, | ||
| 170 | classpath, | ||
| 171 | asset_index_id, | ||
| 172 | ); | ||
| 173 | |||
| 174 | let status: ExitStatus = cmd.status()?; | ||
| 94 | 175 | ||
| 95 | if !status.success() { | 176 | if !status.success() { |
| 96 | return Err(McError::Process("Minecraft exited with error".into())); | 177 | return Err(Process("Minecraft exited with error".into())); |
| 97 | } | 178 | } |
| 98 | 179 | ||
| 99 | Ok(()) | 180 | Ok(()) |
| 100 | } | 181 | } |
| 101 | 182 | ||
| 102 | fn library_allowed(lib: &Library) -> bool { | 183 | #[cfg(test)] |
| 103 | let rules = match &lib.rules { | 184 | mod tests { |
| 104 | | Some(r) => r, | 185 | use super::*; |
| 105 | | None => return true, | 186 | use std::path::PathBuf; |
| 106 | }; | 187 | |
| 188 | #[test] | ||
| 189 | fn required_java_major_defaults_to_8() { | ||
| 190 | assert_eq!(required_java_major(None), 8); | ||
| 191 | } | ||
| 192 | |||
| 193 | #[test] | ||
| 194 | fn required_java_major_uses_given_value() { | ||
| 195 | assert_eq!(required_java_major(Some(17)), 17); | ||
| 196 | } | ||
| 197 | |||
| 198 | #[test] | ||
| 199 | fn resolve_runtime_errors_when_no_runtimes() { | ||
| 200 | let runtimes: Vec<JavaRuntime> = vec![]; | ||
| 201 | assert!(resolve_runtime(17, &runtimes).is_err()); | ||
| 202 | } | ||
| 203 | |||
| 204 | #[test] | ||
| 205 | fn resolve_runtime_picks_lowest_matching_major() { | ||
| 206 | let runtimes: Vec<JavaRuntime> = vec![ | ||
| 207 | JavaRuntime { | ||
| 208 | major: 21, | ||
| 209 | path: PathBuf::from("/bin/true"), | ||
| 210 | }, | ||
| 211 | JavaRuntime { | ||
| 212 | major: 17, | ||
| 213 | path: PathBuf::from("/bin/true"), | ||
| 214 | }, | ||
| 215 | ]; | ||
| 216 | |||
| 217 | let result: PathBuf = resolve_runtime(8, &runtimes).unwrap(); | ||
| 218 | assert_eq!(result, PathBuf::from("/bin/true")); | ||
| 219 | } | ||
| 107 | 220 | ||
| 108 | let mut allowed = false; | 221 | #[test] |
| 222 | fn resolve_runtime_respects_required_version() { | ||
| 223 | let runtimes: Vec<JavaRuntime> = vec![ | ||
| 224 | JavaRuntime { | ||
| 225 | major: 8, | ||
| 226 | path: PathBuf::from("/bin/true"), | ||
| 227 | }, | ||
| 228 | JavaRuntime { | ||
| 229 | major: 17, | ||
| 230 | path: PathBuf::from("/bin/true"), | ||
| 231 | }, | ||
| 232 | ]; | ||
| 233 | |||
| 234 | let result: PathBuf = resolve_runtime(17, &runtimes).unwrap(); | ||
| 235 | assert_eq!(result, PathBuf::from("/bin/true")); | ||
| 236 | } | ||
| 109 | 237 | ||
| 110 | for rule in rules { | 238 | #[test] |
| 111 | let os_match = match &rule.os { | 239 | fn validate_succeeds_for_true_binary() { |
| 112 | | Some(os) => os.name == "linux", | 240 | let runtime = JavaRuntime { |
| 113 | | None => true, | 241 | major: 17, |
| 242 | path: PathBuf::from("/bin/true"), | ||
| 114 | }; | 243 | }; |
| 115 | if os_match { | 244 | |
| 116 | allowed = rule.action == "allow"; | 245 | assert!(runtime.validate().is_ok()); |
| 117 | } | ||
| 118 | } | 246 | } |
| 119 | 247 | ||
| 120 | allowed | 248 | #[test] |
| 249 | fn validate_fails_for_false_binary() { | ||
| 250 | let runtime = JavaRuntime { | ||
| 251 | major: 17, | ||
| 252 | path: PathBuf::from("/bin/false"), | ||
| 253 | }; | ||
| 254 | |||
| 255 | assert!(runtime.validate().is_err()); | ||
| 256 | } | ||
| 121 | } | 257 | } |
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 @@ | |||
| 1 | #![allow(dead_code)] | 1 | #![allow(dead_code)] |
| 2 | 2 | ||
| 3 | use std::collections::HashMap; | 3 | use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; |
| 4 | 4 | use reqwest::get; | |
| 5 | use reqwest; | ||
| 6 | use serde::Deserialize; | 5 | use serde::Deserialize; |
| 6 | use serde_json::{from_str, Value}; | ||
| 7 | use std::collections::HashMap; | ||
| 8 | use McError::Config; | ||
| 7 | 9 | ||
| 8 | use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; | 10 | #[derive(Debug, Deserialize)] |
| 11 | #[serde(rename_all = "camelCase")] | ||
| 12 | pub struct JavaVersionInfo { | ||
| 13 | pub component: String, | ||
| 14 | pub major_version: u8, | ||
| 15 | } | ||
| 9 | 16 | ||
| 10 | #[derive(Debug, Deserialize)] | 17 | #[derive(Debug, Deserialize)] |
| 11 | pub struct Version { | 18 | pub struct Version { |
| @@ -19,6 +26,10 @@ pub struct Version { | |||
| 19 | 26 | ||
| 20 | #[serde(rename = "assetIndex")] | 27 | #[serde(rename = "assetIndex")] |
| 21 | pub asset_index: Option<AssetIndex>, | 28 | pub asset_index: Option<AssetIndex>, |
| 29 | |||
| 30 | #[serde(default)] | ||
| 31 | #[serde(rename = "javaVersion")] | ||
| 32 | pub java_version: Option<JavaVersionInfo>, | ||
| 22 | } | 33 | } |
| 23 | 34 | ||
| 24 | #[derive(Debug, Deserialize)] | 35 | #[derive(Debug, Deserialize)] |
| @@ -83,15 +94,13 @@ pub struct OsRule { | |||
| 83 | pub async fn load_version( | 94 | pub async fn load_version( |
| 84 | cfg: &crate::config::Config, | 95 | cfg: &crate::config::Config, |
| 85 | ) -> Result<Version, McError> { | 96 | ) -> Result<Version, McError> { |
| 86 | let manifest_text = reqwest::get(VERSION_MANIFEST_URL) | 97 | let manifest_text = get(VERSION_MANIFEST_URL).await?.text().await?; |
| 87 | .await? | 98 | let root: Value = from_str(&manifest_text)?; |
| 88 | .text() | 99 | |
| 89 | .await?; | ||
| 90 | let root: serde_json::Value = serde_json::from_str(&manifest_text)?; | ||
| 91 | let version_id = if cfg.version == "latest" { | 100 | let version_id = if cfg.version == "latest" { |
| 92 | root["latest"]["release"] | 101 | root["latest"]["release"] |
| 93 | .as_str() | 102 | .as_str() |
| 94 | .ok_or_else(|| McError::Config("missing latest.release".into()))? | 103 | .ok_or_else(|| Config("missing latest.release".into()))? |
| 95 | .to_string() | 104 | .to_string() |
| 96 | } else { | 105 | } else { |
| 97 | cfg.version.clone() | 106 | cfg.version.clone() |
| @@ -99,21 +108,19 @@ pub async fn load_version( | |||
| 99 | 108 | ||
| 100 | let versions = root["versions"] | 109 | let versions = root["versions"] |
| 101 | .as_array() | 110 | .as_array() |
| 102 | .ok_or_else(|| McError::Config("missing versions array".into()))?; | 111 | .ok_or_else(|| Config("missing versions array".into()))?; |
| 103 | 112 | ||
| 104 | let version_entry = versions | 113 | let version_entry = versions |
| 105 | .iter() | 114 | .iter() |
| 106 | .find(|v| v["id"].as_str() == Some(&version_id)) | 115 | .find(|v| v["id"].as_str() == Some(&version_id)) |
| 107 | .ok_or_else(|| { | 116 | .ok_or_else(|| Config(format!("version '{}' not found", version_id)))?; |
| 108 | McError::Config(format!("version '{}' not found", version_id)) | ||
| 109 | })?; | ||
| 110 | 117 | ||
| 111 | let url = version_entry["url"] | 118 | let url = version_entry["url"] |
| 112 | .as_str() | 119 | .as_str() |
| 113 | .ok_or_else(|| McError::Config("missing version url".into()))?; | 120 | .ok_or_else(|| Config("missing version url".into()))?; |
| 114 | 121 | ||
| 115 | let version_text = reqwest::get(url).await?.text().await?; | 122 | let version_text = get(url).await?.text().await?; |
| 116 | let version: Version = serde_json::from_str(&version_text)?; | 123 | let version: Version = from_str(&version_text)?; |
| 117 | 124 | ||
| 118 | Ok(version) | 125 | Ok(version) |
| 119 | } | 126 | } |
diff --git a/src/platform/paths.rs b/src/platform/paths.rs index b430f09..07313fd 100644 --- a/src/platform/paths.rs +++ b/src/platform/paths.rs | |||
| @@ -1,44 +1,268 @@ | |||
| 1 | use std::{fs::create_dir_all, path::PathBuf}; | 1 | use std::{fs::create_dir_all, path::PathBuf}; |
| 2 | 2 | ||
| 3 | use crate::{config::Config, errors::McError}; | 3 | use crate::{config::Config, constants::directory, errors::McError}; |
| 4 | 4 | ||
| 5 | /// ~/.local/share/dml/minecraft | 5 | /// Returns the path to the root Minecraft directory inside the launcher data folder. |
| 6 | pub fn minecraft_root(cfg: &Config) -> PathBuf { | 6 | /// |
| 7 | /// This directory acts as the root for all Minecraft-related files within the launcher data. | ||
| 8 | /// By default, it joins the `minecraft` directory under the data folder (defined by the launcher). | ||
| 9 | /// | ||
| 10 | /// The returned path does not include any specific assets, versions, or libraries directories. | ||
| 11 | /// You can use this as the base path to construct other Minecraft-related paths. | ||
| 12 | /// | ||
| 13 | /// # Parameters | ||
| 14 | /// - `cfg`: The configuration object (`Config`) containing launcher and Minecraft data folder paths. | ||
| 15 | /// | ||
| 16 | /// # Returns | ||
| 17 | /// - A `PathBuf` pointing to the root Minecraft directory. | ||
| 18 | pub fn root_directory(cfg: &Config) -> PathBuf { | ||
| 19 | // Remove DEFAULT_LAUNCHER_DIR to avoid duplicate "dml/dml" | ||
| 7 | cfg.data_dir.join("minecraft") | 20 | cfg.data_dir.join("minecraft") |
| 8 | } | 21 | } |
| 9 | 22 | ||
| 10 | pub fn assets_dir(cfg: &Config) -> PathBuf { | 23 | /// Returns the path to the Minecraft assets directory. |
| 11 | minecraft_root(cfg).join("assets") | 24 | /// |
| 25 | /// This is where Minecraft assets such as textures, sounds, and other resources are stored. | ||
| 26 | /// The assets directory is located inside the root Minecraft directory and can be used | ||
| 27 | /// for accessing game assets. | ||
| 28 | /// | ||
| 29 | /// # Parameters | ||
| 30 | /// - `cfg`: The configuration object (`Config`) that contains paths for the launcher and Minecraft data. | ||
| 31 | /// | ||
| 32 | /// # Returns | ||
| 33 | /// - A `PathBuf` pointing to the assets directory inside the root Minecraft directory. | ||
| 34 | pub fn assets_directory(cfg: &Config) -> PathBuf { | ||
| 35 | root_directory(cfg).join(directory::ASSETS) | ||
| 12 | } | 36 | } |
| 13 | 37 | ||
| 14 | pub fn game_dir(cfg: &Config) -> PathBuf { minecraft_root(cfg) } | 38 | /// Returns the path to the game directory (same as root directory for now). |
| 15 | pub fn ensure_dirs(cfg: &Config) -> Result<(), McError> { | 39 | /// |
| 16 | let root = minecraft_root(cfg); | 40 | /// The game directory currently points to the same location as the root directory. |
| 17 | create_dir_all(&root)?; | 41 | /// In future iterations, this could be adjusted if game-specific files need to be handled separately. |
| 18 | create_dir_all(root.join("versions"))?; | 42 | /// |
| 19 | create_dir_all(root.join("libraries"))?; | 43 | /// # Parameters |
| 20 | create_dir_all(assets_dir(cfg))?; | 44 | /// - `cfg`: The configuration object (`Config`) that holds the paths to the launcher and Minecraft data. |
| 21 | create_dir_all(assets_dir(cfg).join("indexes"))?; | 45 | /// |
| 22 | create_dir_all(assets_dir(cfg).join("objects"))?; | 46 | /// # Returns |
| 23 | create_dir_all(root.join("saves"))?; | 47 | /// - A `PathBuf` pointing to the game directory, which is the same as the root Minecraft directory for now. |
| 48 | pub fn game_directory(cfg: &Config) -> PathBuf { | ||
| 49 | root_directory(cfg) | ||
| 50 | } | ||
| 51 | |||
| 52 | /// Ensures that all necessary directories for Minecraft are created. | ||
| 53 | /// | ||
| 54 | /// This function checks if essential directories for Minecraft's file structure exist and creates them | ||
| 55 | /// if they do not. This includes directories for versions, libraries, assets, and saves. | ||
| 56 | /// | ||
| 57 | /// # Parameters | ||
| 58 | /// - `cfg`: The configuration object (`Config`) that contains paths for the Minecraft data and the required directories. | ||
| 59 | /// | ||
| 60 | /// # Returns | ||
| 61 | /// - `Ok(())` if all directories are created successfully. | ||
| 62 | /// - `Err(McError)` if an error occurs while creating any directory (such as permission issues). | ||
| 63 | /// | ||
| 64 | /// # Example | ||
| 65 | /// ```rust | ||
| 66 | /// let cfg = Config { /* configuration values */ }; | ||
| 67 | /// ensure_directories(&cfg)?; | ||
| 68 | /// ``` | ||
| 69 | /// | ||
| 70 | /// # Notes | ||
| 71 | /// The function creates several directories under the root Minecraft directory, including: | ||
| 72 | /// - Root directory | ||
| 73 | /// - Versions | ||
| 74 | /// - Libraries | ||
| 75 | /// - Assets (with subdirectories for `indexes` and `objects`) | ||
| 76 | /// - Saves | ||
| 77 | /// Each of these directories is essential for managing Minecraft game data and resources. | ||
| 78 | pub fn ensure_directories(cfg: &Config) -> Result<(), McError> { | ||
| 79 | let root: PathBuf = root_directory(cfg); | ||
| 80 | |||
| 81 | for dir in [ | ||
| 82 | root.clone(), | ||
| 83 | root.join(directory::VERSIONS), | ||
| 84 | root.join(directory::LIBRARIES), | ||
| 85 | assets_directory(cfg), | ||
| 86 | assets_directory(cfg).join(directory::INDEXES), | ||
| 87 | assets_directory(cfg).join(directory::OBJECTS), | ||
| 88 | root.join(directory::SAVES), | ||
| 89 | ] { | ||
| 90 | create_dir_all(dir)?; | ||
| 91 | } | ||
| 24 | 92 | ||
| 25 | Ok(()) | 93 | Ok(()) |
| 26 | } | 94 | } |
| 27 | 95 | ||
| 96 | /// Returns the path to a specific version directory inside the root Minecraft directory. | ||
| 97 | /// | ||
| 98 | /// This directory is where the game version-specific files (such as the `.jar` file) are stored. | ||
| 99 | /// The path is constructed by joining the `versions` directory with the provided version name. | ||
| 100 | /// | ||
| 101 | /// # Parameters | ||
| 102 | /// - `cfg`: The configuration object (`Config`) containing paths for the Minecraft data. | ||
| 103 | /// - `version`: The version of Minecraft (e.g., "1.18.2") to generate the path for. | ||
| 104 | /// | ||
| 105 | /// # Returns | ||
| 106 | /// - A `PathBuf` pointing to the directory for the specified version inside the root Minecraft directory. | ||
| 28 | pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { | 107 | pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { |
| 29 | minecraft_root(cfg).join("versions").join(version) | 108 | root_directory(cfg) |
| 109 | .join(directory::VERSIONS) | ||
| 110 | .join(version) | ||
| 30 | } | 111 | } |
| 31 | 112 | ||
| 32 | pub fn client_jar(cfg: &Config, version: &str) -> Result<PathBuf, McError> { | 113 | /// Returns the path to the client `.jar` file for a specific Minecraft version. |
| 33 | Ok(version_dir(cfg, version).join(format!("{version}.jar"))) | 114 | /// |
| 115 | /// This file is typically used to run the Minecraft client for a particular version. | ||
| 116 | /// The path is constructed by joining the version directory with the name of the `.jar` file. | ||
| 117 | /// | ||
| 118 | /// # Parameters | ||
| 119 | /// - `cfg`: The configuration object (`Config`) containing paths for the Minecraft data. | ||
| 120 | /// - `version`: The version of Minecraft (e.g., "1.18.2") to generate the path for. | ||
| 121 | /// | ||
| 122 | /// # Returns | ||
| 123 | /// - A `PathBuf` pointing to the `.jar` file for the specified version in the version directory. | ||
| 124 | pub fn client_jar(cfg: &Config, version: &str) -> PathBuf { | ||
| 125 | version_dir(cfg, version).join(format!("{version}.jar")) | ||
| 34 | } | 126 | } |
| 35 | 127 | ||
| 36 | pub fn library_file(cfg: &Config, rel_path: &str) -> Result<PathBuf, McError> { | 128 | /// Returns the path to a specific library file inside the library directory. |
| 37 | Ok(minecraft_root(cfg) | 129 | /// |
| 38 | .join("libraries") | 130 | /// This function is used to access library files required for running Minecraft. The `rel_path` |
| 39 | .join(rel_path)) | 131 | /// argument specifies the relative path inside the `libraries` directory. It is useful for handling |
| 132 | /// dependencies or loading required libraries based on the game version. | ||
| 133 | /// | ||
| 134 | /// # Parameters | ||
| 135 | /// - `cfg`: The configuration object (`Config`) that contains paths for the Minecraft data. | ||
| 136 | /// - `rel_path`: The relative path of the library file inside the `libraries` directory. | ||
| 137 | /// | ||
| 138 | /// # Returns | ||
| 139 | /// - A `PathBuf` pointing to the specified library file inside the `libraries` directory. | ||
| 140 | pub fn library_file(cfg: &Config, rel_path: &str) -> PathBuf { | ||
| 141 | root_directory(cfg) | ||
| 142 | .join(directory::LIBRARIES) | ||
| 143 | .join(rel_path) | ||
| 40 | } | 144 | } |
| 41 | 145 | ||
| 146 | /// Returns the path to the natives directory for a specific Minecraft version. | ||
| 147 | /// | ||
| 148 | /// The natives directory contains platform-specific native libraries (e.g., `.dll`, `.so`, `.dylib`) | ||
| 149 | /// that Minecraft uses to run the game. This function returns the path to the directory for the | ||
| 150 | /// specified version. | ||
| 151 | /// | ||
| 152 | /// # Parameters | ||
| 153 | /// - `cfg`: The configuration object (`Config`) containing paths for the Minecraft data. | ||
| 154 | /// - `version`: The version of Minecraft (e.g., "1.18.2") to generate the path for. | ||
| 155 | /// | ||
| 156 | /// # Returns | ||
| 157 | /// - A `PathBuf` pointing to the natives directory for the specified version. | ||
| 42 | pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { | 158 | pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { |
| 43 | version_dir(cfg, version).join("natives") | 159 | version_dir(cfg, version).join(directory::NATIVES) |
| 160 | } | ||
| 161 | |||
| 162 | // /// Path to a version’s saves directory. | ||
| 163 | // pub fn saves_dir(cfg: &Config) -> PathBuf { | ||
| 164 | // minecraft_root(cfg).join(dirs::SAVES) | ||
| 165 | // } | ||
| 166 | |||
| 167 | #[cfg(test)] | ||
| 168 | mod tests { | ||
| 169 | use super::*; | ||
| 170 | use std::path::Path; | ||
| 171 | |||
| 172 | fn test_config(tmp: &Path) -> Config { | ||
| 173 | Config { | ||
| 174 | data_dir: tmp.to_path_buf(), | ||
| 175 | ..Default::default() | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | #[test] | ||
| 180 | fn root_directory_is_correct() { | ||
| 181 | let tmp = tempfile::tempdir().unwrap(); | ||
| 182 | let cfg = test_config(tmp.path()); | ||
| 183 | |||
| 184 | let root = root_directory(&cfg); | ||
| 185 | assert_eq!(root, tmp.path().join("minecraft")); | ||
| 186 | } | ||
| 187 | |||
| 188 | #[test] | ||
| 189 | fn assets_directory_is_correct() { | ||
| 190 | let tmp = tempfile::tempdir().unwrap(); | ||
| 191 | let cfg = test_config(tmp.path()); | ||
| 192 | |||
| 193 | let assets = assets_directory(&cfg); | ||
| 194 | assert_eq!(assets, root_directory(&cfg).join(directory::ASSETS)); | ||
| 195 | } | ||
| 196 | |||
| 197 | #[test] | ||
| 198 | fn version_paths_are_correct() { | ||
| 199 | let tmp = tempfile::tempdir().unwrap(); | ||
| 200 | let cfg = test_config(tmp.path()); | ||
| 201 | |||
| 202 | let version = "1.20.1"; | ||
| 203 | assert_eq!( | ||
| 204 | version_dir(&cfg, version), | ||
| 205 | root_directory(&cfg) | ||
| 206 | .join(directory::VERSIONS) | ||
| 207 | .join(version) | ||
| 208 | ); | ||
| 209 | |||
| 210 | assert_eq!( | ||
| 211 | client_jar(&cfg, version), | ||
| 212 | version_dir(&cfg, version).join("1.20.1.jar") | ||
| 213 | ); | ||
| 214 | } | ||
| 215 | |||
| 216 | #[test] | ||
| 217 | fn library_file_path_is_correct() { | ||
| 218 | let tmp = tempfile::tempdir().unwrap(); | ||
| 219 | let cfg = test_config(tmp.path()); | ||
| 220 | |||
| 221 | let rel = "com/example/lib.jar"; | ||
| 222 | assert_eq!( | ||
| 223 | library_file(&cfg, rel), | ||
| 224 | root_directory(&cfg) | ||
| 225 | .join(directory::LIBRARIES) | ||
| 226 | .join(rel) | ||
| 227 | ); | ||
| 228 | } | ||
| 229 | |||
| 230 | #[test] | ||
| 231 | fn natives_dir_path_is_correct() { | ||
| 232 | let tmp = tempfile::tempdir().unwrap(); | ||
| 233 | let cfg = test_config(tmp.path()); | ||
| 234 | |||
| 235 | let version = "1.20.1"; | ||
| 236 | assert_eq!( | ||
| 237 | natives_dir(&cfg, version), | ||
| 238 | version_dir(&cfg, version).join(directory::NATIVES) | ||
| 239 | ); | ||
| 240 | } | ||
| 241 | |||
| 242 | #[test] | ||
| 243 | fn ensure_directories_creates_structure() { | ||
| 244 | let tmp = tempfile::tempdir().unwrap(); | ||
| 245 | let cfg = test_config(tmp.path()); | ||
| 246 | |||
| 247 | ensure_directories(&cfg).unwrap(); | ||
| 248 | |||
| 249 | let root = root_directory(&cfg); | ||
| 250 | |||
| 251 | let expected_dirs = [ | ||
| 252 | root.clone(), | ||
| 253 | root.join(directory::VERSIONS), | ||
| 254 | root.join(directory::LIBRARIES), | ||
| 255 | root.join(directory::ASSETS), | ||
| 256 | root.join(directory::ASSETS) | ||
| 257 | .join(directory::INDEXES), | ||
| 258 | root.join(directory::ASSETS) | ||
| 259 | .join(directory::OBJECTS), | ||
| 260 | root.join(directory::SAVES), | ||
| 261 | ]; | ||
| 262 | |||
| 263 | for dir in expected_dirs { | ||
| 264 | assert!(dir.exists(), "Missing directory: {:?}", dir); | ||
| 265 | assert!(dir.is_dir()); | ||
| 266 | } | ||
| 267 | } | ||
| 44 | } | 268 | } |
diff --git a/src/util/fs.rs b/src/util/fs.rs index 8ecd0d0..b1e9152 100644 --- a/src/util/fs.rs +++ b/src/util/fs.rs | |||
| @@ -4,7 +4,10 @@ use std::path::Path; | |||
| 4 | 4 | ||
| 5 | use tokio::fs::remove_file; | 5 | use tokio::fs::remove_file; |
| 6 | 6 | ||
| 7 | use crate::errors::McError; | 7 | use crate::{ |
| 8 | errors::McError, | ||
| 9 | minecraft::manifests::{Library, Rule}, | ||
| 10 | }; | ||
| 8 | 11 | ||
| 9 | pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { | 12 | pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { |
| 10 | if path.exists() { | 13 | if path.exists() { |
| @@ -12,3 +15,24 @@ pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { | |||
| 12 | } | 15 | } |
| 13 | Ok(()) | 16 | Ok(()) |
| 14 | } | 17 | } |
| 18 | |||
| 19 | pub fn library_allowed(lib: &Library) -> bool { | ||
| 20 | let rules: &Vec<Rule> = match &lib.rules { | ||
| 21 | | Some(r) => r, | ||
| 22 | | None => return true, | ||
| 23 | }; | ||
| 24 | |||
| 25 | let mut allowed: bool = false; | ||
| 26 | |||
| 27 | for rule in rules { | ||
| 28 | let os_match: bool = match &rule.os { | ||
| 29 | | Some(os) => os.name == "linux", | ||
| 30 | | None => true, | ||
| 31 | }; | ||
| 32 | if os_match { | ||
| 33 | allowed = rule.action == "allow"; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | allowed | ||
| 38 | } | ||
diff --git a/src/util/sha1.rs b/src/util/sha1.rs index 6684963..6fed18d 100644 --- a/src/util/sha1.rs +++ b/src/util/sha1.rs | |||
| @@ -2,14 +2,15 @@ | |||
| 2 | 2 | ||
| 3 | use std::path::Path; | 3 | use std::path::Path; |
| 4 | 4 | ||
| 5 | use sha1::{Digest, Sha1}; | 5 | use sha1::{Digest, Sha1, Sha1Core}; |
| 6 | use sha1::digest::core_api::CoreWrapper; | ||
| 6 | use tokio::fs::read; | 7 | use tokio::fs::read; |
| 7 | 8 | ||
| 8 | use crate::errors::McError; | 9 | use crate::errors::McError; |
| 9 | 10 | ||
| 10 | pub async fn sha1_hex(path: &Path) -> Result<String, McError> { | 11 | pub async fn sha1_hex(path: &Path) -> Result<String, McError> { |
| 11 | let data = read(path).await?; | 12 | let data: Vec<u8> = read(path).await?; |
| 12 | let mut hasher = Sha1::new(); | 13 | let mut hasher: CoreWrapper<Sha1Core> = Sha1::new(); |
| 13 | hasher.update(&data); | 14 | hasher.update(&data); |
| 14 | Ok(format!("{:x}", hasher.finalize())) | 15 | Ok(format!("{:x}", hasher.finalize())) |
| 15 | } | 16 | } |
