Skip to main content

uranium_rs/
version_checker.rs

1use std::path::{Path, PathBuf};
2
3use log::{error, info, warn};
4use mine_data_structs::minecraft::{
5    AssetIndex, DownloadData, Library, ObjectData, Os, Resources, Root,
6};
7use rayon::iter::{ParallelBridge, ParallelIterator};
8
9use crate::downloaders::list_instances;
10use crate::error::{Result, UraniumError};
11
12// I know this is duplicated, idc.
13const ASSETS_PATH: &str = "assets/";
14const OBJECTS_PATH: &str = "objects";
15
16/// Manages Minecraft installation verification and integrity checks.
17///
18/// This struct owns the primary data structures needed for verifying
19/// a Minecraft installation, including the installation path, instance
20/// configuration, and game resources. It serves as the central component
21/// for performing comprehensive verification operations on Minecraft
22/// installations.
23///
24/// # Example Usage
25///
26/// Basic verification workflow:
27/// ```ignore
28///     let verifier = InstallationVerifier::new(minecraft_path);
29///     let result = verifier.verify_version();
30///
31///     if result.is_valid() {
32///         println!("Installation is valid!");
33///     } else {
34///         println!("Found problems: {}", result.total_problems());
35///     }
36/// ```
37pub struct InstallationVerifier {
38    minecraft_path: PathBuf,
39    minecraft_instance: Root,
40    resources: Resources,
41}
42
43impl InstallationVerifier {
44    pub async fn new(minecraft_dir: &Path, version_id: &str) -> Result<Self> {
45        let instances = list_instances()
46            .await
47            .unwrap();
48
49        let instance_url = instances
50            .get_instance_url(version_id)
51            .ok_or(UraniumError::OtherWithReason(format!(
52                "Version {version_id} doesn't exist"
53            )))?;
54
55        let requester = reqwest::Client::new();
56
57        let minecraft_instance: Root = requester
58            .get(instance_url)
59            .send()
60            .await?
61            .json()
62            .await?;
63
64        let resources: Resources = requester
65            .get(
66                &minecraft_instance
67                    .asset_index
68                    .url,
69            )
70            .send()
71            .await?
72            .json::<Resources>()
73            .await?;
74
75        Ok(Self {
76            minecraft_path: minecraft_dir.to_path_buf(),
77            minecraft_instance,
78            resources,
79        })
80    }
81
82    /// Performs a comprehensive verification of the Minecraft installation.
83    ///
84    /// Verifies both libraries and objects in the installation and returns
85    /// references to any problematic files found.
86    ///
87    /// # Returns
88    ///
89    /// A `VersionCheckResult` containing references to any problematic
90    /// libraries and objects found during verification. If the installation
91    /// is completely valid, both arrays in the result will be empty.
92    ///
93    /// # Example
94    /// ```ignore
95    /// let mut verifier = InstallationVerifier::new(minecraft_path);
96    /// let result = verifier.verify();
97    ///
98    /// if result.is_valid() {
99    ///     println!("Installation verified successfully!");
100    /// } else {
101    ///     println!("Verification failed: {}", result.summary());
102    /// }
103    /// ```
104    pub fn verify(&self) -> VersionCheckResult<'_> {
105        let libs = self.verify_libs();
106        let objects = self.verify_objects();
107        let index = self.very_index();
108        let client = self.verify_client();
109        info!("Wrong files: {}", libs.len() + objects.len());
110        if let Some(index) = index {
111            info!("Wrong index: {}", index.id);
112        }
113        if let Some(client) = client {
114            info!("Wrong client: {}", client.sha1);
115        }
116
117        VersionCheckResult {
118            objects,
119            libs,
120            index,
121            client,
122        }
123    }
124
125    /// Verifies the integrity of the Minecraft client JAR file and returns
126    /// download data if verification fails.
127    ///
128    /// This function checks whether the client JAR file exists and verifies its
129    /// SHA1 hash against the expected hash from the download data.
130    ///
131    /// # Returns
132    ///
133    /// * `Some(&DownloadData)` - When the client JAR file is missing or has an
134    ///   incorrect hash, indicating that the client needs to be downloaded or
135    ///   re-downloaded
136    /// * `None` - When the client JAR file exists and passes hash verification,
137    ///   meaning the local copy is valid and up-to-date, or if client download
138    ///   data is not available
139    fn verify_client(&self) -> Option<&DownloadData> {
140        let client_path = self
141            .minecraft_path
142            .join("versions")
143            .join(&self.minecraft_instance.id)
144            .join(
145                self.minecraft_instance
146                    .id
147                    .clone()
148                    + ".jar",
149            );
150
151        let client = self
152            .minecraft_instance
153            .downloads
154            .get("client")?;
155
156        if !client_path.exists() {
157            error!("Client doesn't exist: {client_path:?}");
158            Some(client)
159        } else if let Ok(false) = verify_file_hash(&client_path, &client.sha1) {
160            error!("Wrong hash for {:?}, {}", &client_path, &client.sha1);
161            Some(client)
162        } else {
163            None
164        }
165    }
166
167    /// Verifies the integrity of the asset index file and returns it if
168    /// verification fails.
169    ///
170    /// This function checks whether the asset index file exists and verifies
171    /// its SHA1 hash against the expected hash from the Minecraft instance
172    /// configuration.
173    ///
174    /// # Returns
175    ///
176    /// * `Some(&AssetIndex)` - When the asset index file is missing or has an
177    ///   incorrect hash, indicating that the index needs to be downloaded or
178    ///   re-downloaded
179    /// * `None` - When the asset index file exists and passes hash
180    ///   verification, meaning the local copy is valid and up-to-date
181    fn very_index(&self) -> Option<&AssetIndex> {
182        let index = &self
183            .minecraft_instance
184            .asset_index;
185
186        let index_path = self
187            .minecraft_path
188            .join(ASSETS_PATH)
189            .join("indexes")
190            .join(&index.id)
191            .with_extension("json");
192
193        if !index_path.exists() {
194            return Some(index);
195        }
196
197        use std::fs;
198
199        // Mojang json comes with spaces after ',' and ':', so we need to
200        // replace them with the trimmed version.
201        let data = fs::read_to_string(&index_path)
202            .ok()?
203            .replace(":", ": ")
204            .replace(",", ", ");
205
206        use sha1::{Digest, Sha1};
207        let mut hasher = Sha1::new();
208        hasher.update(data.as_bytes());
209
210        let h = format!("{:x}", hasher.finalize());
211        if index.sha1 != h {
212            error!("Wrong hash for {:?}, {}-{}", &index_path, &index.sha1, h);
213            return Some(index);
214        }
215
216        //if let Ok(false) = verify_file_hash(&index_path, &index.sha1) {
217        //    error!("Wrong hash for {:?}, {}", &index_path, &index.sha1);
218        //    return Some(index);
219        //}
220        None
221    }
222
223    fn verify_libs(&self) -> Box<[&Library]> {
224        let current_os = match std::env::consts::OS {
225            "linux" => Os::Linux,
226            "windows" => Os::Windows,
227            _ => Os::Other,
228        };
229
230        // Extract libs for the current os
231        let os_libs = self
232            .minecraft_instance
233            .libraries
234            .iter()
235            .filter(|l| {
236                l.get_os()
237                    .is_none_or(|os| os == current_os)
238            });
239
240        // Set up an iterator with all the data needed
241        let raw_data = os_libs.filter_map(|lib| {
242            lib.downloads
243                .as_ref()
244                .map(|d| (&d.artifact.path, &d.artifact.sha1, lib))
245        });
246
247        // Fix the lib's paths
248        let fixed_data = raw_data.map(|(path, sha1, lib)| {
249            (
250                self.minecraft_path
251                    .join("libraries")
252                    .join(path),
253                sha1,
254                lib,
255            )
256        });
257
258        // Verify libraries in parallel with rayon
259        let bad_objects: Vec<_> = fixed_data
260            .par_bridge()
261            .filter_map(|(lib_path, hash, lib)| {
262                if let Ok(false) = verify_file_hash(&lib_path, hash) {
263                    error!("Wrong hash for {lib_path:?}, {hash}");
264                    Some(lib)
265                } else {
266                    None
267                }
268            })
269            .collect();
270
271        Box::from(bad_objects)
272    }
273
274    /// This method verify the objects under `assets/objects`.
275    ///
276    /// Returns:
277    /// Err(UraniumError) If something went wrong
278    /// Ok(Box<[&str]>) A boxed array of the names of the wrong files, if the
279    /// box is empty then all objects are ok.
280    fn verify_objects(&self) -> Box<[&ObjectData]> {
281        use rayon::prelude::*;
282        let base = self
283            .minecraft_path
284            .join(ASSETS_PATH)
285            .join(OBJECTS_PATH);
286
287        let bad_objects = self
288            .resources
289            .objects
290            .par_iter()
291            .flat_map(|(_, data)| {
292                let object_path = base.join(data.get_path());
293                match verify_file_hash(&object_path, &data.hash) {
294                    Ok(false) => {
295                        warn!("Wrong hash for {object_path:?}, {}", data.hash);
296                        Some(data)
297                    }
298                    Err(e) => {
299                        error!("Error verifying: {}", e);
300                        None
301                    }
302                    _ => None,
303                }
304            })
305            .collect::<Vec<&ObjectData>>();
306        Box::from(bad_objects)
307    }
308}
309
310/// Result of a version check operation containing references to problematic
311/// files.
312///
313/// This structure holds references to objects and libraries that were
314/// identified as having errors or inconsistencies during the verification
315/// process. The lifetime parameter 'a ensures that the references remain valid
316/// as long as the original data in the InstallationVerifier exists.
317///
318/// # Fields
319///
320/// * objects - References to problematic object data files
321/// * libs - References to problematic library files
322///
323/// # Example Usage
324///
325/// ```ignore
326/// let verifier = InstallationVerifier::new(path);
327/// let result = verifier.verify_version();
328/// // Process problematic objects
329/// for object in result.objects.iter() {
330///      println!("Problematic object: {:?}", object);  
331/// }
332/// // Check problematic libs...
333/// ```
334pub struct VersionCheckResult<'a> {
335    pub objects: Box<[&'a ObjectData]>,
336    pub libs: Box<[&'a Library]>,
337    pub index: Option<&'a AssetIndex>,
338    pub client: Option<&'a DownloadData>,
339}
340
341impl VersionCheckResult<'_> {
342    /// Returns true if the verification found no problems.
343    ///
344    /// This is a convenience method that checks if both the objects
345    /// and libraries arrays are empty, indicating a successful verification.
346    ///
347    /// # Returns
348    ///
349    /// `true` if no problematic objects or libraries were found, `false`
350    /// otherwise.
351    ///
352    /// # Example
353    ///```ignore
354    /// let result = verifier.verify_version();
355    /// if result.is_valid() {
356    ///     println!("Installation is clean!");
357    /// }
358    /// ```
359    pub fn is_valid(&self) -> bool {
360        self.objects.is_empty()
361            && self.libs.is_empty()
362            && self.index.is_none()
363            && self.client.is_none()
364    }
365
366    /// Returns the total number of problematic items found.
367    ///
368    /// This combines the count of problematic objects and libraries
369    /// into a single number for quick assessment of verification results.
370    ///
371    /// # Returns
372    ///
373    /// The total count of problematic files.
374    pub fn total_problems(&self) -> usize {
375        self.objects.len()
376            + self.libs.len()
377            + self
378                .index
379                .map(|_| 1)
380                .unwrap_or_default()
381            + self
382                .client
383                .map(|_| 1)
384                .unwrap_or_default()
385    }
386
387    /// Returns the number of problematic objects found.
388    ///
389    /// # Returns
390    ///
391    /// The count of problematic objects.
392    pub fn object_count(&self) -> usize {
393        self.objects.len()
394    }
395
396    /// Returns the number of problematic libraries found.
397    ///
398    /// # Returns
399    ///
400    /// The count of problematic libraries.
401    pub fn lib_count(&self) -> usize {
402        self.libs.len()
403    }
404}
405
406// What do you think this function does eh ?
407// Duh... of course it hashes the file verifier...
408fn verify_file_hash(file_path: &Path, expected_hash: &str) -> Result<bool> {
409    // Rinth hash is sha1
410    use crate::hashes::rinth_hash;
411
412    if !file_path.exists() {
413        return Err(UraniumError::FileNotFound(
414            file_path
415                .to_string_lossy()
416                .to_string(),
417        ));
418    }
419    let actual_hash = rinth_hash(file_path);
420    Ok(actual_hash.to_lowercase() == expected_hash.to_lowercase())
421}
422
423#[cfg(test)]
424mod tests {
425    use std::sync::{Arc, Condvar, Mutex, LazyLock};
426    use tokio::sync::Notify;
427
428    use crate::{
429        downloaders::{Downloader, MinecraftDownloader},
430        variables::constants::TEMP_DIR,
431    };
432
433    use super::*;
434
435    static PAIR: LazyLock<Arc<(Mutex<bool>, Condvar)>> =
436        LazyLock::new(|| Arc::new((Mutex::new(false), Condvar::new())));
437
438    const VERSION: &str = "1.21.1";
439
440    #[tokio::test]
441    async fn a_download_minecraft() -> Result<()> {
442        let (lock, cvar) = &*(PAIR.clone());
443        let mut downloader = MinecraftDownloader::<Downloader>::init(TEMP_DIR.as_path(), VERSION)
444            .await
445            .unwrap();
446        let _ = downloader.start().await;
447        *lock.lock().unwrap() = true;
448        cvar.notify_all();
449
450        Ok(())
451    }
452
453    #[tokio::test]
454    async fn check_file() {
455        let (lock, cvar) = &*(PAIR.clone());
456        let mut started = lock.lock().unwrap();
457        while !*started {
458            started = cvar.wait(started).unwrap();
459        }
460
461        let checker = InstallationVerifier::new(&TEMP_DIR, VERSION)
462            .await
463            .unwrap();
464        let result = checker.verify();
465        assert_eq!(result.total_problems(), 0);
466    }
467
468    #[tokio::test]
469    async fn check_missing_client() {
470        let (lock, cvar) = &*(PAIR.clone());
471        let mut started = lock.lock().unwrap();
472        while !*started {
473            started = cvar.wait(started).unwrap();
474        }
475
476        let checker = InstallationVerifier::new(&TEMP_DIR, VERSION)
477            .await
478            .unwrap();
479        if let Err(e) =
480            std::fs::remove_file("/home/sergio/.local/state/uranium/versions/1.21.1/1.21.1.jar")
481        {
482            panic!("Could not remove {e}");
483        }
484        let result = checker.verify();
485        assert_eq!(result.total_problems(), 1);
486    }
487}