Add error handling

This commit is contained in:
Jai A
2021-07-09 20:05:04 -07:00
parent a0e35ad853
commit 54cd2f873c
12 changed files with 586 additions and 336 deletions

4
.idea/theseus.iml generated
View File

@@ -42,6 +42,10 @@
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crc32fast-f112d4af2349618a/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/crc32fast-f112d4af2349618a/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/miniz_oxide-d683d4a97661215e/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/miniz_oxide-d683d4a97661215e/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/sys-info-0c720b463af8b14b/out" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/target/debug/build/sys-info-0c720b463af8b14b/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-epoch-809784aaf7fd3933/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-queue-96bec372a2ebc7a5/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/crossbeam-utils-bab62be590a5955d/out" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/memoffset-235ac8b3550fb50a/out" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

7
Cargo.lock generated
View File

@@ -894,6 +894,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "sha1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.0" version = "1.4.0"
@@ -972,6 +978,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sha1",
"sys-info", "sys-info",
"thiserror", "thiserror",
"tokio", "tokio",

View File

@@ -16,6 +16,7 @@ chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
bytes = "1" bytes = "1"
zip = "0.5" zip = "0.5"
sha1 = { version = "0.6.0", features = ["std"]}
regex = "1.5" regex = "1.5"
lazy_static = "1.4" lazy_static = "1.4"

View File

