diff options
| author | 2025-02-06 21:09:12 -0600 | |
|---|---|---|
| committer | 2025-02-06 21:09:12 -0600 | |
| commit | d1ac6d7263c78538414f0c944b8f6ca50a8c286e (patch) | |
| tree | 7609b379ed02da76c902033973ce12f98d06db03 | |
| parent | small cli changes (diff) | |
wip: some changes
| -rw-r--r-- | ozone-cli/src/cli.rs | 11 | ||||
| -rw-r--r-- | ozone-cli/src/main.rs | 23 | ||||
| -rw-r--r-- | ozone/src/auth.rs | 5 | ||||
| -rw-r--r-- | ozone/src/launcher.rs | 4 | ||||
| -rw-r--r-- | ozone/src/launcher/assets.rs | 5 | ||||
| -rw-r--r-- | ozone/src/launcher/download.rs | 4 | ||||
| -rw-r--r-- | ozone/src/launcher/jre.rs | 3 | ||||
| -rw-r--r-- | ozone/src/launcher/rules.rs | 2 | ||||
| -rw-r--r-- | ozone/src/launcher/settings.rs | 68 | ||||
| -rw-r--r-- | ozone/src/launcher/version.rs | 128 | ||||
| -rw-r--r-- | ozone/src/util.rs | 11 |
11 files changed, 215 insertions, 49 deletions
diff --git a/ozone-cli/src/cli.rs b/ozone-cli/src/cli.rs index 407ee50..281a996 100644 --- a/ozone-cli/src/cli.rs +++ b/ozone-cli/src/cli.rs @@ -16,7 +16,16 @@ pub struct ProfileCreateArgs { /// Clone profile information from an existing profile.
#[arg(long, short = 'c')]
- pub clone: String
+ pub clone: Option<String>,
+
+ /// The Minecraft version to be launched by this profile. Will use the latest release by default.
+ #[arg(long, short = 'v')]
+ pub version: Option<String>,
+
+ /// The instance in which this profile will launch the game. By default, will create a new instance
+ /// with the same name as this profile.
+ #[arg(long, short = 'i')]
+ pub instance: Option<String>
}
#[derive(Subcommand, Debug)]
diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index 346532a..4b04e48 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -15,7 +15,7 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> { }; info!("Sensible home could be {home:?}"); - let settings = Settings::load(home.join("ozone.json")).await?; + let mut settings = Settings::load(home.join("ozone.json")).await?; match &cli.subcmd { RootCommand::Profile(p) => match p.command() { @@ -23,6 +23,27 @@ async fn main_inner(cli: Cli) -> Result<ExitCode, Box<dyn Error>> { for (name, profile) in settings.get_profiles().iter() { println!("{name}: {profile:#?}"); } + }, + ProfileCommand::Create(args) => { + if settings.profiles.contains_key(&args.name) { + eprintln!("A profile with that name already exists."); + return Ok(ExitCode::FAILURE); + } + + if let Some(ref src) = args.clone { + if let Some(profile) = settings.get_profiles().get(src) { + let profile = profile.clone(); + settings.profiles.insert(args.name.clone(), profile); + } else { + eprintln!("Unknown profile `{src}'."); + return Ok(ExitCode::FAILURE); + } + + return Ok(ExitCode::SUCCESS); + } + + // creating a new profile from scratch + } _ => todo!() }, diff --git a/ozone/src/auth.rs b/ozone/src/auth.rs index 712bd4d..2387ada 100644 --- a/ozone/src/auth.rs +++ b/ozone/src/auth.rs @@ -13,7 +13,7 @@ use oauth2::basic::{BasicErrorResponse, BasicErrorResponseType, BasicRevocationE 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; +use crate::util; #[derive(Debug)] pub enum AuthError { @@ -96,13 +96,12 @@ const NON_AZURE_LOGIN_SCOPES: &[&str] = ["service::user.auth.xboxlive.com::MBI_S 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() + util::build_client() .redirect(reqwest::redirect::Policy::none()) .build().expect("building client should succeed") } diff --git a/ozone/src/launcher.rs b/ozone/src/launcher.rs index 2165698..0916fd8 100644 --- a/ozone/src/launcher.rs +++ b/ozone/src/launcher.rs @@ -1,5 +1,5 @@ mod constants; -mod version; +pub mod version; mod strsub; mod download; mod rules; @@ -407,7 +407,7 @@ impl Launcher { if self.online { info!("Downloading {} libraries...", downloads.len()); - let client = Client::new(); + let client = util::create_client(); MultiDownloader::new(downloads.values_mut()).perform(&client).await .inspect_err(|e| warn!("library download failed: {e}")) .try_fold((), |_, _| async {Ok(())}) diff --git a/ozone/src/launcher/assets.rs b/ozone/src/launcher/assets.rs index aa7d42e..c13b1a2 100644 --- a/ozone/src/launcher/assets.rs +++ b/ozone/src/launcher/assets.rs @@ -6,7 +6,6 @@ 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; @@ -162,7 +161,7 @@ impl AssetRepository { })?; } - let idx_text = reqwest::get(url).await + let idx_text = util::create_client().get(url).send().await .map_err(AssetError::DownloadIndex)? .text().await .map_err(AssetError::DownloadIndex)?; @@ -227,7 +226,7 @@ impl AssetRepository { if self.online { info!("Downloading {} asset objects...", downloads.len()); - let client = Client::new(); + let client = util::create_client(); MultiDownloader::with_concurrent(downloads.iter_mut(), 32).perform(&client).await .inspect_err(|e| warn!("asset download failed: {e}")) .try_fold((), |_, _| async {Ok(())}) diff --git a/ozone/src/launcher/download.rs b/ozone/src/launcher/download.rs index 132cd7f..19a7edc 100644 --- a/ozone/src/launcher/download.rs +++ b/ozone/src/launcher/download.rs @@ -9,7 +9,7 @@ use tokio::fs; use tokio::fs::File; use tokio::io::{self, AsyncWriteExt}; use crate::util; -use crate::util::{FileVerifyError, IntegrityError, USER_AGENT}; +use crate::util::{FileVerifyError, IntegrityError}; pub trait Download: Debug + Display { // return Ok(None) to skip downloading this file @@ -114,8 +114,6 @@ impl<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> MultiDownloader<'j, T, 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 { diff --git a/ozone/src/launcher/jre.rs b/ozone/src/launcher/jre.rs index 5956f4e..b710fe6 100644 --- a/ozone/src/launcher/jre.rs +++ b/ozone/src/launcher/jre.rs @@ -4,7 +4,6 @@ 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; @@ -195,7 +194,7 @@ impl JavaRuntimeRepository { } let dl = MultiDownloader::new(downloads.iter_mut()); - let client = Client::new(); + let client = util::create_client(); dl.perform(&client).await .inspect_err(|e| warn!("jre file download failed: {e}")) diff --git a/ozone/src/launcher/rules.rs b/ozone/src/launcher/rules.rs index 88bed09..ceed3ef 100644 --- a/ozone/src/launcher/rules.rs +++ b/ozone/src/launcher/rules.rs @@ -1,6 +1,6 @@ use std::error::Error; use std::fmt::Display; -use crate::version::{Argument, CompatibilityRule, CompleteVersion, FeatureMatcher, Library, OSRestriction, RuleAction}; +use crate::version::{Argument, CompatibilityRule, CompleteVersion, FeatureMatcher, Library, RuleAction}; use super::SystemInfo; #[derive(Debug)] diff --git a/ozone/src/launcher/settings.rs b/ozone/src/launcher/settings.rs index 842b948..ca0bc33 100644 --- a/ozone/src/launcher/settings.rs +++ b/ozone/src/launcher/settings.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; +use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; -use log::warn; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use tokio::{fs, io}; use tokio::fs::File; @@ -11,18 +13,32 @@ use tokio::io::AsyncWriteExt; use uuid::Uuid; use super::constants; +lazy_static! { + pub static ref VALID_IDENTIFIER: Regex = Regex::new("^[a-z0-9_.\\-]+$").expect("hardcoded regex"); +} + #[derive(Debug, Clone, Serialize, Deserialize)] -struct SettingsInner { - profiles: HashMap<String, Profile>, - instances: HashMap<String, Instance>, +pub struct SettingsData { + pub profiles: HashMap<String, Profile>, + pub instances: HashMap<String, Instance>, #[serde(default = "uuid::Uuid::new_v4")] - client_id: Uuid + pub client_id: Uuid +} + +impl SettingsData { + fn check_consistent(&self) -> Result<(), String> { + if let Some((name, profile)) = self.profiles.iter().find(|(name, profile)| !self.instances.contains_key(&profile.instance)) { + return Err(format!("profile {} refers to instance {}, which does not exist.", name, &profile.instance)); + } + + Ok(()) + } } pub struct Settings { path: Option<PathBuf>, - inner: SettingsInner + inner: SettingsData } #[derive(Debug)] @@ -52,9 +68,9 @@ impl Error for SettingsError { } } -impl Default for SettingsInner { +impl Default for SettingsData { fn default() -> Self { - SettingsInner { + SettingsData { 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(), client_id: Uuid::new_v4() @@ -62,26 +78,31 @@ impl Default for SettingsInner { } } +impl Deref for Settings { + type Target = SettingsData; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Settings { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + impl Settings { - async fn load_inner(path: impl AsRef<Path>) -> Result<SettingsInner, SettingsError> { + async fn load_inner(path: impl AsRef<Path>) -> Result<SettingsData, 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) if e.kind() == ErrorKind::NotFound => Ok(SettingsData::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 + fn check_consistent(inner: SettingsData, path: Option<impl AsRef<Path>>) -> Result<Settings, SettingsError> { + inner.check_consistent().map_err(SettingsError::Inconsistent)?; Ok(Settings { path: path.map(|p| p.as_ref().to_owned()), @@ -115,6 +136,9 @@ impl Settings { } pub async fn save(&self) -> Result<(), SettingsError> { + // forbid saving inconsistent settings + self.inner.check_consistent().map_err(SettingsError::Inconsistent)?; + self.save_to(self.path.as_ref().expect("save() called on Settings instance not loaded from file")).await } @@ -160,7 +184,7 @@ impl Default for Resolution { #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "lowercase")] pub enum JavaRuntimeSetting { - Path(String), // I don't want the path serialized like an OsString (because it's ugly) + Path(PathBuf), Component(String) } diff --git a/ozone/src/launcher/version.rs b/ozone/src/launcher/version.rs index f6cdd58..705d4d5 100644 --- a/ozone/src/launcher/version.rs +++ b/ozone/src/launcher/version.rs @@ -3,9 +3,11 @@ use std::borrow::Cow; use std::collections::HashSet; use std::fmt::Display; use std::path::{Path, PathBuf}; - +use chrono::{DateTime, TimeDelta, Utc}; +use futures::TryFutureExt; use log::{debug, info, warn}; -use sha1_smol::Digest; +use serde::{Deserialize, Serialize}; +use sha1_smol::{Digest, Sha1}; use tokio::{fs, io}; use super::settings::ProfileVersion; use crate::util; @@ -48,16 +50,122 @@ struct RemoteVersionList { latest: LatestVersions } +#[derive(Serialize, Deserialize, Debug)] +struct VersionManifestCache { + last_download: DateTime<Utc>, + digest: Digest, + size: usize +} + +const MANIFEST_CACHE_MAX_AGE: TimeDelta = TimeDelta::seconds(120); +const MANIFEST_CACHE_INFO_PATH: &str = ".manifest_cache.json"; +const MANIFEST_CACHE_PATH: &str = ".manifest.json"; + impl RemoteVersionList { - async fn new() -> Result<RemoteVersionList, VersionError> { - debug!("Looking up remote version manifest."); - let text = reqwest::get(URL_VERSION_MANIFEST).await + // errors from this function are fatal so if it's not a super duper bad error then log and ignore it + async fn load_cached_manifest(home: impl AsRef<Path>) -> Result<Option<VersionManifest>, VersionError> { + let home = home.as_ref(); + let cache_info = match fs::read_to_string(home.join(MANIFEST_CACHE_INFO_PATH)).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(VersionError::IO { what: "load version manifest cache".into(), error: e }) + }; + + let cache_info = match serde_json::from_str::<VersionManifestCache>(&cache_info) { + Ok(info) => info, + Err(e) => { + warn!("Error loading version manifest cache info: {e}"); + warn!("Ignoring it and moving on."); + return Ok(None); + } + }; + + let now = Utc::now(); + if now.signed_duration_since(cache_info.last_download) > MANIFEST_CACHE_MAX_AGE { + debug!("Cached version manifest is older than maximum age. Redownloading."); + return Ok(None); + } + + let manifest_cache = match fs::read_to_string(home.join(MANIFEST_CACHE_PATH)).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(VersionError::IO { what: "load cached version manifest".into(), error: e }) + }; + + if cache_info.size != manifest_cache.len() { + warn!("Cached version manifest has bad integrity (expected {} bytes, got {} bytes)", cache_info.size, manifest_cache.len()); + warn!("Will attempt to download it again."); + return Ok(None); + } + + if let Err(got) = util::verify_sha1(cache_info.digest, &manifest_cache) { + warn!("Cached version manifest has bad integrity (expected {} sha1, got {} sha1)", cache_info.digest, got); + warn!("Will attempt to download it again."); + return Ok(None); + } + + let manifest = match serde_json::from_str::<VersionManifest>(&manifest_cache) { + Ok(m) => m, + Err(e) => { + warn!("Error loading cached version manifest info: {e}"); + warn!("Trying to download it again."); + return Ok(None); + } + }; + + Ok(Some(manifest)) + } + + async fn load_manifest(home: impl AsRef<Path>) -> Result<VersionManifest, VersionError> { + let home = home.as_ref(); + if let Some(manifest) = Self::load_cached_manifest(home).await? { + return Ok(manifest); + } + + debug!("Will download version manifest, since we couldn't load the cached version."); + + let manifest_str = util::create_client().get(URL_VERSION_MANIFEST) + .header(reqwest::header::ACCEPT, "application/json").send().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 })?; + .text().await + .map_err(|e| VersionError::Request { what: "download version manifest (receive)".into(), error: e })?; + + let manifest: VersionManifest = serde_json::from_str(&manifest_str) + .map_err(|e| VersionError::MalformedObject { what: "version manifest".into(), error: e })?; + + // since it decoded correctly, let's cache it for next time + + debug!("Manifest received correctly. Caching it."); + let manifest_path = home.join(MANIFEST_CACHE_PATH); + match fs::write(&manifest_path, &manifest_str).await { + Ok(_) => { + let manifest_info = serde_json::to_string(&VersionManifestCache { + last_download: Utc::now(), + digest: Sha1::from(&manifest_str).digest(), + size: manifest_str.len() + }).expect("cache info serialization failed (shouldn't be possible)"); + + let info_path = home.join(MANIFEST_CACHE_INFO_PATH); + + // this error isn't fatal even though it's weird + let _ = fs::write(&info_path, &manifest_info).await + .inspect_err(|e| { + warn!("Failed to save version manifest cache info to {}. Will have to download it next time.", info_path.display()); + warn!("Error: {e}"); + }); + }, + Err(e) => { + warn!("Failed to save cached version manifest to {}. Will have to download it next time.", manifest_path.display()); + warn!("Error: {e}"); + } + } + + Ok(manifest) + } - 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 })?; + async fn new(home: impl AsRef<Path>) -> Result<RemoteVersionList, VersionError> { + let manifest: VersionManifest = Self::load_manifest(&home)?; let mut versions = HashMap::new(); for v in manifest.versions { @@ -79,7 +187,7 @@ impl RemoteVersionList { .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 + let ver_text = util::create_client().get(ver.url.as_str()).send().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 })?; @@ -285,7 +393,7 @@ impl VersionList { 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 remote = RemoteVersionList::new(home).await?; let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?; Ok(VersionList { diff --git a/ozone/src/util.rs b/ozone/src/util.rs index a15a4fd..5d7c2b3 100644 --- a/ozone/src/util.rs +++ b/ozone/src/util.rs @@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use log::{debug, info, warn}; +use reqwest::{Client, ClientBuilder}; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; use tokio::{fs, io}; @@ -14,6 +15,14 @@ pub const NAME: &str = env!("CARGO_PKG_NAME"); pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); +pub fn build_client() -> ClientBuilder { + ClientBuilder::new().user_agent(USER_AGENT) +} + +pub fn create_client() -> Client { + build_client().build().expect("client should work") +} + #[derive(Debug)] pub enum IntegrityError { SizeMismatch{ expect: usize, actual: usize }, @@ -210,7 +219,7 @@ pub async fn ensure_file(path: impl AsRef<Path>, url: Option<&str>, expect_size: 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 response = create_client().get(url).send().await.map_err(|e| EnsureFileError::Download { url: url.to_owned(), error: e })?; let mut tally = 0usize; let mut sha1 = Sha1::new(); |
