From 54cd2f873c67c71734debdf88a08f5e82323bf25 Mon Sep 17 00:00:00 2001 From: Jai A Date: Fri, 9 Jul 2021 20:05:04 -0700 Subject: [PATCH] Add error handling --- .idea/theseus.iml | 4 + Cargo.lock | 7 + theseus/Cargo.toml | 1 + theseus/src/launcher/args.rs | 156 +++++++++++----- theseus/src/launcher/auth.rs | 298 ++++++++++++++++--------------- theseus/src/launcher/download.rs | 284 +++++++++++++++++++++-------- theseus/src/launcher/java.rs | 17 +- theseus/src/launcher/meta.rs | 4 +- theseus/src/launcher/mod.rs | 95 +++++++--- theseus/src/launcher/rules.rs | 15 +- theseus/src/lib.rs | 13 +- theseus_cli/src/main.rs | 28 +-- 12 files changed, 586 insertions(+), 336 deletions(-) diff --git a/.idea/theseus.iml b/.idea/theseus.iml index e70532b3a..9cd854a33 100644 --- a/.idea/theseus.iml +++ b/.idea/theseus.iml @@ -42,6 +42,10 @@ + + + + diff --git a/Cargo.lock b/Cargo.lock index e7dbc8669..f88a687a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -894,6 +894,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -972,6 +978,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sha1", "sys-info", "thiserror", "tokio", diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index 8d3a2acbe..c4d8a9f57 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4", features = ["serde"] } uuid = { version = "0.8", features = ["serde", "v4"] } bytes = "1" zip = "0.5" +sha1 = { version = "0.6.0", features = ["std"]} regex = "1.5" lazy_static = "1.4" diff --git a/theseus/src/launcher/args.rs b/theseus/src/launcher/args.rs index a68abca05..a7e9b47d4 100644 --- a/theseus/src/launcher/args.rs +++ b/theseus/src/launcher/args.rs @@ -1,9 +1,15 @@ +use crate::launcher::auth::provider::Credentials; use crate::launcher::meta::{Argument, ArgumentValue, Library, Os, VersionType}; use crate::launcher::rules::parse_rules; +use crate::launcher::LauncherError; use std::path::Path; use uuid::Uuid; -pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path: &Path) -> String { +pub fn get_class_paths( + libraries_path: &Path, + libraries: &[Library], + client_path: &Path, +) -> Result { let mut class_paths = Vec::new(); for library in libraries { @@ -16,13 +22,28 @@ pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path let name_items = library.name.split(':').collect::>(); - let package = name_items.get(0).unwrap(); - let name = name_items.get(1).unwrap(); - let version = name_items.get(2).unwrap(); + let package = name_items.get(0).ok_or_else(|| { + LauncherError::ParseError(format!( + "Unable to find package for library {}", + &library.name + )) + })?; + let name = name_items.get(1).ok_or_else(|| { + LauncherError::ParseError(format!( + "Unable to find name for library {}", + &library.name + )) + })?; + let version = name_items.get(2).ok_or_else(|| { + LauncherError::ParseError(format!( + "Unable to find version for library {}", + &library.name + )) + })?; let mut path = libraries_path.to_path_buf(); - for directory in package.split(".") { + for directory in package.split('.') { path.push(directory); } @@ -32,7 +53,12 @@ pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path class_paths.push( std::fs::canonicalize(&path) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Library file at path {} does not exist", + path.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ) @@ -41,33 +67,41 @@ pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path class_paths.push( std::fs::canonicalize(&client_path) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Specified client path {} does not exist", + client_path.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ); - class_paths.join(match super::download::get_os() { + Ok(class_paths.join(match super::download::get_os() { Os::Osx | Os::Linux | Os::Unknown => ":", Os::Windows => ";", - }) + })) } pub fn get_jvm_arguments( arguments: Option<&[Argument]>, natives_path: &Path, class_paths: &str, -) -> Vec { +) -> Result, LauncherError> { let mut parsed_arguments = Vec::new(); if let Some(args) = arguments { parse_arguments(args, &mut parsed_arguments, |arg| { parse_jvm_argument(arg, natives_path, class_paths) - }); + })?; } else { parsed_arguments.push(format!( "-Djava.library.path={}", &*std::fs::canonicalize(natives_path) - .unwrap() + .map_err(|_| LauncherError::InvalidInput(format!( + "Specified natives path {} does not exist", + natives_path.to_string_lossy() + )))? .to_string_lossy() .to_string() )); @@ -75,74 +109,83 @@ pub fn get_jvm_arguments( parsed_arguments.push(class_paths.to_string()); } - parsed_arguments + Ok(parsed_arguments) } -fn parse_jvm_argument(argument: &str, natives_path: &Path, class_paths: &str) -> String { - argument +fn parse_jvm_argument( + argument: &str, + natives_path: &Path, + class_paths: &str, +) -> Result { + Ok(argument .replace( "${natives_directory}", &*std::fs::canonicalize(natives_path) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Specified natives path {} does not exist", + natives_path.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ) .replace("${launcher_name}", "theseus") .replace("${launcher_version}", env!("CARGO_PKG_VERSION")) - .replace("${classpath}", class_paths) + .replace("${classpath}", class_paths)) } +#[allow(clippy::too_many_arguments)] pub fn get_minecraft_arguments( arguments: Option<&[Argument]>, legacy_arguments: Option<&str>, - access_token: &str, - username: &str, - uuid: &Uuid, + credentials: &Credentials, version: &str, asset_index_name: &str, game_directory: &Path, assets_directory: &Path, version_type: &VersionType, -) -> Vec { +) -> Result, LauncherError> { if let Some(arguments) = arguments { let mut parsed_arguments = Vec::new(); parse_arguments(arguments, &mut parsed_arguments, |arg| { parse_minecraft_argument( arg, - access_token, - username, - uuid, + &*credentials.access_token, + &*credentials.username, + &credentials.id, version, asset_index_name, game_directory, assets_directory, version_type, ) - }); + })?; - parsed_arguments + Ok(parsed_arguments) } else if let Some(legacy_arguments) = legacy_arguments { - parse_minecraft_argument( + Ok(parse_minecraft_argument( legacy_arguments, - access_token, - username, - uuid, + &*credentials.access_token, + &*credentials.username, + &credentials.id, version, asset_index_name, game_directory, assets_directory, version_type, - ) - .split(" ") + )? + .split(' ') .into_iter() .map(|x| x.to_string()) - .collect() + .collect()) } else { - Vec::new() + Ok(Vec::new()) } } +#[allow(clippy::too_many_arguments)] fn parse_minecraft_argument( argument: &str, access_token: &str, @@ -153,8 +196,8 @@ fn parse_minecraft_argument( game_directory: &Path, assets_directory: &Path, version_type: &VersionType, -) -> String { - argument +) -> Result { + Ok(argument .replace("${auth_access_token}", access_token) .replace("${auth_session}", access_token) .replace("${auth_player_name}", username) @@ -166,37 +209,56 @@ fn parse_minecraft_argument( .replace( "${game_directory}", &*std::fs::canonicalize(game_directory) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Specified game directory {} does not exist", + game_directory.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ) .replace( "${assets_root}", &*std::fs::canonicalize(assets_directory) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Specified assets directory {} does not exist", + assets_directory.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ) .replace( "${game_assets}", &*std::fs::canonicalize(assets_directory) - .unwrap() + .map_err(|_| { + LauncherError::InvalidInput(format!( + "Specified assets directory {} does not exist", + assets_directory.to_string_lossy() + )) + })? .to_string_lossy() .to_string(), ) - .replace("${version_type}", version_type.as_str()) + .replace("${version_type}", version_type.as_str())) } -fn parse_arguments(arguments: &[Argument], parsed_arguments: &mut Vec, parse_function: F) +fn parse_arguments( + arguments: &[Argument], + parsed_arguments: &mut Vec, + parse_function: F, +) -> Result<(), LauncherError> where - F: Fn(&str) -> String, + F: Fn(&str) -> Result, { for argument in arguments { match argument { Argument::Normal(arg) => { - let parsed = parse_function(arg); + let parsed = parse_function(arg)?; - for arg in parsed.split(" ") { + for arg in parsed.split(' ') { parsed_arguments.push(arg.to_string()); } } @@ -204,11 +266,11 @@ where if parse_rules(rules.as_slice()) { match value { ArgumentValue::Single(arg) => { - //parsed_arguments.push(parse_function(arg)); + parsed_arguments.push(parse_function(arg)?); } ArgumentValue::Many(args) => { for arg in args { - //parsed_arguments.push(parse_function(arg)); + parsed_arguments.push(parse_function(arg)?); } } } @@ -216,4 +278,6 @@ where } } } + + Ok(()) } diff --git a/theseus/src/launcher/auth.rs b/theseus/src/launcher/auth.rs index 68e5c2b68..0981010d6 100644 --- a/theseus/src/launcher/auth.rs +++ b/theseus/src/launcher/auth.rs @@ -1,162 +1,178 @@ -use serde::{Deserialize, Serialize}; -use uuid::Uuid; +pub mod api { + use serde::{Deserialize, Serialize}; + use uuid::Uuid; -#[derive(Debug, Serialize, Deserialize)] -pub struct GameProfile { - pub id: Uuid, - pub name: String, -} + #[derive(Debug, Serialize, Deserialize)] + pub struct GameProfile { + pub id: Uuid, + pub name: String, + } -#[derive(Debug, Deserialize)] -pub struct UserProperty { - pub name: String, - pub value: String, -} + #[derive(Debug, Deserialize)] + pub struct UserProperty { + pub name: String, + pub value: String, + } -#[derive(Debug, Deserialize)] -pub struct User { - pub id: String, - pub username: String, - pub properties: Option>, -} + #[derive(Debug, Deserialize)] + pub struct User { + pub id: String, + pub username: String, + pub properties: Option>, + } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AuthenticateResponse { - pub user: Option, - pub client_token: Uuid, - pub access_token: String, - pub available_profiles: Vec, - pub selected_profile: Option, -} + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct AuthenticateResponse { + pub user: Option, + pub client_token: Uuid, + pub access_token: String, + pub available_profiles: Vec, + pub selected_profile: Option, + } -pub async fn login(username: &str, password: &str, request_user: bool) -> AuthenticateResponse { - let client = reqwest::Client::new(); + pub async fn login( + username: &str, + password: &str, + request_user: bool, + ) -> Result { + let client = reqwest::Client::new(); - client - .post("https://authserver.mojang.com/authenticate") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::json!( - { - "agent": { - "name": "Minecraft", - "version": 1 - }, - "username": username, - "password": password, - "clientToken": Uuid::new_v4(), - "requestUser": request_user - } + client + .post("https://authserver.mojang.com/authenticate") + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::json!( + { + "agent": { + "name": "Minecraft", + "version": 1 + }, + "username": username, + "password": password, + "clientToken": Uuid::new_v4(), + "requestUser": request_user + } + ) + .to_string(), ) - .to_string(), - ) - .send() - .await - .unwrap() - .json() - .await - .unwrap() -} + .send() + .await? + .json() + .await + } -pub async fn sign_out(username: &str, password: &str) { - let client = reqwest::Client::new(); + pub async fn sign_out(username: &str, password: &str) -> Result<(), reqwest::Error> { + let client = reqwest::Client::new(); - client - .post("https://authserver.mojang.com/signout") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::json!( - { - "username": username, - "password": password - } + client + .post("https://authserver.mojang.com/signout") + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::json!( + { + "username": username, + "password": password + } + ) + .to_string(), ) - .to_string(), - ) - .send() - .await - .unwrap(); -} + .send() + .await?; -pub async fn validate(access_token: &str, client_token: &str) { - let client = reqwest::Client::new(); + Ok(()) + } - client - .post("https://authserver.mojang.com/validate") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::json!( - { - "accessToken": access_token, - "clientToken": client_token - } + pub async fn validate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> { + let client = reqwest::Client::new(); + + client + .post("https://authserver.mojang.com/validate") + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::json!( + { + "accessToken": access_token, + "clientToken": client_token + } + ) + .to_string(), ) - .to_string(), - ) - .send() - .await - .unwrap(); -} + .send() + .await?; -pub async fn invalidate(access_token: &str, client_token: &str) { - let client = reqwest::Client::new(); + Ok(()) + } - client - .post("https://authserver.mojang.com/invalidate") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::json!( - { - "accessToken": access_token, - "clientToken": client_token - } + pub async fn invalidate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> { + let client = reqwest::Client::new(); + + client + .post("https://authserver.mojang.com/invalidate") + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::json!( + { + "accessToken": access_token, + "clientToken": client_token + } + ) + .to_string(), ) - .to_string(), - ) - .send() - .await - .unwrap(); -} + .send() + .await?; -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RefreshResponse { - pub user: Option, - pub client_token: Uuid, - pub access_token: String, - pub selected_profile: Option, -} + Ok(()) + } -pub async fn refresh( - access_token: &str, - client_token: &str, - selected_profile: &GameProfile, - request_user: bool, -) -> RefreshResponse { - let client = reqwest::Client::new(); + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct RefreshResponse { + pub user: Option, + pub client_token: Uuid, + pub access_token: String, + pub selected_profile: Option, + } - client - .post("https://authserver.mojang.com/refresh") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body( - serde_json::json!( - { - "accessToken": access_token, - "clientToken": client_token, - "selectedProfile": { - "id": selected_profile.id, - "name": selected_profile.name, - }, - "requestUser": request_user, - } + pub async fn refresh( + access_token: &str, + client_token: &str, + selected_profile: &GameProfile, + request_user: bool, + ) -> Result { + let client = reqwest::Client::new(); + + client + .post("https://authserver.mojang.com/refresh") + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body( + serde_json::json!( + { + "accessToken": access_token, + "clientToken": client_token, + "selectedProfile": { + "id": selected_profile.id, + "name": selected_profile.name, + }, + "requestUser": request_user, + } + ) + .to_string(), ) - .to_string(), - ) - .send() - .await - .unwrap() - .json() - .await - .unwrap() + .send() + .await? + .json() + .await + } +} + +pub mod provider { + use uuid::Uuid; + + #[derive(Debug)] + pub struct Credentials { + pub id: Uuid, + pub username: String, + pub access_token: String, + } } diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index 488f173be..b26007a45 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -1,106 +1,178 @@ use crate::launcher::meta::{ - Asset, AssetIndex, AssetsIndex, DownloadType, Library, Os, OsRule, RuleAction, VersionInfo, + fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType, Library, Os, Version, + VersionInfo, }; +use crate::launcher::LauncherError; use futures::future; -use regex::Regex; -use reqwest::{Error, Response}; use std::fs::File; use std::io::{BufReader, Write}; use std::path::Path; -pub async fn download_client(client_path: &Path, version_info: &VersionInfo) { - let client = download_file( - &version_info - .downloads - .get(&DownloadType::Client) - .unwrap() - .url, - ) - .await; +pub async fn download_version_info( + client_path: &Path, + version: &Version, +) -> Result { + let path = &*client_path + .join(&version.id) + .join(format!("{}.json", &version.id)); - save_file( - &*client_path - .join(&version_info.id) - .join(format!("{}.jar", &version_info.id)), - &client, - ); - save_file( - &*client_path - .join(&version_info.id) - .join(format!("{}.json", &version_info.id)), - &bytes::Bytes::from(serde_json::to_string(version_info).unwrap()), - ); + if path.exists() { + Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + } else { + let info = fetch_version_info(version) + .await + .map_err(|err| LauncherError::FetchError { + inner: err, + item: "version info".to_string(), + })?; + + save_file(path, &bytes::Bytes::from(serde_json::to_string(&info)?))?; + + Ok(info) + } +} + +pub async fn download_client( + client_path: &Path, + version_info: &VersionInfo, +) -> Result<(), LauncherError> { + let client_download = version_info + .downloads + .get(&DownloadType::Client) + .ok_or_else(|| { + LauncherError::InvalidInput(format!( + "Version {} does not have any client downloads", + &version_info.id + )) + })?; + + let path = &*client_path + .join(&version_info.id) + .join(format!("{}.jar", &version_info.id)); + + save_and_download_file(path, &client_download.url, &client_download.sha1).await?; + + Ok(()) +} + +pub async fn download_assets_index( + assets_path: &Path, + version: &VersionInfo, +) -> Result { + let path = &*assets_path + .join("indexes") + .join(format!("{}.json", &version.asset_index.id)); + + if path.exists() { + Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + } else { + let index = fetch_assets_index(version) + .await + .map_err(|err| LauncherError::FetchError { + inner: err, + item: "assets index".to_string(), + })?; + + save_file(path, &bytes::Bytes::from(serde_json::to_string(&index)?))?; + + Ok(index) + } } pub async fn download_assets( assets_path: &Path, legacy_path: Option<&Path>, - meta: &AssetIndex, index: &AssetsIndex, -) { - save_file( - &*assets_path - .join("indexes") - .join(format!("{}.json", meta.id)), - &bytes::Bytes::from(serde_json::to_string(index).unwrap()), - ); - +) -> Result<(), LauncherError> { future::join_all( index .objects .iter() .map(|x| download_asset(assets_path, legacy_path, x.0, x.1)), ) - .await; + .await + .into_iter() + .collect::, LauncherError>>()?; + + Ok(()) } async fn download_asset( assets_path: &Path, legacy_path: Option<&Path>, - name: &String, + name: &str, asset: &Asset, -) { +) -> Result<(), LauncherError> { let sub_hash = &&asset.hash[..2]; - let resource = download_file(&format!( - "https://resources.download.minecraft.net/{}/{}", - sub_hash, asset.hash - )) - .await; - let resource_path = assets_path.join("objects").join(sub_hash).join(&asset.hash); - save_file(resource_path.as_path(), &resource); + + let resource = save_and_download_file( + &*resource_path, + &format!( + "https://resources.download.minecraft.net/{}/{}", + sub_hash, asset.hash + ), + &*asset.hash, + ) + .await?; if let Some(legacy_path) = legacy_path { let resource_path = 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)?; } + + Ok(()) } -pub async fn download_libraries(libraries_path: &Path, natives_path: &Path, libraries: &[Library]) { +pub async fn download_libraries( + libraries_path: &Path, + natives_path: &Path, + libraries: &[Library], +) -> Result<(), LauncherError> { future::join_all( libraries .iter() .map(|x| download_library(libraries_path, natives_path, x)), ) - .await; + .await + .into_iter() + .collect::, LauncherError>>()?; + + Ok(()) } -async fn download_library(libraries_path: &Path, natives_path: &Path, library: &Library) { +async fn download_library( + libraries_path: &Path, + natives_path: &Path, + library: &Library, +) -> Result<(), LauncherError> { if let Some(rules) = &library.rules { if !super::rules::parse_rules(rules.as_slice()) { - return; + return Ok(()); } } let name_items = library.name.split(':').collect::>(); - let package = name_items.get(0).unwrap(); - let name = name_items.get(1).unwrap(); - let version = name_items.get(2).unwrap(); + let package = name_items.get(0).ok_or_else(|| { + LauncherError::ParseError(format!( + "Unable to find package for library {}", + &library.name + )) + })?; + let name = name_items.get(1).ok_or_else(|| { + LauncherError::ParseError(format!("Unable to find name for library {}", &library.name)) + })?; + let version = name_items.get(2).ok_or_else(|| { + LauncherError::ParseError(format!( + "Unable to find version for library {}", + &library.name + )) + })?; - future::join( + let (a, b) = future::join( download_library_jar(libraries_path, library, package, name, version), download_native( libraries_path, @@ -112,6 +184,11 @@ async fn download_library(libraries_path: &Path, natives_path: &Path, library: & ), ) .await; + + a?; + b?; + + Ok(()) } async fn download_library_jar( @@ -120,13 +197,11 @@ async fn download_library_jar( package: &str, name: &str, version: &str, -) { +) -> Result<(), LauncherError> { if let Some(library) = &library.downloads.artifact { - let bytes = download_file(&library.url).await; - let mut path = libraries_path.to_path_buf(); - for directory in package.split(".") { + for directory in package.split('.') { path.push(directory); } @@ -134,8 +209,10 @@ async fn download_library_jar( path.push(version); path.push(format!("{}-{}.jar", name, version)); - save_file(&path, &bytes); + save_and_download_file(&*path, &library.url, &library.sha1).await?; } + + Ok(()) } async fn download_native( @@ -145,7 +222,7 @@ async fn download_native( package: &str, name: &str, version: &str, -) { +) -> Result<(), LauncherError> { if let Some(natives) = &library.natives { if let Some(os_key) = natives.get(&get_os()) { if let Some(classifiers) = &library.downloads.classifiers { @@ -157,7 +234,7 @@ async fn download_native( if let Some(native) = classifiers.get(&*parsed_key) { let mut path = libraries_path.to_path_buf(); - for directory in package.split(".") { + for directory in package.split('.') { path.push(directory); } @@ -165,9 +242,7 @@ async fn download_native( path.push(version); path.push(format!("{}-{}-{}.jar", name, version, parsed_key)); - let bytes = download_file(&native.url).await; - - save_file(&path, &bytes); + save_and_download_file(&*path, &native.url, &native.sha1).await?; let file = File::open(&path).unwrap(); let reader = BufReader::new(file); @@ -178,34 +253,99 @@ async fn download_native( } } } + + Ok(()) } -fn save_file(path: &Path, bytes: &bytes::Bytes) { - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - let mut file = File::create(path).unwrap(); - file.write_all(bytes).unwrap(); +async fn save_and_download_file( + path: &Path, + url: &str, + sha1: &str, +) -> Result { + let read = std::fs::read(path).ok().map(bytes::Bytes::from); + + if let Some(bytes) = read { + Ok(bytes) + } else { + let file = download_file(url, Some(sha1)).await?; + + save_file(path, &file)?; + + Ok(file) + } } -async fn download_file(url: &str) -> bytes::Bytes { +fn save_file(path: &Path, bytes: &bytes::Bytes) -> Result<(), std::io::Error> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let mut file = File::create(path)?; + file.write_all(bytes)?; + + Ok(()) +} + +async fn download_file(url: &str, sha1: Option<&str>) -> Result { let client = reqwest::Client::builder() - .pool_max_idle_per_host(0) .tcp_keepalive(Some(std::time::Duration::from_secs(10))) .build() - .unwrap(); + .map_err(|err| LauncherError::FetchError { + inner: err, + item: url.to_string(), + })?; for attempt in 1..4 { let result = client.get(url).send().await; match result { - Ok(x) => return x.bytes().await.unwrap(), - Err(e) if attempt <= 3 => continue, - Err(e) => panic!(e), + Ok(x) => { + let bytes = x.bytes().await; + + if let Ok(bytes) = bytes { + if let Some(sha1) = sha1 { + if &*get_hash(bytes.clone()).await? != sha1 { + if attempt <= 3 { + continue; + } else { + return Err(LauncherError::ChecksumFailure { + hash: sha1.to_string(), + url: url.to_string(), + tries: attempt, + }); + } + } + } + + return Ok(bytes); + } else if attempt <= 3 { + continue; + } else if let Err(err) = bytes { + return Err(LauncherError::FetchError { + inner: err, + item: url.to_string(), + }); + } + } + Err(_) if attempt <= 3 => continue, + Err(err) => { + return Err(LauncherError::FetchError { + inner: err, + item: url.to_string(), + }) + } } } unreachable!() } +async fn get_hash(bytes: bytes::Bytes) -> Result { + let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?; + + Ok(hash) +} + pub fn get_os() -> Os { match std::env::consts::OS { "windows" => Os::Windows, diff --git a/theseus/src/launcher/java.rs b/theseus/src/launcher/java.rs index addf274e0..8848d66bf 100644 --- a/theseus/src/launcher/java.rs +++ b/theseus/src/launcher/java.rs @@ -1,21 +1,20 @@ +use crate::launcher::LauncherError; use lazy_static::lazy_static; use regex::Regex; use std::process::Command; -#[derive(thiserror::Error, Debug)] -pub enum JavaError { - #[error("System Error")] - SystemError(#[from] std::io::Error), -} - lazy_static! { static ref JAVA_VERSION_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap(); } -pub fn check_java() -> Result, JavaError> { - let child = Command::new("/usr/lib/jvm/java-8-openjdk/jre/bin/java") +pub fn check_java() -> Result, LauncherError> { + let child = Command::new("java") .arg("-version") - .output()?; + .output() + .map_err(|err| LauncherError::ProcessError { + inner: err, + process: "java".to_string(), + })?; let output = &*String::from_utf8_lossy(&*child.stderr); diff --git a/theseus/src/launcher/meta.rs b/theseus/src/launcher/meta.rs index bbf5475f2..ac964b400 100644 --- a/theseus/src/launcher/meta.rs +++ b/theseus/src/launcher/meta.rs @@ -119,14 +119,14 @@ pub struct OsRule { #[derive(Serialize, Deserialize, Debug)] pub struct FeatureRule { pub is_demo_user: Option, - pub has_custom_resolution: Option, + pub has_demo_resolution: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct Rule { pub action: RuleAction, pub os: Option, - pub feature: Option, + pub features: Option, } #[derive(Serialize, Deserialize, Debug)] diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 1fc4a82fa..2b7994eae 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -1,34 +1,65 @@ +use crate::launcher::auth::provider::Credentials; use std::path::Path; use std::process::{Command, Stdio}; +use thiserror::Error; -mod args; +pub mod args; pub mod auth; pub mod download; pub mod java; pub mod meta; -mod rules; +pub mod rules; -pub async fn launch_minecraft(version_name: &str, root_dir: &Path) { +#[derive(Error, Debug)] +pub enum LauncherError { + #[error("Failed to violate file checksum at url {url} with hash {hash} after {tries} tries")] + ChecksumFailure { + hash: String, + url: String, + tries: u32, + }, + #[error("Invalid input: {0}")] + InvalidInput(String), + #[error("Error while managing asynchronous tasks")] + TaskError(#[from] tokio::task::JoinError), + #[error("Error while reading/writing to the disk")] + IoError(#[from] std::io::Error), + #[error("Error while spawning child process {process}")] + ProcessError { + inner: std::io::Error, + process: String, + }, + #[error("Error while deserializing JSON")] + SerdeError(#[from] serde_json::Error), + #[error("Unable to fetch {item}")] + FetchError { inner: reqwest::Error, item: String }, + #[error("{0}")] + ParseError(String), +} + +pub async fn launch_minecraft( + version_name: &str, + root_dir: &Path, + credentials: &Credentials, +) -> Result<(), LauncherError> { let manifest = meta::fetch_version_manifest().await.unwrap(); - let version = meta::fetch_version_info( + let version = download::download_version_info( + &*root_dir.join("versions"), manifest .versions .iter() .find(|x| x.id == version_name) - .unwrap(), + .ok_or_else(|| { + LauncherError::InvalidInput(format!("Version {} does not exist", version_name)) + })?, ) - .await - .unwrap(); + .await?; - //download_minecraft(&version, root_dir).await; - - let auth = auth::login("username", "password", true).await; + download_minecraft(&version, root_dir).await?; let arguments = version.arguments.unwrap(); - let profile = auth.selected_profile.unwrap(); - let mut child = Command::new("java") .args(args::get_jvm_arguments( arguments @@ -42,38 +73,47 @@ pub async fn launch_minecraft(version_name: &str, root_dir: &Path) { .join("versions") .join(&version.id) .join(format!("{}.jar", &version.id)), - ), - )) + )?, + )?) .arg(version.main_class) .args(args::get_minecraft_arguments( arguments .get(&meta::ArgumentType::Game) .map(|x| x.as_slice()), version.minecraft_arguments.as_deref(), - &*auth.access_token, - &*profile.name, - &profile.id, + credentials, &*version.id, &version.asset_index.id, root_dir, &*root_dir.join("assets"), &version.type_, - )) + )?) .current_dir(root_dir) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn() - .unwrap(); + .map_err(|err| LauncherError::ProcessError { + inner: err, + process: "minecraft".to_string(), + })?; - child.wait().unwrap(); + child.wait().map_err(|err| LauncherError::ProcessError { + inner: err, + process: "minecraft".to_string(), + })?; + + Ok(()) } -pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) { - let assets_dir = meta::fetch_assets_index(&version).await.unwrap(); +pub async fn download_minecraft( + version: &meta::VersionInfo, + root_dir: &Path, +) -> Result<(), LauncherError> { + let assets_index = download::download_assets_index(&*root_dir.join("assets"), &version).await?; let legacy_dir = root_dir.join("resources"); - futures::future::join3( + let (a, b, c) = futures::future::join3( download::download_client(&*root_dir.join("versions"), &version), download::download_assets( &*root_dir.join("assets"), @@ -82,8 +122,7 @@ pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) { } else { None }, - &version.asset_index, - &assets_dir, + &assets_index, ), download::download_libraries( &*root_dir.join("libraries"), @@ -92,4 +131,10 @@ pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) { ), ) .await; + + a?; + b?; + c?; + + Ok(()) } diff --git a/theseus/src/launcher/rules.rs b/theseus/src/launcher/rules.rs index 9e275461b..1970c8852 100644 --- a/theseus/src/launcher/rules.rs +++ b/theseus/src/launcher/rules.rs @@ -9,16 +9,16 @@ pub fn parse_rules(rules: &[Rule]) -> bool { pub fn parse_rule(rule: &Rule) -> bool { let result = if let Some(os) = &rule.os { parse_os_rule(os) - } else if let Some(feature) = &rule.feature { + } else if rule.features.is_some() { false } else { true }; - return match rule.action { + match rule.action { RuleAction::Allow => result, RuleAction::Disallow => !result, - }; + } } pub fn parse_os_rule(rule: &OsRule) -> bool { @@ -42,9 +42,12 @@ pub fn parse_os_rule(rule: &OsRule) -> bool { } } if let Some(version) = &rule.version { - let regex = Regex::new(version.as_str()).unwrap(); - if !regex.is_match(&*sys_info::os_release().unwrap()) { - return false; + let regex = Regex::new(version.as_str()); + + if let Ok(regex) = regex { + if !regex.is_match(&*sys_info::os_release().unwrap_or_default()) { + return false; + } } } diff --git a/theseus/src/lib.rs b/theseus/src/lib.rs index 5ec4e61d9..da52d023b 100644 --- a/theseus/src/lib.rs +++ b/theseus/src/lib.rs @@ -1,11 +1,8 @@ +//! # Theseus +//! +//! Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs, +//! and launching Modrinth mod packs + #![warn(missing_docs, unused_import_braces, missing_debug_implementations)] pub mod launcher; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} diff --git a/theseus_cli/src/main.rs b/theseus_cli/src/main.rs index 3c1488e0e..7f755fb76 100644 --- a/theseus_cli/src/main.rs +++ b/theseus_cli/src/main.rs @@ -1,28 +1,2 @@ -use futures::{executor, future}; -use std::path::Path; -use theseus::launcher::launch_minecraft; -use theseus::launcher::meta::ArgumentType; - #[tokio::main] -async fn main() { - launch_minecraft("1.15.2", &Path::new("./test")).await; - - // let mut thing1 = theseus::launcher::meta::fetch_version_manifest() - // .await - // .unwrap(); - // - // future::join_all(thing1.versions.iter().map(|x| async move { - // //println!("{}", x.url); - // let version = theseus::launcher::meta::fetch_version_info(x) - // .await - // .unwrap(); - // - // if let Some(args) = &version.minecraft_arguments { - // println!("{:?}", args); - // } - // if let Some(args) = &version.arguments { - // println!("{:?}", args.get(&ArgumentType::Game).unwrap()); - // } - // })) - // .await; -} +async fn main() {}