summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-22 19:03:31 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-22 19:03:31 -0600
commitb2edba152d0256a8921f3a25d67a062163a54f59 (patch)
tree0bdc8b1ea4fedbb4d18c8347da76884c381b45b9
parentmore jre download stuff (diff)
finish downloaded jres
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--src/launcher.rs4
-rw-r--r--src/launcher/download.rs23
-rw-r--r--src/launcher/jre.rs126
-rw-r--r--src/launcher/jre/download.rs211
-rw-r--r--src/launcher/jre/manifest.rs4
-rw-r--r--src/launcher/version.rs6
-rw-r--r--src/util.rs42
9 files changed, 360 insertions, 58 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0600dab..b5dde06 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2482,6 +2482,7 @@ dependencies = [
"indexmap",
"lazy_static",
"log",
+ "lzma-rs",
"regex",
"reqwest",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index f3786a3..0b15061 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ futures = "0.3.31"
indexmap = { version = "2.7.1", features = ["serde"] }
lazy_static = "1.5.0"
log = "0.4.22"
+lzma-rs = { version = "0.3.0", features = ["stream"] }
regex = "1.11.1"
reqwest = { version = "0.12.12", features = ["json", "stream"] }
serde = { version = "1.0.216", features = ["derive"] }
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);
}
}