Files
untitled-minecraft-launcher/src/mrpack.rs

171 lines
5.3 KiB
Rust

use crate::{
download::{download, verify_sha512},
instance::{Loader, create, load, save},
util::{Hashes, is_safe_relative_path},
};
use indicatif::{ProgressBar, ProgressStyle};
use serde::Deserialize;
use std::{collections::HashMap, fs::File, io::Write, path::Path};
use zip::ZipArchive;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct Index {
format_version: u32,
game: String, // now what is modrinth up to?
version_id: String,
name: String,
files: Vec<PackFile>,
dependencies: HashMap<String, String>,
}
#[derive(Deserialize)]
struct PackFile {
path: String,
hashes: Hashes,
downloads: Vec<String>,
#[serde(default)]
env: Option<Env>,
}
#[derive(Deserialize)]
struct Env {
client: String,
}
fn extract_overrides(
archive: &mut ZipArchive<File>,
prefix: &str,
instance_dir: &Path,
) -> anyhow::Result<()> {
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let name = entry.name().to_string();
let Some(rel) = name.strip_prefix(prefix) else {
continue;
};
if rel.is_empty() || entry.is_dir() {
continue;
}
if !is_safe_relative_path(rel) {
anyhow::bail!("suspicious path in pack: {name}");
}
let dest = instance_dir.join(rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out = File::create(&dest)?;
std::io::copy(&mut entry, &mut out)?;
}
Ok(())
}
pub fn import(
pack: &Path,
instances_dir: &Path,
name: Option<&str>,
yes: bool,
) -> anyhow::Result<()> {
let mut archive = ZipArchive::new(File::open(pack)?)?;
let index_file = archive.by_name("modrinth.index.json")?;
let index: Index = serde_json::from_reader(index_file)?;
if index.format_version != 1 {
anyhow::bail!("unsupported mrpack format version {}", index.format_version);
}
let instance_name = match name {
Some(n) => n.to_string(),
None => crate::instance::sanitize(&index.name),
};
let mc = index
.dependencies
.get("minecraft")
.ok_or_else(|| anyhow::anyhow!("pack has no minecraft version"))?;
let loader = if let Some(v) = index.dependencies.get("fabric-loader") {
Some(Loader {
kind: "fabric".to_string(),
version: v.clone(),
})
} else if index.dependencies.contains_key("forge")
|| index.dependencies.contains_key("neoforge")
{
anyhow::bail!("Forge modpacks are not currently supported.");
} else if index.dependencies.contains_key("quilt-loader") {
anyhow::bail!("Quilt modpacks are not currently supported.");
} else {
println!(
"This modpack either doesn't have a modloader, or uses one that is unsupported. This is fine, but any mods won't load."
);
None
};
let as_name = match name {
Some(n) => format!(" as {n}"),
None => String::new(),
};
let wanted: Vec<_> = index
.files
.iter()
.filter(|f| f.env.as_ref().map_or(true, |e| e.client != "unsupported"))
.collect();
let loader_bit = match &loader {
Some(l) => format!(", which will use {} with {} mods", l.kind, wanted.len()),
None => String::new(),
};
println!(
"You are importing the modpack {}{}, this modpack will use minecraft version {}{}.",
index.name, as_name, mc, loader_bit
);
if !yes {
print!("Continue? [y/N] ");
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !answer.trim().eq_ignore_ascii_case("y") {
anyhow::bail!("aborted");
}
}
println!("Creating instance...");
let instance_dir = create(instances_dir, &instance_name, mc)?;
let (mut cfg, _) = load(instances_dir, &instance_name)?;
cfg.loader = loader;
save(&instance_dir, &cfg)?;
println!("Downloading files...");
let pb = ProgressBar::new(wanted.len() as u64);
pb.set_style(ProgressStyle::with_template("{bar:40} {pos}/{len} {msg}").unwrap());
for f in &wanted {
if !is_safe_relative_path(&f.path) {
anyhow::bail!("suspicious path in pack: {}", f.path);
}
pb.set_message(f.path.clone());
let dest = instance_dir.join(&f.path);
if !dest.exists() || !verify_sha512(&dest, &f.hashes.sha512)? {
let url = f
.downloads
.first()
.ok_or_else(|| anyhow::anyhow!("no download url for {}", f.path))?;
download(url, &dest)?;
if !verify_sha512(&dest, &f.hashes.sha512)? {
anyhow::bail!(
"hash mismatch after download: {}. This file is NOT DELETED AND WILL LOAD ON NEXT LAUNCH. DO NOT LAUNCH MINECRAFT, until you are SURE this is safe.",
f.path
);
}
}
pb.inc(1);
}
pb.finish_and_clear();
println!("Copying overrides...");
extract_overrides(&mut archive, "overrides/", &instance_dir)?;
extract_overrides(&mut archive, "client-overrides/", &instance_dir)?;
println!("The modpack has (probably) been installed successfully!");
Ok(())
}