summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-22 02:08:31 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-22 02:10:20 -0600
commite88c17a44c94f788e945c5728bc18beca7e0f8a6 (patch)
tree586873a545cb6bc799129219d5e809106daa58c1
parentsupport jre specified in profile (diff)
get started on downloading JREs
also refactor ensure file logic
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--src/launcher.rs115
-rw-r--r--src/launcher/jre.rs101
-rw-r--r--src/launcher/jre/arch.rs45
-rw-r--r--src/launcher/jre/manifest.rs49
-rw-r--r--src/util.rs122
7 files changed, 326 insertions, 108 deletions
diff --git a/Cargo.lock b/Cargo.lock
index dd73e3a..d31e1be 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2474,6 +2474,7 @@ dependencies = [
name = "o3launcher"
version = "0.1.0"
dependencies = [
+ "cfg-if",
"chrono",
"const_format",
"futures",
diff --git a/Cargo.toml b/Cargo.toml
index 5b3e38d..b5586ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);