feat: inspect local profile file integrity
This commit is contained in:
Generated
+1
@@ -2799,6 +2799,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ tauri-build = { version = "2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,5 @@
|
|||||||
mod manifest;
|
mod manifest;
|
||||||
|
mod profile;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
@@ -33,9 +34,26 @@ fn validate_manifest(manifest_json: String) -> Result<(), String> {
|
|||||||
manifest::validate_json(&manifest_json).map(|_| ()).map_err(|error| error.to_string())
|
manifest::validate_json(&manifest_json).map(|_| ()).map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inspects the local profile without changing player files.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn inspect_profile(app: AppHandle, manifest_json: String) -> Result<profile::ProfileInspection, String> {
|
||||||
|
let manifest = manifest::validate_json(&manifest_json).map_err(|error| error.to_string())?;
|
||||||
|
let root = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?
|
||||||
|
.join("profiles")
|
||||||
|
.join(&manifest.id);
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn_blocking(move || profile::inspect(&root, &manifest))
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("Profile inspection task failed: {error}"))?
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![native_host, validate_manifest])
|
.invoke_handler(tauri::generate_handler![native_host, validate_manifest, inspect_profile])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running ShaCraft Launcher");
|
.expect("error while running ShaCraft Launcher");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user