diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/launcher.rs | 115 | ||||
| -rw-r--r-- | src/launcher/jre.rs | 101 | ||||
| -rw-r--r-- | src/launcher/jre/arch.rs | 45 | ||||
| -rw-r--r-- | src/launcher/jre/manifest.rs | 49 | ||||
| -rw-r--r-- | src/util.rs | 122 |
7 files changed, 326 insertions, 108 deletions
@@ -2474,6 +2474,7 @@ dependencies = [ name = "o3launcher" version = "0.1.0" dependencies = [ + "cfg-if", "chrono", "const_format", "futures", @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +cfg-if = "1.0.0" chrono = { version = "0.4.39", default-features = false, features = ["std", "alloc", "clock", "now", "serde"] } const_format = "0.2.34" futures = "0.3.31" diff --git a/src/launcher.rs b/src/launcher.rs index f8be78b..19a9fa7 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -7,6 +7,7 @@ mod assets; mod extract; mod settings; mod runner; +mod jre; use std::borrow::Cow; use std::cmp::min; @@ -24,11 +25,8 @@ use const_format::formatcp; use futures::{StreamExt, TryStreamExt}; use log::{debug, info, trace, warn}; use reqwest::Client; -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 download::{MultiDownloader, VerifiedDownload}; use rules::{CompatCheck, IncompatibleError}; @@ -36,13 +34,14 @@ use version::{VersionList, VersionResolveError, VersionResult}; use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo, LibraryExtractRule, CompleteVersion, FeatureMatcher}; use assets::{AssetError, AssetRepository}; -use crate::util::{self, AsJavaPath, FileVerifyError, IntegrityError}; +use crate::util::{self, AsJavaPath}; pub use settings::*; pub use runner::run_the_game; +pub use crate::util::{EnsureFileError, FileVerifyError, IntegrityError}; use crate::assets::AssetIndex; -use crate::launcher::runner::ArgumentType; -use crate::launcher::strsub::SubFunc; +use runner::ArgumentType; +use strsub::SubFunc; use crate::version::manifest::VersionType; #[derive(Debug)] @@ -126,11 +125,8 @@ pub enum LaunchError { LibraryClasspathError(JoinPathsError), // ensure file errors - MissingURL, + EnsureFile(EnsureFileError), IO { what: &'static str, error: io::Error }, - Offline, - Download { url: String, error: reqwest::Error }, - Integrity(IntegrityError), // log errors UnknownLogType(String), @@ -158,11 +154,8 @@ impl Display for LaunchError { LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks LaunchError::LibraryExtractError(e) => write!(f, "library extract zip error: {e}"), LaunchError::LibraryClasspathError(e) => write!(f, "error building classpath: {e}"), - LaunchError::MissingURL => f.write_str("cannot download required file, URL is missing"), LaunchError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), - LaunchError::Offline => f.write_str("cannot download file in offline mode"), - LaunchError::Download { url, error } => write!(f, "failed to download file ({}): {}", url, error), - LaunchError::Integrity(e) => write!(f, "file verify error: {}", e), + LaunchError::EnsureFile(e) => e.fmt(f), LaunchError::UnknownLogType(t) => write!(f, "unknown log type: {}", t), LaunchError::InvalidLogId(Some(id)) => write!(f, "invalid log id: {}", id), LaunchError::InvalidLogId(None) => write!(f, "missing log id"), @@ -184,8 +177,7 @@ impl Error for LaunchError { LaunchError::LibraryExtractError(e) => Some(e), LaunchError::LibraryClasspathError(e) => Some(e), LaunchError::IO { error: e, .. } => Some(e), - LaunchError::Download { error: e, .. } => Some(e), - LaunchError::Integrity(e) => Some(e), + LaunchError::EnsureFile(e) => Some(e), LaunchError::Assets(e) => Some(e), LaunchError::ResolveJavaRuntime { error: e, .. } => Some(e), _ => None @@ -272,91 +264,6 @@ impl Launcher { lib.natives.as_ref().map_or(None, |n| n.get(&self.system_info.os)).map(|s| s.as_str()) } - async fn ensure_file(&self, path: &Path, dlinfo: &DownloadInfo) -> Result<(), LaunchError> { - // verify the file - match util::verify_file(path, dlinfo.size, dlinfo.sha1).await { - // integrity passed. return - Ok(_) => { - info!("File {} exists and integrity matches. Skipping.", path.display()); - return Ok(()); - }, - - // ruh roh - Err(e) => match e { - FileVerifyError::Open(_, ioe) if ioe.kind() != ErrorKind::NotFound => - return Err(LaunchError::IO{ what: "verify file (open)", error: ioe }), - FileVerifyError::Read(_, ioe) => return Err(LaunchError::IO{ what: "verify file (read)", error: ioe }), - FileVerifyError::Integrity(_, ie) => info!("file {} failed integrity check: {}", path.display(), ie), - _ => () - } - } - - if !self.online { - warn!("Cannot download file {}! We are offline. Rerun the launcher in online mode to launch this version.", path.display()); - return Err(LaunchError::Offline); - } - - // download it - let Some(url) = dlinfo.url.as_ref() else { - return Err(LaunchError::MissingURL); - }; - - let mut file = File::create(path).await.map_err(|e| LaunchError::IO { - what: "save downloaded file (open)", - error: e - })?; - - debug!("File {} must be downloaded ({}).", path.display(), url); - - let mut response = reqwest::get(url).await.map_err(|e| LaunchError::Download{ url: url.to_owned(), error: e })?; - let mut tally = 0usize; - let mut sha1 = Sha1::new(); - - while let Some(chunk) = response.chunk().await.map_err(|e| LaunchError::Download{ url: url.to_owned(), error: e })? { - let slice = chunk.as_ref(); - - file.write_all(slice).await.map_err(|e| LaunchError::IO { - what: "save downloaded file (write)", - error: e - })?; - - tally += slice.len(); - sha1.update(slice); - } - - drop(file); // manually close file - - let del_file_silent = || async { - debug!("Deleting downloaded file {} since its integrity doesn't match :(", path.display()); - let _ = fs::remove_file(path).await.map_err(|e| warn!("failed to delete invalid downloaded file: {}", e)); - () - }; - - if dlinfo.size.is_some_and(|s| s != tally) { - del_file_silent().await; - - return Err(LaunchError::Integrity(IntegrityError::SizeMismatch { - expect: dlinfo.size.unwrap(), - actual: tally - })); - } - - let digest = sha1.digest(); - - if dlinfo.sha1.is_some_and(|exp_dig| exp_dig != digest) { - del_file_silent().await; - - return Err(LaunchError::Integrity(IntegrityError::Sha1Mismatch { - expect: dlinfo.sha1.unwrap(), - actual: digest - })); - } - - info!("File {} downloaded successfully.", path.display()); - - Ok(()) - } - async fn log_config_ensure(&self, config: &Logging) -> Result<String, LaunchError> { info!("Ensuring log configuration exists and is valid."); @@ -381,7 +288,8 @@ impl Launcher { debug!("Logger config {} is at {}", id, path.display()); - self.ensure_file(&path, dlinfo).await?; + util::ensure_file(&path, dlinfo.url.as_ref().map(|s| s.as_str()), dlinfo.size, dlinfo.sha1, self.online).await + .map_err(|e| LaunchError::EnsureFile(e))?; struct PathSub<'a>(&'a Path); impl<'a> SubFunc<'a> for PathSub<'a> { @@ -535,7 +443,8 @@ impl Launcher { info!("Downloading client jar {}", client_path.display()); - self.ensure_file(client_path.as_path(), client).await?; + util::ensure_file(client_path.as_path(), client.url.as_ref().map(|s| s.as_str()), client.size, client.sha1, self.online).await + .map_err(|e| LaunchError::EnsureFile(e))?; client_jar_path = Some(client_path); } else { diff --git a/src/launcher/jre.rs b/src/launcher/jre.rs new file mode 100644 index 0000000..2979c52 --- /dev/null +++ b/src/launcher/jre.rs @@ -0,0 +1,101 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; +use log::{debug, info, warn}; +use tokio::{fs, io}; + +mod arch; +mod manifest; + +use arch::JRE_ARCH; +use manifest::JavaRuntimesManifest; +use crate::launcher::jre::manifest::JavaRuntimeManifest; +use super::constants; + +pub struct JavaRuntimeRepository { + home: PathBuf, + manifest: JavaRuntimesManifest +} + +impl JavaRuntimeRepository { + pub async fn new(home: impl AsRef<Path>) -> Result<Self, JavaRuntimeError> { + info!("Java runtime architecture is \"{}\".", JRE_ARCH); + + fs::create_dir_all(&home).await.map_err(|e| JavaRuntimeError::IO { what: "creating home directory", error: e })?; + + let manifest: JavaRuntimesManifest = reqwest::get(constants::URL_JRE_MANIFEST).await + .map_err(|e| JavaRuntimeError::Download { + what: "runtime manifest (all.json)", + error: e + })?.json().await + .map_err(|e| JavaRuntimeError::Download { + what: "runtime manifest (all.json)", + error: e + })?; + + Ok(JavaRuntimeRepository { + home: home.as_ref().to_path_buf(), + manifest + }) + } + + pub async fn choose_runtime(&self, component: &str) -> Result<JavaRuntimeManifest, JavaRuntimeError> { + let Some(runtime_components) = self.manifest.get(JRE_ARCH) else { + return Err(JavaRuntimeError::UnsupportedArch(JRE_ARCH)); + }; + + let Some(runtime_component) = runtime_components.get(component) else { + return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() }); + }; + + let Some(runtime) = runtime_component.iter().filter(|r| r.availability.progress == 100).next() else { + if !runtime_components.is_empty() { + warn!("Weird: the only java runtimes in {JRE_ARCH}.{component} has a progress of less than 100!"); + } + + return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() }); + }; + + let Some(ref url) = runtime.manifest.url else { + return Err(JavaRuntimeError::MalformedManifest); + }; + + debug!("Ensuring manifest for runtime {JRE_ARCH}.{component}: {url}"); + + + // hmm maybe + + todo!() + } +} + +#[derive(Debug)] +pub enum JavaRuntimeError { + IO { what: &'static str, error: io::Error }, + Download { what: &'static str, error: reqwest::Error }, + UnsupportedArch(&'static str), + UnsupportedComponent { arch: &'static str, component: String }, + MalformedManifest +} + +impl Display for JavaRuntimeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + JavaRuntimeError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), + JavaRuntimeError::Download { what, error } => write!(f, "error downloading {}: {}", what, error), + JavaRuntimeError::UnsupportedArch(arch) => write!(f, r#"unsupported architecture "{arch}""#), + JavaRuntimeError::UnsupportedComponent { arch, component } => write!(f, r#"unsupported component "{component}" for architecture "{arch}""#), + JavaRuntimeError::MalformedManifest => f.write_str("malformed runtime manifest (launcher bug?)"), + } + } +} + +impl Error for JavaRuntimeError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + JavaRuntimeError::IO { error, .. } => Some(error), + JavaRuntimeError::Download { error, .. } => Some(error), + _ => None + } + } +} diff --git a/src/launcher/jre/arch.rs b/src/launcher/jre/arch.rs new file mode 100644 index 0000000..e984171 --- /dev/null +++ b/src/launcher/jre/arch.rs @@ -0,0 +1,45 @@ +use cfg_if::cfg_if; + +macro_rules! define_arch { + ($arch:expr) => { + pub const JRE_ARCH: &str = $arch; + } +} + +cfg_if! { + if #[cfg(target_os = "windows")] { + cfg_if! { + if #[cfg(target_arch = "x86_64")] { + define_arch!("windows-x64"); + } else if #[cfg(target_arch = "x86")] { + define_arch!("windows-x86"); + } else if #[cfg(target_arch = "aarch64")] { + define_arch!("windows-arm64"); + } else { + define_arch!("gamecore"); + } + } + } else if #[cfg(target_os = "linux")] { + cfg_if! { + if #[cfg(target_arch = "x86_64")] { + define_arch!("linux"); + } else if #[cfg(target_arch = "x86")] { + define_arch!("linux-i386"); + } else { + define_arch!("gamecore"); + } + } + } else if #[cfg(target_os = "macos")] { + cfg_if! { + if #[cfg(target_arch = "aarch64")] { + define_arch!("mac-os-arm64"); + } else if #[cfg(target_arch = "x86_64")] { + define_arch!("mac-os"); + } else { + define_arch!("gamecore"); + } + } + } else { + define_arch!("gamecore"); + } +} diff --git a/src/launcher/jre/manifest.rs b/src/launcher/jre/manifest.rs new file mode 100644 index 0000000..9b84377 --- /dev/null +++ b/src/launcher/jre/manifest.rs @@ -0,0 +1,49 @@ +use std::collections::HashMap; +use serde::Deserialize; +use crate::version::DownloadInfo; + +#[derive(Debug, Deserialize)] +pub struct Availability { + pub group: u32, // unknown meaning + pub progress: u32 // unknown meaning +} + +#[derive(Debug, Deserialize)] +pub struct Version { + pub name: String, + pub version: String +} + +#[derive(Debug, Deserialize)] +pub struct JavaRuntimeInfo { + // I don't see how half of this information is useful with how the JRE system currently functions -figboot + pub availability: Availability, + pub manifest: DownloadInfo, + pub version: Version +} + +pub type JavaRuntimesManifest = HashMap<String, HashMap<String, Vec<JavaRuntimeInfo>>>; + +#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum FileType { + File, + Directory, + Link +} + +#[derive(Debug, Deserialize)] +pub struct JavaRuntimeFile { + #[serde(rename = "type")] + pub file_type: FileType, + #[serde(default)] + pub executable: bool, + + pub lzma: DownloadInfo, + pub raw: DownloadInfo +} + +#[derive(Debug, Deserialize)] +pub struct JavaRuntimeManifest { + pub files: HashMap<String, JavaRuntimeFile> +} diff --git a/src/util.rs b/src/util.rs index 7927620..1184a83 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,13 +1,13 @@ use std::error::Error; -use std::ffi::OsStr; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; -use std::path::{Component, Path, PathBuf, Prefix}; -use log::debug; +use std::path::{Component, Path, PathBuf}; +use log::{debug, info, warn}; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; -use tokio::io::AsyncReadExt; -use crate::util; +use tokio::{fs, io}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use crate::launcher::LaunchError; #[derive(Debug)] pub enum IntegrityError { @@ -113,6 +113,118 @@ pub async fn verify_file(path: impl AsRef<Path>, expect_size: Option<usize>, exp Ok(()) } +#[derive(Debug)] +pub enum EnsureFileError { + IO { what: &'static str, error: io::Error }, + Download { url: String, error: reqwest::Error }, + Integrity(IntegrityError), + Offline, + MissingURL +} + +impl Display for EnsureFileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EnsureFileError::IO { what, error } => write!(f, "i/o error ensuring file ({what}): {error}"), + EnsureFileError::Download { url, error } => write!(f, "error downloading file ({url}): {error}"), + EnsureFileError::Integrity(e) => write!(f, "integrity error for downloaded file: {e}"), + EnsureFileError::Offline => f.write_str("unable to download file while offline"), + EnsureFileError::MissingURL => f.write_str("missing url"), + } + } +} + +impl Error for EnsureFileError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + EnsureFileError::IO { error, .. } => Some(error), + EnsureFileError::Download { error, .. } => Some(error), + EnsureFileError::Integrity(error) => Some(error), + _ => None + } + } +} + +pub async fn ensure_file(path: impl AsRef<Path>, url: Option<&str>, expect_size: Option<usize>, expect_sha1: Option<Digest>, online: bool) -> Result<bool, EnsureFileError> { + let path = path.as_ref(); + + match verify_file(path, expect_size, expect_sha1).await { + Ok(_) => { + info!("File {} exists and integrity matches. Skipping.", path.display()); + return Ok(false); + }, + Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => (), + Err(FileVerifyError::Integrity(_, e)) => + info!("File {} on disk failed integrity check: {}", path.display(), e), + Err(FileVerifyError::Open(_, e)) | Err(FileVerifyError::Read(_, e)) => + return Err(EnsureFileError::IO { what: "verifying fileon disk", error: e }) + } + + if !online { + warn!("Cannot download {} to {} while offline!", url.unwrap_or("(no url)"), path.display()); + return Err(EnsureFileError::Offline); + } + + // download the file + let Some(url) = url else { + return Err(EnsureFileError::MissingURL); + }; + + let mut file = File::create(path).await.map_err(|e| EnsureFileError::IO { + what: "save downloaded file (open)", + error: e + })?; + + 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 tally = 0usize; + let mut sha1 = Sha1::new(); + + while let Some(chunk) = response.chunk().await.map_err(|e| EnsureFileError::Download { url: url.to_owned(), error: e })? { + let slice = chunk.as_ref(); + + file.write_all(slice).await.map_err(|e| EnsureFileError::IO { + what: "save downloaded file (write)", + error: e + })?; + + tally += slice.len(); + sha1.update(slice); + } + + drop(file); // manually close file + + let del_file_silent = || async { + debug!("Deleting downloaded file {} since its integrity doesn't match :(", path.display()); + let _ = fs::remove_file(path).await.map_err(|e| warn!("failed to delete invalid downloaded file: {}", e)); + () + }; + + if expect_size.is_some_and(|s| s != tally) { + del_file_silent().await; + + return Err(EnsureFileError::Integrity(IntegrityError::SizeMismatch { + expect: expect_size.unwrap(), + actual: tally + })); + } + + let digest = sha1.digest(); + + if expect_sha1.is_some_and(|exp_dig| exp_dig != digest) { + del_file_silent().await; + + return Err(EnsureFileError::Integrity(IntegrityError::Sha1Mismatch { + expect: expect_sha1.unwrap(), + actual: digest + })); + } + + info!("File {} downloaded successfully.", path.display()); + Ok(true) +} + pub fn check_path(name: &str) -> Result<&Path, &'static str> { let entry_path: &Path = Path::new(name); |
