diff options
| author | 2025-01-22 19:03:31 -0600 | |
|---|---|---|
| committer | 2025-01-22 19:03:31 -0600 | |
| commit | b2edba152d0256a8921f3a25d67a062163a54f59 (patch) | |
| tree | 0bdc8b1ea4fedbb4d18c8347da76884c381b45b9 /src | |
| parent | more jre download stuff (diff) | |
finish downloaded jres
Diffstat (limited to 'src')
| -rw-r--r-- | src/launcher.rs | 4 | ||||
| -rw-r--r-- | src/launcher/download.rs | 23 | ||||
| -rw-r--r-- | src/launcher/jre.rs | 126 | ||||
| -rw-r--r-- | src/launcher/jre/download.rs | 211 | ||||
| -rw-r--r-- | src/launcher/jre/manifest.rs | 4 | ||||
| -rw-r--r-- | src/launcher/version.rs | 6 | ||||
| -rw-r--r-- | src/util.rs | 42 |
7 files changed, 358 insertions, 58 deletions
diff --git a/src/launcher.rs b/src/launcher.rs index d49556f..7971c3f 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -501,9 +501,7 @@ impl Launcher { }; let runtime = self.java_runtimes.choose_runtime(java_ver.component.as_str()).await.map_err(LaunchError::JavaRuntimeRepo)?; - dbg!(runtime); - - todo!("download it") + runtime_path = self.java_runtimes.ensure_jre(java_ver.component.as_str(), runtime).await.map_err(LaunchError::JavaRuntimeRepo)?; } let Some(runtime_exe_path) = runner::find_java(runtime_path.as_path(), profile.is_legacy_launch()).await diff --git a/src/launcher/download.rs b/src/launcher/download.rs index 4eedba5..846bed1 100644 --- a/src/launcher/download.rs +++ b/src/launcher/download.rs @@ -215,27 +215,8 @@ impl Download for VerifiedDownload { } async fn prepare(&mut self, req: RequestBuilder) -> Result<Option<RequestBuilder>, Box<dyn Error>> { - match util::verify_file(&self.path, self.expect_size, self.expect_sha1).await { - Ok(()) => { - debug!("Skipping download for file {}, integrity matches.", self.path.display()); - return Ok(None); - }, - Err(e) => match e { - FileVerifyError::Integrity(_, _) => { - warn!("Integrity error on library: {}", e); - - // try to delete the file since it's bad - let _ = fs::remove_file(&self.path).await - .map_err(|e| warn!("Error deleting corrupted/modified file {} (ignoring): {}", self.path.display(), e)); - }, - FileVerifyError::Open(_, e) => match e.kind() { - ErrorKind::NotFound => { - debug!("File {} is missing, downloading it.", self.path.display()); - }, - _ => return Err(e.into()) - }, - _ => return Err(e.into()) - } + if !util::should_download(&self.path, self.expect_size, self.expect_sha1).await? { + return Ok(None) } // potentially racy to close the file and reopen it... :/ diff --git a/src/launcher/jre.rs b/src/launcher/jre.rs index 4be4c7b..a09c8f3 100644 --- a/src/launcher/jre.rs +++ b/src/launcher/jre.rs @@ -1,20 +1,24 @@ use std::collections::HashSet; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; -use std::path::{Path, PathBuf}; +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}; @@ -99,13 +103,17 @@ impl JavaRuntimeRepository { fn clean_up_runtime_sync(path: &Path, manifest: Arc<JavaRuntimeManifest>) -> 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; + } - if !entry.path().strip_prefix(path) - .expect("walkdir escaped root (???)") - .to_str().map_or(None, |s| manifest.files.get(s)) - .is_none_or(|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())) { + if !rel_path.to_str().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()); @@ -136,7 +144,8 @@ impl JavaRuntimeRepository { async fn ensure_jre_dirs(&self, path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { stream::iter(manifest.files.iter().filter(|(_, f)| f.is_directory())) - .map::<Result<_, JavaRuntimeError>, _>(|(name, _)| Ok(async move { + .map::<Result<&String, JavaRuntimeError>, _>(|(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::<PathBuf>(); @@ -165,21 +174,92 @@ impl JavaRuntimeRepository { return Err(JavaRuntimeError::IO { what: "creating directory", error: e }); } } - })) - .try_buffer_unordered(32) - .try_fold((), |_, _| async { Ok(()) }).await + }).await } async fn ensure_jre_files(path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { - stream::iter(manifest.files.iter().filter(|(_, f)| f.is_file())) - .map(|(name, file)| Ok(async move { + 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::<PathBuf>(); + + 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::<Result<_, JavaRuntimeError>, _>(|(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::<PathBuf>(); + + 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 + })?; - todo!() + Ok(()) + })) + .try_buffer_unordered(32) + .try_fold((), |_, _| async { Ok(()) }).await } - async fn ensure_jre(&self, component: &str, manifest: JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { + pub async fn ensure_jre(&self, component: &str, manifest: JavaRuntimeManifest) -> Result<PathBuf, JavaRuntimeError> { let runtime_path = self.get_component_dir(component); let runtime_path = runtime_path.join("runtime"); let manifest = Arc::new(manifest); @@ -188,15 +268,19 @@ impl JavaRuntimeRepository { .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).await + 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?; - todo!() + Ok(runtime_path) } } @@ -209,7 +293,8 @@ pub enum JavaRuntimeError { UnsupportedArch(&'static str), UnsupportedComponent { arch: &'static str, component: String }, MalformedManifest(&'static str), - Integrity(IntegrityError) + Integrity(IntegrityError), + MultiDownloadError } impl Display for JavaRuntimeError { @@ -222,7 +307,8 @@ impl Display for JavaRuntimeError { 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::Integrity(e) => std::fmt::Display::fmt(e, f), + JavaRuntimeError::MultiDownloadError => f.write_str("error in multi downloader (see logs for more details)") } } } diff --git a/src/launcher/jre/download.rs b/src/launcher/jre/download.rs new file mode 100644 index 0000000..d8631aa --- /dev/null +++ b/src/launcher/jre/download.rs @@ -0,0 +1,211 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::fs::Permissions; +use std::io::Write; +use std::ops::AddAssign; +use std::path::{Path, PathBuf}; +use log::debug; +use lzma_rs::decompress; +use reqwest::{IntoUrl, RequestBuilder}; +use sha1_smol::{Digest, Sha1}; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio::fs::File; +use crate::launcher::download::Download; +use crate::launcher::jre::manifest::JavaRuntimeFile; +use crate::util; +use crate::util::IntegrityError; +use crate::version::DownloadInfo; + +pub enum LzmaDownloadError { + NotAFile, + MissingURL +} + +pub struct LzmaDownloadJob { + url: String, + path: PathBuf, + inflate: bool, + executable: bool, + + raw_size: Option<usize>, + raw_sha1: Option<Digest>, + + com_size: Option<usize>, + com_sha1: Option<Digest>, + + raw_sha1_st: Sha1, + raw_tally: usize, + + stream: Option<decompress::Stream<Vec<u8>>>, + out_file: Option<File> +} + +impl LzmaDownloadJob { + fn new_inflate(raw: &DownloadInfo, lzma: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> { + Ok(LzmaDownloadJob { + url: lzma.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, + path, + inflate: true, + executable: exe, + + raw_size: raw.size, + raw_sha1: raw.sha1, + + com_size: lzma.size, + com_sha1: lzma.sha1, + + raw_sha1_st: Sha1::new(), + raw_tally: 0, + + stream: Some(decompress::Stream::new(Vec::new())), + out_file: None + }) + } + + fn new_raw(raw: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> { + Ok(LzmaDownloadJob { + url: raw.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, + path, + inflate: false, + executable: exe, + + raw_size: raw.size, + raw_sha1: raw.sha1, + + com_size: None, + com_sha1: None, + + raw_sha1_st: Sha1::new(), + raw_tally: 0, + + stream: None, + out_file: None + }) + } +} + +impl TryFrom<(&JavaRuntimeFile, PathBuf)> for LzmaDownloadJob { + type Error = LzmaDownloadError; + + fn try_from((file, path): (&JavaRuntimeFile, PathBuf)) -> Result<Self, Self::Error> { + if !file.is_file() { + return Err(LzmaDownloadError::NotAFile); + } + + let JavaRuntimeFile::File { executable, downloads } = file else { + unreachable!("we just made sure this was a file"); + }; + + match downloads.lzma.as_ref() { + Some(lzma) => LzmaDownloadJob::new_inflate(&downloads.raw, lzma, *executable, path), + None => LzmaDownloadJob::new_raw(&downloads.raw, *executable, path) + } + } +} + +impl Debug for LzmaDownloadJob { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LzmaDownloadJob") + .field("url", &self.url) + .field("path", &self.path) + .field("inflate", &self.inflate) + .finish() + } +} + +impl Display for LzmaDownloadJob { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.inflate { + write!(f, "download and inflate {} to {}", &self.url, self.path.display()) + } else { + write!(f, "download {} to {}", &self.url, self.path.display()) + } + } +} + +impl Download for LzmaDownloadJob { + fn get_url(&self) -> impl IntoUrl { + self.url.as_str() + } + + async fn prepare(&mut self, req: RequestBuilder) -> Result<Option<RequestBuilder>, Box<dyn Error>> { + if !util::should_download(&self.path, self.raw_size, self.raw_sha1).await? { + return Ok(None) + } + + let mut options = File::options(); + + #[cfg(unix)] + { + options.mode(match self.executable { + true => 0o775, + _ => 0o664 + }); + } + + let file = options.create(true).write(true).truncate(true).open(&self.path).await?; + self.out_file = Some(file); + + Ok(Some(req)) + } + + async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>> { + let out_file = self.out_file.as_mut().expect("output file gone"); + + if let Some(ref mut stream) = self.stream { + stream.write_all(chunk)?; + let buf = stream.get_output_mut().expect("stream output missing before finish()"); + + out_file.write_all(buf.as_slice()).await?; + + self.raw_sha1_st.update(buf.as_slice()); + self.raw_tally += buf.len(); + + buf.truncate(0); + } else { + out_file.write_all(chunk).await?; + + self.raw_sha1_st.update(chunk); + self.raw_tally += chunk.len(); + } + + Ok(()) + } + + async fn finish(&mut self) -> Result<(), Box<dyn Error>> { + let mut out_file = self.out_file.take().expect("output file gone"); + + if let Some(stream) = self.stream.take() { + let buf = stream.finish()?; + + out_file.write_all(buf.as_slice()).await?; + + self.raw_sha1_st.update(buf.as_slice()); + self.raw_tally += buf.len(); + } + + let inf_digest = self.raw_sha1_st.digest(); + if let Some(sha1) = self.raw_sha1 { + if inf_digest != sha1 { + debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), sha1, inf_digest); + return Err(IntegrityError::Sha1Mismatch { + expect: sha1, + actual: inf_digest + }.into()); + } + } + + if let Some(size) = self.raw_size { + if self.raw_tally != size { + debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), size, self.raw_tally); + return Err(IntegrityError::SizeMismatch { + expect: size, + actual: self.raw_tally + }.into()); + } + } + + Ok(()) + } +} diff --git a/src/launcher/jre/manifest.rs b/src/launcher/jre/manifest.rs index 41780d0..887871a 100644 --- a/src/launcher/jre/manifest.rs +++ b/src/launcher/jre/manifest.rs @@ -27,8 +27,8 @@ pub type JavaRuntimesManifest = HashMap<String, HashMap<String, Vec<JavaRuntimeI #[derive(Debug, Deserialize)] pub struct FileDownloads { - lzma: Option<DownloadInfo>, - raw: DownloadInfo + pub lzma: Option<DownloadInfo>, + pub raw: DownloadInfo } #[derive(Debug, Deserialize)] diff --git a/src/launcher/version.rs b/src/launcher/version.rs index f857b93..328f0a9 100644 --- a/src/launcher/version.rs +++ b/src/launcher/version.rs @@ -281,7 +281,9 @@ impl VersionList { pub fn get_version_lazy(&self, id: &str) -> VersionResult { self.remote.as_ref() - .map_or_else(|| self.local.versions.get(id).into(), |r| r.versions.get(id).into()) + .map_or(None, |r| r.versions.get(id).map(VersionResult::from)) + .or_else(|| self.local.versions.get(id).map(VersionResult::from)) + .unwrap_or(VersionResult::None) } pub fn get_profile_version_id<'v>(&self, ver: &'v ProfileVersion) -> Option<Cow<'v, str>> { @@ -336,7 +338,7 @@ impl VersionList { let mut inherit = inherit.clone(); loop { - if seen.insert(inherit.clone()) { + if !seen.insert(inherit.clone()) { warn!("Version inheritance loop detected in {}: {} transitively inherits from itself.", ver.id, inherit); return Err(VersionResolveError::InheritanceLoop(inherit)); } diff --git a/src/util.rs b/src/util.rs index 8ad3632..5ea0f26 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,6 +7,7 @@ use sha1_smol::{Digest, Sha1}; use tokio::fs::File; use tokio::{fs, io}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use crate::util; #[derive(Debug)] pub enum IntegrityError { @@ -151,20 +152,41 @@ impl Error for EnsureFileError { } } +pub async fn should_download(path: impl AsRef<Path>, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> Result<bool, io::Error> { + let path = path.as_ref(); + + match verify_file(path, expect_size, expect_sha1).await { + Ok(()) => { + debug!("Skipping download for file {}, integrity matches.", path.display()); + Ok(false) + }, + Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => { + debug!("File {} is missing, downloading it.", path.display()); + Ok(true) + }, + Err(FileVerifyError::Integrity(_, e)) => { + warn!("Integrity error on file: {}", e); + + // try to delete the file since it's bad + let _ = fs::remove_file(path).await + .map_err(|e| warn!("Error deleting corrupted/modified file {} (ignoring): {}", path.display(), e)); + Ok(true) + } + Err(FileVerifyError::Open(_, e) | FileVerifyError::Read(_, e)) => { + warn!("Error verifying file {} on disk: {}", path.display(), e); + Err(e) + } + } +} + pub async fn ensure_file(path: impl AsRef<Path>, url: Option<&str>, expect_size: Option<usize>, expect_sha1: Option<Digest>, online: bool, force_download: bool) -> Result<bool, EnsureFileError> { let path = path.as_ref(); if !force_download { - 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 !should_download(path, expect_size, expect_sha1).await + .map_err(|e| EnsureFileError::IO { what: "verifying file on disk", error: e })? { + + return Ok(false); } } |