@@ -1,9 +1,15 @@
use crate::launcher::auth::provider::Credentials;
use crate::launcher::meta::{Argument, ArgumentValue, Library, Os, VersionType}; use crate::launcher::meta::{Argument, ArgumentValue, Library, Os, VersionType};
use crate::launcher::rules::parse_rules; use crate::launcher::rules::parse_rules;
use crate::launcher::LauncherError;
use std::path::Path; use std::path::Path;
use uuid::Uuid; 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<String, LauncherError> {
let mut class_paths = Vec::new(); let mut class_paths = Vec::new();
for library in libraries { 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::<Vec<&str>>(); let name_items = library.name.split(':').collect::<Vec<&str>>();
let package = name_items.get(0).unwrap(); let package = name_items.get(0).ok_or_else(|| {
let name = name_items.get(1).unwrap(); LauncherError::ParseError(format!(
let version = name_items.get(2).unwrap(); "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(); let mut path = libraries_path.to_path_buf();
for directory in package.split(".") { for directory in package.split('.') {
path.push(directory); path.push(directory);
} }
@@ -32,7 +53,12 @@ pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path
class_paths.push( class_paths.push(
std::fs::canonicalize(&path) 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_lossy()
.to_string(), .to_string(),
) )
@@ -41,33 +67,41 @@ pub fn get_class_paths(libraries_path: &Path, libraries: &[Library], client_path
class_paths.push( class_paths.push(
std::fs::canonicalize(&client_path) 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_lossy()
.to_string(), .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::Osx | Os::Linux | Os::Unknown => ":",
Os::Windows => ";", Os::Windows => ";",
}) }))
} }
pub fn get_jvm_arguments( pub fn get_jvm_arguments(
arguments: Option<&[Argument]>, arguments: Option<&[Argument]>,
natives_path: &Path, natives_path: &Path,
class_paths: &str, class_paths: &str,
) -> Vec<String> { ) -> Result<Vec<String>, LauncherError> {
let mut parsed_arguments = Vec::new(); let mut parsed_arguments = Vec::new();
if let Some(args) = arguments { if let Some(args) = arguments {
parse_arguments(args, &mut parsed_arguments, |arg| { parse_arguments(args, &mut parsed_arguments, |arg| {
parse_jvm_argument(arg, natives_path, class_paths) parse_jvm_argument(arg, natives_path, class_paths)
}); })?;
} else { } else {
parsed_arguments.push(format!( parsed_arguments.push(format!(
"-Djava.library.path={}", "-Djava.library.path={}",
&*std::fs::canonicalize(natives_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_lossy()
.to_string() .to_string()
)); ));
@@ -75,74 +109,83 @@ pub fn get_jvm_arguments(
parsed_arguments.push(class_paths.to_string()); 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 { fn parse_jvm_argument(
argument argument: &str,
natives_path: &Path,
class_paths: &str,
) -> Result<String, LauncherError> {
Ok(argument
.replace( .replace(
"${natives_directory}", "${natives_directory}",
&*std::fs::canonicalize(natives_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_lossy()
.to_string(), .to_string(),
) )
.replace("${launcher_name}", "theseus") .replace("${launcher_name}", "theseus")
.replace("${launcher_version}", env!("CARGO_PKG_VERSION")) .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( pub fn get_minecraft_arguments(
arguments: Option<&[Argument]>, arguments: Option<&[Argument]>,
legacy_arguments: Option<&str>, legacy_arguments: Option<&str>,
access_token: &str, credentials: &Credentials,
username: &str,
uuid: &Uuid,
version: &str, version: &str,
asset_index_name: &str, asset_index_name: &str,
game_directory: &Path, game_directory: &Path,
assets_directory: &Path, assets_directory: &Path,
version_type: &VersionType, version_type: &VersionType,
) -> Vec<String> { ) -> Result<Vec<String>, LauncherError> {
if let Some(arguments) = arguments { if let Some(arguments) = arguments {
let mut parsed_arguments = Vec::new(); let mut parsed_arguments = Vec::new();
parse_arguments(arguments, &mut parsed_arguments, |arg| { parse_arguments(arguments, &mut parsed_arguments, |arg| {
parse_minecraft_argument( parse_minecraft_argument(
arg, arg,
access_token, &*credentials.access_token,
username, &*credentials.username,
uuid, &credentials.id,
version, version,
asset_index_name, asset_index_name,
game_directory, game_directory,
assets_directory, assets_directory,
version_type, version_type,
) )
}); })?;
parsed_arguments Ok(parsed_arguments)
} else if let Some(legacy_arguments) = legacy_arguments { } else if let Some(legacy_arguments) = legacy_arguments {
parse_minecraft_argument( Ok(parse_minecraft_argument(
legacy_arguments, legacy_arguments,
access_token, &*credentials.access_token,
username, &*credentials.username,
uuid, &credentials.id,
version, version,
asset_index_name, asset_index_name,
game_directory, game_directory,
assets_directory, assets_directory,
version_type, version_type,
) )?
.split(" ") .split(' ')
.into_iter() .into_iter()
.map(|x| x.to_string()) .map(|x| x.to_string())
.collect() .collect())
} else { } else {
Vec::new() Ok(Vec::new())
} }
} }
#[allow(clippy::too_many_arguments)]
fn parse_minecraft_argument( fn parse_minecraft_argument(
argument: &str, argument: &str,
access_token: &str, access_token: &str,
@@ -153,8 +196,8 @@ fn parse_minecraft_argument(
game_directory: &Path, game_directory: &Path,
assets_directory: &Path, assets_directory: &Path,
version_type: &VersionType, version_type: &VersionType,
) -> String { ) -> Result<String, LauncherError> {
argument Ok(argument
.replace("${auth_access_token}", access_token) .replace("${auth_access_token}", access_token)
.replace("${auth_session}", access_token) .replace("${auth_session}", access_token)
.replace("${auth_player_name}", username) .replace("${auth_player_name}", username)
@@ -166,37 +209,56 @@ fn parse_minecraft_argument(
.replace( .replace(
"${game_directory}", "${game_directory}",
&*std::fs::canonicalize(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_lossy()
.to_string(), .to_string(),
) )
.replace( .replace(
"${assets_root}", "${assets_root}",
&*std::fs::canonicalize(assets_directory) &*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_lossy()
.to_string(), .to_string(),
) )
.replace( .replace(
"${game_assets}", "${game_assets}",
&*std::fs::canonicalize(assets_directory) &*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_lossy()
.to_string(), .to_string(),
) )
.replace("${version_type}", version_type.as_str()) .replace("${version_type}", version_type.as_str()))
} }
fn parse_arguments<F>(arguments: &[Argument], parsed_arguments: &mut Vec<String>, parse_function: F) fn parse_arguments<F>(
arguments: &[Argument],
parsed_arguments: &mut Vec<String>,
parse_function: F,
) -> Result<(), LauncherError>
where where
F: Fn(&str) -> String, F: Fn(&str) -> Result<String, LauncherError>,
{ {
for argument in arguments { for argument in arguments {
match argument { match argument {
Argument::Normal(arg) => { 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()); parsed_arguments.push(arg.to_string());
} }
} }
@@ -204,11 +266,11 @@ where
if parse_rules(rules.as_slice()) { if parse_rules(rules.as_slice()) {
match value { match value {
ArgumentValue::Single(arg) => { ArgumentValue::Single(arg) => {
//parsed_arguments.push(parse_function(arg)); parsed_arguments.push(parse_function(arg)?);
} }
ArgumentValue::Many(args) => { ArgumentValue::Many(args) => {
for arg in args { for arg in args {
//parsed_arguments.push(parse_function(arg)); parsed_arguments.push(parse_function(arg)?);
} }
} }
} }
@@ -216,4 +278,6 @@ where
} }
} }
} }
Ok(())
} }

View File

@@ -1,162 +1,178 @@
use serde::{Deserialize, Serialize}; pub mod api {
use uuid::Uuid; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct GameProfile { pub struct GameProfile {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct UserProperty { pub struct UserProperty {
pub name: String, pub name: String,
pub value: String, pub value: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
pub id: String, pub id: String,
pub username: String, pub username: String,
pub properties: Option<Vec<UserProperty>>, pub properties: Option<Vec<UserProperty>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AuthenticateResponse { pub struct AuthenticateResponse {
pub user: Option<User>, pub user: Option<User>,
pub client_token: Uuid, pub client_token: Uuid,
pub access_token: String, pub access_token: String,
pub available_profiles: Vec<GameProfile>, pub available_profiles: Vec<GameProfile>,
pub selected_profile: Option<GameProfile>, pub selected_profile: Option<GameProfile>,
} }
pub async fn login(username: &str, password: &str, request_user: bool) -> AuthenticateResponse { pub async fn login(
let client = reqwest::Client::new(); username: &str,
password: &str,
request_user: bool,
) -> Result<AuthenticateResponse, reqwest::Error> {
let client = reqwest::Client::new();
client client
.post("https://authserver.mojang.com/authenticate") .post("https://authserver.mojang.com/authenticate")
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")
.body( .body(
serde_json::json!( serde_json::json!(
{ {
"agent": { "agent": {
"name": "Minecraft", "name": "Minecraft",
"version": 1 "version": 1
}, },
"username": username, "username": username,
"password": password, "password": password,
"clientToken": Uuid::new_v4(), "clientToken": Uuid::new_v4(),
"requestUser": request_user "requestUser": request_user
} }
)
.to_string(),
) )
.to_string(), .send()
) .await?
.send() .json()
.await .await
.unwrap() }
.json()
.await
.unwrap()
}
pub async fn sign_out(username: &str, password: &str) { pub async fn sign_out(username: &str, password: &str) -> Result<(), reqwest::Error> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
client client
.post("https://authserver.mojang.com/signout") .post("https://authserver.mojang.com/signout")
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")
.body( .body(
serde_json::json!( serde_json::json!(
{ {
"username": username, "username": username,
"password": password "password": password
} }
)
.to_string(),
) )
.to_string(), .send()
) .await?;
.send()
.await
.unwrap();
}
pub async fn validate(access_token: &str, client_token: &str) { Ok(())
let client = reqwest::Client::new(); }
client pub async fn validate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
.post("https://authserver.mojang.com/validate") let client = reqwest::Client::new();
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body( client
serde_json::json!( .post("https://authserver.mojang.com/validate")
{ .header(reqwest::header::CONTENT_TYPE, "application/json")
"accessToken": access_token, .body(
"clientToken": client_token serde_json::json!(
} {
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
) )
.to_string(), .send()
) .await?;
.send()
.await
.unwrap();
}
pub async fn invalidate(access_token: &str, client_token: &str) { Ok(())
let client = reqwest::Client::new(); }
client pub async fn invalidate(access_token: &str, client_token: &str) -> Result<(), reqwest::Error> {
.post("https://authserver.mojang.com/invalidate") let client = reqwest::Client::new();
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body( client
serde_json::json!( .post("https://authserver.mojang.com/invalidate")
{ .header(reqwest::header::CONTENT_TYPE, "application/json")
"accessToken": access_token, .body(
"clientToken": client_token serde_json::json!(
} {
"accessToken": access_token,
"clientToken": client_token
}
)
.to_string(),
) )
.to_string(), .send()
) .await?;
.send()
.await
.unwrap();
}
#[derive(Debug, Deserialize)] Ok(())
#[serde(rename_all = "camelCase")] }
pub struct RefreshResponse {
pub user: Option<User>,
pub client_token: Uuid,
pub access_token: String,
pub selected_profile: Option<GameProfile>,
}
pub async fn refresh( #[derive(Debug, Deserialize)]
access_token: &str, #[serde(rename_all = "camelCase")]
client_token: &str, pub struct RefreshResponse {
selected_profile: &GameProfile, pub user: Option<User>,
request_user: bool, pub client_token: Uuid,
) -> RefreshResponse { pub access_token: String,
let client = reqwest::Client::new(); pub selected_profile: Option<GameProfile>,
}
client pub async fn refresh(
.post("https://authserver.mojang.com/refresh") access_token: &str,
.header(reqwest::header::CONTENT_TYPE, "application/json") client_token: &str,
.body( selected_profile: &GameProfile,
serde_json::json!( request_user: bool,
{ ) -> Result<RefreshResponse, reqwest::Error> {
"accessToken": access_token, let client = reqwest::Client::new();
"clientToken": client_token,
"selectedProfile": { client
"id": selected_profile.id, .post("https://authserver.mojang.com/refresh")
"name": selected_profile.name, .header(reqwest::header::CONTENT_TYPE, "application/json")
}, .body(
"requestUser": request_user, 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?
.send() .json()
.await .await
.unwrap() }
.json() }
.await
.unwrap() pub mod provider {
use uuid::Uuid;
#[derive(Debug)]
pub struct Credentials {
pub id: Uuid,
pub username: String,
pub access_token: String,
}
} }

View File

@@ -1,106 +1,178 @@
use crate::launcher::meta::{ 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 futures::future;
use regex::Regex;
use reqwest::{Error, Response};
use std::fs::File; use std::fs::File;
use std::io::{BufReader, Write}; use std::io::{BufReader, Write};
use std::path::Path; use std::path::Path;
pub async fn download_client(client_path: &Path, version_info: &VersionInfo) { pub async fn download_version_info(
let client = download_file( client_path: &Path,
&version_info version: &Version,
.downloads ) -> Result<VersionInfo, LauncherError> {
.get(&DownloadType::Client) let path = &*client_path
.unwrap() .join(&version.id)
.url, .join(format!("{}.json", &version.id));
)
.await;
save_file( if path.exists() {
&*client_path Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
.join(&version_info.id) } else {
.join(format!("{}.jar", &version_info.id)), let info = fetch_version_info(version)
&client, .await
); .map_err(|err| LauncherError::FetchError {
save_file( inner: err,
&*client_path item: "version info".to_string(),
.join(&version_info.id) })?;
.join(format!("{}.json", &version_info.id)),
&bytes::Bytes::from(serde_json::to_string(version_info).unwrap()), 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<AssetsIndex, LauncherError> {
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( pub async fn download_assets(
assets_path: &Path, assets_path: &Path,
legacy_path: Option<&Path>, legacy_path: Option<&Path>,
meta: &AssetIndex,
index: &AssetsIndex, index: &AssetsIndex,
) { ) -> Result<(), LauncherError> {
save_file(
&*assets_path
.join("indexes")
.join(format!("{}.json", meta.id)),
&bytes::Bytes::from(serde_json::to_string(index).unwrap()),
);
future::join_all( future::join_all(
index index
.objects .objects
.iter() .iter()
.map(|x| download_asset(assets_path, legacy_path, x.0, x.1)), .map(|x| download_asset(assets_path, legacy_path, x.0, x.1)),
) )
.await; .await
.into_iter()
.collect::<Result<Vec<()>, LauncherError>>()?;
Ok(())
} }
async fn download_asset( async fn download_asset(
assets_path: &Path, assets_path: &Path,
legacy_path: Option<&Path>, legacy_path: Option<&Path>,
name: &String, name: &str,
asset: &Asset, asset: &Asset,
) { ) -> Result<(), LauncherError> {
let sub_hash = &&asset.hash[..2]; 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); 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 { 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)?;
} }
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( future::join_all(
libraries libraries
.iter() .iter()
.map(|x| download_library(libraries_path, natives_path, x)), .map(|x| download_library(libraries_path, natives_path, x)),
) )
.await; .await
.into_iter()
.collect::<Result<Vec<()>, 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 let Some(rules) = &library.rules {
if !super::rules::parse_rules(rules.as_slice()) { if !super::rules::parse_rules(rules.as_slice()) {
return; return Ok(());
} }
} }
let name_items = library.name.split(':').collect::<Vec<&str>>(); let name_items = library.name.split(':').collect::<Vec<&str>>();
let package = name_items.get(0).unwrap(); let package = name_items.get(0).ok_or_else(|| {
let name = name_items.get(1).unwrap(); LauncherError::ParseError(format!(
let version = name_items.get(2).unwrap(); "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_library_jar(libraries_path, library, package, name, version),
download_native( download_native(
libraries_path, libraries_path,
@@ -112,6 +184,11 @@ async fn download_library(libraries_path: &Path, natives_path: &Path, library: &
), ),
) )
.await; .await;
a?;
b?;
Ok(())
} }
async fn download_library_jar( async fn download_library_jar(
@@ -120,13 +197,11 @@ async fn download_library_jar(
package: &str, package: &str,
name: &str, name: &str,
version: &str, version: &str,
) { ) -> Result<(), LauncherError> {
if let Some(library) = &library.downloads.artifact { if let Some(library) = &library.downloads.artifact {
let bytes = download_file(&library.url).await;
let mut path = libraries_path.to_path_buf(); let mut path = libraries_path.to_path_buf();
for directory in package.split(".") { for directory in package.split('.') {
path.push(directory); path.push(directory);
} }
@@ -134,8 +209,10 @@ async fn download_library_jar(
path.push(version); path.push(version);
path.push(format!("{}-{}.jar", name, 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( async fn download_native(
@@ -145,7 +222,7 @@ async fn download_native(
package: &str, package: &str,
name: &str, name: &str,
version: &str, version: &str,
) { ) -> Result<(), LauncherError> {
if let Some(natives) = &library.natives { if let Some(natives) = &library.natives {
if let Some(os_key) = natives.get(&get_os()) { if let Some(os_key) = natives.get(&get_os()) {
if let Some(classifiers) = &library.downloads.classifiers { if let Some(classifiers) = &library.downloads.classifiers {
@@ -157,7 +234,7 @@ async fn download_native(
if let Some(native) = classifiers.get(&*parsed_key) { if let Some(native) = classifiers.get(&*parsed_key) {
let mut path = libraries_path.to_path_buf(); let mut path = libraries_path.to_path_buf();
for directory in package.split(".") { for directory in package.split('.') {
path.push(directory); path.push(directory);
} }
@@ -165,9 +242,7 @@ async fn download_native(
path.push(version); path.push(version);
path.push(format!("{}-{}-{}.jar", name, version, parsed_key)); path.push(format!("{}-{}-{}.jar", name, version, parsed_key));
let bytes = download_file(&native.url).await; save_and_download_file(&*path, &native.url, &native.sha1).await?;
save_file(&path, &bytes);
let file = File::open(&path).unwrap(); let file = File::open(&path).unwrap();
let reader = BufReader::new(file); let reader = BufReader::new(file);
@@ -178,34 +253,99 @@ async fn download_native(
} }
} }
} }
Ok(())
} }
fn save_file(path: &Path, bytes: &bytes::Bytes) { async fn save_and_download_file(
std::fs::create_dir_all(path.parent().unwrap()).unwrap(); path: &Path,
let mut file = File::create(path).unwrap(); url: &str,
file.write_all(bytes).unwrap(); sha1: &str,
) -> Result<bytes::Bytes, LauncherError> {
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<bytes::Bytes, LauncherError> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.pool_max_idle_per_host(0)
.tcp_keepalive(Some(std::time::Duration::from_secs(10))) .tcp_keepalive(Some(std::time::Duration::from_secs(10)))
.build() .build()
.unwrap(); .map_err(|err| LauncherError::FetchError {
inner: err,
item: url.to_string(),
})?;
for attempt in 1..4 { for attempt in 1..4 {
let result = client.get(url).send().await; let result = client.get(url).send().await;
match result { match result {
Ok(x) => return x.bytes().await.unwrap(), Ok(x) => {
Err(e) if attempt <= 3 => continue, let bytes = x.bytes().await;
Err(e) => panic!(e),
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!() unreachable!()
} }
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?;
Ok(hash)
}
pub fn get_os() -> Os { pub fn get_os() -> Os {
match std::env::consts::OS { match std::env::consts::OS {
"windows" => Os::Windows, "windows" => Os::Windows,

View File

@@ -1,21 +1,20 @@
use crate::launcher::LauncherError;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::process::Command; use std::process::Command;
#[derive(thiserror::Error, Debug)]
pub enum JavaError {
#[error("System Error")]
SystemError(#[from] std::io::Error),
}
lazy_static! { lazy_static! {
static ref JAVA_VERSION_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap(); static ref JAVA_VERSION_REGEX: Regex = Regex::new(r#""(.*?)""#).unwrap();
} }
pub fn check_java() -> Result<Option<String>, JavaError> { pub fn check_java() -> Result<Option<String>, LauncherError> {
let child = Command::new("/usr/lib/jvm/java-8-openjdk/jre/bin/java") let child = Command::new("java")
.arg("-version") .arg("-version")
.output()?; .output()
.map_err(|err| LauncherError::ProcessError {
inner: err,
process: "java".to_string(),
})?;
let output = &*String::from_utf8_lossy(&*child.stderr); let output = &*String::from_utf8_lossy(&*child.stderr);

View File

@@ -119,14 +119,14 @@ pub struct OsRule {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FeatureRule { pub struct FeatureRule {
pub is_demo_user: Option<bool>, pub is_demo_user: Option<bool>,
pub has_custom_resolution: Option<bool>, pub has_demo_resolution: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Rule { pub struct Rule {
pub action: RuleAction, pub action: RuleAction,
pub os: Option<OsRule>, pub os: Option<OsRule>,
pub feature: Option<FeatureRule>, pub features: Option<FeatureRule>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View File

@@ -1,34 +1,65 @@
use crate::launcher::auth::provider::Credentials;
use std::path::Path; use std::path::Path;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use thiserror::Error;
mod args; pub mod args;
pub mod auth; pub mod auth;
pub mod download; pub mod download;
pub mod java; pub mod java;
pub mod meta; 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 manifest = meta::fetch_version_manifest().await.unwrap();
let version = meta::fetch_version_info( let version = download::download_version_info(
&*root_dir.join("versions"),
manifest manifest
.versions .versions
.iter() .iter()
.find(|x| x.id == version_name) .find(|x| x.id == version_name)
.unwrap(), .ok_or_else(|| {
LauncherError::InvalidInput(format!("Version {} does not exist", version_name))
})?,
) )
.await .await?;
.unwrap();
//download_minecraft(&version, root_dir).await; download_minecraft(&version, root_dir).await?;
let auth = auth::login("username", "password", true).await;
let arguments = version.arguments.unwrap(); let arguments = version.arguments.unwrap();
let profile = auth.selected_profile.unwrap();
let mut child = Command::new("java") let mut child = Command::new("java")
.args(args::get_jvm_arguments( .args(args::get_jvm_arguments(
arguments arguments
@@ -42,38 +73,47 @@ pub async fn launch_minecraft(version_name: &str, root_dir: &Path) {
.join("versions") .join("versions")
.join(&version.id) .join(&version.id)
.join(format!("{}.jar", &version.id)), .join(format!("{}.jar", &version.id)),
), )?,
)) )?)
.arg(version.main_class) .arg(version.main_class)
.args(args::get_minecraft_arguments( .args(args::get_minecraft_arguments(
arguments arguments
.get(&meta::ArgumentType::Game) .get(&meta::ArgumentType::Game)
.map(|x| x.as_slice()), .map(|x| x.as_slice()),
version.minecraft_arguments.as_deref(), version.minecraft_arguments.as_deref(),
&*auth.access_token, credentials,
&*profile.name,
&profile.id,
&*version.id, &*version.id,
&version.asset_index.id, &version.asset_index.id,
root_dir, root_dir,
&*root_dir.join("assets"), &*root_dir.join("assets"),
&version.type_, &version.type_,
)) )?)
.current_dir(root_dir) .current_dir(root_dir)
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.spawn() .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) { pub async fn download_minecraft(
let assets_dir = meta::fetch_assets_index(&version).await.unwrap(); 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"); 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_client(&*root_dir.join("versions"), &version),
download::download_assets( download::download_assets(
&*root_dir.join("assets"), &*root_dir.join("assets"),
@@ -82,8 +122,7 @@ pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) {
} else { } else {
None None
}, },
&version.asset_index, &assets_index,
&assets_dir,
), ),
download::download_libraries( download::download_libraries(
&*root_dir.join("libraries"), &*root_dir.join("libraries"),
@@ -92,4 +131,10 @@ pub async fn download_minecraft(version: &meta::VersionInfo, root_dir: &Path) {
), ),
) )
.await; .await;
a?;
b?;
c?;
Ok(())
} }

View File

@@ -9,16 +9,16 @@ pub fn parse_rules(rules: &[Rule]) -> bool {
pub fn parse_rule(rule: &Rule) -> bool { pub fn parse_rule(rule: &Rule) -> bool {
let result = if let Some(os) = &rule.os { let result = if let Some(os) = &rule.os {
parse_os_rule(os) parse_os_rule(os)
} else if let Some(feature) = &rule.feature { } else if rule.features.is_some() {
false false
} else { } else {
true true
}; };
return match rule.action { match rule.action {
RuleAction::Allow => result, RuleAction::Allow => result,
RuleAction::Disallow => !result, RuleAction::Disallow => !result,
}; }
} }
pub fn parse_os_rule(rule: &OsRule) -> bool { 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 { if let Some(version) = &rule.version {
let regex = Regex::new(version.as_str()).unwrap(); let regex = Regex::new(version.as_str());
if !regex.is_match(&*sys_info::os_release().unwrap()) {
return false; if let Ok(regex) = regex {
if !regex.is_match(&*sys_info::os_release().unwrap_or_default()) {
return false;
}
} }
} }

View File

@@ -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)] #![warn(missing_docs, unused_import_braces, missing_debug_implementations)]
pub mod launcher; pub mod launcher;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@@ -1,28 +1,2 @@
use futures::{executor, future};
use std::path::Path;
use theseus::launcher::launch_minecraft;
use theseus::launcher::meta::ArgumentType;
#[tokio::main] #[tokio::main]
async fn 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;
}