uranium_rs/version_checker.rs
1use std::path::{Path, PathBuf};
2
3use log::{error, info};
4use mine_data_structs::minecraft::{
5 AssetIndex, DownloadData, Library, ObjectData, Os, Resources, Root,
6};
7
8use crate::downloaders::list_instances;
9use crate::error::{Result, UraniumError};
10
11// I know this is duplicated, idc.
12const ASSETS_PATH: &str = "assets/";
13const OBJECTS_PATH: &str = "objects";
14
15/// Manages Minecraft installation verification and integrity checks.
16///
17/// This struct owns the primary data structures needed for verifying
18/// a Minecraft installation, including the installation path, instance
19/// configuration, and game resources. It serves as the central component
20/// for performing comprehensive verification operations on Minecraft
21/// installations.
22///
23/// The verifier maintains ownership of all data structures required for
24/// verification, ensuring that verification results can safely reference
25/// this data through lifetimes without risk of dangling pointers.
26///
27/// # Fields
28///
29/// * `minecraft_path` - Path to the Minecraft installation directory
30/// * `minecraft_instance` - Root configuration of the Minecraft instance
31/// * `resources` - Game resources including libraries and objects
32///
33/// # Example Usage
34///
35/// Basic verification workflow:
36/// ```ignore
37/// let verifier = InstallationVerifier::new(minecraft_path);
38/// let result = verifier.verify_version();
39///
40/// if result.is_valid() {
41/// println!("Installation is valid!");
42/// } else {
43/// println!("Found problems: {}", result.summary());
44/// }
45/// ```
46pub struct InstallationVerifier {
47 minecraft_path: PathBuf,
48 minecraft_instance: Root,
49 resources: Resources,
50}
51
52impl InstallationVerifier {
53 pub async fn new(minecraft_dir: &Path, version_id: &str) -> Result<Self> {
54 let instances = list_instances()
55 .await
56 .unwrap();
57
58 let instance_url = instances
59 .get_instance_url(version_id)
60 .ok_or(UraniumError::OtherWithReason(format!(
61 "Version {version_id} doesn't exist"
62 )))?;
63
64 let requester = reqwest::Client::new();
65
66 let minecraft_instance: Root = requester
67 .get(instance_url)
68 .send()
69 .await?
70 .json()
71 .await?;
72
73 let resources: Resources = requester
74 .get(
75 &minecraft_instance
76 .asset_index
77 .url,
78 )
79 .send()
80 .await?
81 .json::<Resources>()
82 .await?;
83
84 Ok(Self {
85 minecraft_path: minecraft_dir.to_path_buf(),
86 minecraft_instance,
87 resources,
88 })
89 }
90
91 /// Performs a comprehensive verification of the Minecraft installation.
92 ///
93 /// Verifies both libraries and objects in the installation and returns
94 /// references to any problematic files found.
95 ///
96 /// # Returns
97 ///
98 /// A `VersionCheckResult` containing references to any problematic
99 /// libraries and objects found during verification. If the installation
100 /// is completely valid, both arrays in the result will be empty.
101 ///
102 /// # Example
103 /// ```ignore
104 /// let mut verifier = InstallationVerifier::new(minecraft_path);
105 /// let result = verifier.verify();
106 ///
107 /// if result.is_valid() {
108 /// println!("Installation verified successfully!");
109 /// } else {
110 /// println!("Verification failed: {}", result.summary());
111 /// }
112 /// ```
113 pub fn verify(&self) -> VersionCheckResult {
114 let libs = self.verify_libs();
115 let objects = self.verify_objects();
116 let index = self.very_index();
117 let client = self.verify_client();
118 info!("Wrong files: {}", libs.len() + objects.len());
119
120 VersionCheckResult {
121 objects,
122 libs,
123 index,
124 client,
125 }
126 }
127
128 /// Verifies the integrity of the Minecraft client JAR file and returns
129 /// download data if verification fails.
130 ///
131 /// This function checks whether the client JAR file exists and verifies its
132 /// SHA1 hash against the expected hash from the download data.
133 ///
134 /// # Returns
135 ///
136 /// * `Some(&DownloadData)` - When the client JAR file is missing or has an
137 /// incorrect hash, indicating that the client needs to be downloaded or
138 /// re-downloaded
139 /// * `None` - When the client JAR file exists and passes hash verification,
140 /// meaning the local copy is valid and up-to-date, or if client download
141 /// data is not available
142 fn verify_client(&self) -> Option<&DownloadData> {
143 let client_path = self
144 .minecraft_path
145 .join("versions")
146 .join(&self.minecraft_instance.id)
147 .join(&self.minecraft_instance.id)
148 .with_extension("jar");
149
150 let client = self
151 .minecraft_instance
152 .downloads
153 .get("client")?;
154
155 if !client_path.exists() {
156 Some(client)
157 } else if let Ok(false) = verify_file_hash(&client_path, &client.sha1) {
158 error!("Wrong hash for {:?}, {}", &client_path, &client.sha1);
159 Some(client)
160 } else {
161 None
162 }
163 }
164
165 /// Verifies the integrity of the asset index file and returns it if
166 /// verification fails.
167 ///
168 /// This function checks whether the asset index file exists and verifies
169 /// its SHA1 hash against the expected hash from the Minecraft instance
170 /// configuration.
171 ///
172 /// # Returns
173 ///
174 /// * `Some(&AssetIndex)` - When the asset index file is missing or has an
175 /// incorrect hash, indicating that the index needs to be downloaded or
176 /// re-downloaded
177 /// * `None` - When the asset index file exists and passes hash
178 /// verification, meaning the local copy is valid and up-to-date
179 fn very_index(&self) -> Option<&AssetIndex> {
180 let index = &self
181 .minecraft_instance
182 .asset_index;
183
184 let index_path = self
185 .minecraft_path
186 .join(ASSETS_PATH)
187 .join("indexes")
188 .join(&index.id)
189 .with_extension("json");
190
191 if !index_path.exists() {
192 return Some(index);
193 }
194
195 if let Ok(false) = verify_file_hash(&index_path, &index.sha1) {
196 error!("Wrong hash for {:?}, {}", &index_path, &index.sha1);
197 return Some(index);
198 }
199 None
200 }
201
202 fn verify_libs(&self) -> Box<[&Library]> {
203 let mut bad_objects = vec![];
204
205 let current_os = match std::env::consts::OS {
206 "linux" => Os::Linux,
207 "windows" => Os::Windows,
208 _ => Os::Other,
209 };
210
211 for lib in self
212 .minecraft_instance
213 .libraries
214 .iter()
215 .filter(|l| {
216 l.get_os()
217 .is_none_or(|os| os == current_os)
218 })
219 {
220 if let Some((path, hash)) = lib
221 .downloads
222 .as_ref()
223 .map(|d| (&d.artifact.path, &d.artifact.sha1))
224 {
225 let lib_path = self
226 .minecraft_path
227 .join("libraries")
228 .join(path);
229 if let Ok(false) = verify_file_hash(&lib_path, hash) {
230 error!("Wrong hash for {lib_path:?}, {hash}");
231 bad_objects.push(lib);
232 }
233 }
234 }
235 Box::from(bad_objects)
236 }
237
238 /// This method verify the objects under `assets/objects`.
239 ///
240 /// Returns:
241 /// Err(UraniumError) If something went wrong
242 /// Ok(Box<[&str]>) A boxed array of the names of the wrong files, if the
243 /// box is empty then all objects are ok.
244 fn verify_objects(&self) -> Box<[&ObjectData]> {
245 use rayon::prelude::*;
246 let base = self
247 .minecraft_path
248 .join(ASSETS_PATH)
249 .join(OBJECTS_PATH);
250
251 let bad_objects = self
252 .resources
253 .objects
254 .par_iter()
255 .flat_map(|(_, data)| {
256 let object_path = base.join(data.get_path());
257 if let Ok(false) = verify_file_hash(&object_path, &data.hash) {
258 error!("Wrong hash for {object_path:?}, {}", data.hash);
259 Some(data)
260 } else {
261 None
262 }
263 })
264 .collect::<Vec<&ObjectData>>();
265 Box::from(bad_objects)
266 }
267}
268
269/// Result of a version check operation containing references to problematic
270/// files.
271///
272/// This structure holds references to objects and libraries that were
273/// identified as having errors or inconsistencies during the verification
274/// process. The lifetime parameter 'a ensures that the references remain valid
275/// as long as the original data in the InstallationVerifier exists.
276///
277/// # Fields
278///
279/// * objects - References to problematic object data files
280/// * libs - References to problematic library files
281///
282/// # Example Usage
283///
284/// ```ignore
285/// let verifier = InstallationVerifier::new(path);
286/// let result = verifier.verify_version();
287/// // Process problematic objects
288/// for object in result.objects.iter() {
289/// println!("Problematic object: {:?}", object);
290/// }
291/// // Check problematic libs...
292/// ```
293pub struct VersionCheckResult<'a> {
294 pub objects: Box<[&'a ObjectData]>,
295 pub libs: Box<[&'a Library]>,
296 pub index: Option<&'a AssetIndex>,
297 pub client: Option<&'a DownloadData>,
298}
299
300impl VersionCheckResult<'_> {
301 /// Returns true if the verification found no problems.
302 ///
303 /// This is a convenience method that checks if both the objects
304 /// and libraries arrays are empty, indicating a successful verification.
305 ///
306 /// # Returns
307 ///
308 /// `true` if no problematic objects or libraries were found, `false`
309 /// otherwise.
310 ///
311 /// # Example
312 ///```ignore
313 /// let result = verifier.verify_version();
314 /// if result.is_valid() {
315 /// println!("Installation is clean!");
316 /// }
317 /// ```
318 pub fn is_valid(&self) -> bool {
319 self.objects.is_empty() && self.libs.is_empty() && self.index.is_none()
320 }
321
322 /// Returns the total number of problematic items found.
323 ///
324 /// This combines the count of problematic objects and libraries
325 /// into a single number for quick assessment of verification results.
326 ///
327 /// # Returns
328 ///
329 /// The total count of problematic files.
330 pub fn total_problems(&self) -> usize {
331 self.objects.len()
332 + self.libs.len()
333 + self
334 .index
335 .map(|_| 1)
336 .unwrap_or_default()
337 }
338
339 /// Returns the number of problematic objects found.
340 ///
341 /// # Returns
342 ///
343 /// The count of problematic objects.
344 pub fn object_count(&self) -> usize {
345 self.objects.len()
346 }
347
348 /// Returns the number of problematic libraries found.
349 ///
350 /// # Returns
351 ///
352 /// The count of problematic libraries.
353 pub fn lib_count(&self) -> usize {
354 self.libs.len()
355 }
356}
357
358// What do you think this function does eh ?
359// Duh... of course it hashes the file verifier...
360fn verify_file_hash(file_path: &Path, expected_hash: &str) -> Result<bool> {
361 // Rinth hash is sha1
362 use crate::hashes::rinth_hash;
363
364 if !file_path.exists() {
365 return Ok(false);
366 }
367 let actual_hash = rinth_hash(file_path);
368 Ok(actual_hash.to_lowercase() == expected_hash.to_lowercase())
369}