aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/config/file.rs26
-rw-r--r--src/config/loader.rs129
-rw-r--r--src/config/mod.rs17
-rw-r--r--src/config/runtime.rs95
-rw-r--r--src/constants.rs110
-rw-r--r--src/errors.rs320
-rw-r--r--src/main.rs105
-rw-r--r--src/minecraft/downloads.rs423
-rw-r--r--src/minecraft/extraction.rs70
-rw-r--r--src/minecraft/launcher.rs262
-rw-r--r--src/minecraft/manifests.rs41
-rw-r--r--src/platform/paths.rs270
-rw-r--r--src/util/fs.rs26
-rw-r--r--src/util/sha1.rs7
14 files changed, 1533 insertions, 368 deletions
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 @@
1use std::path::PathBuf;
2
3use serde::Deserialize;
4
5use crate::minecraft::launcher::JavaRuntime;
6
7#[derive(Debug, Deserialize)]
8pub 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 @@
1use std::{env::var, fs::read_to_string, path::PathBuf}; 1use std::{env, fs::read_to_string, path::PathBuf};
2
3use directories::ProjectDirs; 2use directories::ProjectDirs;
4use serde::Deserialize; 3use uuid::Uuid;
5 4
5use super::{file::FileConfig, runtime::RuntimeConfig};
6use crate::{constants::*, errors::McError}; 6use crate::{constants::*, errors::McError};
7#[allow(dead_code)] 7
8#[derive(Debug, Deserialize)] 8pub struct ConfigLoader;
9pub struct Config { 9
10 pub username: String, 10impl 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}
21impl 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}
61fn 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
3pub mod file;
11pub mod loader; 4pub mod loader;
12pub use loader::Config; 5pub mod runtime;
6
7pub use loader::ConfigLoader;
8pub use runtime::RuntimeConfig;
9
10/// Backwards-compatibility alias so existing code can keep using `Config`
11pub 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 @@
1use std::path::PathBuf;
2
3use super::file::FileConfig;
4use 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)]
18pub 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
46impl 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
3use std::time::Duration; 13use 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.
5pub const VERSION_MANIFEST_URL: &str = 20pub 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.
8pub const DOWNLOAD_RETRIES: usize = 3; 28pub 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.
9pub const DOWNLOAD_BACKOFF: Duration = Duration::from_millis(400); 34pub 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.
11pub const DEFAULT_MAX_MEMORY_MB: u32 = 4048; 41pub 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.
12pub const DEFAULT_JAVA_PATH: &str = "java"; 48pub 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.
13pub const DEFAULT_VERSION: &str = "latest"; 54pub 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.
60pub 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.
66pub 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.
72pub 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.
78pub const DEFAULT_LAUNCHER_DIR: &str = "dml";
79
80/// Default environment variable names for overriding configuration.
81pub const ENV_USERNAME: &str = "MC_USERNAME";
82pub const ENV_VERSION: &str = "MC_VERSION";
83pub const ENV_JAVA_PATH: &str = "MC_JAVA_PATH";
84pub const ENV_MAX_MEMORY_MB: &str = "MC_MAX_MEMORY_MB";
85
86/// Default project/platform constants for `directories::ProjectDirs`.
87pub const DEFAULT_COMPANY: &str = "com";
88pub const DEFAULT_PROJECT_GROUP: &str = "example";
89pub const DEFAULT_PROJECT_NAME: &str = "dml";
90
91/// Default error messages for common failures.
92pub const DEFAULT_ERR_PLATFORM_DIR: &str = "cannot determine config dir";
93pub const DEFAULT_ERR_CREATE_DIR: &str = "failed to create directory";
94
95/// Default configuration filename within the launcher directory.
96pub const DEFAULT_CONFIG_FILENAME: &str = "config.toml";
97
98/// Default username if none is provided by environment or config.
99pub const DEFAULT_USERNAME: &str = "phil";
100
101/// Default subdirectory names within the launcher directory.
102pub 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 @@
1use std::{fmt, io}; 1use std::{fmt, io};
2 2
3use fmt::{Display, Formatter, Result}; 3use fmt::{Display, Formatter, Result};
4use io::Error;
4use zip::result::ZipError; 5use 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)]
13pub enum McError { 51pub 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.
24impl Display for McError { 149impl 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
31impl 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.
163impl 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.
35impl From<reqwest::Error> for McError { 178impl 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.
39impl From<serde_json::Error> for McError { 192impl 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.
43impl From<ZipError> for McError { 206impl 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)]
219mod 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 @@
1mod constants; 1//! Main module for the DML Launcher.
2mod 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
4mod config; 34mod config;
35mod constants;
36mod errors;
5mod minecraft; 37mod minecraft;
6mod platform; 38mod platform;
7mod util; 39mod util;
8 40
9use clap::Parser; 41use clap::Parser;
10use config::Config;
11use dotenvy::dotenv; 42use dotenvy::dotenv;
12use errors::McError; 43use errors::McError;
13use log::{debug, info}; 44use log::LevelFilter::Warn;
45use reqwest::Client;
46use platform::paths::ensure_directories;
14 47
15use crate::minecraft::{ 48use 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};
55use crate::config::RuntimeConfig;
56use 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)]
22struct Cli { 65struct 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]
34async fn main() -> Result<(), McError> { 101async 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 @@
1use log::{debug, info}; 1use std::{
2use reqwest::get; 2 collections::HashMap,
3 io::{Write, stdout},
4 path::{Path, PathBuf},
5 sync::{
6 Arc,
7 atomic::{AtomicUsize, Ordering},
8 },
9};
10
11use McError::Config;
12use Ordering::SeqCst;
13use futures::stream::{FuturesUnordered, StreamExt};
14use reqwest::Response;
3use serde::Deserialize; 15use serde::Deserialize;
4use tokio::{ 16use 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
9use crate::{ 24use 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
31const MAX_CONCURRENT_DOWNLOADS: usize = 100;
32const PROGRESS_INITIAL_VALUE: usize = 0;
33const ASSET_URL_BASE: &str = "https://resources.download.minecraft.net/";
34const MAX_PROGRESS_PERCENT: f64 = 100.0;
35const PROGRESS_PRINT_WIDTH: usize = 3;
36const ATOMIC_ORDERING: Ordering = SeqCst;
37const DOWNLOAD_COMPLETE_MESSAGE: &str = "All downloads completed successfully!";
38const ASSET_INDEX_MISSING_ERROR: &str = "Missing asset_index for the version";
39
40impl 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)]
49struct DownloadJob {
50 url: String,
51 destination_path: PathBuf,
52}
53
54impl 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)]
17struct AssetObject { 60struct 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)]
23struct AssetIndexManifest { 66struct AssetIndexManifest {
24 objects: std::collections::HashMap<String, AssetObject>, 67 objects: HashMap<String, AssetObject>,
25} 68}
26 69
27pub async fn download_all( 70/// Download all files required to run a specific Minecraft version.
28 config: &Config, 71pub 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
37async fn download_client( 98/// Ensure the essential assets directories exist.
38 config: &Config, 99async 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
52async fn download_libraries( 108/// Load the asset index manifest for the given Minecraft version.
53 config: &Config, 109async 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
83async fn download_asset_index( 132fn 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 147fn 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() { 158fn 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}
168fn 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); 184fn 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)?; 203fn 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
120async fn download_assets( 220async 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() { 242fn 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]; 271async 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
280fn 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.
304fn 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
322async 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
166async fn download_file( 338async 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 @@
1use std::{fs, io, path::Path}; 1use std::{
2 collections::HashMap,
3 fs,
4 fs::File,
5 io,
6 path::{Path, PathBuf},
7};
2 8
3use log::info; 9use zip::{read::ZipFile, ZipArchive};
4use zip::ZipArchive;
5 10
6use crate::{ 11use 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
11pub fn extract_natives( 17pub 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
68fn 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
90fn extract_zip(jar_path: &Path, out_dir: &Path) -> Result<(), McError> { 70fn 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 @@
1use std::process::Command; 1use paths::{client_jar, library_file, natives_dir};
2 2use serde::Deserialize;
3use log::{debug, info}; 3use std::path::Path;
4use std::process::Output;
5use std::{
6 path::PathBuf,
7 process::{Command, ExitStatus},
8};
9use McError::Process;
4 10
5use crate::{ 11use 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
12fn build_classpath( 16#[derive(Debug, Clone, Deserialize)]
13 config: &Config, 17pub 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
22impl 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
46fn required_java_major(java_major: Option<u8>) -> u8 {
47 java_major.unwrap_or(8)
48}
49
50fn 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
72fn 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
35pub fn launch(config: &Config, version: &Version) -> Result<(), McError> { 93fn 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
136pub 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
102fn library_allowed(lib: &Library) -> bool { 183#[cfg(test)]
103 let rules = match &lib.rules { 184mod 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
3use std::collections::HashMap; 3use crate::{constants::VERSION_MANIFEST_URL, errors::McError};
4 4use reqwest::get;
5use reqwest;
6use serde::Deserialize; 5use serde::Deserialize;
6use serde_json::{from_str, Value};
7use std::collections::HashMap;
8use McError::Config;
7 9
8use crate::{constants::VERSION_MANIFEST_URL, errors::McError}; 10#[derive(Debug, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct JavaVersionInfo {
13 pub component: String,
14 pub major_version: u8,
15}
9 16
10#[derive(Debug, Deserialize)] 17#[derive(Debug, Deserialize)]
11pub struct Version { 18pub 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 {
83pub async fn load_version( 94pub 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 @@
1use std::{fs::create_dir_all, path::PathBuf}; 1use std::{fs::create_dir_all, path::PathBuf};
2 2
3use crate::{config::Config, errors::McError}; 3use 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.
6pub 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.
18pub 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
10pub 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.
34pub fn assets_directory(cfg: &Config) -> PathBuf {
35 root_directory(cfg).join(directory::ASSETS)
12} 36}
13 37
14pub fn game_dir(cfg: &Config) -> PathBuf { minecraft_root(cfg) } 38/// Returns the path to the game directory (same as root directory for now).
15pub 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.
48pub 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.
78pub 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.
28pub fn version_dir(cfg: &Config, version: &str) -> PathBuf { 107pub 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
32pub 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.
124pub fn client_jar(cfg: &Config, version: &str) -> PathBuf {
125 version_dir(cfg, version).join(format!("{version}.jar"))
34} 126}
35 127
36pub 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.
140pub 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.
42pub fn natives_dir(cfg: &Config, version: &str) -> PathBuf { 158pub 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)]
168mod 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
5use tokio::fs::remove_file; 5use tokio::fs::remove_file;
6 6
7use crate::errors::McError; 7use crate::{
8 errors::McError,
9 minecraft::manifests::{Library, Rule},
10};
8 11
9pub async fn remove_if_exists(path: &Path) -> Result<(), McError> { 12pub 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
19pub 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
3use std::path::Path; 3use std::path::Path;
4 4
5use sha1::{Digest, Sha1}; 5use sha1::{Digest, Sha1, Sha1Core};
6use sha1::digest::core_api::CoreWrapper;
6use tokio::fs::read; 7use tokio::fs::read;
7 8
8use crate::errors::McError; 9use crate::errors::McError;
9 10
10pub async fn sha1_hex(path: &Path) -> Result<String, McError> { 11pub 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}