diff options
| author | 2025-01-18 23:47:48 -0600 | |
|---|---|---|
| committer | 2025-01-18 23:47:48 -0600 | |
| commit | cd8bf1667494070c3a22ab5d63b559a9742b8a1a (patch) | |
| tree | 6f93f0c0fbdccfa18733499845a8bc7c298c402f | |
| parent | building classpath (diff) | |
more stuff
| -rw-r--r-- | ozone-cli/src/main.rs | 19 | ||||
| -rw-r--r-- | src/launcher.rs | 144 | ||||
| -rw-r--r-- | src/launcher/assets.rs | 84 | ||||
| -rw-r--r-- | src/launcher/constants.rs | 3 | ||||
| -rw-r--r-- | src/launcher/extract.rs | 26 | ||||
| -rw-r--r-- | src/launcher/profile.rs | 33 | ||||
| -rw-r--r-- | src/launcher/settings.rs | 184 | ||||
| -rw-r--r-- | src/launcher/version.rs | 11 | ||||
| -rw-r--r-- | src/util.rs | 20 |
9 files changed, 369 insertions, 155 deletions
diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index 965d27f..bb72ce3 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -3,7 +3,7 @@ use std::error::Error; use std::path::PathBuf; use log::{error, info}; use sysinfo::System; -use o3launcher::launcher::Profile; +use o3launcher::launcher::{Launcher, Settings}; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { @@ -12,14 +12,15 @@ async fn main() -> Result<(), Box<dyn Error>> { info!("Hello, world!"); info!("stuff: {:?} {:?} {:?} {:?} {:?}", System::name(), System::os_version(), System::long_os_version(), System::kernel_version(), System::cpu_arch()); info!("stuff: {:?} {:?} {:?} {}", System::distribution_id(), OS, ARCH, size_of::<*const i32>()); - - let launcher = o3launcher::launcher::Launcher::new(PathBuf::from("./work").as_path(), true).await?; - let profile = Profile { - version_id: "1.21.4".into(), - java_runtime: None, - instance: "".into() - }; - launcher.prepare_launch(&profile).await.map_err(|e| { + + let settings = Settings::load("./work/ozone.json").await?; + settings.save().await?; + + let launcher = Launcher::new("./work", true).await?; + + let profile = settings.get_profile("default").unwrap(); + + launcher.prepare_launch(profile.get_version(), settings.get_instance_for(profile)).await.map_err(|e| { error!("error launching: {e}"); e })?; diff --git a/src/launcher.rs b/src/launcher.rs index 8a68a65..7611011 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1,92 +1,43 @@ mod constants; mod version; -mod profile; mod strsub; mod download; mod rules; mod assets; mod extract; +mod settings; use std::borrow::Cow; use std::cmp::min; -use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::ffi::OsStr; use std::fmt::{Display, Formatter}; -use std::fs::FileType; use std::io::ErrorKind; use std::io::ErrorKind::AlreadyExists; use std::path::{Component, Path, PathBuf}; use std::{env, process}; use std::env::JoinPathsError; use std::time::{SystemTime, UNIX_EPOCH}; -use chrono::NaiveDateTime; use const_format::formatcp; -use futures::{stream, FutureExt, StreamExt, TryStreamExt}; +use futures::{StreamExt, TryStreamExt}; use log::{debug, info, warn}; use reqwest::Client; -use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; use sysinfo::System; use tokio::{fs, io}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio_stream::wrappers::ReadDirStream; -use zip::ZipArchive; use download::{MultiDownloader, VerifiedDownload}; use rules::{CompatCheck, IncompatibleError}; use version::{VersionList, VersionResolveError, VersionResult}; use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo, LibraryExtractRule}; -pub use profile::{Instance, Profile}; -use crate::launcher::assets::{AssetError, AssetRepository}; -use crate::util; -use crate::util::{FileVerifyError, IntegrityError}; +use assets::{AssetError, AssetRepository}; +use crate::util::{self, FileVerifyError, IntegrityError}; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct Settings { - profiles: HashMap<String, Profile>, - instances: HashMap<String, Instance> -} - -#[derive(Debug)] -enum SettingsLoadError { - IO(io::Error), - Format(serde_json::Error) -} - -impl Display for SettingsLoadError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SettingsLoadError::IO(err) => write!(f, "I/O error loading settings: {}", err), - SettingsLoadError::Format(err) => write!(f, "settings format error: {}", err), - } - } -} - -impl Error for SettingsLoadError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - SettingsLoadError::IO(err) => Some(err), - SettingsLoadError::Format(err) => Some(err), - } - } -} - -impl Settings { - async fn load(path: impl AsRef<Path>) -> Result<Settings, SettingsLoadError> { - let data = match fs::read_to_string(&path).await { - Ok(data) => data, - Err(e) => return match e.kind() { - ErrorKind::NotFound => Ok(Settings::default()), - _ => Err(SettingsLoadError::IO(e)) - } - }; - - serde_json::from_str(data.as_str()).map_err(SettingsLoadError::Format) - } -} +pub use settings::*; #[derive(Debug)] pub enum LogConfigError { @@ -144,9 +95,6 @@ pub struct Launcher { home: PathBuf, versions: VersionList, - settings_path: PathBuf, // maybe redundant but idc >:3 - settings: Settings, - system_info: SystemInfo, libraries: LibraryRepository, @@ -155,6 +103,8 @@ pub struct Launcher { #[derive(Debug)] pub enum LaunchError { + UnknownInstance(String), + // version resolution errors UnknownVersion(String), LoadVersion(Box<dyn Error>), @@ -186,6 +136,7 @@ pub enum LaunchError { impl Display for LaunchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { + LaunchError::UnknownInstance(inst) => write!(f, "unknown instance: {inst}"), LaunchError::UnknownVersion(id) => write!(f, "unknown version id: {id}"), LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"), LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}"), @@ -229,12 +180,8 @@ impl Error for LaunchError { impl Launcher { // FIXME: more descriptive error type por favor - pub async fn new(home: &Path, online: bool) -> Result<Launcher, Box<dyn Error>> { - let home = home.to_owned(); - let versions_home = home.join("versions"); - let versions; - - match tokio::fs::create_dir_all(&home).await { + pub async fn new(home: impl AsRef<Path>, online: bool) -> Result<Launcher, Box<dyn Error>> { + match tokio::fs::create_dir_all(home.as_ref()).await { Err(e) if e.kind() != AlreadyExists => { warn!("Failed to create launcher home directory: {}", e); return Err(e.into()); @@ -242,23 +189,22 @@ 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?; } else { versions = VersionList::offline(versions_home.as_ref()).await?; } - - let settings_path = home.join("ozone.json"); - let settings = Settings::load(&settings_path).await?; let assets_path = home.join("assets"); Ok(Launcher { online, versions, - settings_path, - settings, system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries"), @@ -390,7 +336,7 @@ impl Launcher { })) } - pub async fn prepare_launch(&self, profile: &Profile) -> Result<(), LaunchError> { + pub async fn prepare_launch(&self, version_id: &ProfileVersion, instance: &Instance) -> Result<(), LaunchError> { /* tasks 2 l;aunch the gayme!!!! :3 * - java runtime * - normal process (good research, past figboot :3) @@ -403,26 +349,44 @@ impl Launcher { * - (done) logging * - (done) download the config if present and necessary * - (done) (explode if offline mode and we need to download stuff) - * - assets + * - (done) assets * - (done) get asset index (check if our local copy is good and redownload if not) * - (done) check what ones are good and what needs to be downloaded * - (done) download them * - (done) (if offline mode, explode) - * - if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) + * - (done) if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) * - (done) the actual client jar * - (done) check integriddy and download if needed * - (done) (explode if offline mode) * - launch the game * - build argument list and whatnot also */ - info!("Preparing launch for \"{}\"...", profile.version_id); + let Some(version_id) = self.versions.get_profile_version_id(version_id) else { + // idk how common this usecase actually is + warn!("Can't use latest release/snapshot profiles while offline!"); + return Err(LaunchError::UnknownVersion("<latest>".into())); + }; + + info!("Preparing launch for \"{}\"...", version_id); + + let inst_home = instance.get_path(&self.home).await.map_err(|e| LaunchError::IO { + what: "resolving instance directory", + error: e + })?; + + fs::create_dir_all(inst_home.as_path()).await.map_err(|e| LaunchError::IO { + what: "creating instance directory", + error: e + })?; + + info!("Launching the game in {}", inst_home.display()); - let ver_res = self.versions.get_version_lazy(&profile.version_id); + 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::Complete(cv) => Cow::Borrowed(cv), VersionResult::None => { - return Err(LaunchError::UnknownVersion(profile.version_id.clone()).into()) + return Err(LaunchError::UnknownVersion(version_id.into_owned()).into()) } }; @@ -483,12 +447,18 @@ impl Launcher { // download assets - if let Some(idx_download) = ver.asset_index.as_ref() { - let asset_idx = self.assets.load_index(idx_download, ver.assets.as_ref().map(|s| s.as_ref())).await - .map_err(|e| LaunchError::Assets(e))?; + let (asset_idx_name, asset_idx) = + if let Some(idx_download) = ver.asset_index.as_ref() { + let asset_idx_name = idx_download.id.as_ref().or(ver.assets.as_ref()).map(String::as_str); + let asset_idx = self.assets.load_index(idx_download, asset_idx_name).await + .map_err(|e| LaunchError::Assets(e))?; - self.assets.ensure_assets(&asset_idx).await.map_err(|e| LaunchError::Assets(e))?; - } + self.assets.ensure_assets(&asset_idx).await.map_err(|e| LaunchError::Assets(e))?; + + (Some(asset_idx_name), Some(asset_idx)) + } else { + (None, None) + }; // download client jar @@ -513,10 +483,18 @@ impl Launcher { info!("Cleaned up {} old natives directories.", nnatives); // extract natives (execute this function unconditionally because we still need the natives dir to exist) - info!("Extracting natives."); + info!("Extracting natives from libraries"); let natives_dir = self.libraries.extract_natives(extract_jobs).await?; - info!("Building classpath."); + let assets_root = if let Some(asset_idx) = asset_idx { + info!("Reconstructing assets"); + self.assets.reconstruct_assets(&asset_idx, inst_home.as_path(), asset_idx_name.unwrap()).await + .map_err(|e| LaunchError::Assets(e))? + } else { + None + }; + + info!("Building classpath"); let classpath = env::join_paths(downloads.iter() .map(|job| job.get_path()) .chain(client_jar_path.iter().map(|p| p.as_path()))) @@ -650,10 +628,10 @@ impl LibraryRepository { let path = entry.path(); info!("Deleting old natives directory {}", path.display()); - /*fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO { + fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e - })?;*/ + })?; return Ok(true); } diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs index e540e50..992af2a 100644 --- a/src/launcher/assets.rs +++ b/src/launcher/assets.rs @@ -4,11 +4,13 @@ use std::fmt::{Display, Formatter}; use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::path::Component::Normal;
-use futures::TryStreamExt;
+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 tokio_stream::StreamExt;
use crate::assets::{Asset, AssetIndex};
use crate::launcher::download::{MultiDownloader, VerifiedDownload};
use crate::util;
@@ -33,7 +35,8 @@ pub enum AssetError { DownloadIndex(reqwest::Error),
Integrity(IntegrityError),
AssetObjectDownload,
- AssetVerifyError(FileVerifyError)
+ AssetVerifyError(FileVerifyError),
+ AssetNameError(&'static str)
}
impl Display for AssetError {
@@ -48,7 +51,8 @@ impl Display for AssetError { 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::AssetVerifyError(e) => write!(f, "error verifying asset object: {e}"),
+ AssetError::AssetNameError(e) => write!(f, "invalid asset name: {e}")
}
}
}
@@ -105,7 +109,7 @@ impl AssetRepository { }
pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> {
- let Some(id) = index.id.as_ref().map(|s| s.as_str()).or(id) else {
+ let Some(id) = id else {
return Err(AssetError::InvalidId(None));
};
@@ -188,6 +192,11 @@ impl AssetRepository { format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash)
}
+ 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(()),
@@ -206,8 +215,7 @@ impl AssetRepository { })?;
for object in index.objects.iter() {
- let hex_digest = object.hash.to_string();
- let path = [objects_path.as_ref(), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect::<PathBuf>();
+ 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" })?;
@@ -229,6 +237,70 @@ impl AssetRepository { 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.iter()
+ .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(16, |(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))
+ }
}
mod tests {
diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs index a0ef036..ec7d6ba 100644 --- a/src/launcher/constants.rs +++ b/src/launcher/constants.rs @@ -13,6 +13,9 @@ pub const URL_JRE_MANIFEST: &str = "https://launchermeta.mojang.com/v1/products/ pub const NATIVES_PREFIX: &str = "natives-"; +pub const DEF_INSTANCE_NAME: &'static str = "default"; +pub const DEF_PROFILE_NAME: &'static str = "default"; + lazy_static! { pub static ref NATIVES_DIR_PATTERN: Regex = Regex::new("^natives-(\\d+)").unwrap(); }
\ No newline at end of file diff --git a/src/launcher/extract.rs b/src/launcher/extract.rs index 206d34f..0a08175 100644 --- a/src/launcher/extract.rs +++ b/src/launcher/extract.rs @@ -7,6 +7,7 @@ use std::path::{Component, Path, PathBuf}; use log::{debug, trace}; use zip::result::ZipError; use zip::ZipArchive; +use crate::util; #[derive(Debug)] pub enum ZipExtractError { @@ -48,27 +49,10 @@ impl Error for ZipExtractError { } fn check_entry_path(name: &str) -> Result<&Path, ZipExtractError> { - let entry_path: &Path = Path::new(name); - - let mut depth = 0usize; - for component in entry_path.components() { - depth = match component { - Component::Prefix(_) | Component::RootDir => - return Err(ZipExtractError::InvalidEntry { - why: "root path component in entry", - name: name.to_owned() - }), - Component::ParentDir => depth.checked_sub(1) - .map_or_else(|| Err(ZipExtractError::InvalidEntry { - why: "entry path escapes extraction root", - name: name.to_owned() - }), |s| Ok(s))?, - Component::Normal(_) => depth + 1, - _ => depth - } - } - - Ok(entry_path) + util::check_path(name).map_err(|e| ZipExtractError::InvalidEntry { + why: e, + name: name.to_owned() + }) } #[cfg(unix)] diff --git a/src/launcher/profile.rs b/src/launcher/profile.rs deleted file mode 100644 index 104faef..0000000 --- a/src/launcher/profile.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::path::{Path, PathBuf}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Instance { - pub name: String, - pub path: Option<PathBuf> // relative to launcher home (or absolute) -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Profile { - pub version_id: String, - pub java_runtime: Option<String>, - pub instance: String // ugly that this is a string instead of reference to an Instance but whatever I'm lazy -} - -impl Instance { - fn instance_dir(home: impl AsRef<Path>, name: impl AsRef<Path>) -> PathBuf { - let mut out = home.as_ref().join("instances"); - out.push(name); - out - } - - pub fn get_path(&self, home: impl AsRef<Path>) -> PathBuf { - self.path.as_ref().map(|p| { - if p.is_relative() { - Self::instance_dir(home.as_ref(), p) - } else { - p.to_owned() - } - }).unwrap_or_else(|| Self::instance_dir(home, &self.name)) - } -}
\ No newline at end of file diff --git a/src/launcher/settings.rs b/src/launcher/settings.rs new file mode 100644 index 0000000..5a96589 --- /dev/null +++ b/src/launcher/settings.rs @@ -0,0 +1,184 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use log::warn; +use serde::{Deserialize, Serialize}; +use tokio::{fs, io}; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; +use super::constants; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SettingsInner { + profiles: HashMap<String, Profile>, + instances: HashMap<String, Instance> +} + +pub struct Settings { + path: Option<PathBuf>, + inner: SettingsInner +} + +#[derive(Debug)] +pub enum SettingsError { + IO { what: &'static str, error: io::Error }, + Format(serde_json::Error), + Inconsistent(String) +} + +impl Display for SettingsError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SettingsError::IO { what, error } => write!(f, "settings i/o error ({}): {}", what, error), + SettingsError::Format(err) => write!(f, "settings format error: {}", err), + SettingsError::Inconsistent(err) => write!(f, "inconsistent settings: {}", err), + } + } +} + +impl Error for SettingsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsError::IO { error: err, .. } => Some(err), + SettingsError::Format(err) => Some(err), + _ => None + } + } +} + +impl Default for SettingsInner { + fn default() -> Self { + SettingsInner { + instances: [(String::from(constants::DEF_INSTANCE_NAME), PathBuf::from(constants::DEF_INSTANCE_NAME).into())].into_iter().collect(), + profiles: [(String::from(constants::DEF_PROFILE_NAME), Profile::new(constants::DEF_INSTANCE_NAME))].into_iter().collect() + } + } +} + +impl Settings { + async fn load_inner(path: impl AsRef<Path>) -> Result<SettingsInner, SettingsError> { + match fs::read_to_string(&path).await { + Ok(data) => serde_json::from_str(data.as_str()).map_err(SettingsError::Format), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(SettingsInner::default()), + Err(e) => Err(SettingsError::IO { what: "loading settings", error: e }) + } + } + + fn check_consistent(mut inner: SettingsInner, path: Option<impl AsRef<Path>>) -> Result<Settings, SettingsError> { + inner.profiles.retain(|name, profile| { + if !inner.instances.contains_key(&profile.instance) { + warn!("Settings inconsistency: profile {} refers to instance {}, which does not exist. Ignoring this profile.", name, profile.instance); + false + } else { + true + } + }); + + // there will be more checks later maybe + + Ok(Settings { + path: path.map(|p| p.as_ref().to_owned()), + inner + }) + } + + pub async fn load(path: impl AsRef<Path>) -> Result<Settings, SettingsError> { + Self::check_consistent(Self::load_inner(&path).await?, Some(path)) + } + + pub fn get_path(&self) -> Option<&Path> { + self.path.as_ref().map(|p| p.as_path()) + } + + pub async fn save_to(&self, path: impl AsRef<Path>) -> Result<(), SettingsError> { + let path = path.as_ref(); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await + .map_err(|e| SettingsError::IO { what: "saving settings (creating directory)", error: e })?; + } + + let mut file = File::create(path).await + .map_err(|e| SettingsError::IO { what: "saving settings (open)", error: e })?; + + file.write_all(serde_json::to_string_pretty(&self.inner).map_err(SettingsError::Format)?.as_bytes()).await + .map_err(|e| SettingsError::IO { what: "saving settings (write)", error: e })?; + + Ok(()) + } + + pub async fn save(&self) -> Result<(), SettingsError> { + self.save_to(self.path.as_ref().expect("save() called on Settings instance not loaded from file")).await + } + + pub fn get_instance(&self, name: &str) -> Option<&Instance> { + self.inner.instances.get(name) + } + + pub fn get_profile(&self, name: &str) -> Option<&Profile> { + self.inner.profiles.get(name) + } + + pub fn get_instance_for(&self, profile: &Profile) -> &Instance { + self.inner.instances.get(&profile.instance).unwrap() + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Instance { + path: PathBuf // relative to launcher home (or absolute) +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ProfileVersion { + LatestSnapshot, + LatestRelease, + #[serde(untagged)] + Specific(String) +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Profile { + game_version: ProfileVersion, + java_runtime: Option<String>, + instance: String // ugly that this is a string instead of reference to an Instance but whatever I'm lazy +} + +impl<P: AsRef<Path>> From<P> for Instance { + fn from(path: P) -> Self { + Self { path: path.as_ref().into() } + } +} + +impl Instance { + pub async fn get_path(&self, home: impl AsRef<Path>) -> Result<PathBuf, io::Error> { + let path = self.path.as_path(); + + if path.is_relative() { + Ok([home.as_ref(), Path::new("instances"), path].iter().collect::<PathBuf>()) + } else { + fs::canonicalize(path).await + } + } +} + +impl Profile { + fn new(instance_name: &str) -> Self { + Self { + game_version: ProfileVersion::LatestRelease, + java_runtime: None, + instance: instance_name.into() + } + } + + pub fn get_version(&self) -> &ProfileVersion { + &self.game_version + } + + pub fn get_instance_name(&self) -> &str { + &self.instance + } +} diff --git a/src/launcher/version.rs b/src/launcher/version.rs index 2e320f2..f857b93 100644 --- a/src/launcher/version.rs +++ b/src/launcher/version.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use log::{debug, info, warn}; use sha1_smol::Digest; use tokio::{fs, io}; +use crate::launcher::settings::ProfileVersion; use crate::util; use crate::version::{*, manifest::*}; @@ -234,8 +235,6 @@ impl Display for VersionResolveError { impl Error for VersionResolveError {} - - impl VersionList { async fn create_dir_for(home: &Path) -> Result<(), io::Error> { debug!("Creating versions directory."); @@ -285,6 +284,14 @@ impl VersionList { .map_or_else(|| self.local.versions.get(id).into(), |r| r.versions.get(id).into()) } + 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::Specific(ver) => Some(Cow::Borrowed(ver)) + } + } + pub fn get_remote_version(&self, id: &str) -> Option<&VersionManifestVersion> { let remote = self.remote.as_ref().expect("get_remote_version called in offline mode!"); diff --git a/src/util.rs b/src/util.rs index fe11c38..a7c2d5e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use log::debug; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; @@ -110,3 +110,21 @@ pub async fn verify_file(path: impl AsRef<Path>, expect_size: Option<usize>, exp Ok(()) } + +pub fn check_path(name: &str) -> Result<&Path, &'static str> { + let entry_path: &Path = Path::new(name); + + let mut depth = 0usize; + for component in entry_path.components() { + depth = match component { + Component::Prefix(_) | Component::RootDir => + return Err("root path component in entry"), + Component::ParentDir => depth.checked_sub(1) + .map_or_else(|| Err("entry path escapes"), |s| Ok(s))?, + Component::Normal(_) => depth + 1, + _ => depth + } + } + + Ok(entry_path) +} |
