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