Implement basic unpacker

This commit is contained in:
Gabriel Tofvesson 2025-05-04 16:22:28 +02:00
commit e524aba94b
8 changed files with 2337 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mrunpack.iml" filepath="$PROJECT_DIR$/.idea/mrunpack.iml" />
</modules>
</component>
</project>

11
.idea/mrunpack.iml generated Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2068
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "mrunpack"
version = "0.1.0"
edition = "2021"
[dependencies]
zip = "2.6.1"
reqwest = { version = "0.12.15", features = ["json"] }
serde_json = "1.0"
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.44.2", features = ["full"] }
tokio-macros = { version = "0.2.6" }
bytes = "*"

222
src/main.rs Normal file
View File

@ -0,0 +1,222 @@
use std::collections::HashMap;
use std::env::args;
use std::io::Read;
use std::path::PathBuf;
use zip::ZipArchive;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
enum Side {
#[serde(rename = "client")]
Client,
#[serde(rename = "server")]
Server,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
enum Requirement {
#[serde(rename = "required")]
Required,
#[serde(rename = "optional")]
Optional,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
enum Dependency {
#[serde(rename = "minecraft")]
Minecraft,
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
NeoForge,
#[serde(rename = "fabric-loader")]
Fabric,
}
impl Dependency {
async fn download(&self, target: impl Into<PathBuf>, version: String, mc_version: String) -> Result<(), Box<dyn std::error::Error>> {
let url = match self {
Dependency::NeoForge => format!("https://maven.neoforged.net/releases/net/neoforged/forge/{0}/forge-{0}-universal.jar", version),
Dependency::Fabric => format!(
"https://meta2.fabricmc.net/v2/versions/loader/{}/{}/{}/server/jar",
mc_version,
version,
reqwest::get("https://meta2.fabricmc.net/v2/versions/installer")
.await?
.json::<Vec<FabricInstallerVersion>>()
.await?
.first()
.ok_or("No fabric installer version found")?.version
),
Dependency::Minecraft => {
let version_map = reqwest::get("https://raw.githubusercontent.com/liebki/MinecraftServerForkDownloads/refs/heads/main/release_vanilla_downloads.json")
.await?
.json::<HashMap<String, String>>()
.await?;
version_map.get(&mc_version)
.ok_or("No Minecraft version found")?
.to_string()
},
Dependency::Forge => format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-universal.jar", version)
};
let target = target.into();
if target.exists() {
println!("File {} already exists, skipping download", target.display());
return Ok(());
}
let response = reqwest::get(url.clone()).await?;
if response.status().is_success() {
let content = response.bytes().await?;
let mut file = File::create(&target).await?;
tokio::io::copy(&mut content.as_ref(), &mut file).await?;
return Ok(())
}
Err(format!("Failed to download file: {}", url).into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ConfigFile {
path: String,
hashes: HashMap<String, String>,
env: HashMap<Side, Requirement>,
downloads: Vec<String>,
#[serde(rename = "fileSize")]
file_size: u64,
}
impl ConfigFile {
async fn download(&self, target: impl Into<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
let target = target.into();
let target_path = target.join(&self.path);
if target_path.exists() {
println!("File {} already exists, skipping download", target_path.display());
return Ok(());
}
for link in &self.downloads {
let response = reqwest::get(link).await?;
if response.status().is_success() {
let content = response.bytes().await?;
let mut file = File::create(&target_path).await?;
tokio::io::copy(&mut content.as_ref(), &mut file).await?;
return Ok(())
} else {
println!("Failed to download file from {}", link);
}
}
Err(format!("Failed to download file: {}", self.path).into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ModrinthConfig {
game: String,
#[serde(rename = "formatVersion")]
format_version: u8,
#[serde(rename = "versionId")]
version_id: String,
name: String,
summary: String,
files: Vec<ConfigFile>,
dependencies: HashMap<Dependency, String>,
}
impl ModrinthConfig {
fn is_modded(&self) -> bool {
self.dependencies.len() > 1
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FabricInstallerVersion {
url: String,
maven: String,
version: String,
stable: bool,
}
async fn get_config_from_archive(archive: &mut ZipArchive<std::fs::File>, out_dir: impl Into<PathBuf>) -> Option<ModrinthConfig> {
let overrides = "overrides/";
let modrinth_index = "modrinth.index.json";
let out_dir = out_dir.into();
for i in 0..archive.len() {
let mut file = archive.by_index(i).expect("Unable to read file");
if file.name() == modrinth_index {
let mut contents = String::new();
if let Ok(_) = file.read_to_string(&mut contents) {
println!("Found modrinth index file");
return Some(serde_json::from_str::<ModrinthConfig>(contents.as_str()).expect("Unable to parse modrinth config"))
} else {
eprintln!("Can't read contents");
}
} else if file.name().starts_with(overrides) && file.is_file() {
if let Some(path) = file.enclosed_name() {
let realpath = path.parent().unwrap().to_str().unwrap()[overrides.len()..].to_string();
// Create directory "out/" + realpath if it doesn't exist
let out_path = out_dir.join(realpath);
tokio::fs::create_dir_all(out_path.clone()).await.ok()?;
let mut out_file = std::fs::File::create(out_path.join(path.file_name().unwrap())).ok()?;
std::io::copy(&mut file, &mut out_file).expect("Unable to copy file");
}
} else {
println!("File {} is not a modrinth index file", file.name());
}
}
None
}
#[tokio::main]
async fn main() {
let args: Vec<String> = args().collect();
if args.len() != 2 && args.len() != 3 {
eprintln!("Usage: {} <mrpack> [target dir]", args[0]);
return;
}
let out_dir = if args.len() == 3 {
PathBuf::from(&args[2])
} else {
PathBuf::from("out/")
};
let file = std::fs::File::open(&args[1]).expect("Unable to open file");
let mut archive = ZipArchive::new(file).expect("Unable to read zip archive");
// Create directory "out" and subdirectory "mods"
std::fs::create_dir_all(&out_dir).expect("Unable to create directory");
let config = get_config_from_archive(&mut archive, &out_dir).await.unwrap();
for entry in &config.files {
entry.download(&out_dir).await.unwrap();
}
let (server, server_version, mc_version) = if config.is_modded() {
let (dep, version) = config.dependencies.iter().find(|(k, _)| **k != Dependency::Minecraft).expect("Modded dependency version not found");
let mc_version = config.dependencies.iter().find(|(k, _)| **k == Dependency::Minecraft).map(|(_, v)| v.clone()).expect("Minecraft version not found");
(dep.clone(), version.clone(), mc_version)
} else {
let (dep, version) = config.dependencies.iter().find(|(k, _)| **k == Dependency::Minecraft).expect("Modded dependency version not found");
(dep.clone(), version.clone(), version.clone())
};
server.download(out_dir.join("server.jar"), server_version, mc_version).await.unwrap();
let eula = "#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA).
#Thu Jan 01 00:00:00 GMT 1970
eula=true
";
let mut eula_file = File::create(out_dir.join("eula.txt")).await.unwrap();
eula_file.write_all(eula.as_bytes()).await.unwrap();
}