the first wave of refactors

This commit is contained in:
leocth 2022-02-20 22:20:50 +08:00
parent 9d74e84c01
commit 14e8e92f46
9 changed files with 264 additions and 294 deletions

23
Cargo.lock generated
View File

@ -167,6 +167,26 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "const_format"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22bc6cd49b0ec407b680c3e380182b6ac63b73991cb7602de350352fc309b614"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.2" version = "0.9.2"
@ -1165,13 +1185,12 @@ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"chrono", "chrono",
"const_format",
"daedalus", "daedalus",
"fs_extra", "fs_extra",
"futures", "futures",
"json5", "json5",
"lazy_static",
"log", "log",
"once_cell",
"path-clean", "path-clean",
"regex", "regex",
"reqwest", "reqwest",

View File

@ -9,7 +9,6 @@ edition = "2018"
[dependencies] [dependencies]
thiserror = "1.0" thiserror = "1.0"
async-trait = "0.1.51" async-trait = "0.1.51"
once_cell = "1.9.0"
daedalus = "0.1.12" daedalus = "0.1.12"
@ -27,7 +26,6 @@ path-clean = "0.1.0"
fs_extra = "1.2.0" fs_extra = "1.2.0"
regex = "1.5" regex = "1.5"
lazy_static = "1.4"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
futures = "0.3" futures = "0.3"
@ -35,9 +33,12 @@ futures = "0.3"
sys-info = "0.9.0" sys-info = "0.9.0"
log = "0.4.14" log = "0.4.14"
const_format = "0.2.22"
[dev-dependencies] [dev-dependencies]
argh = "0.1.6" argh = "0.1.6"
[[example]] [[example]]
name = "download-pack" name = "download-pack"
[features]

View File

