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