Implement basic unpacker
This commit is contained in:
commit
e524aba94b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
8
.idea/modules.xml
generated
Normal 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
11
.idea/mrunpack.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
2068
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal 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
222
src/main.rs
Normal 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();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user