@ -9,30 +9,35 @@ use futures::future;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::time::Duration;
pub async fn download_version_info( pub async fn download_version_info(
client_path: &Path, client_path: &Path,
version: &Version, version: &Version,
loader_version: Option<&LoaderVersion>, loader_version: Option<&LoaderVersion>,
) -> Result<VersionInfo, LauncherError> { ) -> Result<VersionInfo, LauncherError> {
let id = loader_version.map(|x| &x.id).unwrap_or(&version.id); let id = match loader_version {
Some(x) => &x.id,
None => &version.id,
};
let path = &*client_path.join(id).join(format!("{}.json", id)); let mut path = client_path.join(id);
path.push(id);
path.set_extension("json");
if path.exists() { if path.exists() {
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) let contents = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
} else { } else {
let mut info = fetch_version_info(version).await?; let mut info = fetch_version_info(version).await?;
if let Some(loader_version) = loader_version { if let Some(loader_version) = loader_version {
let partial = fetch_partial_version(&*loader_version.url).await?; let partial = fetch_partial_version(&loader_version.url).await?;
info = merge_partial_version(partial, info); info = merge_partial_version(partial, info);
info.id = loader_version.id.clone(); info.id = loader_version.id.clone();
} }
let info_s = serde_json::to_string(&info)?;
save_file(path, &bytes::Bytes::from(serde_json::to_string(&info)?))?; save_file(&path, &bytes::Bytes::from(info_s))?;
Ok(info) Ok(info)
} }
@ -42,22 +47,21 @@ pub async fn download_client(
client_path: &Path, client_path: &Path,
version_info: &VersionInfo, version_info: &VersionInfo,
) -> Result<(), LauncherError> { ) -> Result<(), LauncherError> {
let version = &version_info.id;
let client_download = version_info let client_download = version_info
.downloads .downloads
.get(&DownloadType::Client) .get(&DownloadType::Client)
.ok_or_else(|| { .ok_or_else(|| {
LauncherError::InvalidInput(format!( LauncherError::InvalidInput(format!(
"Version {} does not have any client downloads", "Version {version} does not have any client downloads"
&version_info.id
)) ))
})?; })?;
let path = &*client_path let mut path = client_path.join(version);
.join(&version_info.id) path.push(version);
.join(format!("{}.jar", &version_info.id)); path.set_extension("jar");
save_and_download_file(path, &client_download.url, Some(&client_download.sha1)).await?;
save_and_download_file(&path, &client_download.url, Some(&client_download.sha1)).await?;
Ok(()) Ok(())
} }
@ -65,16 +69,15 @@ pub async fn download_assets_index(
assets_path: &Path, assets_path: &Path,
version: &VersionInfo, version: &VersionInfo,
) -> Result<AssetsIndex, LauncherError> { ) -> Result<AssetsIndex, LauncherError> {
let path = &*assets_path let path = assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
.join("indexes")
.join(format!("{}.json", &version.asset_index.id));
if path.exists() { if path.exists() {
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content)?)
} else { } else {
let index = fetch_assets_index(version).await?; let index = fetch_assets_index(version).await?;
save_file(path, &bytes::Bytes::from(serde_json::to_string(&index)?))?; save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))?;
Ok(index) Ok(index)
} }
@ -89,7 +92,7 @@ pub async fn download_assets(
index index
.objects .objects
.iter() .iter()
.map(|x| download_asset(assets_path, legacy_path, x.0, x.1)), .map(|(name, asset)| download_asset(assets_path, legacy_path, name, asset)),
) )
.await .await
.into_iter() .into_iter()
@ -104,23 +107,20 @@ async fn download_asset(
name: &str, name: &str,
asset: &Asset, asset: &Asset,
) -> Result<(), LauncherError> { ) -> Result<(), LauncherError> {
let sub_hash = &&asset.hash[..2]; let hash = &asset.hash;
let sub_hash = &hash[..2];
let resource_path = assets_path.join("objects").join(sub_hash).join(&asset.hash); let mut resource_path = assets_path.join("objects");
resource_path.push(sub_hash);
resource_path.push(hash);
let resource = save_and_download_file( let url = format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
&*resource_path,
&format!( let resource = save_and_download_file(&resource_path, &url, Some(hash)).await?;
"https://resources.download.minecraft.net/{}/{}",
sub_hash, asset.hash
),
Some(&*asset.hash),
)
.await?;
if let Some(legacy_path) = legacy_path { if let Some(legacy_path) = legacy_path {
let resource_path = let resource_path =
legacy_path.join(name.replace('/', &*std::path::MAIN_SEPARATOR.to_string())); legacy_path.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
save_file(resource_path.as_path(), &resource)?; save_file(resource_path.as_path(), &resource)?;
} }
@ -135,7 +135,7 @@ pub async fn download_libraries(
future::join_all( future::join_all(
libraries libraries
.iter() .iter()
.map(|x| download_library(libraries_path, natives_path, x)), .map(|library| download_library(libraries_path, natives_path, library)),
) )
.await .await
.into_iter() .into_iter()
@ -150,19 +150,16 @@ async fn download_library(
library: &Library, library: &Library,
) -> Result<(), LauncherError> { ) -> Result<(), LauncherError> {
if let Some(rules) = &library.rules { if let Some(rules) = &library.rules {
if !super::rules::parse_rules(rules.as_slice()) { if !super::rules::parse_rules(rules) {
return Ok(()); return Ok(());
} }
} }
let (a, b) = future::join( future::try_join(
download_library_jar(libraries_path, library), download_library_jar(libraries_path, library),
download_native(natives_path, library), download_native(natives_path, library),
) )
.await; .await?;
a?;
b?;
Ok(()) Ok(())
} }
@ -171,55 +168,53 @@ async fn download_library_jar(
libraries_path: &Path, libraries_path: &Path,
library: &Library, library: &Library,
) -> Result<(), LauncherError> { ) -> Result<(), LauncherError> {
let mut path = libraries_path.to_path_buf(); let artifact_path = get_path_from_artifact(&library.name)?;
path.push(get_path_from_artifact(&*library.name)?); let path = libraries_path.join(&artifact_path);
if let Some(downloads) = &library.downloads { if let Some(downloads) = &library.downloads {
if let Some(library) = &downloads.artifact { if let Some(library) = &downloads.artifact {
save_and_download_file(&*path, &library.url, Some(&library.sha1)).await?; save_and_download_file(&path, &library.url, Some(&library.sha1)).await?;
} }
} else { } else {
save_and_download_file( let url = format!(
&*path, "{}{artifact_path}",
&format!( library
"{}{}", .url
library .as_deref()
.url .unwrap_or("https://libraries.minecraft.net/"),
.as_deref() );
.unwrap_or("https://libraries.minecraft.net/"), save_and_download_file(&path, &url, None).await?;
get_path_from_artifact(&*library.name)?
),
None,
)
.await?;
} }
Ok(()) Ok(())
} }
async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> { async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> {
if let Some(natives) = &library.natives { use daedalus::minecraft::LibraryDownload;
if let Some(os_key) = natives.get(&get_os()) { use std::collections::HashMap;
if let Some(downloads) = &library.downloads {
if let Some(classifiers) = &downloads.classifiers {
#[cfg(target_pointer_width = "64")]
let parsed_key = os_key.replace("${arch}", "64");
#[cfg(target_pointer_width = "32")]
let parsed_key = os_key.replace("${arch}", "32");
if let Some(native) = classifiers.get(&*parsed_key) { // Try blocks in stable Rust when?
let file = download_file(&native.url, Some(&native.sha1)).await?; let optional_cascade = || -> Option<(&String, &HashMap<String, LibraryDownload>)> {
let os_key = library.natives.as_ref()?.get(&get_os())?;
let classifiers = library.downloads.as_ref()?.classifiers.as_ref()?;
Some((os_key, classifiers))
};
let reader = std::io::Cursor::new(&*file); if let Some((os_key, classifiers)) = optional_cascade() {
#[cfg(target_pointer_width = "64")]
let parsed_key = os_key.replace("${arch}", "64");
#[cfg(target_pointer_width = "32")]
let parsed_key = os_key.replace("${arch}", "32");
let mut archive = zip::ZipArchive::new(reader).unwrap(); if let Some(native) = classifiers.get(&parsed_key) {
archive.extract(natives_path).unwrap(); let file = download_file(&native.url, Some(&native.sha1)).await?;
}
} let reader = std::io::Cursor::new(&file);
}
let mut archive = zip::ZipArchive::new(reader).unwrap();
archive.extract(natives_path).unwrap();
} }
} }
Ok(()) Ok(())
} }
@ -228,27 +223,23 @@ async fn save_and_download_file(
url: &str, url: &str,
sha1: Option<&str>, sha1: Option<&str>,
) -> Result<bytes::Bytes, LauncherError> { ) -> Result<bytes::Bytes, LauncherError> {
let read = std::fs::read(path).ok().map(bytes::Bytes::from); match std::fs::read(path) {
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
if let Some(bytes) = read { Err(_) => {
Ok(bytes) let file = download_file(url, sha1).await?;
} else { save_file(path, &file)?;
let file = download_file(url, sha1).await?; Ok(file)
}
save_file(path, &file)?;
Ok(file)
} }
} }
fn save_file(path: &Path, bytes: &bytes::Bytes) -> Result<(), std::io::Error> { fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
let mut file = File::create(path)?; let mut file = File::create(path)?;
file.write_all(bytes)?; file.write_all(bytes)?;
Ok(()) Ok(())
} }
@ -263,7 +254,7 @@ pub fn get_os() -> Os {
pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, LauncherError> { pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, LauncherError> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.tcp_keepalive(Some(std::time::Duration::from_secs(10))) .tcp_keepalive(Some(Duration::from_secs(10)))
.build() .build()
.map_err(|err| LauncherError::FetchError { .map_err(|err| LauncherError::FetchError {
inner: err, inner: err,
@ -311,12 +302,11 @@ pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes
} }
} }
} }
unreachable!() unreachable!()
} }
/// Computes a checksum of the input bytes /// Computes a checksum of the input bytes
pub async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> { async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?; let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?;
Ok(hash) Ok(hash)

View File

@ -1,30 +1,17 @@
use crate::launcher::LauncherError; use crate::launcher::LauncherError;
use lazy_static::lazy_static;
use regex::Regex;
use std::process::Command; use std::process::Command;
lazy_static! {
static ref JAVA_VERSION_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap();
}
pub fn check_java() -> Result<Option<String>, LauncherError> { pub fn check_java() -> Result<String, LauncherError> {
let child = Command::new("java") let child = Command::new("java")
.arg("-version") .arg("-version")
.output() .output()
.map_err(|err| LauncherError::ProcessError { .map_err(|inner| LauncherError::ProcessError {
inner: err, inner,
process: "java".to_string(), process: "java".into(),
})?; })?;
let output = &*String::from_utf8_lossy(&*child.stderr); let output = String::from_utf8_lossy(&child.stderr);
let output = output.trim_matches('\"');
if let Some(version_raw) = JAVA_VERSION_REGEX.find(output) { Ok(output.into())
let mut raw = version_raw.as_str().chars();
raw.next();
raw.next_back();
return Ok(Some(raw.as_str().to_string()));
}
Ok(None)
} }

View File

@ -5,11 +5,7 @@
#![warn(missing_docs, unused_import_braces, missing_debug_implementations)] #![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
use std::path::Path; static LAUNCHER_WORK_DIR: &'static str = "./launcher";
lazy_static::lazy_static! {
pub static ref LAUNCHER_WORK_DIR: &'static Path = Path::new("./launcher");
}
mod data; mod data;
pub mod launcher; pub mod launcher;
@ -29,7 +25,7 @@ pub enum Error {
} }
pub async fn init() -> Result<(), Error> { pub async fn init() -> Result<(), Error> {
std::fs::create_dir_all(*LAUNCHER_WORK_DIR).expect("Unable to create launcher root directory!"); std::fs::create_dir_all(LAUNCHER_WORK_DIR).expect("Unable to create launcher root directory!");
crate::data::Metadata::init().await?; crate::data::Metadata::init().await?;
crate::data::Settings::init().await?; crate::data::Settings::init().await?;

View File

@ -1,5 +1,4 @@
use std::collections::HashSet; use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::convert::TryFrom; use std::convert::TryFrom;
@ -10,48 +9,47 @@ use super::{pack, ModpackError, ModpackResult};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const DEFAULT_FORMAT_VERSION: u32 = 1; pub const DEFAULT_FORMAT_VERSION: u32 = 1;
const MODRINTH_GAMEDATA_URL: &str = "https://staging-cdn.modrinth.com/gamedata";
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Manifest<'a> { pub struct Manifest {
pub format_version: u32, pub format_version: u32,
pub game: &'a str, pub game: String,
pub version_id: &'a str, pub version_id: String,
pub name: &'a str, pub name: String,
#[serde(borrow)] pub summary: Option<String>,
pub summary: Option<&'a str>, pub files: Vec<ManifestFile>,
pub files: Vec<ManifestFile<'a>>, pub dependencies: ManifestDeps,
pub dependencies: ManifestDeps<'a>,
} }
impl TryFrom<Manifest<'_>> for pack::Modpack { impl TryFrom<Manifest> for pack::Modpack {
type Error = ModpackError; type Error = ModpackError;
fn try_from(manifest: Manifest<'_>) -> Result<Self, Self::Error> { fn try_from(manifest: Manifest) -> Result<Self, Self::Error> {
let files = manifest let files = manifest
.files .files
.into_iter() .into_iter()
.map(pack::ModpackFile::try_from) .map(pack::ModpackFile::try_from)
.collect::<ModpackResult<HashSet<pack::ModpackFile>>>()?; .collect::<ModpackResult<_>>()?;
Ok(Self { Ok(Self {
name: String::from(manifest.name), name: manifest.name,
version: String::from(manifest.version_id), version: manifest.version_id,
summary: manifest.summary.map(String::from), summary: manifest.summary,
game: ModpackGame::from(manifest.dependencies), game: ModpackGame::from(manifest.dependencies),
files, files,
}) })
} }
} }
const MODRINTH_GAMEDATA_URL: &str = "https://staging-cdn.modrinth.com/gamedata";
fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String> { fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String> {
let source = match loader { let source = match loader {
ModLoader::Vanilla => Err(ModpackError::VersionError(String::from( ModLoader::Vanilla => Err(ModpackError::VersionError(String::from(
"Attempted to get mod loader version of Vanilla", "Attempted to get mod loader version of Vanilla",
))), ))),
ModLoader::Forge => Ok(format!("{}/forge/v0/manifest.json", MODRINTH_GAMEDATA_URL)), ModLoader::Forge => Ok(format!("{MODRINTH_GAMEDATA_URL}/forge/v0/manifest.json")),
ModLoader::Fabric => Ok(format!("{}/fabric/v0/manifest.json", MODRINTH_GAMEDATA_URL)), ModLoader::Fabric => Ok(format!("{MODRINTH_GAMEDATA_URL}/fabric/v0/manifest.json")),
}?; }?;
let manifest = futures::executor::block_on(daedalus::modded::fetch_manifest(&source))?; let manifest = futures::executor::block_on(daedalus::modded::fetch_manifest(&source))?;
@ -63,96 +61,90 @@ fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String>
.flatten() .flatten()
.ok_or_else(|| { .ok_or_else(|| {
ModpackError::VersionError(format!( ModpackError::VersionError(format!(
"No versions of modloader {:?} exist for Minecraft {}", "No versions of modloader {loader:?} exist for Minecraft {version}",
loader, version
)) ))
})? })?
.id .id)
.clone())
} }
impl<'a> TryFrom<&'a pack::Modpack> for Manifest<'a> { impl TryFrom<pack::Modpack> for Manifest {
type Error = ModpackError; type Error = ModpackError;
fn try_from(pack: &'a pack::Modpack) -> Result<Self, Self::Error> { fn try_from(pack: pack::Modpack) -> Result<Self, Self::Error> {
let game_field: &'a str = match pack.game { let pack::Modpack {
ModpackGame::Minecraft(..) => "minecraft", game,
version,
name,
summary,
files,
} = pack;
let game = match game {
ModpackGame::Minecraft(..) => "minecraft".into(),
}; };
let files = pack let files: Vec<_> = pack.files.into_iter().map(ManifestFile::from).collect();
.files
.iter()
.map(ManifestFile::from)
.collect::<Vec<ManifestFile>>();
Ok(Manifest { Ok(Manifest {
format_version: DEFAULT_FORMAT_VERSION, format_version: DEFAULT_FORMAT_VERSION,
game: game_field, game,
version_id: &pack.version, version_id: version,
name: &pack.name, name,
summary: pack.summary.as_deref(), summary,
files, files,
dependencies: ManifestDeps::try_from(&pack.game)?, dependencies: ManifestDeps::try_from(pack.game)?,
}) })
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ManifestFile<'a> { pub struct ManifestFile {
#[serde(borrow)] pub path: PathBuf,
pub path: &'a Path, pub hashes: Option<ManifestHashes>,
pub hashes: Option<ManifestHashes<'a>>,
#[serde(default)] #[serde(default)]
pub env: ManifestEnvs, pub env: ManifestEnvs,
#[serde(borrow)] pub downloads: Vec<String>,
pub downloads: Vec<&'a str>,
} }
impl TryFrom<ManifestFile<'_>> for pack::ModpackFile { impl TryFrom<ManifestFile> for pack::ModpackFile {
type Error = ModpackError; type Error = ModpackError;
fn try_from(file: ManifestFile<'_>) -> Result<Self, Self::Error> { fn try_from(file: ManifestFile) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
path: PathBuf::from(file.path), path: file.path,
hashes: file.hashes.map(pack::ModpackFileHashes::from), hashes: file.hashes.map(pack::ModpackFileHashes::from),
env: pack::ModpackEnv::try_from(file.env)?, env: pack::ModpackEnv::try_from(file.env)?,
downloads: file.downloads.into_iter().map(ToOwned::to_owned).collect(), downloads: file.downloads.into_iter().collect(),
}) })
} }
} }
impl<'a> From<&'a pack::ModpackFile> for ManifestFile<'a> { impl From<pack::ModpackFile> for ManifestFile {
fn from(file: &'a pack::ModpackFile) -> Self { fn from(file: pack::ModpackFile) -> Self {
Self { Self {
path: file.path.as_path(), path: file.path,
hashes: file.hashes.as_ref().map(ManifestHashes::from), hashes: file.hashes.map(ManifestHashes::from),
env: file.env.into(), env: file.env.into(),
downloads: file downloads: file.downloads.into_iter().collect(),
.downloads
.iter()
.map(String::as_str)
.collect::<Vec<&str>>(),
} }
} }
} }
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ManifestHashes<'a> { pub struct ManifestHashes {
pub sha1: &'a str, pub sha1: String,
} }
impl From<ManifestHashes<'_>> for pack::ModpackFileHashes { impl From<ManifestHashes> for pack::ModpackFileHashes {
fn from(hashes: ManifestHashes<'_>) -> Self { fn from(hashes: ManifestHashes) -> Self {
Self { Self { sha1: hashes.sha1 }
sha1: String::from(hashes.sha1),
}
} }
} }
impl<'a> From<&'a pack::ModpackFileHashes> for ManifestHashes<'a> { impl From<pack::ModpackFileHashes> for ManifestHashes {
fn from(hashes: &'a pack::ModpackFileHashes) -> Self { fn from(hashes: pack::ModpackFileHashes) -> Self {
Self { sha1: &hashes.sha1 } Self { sha1: hashes.sha1 }
} }
} }
@ -213,55 +205,47 @@ impl From<pack::ModpackEnv> for ManifestEnvs {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
// HACK: I've tried for hours to get this working zero-copy, but I'm beat. If someone else wants to pub enum ManifestDeps {
// go through the #<!! of implementing it, be my guest.
pub enum ManifestDeps<'a> {
MinecraftFabric { MinecraftFabric {
minecraft: &'a str, minecraft: String,
#[serde(rename = "fabric-loader")] #[serde(rename = "fabric-loader")]
fabric_loader: String, fabric_loader: String,
}, },
MinecraftForge { MinecraftForge {
minecraft: &'a str, minecraft: String,
forge: String, forge: String,
}, },
MinecraftVanilla { MinecraftVanilla {
minecraft: &'a str, minecraft: String,
}, },
} }
impl From<ManifestDeps<'_>> for pack::ModpackGame { impl From<ManifestDeps> for pack::ModpackGame {
fn from(deps: ManifestDeps<'_>) -> Self { fn from(deps: ManifestDeps) -> Self {
use ManifestDeps::*; use ManifestDeps::*;
match deps { match deps {
MinecraftVanilla { minecraft } => { MinecraftVanilla { minecraft } => Self::Minecraft(minecraft, ModLoader::Vanilla),
Self::Minecraft(String::from(minecraft), ModLoader::Vanilla) MinecraftFabric { minecraft, .. } => Self::Minecraft(minecraft, ModLoader::Fabric),
} MinecraftForge { minecraft, .. } => Self::Minecraft(minecraft, ModLoader::Forge),
MinecraftFabric { minecraft, .. } => {
Self::Minecraft(String::from(minecraft), ModLoader::Fabric)
}
MinecraftForge { minecraft, .. } => {
Self::Minecraft(String::from(minecraft), ModLoader::Forge)
}
} }
} }
} }
impl<'a> TryFrom<&'a pack::ModpackGame> for ManifestDeps<'a> { impl TryFrom<pack::ModpackGame> for ManifestDeps {
type Error = ModpackError; type Error = ModpackError;
fn try_from(game: &'a pack::ModpackGame) -> Result<Self, Self::Error> { fn try_from(game: pack::ModpackGame) -> Result<Self, Self::Error> {
use super::pack::ModpackGame::*; use super::pack::ModpackGame::*;
Ok(match game { Ok(match game {
Minecraft(ref ver, ModLoader::Vanilla) => Self::MinecraftVanilla { minecraft: ver }, Minecraft(minecraft, ModLoader::Vanilla) => Self::MinecraftVanilla { minecraft },
Minecraft(ref ver, loader @ ModLoader::Fabric) => Self::MinecraftFabric { Minecraft(minecraft, ModLoader::Fabric) => Self::MinecraftFabric {
minecraft: ver, minecraft,
fabric_loader: get_loader_version(*loader, ver)?, fabric_loader: get_loader_version(ModLoader::Fabric, &minecraft)?,
}, },
Minecraft(ref ver, loader @ ModLoader::Forge) => Self::MinecraftForge { Minecraft(minecraft, ModLoader::Forge) => Self::MinecraftForge {
minecraft: ver, minecraft,
forge: get_loader_version(*loader, ver)?, forge: get_loader_version(ModLoader::Fabric, &minecraft)?,
}, },
}) })
} }
@ -287,13 +271,13 @@ mod tests {
"#; "#;
let expected_manifest = Manifest { let expected_manifest = Manifest {
format_version: 1, format_version: 1,
game: "minecraft", game: "minecraft".into(),
version_id: "deadbeef", version_id: "deadbeef".into(),
name: "Example Pack", name: "Example Pack".into(),
summary: None, summary: None,
files: Vec::new(), files: vec![],
dependencies: ManifestDeps::MinecraftVanilla { dependencies: ManifestDeps::MinecraftVanilla {
minecraft: "1.17.1", minecraft: "1.17.1".into(),
}, },
}; };
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
@ -329,21 +313,21 @@ mod tests {
"#; "#;
let expected_manifest = Manifest { let expected_manifest = Manifest {
format_version: 1, format_version: 1,
game: "minecraft", game: "minecraft".into(),
version_id: "deadbeef", version_id: "deadbeef".into(),
name: "Example Pack", name: "Example Pack".into(),
summary: None, summary: None,
files: vec![ManifestFile { files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"), path: "mods/testmod.jar".into(),
hashes: Some(ManifestHashes { hashes: Some(ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
}), }),
env: ManifestEnvs::default(), env: ManifestEnvs::default(),
downloads: vec!["https://example.com/testmod.jar"], downloads: vec!["https://example.com/testmod.jar".into()],
}], }],
dependencies: ManifestDeps::MinecraftForge { dependencies: ManifestDeps::MinecraftForge {
minecraft: "1.17.1", minecraft: "1.17.1".into(),
forge: String::from("37.0.110"), forge: "37.0.110".into(),
}, },
}; };
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
@ -379,21 +363,21 @@ mod tests {
"#; "#;
let expected_manifest = Manifest { let expected_manifest = Manifest {
format_version: 1, format_version: 1,
game: "minecraft", game: "minecraft".into(),
version_id: "deadbeef", version_id: "deadbeef".into(),
name: "Example Pack", name: "Example Pack".into(),
summary: None, summary: None,
files: vec![ManifestFile { files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"), path: "mods/testmod.jar".into(),
hashes: Some(ManifestHashes { hashes: Some(ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
}), }),
env: ManifestEnvs::default(), env: ManifestEnvs::default(),
downloads: vec!["https://example.com/testmod.jar"], downloads: vec!["https://example.com/testmod.jar".into()],
}], }],
dependencies: ManifestDeps::MinecraftFabric { dependencies: ManifestDeps::MinecraftFabric {
minecraft: "1.17.1", minecraft: "1.17.1".into(),
fabric_loader: String::from("0.9.0"), fabric_loader: "0.9.0".into(),
}, },
}; };
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
@ -434,24 +418,24 @@ mod tests {
"#; "#;
let expected_manifest = Manifest { let expected_manifest = Manifest {
format_version: 1, format_version: 1,
game: "minecraft", game: "minecraft".into(),
version_id: "deadbeef", version_id: "deadbeef".into(),
name: "Example Pack", name: "Example Pack".into(),
summary: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."), summary: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".into()),
files: vec![ManifestFile { files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"), path: "mods/testmod.jar".into(),
hashes: Some(ManifestHashes { hashes: Some(ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
}), }),
env: ManifestEnvs { env: ManifestEnvs {
client: ManifestEnv::Required, client: ManifestEnv::Required,
server: ManifestEnv::Unsupported, server: ManifestEnv::Unsupported,
}, },
downloads: vec!["https://example.com/testmod.jar"], downloads: vec!["https://example.com/testmod.jar".into()],
}], }],
dependencies: ManifestDeps::MinecraftForge { dependencies: ManifestDeps::MinecraftForge {
minecraft: "1.17.1", minecraft: "1.17.1".into(),
forge: String::from("37.0.110"), forge: "37.0.110".into(),
}, },
}; };
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");

