summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-02-06 21:09:12 -0600
committerLibravatar bigfoot547 <[email protected]>2025-02-06 21:09:12 -0600
commitd1ac6d7263c78538414f0c944b8f6ca50a8c286e (patch)
tree7609b379ed02da76c902033973ce12f98d06db03
parentsmall cli changes (diff)
wip: some changes
-rw-r--r--ozone-cli/src/cli.rs11
-rw-r--r--ozone-cli/src/main.rs23
-rw-r--r--ozone/src/auth.rs5
-rw-r--r--ozone/src/launcher.rs4
-rw-r--r--ozone/src/launcher/assets.rs5
-rw-r--r--ozone/src/launcher/download.rs4
-rw-r--r--ozone/src/launcher/jre.rs3
-rw-r--r--ozone/src/launcher/rules.rs2
-rw-r--r--ozone/src/launcher/settings.rs68
-rw-r--r--ozone/src/launcher/version.rs128
-rw-r--r--ozone/src/util.rs11
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();