use std::path::{Path, PathBuf};
use log::{error, info};
use mine_data_structs::minecraft::{
AssetIndex, DownloadData, Library, ObjectData, Os, Resources, Root,
};
use crate::downloaders::list_instances;
use crate::error::{Result, UraniumError};
const ASSETS_PATH: &str = "assets/";
const OBJECTS_PATH: &str = "objects";
pub struct InstallationVerifier {
minecraft_path: PathBuf,
minecraft_instance: Root,
resources: Resources,
}
impl InstallationVerifier {
pub async fn new(minecraft_dir: &Path, version_id: &str) -> Result<Self> {
let instances = list_instances()
.await
.unwrap();
let instance_url = instances
.get_instance_url(version_id)
.ok_or(UraniumError::OtherWithReason(format!(
"Version {version_id} doesn't exist"
)))?;
let requester = reqwest::Client::new();
let minecraft_instance: Root = requester
.get(instance_url)
.send()
.await?
.json()
.await?;
let resources: Resources = requester
.get(
&minecraft_instance
.asset_index
.url,
)
.send()
.await?
.json::<Resources>()
.await?;
Ok(Self {
minecraft_path: minecraft_dir.to_path_buf(),
minecraft_instance,
resources,
})
}
pub fn verify(&self) -> VersionCheckResult {
let libs = self.verify_libs();
let objects = self.verify_objects();
let index = self.very_index();
let client = self.verify_client();
info!("Wrong files: {}", libs.len() + objects.len());
VersionCheckResult {
objects,
libs,
index,
client,
}
}
fn verify_client(&self) -> Option<&DownloadData> {
let client_path = self
.minecraft_path
.join("versions")
.join(&self.minecraft_instance.id)
.join(&self.minecraft_instance.id)
.with_extension("jar");
let client = self
.minecraft_instance
.downloads
.get("client")?;
if !client_path.exists() {
Some(client)
} else if let Ok(false) = verify_file_hash(&client_path, &client.sha1) {
error!("Wrong hash for {:?}, {}", &client_path, &client.sha1);
Some(client)
} else {
None
}
}
fn very_index(&self) -> Option<&AssetIndex> {
let index = &self
.minecraft_instance
.asset_index;
let index_path = self
.minecraft_path
.join(ASSETS_PATH)
.join("indexes")
.join(&index.id)
.with_extension("json");
if !index_path.exists() {
return Some(index);
}
use std::fs;
let data = fs::read_to_string(&index_path)
.ok()?
.replace(":", ": ")
.replace(",", ", ");
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
hasher.update(data.as_bytes());
let h = format!("{:x}", hasher.finalize());
if index.sha1 != h {
error!("Wrong hash for {:?}, {}-{}", &index_path, &index.sha1, h);
return Some(index);
}
None
}
fn verify_libs(&self) -> Box<[&Library]> {
let mut bad_objects = vec![];
let current_os = match std::env::consts::OS {
"linux" => Os::Linux,
"windows" => Os::Windows,
_ => Os::Other,
};
for lib in self
.minecraft_instance
.libraries
.iter()
.filter(|l| {
l.get_os()
.is_none_or(|os| os == current_os)
})
{
if let Some((path, hash)) = lib
.downloads
.as_ref()
.map(|d| (&d.artifact.path, &d.artifact.sha1))
{
let lib_path = self
.minecraft_path
.join("libraries")
.join(path);
if let Ok(false) = verify_file_hash(&lib_path, hash) {
error!("Wrong hash for {lib_path:?}, {hash}");
bad_objects.push(lib);
}
}
}
Box::from(bad_objects)
}
fn verify_objects(&self) -> Box<[&ObjectData]> {
use rayon::prelude::*;
let base = self
.minecraft_path
.join(ASSETS_PATH)
.join(OBJECTS_PATH);
let bad_objects = self
.resources
.objects
.par_iter()
.flat_map(|(_, data)| {
let object_path = base.join(data.get_path());
if let Ok(false) = verify_file_hash(&object_path, &data.hash) {
error!("Wrong hash for {object_path:?}, {}", data.hash);
Some(data)
} else {
None
}
})
.collect::<Vec<&ObjectData>>();
Box::from(bad_objects)
}
}
pub struct VersionCheckResult<'a> {
pub objects: Box<[&'a ObjectData]>,
pub libs: Box<[&'a Library]>,
pub index: Option<&'a AssetIndex>,
pub client: Option<&'a DownloadData>,
}
impl VersionCheckResult<'_> {
pub fn is_valid(&self) -> bool {
self.objects.is_empty() && self.libs.is_empty() && self.index.is_none()
}
pub fn total_problems(&self) -> usize {
self.objects.len()
+ self.libs.len()
+ self
.index
.map(|_| 1)
.unwrap_or_default()
}
pub fn object_count(&self) -> usize {
self.objects.len()
}
pub fn lib_count(&self) -> usize {
self.libs.len()
}
}
fn verify_file_hash(file_path: &Path, expected_hash: &str) -> Result<bool> {
use crate::hashes::rinth_hash;
if !file_path.exists() {
return Ok(false);
}
let actual_hash = rinth_hash(file_path);
Ok(actual_hash.to_lowercase() == expected_hash.to_lowercase())
}