View File

@ -2,7 +2,6 @@
use daedalus::download_file; use daedalus::download_file;
use fs_extra::dir::CopyOptions; use fs_extra::dir::CopyOptions;
use serde::Deserialize;
use std::{convert::TryFrom, env, io, path::Path}; use std::{convert::TryFrom, env, io, path::Path};
use tokio::{fs, try_join}; use tokio::{fs, try_join};
use uuid::Uuid; use uuid::Uuid;
@ -23,6 +22,10 @@ pub const COMPILED_ZIP: &str = "compiled.mrpack";
pub const MANIFEST_PATH: &str = "modrinth.index.json"; pub const MANIFEST_PATH: &str = "modrinth.index.json";
pub const OVERRIDES_PATH: &str = "overrides/"; pub const OVERRIDES_PATH: &str = "overrides/";
pub const PACK_JSON5_PATH: &str = "modpack.json5"; pub const PACK_JSON5_PATH: &str = "modpack.json5";
const PACK_GITIGNORE: &'static str = const_format::formatcp!(r#"
{COMPILED_PATH}
{COMPILED_ZIP}
"#);
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum ModpackError { pub enum ModpackError {
@ -86,7 +89,8 @@ pub async fn realise_modpack_zip(
dest: &Path, dest: &Path,
side: pack::ModpackSide, side: pack::ModpackSide,
) -> ModpackResult<()> { ) -> ModpackResult<()> {
let tmp = env::temp_dir().join(format!("theseus-{}/", Uuid::new_v4())); let mut tmp = env::temp_dir();
tmp.push(format!("theseus-{}/", Uuid::new_v4()));
archive.extract(&tmp)?; archive.extract(&tmp)?;
realise_modpack(&tmp, dest, side).await realise_modpack(&tmp, dest, side).await
} }
@ -102,7 +106,7 @@ pub async fn realise_modpack(
"Output is not a directory", "Output is not a directory",
))); )));
} }
if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) { if std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) {
return Err(ModpackError::InvalidDirectory(String::from( return Err(ModpackError::InvalidDirectory(String::from(
"Output directory is non-empty", "Output directory is non-empty",
))); )));
@ -112,22 +116,22 @@ pub async fn realise_modpack(
} }
// Copy overrides // Copy overrides
let overrides = Some(dir.join(OVERRIDES_PATH)).filter(|it| it.exists() && it.is_dir()); let overrides = dir.join(OVERRIDES_PATH);
if let Some(overrides) = overrides { if overrides.is_dir() {
fs_extra::dir::copy(overrides, dest, &CopyOptions::new())?; fs_extra::dir::copy(overrides, dest, &CopyOptions::new())?;
} }
// Parse manifest // Parse manifest
// NOTE: I'm using standard files here, since Serde does not support async readers // NOTE: I'm using standard files here, since Serde does not support async readers
let manifest_path = Some(dir.join(MANIFEST_PATH)) let manifest_path = Some(dir.join(MANIFEST_PATH))
.filter(|it| it.exists() && it.is_file()) .filter(|it| it.is_file())
.ok_or_else(|| { .ok_or_else(|| {
ModpackError::ManifestError(String::from("Manifest missing or is not a file")) ModpackError::ManifestError(String::from("Manifest missing or is not a file"))
})?; })?;
let manifest_file = std::fs::File::open(manifest_path)?; let manifest_file = std::fs::File::open(manifest_path)?;
let reader = io::BufReader::new(manifest_file); let reader = io::BufReader::new(manifest_file);
let mut deserializer = serde_json::Deserializer::from_reader(reader);
let manifest = Manifest::deserialize(&mut deserializer)?; let manifest: Manifest = serde_json::from_reader(reader)?;
let modpack = Modpack::try_from(manifest)?; let modpack = Modpack::try_from(manifest)?;
// Realise modpack // Realise modpack
@ -137,14 +141,7 @@ pub async fn realise_modpack(
pub fn to_pack_json5(pack: &Modpack) -> ModpackResult<String> { pub fn to_pack_json5(pack: &Modpack) -> ModpackResult<String> {
let json5 = json5::to_string(pack)?; let json5 = json5::to_string(pack)?;
Ok(format!("// This modpack is managed using Theseus. It can be edited using either a Theseus-compatible launcher or manually.\n{}", json5)) Ok(format!("// This modpack is managed using Theseus. It can be edited using either a Theseus-compatible launcher or manually.\n{json5}"))
}
lazy_static::lazy_static! {
static ref PACK_GITIGNORE: String = format!(r#"
{0}
{1}
"#, COMPILED_PATH, COMPILED_ZIP);
} }
pub async fn create_modpack( pub async fn create_modpack(
@ -158,7 +155,7 @@ pub async fn create_modpack(
try_join!( try_join!(
fs::create_dir(&output_dir), fs::create_dir(&output_dir),
fs::create_dir(output_dir.join(OVERRIDES_PATH)), fs::create_dir(output_dir.join(OVERRIDES_PATH)),
fs::write(output_dir.join(".gitignore"), PACK_GITIGNORE.as_str()), fs::write(output_dir.join(".gitignore"), PACK_GITIGNORE),
fs::write(output_dir.join(PACK_JSON5_PATH), to_pack_json5(&pack)?), fs::write(output_dir.join(PACK_JSON5_PATH), to_pack_json5(&pack)?),
)?; )?;
@ -177,7 +174,7 @@ pub async fn compile_modpack(dir: &Path) -> ModpackResult<()> {
&CopyOptions::new(), &CopyOptions::new(),
)?; )?;
} }
let manifest = Manifest::try_from(&pack)?; let manifest = Manifest::try_from(pack)?;
fs::write( fs::write(
result_dir.join(MANIFEST_PATH), result_dir.join(MANIFEST_PATH),
serde_json::to_string(&manifest)?, serde_json::to_string(&manifest)?,

View File

@ -28,42 +28,35 @@ pub trait ModrinthAPI {
pub struct ModrinthV1(pub String); pub struct ModrinthV1(pub String);
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ModrinthV1Project<'a> { struct ModrinthV1Project {
title: &'a str, title: String,
client_side: &'a str, client_side: String,
server_side: &'a str, server_side: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ModrinthV1ProjectVersion<'a> { struct ModrinthV1ProjectVersion {
#[serde(borrow)] dependencies: HashSet<String>,
dependencies: HashSet<&'a str>, game_versions: HashSet<String>,
#[serde(borrow)] version_type: String,
game_versions: HashSet<&'a str>, files: Vec<ModrinthV1ProjectVersionFile>,
version_type: &'a str, loaders: HashSet<String>,
files: Vec<ModrinthV1ProjectVersionFile<'a>>,
#[serde(borrow)]
loaders: HashSet<&'a str>,
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
struct ModrinthV1ProjectVersionFile<'a> { struct ModrinthV1ProjectVersionFile {
hashes: ManifestHashes<'a>, hashes: ManifestHashes,
url: &'a str, url: String,
filename: &'a str, filename: String,
} }
impl From<ModrinthV1ProjectVersionFile<'_>> for ModpackFile { impl From<ModrinthV1ProjectVersionFile> for ModpackFile {
fn from(file: ModrinthV1ProjectVersionFile<'_>) -> Self { fn from(file: ModrinthV1ProjectVersionFile) -> Self {
Self { Self {
hashes: Some(ModpackFileHashes::from(file.hashes)), hashes: Some(ModpackFileHashes::from(file.hashes)),
downloads: { downloads: HashSet::from([file.url]),
let mut downloads: HashSet<String> = HashSet::new();
downloads.insert(String::from(file.url));
downloads
},
path: PathBuf::from(file.filename), path: PathBuf::from(file.filename),
// WARNING: Since the sidedness of version 1 API requests is unknown, the environemnt is // WARNING: Since the sidedness of version 1 API requests is unknown, the environment is
// set here as both. // set here as both.
env: ModpackEnv::Both, env: ModpackEnv::Both,
} }
@ -78,10 +71,11 @@ impl ModrinthAPI for ModrinthV1 {
channel: &str, channel: &str,
game: &ModpackGame, game: &ModpackGame,
) -> ModpackResult<HashSet<ModpackFile>> { ) -> ModpackResult<HashSet<ModpackFile>> {
let domain = &self.0;
// Fetch metadata // Fetch metadata
let (project_json, versions_json): (Bytes, Bytes) = try_join!( let (project_json, versions_json): (Bytes, Bytes) = try_join!(
try_get_json(format!("{}/api/v1/mod/{}", self.0, project)), try_get_json(format!("{domain}/api/v1/mod/{project}")),
try_get_json(format!("{}/api/v1/mod/{}/version", self.0, project)), try_get_json(format!("{domain}/api/v1/mod/{project}/version")),
)?; )?;
let (mut project_deserializer, mut versions_deserializer) = ( let (mut project_deserializer, mut versions_deserializer) = (
@ -113,8 +107,8 @@ impl ModrinthAPI for ModrinthV1 {
ModLoader::Vanilla => unreachable!(), ModLoader::Vanilla => unreachable!(),
}; };
it.version_type == channel it.version_type == channel
&& it.game_versions.contains(&game_version.as_str()) && it.game_versions.contains(game_version)
&& it.loaders.contains(&loader_str) && it.loaders.contains(loader_str)
}) })
.ok_or_else(|| { .ok_or_else(|| {
ModpackError::VersionError(format!( ModpackError::VersionError(format!(
@ -125,8 +119,8 @@ impl ModrinthAPI for ModrinthV1 {
// Project fields // Project fields
let envs = ModpackEnv::try_from(ManifestEnvs { let envs = ModpackEnv::try_from(ManifestEnvs {
client: serde_json::from_str(project.client_side)?, client: serde_json::from_str(&project.client_side)?,
server: serde_json::from_str(project.server_side)?, server: serde_json::from_str(&project.server_side)?,
})?; })?;
// Conversions // Conversions
@ -155,7 +149,8 @@ impl ModrinthAPI for ModrinthV1 {
} }
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>> { async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>> {
let version_json = try_get_json(format!("{}/api/v1/version/{}", self.0, version)).await?; let domain = &self.0;
let version_json = try_get_json(format!("{domain}/api/v1/version/{version}")).await?;
let mut version_deserializer = serde_json::Deserializer::from_slice(&version_json); let mut version_deserializer = serde_json::Deserializer::from_slice(&version_json);
let version = ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?; let version = ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
let base_path = PathBuf::from("mods/"); let base_path = PathBuf::from("mods/");
@ -164,7 +159,7 @@ impl ModrinthAPI for ModrinthV1 {
.files .files
.into_iter() .into_iter()
.map(ModpackFile::from) .map(ModpackFile::from)
.collect::<HashSet<ModpackFile>>()) .collect::<HashSet<_>>())
} }
} }

View File

@ -144,6 +144,7 @@ impl Modpack {
} }
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ModpackGame { pub enum ModpackGame {
// TODO: Currently, the launcher does not support specifying mod loader versions, so I just // TODO: Currently, the launcher does not support specifying mod loader versions, so I just
// store the loader here. // store the loader here.
@ -256,9 +257,9 @@ mod tests {
let mut files = HashSet::new(); let mut files = HashSet::new();
files.insert(ModpackFile { files.insert(ModpackFile {
path: PathBuf::from("mods/gravestones-v1.9.jar"), path: PathBuf::from("mods/gravestones-v1.9.jar"),
hashes: ModpackFileHashes { hashes: Some(ModpackFileHashes {
sha1: String::from("3f0f6d523d218460310b345be03ab3f1d294e04d"), sha1: String::from("3f0f6d523d218460310b345be03ab3f1d294e04d"),
}, }),
env: ModpackEnv::Both, env: ModpackEnv::Both,
downloads: { downloads: {
let mut downloads = HashSet::new(); let mut downloads = HashSet::new();