Files
shacraft-launcher/src-tauri/src/profile.rs
T

130 lines
3.9 KiB
Rust

use crate::manifest::Manifest;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::{fmt, fs::File, io::{self, Read}, path::Path};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProfileInspection {
pub root: String,
pub managed_files: usize,
pub missing_files: usize,
pub mismatched_files: usize,
pub up_to_date: bool,
}
#[derive(Debug)]
pub enum ProfileError {
Io(io::Error),
}
impl fmt::Display for ProfileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "Cannot inspect profile: {error}"),
}
}
}
pub fn inspect(root: &Path, manifest: &Manifest) -> Result<ProfileInspection, ProfileError> {
let mut missing_files = 0;
let mut mismatched_files = 0;
for expected in &manifest.files {
let path = root.join(&expected.path);
let metadata = match path.metadata() {
Ok(metadata) if metadata.is_file() => metadata,
Ok(_) => {
mismatched_files += 1;
continue;
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
missing_files += 1;
continue;
}
Err(error) => return Err(ProfileError::Io(error)),
};
if metadata.len() != expected.size || sha256(&path)? != expected.sha256.to_ascii_lowercase() {
mismatched_files += 1;
}
}
Ok(ProfileInspection {
root: root.display().to_string(),
managed_files: manifest.files.len(),
missing_files,
mismatched_files,
up_to_date: missing_files == 0 && mismatched_files == 0,
})
}
fn sha256(path: &Path) -> Result<String, ProfileError> {
let mut file = File::open(path).map_err(ProfileError::Io)?;
let mut digest = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).map_err(ProfileError::Io)?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
Ok(format!("{:x}", digest.finalize()))
}
#[cfg(test)]
mod tests {
use super::inspect;
use crate::manifest::{FilePolicy, Loader, ManagedFile, Manifest, Minecraft};
use sha2::{Digest, Sha256};
use std::{fs, process, time::{SystemTime, UNIX_EPOCH}};
fn manifest(hash: String, size: u64) -> Manifest {
Manifest {
schema_version: 1,
id: "aeronautics".into(),
display_name: "Aeronautics".into(),
minecraft: Minecraft {
version: "1.21.1".into(),
loader: Loader { kind: "neoforge".into(), version: "21.1.248".into() },
java_major: 21,
},
files: vec![ManagedFile {
path: "mods/example.jar".into(),
url: "https://cdn.shacraft.ru/example.jar".into(),
sha256: hash,
size,
policy: FilePolicy::Managed,
}],
}
}
#[test]
fn reports_missing_and_matching_files() {
let root = std::env::temp_dir().join(format!(
"shacraft-launcher-test-{}-{}",
process::id(),
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
));
let bytes = b"ShaCraft test file";
let digest = format!("{:x}", Sha256::digest(bytes));
let expected = manifest(digest, bytes.len() as u64);
let missing = inspect(&root, &expected).unwrap();
assert_eq!(missing.missing_files, 1);
assert!(!missing.up_to_date);
fs::create_dir_all(root.join("mods")).unwrap();
fs::write(root.join("mods/example.jar"), bytes).unwrap();
let current = inspect(&root, &expected).unwrap();
assert_eq!(current.missing_files, 0);
assert_eq!(current.mismatched_files, 0);
assert!(current.up_to_date);
fs::remove_dir_all(root).unwrap();
}
}