summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-18 23:47:48 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-18 23:47:48 -0600
commitcd8bf1667494070c3a22ab5d63b559a9742b8a1a (patch)
tree6f93f0c0fbdccfa18733499845a8bc7c298c402f
parentbuilding classpath (diff)
more stuff
-rw-r--r--ozone-cli/src/main.rs19
-rw-r--r--src/launcher.rs144
-rw-r--r--src/launcher/assets.rs84
-rw-r--r--src/launcher/constants.rs3
-rw-r--r--src/launcher/extract.rs26
-rw-r--r--src/launcher/profile.rs33
-rw-r--r--src/launcher/settings.rs184
-rw-r--r--src/launcher/version.rs11
-rw-r--r--src/util.rs20
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)
+}