From ff428f36935cefd2b6d8ea6ba4a0572d75d6512d Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Mon, 13 Jan 2025 20:58:36 -0600 Subject: library downloads complete --- src/launcher.rs | 61 +++++++++++++++++++------ src/launcher/download.rs | 4 ++ src/launcher/rules.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++ src/launcher/version.rs | 13 +++--- src/version.rs | 46 +++++++++++++++---- 5 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 src/launcher/rules.rs (limited to 'src') diff --git a/src/launcher.rs b/src/launcher.rs index a849cc5..83ec342 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -3,6 +3,7 @@ mod version; mod profile; mod strsub; mod download; +mod rules; use std::borrow::Cow; use std::collections::HashMap; @@ -12,19 +13,18 @@ use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use const_format::formatcp; -use futures::TryFutureExt; -use log::{debug, warn}; +use futures::StreamExt; +use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; -use sha1_smol::Sha1; use sysinfo::System; -use tokio::fs::File; use tokio::{fs, io}; -use tokio::io::AsyncReadExt; use version::VersionList; -use profile::{Instance, Profile}; -use crate::launcher::download::{Download, VerifiedDownload}; -use crate::launcher::version::{VersionResolveError, VersionResult}; -use crate::version::{DownloadInfo, Library, OSRestriction, OperatingSystem}; +use download::{MultiDownloader, PhaseDownloadError, VerifiedDownload}; +use rules::{CompatCheck, IncompatibleError}; +use version::{VersionResolveError, VersionResult}; +use crate::version::{Library, OSRestriction, OperatingSystem}; + +pub use profile::{Instance, Profile}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { @@ -95,9 +95,14 @@ pub struct Launcher { #[derive(Debug)] pub enum LaunchError { + // version resolution errors UnknownVersion(String), LoadVersion(Box), - ResolveVersion(VersionResolveError) + ResolveVersion(VersionResolveError), + IncompatibleVersion(IncompatibleError), + + // library errors + LibraryDirError(PathBuf, io::Error) } impl Display for LaunchError { @@ -105,7 +110,9 @@ impl Display for LaunchError { match &self { 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}") + LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}"), + LaunchError::IncompatibleVersion(e) => e.fmt(f), + LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e) } } } @@ -115,6 +122,8 @@ impl Error for LaunchError { match &self { LaunchError::LoadVersion(e) => Some(e.as_ref()), LaunchError::ResolveVersion(e) => Some(e), + LaunchError::IncompatibleVersion(e) => Some(e), + LaunchError::LibraryDirError(_, e) => Some(e), _ => None } } @@ -196,10 +205,34 @@ impl Launcher { }; let ver = self.versions.resolve_version(ver.as_ref()).await.map_err(|e| LaunchError::ResolveVersion(e))?; + ver.rules_apply(&self.system_info, |_| false).map_err(|e| LaunchError::IncompatibleVersion(e))?; + + let mut libs = Vec::new(); + let mut downloads = Vec::new(); + + for lib in ver.libraries.values() { + if lib.rules_apply(&self.system_info, |_| false).is_err() { + continue; + } + + libs.push(lib); + if let Some(dl) = self.libraries.create_download(lib, self.system_info.os) { + dl.make_dirs().await.map_err(|e| LaunchError::LibraryDirError(dl.get_path().to_path_buf(), e))?; + downloads.push(dl); + } + } + + // TODO: offline + info!("Downloading {} libraries...", downloads.len()); + + let mut multi = MultiDownloader::new(downloads); + let dl: Vec<_> = multi.perform().await.collect().await; + info!("amogus: {dl:?}"); - // todo: make a list of all the required libraries + // todo: offline mode - todo!() + //todo!() + Ok(()) } } @@ -264,7 +297,7 @@ impl LibraryRepository { } fn create_download(&self, lib: &Library, os: OperatingSystem) -> Option { - let classifier = lib.natives.as_ref()?.get(&os).map(|s| s.as_str()); + let classifier = lib.natives.as_ref().map_or(None, |n| n.get(&os)).map(|s| s.as_str()); if lib.url.is_some() || lib.downloads.is_none() { // TODO: derive download URL in this situation? diff --git a/src/launcher/download.rs b/src/launcher/download.rs index ec89a15..813117c 100644 --- a/src/launcher/download.rs +++ b/src/launcher/download.rs @@ -206,6 +206,10 @@ impl VerifiedDownload { self } + pub fn get_path(&self) -> &Path { + &self.path + } + pub async fn make_dirs(&self) -> Result<(), io::Error> { fs::create_dir_all(self.path.parent().expect("download created with no containing directory (?)")).await } diff --git a/src/launcher/rules.rs b/src/launcher/rules.rs new file mode 100644 index 0000000..29da8a2 --- /dev/null +++ b/src/launcher/rules.rs @@ -0,0 +1,114 @@ +use std::error::Error; +use std::fmt::Display; +use crate::version::{Argument, CompatibilityRule, CompleteVersion, FeatureMatcher, Library, OSRestriction, RuleAction}; +use super::SystemInfo; + +#[derive(Debug)] +pub struct IncompatibleError { + what: &'static str, + reason: Option +} + +impl Display for IncompatibleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(reason) = self.reason.as_ref() { + write!(f, "{} incompatible: {}", self.what, reason) + } else { + write!(f, "{} incompatible", self.what) + } + } +} + +impl Error for IncompatibleError {} + +mod seal { + pub trait CompatCheckInner { + const WHAT: &'static str; + + fn get_rules(&self) -> Option>; + fn get_incompatibility_reason(&self) -> Option<&str>; + } +} + +pub trait CompatCheck: seal::CompatCheckInner { + fn rules_apply<'a>(&'a self, system: &SystemInfo, feature_matcher: impl FeatureMatcher<'a>) -> Result<(), IncompatibleError> { + let Some(rules) = self.get_rules() else { return Ok(()) }; + let mut action = RuleAction::Disallow; + + fn match_os(os: &OSRestriction, system: &SystemInfo) -> bool { + os.os.is_none_or(|o| system.is_our_os(o)) + && os.version.as_ref().is_none_or(|v| v.is_match(system.os_version.as_str())) + && os.arch.as_ref().is_none_or(|a| a.is_match(system.arch.as_str())) + } + + for rule in rules { + if rule.os.as_ref().is_none_or(|o| match_os(o, system)) + && rule.features_match(feature_matcher) { + action = rule.action; + } + } + + if action == RuleAction::Disallow { + Err(IncompatibleError { + what: Self::WHAT, + reason: self.get_incompatibility_reason().map(|s| s.to_owned()) + }) + } else { + Ok(()) + } + } +} + +// trivial +impl seal::CompatCheckInner for CompatibilityRule { + const WHAT: &'static str = "rule"; + + fn get_rules(&self) -> Option> { + Some(Some(self)) + } + + fn get_incompatibility_reason(&self) -> Option<&str> { + None + } +} + +impl seal::CompatCheckInner for CompleteVersion { + const WHAT: &'static str = "version"; + + fn get_rules(&self) -> Option> { + self.compatibility_rules.as_ref() + } + + fn get_incompatibility_reason(&self) -> Option<&str> { + self.incompatibility_reason.as_ref().map(|s| s.as_str()) + } +} + +impl seal::CompatCheckInner for Library { + const WHAT: &'static str = "library"; + + fn get_rules(&self) -> Option> { + self.rules.as_ref() + } + + fn get_incompatibility_reason(&self) -> Option<&str> { + None + } +} + +impl seal::CompatCheckInner for Argument { + const WHAT: &'static str = "argument"; + + fn get_rules(&self) -> Option> { + self.rules.as_ref() + } + + fn get_incompatibility_reason(&self) -> Option<&str> { + None + } +} + +impl CompatCheck for CompatibilityRule {} +impl CompatCheck for CompleteVersion {} +impl CompatCheck for Library {} +impl CompatCheck for Argument {} \ No newline at end of file diff --git a/src/launcher/version.rs b/src/launcher/version.rs index 411ac59..40bb953 100644 --- a/src/launcher/version.rs +++ b/src/launcher/version.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use std::fmt::Display; use std::path::{Path, PathBuf}; -use log::{debug, info, warn}; +use log::{debug, info, trace, warn}; use sha1_smol::Digest; use tokio::{fs, io}; use crate::util; @@ -51,19 +51,22 @@ impl RemoteVersionList { // download it let ver_text = reqwest::get(ver.url.as_str()).await?.error_for_status()?.text().await?; - + debug!("Validating downloaded {}...", ver.id); // make sure it's valid util::verify_sha1(ver.sha1, ver_text.as_str()) .map_err::, _>(|e| format!("downloaded version {} has wrong hash! (expect {}, got {})", ver.id.as_str(), &ver.sha1, e).as_str().into())?; - + // make sure it's well-formed - let cver: CompleteVersion = serde_json::from_str(ver.url.as_str())?; + let cver: CompleteVersion = serde_json::from_str(ver_text.as_str())?; debug!("Saving version {}...", ver.id); // write it out - tokio::fs::write(path, ver_text).await?; + tokio::fs::write(path, ver_text).await.map_err(|e| { + warn!("Failed to save version {}: {}", ver.id, e); + e + })?; info!("Done downloading and verifying {}!", ver.id); diff --git a/src/version.rs b/src/version.rs index ae91149..af61481 100644 --- a/src/version.rs +++ b/src/version.rs @@ -10,7 +10,7 @@ use sha1_smol::Digest; pub mod manifest; use manifest::*; -#[derive(Deserialize, Debug, Clone, Copy)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum RuleAction { Allow, @@ -81,21 +81,51 @@ pub struct CompatibilityRule { pub os: Option } -pub trait FeatureMatcher { - fn matches(&self, feature: &str) -> bool; -} -impl FeatureMatcher for F +/// ## Details +/// ### Why is the lifetime `'f` specified instead of elided? +/// This trait has a blanket implementation for all `Fn(&str) -> bool`, with the intent of allowing +/// one to specify trivial feature matcher implementations using closures (maybe `|_| false` or +/// `|f| f == "something"`). If the lifetime is left unbounded, the following error is produced by +/// the compiler: +/// +/// ```text +/// Implementation of `Fn` is not general enough +/// Note: closure with signature `fn(&'2 str) -> bool` must implement `Fn<(&'1 str,)>`, for any lifetime `'1`... +/// Note: ...but it actually implements `Fn<(&'2 str,)>`, for some specific lifetime `'2` +/// ``` +/// +/// ### How do I implement this type for non-`Copy` custom feature matchers? +/// The short answer is: implement the trait for the reference type. Here's an example. +/// ```rust +/// use o3launcher::version::FeatureMatcher; +/// +/// #[derive(Clone)] // can't derive Copy here +/// struct FM { +/// our_feature: String // heap-owned data! +/// } +/// +/// impl<'f> FeatureMatcher<'f> for &FM { +/// fn matches(self, feature: &'f str) -> bool { +/// self.our_feature == feature +/// } +/// } +/// ``` +pub trait FeatureMatcher<'f>: Copy { + fn matches(self, feature: &'f str) -> bool; +} + +impl<'f, F> FeatureMatcher<'f> for F where - F: Fn(&str) -> bool + F: Fn(&'f str) -> bool + Copy { - fn matches(&self, feature: &str) -> bool { + fn matches(self, feature: &'f str) -> bool { self(feature) } } impl CompatibilityRule { - pub fn features_match(&self, checker: impl FeatureMatcher) -> bool { + pub fn features_match<'a>(&'a self, checker: impl FeatureMatcher<'a>) -> bool { if let Some(m) = self.features.as_ref() { for (feat, expect) in m { if checker.matches(feat) != *expect { -- cgit v1.2.3-70-g09d2