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}