use std::collections::HashSet; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; use futures::{stream, StreamExt, TryStreamExt}; use log::{debug, info, warn}; use reqwest::Client; use tokio::{fs, io, io::ErrorKind}; use tokio::fs::File; use tokio::io::AsyncWriteExt; mod arch; mod manifest; mod download; use arch::JRE_ARCH; use manifest::JavaRuntimesManifest; use manifest::JavaRuntimeManifest; use crate::launcher::download::MultiDownloader; use crate::launcher::jre::download::{LzmaDownloadError, LzmaDownloadJob}; use crate::launcher::jre::manifest::JavaRuntimeFile; use crate::util; use crate::util::{EnsureFileError, FileVerifyError, IntegrityError}; use crate::version::DownloadInfo; use super::constants; pub struct JavaRuntimeRepository { online: bool, home: PathBuf, manifest: JavaRuntimesManifest } impl JavaRuntimeRepository { pub async fn new(home: impl AsRef, online: bool) -> Result { 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_path = home.as_ref().join("manifest.json"); match util::ensure_file(manifest_path.as_path(), Some(constants::URL_JRE_MANIFEST), None, None, online, true).await { Ok(_) => (), Err(EnsureFileError::Offline) => { info!("Launcher is offline, cannot download runtime manifest."); }, Err(e) => return Err(JavaRuntimeError::EnsureFile(e)) }; let manifest_file = fs::read_to_string(&manifest_path).await .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?; Ok(JavaRuntimeRepository { online, home: home.as_ref().to_path_buf(), manifest: serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtimes manifest", error: e })?, }) } fn get_component_dir(&self, component: &str) -> PathBuf { [self.home.as_path(), Path::new(JRE_ARCH), Path::new(component)].into_iter().collect() } async fn load_runtime_manifest(&self, component: &str, info: &DownloadInfo) -> Result { let comp_dir = self.get_component_dir(component); let manifest_path = comp_dir.join("manifest.json"); debug!("Ensuring manifest for runtime {JRE_ARCH}.{component}"); fs::create_dir_all(comp_dir.as_path()).await .inspect_err(|e| warn!("Failed to create directory for JRE component {}: {}", component, e)) .map_err(|e| JavaRuntimeError::IO { what: "creating component directory", error: e })?; util::ensure_file(&manifest_path, info.url.as_ref().map(|s| s.as_str()), info.size, info.sha1, self.online, false).await .map_err(JavaRuntimeError::EnsureFile)?; let manifest_file = fs::read_to_string(&manifest_path).await .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?; Ok(serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtime manifest", error: e })?) } // not very descriptive function name pub async fn choose_runtime(&self, component: &str) -> Result { 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() }); }; self.load_runtime_manifest(component, &runtime.manifest).await } fn clean_up_runtime_sync(path: &Path, manifest: Arc) -> Result<(), io::Error> { for entry in walkdir::WalkDir::new(path).contents_first(true) { let entry = entry?; let rel_path = entry.path().strip_prefix(path).expect("walkdir escaped root (???)"); if rel_path.components().filter(|c| !matches!(c, Component::CurDir)).next().is_none() { // if this path is trivial (points at the root), ignore it continue; } let rel_path_str; if std::path::MAIN_SEPARATOR != '/' { rel_path_str = rel_path.to_str().map(|s| s.replace(std::path::MAIN_SEPARATOR, "/")); } else { rel_path_str = rel_path.to_str().map(String::from); } if !rel_path_str.as_ref().is_some_and(|s| manifest.files.get(s) .is_some_and(|f| (f.is_file() == entry.file_type().is_file()) || (f.is_directory() == entry.file_type().is_dir()) || (f.is_link() == entry.file_type().is_symlink()))) { // path is invalid utf-8, extraneous, or of the wrong type debug!("File {} is extraneous or of wrong type ({:?}). Deleting it.", entry.path().display(), entry.file_type()); if entry.file_type().is_dir() { std::fs::remove_dir(entry.path())?; } else { std::fs::remove_file(entry.path())?; } } } Ok(()) } async fn clean_up_runtime(path: &Path, manifest: Arc) -> Result<(), io::Error> { let (tx, rx) = tokio::sync::oneshot::channel(); let path = path.to_owned(); let manifest = manifest.clone(); tokio::task::spawn_blocking(move || { let res = Self::clean_up_runtime_sync(&path, manifest); let _ = tx.send(res); }).await.expect("clean_up_runtime_sync panicked"); rx.await.expect("clean_up_runtime_sync hung up") } async fn ensure_jre_dirs(&self, path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { stream::iter(manifest.files.iter().filter(|(_, f)| f.is_directory())) .map::, _>(|(name, _)| Ok(name)) .try_for_each(|name| async move { let ent_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?; let ent_path = [path, ent_path].into_iter().collect::(); match fs::metadata(&ent_path).await { Ok(meta) => { if !meta.is_dir() { debug!("Deleting misplaced file at {}", ent_path.display()); fs::remove_file(&ent_path).await.map_err(|e| JavaRuntimeError::IO { what: "deleting misplaced file", error: e })?; } }, Err(e) if e.kind() == ErrorKind::NotFound => (), Err(e) => return Err(JavaRuntimeError::IO { what: "'stat'ing directory", error: e }) } match fs::create_dir(&ent_path).await { Ok(_) => { debug!("Created directory at {}", ent_path.display()); Ok(()) }, Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), Err(e) => { warn!("Could not create directory {} for JRE!", ent_path.display()); return Err(JavaRuntimeError::IO { what: "creating directory", error: e }); } } }).await } async fn ensure_jre_files(path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { let mut downloads = Vec::new(); for (name, file) in manifest.files.iter().filter(|(_, f)| f.is_file()) { let file_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?; let file_path = [path, file_path].into_iter().collect::(); downloads.push(LzmaDownloadJob::try_from((file, file_path)).map_err(|e| { match e { LzmaDownloadError::MissingURL => JavaRuntimeError::MalformedManifest("runtime manifest missing URL"), LzmaDownloadError::NotAFile => unreachable!("we just made sure this was a file") } })?); } let dl = MultiDownloader::new(downloads.iter_mut()); let client = Client::new(); dl.perform(&client).await .inspect_err(|e| warn!("jre file download failed: {e}")) .try_fold((), |_, _| async { Ok(()) }) .await .map_err(|_| JavaRuntimeError::MultiDownloadError) } async fn ensure_links(root_path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { stream::iter(manifest.files.iter().filter(|(_, f)| f.is_link())) .map::, _>(|(name, file)| Ok(async move { let JavaRuntimeFile::Link { target } = file else { unreachable!(); }; let target_exp = PathBuf::from(target); let path = util::check_path(name.as_str()).map_err(JavaRuntimeError::MalformedManifest)?; let link_path = [root_path, path].into_iter().collect::(); match fs::read_link(&link_path).await { Ok(target_path) => { if target_path == target_exp { debug!("Symbolic link at {} matches! Nothing to be done.", link_path.display()); return Ok(()) } debug!("Symbolic link at {} does not match (exp {}, got {}). Recreating it.", link_path.display(), target_exp.display(), target_path.display()); fs::remove_file(&link_path).await.map_err(|e| JavaRuntimeError::IO { what: "deleting bad symlink", error: e })?; } Err(e) if e.kind() == ErrorKind::NotFound => (), Err(e) => return Err(JavaRuntimeError::IO { what: "reading jre symlink", error: e }) } debug!("Creating symbolic link at {} to {}", link_path.display(), target_exp.display()); let symlink; #[cfg(unix)] { symlink = |targ, path| async { fs::symlink(targ, path).await }; } #[cfg(windows)] { symlink = |targ, path| async { fs::symlink_file(targ, path).await }; } #[cfg(not(any(unix, windows)))] { symlink = |_, _| async { Ok(()) }; } symlink(target_exp, link_path).await.map_err(|e| JavaRuntimeError::IO { what: "creating symlink", error: e })?; Ok(()) })) .try_buffer_unordered(32) .try_fold((), |_, _| async { Ok(()) }).await } pub async fn ensure_jre(&self, component: &str, manifest: JavaRuntimeManifest) -> Result { let runtime_path = self.get_component_dir(component); let runtime_path = runtime_path.join("runtime"); let manifest = Arc::new(manifest); fs::create_dir_all(&runtime_path).await .map_err(|e| JavaRuntimeError::IO { what: "creating runtime directory", error: e })?; debug!("Cleaning up JRE directory for {component}"); Self::clean_up_runtime(runtime_path.as_path(), manifest.clone()).await .map_err(|e| JavaRuntimeError::IO { what: "cleaning up runtime directory", error: e })?; debug!("Building directory structure for {component}"); self.ensure_jre_dirs(&runtime_path, manifest.as_ref()).await?; debug!("Downloading JRE files for {component}"); Self::ensure_jre_files(&runtime_path, manifest.as_ref()).await?; debug!("Ensuring symbolic links for {component}"); Self::ensure_links(&runtime_path, manifest.as_ref()).await?; Ok(runtime_path) } } #[derive(Debug)] pub enum JavaRuntimeError { EnsureFile(EnsureFileError), IO { what: &'static str, error: io::Error }, Download { what: &'static str, error: reqwest::Error }, Deserialize { what: &'static str, error: serde_json::Error }, UnsupportedArch(&'static str), UnsupportedComponent { arch: &'static str, component: String }, MalformedManifest(&'static str), Integrity(IntegrityError), MultiDownloadError } impl Display for JavaRuntimeError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { JavaRuntimeError::EnsureFile(e) => std::fmt::Display::fmt(e, f), JavaRuntimeError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), JavaRuntimeError::Download { what, error } => write!(f, "error downloading {}: {}", what, error), JavaRuntimeError::Deserialize { what, error } => write!(f, "error deserializing ({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(what) => write!(f, "malformed runtime manifest: {what} (launcher bug?)"), JavaRuntimeError::Integrity(e) => std::fmt::Display::fmt(e, f), JavaRuntimeError::MultiDownloadError => f.write_str("error in multi downloader (see logs for more details)") } } } impl Error for JavaRuntimeError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { JavaRuntimeError::EnsureFile(error) => Some(error), JavaRuntimeError::IO { error, .. } => Some(error), JavaRuntimeError::Download { error, .. } => Some(error), JavaRuntimeError::Deserialize { error, .. } => Some(error), JavaRuntimeError::Integrity(error) => Some(error), _ => None } } }