summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-02-01 23:06:37 -0600
committerLibravatar bigfoot547 <[email protected]>2025-02-01 23:06:37 -0600
commitc19a1077e85334a3e5ba885a60b03d76409a2b2e (patch)
tree5e726e8180770ac9c2f6c415a0437d6d2c29c226 /src
parentrandom changes (diff)
restructure project
Diffstat (limited to 'src')
-rw-r--r--src/assets.rs86
-rw-r--r--src/auth.rs336
-rw-r--r--src/auth/mcservices.rs92
-rw-r--r--src/auth/msa.rs170
-rw-r--r--src/auth/types.rs130
-rw-r--r--src/auth/types/property_map.rs61
-rw-r--r--src/launcher.rs765
-rw-r--r--src/launcher/assets.rs322
-rw-r--r--src/launcher/constants.rs18
-rw-r--r--src/launcher/download.rs267
-rw-r--r--src/launcher/extract.rs136
-rw-r--r--src/launcher/jre.rs330
-rw-r--r--src/launcher/jre/arch.rs45
-rw-r--r--src/launcher/jre/download.rs195
-rw-r--r--src/launcher/jre/manifest.rs65
-rw-r--r--src/launcher/rules.rs114
-rw-r--r--src/launcher/runner.rs222
-rw-r--r--src/launcher/settings.rs232
-rw-r--r--src/launcher/strsub.rs192
-rw-r--r--src/launcher/version.rs398
-rw-r--r--src/lib.rs5
-rw-r--r--src/util.rs334
-rw-r--r--src/util/progress.rs3
-rw-r--r--src/version.rs489
-rw-r--r--src/version/manifest.rs91
25 files changed, 0 insertions, 5098 deletions
diff --git a/src/assets.rs b/src/assets.rs
deleted file mode 100644
index 15087c9..0000000
--- a/src/assets.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use std::collections::HashMap;
-use std::fmt::Formatter;
-use std::marker::PhantomData;
-use serde::{Deserialize, Deserializer};
-use serde::de::{MapAccess, Visitor};
-use sha1_smol::Digest;
-
-#[derive(Debug, Deserialize)]
-pub struct Asset {
- #[serde(skip)]
- pub name: String,
- pub hash: Digest,
- pub size: usize
-}
-
-#[derive(Debug, Deserialize)]
-pub struct AssetIndex {
- #[serde(rename = "virtual", default)]
- pub virtual_assets: bool,
- #[serde(default)]
- pub map_to_resources: bool,
-
- #[serde(deserialize_with = "deserialize_assets")]
- pub objects: HashMap<String, Asset>
-}
-
-trait SetName {
- fn set_name(&mut self, name: String);
-}
-
-impl SetName for Asset {
- fn set_name(&mut self, name: String) {
- self.name = name;
- }
-}
-
-fn deserialize_assets<'de, D, T>(deserializer: D) -> Result<HashMap<String, T>, D::Error>
-where
- D: Deserializer<'de>,
- T: SetName + Deserialize<'de>
-{
- struct AssetVisitor<T>(PhantomData<T>);
-
- impl<'de, T> Visitor<'de> for AssetVisitor<T>
- where
- T: SetName + Deserialize<'de>
- {
- type Value = HashMap<String, T>;
-
- fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
- formatter.write_str("asset objects map")
- }
-
- fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
- where
- A: MapAccess<'de>,
- {
- let mut out = HashMap::new();
-
- while let Some((key, mut asset)) = map.next_entry::<String, T>()? {
- asset.set_name(key.clone());
- out.insert(key, asset);
- }
-
- Ok(out)
- }
- }
-
- deserializer.deserialize_any(AssetVisitor(PhantomData))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_it() {
- dbg!(serde_json::from_str::<AssetIndex>(r#"{
- "virtual": true,
- "objects": {
- "object1": { "hash": "0d000710b71ca9aafabd8f587768431d0b560b32", "size": 100 },
- "object2/abc": { "hash": "0e000710b71ca9aafabd8f587768431d0b560b32", "size": 10000 }
- }
- }"#).unwrap());
- }
-}
diff --git a/src/auth.rs b/src/auth.rs
deleted file mode 100644
index 057cceb..0000000
--- a/src/auth.rs
+++ /dev/null
@@ -1,336 +0,0 @@
-mod types;
-mod msa;
-mod mcservices;
-
-use std::error::Error;
-use std::fmt::{Display, Formatter};
-use std::future::Future;
-use std::time::{Duration, SystemTime};
-use chrono::{DateTime, TimeDelta, Utc};
-use log::debug;
-use oauth2::{AccessToken, DeviceAuthorizationUrl, DeviceCodeErrorResponse, EndpointNotSet, EndpointSet, HttpClientError, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardRevocableToken, TokenResponse, TokenUrl};
-use oauth2::basic::{BasicErrorResponse, BasicErrorResponseType, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenResponse};
-use reqwest::{IntoUrl, Method, RequestBuilder};
-pub use types::*;
-use crate::auth::msa::{XSTS_RP_MINECRAFT_SERVICES, XSTS_RP_XBOX_LIVE};
-use crate::util::USER_AGENT;
-
-#[derive(Debug)]
-pub enum AuthError {
- // An unexpected error happened while performing a request
- Request { what: &'static str, error: reqwest::Error },
- OAuthRequestToken { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, BasicErrorResponse> },
- OAuthRequestDeviceCode { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse> },
-
- // Some internal auth error (unrecoverable)
- Internal(String),
-
- // Device code auth was cancelled
- Cancel(Option<Box<dyn Error>>),
-
- // Device code auth timed out
- Timeout,
-
- // Requires interactive authentication
- RequireInteractive(&'static str),
-
- // XSTS error
- AuthXError { what: &'static str, x_error: u64, message: Option<String> },
-
- // You don't own the game!
- EntitlementError
-}
-
-impl Display for AuthError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- AuthError::Request { what, error } => write!(f, "auth request error ({}): {}", what, error),
- AuthError::OAuthRequestToken { what, error } => write!(f, "oauth error requesting token ({what}): {error}"),
- AuthError::OAuthRequestDeviceCode { what, error } => write!(f, "oauth error with device code ({what}): {error}"),
- AuthError::Internal(msg) => write!(f, "internal auth error: {}", msg),
- AuthError::Cancel(Some(error)) => write!(f, "operation cancelled: {error}"),
- AuthError::Cancel(None) => f.write_str("operation cancelled"),
- AuthError::Timeout => f.write_str("interactive authentication timed out"),
- AuthError::RequireInteractive(why) => write!(f, "user must log in interactively: {why}"),
- AuthError::AuthXError { what, x_error, message } => write!(f, "XSTS error: {what} ({x_error} -> {})", message.as_ref().map_or("<no message>", |s| s.as_str())),
- AuthError::EntitlementError => f.write_str("no minecraft entitlement (do you own the game?)")
- }
- }
-}
-
-impl Error for AuthError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- AuthError::Request { error, .. } => Some(error),
- AuthError::OAuthRequestToken { error, .. } => Some(error),
- AuthError::OAuthRequestDeviceCode { error, .. } => Some(error),
- AuthError::Cancel(Some(error)) => Some(error.as_ref()),
- _ => None
- }
- }
-}
-
-impl Token {
- fn is_expired(&self, now: DateTime<Utc>) -> bool {
- self.expire.is_some_and(|exp| now >= exp)
- }
-}
-
-macro_rules! create_oauth_client {
- ($is_azure_client_id:expr, $client_id:expr) => {
- oauth2::Client::new($client_id)
- .set_token_uri(TokenUrl::new(if $is_azure_client_id { AZURE_TOKEN_URL.into() } else { NON_AZURE_TOKEN_URL.into() }).expect("hardcoded url"))
- .set_device_authorization_url(DeviceAuthorizationUrl::new(if $is_azure_client_id { AZURE_DEVICE_CODE_URL.into() } else { NON_AZURE_DEVICE_CODE_URL.into() }).expect("hardcoded url"))
- as oauth2::Client<BasicErrorResponse, BasicTokenResponse, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse,
- EndpointNotSet, EndpointSet, EndpointNotSet, EndpointNotSet, EndpointSet>
- }
-}
-
-const AZURE_TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
-const AZURE_DEVICE_CODE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
-const NON_AZURE_TOKEN_URL: &str = "https://login.live.com/oauth20_token.srf";
-const NON_AZURE_DEVICE_CODE_URL: &str = "https://login.live.com/oauth20_connect.srf";
-
-const AZURE_LOGIN_SCOPES: &[&str] = ["XboxLive.signin", "offline_access"].as_slice();
-const NON_AZURE_LOGIN_SCOPES: &[&str] = ["service::user.auth.xboxlive.com::MBI_SSL"].as_slice();
-
-fn build_json_request(client: &reqwest::Client, url: impl IntoUrl, method: Method) -> RequestBuilder {
- client.request(method, url)
- .header(reqwest::header::USER_AGENT, USER_AGENT)
- .header(reqwest::header::ACCEPT, "application/json")
-}
-
-impl MsaUser {
- pub fn create_client() -> reqwest::Client {
- reqwest::ClientBuilder::new()
- .redirect(reqwest::redirect::Policy::none())
- .build().expect("building client should succeed")
- }
-
- fn scopes_iter(&self) -> impl Iterator<Item = Scope> {
- let to_scope = |f: &&str| Scope::new(String::from(*f));
-
- if self.is_azure_client_id {
- AZURE_LOGIN_SCOPES.iter().map(to_scope)
- } else {
- NON_AZURE_LOGIN_SCOPES.iter().map(to_scope)
- }
- }
-
- // uses an access token from, for example, a device code grant logs into xbox live
- async fn xbl_login(&mut self, client: &reqwest::Client, token: &AccessToken) -> Result<(), AuthError> {
- debug!("Logging into xbox live using access token");
- self.xbl_token = Some(msa::xbox_live_login(client, token, self.is_azure_client_id).await?);
-
- Ok(())
- }
-
- // logs into xbox live using a refresh token
- // (panics if no refresh token present)
- async fn xbl_login_refresh(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
- debug!("Using refresh token for XBL login");
- let oauth_client = create_oauth_client!(self.is_azure_client_id, self.client_id.clone());
- let refresh_token = self.refresh_token.as_ref().expect("refresh_access_token called with no refresh token");
-
- let tokenres: BasicTokenResponse = oauth_client
- .exchange_refresh_token(refresh_token)
- .add_scopes(self.scopes_iter())
- .add_extra_param("response_type", "device_code")
- .request_async(client)
- .await.map_err(|e| AuthError::OAuthRequestToken { what: "refresh", error: e })?;
-
- self.refresh_token = tokenres.refresh_token().cloned();
-
- self.xbl_login(client, tokenres.access_token()).await
- }
-
- pub async fn xbl_login_device<D, DF>(&mut self, client: &reqwest::Client, handle_device: D) -> Result<(), AuthError>
- where
- D: FnOnce(StandardDeviceAuthorizationResponse) -> DF,
- DF: Future<Output = ()>
- {
- debug!("Using device authorization for XBL login");
- let oauth_client = create_oauth_client!(self.is_azure_client_id, self.client_id.clone());
- let device_auth: StandardDeviceAuthorizationResponse = oauth_client.exchange_device_code()
- .add_scopes(self.scopes_iter())
- .add_extra_param("response_type", "device_code")
- .request_async(client)
- .await.map_err(|e| AuthError::OAuthRequestToken { what: "device code", error: e })?;
-
- handle_device(device_auth.clone()).await;
-
- let tokenres = oauth_client.exchange_device_access_token(&device_auth)
- .set_max_backoff_interval(Duration::from_secs(20u64))
- .request_async(client, tokio::time::sleep, None)
- .await.map_err(|e| AuthError::OAuthRequestDeviceCode { what: "device access code", error: e })?;
-
- self.refresh_token = tokenres.refresh_token().cloned();
-
- self.xbl_login(client, tokenres.access_token()).await
- }
-
- // ensure we have an xbox live token for this person
- // tasks for this function:
- // - check if the XBL token is valid/not expired
- // - if it is expired, try to use refresh token to get a new one
- // - get rid of auth token if yeah
- async fn ensure_xbl(&mut self, client: &reqwest::Client, now: DateTime<Utc>) -> Result<(), AuthError> {
- if self.xbl_token.as_ref().is_some_and(|tok| !tok.is_expired(now)) {
- debug!("XBL token valid. Using it.");
- return Ok(())
- }
-
- if self.refresh_token.is_none() {
- return Err(AuthError::RequireInteractive("no refresh token"));
- }
-
- debug!("XBL token expired. Trying to refresh it.");
- self.xbl_login_refresh(client).await
- .map_err(|e| match &e {
- AuthError::OAuthRequestToken { error: RequestTokenError::ServerResponse(res), .. } => match res.error() {
- BasicErrorResponseType::Extension(s) if s == "interaction_required" || s == "consent_required" => {
- AuthError::RequireInteractive("msa requested interactive logon")
- },
- _ => e
- },
- _ => e
- })?;
-
- self.mc_token = None;
-
- Ok(())
- }
-
- // function's tasks:
- // - if the minecraft services token invalid/expired/missing, do the following
- // - get minecraftservices xsts token
- // - use minecraftservices to get mojang token with that xsts token
- async fn ensure_mc_token(&mut self, client: &reqwest::Client, now: DateTime<Utc>) -> Result<(), AuthError> {
- if self.mc_token.as_ref().is_some_and(|tok| !tok.is_expired(now)) {
- debug!("Mojang token valid. Using it.");
- return Ok(())
- }
-
- debug!("Mojang token has expired. Must log in again.");
- let xbl_token = self.xbl_token.as_ref().expect("ensure_mc_token requires xbl token").value.as_str();
- let (user_hash, mc_xsts_tok) = match msa::xsts_request(client, xbl_token, XSTS_RP_MINECRAFT_SERVICES).await? {
- msa::XSTSAuthResponse::Success(res) => {
- let user_hash = res.get_user_hash()
- .map_or(Err(AuthError::Internal("malformed response: no user hash".into())), |h| Ok(h.to_owned()))?;
- (user_hash, res.into_token())
- },
- msa::XSTSAuthResponse::Error(e) => return Err(e.into())
- };
-
- debug!("Got MinecraftServices XSTS, logging in.");
- self.mc_token = Some(mcservices::login_with_xbox(client, mc_xsts_tok.as_str(), user_hash.as_str()).await?);
-
- Ok(())
- }
-
- async fn load_xbox_info(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
- debug!("Loading Xbox info...");
- let xbl_token = self.xbl_token.as_ref().expect("xbl token missing").value.as_str();
-
- let res = match msa::xsts_request(client, xbl_token, XSTS_RP_XBOX_LIVE).await? {
- msa::XSTSAuthResponse::Success(res) => res,
- msa::XSTSAuthResponse::Error(e) => return Err(e.into())
- };
-
- let Some(xuid) = res.get_xuid() else {
- return Err(AuthError::Internal("missing xuid for user".into()));
- };
-
- self.xuid = Some(xuid.to_owned());
- self.gamertag = res.get_gamertag().map(|s| s.to_owned());
-
- debug!("Xbox info loaded: (xuid {xuid}, gamertag {})", res.get_gamertag().unwrap_or("<none>"));
-
- Ok(())
- }
-
- async fn load_profile(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
- self.load_xbox_info(client).await?;
-
- let mc_token = self.mc_token.as_ref().expect("minecraft token missing").value.as_str();
-
- debug!("Checking if you own the game...");
- if !mcservices::owns_the_game(client, mc_token).await? {
- return Err(AuthError::EntitlementError);
- }
-
- debug!("Getting your profile info...");
- let player_info = mcservices::get_player_info(client, mc_token).await?;
- let player_profile = mcservices::get_player_profile(client, player_info.id).await
- .map_err(|e| AuthError::Request { what: "looking up profile", error: e })?;
-
- self.player_info = Some(player_info);
- self.player_profile = Some(player_profile);
-
- Ok(())
- }
-
- pub async fn log_in_silent(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
- let now: DateTime<Utc> = DateTime::from(SystemTime::now()) + TimeDelta::hours(12);
-
- self.ensure_xbl(client, now).await?;
- self.ensure_mc_token(client, now).await?;
- self.load_profile(client).await?;
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod test {
- use oauth2::ClientId;
- use super::*;
-
- #[tokio::test]
- async fn abc() {
- simple_logger::SimpleLogger::new().with_colors(true).with_level(log::LevelFilter::Trace).init().unwrap();
-
- let mut user = match tokio::fs::read_to_string("test_stuff/test.json").await {
- Ok(s) => serde_json::from_str::<MsaUser>(&s).unwrap(),
- Err(e) if e.kind() == tokio::io::ErrorKind::NotFound => {
- MsaUser {
- player_profile: None,
- xuid: None,
- gamertag: None,
- player_info: None,
- //client_id: ClientId::new("00000000402b5328".into()),
- client_id: ClientId::new("60b6cc54-fc07-4bab-bca9-cbe9aa713c80".into()),
- is_azure_client_id: true,
- mc_token: None,
- xbl_token: None,
- refresh_token: None
- }
- },
- Err(e) => panic!("i/o error: {}", e)
- };
-
- let client = MsaUser::create_client();
-
- loop {
- match user.log_in_silent(&client).await {
- Ok(_) => break,
- Err(AuthError::RequireInteractive(s)) => {
- debug!("Requires interactive auth: {s}")
- },
- Err(e) => {
- panic!("{}", e);
- }
- }
-
- user.xbl_login_device(&client, |d| async move {
- let d = dbg!(d);
- debug!("User code: {}", d.user_code().secret());
- }).await.unwrap()
- }
-
- debug!("User: {user:?}");
-
- let user_str = serde_json::to_string_pretty(&user).unwrap();
- tokio::fs::write("test_stuff/test.json", user_str.as_str()).await.unwrap();
- }
-}
diff --git a/src/auth/mcservices.rs b/src/auth/mcservices.rs
deleted file mode 100644
index 45ef795..0000000
--- a/src/auth/mcservices.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::time::{Duration, SystemTime};
-use chrono::{DateTime, Utc};
-use reqwest::{IntoUrl, Method, RequestBuilder};
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-use super::{AuthError, MinecraftPlayerInfo, PlayerProfile};
-use super::types::Token;
-
-const MINECRAFT_LOGIN: &str = "https://api.minecraftservices.com/authentication/login_with_xbox";
-const MINECRAFT_ENTITLEMENTS: &str = "https://api.minecraftservices.com/entitlements";
-const MINECRAFT_PROFILE: &str = "https://api.minecraftservices.com/minecraft/profile";
-
-const MINECRAFT_SESSION_PROFILE: &str = "https://sessionserver.mojang.com/session/minecraft/profile/";
-
-fn build_authenticated(client: &reqwest::Client, url: impl IntoUrl, method: Method, mc_token: &str) -> RequestBuilder {
- super::build_json_request(client, url, method)
- .bearer_auth(mc_token)
-}
-
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct MinecraftXboxLoginRequest<'a> {
- identity_token: &'a str,
- ensure_legacy_enabled: bool
-}
-
-#[derive(Deserialize, Debug)]
-struct MinecraftXboxLoginResponse {
- access_token: String,
- expires_in: u64
-}
-
-pub async fn login_with_xbox(client: &reqwest::Client, xsts_token: &str, user_hash: &str) -> Result<Token, AuthError> {
- let tok = format!("XBL3.0 x={user_hash};{xsts_token}");
- let req = MinecraftXboxLoginRequest {
- identity_token: tok.as_str(),
- ensure_legacy_enabled: true
- };
-
- let res: MinecraftXboxLoginResponse = super::build_json_request(client, MINECRAFT_LOGIN, Method::POST)
- .json(&req).send().await
- .and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "minecraft xbox login", error: e })?
- .json().await
- .map_err(|e| AuthError::Request { what: "minecraft xbox login (decode)", error: e })?;
-
- let now: DateTime<Utc> = SystemTime::now().into();
-
- Ok(Token {
- value: res.access_token,
- expire: Some(now + Duration::from_secs(res.expires_in))
- })
-}
-
-#[derive(Deserialize, Debug)]
-struct EntitlementItem {
- name: String
- // we don't care about the signature
-}
-
-#[derive(Deserialize, Debug)]
-struct EntitlementResponse {
- #[serde(default)]
- items: Vec<EntitlementItem>
-}
-
-pub async fn owns_the_game(client: &reqwest::Client, token: &str) -> Result<bool, AuthError> {
- let res: EntitlementResponse = build_authenticated(client, MINECRAFT_ENTITLEMENTS, Method::GET, token)
- .send().await
- .and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "entitlements", error: e })?
- .json().await
- .map_err(|e| AuthError::Request { what: "entitlements (receive)", error: e})?;
-
- Ok(res.items.iter().any(|i| i.name == "game_minecraft" || i.name == "product_minecraft"))
-}
-
-pub async fn get_player_info(client: &reqwest::Client, token: &str) -> Result<MinecraftPlayerInfo, AuthError> {
- build_authenticated(client, MINECRAFT_PROFILE, Method::GET, token)
- .send().await
- .and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "player info", error: e })?
- .json().await
- .map_err(|e| AuthError::Request { what: "player info (receive)", error: e })
-}
-
-pub async fn get_player_profile(client: &reqwest::Client, uuid: Uuid) -> Result<PlayerProfile, reqwest::Error> {
- super::build_json_request(client, format!("{}{}?unsigned=false", MINECRAFT_SESSION_PROFILE, uuid.as_simple()), Method::GET)
- .send().await
- .and_then(|r| r.error_for_status())?
- .json().await
-}
diff --git a/src/auth/msa.rs b/src/auth/msa.rs
deleted file mode 100644
index add345c..0000000
--- a/src/auth/msa.rs
+++ /dev/null
@@ -1,170 +0,0 @@
-use std::borrow::Cow;
-use std::collections::HashMap;
-use chrono::{DateTime, Utc};
-use log::debug;
-use oauth2::AccessToken;
-use reqwest::{Method};
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-use crate::auth::AuthError;
-use crate::auth::types::Token;
-
-const XBOX_LIVE_AUTH: &str = "https://user.auth.xboxlive.com/user/authenticate";
-const XBOX_LIVE_XSTS: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "PascalCase")]
-struct XboxLiveAuthRequestProperties<'a> {
- auth_method: &'a str,
- site_name: &'a str,
- rps_ticket: &'a str
-}
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "PascalCase")]
-struct XboxLiveAuthRequest<'a> {
- properties: XboxLiveAuthRequestProperties<'a>,
- relying_party: &'a str,
- token_type: &'a str
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-struct XboxLiveAuthResponse {
- token: String,
- not_after: DateTime<Utc>
-}
-
-pub async fn xbox_live_login(client: &reqwest::Client, access_token: &AccessToken, azure: bool) -> Result<Token, AuthError> {
- debug!("MSA performing xbox live login ({azure})");
-
- let ticket = match azure {
- true => Cow::Owned(format!("d={}", access_token.secret())),
- _ => Cow::Borrowed(access_token.secret().as_str())
- };
-
- let request = XboxLiveAuthRequest {
- properties: XboxLiveAuthRequestProperties {
- auth_method: "RPS",
- site_name: "user.auth.xboxlive.com",
- rps_ticket: ticket.as_ref()
- },
- relying_party: "http://auth.xboxlive.com",
- token_type: "JWT"
- };
-
- let res: XboxLiveAuthResponse = super::build_json_request(client, XBOX_LIVE_AUTH, Method::POST).json(&request).send().await
- .and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "xbox live auth", error: e })?
- .json().await.map_err(|e| AuthError::Request { what: "xbox live auth (decode)", error: e })?;
-
- Ok(Token {
- value: res.token,
- expire: Some(res.not_after)
- })
-}
-
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "PascalCase")]
-struct XSTSAuthRequest<'a> {
- properties: XSTSAuthRequestProperties<'a>,
- relying_party: &'a str,
- token_type: &'a str
-}
-
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "PascalCase")]
-struct XSTSAuthRequestProperties<'a> {
- sandbox_id: &'a str,
- user_tokens: &'a[&'a str]
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "PascalCase")]
-pub(super) struct XSTSAuthSuccessResponse {
- token: String,
- #[serde(default)]
- display_claims: XSTSAuthResponseDisplayClaims
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "PascalCase")]
-pub(super) struct XSTSAuthErrorResponse {
- x_err: u64,
- message: Option<String>
-}
-
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "PascalCase", untagged)]
-pub(super) enum XSTSAuthResponse {
- Success(XSTSAuthSuccessResponse),
- Error(XSTSAuthErrorResponse)
-}
-
-#[derive(Deserialize, Debug, Default)]
-pub(super) struct XSTSAuthResponseDisplayClaims {
- xui: Vec<HashMap<String, String>>
-}
-
-impl XSTSAuthSuccessResponse {
- pub(super) fn into_token(self) -> String {
- self.token
- }
-
- fn get_display_claim(&self, name: &str) -> Option<&str> {
- self.display_claims.xui.iter().find(|m| m.contains_key(name)).and_then(|f| f.get(name).map(|s| s.as_str()))
- }
-
- pub(super) fn get_user_hash(&self) -> Option<&str> {
- self.get_display_claim("uhs")
- }
-
- pub(super) fn get_xuid(&self) -> Option<&str> {
- self.get_display_claim("xid")
- }
-
- pub(super) fn get_gamertag(&self) -> Option<&str> {
- self.get_display_claim("gtg")
- }
-}
-
-#[allow(clippy::from_over_into)]
-impl Into<AuthError> for XSTSAuthErrorResponse {
- fn into(self) -> AuthError {
- AuthError::AuthXError {
- // some well-known error values
- what: match self.x_err {
- 2148916238u64 => "Microsoft account held by a minor outside of a family.",
- 2148916233u64 => "Account is not on Xbox.",
- _ => "Unknown error."
- },
- x_error: self.x_err,
- message: self.message
- }
- }
-}
-
-pub(super) const XSTS_RP_MINECRAFT_SERVICES: &str = "rp://api.minecraftservices.com/";
-pub(super) const XSTS_RP_XBOX_LIVE: &str = "http://xboxlive.com";
-
-pub async fn xsts_request(client: &reqwest::Client, xbl_token: &str, relying_party: &str) -> Result<XSTSAuthResponse, AuthError> {
- debug!("Performing XSTS auth {relying_party}");
-
- let token_array = [xbl_token];
- let req = XSTSAuthRequest {
- properties: XSTSAuthRequestProperties {
- sandbox_id: "RETAIL",
- user_tokens: token_array.as_slice()
- },
- relying_party,
- token_type: "JWT"
- };
-
- let res: XSTSAuthResponse = super::build_json_request(client, XBOX_LIVE_XSTS, Method::POST).json(&req).send().await
- .and_then(|r| r.error_for_status())
- .map_err(|e| AuthError::Request { what: "xsts", error: e })?
- .json().await
- .map_err(|e| AuthError::Request { what: "xsts (decode)", error: e })?;
-
- Ok(res)
-}
diff --git a/src/auth/types.rs b/src/auth/types.rs
deleted file mode 100644
index b9cdaad..0000000
--- a/src/auth/types.rs
+++ /dev/null
@@ -1,130 +0,0 @@
-pub mod property_map;
-pub use property_map::PropertyMap;
-
-use std::fmt::{Debug, Formatter};
-use chrono::{DateTime, Utc};
-use multimap::MultiMap;
-use oauth2::RefreshToken;
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct Property {
- pub name: String,
- pub value: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub signature: Option<String>
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct PlayerProfile {
- #[serde(with = "uuid::serde::simple")]
- pub id: Uuid,
- pub name: String,
-
- #[serde(default, skip_serializing_if = "MultiMap::is_empty", with = "property_map")]
- pub properties: PropertyMap
-}
-
-#[derive(Serialize, Deserialize)]
-pub(super) struct Token {
- pub value: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expire: Option<DateTime<Utc>>
-}
-
-struct RedactedValue;
-impl Debug for RedactedValue {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.write_str("[redacted]")
- }
-}
-
-impl Debug for Token {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Token")
- .field("value", &RedactedValue)
- .field("expire", &self.expire)
- .finish()
- }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "UPPERCASE")]
-pub enum SkinState {
- Active,
- Inactive
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "UPPERCASE")]
-pub enum SkinVariant {
- Classic,
- Slim
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SkinInfo {
- pub id: Uuid,
- pub state: SkinState,
- pub url: String,
- pub texture_key: Option<String>,
- pub variant: Option<SkinVariant>,
- pub alias: Option<String>
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct MinecraftPlayerInfo {
- #[serde(with = "uuid::serde::simple")]
- pub id: Uuid,
- pub name: String,
-
- #[serde(default)]
- pub skins: Vec<SkinInfo>,
- #[serde(default)]
- pub capes: Vec<SkinInfo>,
-
- #[serde(default)]
- pub demo: bool,
-
- #[serde(default)]
- pub legacy: bool,
-
- // todo: profile actions (idk the format)
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct MsaUser {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub player_profile: Option<PlayerProfile>,
- pub xuid: Option<String>,
- pub gamertag: Option<String>,
-
- #[serde(skip)] // this information is transient
- pub player_info: Option<MinecraftPlayerInfo>,
-
- pub(super) client_id: oauth2::ClientId,
-
- #[serde(default, skip_serializing_if = "std::ops::Not::not")]
- pub(super) is_azure_client_id: bool,
-
- pub(super) mc_token: Option<Token>,
- pub(super) xbl_token: Option<Token>,
- pub(super) refresh_token: Option<RefreshToken>
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "lowercase")]
-pub enum User {
- Dummy(PlayerProfile),
- MSA(Box<MsaUser>)
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct AuthenticationDatabase {
- pub users: Vec<User>
-}
diff --git a/src/auth/types/property_map.rs b/src/auth/types/property_map.rs
deleted file mode 100644
index ddfc9ce..0000000
--- a/src/auth/types/property_map.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-use std::fmt::Formatter;
-use multimap::MultiMap;
-use serde::de::{SeqAccess, Visitor};
-use serde::{Deserializer, Serializer};
-use crate::auth::Property;
-
-pub type PropertyMap = MultiMap<String, Property>;
-
-pub mod legacy {
- use serde::Serializer;
- use super::PropertyMap;
-
- pub fn serialize<S>(value: &PropertyMap, serializer: S) -> Result<S::Ok, S::Error>
- where S: Serializer
- {
- serializer.collect_map(value.iter_all()
- .filter_map(|(k, v)| {
- if v.is_empty() {
- None
- } else {
- Some((k, v.iter().map(|p| &p.value).collect::<Vec<_>>()))
- }
- }))
- }
-}
-
-pub fn serialize<S>(value: &PropertyMap, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer
-{
- serializer.collect_seq(value.flat_iter().map(|(_, v)| v))
-}
-
-pub fn deserialize<'de, D>(deserializer: D) -> Result<PropertyMap, D::Error>
-where
- D: Deserializer<'de>
-{
- struct PropertyMapVisitor;
-
- impl<'de> Visitor<'de> for PropertyMapVisitor {
- type Value = PropertyMap;
-
- fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
- formatter.write_str("a property map")
- }
-
- fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
- where
- A: SeqAccess<'de>,
- {
- let mut map = MultiMap::new() as PropertyMap;
- while let Some(prop) = seq.next_element::<Property>()? {
- map.insert(prop.name.clone(), prop);
- }
-
- Ok(map)
- }
- }
-
- deserializer.deserialize_seq(PropertyMapVisitor)
-}
diff --git a/src/launcher.rs b/src/launcher.rs
deleted file mode 100644
index 2836db5..0000000
--- a/src/launcher.rs
+++ /dev/null
@@ -1,765 +0,0 @@
-mod constants;
-mod version;
-mod strsub;
-mod download;
-mod rules;
-mod assets;
-mod extract;
-mod settings;
-mod runner;
-mod jre;
-
-use std::borrow::Cow;
-use std::cmp::min;
-use std::env::consts::{ARCH, OS};
-use std::error::Error;
-use std::ffi::{OsStr, OsString};
-use std::fmt::{Display, Formatter};
-use std::io::ErrorKind;
-use std::io::ErrorKind::AlreadyExists;
-use std::path::{Component, Path, PathBuf};
-use std::{env, process};
-use std::env::JoinPathsError;
-use std::time::{Instant, SystemTime, UNIX_EPOCH};
-use const_format::formatcp;
-use futures::{StreamExt, TryStreamExt};
-use indexmap::IndexMap;
-use log::{debug, info, trace, warn};
-use reqwest::Client;
-use sysinfo::System;
-use tokio::{fs, io};
-use tokio_stream::wrappers::ReadDirStream;
-use download::{MultiDownloader, VerifiedDownload};
-use rules::{CompatCheck, IncompatibleError};
-use version::{VersionList, VersionResolveError, VersionResult};
-use crate::version::{Library, OSRestriction, OperatingSystem, DownloadType, LibraryExtractRule, FeatureMatcher, ClientLogging};
-
-use assets::{AssetError, AssetRepository};
-use crate::util::{self, AsJavaPath};
-
-pub use settings::*;
-pub use runner::run_the_game;
-pub use crate::util::{EnsureFileError, FileVerifyError, IntegrityError};
-use crate::assets::AssetIndex;
-use runner::ArgumentType;
-use strsub::SubFunc;
-use crate::launcher::download::FileDownload;
-use crate::launcher::jre::{JavaRuntimeError, JavaRuntimeRepository};
-use crate::launcher::version::VersionError;
-use crate::version::manifest::VersionType;
-
-#[derive(Debug)]
-pub enum LogConfigError {
- UnknownType(String),
- InvalidId(Option<String>),
- MissingURL,
- IO{ what: &'static str, error: io::Error },
- Offline,
- Download{ url: String, error: reqwest::Error },
-
- Integrity(IntegrityError)
-}
-
-impl Display for LogConfigError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- LogConfigError::UnknownType(log_type) => write!(f, "unknown log type {}", log_type),
- LogConfigError::InvalidId(oid) => match oid {
- Some(id) => write!(f, "invalid log config id: {}", id),
- None => f.write_str("missing log config id")
- },
- LogConfigError::MissingURL => f.write_str("missing log config download URL"),
- LogConfigError::IO { what, error} => write!(f, "i/o error ({}): {}", what, error),
- LogConfigError::Offline => f.write_str("launcher in offline mode"),
- LogConfigError::Download { url, error } => write!(f, "failed to download log config ({}): {}", url, error),
- LogConfigError::Integrity(e) => write!(f, "log config verify error: {}", e)
- }
- }
-}
-
-impl Error for LogConfigError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- LogConfigError::IO { error, .. } => Some(error),
- LogConfigError::Download {error, ..} => Some(error),
- LogConfigError::Integrity(error) => Some(error),
- _ => None
- }
- }
-}
-
-struct SystemInfo {
- os: OperatingSystem,
- os_version: String,
- arch: String
-}
-
-struct LibraryRepository {
- home: PathBuf,
- natives: PathBuf
-}
-
-pub struct Launcher {
- online: bool,
- home: PathBuf,
- versions: VersionList,
-
- system_info: SystemInfo,
-
- libraries: LibraryRepository,
- assets: AssetRepository,
- java_runtimes: JavaRuntimeRepository
-}
-
-#[derive(Debug)]
-pub enum LaunchError {
- UnknownInstance(String),
-
- // version resolution errors
- VersionInit(VersionError),
- UnknownVersion(String),
- LoadVersion(VersionError),
- ResolveVersion(VersionResolveError),
- IncompatibleVersion(IncompatibleError),
- MissingMainClass,
-
- // library errors
- LibraryDirError(PathBuf, io::Error),
- LibraryVerifyError(FileVerifyError),
- LibraryDownloadError,
- LibraryExtractError(extract::ZipExtractError),
- LibraryClasspathError(JoinPathsError),
-
- // ensure file errors
- EnsureFile(EnsureFileError),
- IO { what: &'static str, error: io::Error },
-
- // log errors
- UnknownLogType(String),
- InvalidLogId(Option<String>),
-
- // asset errors
- Assets(AssetError),
-
- // java runtime errors
- ResolveJavaRuntime { what: &'static str, error: io::Error },
- MissingJavaRuntime,
- JavaRuntimeRepo(JavaRuntimeError)
-}
-
-impl Display for LaunchError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match &self {
- LaunchError::UnknownInstance(inst) => write!(f, "unknown instance: {inst}"),
- LaunchError::VersionInit(e) => write!(f, "initializing version: {e}"),
- LaunchError::UnknownVersion(id) => write!(f, "unknown version id: {id}"),
- LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"),
- LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}"),
- LaunchError::IncompatibleVersion(e) => e.fmt(f),
- LaunchError::MissingMainClass => f.write_str("main class not specified"),
- LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e),
- LaunchError::LibraryVerifyError(e) => write!(f, "failed to verify library: {}", e),
- LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks
- LaunchError::LibraryExtractError(e) => write!(f, "library extract zip error: {e}"),
- LaunchError::LibraryClasspathError(e) => write!(f, "error building classpath: {e}"),
- LaunchError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error),
- LaunchError::EnsureFile(e) => e.fmt(f),
- LaunchError::UnknownLogType(t) => write!(f, "unknown log type: {}", t),
- LaunchError::InvalidLogId(Some(id)) => write!(f, "invalid log id: {}", id),
- LaunchError::InvalidLogId(None) => write!(f, "missing log id"),
- LaunchError::Assets(e) => write!(f, "failed to fetch assets: {}", e),
- LaunchError::ResolveJavaRuntime { what, error } => write!(f, "failed to find java runtime ({}): {}", what, error),
- LaunchError::MissingJavaRuntime => f.write_str("suitable java executable not found"),
- LaunchError::JavaRuntimeRepo(e) => write!(f, "runtime repository error: {e}")
- }
- }
-}
-
-impl Error for LaunchError {
- fn cause(&self) -> Option<&dyn Error> {
- match &self {
- LaunchError::VersionInit(e) => Some(e),
- LaunchError::LoadVersion(e) => Some(e),
- LaunchError::ResolveVersion(e) => Some(e),
- LaunchError::IncompatibleVersion(e) => Some(e),
- LaunchError::LibraryDirError(_, e) => Some(e),
- LaunchError::LibraryVerifyError(e) => Some(e),
- LaunchError::LibraryExtractError(e) => Some(e),
- LaunchError::LibraryClasspathError(e) => Some(e),
- LaunchError::IO { error: e, .. } => Some(e),
- LaunchError::EnsureFile(e) => Some(e),
- LaunchError::Assets(e) => Some(e),
- LaunchError::ResolveJavaRuntime { error: e, .. } => Some(e),
- LaunchError::JavaRuntimeRepo(e) => Some(e),
- _ => None
- }
- }
-}
-
-struct LaunchInfo<'l, F: FeatureMatcher> {
- launcher: &'l Launcher,
- feature_matcher: &'l F,
-
- asset_index_name: Option<String>,
- classpath: String,
- virtual_assets_path: Option<PathBuf>,
- instance_home: PathBuf,
- natives_path: PathBuf,
- client_jar: Option<PathBuf>,
- version_id: String,
- version_type: Option<VersionType>,
- asset_index: Option<AssetIndex>
-}
-
-#[derive(Debug)]
-pub struct Launch {
- jvm_args: Vec<OsString>,
- game_args: Vec<OsString>,
- main_class: String,
- instance_path: PathBuf,
- runtime_path: PathBuf,
- runtime_legacy_launch: bool
-}
-
-struct ProfileFeatureMatcher<'prof> {
- profile: &'prof Profile
-}
-
-impl FeatureMatcher for ProfileFeatureMatcher<'_> {
- fn matches(&self, feature: &str) -> bool {
- match feature {
- "has_custom_resolution" => self.profile.get_resolution().is_some(),
- _ => false
- }
- }
-}
-
-impl Launcher {
- // FIXME: more descriptive error type por favor
- pub async fn new(home: impl AsRef<Path>, online: bool) -> Result<Launcher, LaunchError> {
- match tokio::fs::create_dir_all(home.as_ref()).await {
- Err(e) if e.kind() != AlreadyExists => {
- warn!("Failed to create launcher home directory: {}", e);
- return Err(LaunchError::IO { what: "create launcher home", error: e });
- },
- _ => ()
- }
-
- let home = fs::canonicalize(home.as_ref()).await
- .map_err(|e| LaunchError::IO { what: "resolve home path", error: e })?;
-
- let versions_home = home.join("versions");
-
- debug!("Version list online?: {online}");
- let versions = if online {
- VersionList::online(versions_home.as_ref()).await.map_err(LaunchError::VersionInit)?
- } else {
- VersionList::offline(versions_home.as_ref()).await.map_err(LaunchError::VersionInit)?
- };
-
- let assets_path = home.join("assets");
-
- let java_runtimes = JavaRuntimeRepository::new(home.join("jre"), online).await.map_err(LaunchError::JavaRuntimeRepo)?;
-
- Ok(Launcher {
- online,
- versions,
- system_info: SystemInfo::new(),
- libraries: LibraryRepository {
- home: home.join("libraries"),
- natives: home.join("natives")
- },
- assets: AssetRepository::new(online, &assets_path).await.map_err(|e| LaunchError::IO { what: "setting up assets", error: e })?,
- java_runtimes,
- home
- })
- }
-
- fn choose_lib_classifier<'lib>(&self, lib: &'lib Library) -> Option<&'lib str> {
- lib.natives.as_ref().and_then(|n| n.get(&self.system_info.os)).map(|s| s.as_str())
- }
-
- async fn log_config_ensure(&self, config: &ClientLogging) -> Result<String, LaunchError> {
- info!("Ensuring log configuration exists and is valid.");
-
- if config.log_type != "log4j2-xml" {
- return Err(LaunchError::UnknownLogType(config.log_type.clone()));
- }
-
- let dlinfo = &config.file;
- let Some(id) = dlinfo.id.as_ref() else {
- return Err(LaunchError::InvalidLogId(None));
- };
-
- let mut path = self.home.join("logging");
- fs::create_dir_all(path.as_path()).await
- .map_err(|e| LaunchError::IO{ what: "creating log directory", error: e })?;
-
- let Some(Component::Normal(filename)) = Path::new(id).components().last() else {
- return Err(LaunchError::InvalidLogId(Some(id.clone())));
- };
-
- path.push(filename);
-
- debug!("Logger config {} is at {}", id, path.display());
-
- util::ensure_file(&path, dlinfo.url.as_deref(), dlinfo.size, dlinfo.sha1, self.online, false).await
- .map_err(LaunchError::EnsureFile)?;
-
- struct PathSub<'a>(&'a Path);
- impl<'a> SubFunc<'a> for PathSub<'a> {
- fn substitute(&self, key: &str) -> Option<Cow<'a, str>> {
- match key {
- "path" => Some(self.0.as_java_path().to_string_lossy()),
- _ => None
- }
- }
- }
-
- Ok(strsub::replace_string(config.argument.as_str(), &PathSub(path.as_ref())).to_string())
- }
-
- /* TODO:
- * - launch game using JNI
- * - auth
- */
- pub async fn prepare_launch(&self, profile: &Profile, instance: &Instance) -> Result<Launch, LaunchError> {
- let start = Instant::now();
- let feature_matcher = ProfileFeatureMatcher { profile };
- let version_id = profile.get_version();
-
- let Some(version_id) = self.versions.get_profile_version_id(version_id) else {
- // idk how common this use case actually is
- warn!("Can't use latest release/snapshot profiles while offline!");
- return Err(LaunchError::UnknownVersion("<latest>".into()));
- };
-
- info!("Preparing launch for \"{}\"...", version_id);
-
- let inst_home = instance.get_path(&self.home).await.map_err(|e| LaunchError::IO {
- what: "resolving instance directory",
- error: e
- })?;
-
- fs::create_dir_all(inst_home.as_path()).await.map_err(|e| LaunchError::IO {
- what: "creating instance directory",
- error: e
- })?;
-
- info!("Launching the game in {}", inst_home.display());
-
- let ver_res = self.versions.get_version_lazy(version_id.as_ref());
- let ver = match ver_res {
- VersionResult::Remote(mv) => Cow::Owned(self.versions.load_remote_version(mv).await.map_err(LaunchError::LoadVersion)?),
- VersionResult::Complete(cv) => Cow::Borrowed(cv),
- VersionResult::None => {
- return Err(LaunchError::UnknownVersion(version_id.into_owned()))
- }
- };
-
- let ver = self.versions.resolve_version(ver.as_ref()).await.map_err(LaunchError::ResolveVersion)?;
- ver.rules_apply(&self.system_info, &feature_matcher).map_err(LaunchError::IncompatibleVersion)?;
-
- info!("Resolved launch version {}!", ver.id);
-
- let mut extract_jobs = Vec::new();
- let mut downloads = IndexMap::new();
-
- for lib in ver.libraries.iter() {
- if lib.rules_apply(&self.system_info, &feature_matcher).is_err() {
- trace!("Skipping library {}, compatibility rules failed", lib.name);
- continue;
- }
-
- let classifier = self.choose_lib_classifier(lib);
-
- if let Some(dl) = self.libraries.create_download(lib, classifier) {
- let canon_name = lib.get_canonical_name();
- if downloads.contains_key(&canon_name) {
- debug!("Skipping library {}, we already have another version of that library.", lib.name);
- continue;
- }
-
- trace!("Using library {} ({})", lib.name, classifier.unwrap_or("None"));
- dl.make_dirs().await.map_err(|e| LaunchError::LibraryDirError(dl.get_path().to_path_buf(), e))?;
-
- if lib.natives.is_some() {
- extract_jobs.push(LibraryExtractJob {
- source: dl.get_path().to_owned(),
- rule: lib.extract.clone()
- });
- }
-
- downloads.insert(canon_name, dl);
- } else {
- trace!("Skipping library {} ({}), no download", lib.name, classifier.unwrap_or("None"));
- }
- }
-
- if self.online {
- info!("Downloading {} libraries...", downloads.len());
- let client = Client::new();
- MultiDownloader::new(downloads.values_mut()).perform(&client).await
- .inspect_err(|e| warn!("library download failed: {e}"))
- .try_fold((), |_, _| async {Ok(())})
- .await
- .map_err(|_| LaunchError::LibraryDownloadError)?;
- } else {
- info!("Verifying {} libraries...", downloads.len());
- download::verify_files(downloads.values_mut()).await.map_err(|e| {
- warn!("A library could not be verified: {}", e);
- warn!("Since the launcher is in offline mode, libraries cannot be downloaded. Please try again in online mode.");
- LaunchError::LibraryVerifyError(e)
- })?;
- }
-
- let log_arg;
- if let Some(logging) = ver.logging.as_ref().and_then(|l| l.client.as_ref()) {
- log_arg = Some(self.log_config_ensure(logging).await?);
- } else {
- log_arg = None;
- }
-
- // download assets
-
- let (asset_idx_name, asset_idx) =
- if let Some(idx_download) = ver.asset_index.as_ref() {
- let asset_idx_name = idx_download.id.as_ref().or(ver.assets.as_ref()).map(String::as_str);
- let asset_idx = self.assets.load_index(idx_download, asset_idx_name).await
- .map_err(LaunchError::Assets)?;
-
- self.assets.ensure_assets(&asset_idx).await.map_err(LaunchError::Assets)?;
-
- (asset_idx_name, Some(asset_idx))
- } else {
- (None, None)
- };
-
- // download client jar
-
- let client_jar_path;
- if let Some(client) = ver.downloads.get(&DownloadType::Client) {
- let mut client_path: PathBuf = [self.home.as_ref(), OsStr::new("versions"), OsStr::new(&ver.id)].iter().collect();
- fs::create_dir_all(&client_path).await.map_err(|e| LaunchError::IO{ what: "creating client download directory", error: e })?;
-
- client_path.push(format!("{}.jar", ver.id));
-
- info!("Downloading client jar {}", client_path.display());
-
- util::ensure_file(client_path.as_path(), client.url.as_deref(), client.size, client.sha1, self.online, false).await
- .map_err(LaunchError::EnsureFile)?;
-
- client_jar_path = Some(client_path);
- } else {
- client_jar_path = None;
- }
-
- // clean up old natives
- let nnatives = self.libraries.clean_old_natives().await?;
- info!("Cleaned up {} old natives directories.", nnatives);
-
- // extract natives (execute this function unconditionally because we still need the natives dir to exist)
- info!("Extracting natives from libraries");
- let natives_dir = self.libraries.extract_natives(extract_jobs).await?;
-
- let game_assets = if let Some(asset_idx) = asset_idx.as_ref() {
- info!("Reconstructing assets");
- self.assets.reconstruct_assets(asset_idx, inst_home.as_path(), asset_idx_name).await
- .map_err(LaunchError::Assets)?
- } else {
- None
- };
-
- info!("Building classpath");
- let classpath = env::join_paths(downloads.values()
- .map(|job| job.get_path().as_java_path())
- .chain(client_jar_path.iter().map(|p| p.as_path().as_java_path())))
- .map_err(LaunchError::LibraryClasspathError)?
- .into_string()
- .unwrap_or_else(|os| {
- warn!("Classpath contains invalid UTF-8. The game may not launch correctly.");
- os.to_string_lossy().to_string()
- });
-
- trace!("Classpath: {classpath}");
-
- info!("Resolving java runtime environment path");
- let runtime_path;
-
- if let Some(ref profile_jre) = profile.get_java_runtime() {
- runtime_path = fs::canonicalize(profile_jre).await
- .map_err(|e| LaunchError::ResolveJavaRuntime {what: "resolving jre path", error: e})?;
- } else {
- let Some(ref java_ver) = ver.java_version else {
- warn!("Version {} does not specify java version information. You must select a runtime manually.", ver.id);
- return Err(LaunchError::MissingJavaRuntime);
- };
-
- let runtime = self.java_runtimes.choose_runtime(java_ver.component.as_str()).await.map_err(LaunchError::JavaRuntimeRepo)?;
- runtime_path = self.java_runtimes.ensure_jre(java_ver.component.as_str(), runtime).await.map_err(LaunchError::JavaRuntimeRepo)?;
- }
-
- let Some(runtime_exe_path) = runner::find_java(runtime_path.as_path(), profile.is_legacy_launch()).await
- .map_err(|e| LaunchError::ResolveJavaRuntime {what: "finding java executable", error: e})? else {
- return Err(LaunchError::MissingJavaRuntime);
- };
-
-
- debug!("Found runtime exe: {}", runtime_exe_path.display());
-
- info!("Deriving launch arguments");
- let info = LaunchInfo {
- launcher: self,
- feature_matcher: &feature_matcher,
-
- asset_index_name: asset_idx_name.map(|s| s.to_owned()),
- classpath,
- virtual_assets_path: game_assets,
- instance_home: inst_home.clone(),
- natives_path: natives_dir,
- client_jar: client_jar_path,
- version_id: ver.id.to_string(),
- version_type: ver.version_type.clone(),
- asset_index: asset_idx
- };
-
- let Some(ref main_class) = ver.main_class else {
- return Err(LaunchError::MissingMainClass);
- };
-
- // yuck
- let jvm_args = profile.iter_arguments().map(OsString::from)
- .chain(runner::build_arguments(&info, ver.as_ref(), ArgumentType::Jvm).drain(..))
- .chain(log_arg.iter().map(OsString::from)).collect();
- let game_args = runner::build_arguments(&info, ver.as_ref(), ArgumentType::Game);
-
- let diff = Instant::now().duration_since(start);
- info!("Finished preparing launch for {} in {:.02} seconds!", ver.id, diff.as_secs_f32());
-
- Ok(Launch {
- jvm_args,
- game_args,
- main_class: main_class.to_string(),
- instance_path: inst_home,
- runtime_path: runtime_exe_path,
- runtime_legacy_launch: profile.is_legacy_launch()
- })
- }
-}
-
-#[derive(Debug)]
-enum LibraryError {
- InvalidName(String),
- IO { what: &'static str, error: io::Error }
-}
-
-impl Display for LibraryError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"),
- LibraryError::IO { what, error } => write!(f, "library i/o error ({what}): {error}"),
- }
- }
-}
-
-impl Error for LibraryError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- LibraryError::IO { error, .. } => Some(error),
- _ => None
- }
- }
-}
-
-#[derive(Debug)]
-struct LibraryExtractJob {
- source: PathBuf,
- rule: Option<LibraryExtractRule>
-}
-
-const ARCH_BITS: &str = formatcp!("{}", usize::BITS);
-
-impl LibraryRepository {
- fn get_artifact_base_dir(name: &str) -> Option<PathBuf> {
- let end_of_gid = name.find(':')?;
-
- Some(name[..end_of_gid].split('.').chain(name.split(':').skip(1).take(2)).collect())
- }
-
- fn get_artifact_filename(name: &str, classifier: Option<&str>) -> Option<PathBuf> {
- let n: Vec<&str> = name.splitn(4, ':').skip(1).collect();
-
- struct LibReplace;
- impl SubFunc<'static> for LibReplace {
- fn substitute(&self, key: &str) -> Option<Cow<'static, str>> {
- match key {
- "arch" => Some(Cow::Borrowed(ARCH_BITS)),
- _ => None
- }
- }
- }
-
- if let Some(classifier) = classifier {
- match n.len() {
- 2 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}.jar", n[0], n[1], classifier).as_str(), &LibReplace).as_ref())),
- 3 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}-{}.jar", n[0], n[1], classifier, n[2]).as_str(), &LibReplace).as_ref())),
- _ => None
- }
- } else {
- match n.len() {
- 2 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}.jar", n[0], n[1]).as_str(), &LibReplace).as_ref())),
- 3 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}.jar", n[0], n[1], n[2]).as_str(), &LibReplace).as_ref())),
- _ => None
- }
- }
- }
-
- fn get_artifact_path(name: &str, classifier: Option<&str>) -> Option<PathBuf> {
- let mut p = Self::get_artifact_base_dir(name)?;
-
- p.push(Self::get_artifact_filename(name, classifier)?);
- Some(p)
- }
-
- fn create_download(&self, lib: &Library, classifier: Option<&str>) -> Option<VerifiedDownload> {
- if let Some(ref url) = lib.url {
- let path = Self::get_artifact_path(lib.name.as_str(), classifier)?;
- let url = [url.as_str(), path.to_string_lossy().as_ref()].into_iter().collect::<String>();
- Some(VerifiedDownload::new(url.as_ref(), self.home.join(path).as_path(), lib.size, lib.sha1)) // TODO: could download sha1
- } else if let Some(ref downloads) = lib.downloads {
- let dlinfo = downloads.get_download_info(classifier)?;
- // drinking game: take a shot once per heap allocation
- let path = self.home.join(dlinfo.path.as_ref().map(PathBuf::from).or_else(|| Self::get_artifact_path(lib.name.as_str(), classifier))?);
-
- Some(VerifiedDownload::new(dlinfo.url.as_ref()?, path.as_path(), dlinfo.size, dlinfo.sha1))
- } else {
- let path = Self::get_artifact_path(lib.name.as_str(), classifier)?;
- let url = ["https://libraries.minecraft.net/", path.to_string_lossy().as_ref()].into_iter().collect::<String>();
- Some(VerifiedDownload::new(url.as_ref(), self.home.join(path).as_path(), lib.size, lib.sha1)) // TODO: could download sha1
- }
- }
-
- async fn clean_old_natives(&self) -> Result<usize, LaunchError> {
- info!("Cleaning up old natives folders...");
-
- let boot_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - min(System::uptime(), 7u64*24*60*60);
-
- let readdir = match fs::read_dir(&self.natives).await {
- Ok(readdir) => readdir,
- Err(e) if e.kind() == ErrorKind::NotFound => return Ok(0),
- Err(e) => return Err(LaunchError::IO { what: "reading natives directory", error: e })
- };
-
- ReadDirStream::new(readdir)
- .map(|entry| Ok(async move {
- let entry = entry.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e })?;
- let ftype = entry.file_type().await.map_err(|e| LaunchError::IO { what: "'stat'ing natives entry", error: e })?;
-
- if !ftype.is_dir() { return Ok(false); }
-
- let Some(ftime) = entry.file_name().to_str()
- .and_then(|s| constants::NATIVES_DIR_PATTERN.captures(s))
- .and_then(|c| c.get(1))
- .and_then(|cap| cap.as_str().parse::<u64>().ok()) else {
- return Ok(false);
- };
-
- if ftime < boot_time {
- let path = entry.path();
- info!("Deleting old natives directory {}", path.display());
-
- fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO {
- what: "reading natives entry",
- error: e
- })?;
-
- return Ok(true);
- }
-
- Ok(false)
- }))
- .try_buffer_unordered(32)
- .try_fold(0usize, |accum, res| async move {
- match res {
- true => Ok(accum + 1),
- _ => Ok(accum)
- }
- }).await
- }
-
- async fn extract_natives(&self, libs: Vec<LibraryExtractJob>) -> Result<PathBuf, LaunchError> {
- fs::create_dir_all(&self.natives).await.map_err(|e| LaunchError::IO {
- what: "creating natives directory",
- error: e
- })?;
-
- let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
- let natives_dir = self.natives.join(format!("{}{}-{}", constants::NATIVES_PREFIX, time, process::id()));
-
- // create_dir_all suppresses "AlreadyExists", but this is a fatal error here.
- fs::create_dir(&natives_dir).await.map_err(|e| LaunchError::IO {
- what: "creating natives directory",
- error: e
- })?;
-
- let (path_again, extracted) = tokio::task::spawn_blocking(move || {
- let mut tally = 0usize;
-
- for job in libs {
- debug!("Extracting natives for {}", job.source.display());
- tally += extract::extract_zip(&job.source, &natives_dir, |name|
- job.rule.as_ref().is_none_or(|rules|
- rules.exclude.iter().any(|ex|
- name.starts_with(ex.as_str()))))?;
- }
-
- Ok((natives_dir, tally))
- }).await.unwrap().map_err(LaunchError::LibraryExtractError)?;
-
- info!("Done extracting natives! Copied {} files.", extracted);
-
- Ok(path_again)
- }
-}
-
-impl SystemInfo {
- fn new() -> SystemInfo {
- let os = match OS {
- "windows" => OperatingSystem::Windows,
- "macos" => OperatingSystem::MacOS,
- "linux" => OperatingSystem::Linux,
- _ => OperatingSystem::Unknown // could probably consider "hurd" and "*bsd" to be linux...
- };
-
- let mut os_version = System::os_version().unwrap_or_default();
- if os == OperatingSystem::Windows && (os_version.starts_with("10") || os_version.starts_with("11")) {
- os_version.replace_range(..2, "10.0"); // minecraft expects this funny business...
- }
-
- let mut arch = ARCH.to_owned();
- if arch == "x86_64" {
- // this nomenclature is preferred, since some versions expect the arch containing "x86" to mean 32-bit.
- arch.replace_range(.., "amd64");
- }
-
- SystemInfo {
- os,
- os_version,
- arch
- }
- }
-
- fn is_our_os(&self, os: OperatingSystem) -> bool {
- if self.os == OperatingSystem::Unknown {
- return false;
- }
-
- self.os == os
- }
-
- fn applies(&self, restriction: &OSRestriction) -> bool {
- restriction.os.is_none_or(|os| self.is_our_os(os))
- && restriction.version.as_deref().is_none_or(|pat| pat.is_match(&self.os_version))
- && restriction.arch.as_deref().is_none_or(|pat| pat.is_match(&self.arch))
- }
-}
diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs
deleted file mode 100644
index 7c5dcf3..0000000
--- a/src/launcher/assets.rs
+++ /dev/null
@@ -1,322 +0,0 @@
-use std::error::Error;
-use std::ffi::OsStr;
-use std::fmt::{Display, Formatter};
-use std::io::ErrorKind;
-use std::path::{Path, PathBuf};
-use std::path::Component::Normal;
-use futures::{stream, TryStreamExt};
-use log::{debug, info, warn};
-use reqwest::Client;
-use sha1_smol::Sha1;
-use tokio::{fs, io};
-use tokio::fs::File;
-use crate::assets::{Asset, AssetIndex};
-use crate::launcher::download::{MultiDownloader, VerifiedDownload};
-use crate::util;
-use crate::util::{FileVerifyError, IntegrityError};
-use crate::version::DownloadInfo;
-
-const INDEX_PATH: &str = "indexes";
-const OBJECT_PATH: &str = "objects";
-
-pub struct AssetRepository {
- online: bool,
- home: PathBuf
-}
-
-#[derive(Debug)]
-pub enum AssetError {
- InvalidId(Option<String>),
- IO { what: &'static str, error: io::Error },
- IndexParse(serde_json::Error),
- Offline,
- MissingURL,
- DownloadIndex(reqwest::Error),
- Integrity(IntegrityError),
- AssetObjectDownload,
- AssetVerifyError(FileVerifyError),
- AssetNameError(&'static str)
-}
-
-impl Display for AssetError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- AssetError::InvalidId(None) => f.write_str("missing asset index id"),
- AssetError::InvalidId(Some(id)) => write!(f, "invalid asset index id: {}", id),
- AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error),
- AssetError::IndexParse(error) => write!(f, "error parsing asset index: {}", error),
- AssetError::Offline => f.write_str("cannot download asset index while offline"),
- AssetError::MissingURL => f.write_str("missing asset index URL"),
- AssetError::DownloadIndex(e) => write!(f, "error downloading asset index: {}", e),
- AssetError::Integrity(e) => write!(f, "asset index integrity error: {}", e),
- AssetError::AssetObjectDownload => f.write_str("asset object download failed"),
- AssetError::AssetVerifyError(e) => write!(f, "error verifying asset object: {e}"),
- AssetError::AssetNameError(e) => write!(f, "invalid asset name: {e}")
- }
- }
-}
-
-impl Error for AssetError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- AssetError::IO { error, .. } => Some(error),
- AssetError::IndexParse(error) => Some(error),
- AssetError::DownloadIndex(error) => Some(error),
- AssetError::Integrity(error) => Some(error),
- AssetError::AssetVerifyError(error) => Some(error),
- _ => None
- }
- }
-}
-
-impl From<(&'static str, io::Error)> for AssetError {
- fn from((what, error): (&'static str, io::Error)) -> Self {
- AssetError::IO { what, error }
- }
-}
-
-impl AssetRepository {
- pub async fn new(online: bool, home: impl AsRef<Path>) -> Result<AssetRepository, io::Error> {
- let home = home.as_ref().to_owned();
-
- match fs::create_dir_all(&home).await {
- Ok(_) => (),
- Err(e) => match e.kind() {
- ErrorKind::AlreadyExists => (),
- _ => return Err(e)
- }
- };
-
- Ok(AssetRepository {
- online,
- home
- })
- }
-
- pub fn get_home(&self) -> &Path {
- self.home.as_path()
- }
-
- fn get_index_path(&self, id: &str) -> Result<PathBuf, AssetError> {
- let mut indexes_path: PathBuf = [self.home.as_ref(), OsStr::new(INDEX_PATH)].iter().collect();
- let Some(Normal(path)) = Path::new(id).components().last() else {
- return Err(AssetError::InvalidId(Some(id.into())));
- };
-
- let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?;
-
- // FIXME: change this once "add_extension" is stabilized
- indexes_path.push(format!("{}.json", path));
-
- Ok(indexes_path)
- }
-
- pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> {
- let Some(id) = id else {
- return Err(AssetError::InvalidId(None));
- };
-
- info!("Loading asset index {}", id);
-
- let path = self.get_index_path(id)?;
- debug!("Asset index {} is located at {}", id, path.display());
-
- match util::verify_file(&path, index.size, index.sha1).await {
- Ok(_) => {
- debug!("Asset index {} verified on disk. Loading it.", id);
- let idx_data = fs::read_to_string(&path).await.map_err(|e| AssetError::IO {
- what: "reading asset index",
- error: e
- })?;
-
- return serde_json::from_str(&idx_data).map_err(AssetError::IndexParse);
- },
- Err(FileVerifyError::Open(_, e)) => match e.kind() {
- ErrorKind::NotFound => {
- debug!("Asset index {} not found on disk. Must download it.", id);
- },
- _ => return Err(("opening asset index", e).into())
- },
- Err(FileVerifyError::Integrity(_, e)) => {
- info!("Asset index {} has mismatched integrity: {}, must download it.", id, e);
- let _ = fs::remove_file(&path).await.map_err(|e| warn!("Error deleting modified index {}: {} (ignored)", id, e));
- },
- Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into())
- }
-
- if !self.online {
- warn!("Must download asset index {}, but the launcher is in offline mode. Please try again in online mode.", id);
- return Err(AssetError::Offline);
- }
-
- let Some(url) = index.url.as_ref() else {
- return Err(AssetError::MissingURL);
- };
-
- debug!("Downloading asset index {} from {}", id, url);
-
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent).await.map_err(|e| AssetError::IO {
- what: "creating asset index folder",
- error: e
- })?;
- }
-
- let idx_text = reqwest::get(url).await
- .map_err(AssetError::DownloadIndex)?
- .text().await
- .map_err(AssetError::DownloadIndex)?;
-
- if index.size.is_some_and(|s| s != idx_text.len()) {
- return Err(AssetError::Integrity(IntegrityError::SizeMismatch {
- expect: index.size.unwrap(),
- actual: idx_text.len()
- }));
- }
-
- if let Some(expect) = index.sha1 {
- let actual = Sha1::from(&idx_text).digest();
-
- if actual != expect {
- return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
- }
- }
-
- debug!("Saving downloaded asset index to {}", path.display());
- fs::write(&path, &idx_text).await.map_err(|e| AssetError::IO {
- what: "writing asset index",
- error: e
- })?;
-
- serde_json::from_str(&idx_text).map_err(AssetError::IndexParse)
- }
-
- fn get_object_url(obj: &Asset) -> String {
- format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash)
- }
-
- pub fn get_object_path(&self, obj: &Asset) -> PathBuf {
- let hex_digest = obj.hash.to_string();
- [self.home.as_ref(), OsStr::new(OBJECT_PATH), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect()
- }
-
- async fn ensure_dir(path: impl AsRef<Path>) -> Result<(), io::Error> {
- match fs::create_dir(path).await {
- Ok(_) => Ok(()),
- Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
- Err(e) => Err(e)
- }
- }
-
- pub async fn ensure_assets(&self, index: &AssetIndex) -> Result<(), AssetError> {
- let mut downloads = Vec::new();
- let objects_path = [self.home.as_ref(), OsStr::new(OBJECT_PATH)].iter().collect::<PathBuf>();
-
- Self::ensure_dir(&objects_path).await.map_err(|e| AssetError::IO {
- what: "creating objects directory",
- error: e
- })?;
-
- for object in index.objects.values() {
- let path = self.get_object_path(object);
-
- Self::ensure_dir(path.parent().unwrap()).await.map_err(|error| AssetError::IO { error, what: "creating directory for object" })?;
-
- downloads.push(VerifiedDownload::new(&Self::get_object_url(object), &path, Some(object.size), Some(object.hash)));
- }
-
- if self.online {
- info!("Downloading {} asset objects...", downloads.len());
- let client = Client::new();
- MultiDownloader::with_concurrent(downloads.iter_mut(), 32).perform(&client).await
- .inspect_err(|e| warn!("asset download failed: {e}"))
- .try_fold((), |_, _| async {Ok(())})
- .await
- .map_err(|_| AssetError::AssetObjectDownload)?;
- } else {
- info!("Verifying {} asset objects...", downloads.len());
- super::download::verify_files(downloads.iter_mut()).await.map_err(AssetError::AssetVerifyError)?;
- }
-
- Ok(())
- }
-
- pub async fn reconstruct_assets(&self, index: &AssetIndex, instance_path: &Path, index_id: Option<&str>) -> Result<Option<PathBuf>, AssetError> {
- let target_path: PathBuf;
- let Some(index_id) = index_id else {
- return Err(AssetError::InvalidId(None));
- };
-
- if index.virtual_assets {
- target_path = [self.home.as_ref(), OsStr::new("virtual"), OsStr::new(index_id)].iter().collect();
- } else if index.map_to_resources {
- target_path = [instance_path, Path::new("resources")].iter().collect();
- } else {
- info!("This asset index does not request a virtual assets folder. Nothing to be done.");
- return Ok(None);
- }
-
- info!("Reconstructing virtual assets for {}", index_id);
-
- fs::create_dir_all(&target_path).await.map_err(|e| AssetError::from(("creating virtual assets directory", e)))?;
-
- stream::iter(index.objects.values()
- .map(|object| {
- let obj_path = util::check_path(object.name.as_str()).map_err(AssetError::AssetNameError)?;
- let obj_path = target_path.join(obj_path);
-
- Ok((object, obj_path))
- }))
- .try_filter_map(|(object, obj_path)| async move {
- match util::verify_file(&obj_path, Some(object.size), Some(object.hash)).await {
- Ok(_) => {
- debug!("Not copying asset {}, integrity matches.", object.name);
- Ok(None)
- }
- Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => {
- debug!("Copying asset {}, file does not exist.", object.name);
- Ok(Some((object, obj_path)))
- },
- Err(FileVerifyError::Integrity(_, e)) => {
- debug!("Copying asset {}: {}", object.name, e);
- Ok(Some((object, obj_path)))
- },
- Err(e) => {
- debug!("Error while reconstructing assets: {e}");
- Err(AssetError::AssetVerifyError(e))
- }
- }
- })
- .try_for_each_concurrent(32, |(object, obj_path)| async move {
- if let Some(parent) = obj_path.parent() {
- fs::create_dir_all(parent).await
- .inspect_err(|e| debug!("Error creating directory for asset object {}: {e}", object.name))
- .map_err(|e| AssetError::from(("creating asset object directory", e)))?;
- }
-
- let mut fromfile = File::open(self.get_object_path(object)).await
- .map_err(|e| AssetError::from(("opening source object", e)))?;
- let mut tofile = File::create(&obj_path).await
- .map_err(|e| AssetError::from(("creating target object", e)))?;
-
- io::copy(&mut fromfile, &mut tofile).await.map_err(|e| AssetError::from(("copying asset object", e)))?;
- debug!("Copied object {} to {}.", object.name, obj_path.display());
- Ok(())
- }).await.map(|_| Some(target_path))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_it() {
- let digest_str = "ad1115931887a73cd596300f2c93f84adf39521d";
- assert_eq!(AssetRepository::get_object_url(&Asset {
- name: String::from("test"),
- hash: digest_str.parse().unwrap(),
- size: 0usize
- }), "https://resources.download.minecraft.net/ad/ad1115931887a73cd596300f2c93f84adf39521d");
- }
-}
diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs
deleted file mode 100644
index 4506ab5..0000000
--- a/src/launcher/constants.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use lazy_static::lazy_static;
-use regex::Regex;
-
-pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
-pub const URL_RESOURCE_BASE: &str = "https://resources.download.minecraft.net/";
-pub const URL_JRE_MANIFEST: &str = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
-
-pub const NATIVES_PREFIX: &str = "natives-";
-
-pub const DEF_INSTANCE_NAME: &str = "default";
-pub const DEF_PROFILE_NAME: &str = "default";
-
-// https://github.com/unmojang/FjordLauncher/pull/14/files
-// https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&redirect_uri=ms-xal-00000000402b5328://auth&response_type=token&display=touch&scope=service::user.auth.xboxlive.com::MBI_SSL%20offline_access&prompt=select_account
-
-lazy_static! {
- pub static ref NATIVES_DIR_PATTERN: Regex = Regex::new("^natives-(\\d+)").unwrap();
-}
diff --git a/src/launcher/download.rs b/src/launcher/download.rs
deleted file mode 100644
index 132cd7f..0000000
--- a/src/launcher/download.rs
+++ /dev/null
@@ -1,267 +0,0 @@
-use std::error::Error;
-use std::fmt::{Debug, Display, Formatter};
-use std::path::{Path, PathBuf};
-use futures::{stream, StreamExt, TryStream, TryStreamExt};
-use log::debug;
-use reqwest::{Client, Method, RequestBuilder};
-use sha1_smol::{Digest, Sha1};
-use tokio::fs;
-use tokio::fs::File;
-use tokio::io::{self, AsyncWriteExt};
-use crate::util;
-use crate::util::{FileVerifyError, IntegrityError, USER_AGENT};
-
-pub trait Download: Debug + Display {
- // return Ok(None) to skip downloading this file
- async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>>;
- async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>>;
- async fn finish(&mut self) -> Result<(), Box<dyn Error>>;
-}
-
-pub trait FileDownload: Download {
- fn get_path(&self) -> &Path;
-}
-
-pub struct MultiDownloader<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> {
- jobs: I,
- nconcurrent: usize
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum Phase {
- Prepare,
- Send,
- Receive,
- HandleChunk,
- Finish
-}
-
-impl Display for Phase {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- /* an error occurred while (present participle) ... */
- Self::Prepare => f.write_str("preparing the request"),
- Self::Send => f.write_str("sending the request"),
- Self::Receive => f.write_str("receiving response data"),
- Self::HandleChunk => f.write_str("handling response data"),
- Self::Finish => f.write_str("finishing the request"),
- }
- }
-}
-
-pub struct PhaseDownloadError<'j, T: Download> {
- phase: Phase,
- inner: Box<dyn Error>,
- job: &'j T
-}
-
-impl<T: Download> Debug for PhaseDownloadError<'_, T> {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("PhaseDownloadError")
- .field("phase", &self.phase)
- .field("inner", &self.inner)
- .field("job", &self.job)
- .finish()
- }
-}
-
-impl<T: Download> Display for PhaseDownloadError<'_, T> {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "error while {} ({}): {}", self.phase, self.job, self.inner)
- }
-}
-
-impl<T: Download> Error for PhaseDownloadError<'_, T> {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- Some(&*self.inner)
- }
-}
-
-impl<'j, T: Download> PhaseDownloadError<'j, T> {
- fn new(phase: Phase, inner: Box<dyn Error>, job: &'j T) -> Self {
- PhaseDownloadError {
- phase, inner, job
- }
- }
-}
-
-impl<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> MultiDownloader<'j, T, I> {
- pub fn new(jobs: I) -> MultiDownloader<'j, T, I> {
- Self::with_concurrent(jobs, 24)
- }
-
- pub fn with_concurrent(jobs: I, n: usize) -> MultiDownloader<'j, T, I> {
- assert!(n > 0);
-
- MultiDownloader {
- jobs,
- nconcurrent: n
- }
- }
-
- pub async fn perform(self, client: &'j Client) -> impl TryStream<Ok = (), Error = PhaseDownloadError<'j, T>> {
- stream::iter(self.jobs).map(move |job| Ok(async move {
- macro_rules! map_err {
- ($result:expr, $phase:expr, $job:expr) => {
- match $result {
- Ok(v) => v,
- Err(e) => return Err(PhaseDownloadError::new($phase, e.into(), $job))
- }
- }
- }
-
- let Some(rq) = map_err!(job.prepare(client).await, Phase::Prepare, job) else {
- return Ok(())
- };
-
- let rq = rq.header(reqwest::header::USER_AGENT, USER_AGENT);
-
- let mut data = map_err!(map_err!(rq.send().await, Phase::Send, job).error_for_status(), Phase::Send, job).bytes_stream();
-
- while let Some(bytes) = data.next().await {
- let bytes = map_err!(bytes, Phase::Receive, job);
-
- map_err!(job.handle_chunk(bytes.as_ref()).await, Phase::HandleChunk, job);
- }
-
- job.finish().await.map_err(|e| PhaseDownloadError::new(Phase::Finish, e, job))?;
-
- Ok(())
- })).try_buffer_unordered(self.nconcurrent)
- }
-}
-
-pub struct VerifiedDownload {
- url: String,
- expect_size: Option<usize>,
- expect_sha1: Option<Digest>,
-
- path: PathBuf,
- file: Option<File>,
- sha1: Sha1,
- tally: usize
-}
-
-impl Debug for VerifiedDownload {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("VerifiedDownload")
- .field("url", &self.url)
- .field("expect_size", &self.expect_size)
- .field("expect_sha1", &self.expect_sha1)
- .field("path", &self.path).finish()
- }
-}
-
-impl Display for VerifiedDownload {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "downloading {} to {}", self.url, self.path.display())
- }
-}
-
-impl VerifiedDownload {
- pub fn new(url: &str, path: &Path, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> VerifiedDownload {
- VerifiedDownload {
- url: url.to_owned(),
- path: path.to_owned(),
-
- expect_size,
- expect_sha1,
-
- file: None,
- sha1: Sha1::new(),
- tally: 0
- }
- }
-
- pub fn with_size(mut self, expect: usize) -> VerifiedDownload {
- self.expect_size = Some(expect);
- self
- }
-
- pub fn with_sha1(mut self, expect: Digest) -> VerifiedDownload {
- self.expect_sha1.replace(expect);
- self
- }
-
- pub fn get_url(&self) -> &str {
- &self.url
- }
-
- pub fn get_expect_size(&self) -> Option<usize> {
- self.expect_size
- }
-
- pub fn get_expect_sha1(&self) -> Option<Digest> {
- self.expect_sha1
- }
-
- pub async fn make_dirs(&self) -> Result<(), io::Error> {
- fs::create_dir_all(self.path.parent().expect("download created with no containing directory (?)")).await
- }
-
- async fn open_output(&mut self) -> Result<(), io::Error> {
- self.file.replace(File::create(&self.path).await?);
- Ok(())
- }
-}
-
-impl Download for VerifiedDownload {
- async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>> {
- if !util::should_download(&self.path, self.expect_size, self.expect_sha1).await? {
- return Ok(None)
- }
-
- // potentially racy to close the file and reopen it... :/
- self.open_output().await?;
-
- Ok(Some(client.request(Method::GET, &self.url)))
- }
-
- async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>> {
- self.file.as_mut().unwrap().write_all(chunk).await?;
- self.tally += chunk.len();
- self.sha1.update(chunk);
-
- Ok(())
- }
-
- async fn finish(&mut self) -> Result<(), Box<dyn Error>> {
- let digest = self.sha1.digest();
-
- if let Some(d) = self.expect_sha1 {
- if d != digest {
- debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), d, digest);
- return Err(IntegrityError::Sha1Mismatch { expect: d, actual: digest }.into());
- }
- } else if let Some(s) = self.expect_size {
- if s != self.tally {
- debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), s, self.tally);
- return Err(IntegrityError::SizeMismatch { expect: s, actual: self.tally }.into());
- }
- }
-
- debug!("Successfully downloaded {} ({} bytes).", self.path.display(), self.tally);
-
- // release the file descriptor (don't want to wait until it's dropped automatically because idk when that would be)
- drop(self.file.take().unwrap());
-
- Ok(())
- }
-}
-
-impl FileDownload for VerifiedDownload {
- fn get_path(&self) -> &Path {
- &self.path
- }
-}
-
-pub async fn verify_files(files: impl Iterator<Item = &mut VerifiedDownload>) -> Result<(), FileVerifyError> {
- stream::iter(files)
- .map(|dl| Ok(async move {
- debug!("Verifying library {}", dl.get_path().display());
- util::verify_file(dl.get_path(), dl.get_expect_size(), dl.get_expect_sha1()).await
- }))
- .try_buffer_unordered(32)
- .try_fold((), |_, _| async {Ok(())})
- .await
-}
diff --git a/src/launcher/extract.rs b/src/launcher/extract.rs
deleted file mode 100644
index 8c5f2b8..0000000
--- a/src/launcher/extract.rs
+++ /dev/null
@@ -1,136 +0,0 @@
-use std::error::Error;
-use std::fmt::{Display, Formatter};
-use std::{fs, io, os};
-use std::fs::File;
-use std::io::{BufReader, Error as IOError, Read};
-use std::path::{Path, PathBuf};
-use log::{debug, trace};
-use zip::result::ZipError;
-use zip::ZipArchive;
-use crate::util;
-
-#[derive(Debug)]
-pub enum ZipExtractError {
- IO { what: &'static str, error: IOError },
- Zip { what: &'static str, error: ZipError },
- InvalidEntry { why: &'static str, name: String }
-}
-
-impl From<(&'static str, IOError)> for ZipExtractError {
- fn from((what, error): (&'static str, IOError)) -> Self {
- ZipExtractError::IO { what, error }
- }
-}
-
-impl From<(&'static str, ZipError)> for ZipExtractError {
- fn from((what, error): (&'static str, ZipError)) -> Self {
- ZipExtractError::Zip { what, error }
- }
-}
-
-impl Display for ZipExtractError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- ZipExtractError::IO { what, error } => write!(f, "i/o error ({what}): {error}"),
- ZipExtractError::Zip { what, error } => write!(f, "zip error ({what}): {error}"),
- ZipExtractError::InvalidEntry { why, name } => write!(f, "invalid entry in zip file ({why}): {name}")
- }
- }
-}
-
-impl Error for ZipExtractError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- ZipExtractError::IO { error, .. } => Some(error),
- ZipExtractError::Zip { error, .. } => Some(error),
- _ => None
- }
- }
-}
-
-fn check_entry_path(name: &str) -> Result<&Path, ZipExtractError> {
- util::check_path(name).map_err(|e| ZipExtractError::InvalidEntry {
- why: e,
- name: name.to_owned()
- })
-}
-
-#[cfg(unix)]
-fn extract_symlink(path: impl AsRef<Path>, target: &str) -> io::Result<()> {
- os::unix::fs::symlink(target, path)
-}
-
-#[cfg(windows)]
-fn extract_symlink(path: impl AsRef<Path>, target: &str) -> io::Result<()> {
- os::windows::fs::symlink_file(target, path)
-}
-
-#[cfg(not(any(unix, windows)))]
-fn extract_symlink(path: impl AsRef<Path>, _target: &str) -> io::Result<()> {
- warn!("Refusing to extract symbolic link to {}. I don't know how to do it on this platform!", path.as_ref().display());
- Ok(())
-}
-
-pub fn extract_zip<F>(zip_path: impl AsRef<Path>, extract_root: impl AsRef<Path>, condition: F) -> Result<usize, ZipExtractError>
-where
- F: Fn(&str) -> bool
-{
- debug!("Extracting zip file {} into {}", zip_path.as_ref().display(), extract_root.as_ref().display());
-
- fs::create_dir_all(&extract_root).map_err(|e| ZipExtractError::from(("create extract root", e)))?;
-
- let mut extracted = 0usize;
-
- let file = File::open(&zip_path).map_err(|e| ZipExtractError::from(("extract zip file (open)", e)))?;
- let read = BufReader::new(file);
-
- let mut archive = ZipArchive::new(read).map_err(|e| ZipExtractError::from(("read zip archive", e)))?;
-
- // create directories
- for n in 0..archive.len() {
- let entry = archive.by_index(n).map_err(|e| ZipExtractError::from(("read zip entry (1)", e)))?;
- if !entry.is_dir() { continue; }
-
- let name = entry.name();
- if !condition(name) {
- continue;
- }
-
- let entry_path = check_entry_path(name)?;
- let entry_path: PathBuf = [extract_root.as_ref(), entry_path].iter().collect();
-
- trace!("Extracting directory {} from {}", entry.name(), zip_path.as_ref().display());
- fs::create_dir_all(entry_path).map_err(|e| ZipExtractError::from(("extract directory", e)))?;
- }
-
- // extract the files
- for n in 0..archive.len() {
- let mut entry = archive.by_index(n).map_err(|e| ZipExtractError::from(("read zip entry (2)", e)))?;
- let name = entry.name();
-
- if entry.is_dir() { continue; }
-
- if !condition(name) {
- continue;
- }
-
- let entry_path = check_entry_path(name)?;
- let entry_path: PathBuf = [extract_root.as_ref(), entry_path].iter().collect();
-
- if entry.is_symlink() {
- let mut target = String::new();
- entry.read_to_string(&mut target).map_err(|e| ZipExtractError::from(("read to symlink target", e)))?;
-
- trace!("Extracting symbolic link {} -> {} from {}", entry.name(), target, zip_path.as_ref().display());
- extract_symlink(entry_path.as_path(), target.as_str()).map_err(|e| ZipExtractError::from(("extract symlink", e)))?;
- } else if entry.is_file() {
- let mut outfile = File::create(&entry_path).map_err(|e| ZipExtractError::from(("extract zip entry (open)", e)))?;
-
- trace!("Extracting file {} from {}", entry.name(), zip_path.as_ref().display());
- io::copy(&mut entry, &mut outfile).map_err(|e| ZipExtractError::from(("extract zip entry (write)", e)))?;
- extracted += 1;
- }
- }
-
- Ok(extracted)
-}
diff --git a/src/launcher/jre.rs b/src/launcher/jre.rs
deleted file mode 100644
index 31034b5..0000000
--- a/src/launcher/jre.rs
+++ /dev/null
@@ -1,330 +0,0 @@
-use std::error::Error;
-use std::fmt::{Debug, Display, Formatter};
-use std::path::{Component, Path, PathBuf};
-use std::sync::Arc;
-use futures::{stream, StreamExt, TryStreamExt};
-use log::{debug, info, warn};
-use reqwest::Client;
-use tokio::{fs, io, io::ErrorKind};
-
-mod arch;
-mod manifest;
-mod download;
-
-use arch::JRE_ARCH;
-use manifest::JavaRuntimesManifest;
-use manifest::JavaRuntimeManifest;
-use crate::launcher::download::MultiDownloader;
-use crate::launcher::jre::download::{LzmaDownloadError, LzmaDownloadJob};
-use crate::launcher::jre::manifest::JavaRuntimeFile;
-use crate::util;
-use crate::util::{EnsureFileError, IntegrityError};
-use crate::version::DownloadInfo;
-use super::constants;
-
-pub struct JavaRuntimeRepository {
- online: bool,
- home: PathBuf,
- manifest: JavaRuntimesManifest
-}
-
-impl JavaRuntimeRepository {
- pub async fn new(home: impl AsRef<Path>, online: bool) -> Result<Self, JavaRuntimeError> {
- info!("Java runtime architecture is \"{}\".", JRE_ARCH);
-
- fs::create_dir_all(&home).await.map_err(|e| JavaRuntimeError::IO { what: "creating home directory", error: e })?;
-
- let manifest_path = home.as_ref().join("manifest.json");
- match util::ensure_file(manifest_path.as_path(), Some(constants::URL_JRE_MANIFEST), None, None, online, true).await {
- Ok(_) => (),
- Err(EnsureFileError::Offline) => {
- info!("Launcher is offline, cannot download runtime manifest.");
- },
- Err(e) => return Err(JavaRuntimeError::EnsureFile(e))
- };
-
- let manifest_file = fs::read_to_string(&manifest_path).await
- .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?;
-
- Ok(JavaRuntimeRepository {
- online,
- home: home.as_ref().to_path_buf(),
- manifest: serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtimes manifest", error: e })?,
- })
- }
-
- fn get_component_dir(&self, component: &str) -> PathBuf {
- [self.home.as_path(), Path::new(JRE_ARCH), Path::new(component)].into_iter().collect()
- }
-
- async fn load_runtime_manifest(&self, component: &str, info: &DownloadInfo) -> Result<JavaRuntimeManifest, JavaRuntimeError> {
- let comp_dir = self.get_component_dir(component);
- let manifest_path = comp_dir.join("manifest.json");
-
- debug!("Ensuring manifest for runtime {JRE_ARCH}.{component}");
-
- fs::create_dir_all(comp_dir.as_path()).await
- .inspect_err(|e| warn!("Failed to create directory for JRE component {}: {}", component, e))
- .map_err(|e| JavaRuntimeError::IO { what: "creating component directory", error: e })?;
-
- util::ensure_file(&manifest_path, info.url.as_deref(), info.size, info.sha1, self.online, false).await
- .map_err(JavaRuntimeError::EnsureFile)?;
-
- let manifest_file = fs::read_to_string(&manifest_path).await
- .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?;
-
- serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtime manifest", error: e })
- }
-
- // not very descriptive function name
- pub async fn choose_runtime(&self, component: &str) -> Result<JavaRuntimeManifest, JavaRuntimeError> {
- let Some(runtime_components) = self.manifest.get(JRE_ARCH) else {
- return Err(JavaRuntimeError::UnsupportedArch(JRE_ARCH));
- };
-
- let Some(runtime_component) = runtime_components.get(component) else {
- return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() });
- };
-
- let Some(runtime) = runtime_component.iter().find(|r| r.availability.progress == 100) else {
- if !runtime_components.is_empty() {
- warn!("Weird: the only java runtimes in {JRE_ARCH}.{component} has a progress of less than 100!");
- }
-
- return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() });
- };
-
- self.load_runtime_manifest(component, &runtime.manifest).await
- }
-
- fn clean_up_runtime_sync(path: &Path, manifest: Arc<JavaRuntimeManifest>) -> Result<(), io::Error> {
- for entry in walkdir::WalkDir::new(path).contents_first(true) {
- let entry = entry?;
- let rel_path = entry.path().strip_prefix(path).expect("walkdir escaped root (???)");
-
- if !rel_path.components().any(|c| !matches!(&c, Component::CurDir)) {
- // if this path is trivial (points at the root), ignore it
- continue;
- }
-
- let rel_path_str = if std::path::MAIN_SEPARATOR != '/' {
- rel_path.to_str().map(|s| s.replace(std::path::MAIN_SEPARATOR, "/"))
- } else {
- rel_path.to_str().map(String::from)
- };
-
- if !rel_path_str.as_ref().is_some_and(|s| manifest.files.get(s)
- .is_some_and(|f| (f.is_file() == entry.file_type().is_file())
- || (f.is_directory() == entry.file_type().is_dir())
- || (f.is_link() == entry.file_type().is_symlink()))) {
- // path is invalid utf-8, extraneous, or of the wrong type
- debug!("File {} is extraneous or of wrong type ({:?}). Deleting it.", entry.path().display(), entry.file_type());
-
- if entry.file_type().is_dir() {
- std::fs::remove_dir(entry.path())?;
- } else {
- std::fs::remove_file(entry.path())?;
- }
- }
- }
-
- Ok(())
- }
-
- async fn clean_up_runtime(path: &Path, manifest: Arc<JavaRuntimeManifest>) -> Result<(), io::Error> {
- let (tx, rx) = tokio::sync::oneshot::channel();
-
- let path = path.to_owned();
- let manifest = manifest.clone();
-
- tokio::task::spawn_blocking(move || {
- let res = Self::clean_up_runtime_sync(&path, manifest);
- let _ = tx.send(res);
- }).await.expect("clean_up_runtime_sync panicked");
-
- rx.await.expect("clean_up_runtime_sync hung up")
- }
-
- async fn ensure_jre_dirs(&self, path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> {
- stream::iter(manifest.files.iter().filter(|(_, f)| f.is_directory()))
- .map::<Result<&String, JavaRuntimeError>, _>(|(name, _)| Ok(name))
- .try_for_each(|name| async move {
- let ent_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?;
- let ent_path = [path, ent_path].into_iter().collect::<PathBuf>();
-
- match fs::metadata(&ent_path).await {
- Ok(meta) => {
- if !meta.is_dir() {
- debug!("Deleting misplaced file at {}", ent_path.display());
- fs::remove_file(&ent_path).await.map_err(|e| JavaRuntimeError::IO {
- what: "deleting misplaced file",
- error: e
- })?;
- }
- },
- Err(e) if e.kind() == ErrorKind::NotFound => (),
- Err(e) => return Err(JavaRuntimeError::IO { what: "'stat'ing directory", error: e })
- }
-
- match fs::create_dir(&ent_path).await {
- Ok(_) => {
- debug!("Created directory at {}", ent_path.display());
- Ok(())
- },
- Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
- Err(e) => {
- warn!("Could not create directory {} for JRE!", ent_path.display());
- Err(JavaRuntimeError::IO { what: "creating directory", error: e })
- }
- }
- }).await
- }
-
- async fn ensure_jre_files(path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> {
- let mut downloads = Vec::new();
- for (name, file) in manifest.files.iter().filter(|(_, f)| f.is_file()) {
- let file_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?;
- let file_path = [path, file_path].into_iter().collect::<PathBuf>();
-
- downloads.push(LzmaDownloadJob::try_from((file, file_path)).map_err(|e| {
- match e {
- LzmaDownloadError::MissingURL => JavaRuntimeError::MalformedManifest("runtime manifest missing URL"),
- LzmaDownloadError::NotAFile => unreachable!("we just made sure this was a file")
- }
- })?);
- }
-
- let dl = MultiDownloader::new(downloads.iter_mut());
- let client = Client::new();
-
- dl.perform(&client).await
- .inspect_err(|e| warn!("jre file download failed: {e}"))
- .try_fold((), |_, _| async { Ok(()) })
- .await
- .map_err(|_| JavaRuntimeError::MultiDownloadError)
- }
-
- async fn ensure_links(root_path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> {
- stream::iter(manifest.files.iter().filter(|(_, f)| f.is_link()))
- .map::<Result<_, JavaRuntimeError>, _>(|(name, file)| Ok(async move {
- let JavaRuntimeFile::Link { target } = file else {
- unreachable!();
- };
-
- let target_exp = PathBuf::from(target);
-
- let path = util::check_path(name.as_str()).map_err(JavaRuntimeError::MalformedManifest)?;
- let link_path = [root_path, path].into_iter().collect::<PathBuf>();
-
- match fs::read_link(&link_path).await {
- Ok(target_path) => {
- if target_path == target_exp {
- debug!("Symbolic link at {} matches! Nothing to be done.", link_path.display());
- return Ok(())
- }
-
- debug!("Symbolic link at {} does not match (exp {}, got {}). Recreating it.", link_path.display(), target_exp.display(), target_path.display());
- fs::remove_file(&link_path).await.map_err(|e| JavaRuntimeError::IO {
- what: "deleting bad symlink",
- error: e
- })?;
- }
- Err(e) if e.kind() == ErrorKind::NotFound => (),
- Err(e) => return Err(JavaRuntimeError::IO { what: "reading jre symlink", error: e })
- }
-
- debug!("Creating symbolic link at {} to {}", link_path.display(), target_exp.display());
-
- let symlink;
- #[cfg(unix)]
- {
- symlink = |targ, path| async { fs::symlink(targ, path).await };
- }
-
- #[cfg(windows)]
- {
- symlink = |targ, path| async { fs::symlink_file(targ, path).await };
- }
-
- #[cfg(not(any(unix, windows)))]
- {
- symlink = |_, _| async { Ok(()) };
- }
-
- symlink(target_exp, link_path).await.map_err(|e| JavaRuntimeError::IO {
- what: "creating symlink",
- error: e
- })?;
-
- Ok(())
- }))
- .try_buffer_unordered(32)
- .try_fold((), |_, _| async { Ok(()) }).await
- }
-
- pub async fn ensure_jre(&self, component: &str, manifest: JavaRuntimeManifest) -> Result<PathBuf, JavaRuntimeError> {
- let runtime_path = self.get_component_dir(component);
- let runtime_path = runtime_path.join("runtime");
- let manifest = Arc::new(manifest);
-
- fs::create_dir_all(&runtime_path).await
- .map_err(|e| JavaRuntimeError::IO { what: "creating runtime directory", error: e })?;
-
- debug!("Cleaning up JRE directory for {component}");
- Self::clean_up_runtime(runtime_path.as_path(), manifest.clone()).await
- .map_err(|e| JavaRuntimeError::IO { what: "cleaning up runtime directory", error: e })?;
-
- debug!("Building directory structure for {component}");
- self.ensure_jre_dirs(&runtime_path, manifest.as_ref()).await?;
-
- debug!("Downloading JRE files for {component}");
- Self::ensure_jre_files(&runtime_path, manifest.as_ref()).await?;
-
- debug!("Ensuring symbolic links for {component}");
- Self::ensure_links(&runtime_path, manifest.as_ref()).await?;
-
- Ok(runtime_path)
- }
-}
-
-#[derive(Debug)]
-pub enum JavaRuntimeError {
- EnsureFile(EnsureFileError),
- IO { what: &'static str, error: io::Error },
- Download { what: &'static str, error: reqwest::Error },
- Deserialize { what: &'static str, error: serde_json::Error },
- UnsupportedArch(&'static str),
- UnsupportedComponent { arch: &'static str, component: String },
- MalformedManifest(&'static str),
- Integrity(IntegrityError),
- MultiDownloadError
-}
-
-impl Display for JavaRuntimeError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- JavaRuntimeError::EnsureFile(e) => std::fmt::Display::fmt(e, f),
- JavaRuntimeError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error),
- JavaRuntimeError::Download { what, error } => write!(f, "error downloading {}: {}", what, error),
- JavaRuntimeError::Deserialize { what, error } => write!(f, "error deserializing ({what}): {error}"),
- JavaRuntimeError::UnsupportedArch(arch) => write!(f, r#"unsupported architecture "{arch}""#),
- JavaRuntimeError::UnsupportedComponent { arch, component } => write!(f, r#"unsupported component "{component}" for architecture "{arch}""#),
- JavaRuntimeError::MalformedManifest(what) => write!(f, "malformed runtime manifest: {what} (launcher bug?)"),
- JavaRuntimeError::Integrity(e) => std::fmt::Display::fmt(e, f),
- JavaRuntimeError::MultiDownloadError => f.write_str("error in multi downloader (see logs for more details)")
- }
- }
-}
-
-impl Error for JavaRuntimeError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- JavaRuntimeError::EnsureFile(error) => Some(error),
- JavaRuntimeError::IO { error, .. } => Some(error),
- JavaRuntimeError::Download { error, .. } => Some(error),
- JavaRuntimeError::Deserialize { error, .. } => Some(error),
- JavaRuntimeError::Integrity(error) => Some(error),
- _ => None
- }
- }
-}
diff --git a/src/launcher/jre/arch.rs b/src/launcher/jre/arch.rs
deleted file mode 100644
index e984171..0000000
--- a/src/launcher/jre/arch.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use cfg_if::cfg_if;
-
-macro_rules! define_arch {
- ($arch:expr) => {
- pub const JRE_ARCH: &str = $arch;
- }
-}
-
-cfg_if! {
- if #[cfg(target_os = "windows")] {
- cfg_if! {
- if #[cfg(target_arch = "x86_64")] {
- define_arch!("windows-x64");
- } else if #[cfg(target_arch = "x86")] {
- define_arch!("windows-x86");
- } else if #[cfg(target_arch = "aarch64")] {
- define_arch!("windows-arm64");
- } else {
- define_arch!("gamecore");
- }
- }
- } else if #[cfg(target_os = "linux")] {
- cfg_if! {
- if #[cfg(target_arch = "x86_64")] {
- define_arch!("linux");
- } else if #[cfg(target_arch = "x86")] {
- define_arch!("linux-i386");
- } else {
- define_arch!("gamecore");
- }
- }
- } else if #[cfg(target_os = "macos")] {
- cfg_if! {
- if #[cfg(target_arch = "aarch64")] {
- define_arch!("mac-os-arm64");
- } else if #[cfg(target_arch = "x86_64")] {
- define_arch!("mac-os");
- } else {
- define_arch!("gamecore");
- }
- }
- } else {
- define_arch!("gamecore");
- }
-}
diff --git a/src/launcher/jre/download.rs b/src/launcher/jre/download.rs
deleted file mode 100644
index ddf1ff6..0000000
--- a/src/launcher/jre/download.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-use std::error::Error;
-use std::fmt::{Debug, Display, Formatter};
-use std::io::Write;
-use std::path::{PathBuf};
-use log::debug;
-use lzma_rs::decompress;
-use reqwest::{Client, RequestBuilder};
-use sha1_smol::{Digest, Sha1};
-use tokio::io::AsyncWriteExt;
-use tokio::fs::File;
-use crate::launcher::download::Download;
-use crate::launcher::jre::manifest::JavaRuntimeFile;
-use crate::util;
-use crate::util::IntegrityError;
-use crate::version::DownloadInfo;
-
-pub enum LzmaDownloadError {
- NotAFile,
- MissingURL
-}
-
-pub struct LzmaDownloadJob {
- url: String,
- path: PathBuf,
- inflate: bool,
- executable: bool,
-
- raw_size: Option<usize>,
- raw_sha1: Option<Digest>,
-
- raw_sha1_st: Sha1,
- raw_tally: usize,
-
- stream: Option<decompress::Stream<Vec<u8>>>,
- out_file: Option<File>
-}
-
-impl LzmaDownloadJob {
- fn new_inflate(raw: &DownloadInfo, lzma: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> {
- Ok(LzmaDownloadJob {
- url: lzma.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?,
- path,
- inflate: true,
- executable: exe,
-
- raw_size: raw.size,
- raw_sha1: raw.sha1,
-
- raw_sha1_st: Sha1::new(),
- raw_tally: 0,
-
- stream: Some(decompress::Stream::new(Vec::new())),
- out_file: None
- })
- }
-
- fn new_raw(raw: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> {
- Ok(LzmaDownloadJob {
- url: raw.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?,
- path,
- inflate: false,
- executable: exe,
-
- raw_size: raw.size,
- raw_sha1: raw.sha1,
-
- raw_sha1_st: Sha1::new(),
- raw_tally: 0,
-
- stream: None,
- out_file: None
- })
- }
-}
-
-impl TryFrom<(&JavaRuntimeFile, PathBuf)> for LzmaDownloadJob {
- type Error = LzmaDownloadError;
-
- fn try_from((file, path): (&JavaRuntimeFile, PathBuf)) -> Result<Self, Self::Error> {
- if !file.is_file() {
- return Err(LzmaDownloadError::NotAFile);
- }
-
- let JavaRuntimeFile::File { executable, downloads } = file else {
- unreachable!("we just made sure this was a file");
- };
-
- match downloads.lzma.as_ref() {
- Some(lzma) => LzmaDownloadJob::new_inflate(&downloads.raw, lzma, *executable, path),
- None => LzmaDownloadJob::new_raw(&downloads.raw, *executable, path)
- }
- }
-}
-
-impl Debug for LzmaDownloadJob {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("LzmaDownloadJob")
- .field("url", &self.url)
- .field("path", &self.path)
- .field("inflate", &self.inflate)
- .finish()
- }
-}
-
-impl Display for LzmaDownloadJob {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- if self.inflate {
- write!(f, "download and inflate {} to {}", &self.url, self.path.display())
- } else {
- write!(f, "download {} to {}", &self.url, self.path.display())
- }
- }
-}
-
-impl Download for LzmaDownloadJob {
- async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>> {
- if !util::should_download(&self.path, self.raw_size, self.raw_sha1).await? {
- return Ok(None)
- }
-
- let mut options = File::options();
-
- #[cfg(unix)]
- {
- options.mode(match self.executable {
- true => 0o775,
- _ => 0o664
- });
- }
-
- let file = options.create(true).write(true).truncate(true).open(&self.path).await?;
- self.out_file = Some(file);
-
- Ok(Some(client.get(&self.url)))
- }
-
- async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>> {
- let out_file = self.out_file.as_mut().expect("output file gone");
-
- if let Some(ref mut stream) = self.stream {
- stream.write_all(chunk)?;
- let buf = stream.get_output_mut().expect("stream output missing before finish()");
-
- out_file.write_all(buf.as_slice()).await?;
-
- self.raw_sha1_st.update(buf.as_slice());
- self.raw_tally += buf.len();
-
- buf.truncate(0);
- } else {
- out_file.write_all(chunk).await?;
-
- self.raw_sha1_st.update(chunk);
- self.raw_tally += chunk.len();
- }
-
- Ok(())
- }
-
- async fn finish(&mut self) -> Result<(), Box<dyn Error>> {
- let mut out_file = self.out_file.take().expect("output file gone");
-
- if let Some(stream) = self.stream.take() {
- let buf = stream.finish()?;
-
- out_file.write_all(buf.as_slice()).await?;
-
- self.raw_sha1_st.update(buf.as_slice());
- self.raw_tally += buf.len();
- }
-
- let inf_digest = self.raw_sha1_st.digest();
- if let Some(sha1) = self.raw_sha1 {
- if inf_digest != sha1 {
- debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), sha1, inf_digest);
- return Err(IntegrityError::Sha1Mismatch {
- expect: sha1,
- actual: inf_digest
- }.into());
- }
- }
-
- if let Some(size) = self.raw_size {
- if self.raw_tally != size {
- debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), size, self.raw_tally);
- return Err(IntegrityError::SizeMismatch {
- expect: size,
- actual: self.raw_tally
- }.into());
- }
- }
-
- Ok(())
- }
-}
diff --git a/src/launcher/jre/manifest.rs b/src/launcher/jre/manifest.rs
deleted file mode 100644
index 3fd6484..0000000
--- a/src/launcher/jre/manifest.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use std::collections::HashMap;
-use indexmap::IndexMap;
-use serde::Deserialize;
-use crate::version::DownloadInfo;
-
-#[derive(Debug, Deserialize)]
-pub struct Availability {
- pub group: u32, // unknown meaning
- pub progress: u32 // unknown meaning
-}
-
-#[derive(Debug, Deserialize)]
-pub struct Version {
- pub name: String,
- pub version: String
-}
-
-#[derive(Debug, Deserialize)]
-pub struct JavaRuntimeInfo {
- // I don't see how half of this information is useful with how the JRE system currently functions -figboot
- pub availability: Availability,
- pub manifest: DownloadInfo,
- //pub version: Version
-}
-
-pub type JavaRuntimesManifest = HashMap<String, HashMap<String, Vec<JavaRuntimeInfo>>>;
-
-#[derive(Debug, Deserialize)]
-pub struct FileDownloads {
- pub lzma: Option<DownloadInfo>,
- pub raw: DownloadInfo
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "lowercase", tag = "type")]
-pub enum JavaRuntimeFile {
- File {
- #[serde(default)]
- executable: bool,
- downloads: Box<FileDownloads>
- },
- Directory,
- Link {
- target: String
- }
-}
-
-impl JavaRuntimeFile {
- pub fn is_file(&self) -> bool {
- matches!(*self, JavaRuntimeFile::File { .. })
- }
-
- pub fn is_directory(&self) -> bool {
- matches!(*self, JavaRuntimeFile::Directory)
- }
-
- pub fn is_link(&self) -> bool {
- matches!(*self, JavaRuntimeFile::Link { .. })
- }
-}
-
-#[derive(Debug, Deserialize)]
-pub struct JavaRuntimeManifest {
- pub files: IndexMap<String, JavaRuntimeFile>
-}
diff --git a/src/launcher/rules.rs b/src/launcher/rules.rs
deleted file mode 100644
index 29a36d1..0000000
--- a/src/launcher/rules.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-use std::error::Error;
-use std::fmt::Display;
-use crate::version::{Argument, CompatibilityRule, CompleteVersion, FeatureMatcher, Library, OSRestriction, RuleAction};
-use super::SystemInfo;
-
-#[derive(Debug)]
-pub struct IncompatibleError {
- what: &'static str,
- reason: Option<String>
-}
-
-impl Display for IncompatibleError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if let Some(reason) = self.reason.as_ref() {
- write!(f, "{} incompatible: {}", self.what, reason)
- } else {
- write!(f, "{} incompatible", self.what)
- }
- }
-}
-
-impl Error for IncompatibleError {}
-
-mod seal {
- pub trait CompatCheckInner {
- const WHAT: &'static str;
-
- fn get_rules(&self) -> Option<impl IntoIterator<Item = &super::CompatibilityRule>>;
- fn get_incompatibility_reason(&self) -> Option<&str>;
- }
-}
-
-pub trait CompatCheck: seal::CompatCheckInner {
- fn rules_apply(&self, system: &SystemInfo, feature_matcher: &impl FeatureMatcher) -> Result<(), IncompatibleError> {
- let Some(rules) = self.get_rules() else { return Ok(()) };
- let mut action = RuleAction::Disallow;
-
- fn match_os(os: &OSRestriction, system: &SystemInfo) -> bool {
- os.os.is_none_or(|o| system.is_our_os(o))
- && os.version.as_ref().is_none_or(|v| v.is_match(system.os_version.as_str()))
- && os.arch.as_ref().is_none_or(|a| a.is_match(system.arch.as_str()))
- }
-
- for rule in rules {
- if rule.os.as_ref().is_none_or(|o| match_os(o, system))
- && rule.features_match(feature_matcher) {
- action = rule.action;
- }
- }
-
- if action == RuleAction::Disallow {
- Err(IncompatibleError {
- what: Self::WHAT,
- reason: self.get_incompatibility_reason().map(|s| s.to_owned())
- })
- } else {
- Ok(())
- }
- }
-}
-
-// trivial
-impl seal::CompatCheckInner for CompatibilityRule {
- const WHAT: &'static str = "rule";
-
- fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> {
- Some(Some(self))
- }
-
- fn get_incompatibility_reason(&self) -> Option<&str> {
- None
- }
-}
-
-impl seal::CompatCheckInner for CompleteVersion {
- const WHAT: &'static str = "version";
-
- fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> {
- self.compatibility_rules.as_ref()
- }
-
- fn get_incompatibility_reason(&self) -> Option<&str> {
- self.incompatibility_reason.as_deref()
- }
-}
-
-impl seal::CompatCheckInner for Library {
- const WHAT: &'static str = "library";
-
- fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> {
- self.rules.as_ref()
- }
-
- fn get_incompatibility_reason(&self) -> Option<&str> {
- None
- }
-}
-
-impl seal::CompatCheckInner for Argument {
- const WHAT: &'static str = "argument";
-
- fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> {
- self.rules.as_ref()
- }
-
- fn get_incompatibility_reason(&self) -> Option<&str> {
- None
- }
-}
-
-impl CompatCheck for CompatibilityRule {}
-impl CompatCheck for CompleteVersion {}
-impl CompatCheck for Library {}
-impl CompatCheck for Argument {} \ No newline at end of file
diff --git a/src/launcher/runner.rs b/src/launcher/runner.rs
deleted file mode 100644
index afdfc7f..0000000
--- a/src/launcher/runner.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use std::borrow::Cow;
-use std::ffi::{OsStr, OsString};
-use std::iter;
-use std::path::{Path, PathBuf};
-use std::process::Command;
-use log::{debug, warn};
-use tokio::{fs, io};
-use crate::util::AsJavaPath;
-use crate::version::{CompleteVersion, FeatureMatcher, OperatingSystem};
-use super::rules::CompatCheck;
-use super::strsub::{self, SubFunc};
-use super::{Launch, LaunchInfo};
-
-#[derive(Clone, Copy)]
-struct LaunchArgSub<'a, 'l, F: FeatureMatcher>(&'a LaunchInfo<'l, F>);
-
-// FIXME: this is not correct
-#[cfg(windows)]
-const PATH_SEP: &str = ";";
-
-#[cfg(not(windows))]
-const PATH_SEP: &str = ":";
-
-impl<'rep, F: FeatureMatcher> SubFunc<'rep> for LaunchArgSub<'rep, '_, F> {
- fn substitute(&self, key: &str) -> Option<Cow<'rep, str>> {
- match key {
- "assets_index_name" => self.0.asset_index_name.as_ref().map(|s| Cow::Borrowed(s.as_str())),
- "assets_root" => Some(self.0.launcher.assets.get_home().as_java_path().to_string_lossy()),
- "auth_access_token" => Some(Cow::Borrowed("-")), // TODO
- "auth_player_name" => Some(Cow::Borrowed("Player")), // TODO
- "auth_session" => Some(Cow::Borrowed("-")), // TODO
- "auth_uuid" => Some(Cow::Borrowed("00000000-0000-0000-0000-000000000000")), // TODO
- "auth_xuid" => Some(Cow::Borrowed("00000000-0000-0000-0000-000000000000")), // TODO
- "classpath" => Some(Cow::Borrowed(self.0.classpath.as_str())),
- "classpath_separator" => Some(Cow::Borrowed(PATH_SEP)),
- "game_assets" => self.0.virtual_assets_path.as_ref()
- .map(|s| s.as_path().as_java_path().to_string_lossy()),
- "game_directory" => Some(self.0.instance_home.as_java_path().to_string_lossy()),
- "language" => Some(Cow::Borrowed("en-us")), // ???
- "launcher_name" => Some(Cow::Borrowed("ozone (olauncher 3)")), // TODO
- "launcher_version" => Some(Cow::Borrowed("yeah")), // TODO
- "library_directory" => Some(self.0.launcher.libraries.home.as_java_path().to_string_lossy()),
- "natives_directory" => Some(self.0.natives_path.as_java_path().to_string_lossy()),
- "primary_jar" => self.0.client_jar.as_ref().map(|p| p.as_path().as_java_path().to_string_lossy()),
- "quickPlayMultiplayer" => None, // TODO
- "quickPlayPath" => None, // TODO
- "quickPlayRealms" => None, // TODO
- "quickPlaySingleplayer" => None, // TODO
- "resolution_height" => None, // TODO
- "resolution_width" => None, // TODO
- "user_properties" => Some(Cow::Borrowed("{}")), // TODO
- "user_property_map" => Some(Cow::Borrowed("[]")), // TODO
- "user_type" => Some(Cow::Borrowed("legacy")), // TODO
- "version_name" => Some(Cow::Borrowed(self.0.version_id.as_ref())),
- "version_type" => self.0.version_type.as_ref().map(|s| Cow::Borrowed(s.to_str())),
- _ => {
- if let Some(asset_key) = key.strip_prefix("asset=") {
- return self.0.asset_index.as_ref().and_then(|idx| idx.objects.get(asset_key))
- .map(|obj| Cow::Owned(self.0.launcher.assets.get_object_path(obj).as_java_path().to_string_lossy().into_owned()))
- }
-
- None
- }
- }
- }
-}
-
-#[derive(Clone, Copy)]
-pub enum ArgumentType {
- Jvm,
- Game
-}
-
-pub fn build_arguments<F: FeatureMatcher>(launch: &LaunchInfo<'_, F>, version: &CompleteVersion, arg_type: ArgumentType) -> Vec<OsString> {
- let sub = LaunchArgSub(launch);
- let system_info = &launch.launcher.system_info;
-
- if let Some(arguments) = version.arguments.as_ref().and_then(|args| match arg_type {
- ArgumentType::Jvm => args.jvm.as_ref(),
- ArgumentType::Game => args.game.as_ref()
- }) {
- arguments.iter()
- .filter(|wa| wa.rules_apply(system_info, launch.feature_matcher).is_ok())
- .flat_map(|wa| &wa.value)
- .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned())).collect()
- } else if let Some(arguments) = version.minecraft_arguments.as_ref() {
- match arg_type {
- ArgumentType::Jvm => {
- [
- "-Djava.library.path=${natives_directory}",
- "-Dminecraft.launcher.brand=${launcher_name}",
- "-Dminecraft.launcher.version=${launcher_version}",
- "-Dminecraft.client.jar=${primary_jar}",
- "-cp",
- "${classpath}"
- ].into_iter()
- .chain(iter::once("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump")
- .take_while(|_| system_info.os == OperatingSystem::Windows))
- .chain(iter::once(["-Dos.name=Windows 10", "-Dos.version=10.0"])
- .take_while(|_| launch.feature_matcher.matches("__ozone_win10_hack"))
- .flatten())
- .chain(iter::once(["-Xdock:icon=${asset=icons/minecraft.icns}", "-Xdock:name=Minecraft"])
- .take_while(|_| system_info.os == OperatingSystem::MacOS)
- .flatten())
- .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned()))
- .collect()
- },
- ArgumentType::Game => {
- arguments.split(' ')
- .chain(iter::once("--demo")
- .take_while(|_| launch.feature_matcher.matches("is_demo_user")))
- .chain(iter::once(["--width", "${resolution_width}", "--height", "${resolution_height}"])
- .take_while(|_| launch.feature_matcher.matches("has_custom_resolution"))
- .flatten())
- .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned()))
- .collect()
- }
- }
- } else {
- Vec::default()
- }
-}
-
-pub fn run_the_game(launch: &Launch) -> Result<(), Box<dyn std::error::Error>> {
- if launch.runtime_legacy_launch {
- Command::new(launch.runtime_path.as_path().as_java_path())
- .args(launch.jvm_args.iter()
- .map(|o| o.as_os_str())
- .chain(iter::once(OsStr::new(launch.main_class.as_str())))
- .chain(launch.game_args.iter().map(|o| o.as_os_str())))
- .current_dir(launch.instance_path.as_path().as_java_path()).spawn()?.wait()?;
- } else {
- todo!("jni launch not supported :(")
- }
-
- Ok(())
-}
-
-#[allow(dead_code)]
-mod windows {
- pub const JNI_SEARCH_PATH: Option<&str> = Some("server/jvm.dll");
- pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java.exe");
- pub const JRE_PLATFORM_KNOWN: bool = true;
-}
-
-#[allow(dead_code)]
-mod linux {
- pub const JNI_SEARCH_PATH: Option<&str> = Some("server/libjvm.so");
- pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java");
- pub const JRE_PLATFORM_KNOWN: bool = true;
-}
-
-#[allow(dead_code)]
-mod macos {
- pub const JNI_SEARCH_PATH: Option<&str> = Some("server/libjvm.dylib");
- pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java");
- pub const JRE_PLATFORM_KNOWN: bool = true;
-}
-
-#[allow(dead_code)]
-mod unknown {
- pub const JNI_SEARCH_PATH: Option<&str> = None;
- pub const JAVA_SEARCH_PATH: Option<&str> = None;
- pub const JRE_PLATFORM_KNOWN: bool = false;
-}
-
-#[cfg(target_os = "windows")]
-use self::windows::*;
-#[cfg(target_os = "linux")]
-use self::linux::*;
-#[cfg(target_os = "macos")]
-use self::macos::*;
-#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
-use self::unknown::*;
-
-fn search_java_sync(base: impl AsRef<Path>, legacy: bool) -> Result<Option<PathBuf>, io::Error> {
- assert!(JRE_PLATFORM_KNOWN);
- let search_path = Path::new(match legacy {
- true => JAVA_SEARCH_PATH,
- _ => JNI_SEARCH_PATH
- }.unwrap());
-
- let walker = walkdir::WalkDir::new(base.as_ref()).into_iter()
- .filter_map(|e| e.ok())
- .filter(|e| e.file_type().is_dir());
-
- for entry in walker {
- let check_path = [base.as_ref(), entry.path(), Path::new(search_path)].into_iter().collect::<PathBuf>();
- match std::fs::metadata(check_path.as_path()) {
- Err(e) if e.kind() == io::ErrorKind::NotFound => (),
- Err(e) => return Err(e),
- Ok(meta) if meta.is_file() => return Ok(Some(check_path)),
- _ => ()
- }
- }
-
- Ok(None) // not found (sadface)
-}
-
-//noinspection RsConstantConditionIf
-pub async fn find_java(base: impl AsRef<Path>, legacy: bool) -> Result<Option<PathBuf>, io::Error> {
- let meta = fs::metadata(&base).await?;
- if meta.is_dir() { // do search
- if !JRE_PLATFORM_KNOWN {
- warn!("Unknown platform! Cannot search for java executable in {}. Please specify the executable file manually.", base.as_ref().display());
- return Ok(None);
- }
-
- let (tx, rx) = tokio::sync::oneshot::channel();
- let base = base.as_ref().to_path_buf(); // idc
-
- tokio::task::spawn_blocking(move || {
- let res = search_java_sync(base, legacy);
- let _ = tx.send(res); // I really don't care if the reader hung up
- }).await.expect("jre search panicked");
-
- rx.await.expect("jre search didn't send us a result")
- } else { // we are pointed directly at a file. assume it's what we want
- debug!("JRE path {} is a file ({}). Assuming it's what we want.", base.as_ref().display(), legacy);
- Ok(Some(base.as_ref().to_path_buf()))
- }
-}
diff --git a/src/launcher/settings.rs b/src/launcher/settings.rs
deleted file mode 100644
index 8453653..0000000
--- a/src/launcher/settings.rs
+++ /dev/null
@@ -1,232 +0,0 @@
-use std::collections::HashMap;
-use std::error::Error;
-use std::fmt::{Display, Formatter};
-use std::io::ErrorKind;
-use std::path::{Path, PathBuf};
-use log::warn;
-use serde::{Deserialize, Serialize};
-use tokio::{fs, io};
-use tokio::fs::File;
-use tokio::io::AsyncWriteExt;
-use super::constants;
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct SettingsInner {
- profiles: HashMap<String, Profile>,
- instances: HashMap<String, Instance>
-}
-
-pub struct Settings {
- path: Option<PathBuf>,
- inner: SettingsInner
-}
-
-#[derive(Debug)]
-pub enum SettingsError {
- IO { what: &'static str, error: io::Error },
- Format(serde_json::Error),
- Inconsistent(String)
-}
-
-impl Display for SettingsError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- SettingsError::IO { what, error } => write!(f, "settings i/o error ({}): {}", what, error),
- SettingsError::Format(err) => write!(f, "settings format error: {}", err),
- SettingsError::Inconsistent(err) => write!(f, "inconsistent settings: {}", err),
- }
- }
-}
-
-impl Error for SettingsError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- SettingsError::IO { error: err, .. } => Some(err),
- SettingsError::Format(err) => Some(err),
- _ => None
- }
- }
-}
-
-impl Default for SettingsInner {
- fn default() -> Self {
- SettingsInner {
- instances: [(String::from(constants::DEF_INSTANCE_NAME), PathBuf::from(constants::DEF_INSTANCE_NAME).into())].into_iter().collect(),
- profiles: [(String::from(constants::DEF_PROFILE_NAME), Profile::new(constants::DEF_INSTANCE_NAME))].into_iter().collect()
- }
- }
-}
-
-impl Settings {
- async fn load_inner(path: impl AsRef<Path>) -> Result<SettingsInner, SettingsError> {
- match fs::read_to_string(&path).await {
- Ok(data) => serde_json::from_str(data.as_str()).map_err(SettingsError::Format),
- Err(e) if e.kind() == ErrorKind::NotFound => Ok(SettingsInner::default()),
- Err(e) => Err(SettingsError::IO { what: "loading settings", error: e })
- }
- }
-
- fn check_consistent(mut inner: SettingsInner, path: Option<impl AsRef<Path>>) -> Result<Settings, SettingsError> {
- inner.profiles.retain(|name, profile| {
- if !inner.instances.contains_key(&profile.instance) {
- warn!("Settings inconsistency: profile {} refers to instance {}, which does not exist. Ignoring this profile.", name, profile.instance);
- false
- } else {
- true
- }
- });
-
- // there will be more checks later maybe
-
- Ok(Settings {
- path: path.map(|p| p.as_ref().to_owned()),
- inner
- })
- }
-
- pub async fn load(path: impl AsRef<Path>) -> Result<Settings, SettingsError> {
- Self::check_consistent(Self::load_inner(&path).await?, Some(path))
- }
-
- pub fn get_path(&self) -> Option<&Path> {
- self.path.as_deref()
- }
-
- pub async fn save_to(&self, path: impl AsRef<Path>) -> Result<(), SettingsError> {
- let path = path.as_ref();
-
- if let Some(parent) = path.parent() {
- fs::create_dir_all(parent).await
- .map_err(|e| SettingsError::IO { what: "saving settings (creating directory)", error: e })?;
- }
-
- let mut file = File::create(path).await
- .map_err(|e| SettingsError::IO { what: "saving settings (open)", error: e })?;
-
- file.write_all(serde_json::to_string_pretty(&self.inner).map_err(SettingsError::Format)?.as_bytes()).await
- .map_err(|e| SettingsError::IO { what: "saving settings (write)", error: e })?;
-
- Ok(())
- }
-
- pub async fn save(&self) -> Result<(), SettingsError> {
- self.save_to(self.path.as_ref().expect("save() called on Settings instance not loaded from file")).await
- }
-
- pub fn get_instance(&self, name: &str) -> Option<&Instance> {
- self.inner.instances.get(name)
- }
-
- pub fn get_profile(&self, name: &str) -> Option<&Profile> {
- self.inner.profiles.get(name)
- }
-
- pub fn get_instance_for(&self, profile: &Profile) -> &Instance {
- self.inner.instances.get(&profile.instance).unwrap()
- }
-}
-
-#[derive(Deserialize, Serialize, Debug, Clone)]
-pub struct Instance {
- path: PathBuf // relative to launcher home (or absolute)
-}
-
-#[derive(Deserialize, Serialize, Debug, Clone)]
-#[serde(rename_all = "snake_case")]
-pub enum ProfileVersion {
- LatestSnapshot,
- LatestRelease,
- #[serde(untagged)]
- Specific(String)
-}
-
-#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
-pub struct Resolution {
- width: u32,
- height: u32
-}
-
-impl Default for Resolution {
- fn default() -> Self {
- Resolution { width: 864, height: 480 }
- }
-}
-
-#[derive(Deserialize, Serialize, Debug, Clone)]
-pub struct Profile {
- game_version: ProfileVersion,
- java_runtime: Option<String>,
- instance: String,
-
- #[serde(default)]
- jvm_arguments: Vec<String>,
- #[serde(default)]
- legacy_launch: bool,
-
- resolution: Option<Resolution>
-}
-
-impl<P: AsRef<Path>> From<P> for Instance {
- fn from(path: P) -> Self {
- Self { path: path.as_ref().into() }
- }
-}
-
-impl Instance {
- pub async fn get_path(&self, home: impl AsRef<Path>) -> Result<PathBuf, io::Error> {
- let path = self.path.as_path();
-
- if path.is_relative() {
- Ok([home.as_ref(), Path::new("instances"), path].iter().collect::<PathBuf>())
- } else {
- fs::canonicalize(path).await
- }
- }
-}
-
-const DEF_JVM_ARGUMENTS: [&str; 7] = [
- "-Xmx2G",
- "-XX:+UnlockExperimentalVMOptions",
- "-XX:+UseG1GC",
- "-XX:G1NewSizePercent=20",
- "-XX:G1ReservePercent=20",
- "-XX:MaxGCPauseMillis=50",
- "-XX:G1HeapRegionSize=32M"
-];
-
-impl Profile {
- fn new(instance_name: &str) -> Self {
- Self {
- game_version: ProfileVersion::LatestRelease,
- java_runtime: None,
- instance: instance_name.into(),
- jvm_arguments: DEF_JVM_ARGUMENTS.iter().map(|s| String::from(*s)).collect(),
- legacy_launch: false,
- resolution: None
- }
- }
-
- pub fn get_version(&self) -> &ProfileVersion {
- &self.game_version
- }
-
- pub fn get_instance_name(&self) -> &str {
- &self.instance
- }
-
- pub fn iter_arguments(&self) -> impl Iterator<Item = &String> {
- self.jvm_arguments.iter()
- }
-
- pub fn get_resolution(&self) -> Option<Resolution> {
- self.resolution
- }
-
- pub fn get_java_runtime(&self) -> Option<&String> {
- self.java_runtime.as_ref()
- }
-
- pub fn is_legacy_launch(&self) -> bool {
- self.legacy_launch
- }
-}
diff --git a/src/launcher/strsub.rs b/src/launcher/strsub.rs
deleted file mode 100644
index 5764405..0000000
--- a/src/launcher/strsub.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-// a cheap-o implementation of StrSubstitutor from apache commons
-// (does not need to support recursive evaluation or preserving escapes, it was never enabled in
-
-use std::borrow::Cow;
-
-const ESCAPE: char = '$';
-const VAR_BEGIN: &str = "${";
-const VAR_END: &str = "}";
-const VAR_DEFAULT: &str = ":-";
-
-pub trait SubFunc<'rep> {
- fn substitute(&self, key: &str) -> Option<Cow<'rep, str>>;
-}
-
-/* NOTE: the in-place implementation has been replaced for the following reasons:
- * - it was annoying to get lifetimes to work, so you could only either pass a trait implementation
- * or a closure
- * - it was probably slower than doing it out-of-place anyway, since you keep having to copy the
- * tail of the string for each replacement
- */
-
-// handles ${replacements} on this string IN-PLACE. Calls the "sub" function for each key it receives.
-// if "sub" returns None, it will use a default value or ignore the ${substitution}.
-// There are no "invalid inputs" and this function should never panic unless "sub" panics.
-/*pub fn replace_string(input: &mut String, sub: impl SubFunc) {
- let mut cursor = input.len();
- while let Some(idx) = input[..cursor].rfind(VAR_BEGIN) {
- // note: for some reason, apache processes escapes BEFORE checking if it's even a valid
- // replacement expression. strange behavior IMO.
- if let Some((pidx, ESCAPE)) = prev_char(input.as_ref(), idx) {
- // this "replacement" is escaped. remove the escape marker and continue.
- input.remove(pidx);
- cursor = pidx;
- continue;
- }
-
- let Some(endidx) = input[idx..cursor].find(VAR_END).map(|v| v + idx) else {
- // unclosed replacement expression. ignore.
- cursor = idx;
- continue;
- };
-
- let spec = &input[(idx + VAR_BEGIN.len())..endidx];
- let name;
- let def_opt;
-
- if let Some(def) = spec.find(VAR_DEFAULT) {
- name = &spec[..def];
- def_opt = Some(&spec[(def + VAR_DEFAULT.len())..]);
- } else {
- name = spec;
- def_opt = None;
- }
-
- if let Some(sub_val) = sub.substitute(name).map_or_else(|| def_opt.map(|d| Cow::Owned(d.to_owned())), |v| Some(v)) {
- input.replace_range(idx..(endidx + VAR_END.len()), sub_val.as_ref());
- }
-
- cursor = idx;
- }
-}*/
-
-pub fn replace_string<'inp, 'rep>(input: &'inp str, sub: &impl SubFunc<'rep>) -> Cow<'inp, str> {
- let mut ret: Option<String> = None;
- let mut cursor = 0usize;
-
- while let Some(idx) = input[cursor..].find(VAR_BEGIN) {
- let idx = idx + cursor; // make idx an absolute index into 'input'
- let spec_start = idx + VAR_BEGIN.len(); // the start of the "spec" (area inside {})
-
- // first, check if this is escaped
- if let Some((prev_idx, ESCAPE)) = input[..idx].char_indices().next_back() {
- let s = ret.get_or_insert_default();
- s.push_str(&input[cursor..prev_idx]);
-
- // advance past this so we don't match it again
- s.push_str(&input[idx..spec_start]);
- cursor = spec_start;
- continue;
- }
-
- // now, find the closing tag
- let Some(spec_end) = input[spec_start..].find(VAR_END).map(|v| v + spec_start) else {
- break; // reached the end of the string
- };
-
- let full_spec = &input[spec_start..spec_end];
-
- // check for a default argument
- let (name, def) = if let Some(defidx) = full_spec.find(VAR_DEFAULT) {
- (&full_spec[..defidx], Some(&full_spec[(defidx + VAR_DEFAULT.len())..]))
- } else {
- (full_spec, None)
- };
-
- let after = spec_end + VAR_END.len();
- if let Some(subst) = sub.substitute(name).map_or_else(|| def.map(Cow::Borrowed), Some) {
- let s = ret.get_or_insert_default();
- s.push_str(&input[cursor..idx]);
- s.push_str(subst.as_ref());
- } else {
- ret.get_or_insert_default().push_str(&input[cursor..after]);
- }
-
- cursor = after;
- }
-
- if let Some(ret) = ret.as_mut() {
- ret.push_str(&input[cursor..]);
- }
-
- ret.map_or(Cow::Borrowed(input), Cow::Owned)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[derive(Clone, Copy)]
- struct TestSub;
- impl SubFunc<'static> for TestSub {
- fn substitute(&self, key: &str) -> Option<Cow<'static, str>> {
- match key {
- "exists" => Some(Cow::Borrowed("value123")),
- "empty" => None,
- "borger" => Some(Cow::Borrowed("\u{1f354}")),
- _ => panic!("replace_fun called with unexpected key: {}", key)
- }
- }
- }
-
- #[test]
- fn test_standard_replace() {
- assert_eq!(replace_string("this has ${exists} and more", &TestSub), "this has value123 and more");
- assert_eq!(replace_string("multiple ${exists} repl${exists}ace", &TestSub), "multiple value123 replvalue123ace");
- assert_eq!(replace_string("${exists}${exists}", &TestSub), "value123value123");
- }
-
- #[test]
- fn test_empty_replace() {
- assert_eq!(replace_string("this has ${empty} and more", &TestSub), "this has ${empty} and more");
- assert_eq!(replace_string("multiple ${empty} repl${empty}ace", &TestSub), "multiple ${empty} repl${empty}ace");
- assert_eq!(replace_string("${empty}${empty}", &TestSub), "${empty}${empty}");
- }
-
- #[test]
- fn test_homogenous_replace() {
- assert_eq!(replace_string("some ${exists} and ${empty} ...", &TestSub), "some value123 and ${empty} ...");
- assert_eq!(replace_string("some ${empty} and ${exists} ...", &TestSub), "some ${empty} and value123 ...");
- assert_eq!(replace_string("${exists}${empty}", &TestSub), "value123${empty}");
- assert_eq!(replace_string("${empty}${exists}", &TestSub), "${empty}value123");
- }
-
- #[test]
- fn test_default_replace() {
- assert_eq!(replace_string("some ${exists:-def1} and ${empty:-def2} ...", &TestSub), "some value123 and def2 ...");
- assert_eq!(replace_string("some ${empty:-def1} and ${exists:-def2} ...", &TestSub), "some def1 and value123 ...");
- assert_eq!(replace_string("abc${empty:-}def", &TestSub), "abcdef");
- assert_eq!(replace_string("${empty:-}${empty:-}", &TestSub), "");
- }
-
- #[test]
- fn test_escape() {
- assert_eq!(replace_string("an $${escaped} replacement (${exists})", &TestSub), "an ${escaped} replacement (value123)");
- assert_eq!(replace_string("${exists}$${escaped}${exists}", &TestSub), "value123${escaped}value123");
-
- // make sure this weird behavior is preserved... (the original code seemed to show it)
- assert_eq!(replace_string("some $${ else", &TestSub), "some ${ else");
- }
-
- #[test]
- fn test_weird() {
- assert_eq!(replace_string("${exists}", &TestSub), "value123");
- assert_eq!(replace_string("$${empty}", &TestSub), "${empty}");
- assert_eq!(replace_string("${empty:-a}", &TestSub), "a");
- assert_eq!(replace_string("${empty:-}", &TestSub), "");
- }
-
- // these make sure it doesn't chop up multibyte characters illegally
- #[test]
- fn test_multibyte_surround() {
- assert_eq!(replace_string("\u{1f354}$${}\u{1f354}", &TestSub), "\u{1f354}${}\u{1f354}");
- assert_eq!(replace_string("\u{1f354}${exists}\u{1f354}${empty:-}\u{1f354}", &TestSub), "\u{1f354}value123\u{1f354}\u{1f354}");
- }
-
- #[test]
- fn test_multibyte_replace() {
- assert_eq!(replace_string("borger ${borger}", &TestSub), "borger \u{1f354}");
- assert_eq!(replace_string("${exists:-\u{1f354}}${empty:-\u{1f354}}", &TestSub), "value123\u{1f354}");
- assert_eq!(replace_string("${borger}$${}${borger}", &TestSub), "\u{1f354}${}\u{1f354}");
- }
-}
diff --git a/src/launcher/version.rs b/src/launcher/version.rs
deleted file mode 100644
index 0f55223..0000000
--- a/src/launcher/version.rs
+++ /dev/null
@@ -1,398 +0,0 @@
-use std::{collections::{BTreeMap, HashMap}, error::Error, io::ErrorKind};
-use std::borrow::Cow;
-use std::collections::HashSet;
-use std::fmt::Display;
-use std::path::{Path, PathBuf};
-
-use log::{debug, info, warn};
-use sha1_smol::Digest;
-use tokio::{fs, io};
-use crate::launcher::settings::ProfileVersion;
-use crate::util;
-use crate::version::{*, manifest::*};
-
-use super::constants::*;
-
-#[derive(Debug)]
-pub enum VersionError {
- IO { what: String, error: io::Error },
- Request { what: String, error: reqwest::Error },
- MalformedObject { what: String, error: serde_json::Error },
- VersionIntegrity { id: String, expect: Digest, got: Digest }
-}
-
-impl Display for VersionError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- VersionError::IO { what, error } => write!(f, "i/o error ({what}): {error}"),
- VersionError::Request { what, error } => write!(f, "request error ({what}): {error}"),
- VersionError::MalformedObject { what, error } => write!(f, "malformed {what}: {error}"),
- VersionError::VersionIntegrity { id, expect, got } => write!(f, "version {id} integrity mismatch (expect {expect}, got {got})")
- }
- }
-}
-
-impl Error for VersionError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- VersionError::IO { error, .. } => Some(error),
- VersionError::Request { error, .. } => Some(error),
- VersionError::MalformedObject { error, .. } => Some(error),
- _ => None
- }
- }
-}
-
-struct RemoteVersionList {
- versions: HashMap<String, VersionManifestVersion>,
- latest: LatestVersions
-}
-
-impl RemoteVersionList {
- async fn new() -> Result<RemoteVersionList, VersionError> {
- debug!("Looking up remote version manifest.");
- let text = reqwest::get(URL_VERSION_MANIFEST).await
- .and_then(|r| r.error_for_status())
- .map_err(|e| VersionError::Request { what: "download version manifest".into(), error: e })?
- .text().await.map_err(|e| VersionError::Request { what: "download version manifest (decode)".into(), error: e })?;
-
- debug!("Parsing version manifest.");
- let manifest: VersionManifest = serde_json::from_str(text.as_str()).map_err(|e| VersionError::MalformedObject { what: "version manifest".into(), error: e })?;
-
- let mut versions = HashMap::new();
- for v in manifest.versions {
- versions.insert(v.id.clone(), v);
- }
-
- debug!("Done loading remote versions!");
- Ok(RemoteVersionList {
- versions,
- latest: manifest.latest
- })
- }
-
- async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result<CompleteVersion, VersionError> {
- // ensure parent directory exists
- info!("Downloading version {}.", ver.id);
- tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await
- .inspect_err(|e| warn!("failed to create {} parent dirs: {e}", path.display()))
- .map_err(|e| VersionError::IO { what: format!("creating version directory for {}", path.display()), error: e })?;
-
- // download it
- let ver_text = reqwest::get(ver.url.as_str()).await
- .and_then(|r| r.error_for_status())
- .map_err(|e| VersionError::Request { what: format!("download version {} from {}", ver.id, ver.url), error: e })?
- .text().await.map_err(|e| VersionError::Request { what: format!("download version {} from {} (receive)", ver.id, ver.url), error: e })?;
-
- debug!("Validating downloaded {}...", ver.id);
- // make sure it's valid
- util::verify_sha1(ver.sha1, ver_text.as_str())
- .map_err(|e| VersionError::VersionIntegrity {
- id: ver.id.clone(),
- expect: ver.sha1,
- got: e
- })?;
-
- // make sure it's well-formed
- let cver: CompleteVersion = serde_json::from_str(ver_text.as_str()).map_err(|e| VersionError::MalformedObject { what: format!("complete version {}", ver.id), error: e })?;
-
- debug!("Saving version {}...", ver.id);
-
- // write it out
- tokio::fs::write(path, ver_text).await
- .inspect_err(|e| warn!("Failed to save version {}: {}", ver.id, e))
- .map_err(|e| VersionError::IO { what: format!("writing version file at {}", path.display()), error: e })?;
-
- info!("Done downloading and verifying {}!", ver.id);
-
- Ok(cver)
- }
-}
-
-struct LocalVersionList {
- versions: BTreeMap<String, CompleteVersion>
-}
-
-#[derive(Debug)]
-enum LocalVersionError {
- Sha1Mismatch { exp: Digest, got: Digest },
- VersionMismatch { fname: String, json: String },
- Unknown(Box<dyn Error>)
-}
-
-impl Display for LocalVersionError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- LocalVersionError::Sha1Mismatch { exp, got } => {
- write!(f, "sha1 mismatch (exp {exp}, got {got})")
- },
- LocalVersionError::VersionMismatch { fname, json } => {
- write!(f, "version ID mismatch (filename {fname}, json {json})")
- },
- LocalVersionError::Unknown(err) => {
- write!(f, "unknown version error: {err}")
- }
- }
- }
-}
-
-impl Error for LocalVersionError {}
-
-impl LocalVersionList {
- async fn load_version(path: &Path, sha1: Option<Digest>) -> Result<CompleteVersion, LocalVersionError> {
- // grumble grumble I don't like reading in the whole file at once
- info!("Loading local version at {}.", path.display());
- let ver = tokio::fs::read_to_string(path).await.map_err(|e| LocalVersionError::Unknown(Box::new(e)))?;
- if let Some(digest_exp) = sha1 {
- debug!("Verifying local version {}.", path.display());
- util::verify_sha1(digest_exp, ver.as_str())
- .map_err(|got| {
- warn!("Local version sha1 mismatch: {} (exp: {}, got: {})", path.display(), digest_exp, got);
- LocalVersionError::Sha1Mismatch { exp: digest_exp.to_owned(), got }
- })?;
- }
-
- let ver: CompleteVersion = serde_json::from_str(ver.as_str()).map_err(|e| {
- warn!("Invalid version JSON {}: {}", path.display(), e);
- LocalVersionError::Unknown(Box::new(e))
- })?;
-
- let fname_id = path.file_stem()
- .expect("tried to load a local version with no path") // should be impossible
- .to_str()
- .expect("tried to load a local version with invalid UTF-8 filename"); // we already checked if the filename is valid UTF-8 at this point
-
- if fname_id == ver.id.as_str() {
- info!("Loaded local version {}.", ver.id);
- Ok(ver)
- } else {
- warn!("Local version {} has a version ID conflict (filename: {}, json: {})!", path.display(), fname_id, ver.id);
- Err(LocalVersionError::VersionMismatch { fname: fname_id.to_owned(), json: ver.id })
- }
- }
-
- async fn load_versions(home: &Path, skip: impl Fn(&str) -> bool) -> Result<LocalVersionList, VersionError> {
- info!("Loading local versions.");
- let mut rd = tokio::fs::read_dir(home).await.map_err(|e| VersionError::IO { what: format!("open local versions directory {}", home.display()), error: e })?;
- let mut versions = BTreeMap::new();
-
- while let Some(ent) = rd.next_entry().await.map_err(|e| VersionError::IO { what: format!("read local versions directory {}", home.display()), error: e })? {
- if !ent.file_type().await.map_err(|e| VersionError::IO { what: format!("version entry metadata {}", ent.path().display()), error: e} )?.is_dir() { continue; }
-
- // when the code is fugly
- let path = match ent.file_name().to_str() {
- Some(s) => {
- if skip(s) {
- debug!("Skipping local version {s} because (I assume) it is remotely tracked.");
- continue
- }
-
- /* FIXME: once https://github.com/rust-lang/rust/issues/127292 is closed,
- * use add_extension to avoid extra heap allocations (they hurt my feelings) */
- let mut path = ent.path();
-
- // can't use set_extension since s might contain a . (like 1.8.9)
- path.push(format!("{s}.json"));
- path
- },
-
- /* We just ignore directories with names that contain invalid unicode. Unfortunately, the laucher
- * will not be supporting such custom versions. Name your version something sensible please. */
- None => {
- warn!("Ignoring a local version {} because its id contains invalid unicode.", ent.file_name().to_string_lossy());
- continue
- }
- };
-
- match Self::load_version(&path, None).await {
- Ok(v) => {
- versions.insert(v.id.clone(), v);
- },
- Err(e) => {
- // FIXME: just display the filename without to_string_lossy when https://github.com/rust-lang/rust/issues/120048 is closed
- warn!("Ignoring local version {}: {e}", ent.file_name().to_string_lossy());
- }
- }
- }
-
- info!("Loaded {} local version(s).", versions.len());
- Ok(LocalVersionList { versions })
- }
-}
-
-pub struct VersionList {
- remote: Option<RemoteVersionList>,
- local: LocalVersionList,
- home: PathBuf
-}
-
-pub enum VersionResult<'a> {
- Complete(&'a CompleteVersion),
- Remote(&'a VersionManifestVersion),
- None
-}
-
-impl<'a> From<&'a CompleteVersion> for VersionResult<'a> {
- fn from(value: &'a CompleteVersion) -> Self {
- Self::Complete(value)
- }
-}
-
-impl<'a> From<&'a VersionManifestVersion> for VersionResult<'a> {
- fn from(value: &'a VersionManifestVersion) -> Self {
- Self::Remote(value)
- }
-}
-
-impl<'a, T: Into<VersionResult<'a>>> From<Option<T>> for VersionResult<'a> {
- fn from(value: Option<T>) -> Self {
- value.map_or(VersionResult::None, |v| v.into())
- }
-}
-
-#[derive(Debug)]
-pub enum VersionResolveError {
- InheritanceLoop(String),
- MissingVersion(String),
- VersionLoad(VersionError)
-}
-
-impl Display for VersionResolveError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- VersionResolveError::InheritanceLoop(s) => write!(f, "inheritance loop (saw {s} twice)"),
- VersionResolveError::MissingVersion(s) => write!(f, "unknown version {s}"),
- VersionResolveError::VersionLoad(err) => write!(f, "version load error: {err}")
- }
- }
-}
-
-impl Error for VersionResolveError {}
-
-impl VersionList {
- async fn create_dir_for(home: &Path) -> Result<(), io::Error> {
- debug!("Creating versions directory.");
- match fs::create_dir(home).await {
- Ok(_) => Ok(()),
- Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
- Err(e) => {
- debug!("failed to create version home: {}", e);
- Err(e)
- }
- }
- }
-
- pub async fn online(home: &Path) -> Result<VersionList, VersionError> {
- Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?;
-
- let remote = RemoteVersionList::new().await?;
- let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?;
-
- Ok(VersionList {
- remote: Some(remote),
- local,
- home: home.to_path_buf()
- })
- }
-
- pub async fn offline(home: &Path) -> Result<VersionList, VersionError> {
- Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?;
-
- let local = LocalVersionList::load_versions(home, |_| false).await?;
-
- Ok(VersionList {
- remote: None,
- local,
- home: home.to_path_buf()
- })
- }
-
- pub fn is_online(&self) -> bool {
- self.remote.is_some()
- }
-
- pub fn get_version_lazy(&self, id: &str) -> VersionResult {
- self.remote.as_ref().and_then(|r| r.versions.get(id).map(VersionResult::from))
- .or_else(|| self.local.versions.get(id).map(VersionResult::from))
- .unwrap_or(VersionResult::None)
- }
-
- pub fn get_profile_version_id<'v>(&self, ver: &'v ProfileVersion) -> Option<Cow<'v, str>> {
- match ver {
- ProfileVersion::LatestRelease => self.remote.as_ref().map(|r| Cow::Owned(r.latest.release.clone())),
- ProfileVersion::LatestSnapshot => self.remote.as_ref().map(|r| Cow::Owned(r.latest.snapshot.clone())),
- ProfileVersion::Specific(ver) => Some(Cow::Borrowed(ver))
- }
- }
-
- pub fn get_remote_version(&self, id: &str) -> Option<&VersionManifestVersion> {
- let remote = self.remote.as_ref().expect("get_remote_version called in offline mode!");
-
- remote.versions.get(id)
- }
-
- pub async fn load_remote_version(&self, ver: &VersionManifestVersion) -> Result<CompleteVersion, VersionError> {
- let remote = self.remote.as_ref().expect("load_remote_version called in offline mode!");
-
- let id = ver.id.as_str();
- let mut ver_path = self.home.join(id);
- ver_path.push(format!("{id}.json"));
-
- debug!("Loading local copy of remote version {}", ver.id);
-
- match LocalVersionList::load_version(ver_path.as_path(), Some(ver.sha1)).await {
- Ok(v) => return Ok(v),
- Err(e) => {
- info!("Redownloading {id}, since the local copy could not be loaded: {e}");
- }
- }
-
- remote.download_version(ver, ver_path.as_path()).await
- }
-
- pub async fn resolve_version<'v>(&self, ver: &'v CompleteVersion) -> Result<Cow<'v, CompleteVersion>, VersionResolveError> {
- let mut seen: HashSet<String> = HashSet::new();
- seen.insert(ver.id.clone());
-
- let Some(inherit) = ver.inherits_from.as_ref() else {
- return Ok(Cow::Borrowed(ver));
- };
-
- if *inherit == ver.id {
- warn!("Version {} directly inherits from itself!", ver.id);
- return Err(VersionResolveError::InheritanceLoop(ver.id.clone()));
- }
-
- debug!("Resolving version inheritance: {} (inherits from {})", ver.id, inherit);
-
- let mut ver = ver.clone();
- let mut inherit = inherit.clone();
-
- loop {
- if !seen.insert(inherit.clone()) {
- warn!("Version inheritance loop detected in {}: {} transitively inherits from itself.", ver.id, inherit);
- return Err(VersionResolveError::InheritanceLoop(inherit));
- }
-
- let inherited_ver = match self.get_version_lazy(inherit.as_str()) {
- VersionResult::Complete(v) => Cow::Borrowed(v),
- VersionResult::Remote(v) =>
- Cow::Owned(self.load_remote_version(v).await.map_err(VersionResolveError::VersionLoad)?),
- VersionResult::None => {
- warn!("Cannot resolve version {}, it inherits an unknown version {inherit}", ver.id);
- return Err(VersionResolveError::MissingVersion(inherit));
- }
- };
-
- ver.apply_child(inherited_ver.as_ref());
-
- let Some(new_inherit) = inherited_ver.inherits_from.as_ref() else {
- break
- };
-
- inherit.replace_range(.., new_inherit.as_str());
- }
-
- Ok(Cow::Owned(ver))
- }
-}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 0d2233b..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-mod util;
-pub mod version;
-pub mod assets;
-pub mod launcher;
-pub mod auth; // temporarily public
diff --git a/src/util.rs b/src/util.rs
deleted file mode 100644
index 7510a33..0000000
--- a/src/util.rs
+++ /dev/null
@@ -1,334 +0,0 @@
-mod progress;
-
-use std::error::Error;
-use std::fmt::{Display, Formatter};
-use std::io::ErrorKind;
-use std::path::{Component, Path, PathBuf};
-use const_format::formatcp;
-use log::{debug, info, warn};
-use sha1_smol::{Digest, Sha1};
-use tokio::fs::File;
-use tokio::{fs, io};
-use tokio::io::{AsyncReadExt, AsyncWriteExt};
-
-const PKG_NAME: &str = env!("CARGO_PKG_NAME");
-const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
-const CRATE_NAME: &str = env!("CARGO_CRATE_NAME");
-
-pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME})");
-
-#[derive(Debug)]
-pub enum IntegrityError {
- SizeMismatch{ expect: usize, actual: usize },
- Sha1Mismatch{ expect: Digest, actual: Digest }
-}
-
-impl Display for IntegrityError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- IntegrityError::SizeMismatch{ expect, actual } =>
- write!(f, "size mismatch (expect {expect} bytes, got {actual} bytes)"),
- IntegrityError::Sha1Mismatch {expect, actual} =>
- write!(f, "sha1 mismatch (expect {expect}, got {actual})")
- }
- }
-}
-
-impl Error for IntegrityError {}
-
-pub fn verify_sha1(expect: Digest, s: &str) -> Result<(), Digest> {
- let dig = Sha1::from(s).digest();
-
- if dig == expect {
- return Ok(());
- }
-
- Err(dig)
-}
-
-#[derive(Debug)]
-pub enum FileVerifyError {
- Integrity(PathBuf, IntegrityError),
- Open(PathBuf, tokio::io::Error),
- Read(PathBuf, tokio::io::Error),
-}
-
-impl Display for FileVerifyError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- FileVerifyError::Integrity(path, e) => write!(f, "file integrity error {}: {}", path.display(), e),
- FileVerifyError::Open(path, e) => write!(f, "error opening file {}: {}", path.display(), e),
- FileVerifyError::Read(path, e) => write!(f, "error reading file {}: {}", path.display(), e)
- }
- }
-}
-
-impl Error for FileVerifyError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- FileVerifyError::Integrity(_, e) => Some(e),
- FileVerifyError::Open(_, e) => Some(e),
- FileVerifyError::Read(_, e) => Some(e)
- }
- }
-}
-
-pub async fn verify_file(path: impl AsRef<Path>, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> Result<(), FileVerifyError> {
- let path = path.as_ref();
-
- if expect_size.is_none() && expect_sha1.is_none() {
- return match fs::metadata(path).await {
- Ok(_) => {
- debug!("No size or sha1 for {}, have to assume it's good.", path.display());
- Ok(())
- },
- Err(e) => {
- Err(FileVerifyError::Open(path.to_path_buf(), e))
- }
- }
- }
-
- let mut file = File::open(path).await.map_err(|e| FileVerifyError::Open(path.to_owned(), e))?;
-
- let mut tally = 0usize;
- let mut st = Sha1::new();
- let mut buf = [0u8; 4096];
-
- loop {
- let n = match file.read(&mut buf).await {
- Ok(n) => n,
- Err(e) => match e.kind() {
- ErrorKind::Interrupted => continue,
- _ => return Err(FileVerifyError::Read(path.to_owned(), e))
- }
- };
-
- if n == 0 {
- break;
- }
-
- st.update(&buf[..n]);
- tally += n;
- }
-
- let dig = st.digest();
-
- if expect_size.is_some_and(|sz| sz != tally) {
- return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::SizeMismatch {
- expect: expect_size.unwrap(),
- actual: tally
- }));
- } else if expect_sha1.is_some_and(|exp_dig| exp_dig != dig) {
- return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::Sha1Mismatch {
- expect: expect_sha1.unwrap(),
- actual: dig
- }));
- }
-
- Ok(())
-}
-
-#[derive(Debug)]
-pub enum EnsureFileError {
- IO { what: &'static str, error: io::Error },
- Download { url: String, error: reqwest::Error },
- Integrity(IntegrityError),
- Offline,
- MissingURL
-}
-
-impl Display for EnsureFileError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- EnsureFileError::IO { what, error } => write!(f, "i/o error ensuring file ({what}): {error}"),
- EnsureFileError::Download { url, error } => write!(f, "error downloading file ({url}): {error}"),
- EnsureFileError::Integrity(e) => write!(f, "integrity error for downloaded file: {e}"),
- EnsureFileError::Offline => f.write_str("unable to download file while offline"),
- EnsureFileError::MissingURL => f.write_str("missing url"),
- }
- }
-}
-
-impl Error for EnsureFileError {
- fn source(&self) -> Option<&(dyn Error + 'static)> {
- match self {
- EnsureFileError::IO { error, .. } => Some(error),
- EnsureFileError::Download { error, .. } => Some(error),
- EnsureFileError::Integrity(error) => Some(error),
- _ => None
- }
- }
-}
-
-pub async fn should_download(path: impl AsRef<Path>, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> Result<bool, io::Error> {
- let path = path.as_ref();
-
- match verify_file(path, expect_size, expect_sha1).await {
- Ok(()) => {
- debug!("Skipping download for file {}, integrity matches.", path.display());
- Ok(false)
- },
- Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => {
- debug!("File {} is missing, downloading it.", path.display());
- Ok(true)
- },
- Err(FileVerifyError::Integrity(p, e)) => {
- warn!("Integrity error on file {}: {}", p.display(), e);
-
- // try to delete the file since it's bad
- let _ = fs::remove_file(path).await
- .map_err(|e| warn!("Error deleting corrupted/modified file {} (ignoring): {}", path.display(), e));
- Ok(true)
- }
- Err(FileVerifyError::Open(_, e) | FileVerifyError::Read(_, e)) => {
- warn!("Error verifying file {} on disk: {}", path.display(), e);
- Err(e)
- }
- }
-}
-
-pub async fn ensure_file(path: impl AsRef<Path>, url: Option<&str>, expect_size: Option<usize>, expect_sha1: Option<Digest>, online: bool, force_download: bool) -> Result<bool, EnsureFileError> {
- let path = path.as_ref();
-
- if !force_download && !should_download(path, expect_size, expect_sha1).await
- .map_err(|e| EnsureFileError::IO { what: "verifying file on disk", error: e })? {
-
- return Ok(false);
- }
-
- if !online {
- warn!("Cannot download {} to {} while offline!", url.unwrap_or("(no url)"), path.display());
- return Err(EnsureFileError::Offline);
- }
-
- // download the file
- let Some(url) = url else {
- return Err(EnsureFileError::MissingURL);
- };
-
- let mut file = File::create(path).await.map_err(|e| EnsureFileError::IO {
- what: "save downloaded file (open)",
- error: e
- })?;
-
- debug!("File {} must be downloaded ({}).", path.display(), url);
-
- let mut response = reqwest::get(url).await.map_err(|e| EnsureFileError::Download { url: url.to_owned(), error: e })?;
- let mut tally = 0usize;
- let mut sha1 = Sha1::new();
-
- while let Some(chunk) = response.chunk().await.map_err(|e| EnsureFileError::Download { url: url.to_owned(), error: e })? {
- let slice = chunk.as_ref();
-
- file.write_all(slice).await.map_err(|e| EnsureFileError::IO {
- what: "save downloaded file (write)",
- error: e
- })?;
-
- tally += slice.len();
- sha1.update(slice);
- }
-
- drop(file); // manually close file
-
- let del_file_silent = || async {
- debug!("Deleting downloaded file {} since its integrity doesn't match :(", path.display());
- let _ = fs::remove_file(path).await.map_err(|e| warn!("failed to delete invalid downloaded file: {}", e));
- ()
- };
-
- if expect_size.is_some_and(|s| s != tally) {
- del_file_silent().await;
-
- return Err(EnsureFileError::Integrity(IntegrityError::SizeMismatch {
- expect: expect_size.unwrap(),
- actual: tally
- }));
- }
-
- let digest = sha1.digest();
-
- if expect_sha1.is_some_and(|exp_dig| exp_dig != digest) {
- del_file_silent().await;
-
- return Err(EnsureFileError::Integrity(IntegrityError::Sha1Mismatch {
- expect: expect_sha1.unwrap(),
- actual: digest
- }));
- }
-
- info!("File {} downloaded successfully.", path.display());
- Ok(true)
-}
-
-pub fn check_path(name: &str) -> Result<&Path, &'static str> {
- let entry_path: &Path = Path::new(name);
-
- let mut depth = 0usize;
- for component in entry_path.components() {
- depth = match component {
- Component::Prefix(_) | Component::RootDir =>
- return Err("root path component in entry"),
- Component::ParentDir => depth.checked_sub(1)
- .map_or_else(|| Err("entry path escapes"), |s| Ok(s))?,
- Component::Normal(_) => depth + 1,
- _ => depth
- }
- }
-
- Ok(entry_path)
-}
-
-#[cfg(windows)]
-pub fn strip_verbatim(path: &Path) -> &Path {
- let Some(Component::Prefix(p)) = path.components().next() else {
- return path;
- };
-
- use std::path::Prefix;
- use std::ffi::OsStr;
-
- match p.kind() {
- Prefix::VerbatimDisk(_) =>
- Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(&path.as_os_str().as_encoded_bytes()[4..]) }),
- _ => path
- }
-}
-
-#[cfg(not(windows))]
-pub fn strip_verbatim(path: &Path) -> &Path {
- path
-}
-
-pub trait AsJavaPath {
- fn as_java_path(&self) -> &Path;
-}
-
-impl AsJavaPath for Path {
- fn as_java_path(&self) -> &Path {
- strip_verbatim(self)
- }
-}
-
-#[cfg(test)]
-mod tests {
- #[allow(unused_imports)]
- use super::*;
- use std::path::Prefix;
-
- #[test]
- #[cfg(windows)]
- fn test_strip_verbatim() {
- let path = Path::new(r"\\?\C:\Some\Verbatim\Path");
- match path.components().next().unwrap() {
- Component::Prefix(p) => assert!(matches!(p.kind(), Prefix::VerbatimDisk(_)), "(TEST BUG) path does not start with verbatim disk"),
- _ => panic!("(TEST BUG) path does not start with prefix")
- }
-
- let path2 = path.as_java_path();
- match path2.components().next().unwrap() {
- Component::Prefix(p) => assert!(matches!(p.kind(), Prefix::Disk(_))),
- _ => panic!("path does not begin with prefix")
- }
- }
-}
diff --git a/src/util/progress.rs b/src/util/progress.rs
deleted file mode 100644
index e8bdde1..0000000
--- a/src/util/progress.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-struct Progress {
-
-} \ No newline at end of file
diff --git a/src/version.rs b/src/version.rs
deleted file mode 100644
index 6e9ad3f..0000000
--- a/src/version.rs
+++ /dev/null
@@ -1,489 +0,0 @@
-use core::fmt;
-use std::{collections::BTreeMap, convert::Infallible, marker::PhantomData, ops::Deref, str::FromStr};
-use chrono::{DateTime, NaiveDateTime, Utc};
-use chrono::format::ParseErrorKind;
-use regex::Regex;
-use serde::{de::{self, Visitor}, Deserialize, Deserializer};
-use serde::de::{Error, SeqAccess};
-use sha1_smol::Digest;
-
-pub mod manifest;
-use manifest::*;
-
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
-#[serde(rename_all = "lowercase")]
-pub enum RuleAction {
- Allow,
- Disallow
-}
-
-// must derive an order on this because it's used as a key for a btreemap
-#[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-#[serde(rename_all = "lowercase")]
-pub enum OperatingSystem {
- Linux, // "linux"
- Windows, // "windows"
-
- #[serde(alias = "osx")] // not technically correct but it works
- MacOS, // "osx"
-
- #[serde(other)]
- Unknown // (not used in official jsons)
-}
-
-#[derive(Debug, Clone)]
-pub struct WrappedRegex(Regex);
-
-impl Deref for WrappedRegex {
- type Target = Regex;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-struct RegexVisitor;
-impl Visitor<'_> for RegexVisitor {
- type Value = WrappedRegex;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a valid regular expression")
- }
-
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error, {
- Regex::new(v).map_err(Error::custom).map(WrappedRegex)
- }
-}
-
-impl<'de> Deserialize<'de> for WrappedRegex {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de> {
- deserializer.deserialize_any(RegexVisitor)
- }
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct OSRestriction {
- #[serde(rename = "name")]
- pub os: Option<OperatingSystem>,
-
- pub version: Option<WrappedRegex>,
- pub arch: Option<WrappedRegex>
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct CompatibilityRule {
- pub action: RuleAction,
- pub features: Option<BTreeMap<String, bool>>,
- pub os: Option<OSRestriction>
-}
-
-pub trait FeatureMatcher {
- fn matches(&self, feature: &str) -> bool;
-}
-
-impl CompatibilityRule {
- pub fn features_match(&self, checker: &impl FeatureMatcher) -> bool {
- if let Some(m) = self.features.as_ref() {
- for (feat, expect) in m {
- if checker.matches(feat) != *expect {
- return false;
- }
- }
- }
-
- true
- }
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct Argument {
- #[serde(default)]
- pub rules: Option<Vec<CompatibilityRule>>,
-
- #[serde(default)]
- #[serde(deserialize_with = "string_or_array")]
- pub value: Vec<String>
-}
-
-#[derive(Debug, Clone)]
-pub struct WrappedArgument(Argument);
-
-impl FromStr for Argument {
- type Err = Infallible;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Ok(Argument {
- value: vec![s.to_owned()],
- rules: None
- })
- }
-}
-
-impl Deref for WrappedArgument {
- type Target = Argument;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl<'de> Deserialize<'de> for WrappedArgument {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de> {
- Ok(WrappedArgument(string_or_struct(deserializer)?))
- }
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct Arguments {
- pub game: Option<Vec<WrappedArgument>>,
- pub jvm: Option<Vec<WrappedArgument>>
-}
-
-impl Arguments {
- fn apply_child(&mut self, other: &Arguments) {
- if let Some(game) = other.game.as_ref() {
- self.game.get_or_insert_default().splice(0..0, game.iter().cloned());
- }
-
- if let Some(jvm) = other.jvm.as_ref() {
- self.jvm.get_or_insert_default().splice(0..0, jvm.iter().cloned());
- }
- }
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-#[serde(rename_all = "snake_case")]
-pub enum DownloadType {
- Client,
- ClientMappings,
- Server,
- ServerMappings,
- WindowsServer
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct DownloadInfo {
- pub sha1: Option<Digest>,
- pub size: Option<usize>,
- pub total_size: Option<usize>, // available for asset index
- pub url: Option<String>, // may not be present for libraries
- pub id: Option<String>,
- pub path: Option<String>
-}
-
-#[derive(Deserialize, Debug, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct JavaVersionInfo {
- pub component: String,
- pub major_version: u32
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct LibraryDownloads {
- pub artifact: Option<DownloadInfo>,
- pub classifiers: Option<BTreeMap<String, DownloadInfo>>
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct LibraryExtractRule {
- #[serde(default)]
- pub exclude: Vec<String>
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct Library {
- pub downloads: Option<LibraryDownloads>,
- pub name: String,
- pub extract: Option<LibraryExtractRule>,
- pub natives: Option<BTreeMap<OperatingSystem, String>>,
- pub rules: Option<Vec<CompatibilityRule>>,
-
- // old format
- pub url: Option<String>,
- pub size: Option<usize>,
- pub sha1: Option<Digest>
-}
-
-impl Library {
- pub fn get_canonical_name(&self) -> String {
- canonicalize_library_name(self.name.as_str(), self.natives.as_ref().map(|_| "__ozone_natives"))
- }
-}
-
-impl LibraryDownloads {
- pub fn get_download_info(&self, classifier: Option<&str>) -> Option<&DownloadInfo> {
- if let Some(classifier) = classifier {
- self.classifiers.as_ref()?.get(classifier)
- } else {
- self.artifact.as_ref()
- }
- }
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct ClientLogging {
- pub argument: String,
-
- #[serde(rename = "type")]
- pub log_type: String,
- pub file: DownloadInfo
-}
-
-#[derive(Deserialize, Debug, Clone)]
-pub struct Logging {
- pub client: Option<ClientLogging> // other fields unknown
-}
-
-#[derive(Deserialize, Debug, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct CompleteVersion {
- pub arguments: Option<Arguments>,
- pub minecraft_arguments: Option<String>,
-
- pub asset_index: Option<DownloadInfo>,
- pub assets: Option<String>,
-
- pub compliance_level: Option<u32>,
-
- pub java_version: Option<JavaVersionInfo>,
-
- #[serde(default)]
- pub downloads: BTreeMap<DownloadType, DownloadInfo>,
-
- #[serde(default)]
- pub libraries: Vec<Library>,
-
- pub id: String,
- pub jar: Option<String>, // used as the jar filename if specified? (no longer used officially)
-
- pub logging: Option<Logging>,
-
- pub main_class: Option<String>,
- pub minimum_launcher_version: Option<u32>,
-
- #[serde(deserialize_with = "deserialize_datetime_lenient")]
- pub release_time: Option<DateTime<Utc>>,
- #[serde(deserialize_with = "deserialize_datetime_lenient")]
- pub time: Option<DateTime<Utc>>,
-
- #[serde(rename = "type")]
- pub version_type: Option<VersionType>,
-
- pub compatibility_rules: Option<Vec<CompatibilityRule>>, //
- pub incompatibility_reason: Option<String>, // message shown when compatibility rules fail for this version
-
- pub inherits_from: Option<String>
-
- /* omitting field `savableVersion' because it seems like a vestigial part from old launcher versions
- * (also it isn't even a string that is present in modern liblauncher.so, so I assume it will never be used.)
- */
-}
-
-impl CompleteVersion {
- pub fn get_jar(&self) -> &String {
- self.jar.as_ref().unwrap_or(&self.id)
- }
-
- pub fn apply_child(&mut self, other: &CompleteVersion) {
- macro_rules! replace_missing {
- ($name:ident) => {
- if self.$name.is_none() {
- if let Some($name) = other.$name.as_ref() {
- self.$name.replace($name.to_owned());
- }
- }
- };
- }
-
- if let Some(arguments) = other.arguments.as_ref() {
- if let Some(my_args) = self.arguments.as_mut() {
- my_args.apply_child(arguments);
- } else {
- self.arguments.replace(arguments.to_owned());
- }
- }
-
- replace_missing!(minecraft_arguments);
- replace_missing!(asset_index);
- replace_missing!(assets);
- replace_missing!(compliance_level);
- replace_missing!(java_version);
-
- for (dltype, dl) in other.downloads.iter().by_ref() {
- self.downloads.entry(*dltype).or_insert_with(|| dl.clone());
- }
-
- // we use extend here instead of splice for library resolution priority reasons
- // (libraries earlier in the list will override libraries later in the list)
- self.libraries.extend(other.libraries.iter().cloned());
-
- replace_missing!(logging);
- replace_missing!(main_class);
- replace_missing!(minimum_launcher_version);
- replace_missing!(release_time);
- replace_missing!(time);
- replace_missing!(version_type);
-
- if let Some(rules) = other.compatibility_rules.as_ref() {
- if let Some(my_rules) = self.compatibility_rules.as_mut() {
- my_rules.splice(0..0, rules.iter().cloned());
- } else {
- self.compatibility_rules.replace(rules.to_owned());
- }
- }
-
- replace_missing!(incompatibility_reason);
- }
-}
-
-fn canonicalize_library_name(name: &str, suffix: Option<&str>) -> String {
- name.split(':')
- .enumerate()
- .filter(|(i, _)| *i != 2)
- .map(|(_, s)| s.to_ascii_lowercase())
- .chain(suffix.into_iter().map(|s| s.to_owned()))
- .collect::<Vec<_>>()
- .join(":")
-}
-
-fn deserialize_datetime_lenient<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
-where
- D: Deserializer<'de>
-{
- struct DateTimeVisitor;
-
- impl Visitor<'_> for DateTimeVisitor {
- type Value = Option<DateTime<Utc>>;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a valid datetime")
- }
-
- fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
- where
- E: Error
- {
- match value.parse::<DateTime<Utc>>() {
- Ok(dt) => Ok(Some(dt)),
- Err(e) if e.kind() == ParseErrorKind::TooShort => {
- // this probably just doesn't have an offset for some reason
- match value.parse::<NaiveDateTime>() {
- Ok(ndt) => Ok(Some(ndt.and_utc())),
- Err(e) => Err(Error::custom(e))
- }
- },
- Err(e) => Err(Error::custom(e))
- }
- }
- }
-
- deserializer.deserialize_str(DateTimeVisitor)
-}
-
-// https://serde.rs/string-or-struct.html
-fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
-where
- T: Deserialize<'de> + FromStr<Err = Infallible>,
- D: Deserializer<'de>,
-{
- struct StringOrStruct<T>(PhantomData<fn() -> T>);
-
- impl<'de, T> Visitor<'de> for StringOrStruct<T>
- where
- T: Deserialize<'de> + FromStr<Err = Infallible>,
- {
- type Value = T;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("string or map")
- }
-
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error, {
- Ok(FromStr::from_str(v).unwrap())
- }
-
- fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
- where
- A: de::MapAccess<'de>, {
- // wizardry (check comment in link)
- Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
- }
- }
-
- deserializer.deserialize_any(StringOrStruct(PhantomData))
-}
-
-// adapted from above
-fn string_or_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
-where
- T: Deserialize<'de> + FromStr<Err = Infallible>,
- D: Deserializer<'de>,
-{
- struct StringOrVec<T>(PhantomData<fn() -> T>);
-
- impl<'de, T> Visitor<'de> for StringOrVec<T>
- where
- T: Deserialize<'de> + FromStr<Err = Infallible>,
- {
- type Value = Vec<T>;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("string or array")
- }
-
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: Error, {
- Ok(vec![FromStr::from_str(v).unwrap()])
- }
-
- fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
- where
- A: SeqAccess<'de>, {
- Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
- }
- }
-
- deserializer.deserialize_any(StringOrVec(PhantomData))
-}
-
-#[cfg(test)]
-mod tests {
- use std::fs;
-
- use super::*;
-
- #[test]
- fn test_it() {
- let s = fs::read_to_string("./test_stuff/versions/1.7.10.json");
-
- let arg: CompleteVersion = serde_json::from_str(s.unwrap().as_str()).unwrap();
- dbg!(arg);
- }
-
- #[test]
- fn test_it2() {
- let s = fs::read_to_string("./test_stuff/version_manifest_v2.json");
-
- let arg: VersionManifest = serde_json::from_str(s.unwrap().as_str()).unwrap();
- dbg!(arg);
- }
-
- #[test]
- fn test_it3() {
- assert_eq!(canonicalize_library_name("group:artifact:version", None), String::from("group:artifact"));
- assert_eq!(canonicalize_library_name("group:artifact:version:specifier", None), String::from("group:artifact:specifier"));
- assert_eq!(canonicalize_library_name("not_enough:fields", None), String::from("not_enough:fields"));
- assert_eq!(canonicalize_library_name("word", None), String::from("word"));
- assert_eq!(canonicalize_library_name("", None), String::from(""));
- assert_eq!(canonicalize_library_name("group:artifact:version", Some("suffix")), String::from("group:artifact:suffix"));
- }
-}
diff --git a/src/version/manifest.rs b/src/version/manifest.rs
deleted file mode 100644
index b2b8524..0000000
--- a/src/version/manifest.rs
+++ /dev/null
@@ -1,91 +0,0 @@
-use core::fmt;
-use std::convert::Infallible;
-use std::str::FromStr;
-use chrono::{DateTime, Utc};
-use serde::{de::Visitor, Deserialize};
-use sha1_smol::Digest;
-
-#[derive(Deserialize, Debug)]
-pub struct LatestVersions {
- pub release: String,
- pub snapshot: String
-}
-
-#[derive(Debug, Clone)]
-pub enum VersionType {
- Snapshot,
- Release,
- OldBeta,
- OldAlpha,
- Other(String)
-}
-
-impl FromStr for VersionType {
- type Err = Infallible;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- "snapshot" => Ok(Self::Snapshot),
- "release" => Ok(Self::Release),
- "old_beta" => Ok(Self::OldBeta),
- "old_alpha" => Ok(Self::OldAlpha),
- _ => Ok(Self::Other(s.to_owned()))
- }
- }
-}
-
-impl VersionType {
- pub fn to_str(&self) -> &str {
- match self {
- Self::Snapshot => "snapshot",
- Self::Release => "release",
- Self::OldBeta => "old_beta",
- Self::OldAlpha => "old_alpha",
- Self::Other(s) => s
- }
- }
-}
-
-struct VersionTypeVisitor;
-
-impl Visitor<'_> for VersionTypeVisitor {
- type Value = VersionType;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a Minecraft release type")
- }
-
- fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
- where
- E: serde::de::Error, {
- Ok(VersionType::from_str(v).unwrap(/* infallible */))
- }
-}
-
-impl<'de> Deserialize<'de> for VersionType {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de> {
- deserializer.deserialize_string(VersionTypeVisitor)
- }
-}
-
-// https://piston-meta.mojang.com/mc/game/version_manifest_v2.json
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct VersionManifestVersion {
- pub id: String,
- #[serde(rename = "type")]
- pub version_type: VersionType,
- pub url: String,
- pub time: DateTime<Utc>,
- pub release_time: DateTime<Utc>,
- pub sha1: Digest,
- pub compliance_level: u32
-}
-
-#[derive(Deserialize, Debug)]
-pub struct VersionManifest {
- pub latest: LatestVersions,
- pub versions: Vec<VersionManifestVersion>
-}