summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-31 02:32:19 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-31 02:32:19 -0600
commitcdeee17c2be5b8b9a333b977b3e2d373b94dfe0a (patch)
tree58ec48b5bfa9afe03ebbd9716f1f90841af914e9 /src
parentRemove some unused imports but not all of them (diff)
do clippy stuff and change line endings
Diffstat (limited to 'src')
-rw-r--r--src/auth.rs671
-rw-r--r--src/auth/mcservices.rs184
-rw-r--r--src/auth/msa.rs7
-rw-r--r--src/auth/types.rs256
-rw-r--r--src/auth/types/property_map.rs100
-rw-r--r--src/launcher.rs59
-rw-r--r--src/launcher/assets.rs644
-rw-r--r--src/launcher/constants.rs4
-rw-r--r--src/launcher/download.rs10
-rw-r--r--src/launcher/jre.rs19
-rw-r--r--src/launcher/jre/manifest.rs2
-rw-r--r--src/launcher/rules.rs2
-rw-r--r--src/launcher/runner.rs17
-rw-r--r--src/launcher/settings.rs4
-rw-r--r--src/launcher/strsub.rs4
-rw-r--r--src/launcher/version.rs22
-rw-r--r--src/version.rs16
-rw-r--r--src/version/manifest.rs2
18 files changed, 1005 insertions, 1018 deletions
diff --git a/src/auth.rs b/src/auth.rs
index 1eb65ed..7336e64 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -1,336 +1,335 @@
-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, EmptyExtraDeviceAuthorizationFields, EndpointNotSet, EndpointSet, HttpClientError, RefreshToken, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardRevocableToken, StandardTokenResponse, 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/token";
-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
- }
-
- 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, .. } => match 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
- },
- _ => 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);
- 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();
-
- if !mcservices::owns_the_game(client, mc_token).await? {
- return Err(AuthError::EntitlementError);
- }
-
- 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(())
- }
-
- 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 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()),
- is_azure_client_id: false,
- 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();
- }
-}
+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/token";
+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);
+ 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()),
+ is_azure_client_id: false,
+ 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
index 4305363..45ef795 100644
--- a/src/auth/mcservices.rs
+++ b/src/auth/mcservices.rs
@@ -1,92 +1,92 @@
-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().filter(|i| i.name == "game_minecraft" || i.name == "product_minecraft").next().is_some())
-}
-
-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
-}
+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
index 47e088b..404329b 100644
--- a/src/auth/msa.rs
+++ b/src/auth/msa.rs
@@ -112,8 +112,7 @@ impl XSTSAuthSuccessResponse {
}
fn get_display_claim(&self, name: &str) -> Option<&str> {
- self.display_claims.xui.iter().filter(|m| m.contains_key(name))
- .next().map_or(None, |f| f.get(name).map(|s| s.as_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> {
@@ -121,8 +120,7 @@ impl XSTSAuthSuccessResponse {
}
pub(super) fn get_xuid(&self) -> Option<Uuid> {
- self.get_display_claim("xid")
- .map_or(None, |s| Some(Uuid::from_u64_pair(0, s.parse().ok()?)))
+ self.get_display_claim("xid").and_then(|s| s.parse().ok()).map(|n| Uuid::from_u64_pair(0, n))
}
pub(super) fn get_gamertag(&self) -> Option<&str> {
@@ -130,6 +128,7 @@ impl XSTSAuthSuccessResponse {
}
}
+#[allow(clippy::from_over_into)]
impl Into<AuthError> for XSTSAuthErrorResponse {
fn into(self) -> AuthError {
AuthError::AuthXError {
diff --git a/src/auth/types.rs b/src/auth/types.rs
index 348bdf8..79b89c9 100644
--- a/src/auth/types.rs
+++ b/src/auth/types.rs
@@ -1,128 +1,128 @@
-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,
- 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: MultiMap<String, Property>
-}
-
-#[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<Uuid>,
- 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")]
-pub enum User {
- Dummy(PlayerProfile),
- MSA(MsaUser)
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct AuthenticationDatabase {
- pub users: Vec<User>
-}
+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,
+ 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<Uuid>,
+ 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
index ff67416..964b06f 100644
--- a/src/auth/types/property_map.rs
+++ b/src/auth/types/property_map.rs
@@ -1,50 +1,50 @@
-use std::fmt::Formatter;
-use multimap::MultiMap;
-use serde::de::{SeqAccess, Visitor};
-use serde::{Deserializer, Serializer};
-use serde::ser::SerializeSeq;
-use crate::auth::Property;
-
-pub type PropertyMap = MultiMap<String, Property>;
-
-pub fn serialize<S>(value: &PropertyMap, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer
-{
- let mut seq = serializer.serialize_seq(Some(value.keys().len()))?;
-
- for (_, prop) in value.flat_iter() {
- seq.serialize_element(prop)?;
- }
-
- seq.end()
-}
-
-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)
-}
+use std::fmt::Formatter;
+use multimap::MultiMap;
+use serde::de::{SeqAccess, Visitor};
+use serde::{Deserializer, Serializer};
+use serde::ser::SerializeSeq;
+use crate::auth::Property;
+
+pub type PropertyMap = MultiMap<String, Property>;
+
+pub fn serialize<S>(value: &PropertyMap, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer
+{
+ let mut seq = serializer.serialize_seq(Some(value.keys().len()))?;
+
+ for (_, prop) in value.flat_iter() {
+ seq.serialize_element(prop)?;
+ }
+
+ seq.end()
+}
+
+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
index d414305..0f2b442 100644
--- a/src/launcher.rs
+++ b/src/launcher.rs
@@ -32,7 +32,7 @@ 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, DownloadInfo, LibraryExtractRule, CompleteVersion, FeatureMatcher, ClientLogging};
+use crate::version::{Library, OSRestriction, OperatingSystem, DownloadType, LibraryExtractRule, FeatureMatcher, ClientLogging};
use assets::{AssetError, AssetRepository};
use crate::util::{self, AsJavaPath};
@@ -243,14 +243,13 @@ impl Launcher {
let home = fs::canonicalize(home.as_ref()).await?;
let versions_home = home.join("versions");
- let versions;
debug!("Version list online?: {online}");
- if online {
- versions = VersionList::online(versions_home.as_ref()).await?;
+ let versions = if online {
+ VersionList::online(versions_home.as_ref()).await?
} else {
- versions = VersionList::offline(versions_home.as_ref()).await?;
- }
+ VersionList::offline(versions_home.as_ref()).await?
+ };
let assets_path = home.join("assets");
@@ -271,7 +270,7 @@ impl Launcher {
}
fn choose_lib_classifier<'lib>(&self, lib: &'lib Library) -> Option<&'lib str> {
- lib.natives.as_ref().map_or(None, |n| n.get(&self.system_info.os)).map(|s| s.as_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> {
@@ -298,8 +297,8 @@ impl Launcher {
debug!("Logger config {} is at {}", id, path.display());
- util::ensure_file(&path, dlinfo.url.as_ref().map(|s| s.as_str()), dlinfo.size, dlinfo.sha1, self.online, false).await
- .map_err(|e| LaunchError::EnsureFile(e))?;
+ 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> {
@@ -345,15 +344,15 @@ impl Launcher {
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(|e| LaunchError::LoadVersion(e))?),
+ 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()).into())
+ return Err(LaunchError::UnknownVersion(version_id.into_owned()))
}
};
- let ver = self.versions.resolve_version(ver.as_ref()).await.map_err(|e| LaunchError::ResolveVersion(e))?;
- ver.rules_apply(&self.system_info, &feature_matcher).map_err(|e| LaunchError::IncompatibleVersion(e))?;
+ 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);
@@ -409,7 +408,7 @@ impl Launcher {
}
let log_arg;
- if let Some(logging) = ver.logging.as_ref().map_or(None, |l| l.client.as_ref()) {
+ 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;
@@ -421,9 +420,9 @@ impl Launcher {
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(|e| LaunchError::Assets(e))?;
+ .map_err(LaunchError::Assets)?;
- self.assets.ensure_assets(&asset_idx).await.map_err(|e| LaunchError::Assets(e))?;
+ self.assets.ensure_assets(&asset_idx).await.map_err(LaunchError::Assets)?;
(asset_idx_name, Some(asset_idx))
} else {
@@ -441,8 +440,8 @@ impl Launcher {
info!("Downloading client jar {}", client_path.display());
- util::ensure_file(client_path.as_path(), client.url.as_ref().map(|s| s.as_str()), client.size, client.sha1, self.online, false).await
- .map_err(|e| LaunchError::EnsureFile(e))?;
+ 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 {
@@ -460,7 +459,7 @@ impl Launcher {
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(|e| LaunchError::Assets(e))?
+ .map_err(LaunchError::Assets)?
} else {
None
};
@@ -469,7 +468,7 @@ impl Launcher {
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(|e| LaunchError::LibraryClasspathError(e))?
+ .map_err(LaunchError::LibraryClasspathError)?
.into_string()
.unwrap_or_else(|os| {
warn!("Classpath contains invalid UTF-8. The game may not launch correctly.");
@@ -524,7 +523,7 @@ impl Launcher {
// yuck
let jvm_args = profile.iter_arguments().map(OsString::from)
- .chain(runner::build_arguments(&info, ver.as_ref(), ArgumentType::JVM).drain(..))
+ .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);
@@ -572,7 +571,7 @@ struct LibraryExtractJob {
rule: Option<LibraryExtractRule>
}
-const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS);
+const ARCH_BITS: &str = formatcp!("{}", usize::BITS);
impl LibraryRepository {
fn get_artifact_base_dir(name: &str) -> Option<PathBuf> {
@@ -610,9 +609,7 @@ impl LibraryRepository {
}
fn get_artifact_path(name: &str, classifier: Option<&str>) -> Option<PathBuf> {
- let Some(mut p) = Self::get_artifact_base_dir(name) else {
- return None;
- };
+ let mut p = Self::get_artifact_base_dir(name)?;
p.push(Self::get_artifact_filename(name, classifier)?);
Some(p)
@@ -655,9 +652,9 @@ impl LibraryRepository {
if !ftype.is_dir() { return Ok(false); }
let Some(ftime) = entry.file_name().to_str()
- .map_or(None, |s| constants::NATIVES_DIR_PATTERN.captures(s))
- .map_or(None, |c| c.get(1))
- .map_or(None, |cap| cap.as_str().parse::<u64>().ok()) else {
+ .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);
};
@@ -684,7 +681,7 @@ impl LibraryRepository {
}).await
}
- async fn extract_natives<'lib>(&self, libs: Vec<LibraryExtractJob>) -> Result<PathBuf, LaunchError> {
+ 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
@@ -706,8 +703,8 @@ impl LibraryRepository {
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().filter(|ex|
- name.starts_with(ex.as_str())).next().is_none()))?;
+ rules.exclude.iter().any(|ex|
+ name.starts_with(ex.as_str()))))?;
}
Ok((natives_dir, tally))
diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs
index dacd01d..7c5dcf3 100644
--- a/src/launcher/assets.rs
+++ b/src/launcher/assets.rs
@@ -1,322 +1,322 @@
-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: &'static str = "indexes";
-const OBJECT_PATH: &'static 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 Ok(serde_json::from_str(&idx_data).map_err(|e| AssetError::IndexParse(e))?);
- },
- 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(|e| AssetError::DownloadIndex(e))?
- .text().await
- .map_err(|e| AssetError::DownloadIndex(e))?;
-
- 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
- })?;
-
- Ok(serde_json::from_str(&idx_text).map_err(|e| AssetError::IndexParse(e))?)
- }
-
- 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(|e| AssetError::AssetVerifyError(e))?;
- }
-
- 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");
- }
-}
+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
index db90d2f..4506ab5 100644
--- a/src/launcher/constants.rs
+++ b/src/launcher/constants.rs
@@ -7,8 +7,8 @@ pub const URL_JRE_MANIFEST: &str = "https://piston-meta.mojang.com/v1/products/j
pub const NATIVES_PREFIX: &str = "natives-";
-pub const DEF_INSTANCE_NAME: &'static str = "default";
-pub const DEF_PROFILE_NAME: &'static str = "default";
+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
diff --git a/src/launcher/download.rs b/src/launcher/download.rs
index 3a89d79..132cd7f 100644
--- a/src/launcher/download.rs
+++ b/src/launcher/download.rs
@@ -55,7 +55,7 @@ pub struct PhaseDownloadError<'j, T: Download> {
job: &'j T
}
-impl<'j, T: Download> Debug for PhaseDownloadError<'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)
@@ -65,13 +65,13 @@ impl<'j, T: Download> Debug for PhaseDownloadError<'j, T> {
}
}
-impl<'j, T: Download> Display for PhaseDownloadError<'j, T> {
+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<'j, T: Download> Error for PhaseDownloadError<'j, T> {
+impl<T: Download> Error for PhaseDownloadError<'_, T> {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&*self.inner)
}
@@ -100,7 +100,7 @@ impl<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> MultiDownloader<'j, T,
}
pub async fn perform(self, client: &'j Client) -> impl TryStream<Ok = (), Error = PhaseDownloadError<'j, T>> {
- stream::iter(self.jobs.into_iter()).map(move |job| Ok(async move {
+ stream::iter(self.jobs).map(move |job| Ok(async move {
macro_rules! map_err {
($result:expr, $phase:expr, $job:expr) => {
match $result {
@@ -124,7 +124,7 @@ impl<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> MultiDownloader<'j, T,
map_err!(job.handle_chunk(bytes.as_ref()).await, Phase::HandleChunk, job);
}
- job.finish().await.map_err(|e| PhaseDownloadError::new(Phase::Finish, e.into(), job))?;
+ job.finish().await.map_err(|e| PhaseDownloadError::new(Phase::Finish, e, job))?;
Ok(())
})).try_buffer_unordered(self.nconcurrent)
diff --git a/src/launcher/jre.rs b/src/launcher/jre.rs
index 0b92c20..31034b5 100644
--- a/src/launcher/jre.rs
+++ b/src/launcher/jre.rs
@@ -67,13 +67,13 @@ impl JavaRuntimeRepository {
.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_ref().map(|s| s.as_str()), info.size, info.sha1, self.online, false).await
+ 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 })?;
- Ok(serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtime manifest", error: e })?)
+ serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtime manifest", error: e })
}
// not very descriptive function name
@@ -86,7 +86,7 @@ impl JavaRuntimeRepository {
return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() });
};
- let Some(runtime) = runtime_component.iter().filter(|r| r.availability.progress == 100).next() else {
+ 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!");
}
@@ -102,17 +102,16 @@ impl JavaRuntimeRepository {
let entry = entry?;
let rel_path = entry.path().strip_prefix(path).expect("walkdir escaped root (???)");
- if rel_path.components().filter(|c| !matches!(c, Component::CurDir)).next().is_none() {
+ 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_str = rel_path.to_str().map(|s| s.replace(std::path::MAIN_SEPARATOR, "/"));
+ let rel_path_str = if std::path::MAIN_SEPARATOR != '/' {
+ rel_path.to_str().map(|s| s.replace(std::path::MAIN_SEPARATOR, "/"))
} else {
- rel_path_str = rel_path.to_str().map(String::from);
- }
+ 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())
@@ -175,7 +174,7 @@ impl JavaRuntimeRepository {
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
Err(e) => {
warn!("Could not create directory {} for JRE!", ent_path.display());
- return Err(JavaRuntimeError::IO { what: "creating directory", error: e });
+ Err(JavaRuntimeError::IO { what: "creating directory", error: e })
}
}
}).await
diff --git a/src/launcher/jre/manifest.rs b/src/launcher/jre/manifest.rs
index 887871a..3fd6484 100644
--- a/src/launcher/jre/manifest.rs
+++ b/src/launcher/jre/manifest.rs
@@ -37,7 +37,7 @@ pub enum JavaRuntimeFile {
File {
#[serde(default)]
executable: bool,
- downloads: FileDownloads
+ downloads: Box<FileDownloads>
},
Directory,
Link {
diff --git a/src/launcher/rules.rs b/src/launcher/rules.rs
index 69c967d..29a36d1 100644
--- a/src/launcher/rules.rs
+++ b/src/launcher/rules.rs
@@ -80,7 +80,7 @@ impl seal::CompatCheckInner for CompleteVersion {
}
fn get_incompatibility_reason(&self) -> Option<&str> {
- self.incompatibility_reason.as_ref().map(|s| s.as_str())
+ self.incompatibility_reason.as_deref()
}
}
diff --git a/src/launcher/runner.rs b/src/launcher/runner.rs
index f7fd025..afdfc7f 100644
--- a/src/launcher/runner.rs
+++ b/src/launcher/runner.rs
@@ -21,7 +21,7 @@ const PATH_SEP: &str = ";";
#[cfg(not(windows))]
const PATH_SEP: &str = ":";
-impl<'rep, 'l, F: FeatureMatcher> SubFunc<'rep> for LaunchArgSub<'rep, 'l, F> {
+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())),
@@ -51,12 +51,11 @@ impl<'rep, 'l, F: FeatureMatcher> SubFunc<'rep> for LaunchArgSub<'rep, 'l, F> {
"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_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()
- .map_or(None, |idx| idx.objects.get(asset_key))
+ 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()))
}
@@ -68,16 +67,16 @@ impl<'rep, 'l, F: FeatureMatcher> SubFunc<'rep> for LaunchArgSub<'rep, 'l, F> {
#[derive(Clone, Copy)]
pub enum ArgumentType {
- JVM,
+ Jvm,
Game
}
-pub fn build_arguments<'l, F: FeatureMatcher>(launch: &LaunchInfo<'l, F>, version: &CompleteVersion, arg_type: ArgumentType) -> Vec<OsString> {
+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().map_or(None, |args| match arg_type {
- ArgumentType::JVM => args.jvm.as_ref(),
+ 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()
@@ -86,7 +85,7 @@ pub fn build_arguments<'l, F: FeatureMatcher>(launch: &LaunchInfo<'l, F>, versio
.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 => {
+ ArgumentType::Jvm => {
[
"-Djava.library.path=${natives_directory}",
"-Dminecraft.launcher.brand=${launcher_name}",
diff --git a/src/launcher/settings.rs b/src/launcher/settings.rs
index 4dfc4ac..8453653 100644
--- a/src/launcher/settings.rs
+++ b/src/launcher/settings.rs
@@ -89,7 +89,7 @@ impl Settings {
}
pub fn get_path(&self) -> Option<&Path> {
- self.path.as_ref().map(|p| p.as_path())
+ self.path.as_deref()
}
pub async fn save_to(&self, path: impl AsRef<Path>) -> Result<(), SettingsError> {
@@ -184,7 +184,7 @@ impl Instance {
}
}
-const DEF_JVM_ARGUMENTS: [&'static str; 7] = [
+const DEF_JVM_ARGUMENTS: [&str; 7] = [
"-Xmx2G",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+UseG1GC",
diff --git a/src/launcher/strsub.rs b/src/launcher/strsub.rs
index 0d2357d..5764405 100644
--- a/src/launcher/strsub.rs
+++ b/src/launcher/strsub.rs
@@ -69,7 +69,7 @@ pub fn replace_string<'inp, 'rep>(input: &'inp str, sub: &impl SubFunc<'rep>) ->
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().rev().next() {
+ 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]);
@@ -94,7 +94,7 @@ pub fn replace_string<'inp, 'rep>(input: &'inp str, sub: &impl SubFunc<'rep>) ->
};
let after = spec_end + VAR_END.len();
- if let Some(subst) = sub.substitute(name).map_or_else(|| def.map(|d| Cow::Borrowed(d)), |v| Some(v)) {
+ 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());
diff --git a/src/launcher/version.rs b/src/launcher/version.rs
index 328f0a9..49525b0 100644
--- a/src/launcher/version.rs
+++ b/src/launcher/version.rs
@@ -40,14 +40,9 @@ impl RemoteVersionList {
async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result<CompleteVersion, Box<dyn Error>> {
// ensure parent directory exists
info!("Downloading version {}.", ver.id);
- match tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await {
- Err(e) => {
- if e.kind() != ErrorKind::AlreadyExists {
- warn!("failed to create {} parent dirs: {e}", path.display());
- return Err(e.into());
- }
- },
- Ok(()) => {}
+ if let Err(e) = tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await {
+ warn!("failed to create {} parent dirs: {e}", path.display());
+ return Err(e.into());
}
// download it
@@ -254,7 +249,7 @@ impl VersionList {
Self::create_dir_for(home).await?;
let remote = RemoteVersionList::new().await?;
- let local = LocalVersionList::load_versions(home.as_ref(), |s| remote.versions.contains_key(s)).await?;
+ let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?;
Ok(VersionList {
remote: Some(remote),
@@ -280,16 +275,15 @@ impl VersionList {
}
pub fn get_version_lazy(&self, id: &str) -> VersionResult {
- self.remote.as_ref()
- .map_or(None, |r| r.versions.get(id).map(VersionResult::from))
+ 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_or(None, |r| Some(Cow::Owned(r.latest.release.clone()))),
- ProfileVersion::LatestSnapshot => self.remote.as_ref().map_or(None, |r| Some(Cow::Owned(r.latest.snapshot.clone()))),
+ 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))
}
}
@@ -346,7 +340,7 @@ impl VersionList {
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(|e| VersionResolveError::Unknown(e))?),
+ Cow::Owned(self.load_remote_version(v).await.map_err(VersionResolveError::Unknown)?),
VersionResult::None => {
warn!("Cannot resolve version {}, it inherits an unknown version {inherit}", ver.id);
return Err(VersionResolveError::MissingVersion(inherit));
diff --git a/src/version.rs b/src/version.rs
index 2388390..6e9ad3f 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -43,7 +43,7 @@ impl Deref for WrappedRegex {
}
struct RegexVisitor;
-impl<'de> Visitor<'de> for RegexVisitor {
+impl Visitor<'_> for RegexVisitor {
type Value = WrappedRegex;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
@@ -52,8 +52,8 @@ impl<'de> Visitor<'de> for RegexVisitor {
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
- E: de::Error, {
- Regex::new(v).map_err(de::Error::custom).map(|r| WrappedRegex(r))
+ E: Error, {
+ Regex::new(v).map_err(Error::custom).map(WrappedRegex)
}
}
@@ -212,7 +212,7 @@ pub struct Library {
impl Library {
pub fn get_canonical_name(&self) -> String {
- canonicalize_library_name(self.name.as_str(), self.natives.as_ref().map_or(None, |_| Some("__ozone_natives")))
+ canonicalize_library_name(self.name.as_str(), self.natives.as_ref().map(|_| "__ozone_natives"))
}
}
@@ -287,7 +287,7 @@ pub struct CompleteVersion {
impl CompleteVersion {
pub fn get_jar(&self) -> &String {
- &self.jar.as_ref().unwrap_or(&self.id)
+ self.jar.as_ref().unwrap_or(&self.id)
}
pub fn apply_child(&mut self, other: &CompleteVersion) {
@@ -358,7 +358,7 @@ where
{
struct DateTimeVisitor;
- impl<'de> Visitor<'de> for DateTimeVisitor {
+ impl Visitor<'_> for DateTimeVisitor {
type Value = Option<DateTime<Utc>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
@@ -406,7 +406,7 @@ where
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
- E: de::Error, {
+ E: Error, {
Ok(FromStr::from_str(v).unwrap())
}
@@ -441,7 +441,7 @@ where
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
- E: de::Error, {
+ E: Error, {
Ok(vec![FromStr::from_str(v).unwrap()])
}
diff --git a/src/version/manifest.rs b/src/version/manifest.rs
index 18653f3..b2b8524 100644
--- a/src/version/manifest.rs
+++ b/src/version/manifest.rs
@@ -48,7 +48,7 @@ impl VersionType {
struct VersionTypeVisitor;
-impl<'de> Visitor<'de> for VersionTypeVisitor {
+impl Visitor<'_> for VersionTypeVisitor {
type Value = VersionType;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {