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}