summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/launcher.rs53
-rw-r--r--src/launcher/download.rs116
-rw-r--r--src/launcher/version.rs4
-rw-r--r--src/util.rs165
4 files changed, 182 insertions, 156 deletions
diff --git a/src/launcher.rs b/src/launcher.rs
index 83ec342..1f28257 100644
--- a/src/launcher.rs
+++ b/src/launcher.rs
@@ -13,18 +13,20 @@ use std::fmt::{Display, Formatter};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use const_format::formatcp;
-use futures::StreamExt;
+use futures::{stream, StreamExt, TryStreamExt};
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use sysinfo::System;
use tokio::{fs, io};
use version::VersionList;
-use download::{MultiDownloader, PhaseDownloadError, VerifiedDownload};
+use download::{MultiDownloader, VerifiedDownload};
use rules::{CompatCheck, IncompatibleError};
use version::{VersionResolveError, VersionResult};
use crate::version::{Library, OSRestriction, OperatingSystem};
pub use profile::{Instance, Profile};
+use crate::util;
+use crate::util::FileVerifyError;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Settings {
@@ -102,7 +104,9 @@ pub enum LaunchError {
IncompatibleVersion(IncompatibleError),
// library errors
- LibraryDirError(PathBuf, io::Error)
+ LibraryDirError(PathBuf, io::Error),
+ LibraryVerifyError(FileVerifyError),
+ LibraryDownloadError
}
impl Display for LaunchError {
@@ -112,7 +116,9 @@ impl Display for LaunchError {
LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"),
LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}"),
LaunchError::IncompatibleVersion(e) => e.fmt(f),
- LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e)
+ LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e),
+ LaunchError::LibraryVerifyError(e) => write!(f, "failed to verify library: {}", e),
+ LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks
}
}
}
@@ -124,6 +130,7 @@ impl Error for LaunchError {
LaunchError::ResolveVersion(e) => Some(e),
LaunchError::IncompatibleVersion(e) => Some(e),
LaunchError::LibraryDirError(_, e) => Some(e),
+ LaunchError::LibraryVerifyError(e) => Some(e),
_ => None
}
}
@@ -175,9 +182,9 @@ impl Launcher {
* - java runtime
* - normal process (good research, past figboot :3)
* - libraries
- * - check which libraries we actually need (some have classifiers that don't apply to us)
- * - of the libraries we need, check which have correct size and sha1
- * - redownload necessary libraries
+ * - (done) check which libraries we actually need (some have classifiers that don't apply to us)
+ * - (done) of the libraries we need, check which have correct size and sha1
+ * - (done) redownload necessary libraries
* - (if offline mode and there are libraries to download, then explode violently)
* - extract natives
* - logging
@@ -222,14 +229,30 @@ impl Launcher {
}
}
- // TODO: offline
- info!("Downloading {} libraries...", downloads.len());
-
- let mut multi = MultiDownloader::new(downloads);
- let dl: Vec<_> = multi.perform().await.collect().await;
- info!("amogus: {dl:?}");
-
- // todo: offline mode
+ if self.online {
+ info!("Downloading {} libraries...", downloads.len());
+ let mut multi = MultiDownloader::new(downloads);
+ multi.perform().await
+ .inspect_err(|e| warn!("library download failed: {e}"))
+ .try_fold((), |_, _| async {Ok(())})
+ .await
+ .map_err(|_| LaunchError::LibraryDownloadError)?;
+ } else {
+ info!("Verifying {} libraries...", downloads.len());
+ stream::iter(downloads)
+ .map(|dl| Ok(async move {
+ debug!("Verifying library {}", dl.get_path().display());
+ util::verify_file(dl.get_path(), dl.get_expect_size(), dl.get_expect_sha1()).await
+ }))
+ .try_buffer_unordered(5)
+ .try_fold((), |_, _| async {Ok(())})
+ .await
+ .map_err(|e| {
+ warn!("A library could not be verified: {}", e);
+ warn!("Since the launcher is in offline mode, libraries cannot be downloaded. Please try again in online mode.");
+ LaunchError::LibraryVerifyError(e)
+ })?;
+ }
//todo!()
Ok(())
diff --git a/src/launcher/download.rs b/src/launcher/download.rs
index 813117c..7d9be73 100644
--- a/src/launcher/download.rs
+++ b/src/launcher/download.rs
@@ -2,14 +2,16 @@ use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
-use futures::{stream, Stream, StreamExt};
-use log::debug;
+use futures::{stream, StreamExt, TryStream};
+use log::{debug, warn};
use reqwest::{Client, IntoUrl, Method, RequestBuilder};
use sha1_smol::{Digest, Sha1};
use tokio::fs;
use tokio::fs::File;
-use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
+use tokio::io::{self, AsyncWriteExt};
use crate::launcher::constants::USER_AGENT;
+use crate::util;
+use crate::util::{FileVerifyError, IntegrityError};
pub trait Download: Debug + Display {
// return Ok(None) to skip downloading this file
@@ -99,7 +101,7 @@ impl<T: Download> MultiDownloader<T> {
}
}
- pub async fn perform(&mut self) -> impl Stream<Item = Result<(), PhaseDownloadError<T>>> {
+ pub async fn perform(&mut self) -> impl TryStream<Ok = (), Error = PhaseDownloadError<T>> {
stream::iter(self.jobs.iter_mut()).map(|job| {
let client = &self.client;
@@ -135,25 +137,6 @@ impl<T: Download> MultiDownloader<T> {
}
}
-#[derive(Debug)]
-pub enum IntegrityError {
- SizeMismatch{ expect: usize, actual: usize },
- Sha1Mismatch{ expect: Digest, actual: Digest }
-}
-
-impl Display for IntegrityError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- IntegrityError::SizeMismatch{ expect, actual } =>
- write!(f, "size mismatch (expect {expect} bytes, got {actual} bytes)"),
- IntegrityError::Sha1Mismatch {expect, actual} =>
- write!(f, "sha1 mismatch (expect {expect}, got {actual})")
- }
- }
-}
-
-impl Error for IntegrityError {}
-
pub struct VerifiedDownload {
url: String,
expect_size: Option<usize>,
@@ -177,7 +160,7 @@ impl Debug for VerifiedDownload {
impl Display for VerifiedDownload {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "downloading {} to {}", self.url, self.path.to_string_lossy())
+ write!(f, "downloading {} to {}", self.url, self.path.display())
}
}
@@ -206,10 +189,22 @@ impl VerifiedDownload {
self
}
+ pub fn get_url(&self) -> &str {
+ &self.url
+ }
+
pub fn get_path(&self) -> &Path {
&self.path
}
+ pub fn get_expect_size(&self) -> Option<usize> {
+ self.expect_size
+ }
+
+ pub fn get_expect_sha1(&self) -> Option<Digest> {
+ self.expect_sha1
+ }
+
pub async fn make_dirs(&self) -> Result<(), io::Error> {
fs::create_dir_all(self.path.parent().expect("download created with no containing directory (?)")).await
}
@@ -226,59 +221,32 @@ impl Download for VerifiedDownload {
}
async fn prepare(&mut self, req: RequestBuilder) -> Result<Option<RequestBuilder>, Box<dyn Error>> {
- let mut file = match File::open(&self.path).await {
- Ok(file) => file,
- Err(e) => return if e.kind() == ErrorKind::NotFound {
- // assume the parent folder exists (responsibility of the caller to ensure this)
- debug!("File {} does not exist, downloading it.", self.path.to_string_lossy());
- self.open_output().await?;
- Ok(Some(req))
- } else {
- debug!("Error opening {}: {}", self.path.to_string_lossy(), e);
- Err(e.into())
+ 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())
}
- };
-
- // short-circuit this
- if self.expect_size.is_none() && self.expect_sha1.is_none() {
- debug!("No size or sha1 for {}, have to assume it's good.", self.path.to_string_lossy());
- return Ok(None);
}
- let mut tally = 0usize;
- let mut sha1 = Sha1::new();
-
- let mut buf = [0u8; 4096];
- loop {
- let n = match file.read(&mut buf).await {
- Ok(n) => n,
- Err(e) => match e.kind() {
- ErrorKind::Interrupted => continue,
- _ => {
- debug!("Error reading {}: {}", self.path.to_string_lossy(), e);
- return Err(e.into());
- }
- }
- };
-
- if n == 0 { break; }
-
- tally += n;
- sha1.update(&buf[..n]);
- }
-
- if self.expect_sha1.is_none_or(|d| d == sha1.digest())
- && self.expect_size.is_none_or(|s| s == tally) {
- debug!("Not downloading {}, sha1 and size match.", self.path.to_string_lossy());
- return Ok(None);
- }
-
- drop(file);
-
// potentially racy to close the file and reopen it... :/
self.open_output().await?;
- debug!("Downloading {} because sha1 or size does not match.", self.path.to_string_lossy());
Ok(Some(req))
}
@@ -295,17 +263,17 @@ impl Download for VerifiedDownload {
if let Some(d) = self.expect_sha1 {
if d != digest {
- debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.to_string_lossy(), d, digest);
+ debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), d, digest);
return Err(IntegrityError::Sha1Mismatch { expect: d, actual: digest }.into());
}
} else if let Some(s) = self.expect_size {
if s != self.tally {
- debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.to_string_lossy(), s, self.tally);
+ debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), s, self.tally);
return Err(IntegrityError::SizeMismatch { expect: s, actual: self.tally }.into());
}
}
- debug!("Successfully downloaded {} ({} bytes).", self.path.to_string_lossy(), self.tally);
+ debug!("Successfully downloaded {} ({} bytes).", self.path.display(), self.tally);
// release the file descriptor (don't want to wait until it's dropped automatically because idk when that would be)
drop(self.file.take().unwrap());
diff --git a/src/launcher/version.rs b/src/launcher/version.rs
index 40bb953..2e320f2 100644
--- a/src/launcher/version.rs
+++ b/src/launcher/version.rs
@@ -4,7 +4,7 @@ use std::collections::HashSet;
use std::fmt::Display;
use std::path::{Path, PathBuf};
-use log::{debug, info, trace, warn};
+use log::{debug, info, warn};
use sha1_smol::Digest;
use tokio::{fs, io};
use crate::util;
@@ -42,7 +42,7 @@ impl RemoteVersionList {
match tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await {
Err(e) => {
if e.kind() != ErrorKind::AlreadyExists {
- warn!("failed to create {} parent dirs: {e}", path.to_string_lossy());
+ warn!("failed to create {} parent dirs: {e}", path.display());
return Err(e.into());
}
},
diff --git a/src/util.rs b/src/util.rs
index c6739b6..fe11c38 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,35 +1,30 @@
-// use std::fmt::{Debug, Display, Formatter};
-// use serde::{Deserialize, Deserializer};
-// use serde::de::{Error, Visitor};
-// use hex::{FromHex, FromHexError, ToHex};
-// use sha1_smol::Sha1;
-//
-// // sha1 digests are 20 bytes long
-// pub use sha1_smol::DIGEST_LENGTH;
-// pub type Sha1DigestBytes = [u8; DIGEST_LENGTH];
-//
-// #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
-// pub struct Sha1Digest(pub Sha1DigestBytes);
-//
-// impl Debug for Sha1Digest {
-// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-// write!(f, "Sha1Digest {{{}}}", self.0.encode_hex::<String>())
-// }
-// }
-//
-// impl Display for Sha1Digest {
-// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-// f.write_str(&self.0.encode_hex::<String>())
-// }
-// }
-//
-// impl Sha1Digest {
-// pub fn from_hex(s: &str) -> Result<Sha1Digest, FromHexError> {
-//
-// }
-//
-
+use std::error::Error;
+use std::fmt::{Display, Formatter};
+use std::io::ErrorKind;
+use std::path::{Path, PathBuf};
+use log::debug;
use sha1_smol::{Digest, Sha1};
+use tokio::fs::File;
+use tokio::io::AsyncReadExt;
+
+#[derive(Debug)]
+pub enum IntegrityError {
+ SizeMismatch{ expect: usize, actual: usize },
+ Sha1Mismatch{ expect: Digest, actual: Digest }
+}
+
+impl Display for IntegrityError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ IntegrityError::SizeMismatch{ expect, actual } =>
+ write!(f, "size mismatch (expect {expect} bytes, got {actual} bytes)"),
+ IntegrityError::Sha1Mismatch {expect, actual} =>
+ write!(f, "sha1 mismatch (expect {expect}, got {actual})")
+ }
+ }
+}
+
+impl Error for IntegrityError {}
pub fn verify_sha1(expect: Digest, s: &str) -> Result<(), Digest> {
let dig = Sha1::from(s).digest();
@@ -41,37 +36,77 @@ pub fn verify_sha1(expect: Digest, s: &str) -> Result<(), Digest> {
Err(dig)
}
-//
-// pub fn as_hex(&self) -> String {
-// // allocate the string with capacity first so we only do one heap alloc
-// let mut s: String = String::with_capacity(2 * self.0.len());
-// self.0.iter().for_each(|b| s.push_str(&format!("{:02x}", b)));
-// s
-// }
-// }
-//
-// struct Sha1DigestVisitor;
-//
-// impl <'a> Visitor<'a> for Sha1DigestVisitor {
-// type Value = Sha1Digest;
-//
-// fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
-// write!(formatter, "a valid SHA-1 digest (40-character hex string)")
-// }
-//
-// fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
-// where
-// E: Error,
-// {
-// Sha1DigestBytes::from_hex(v).map_err(|e| E::custom(e)).map(Sha1Digest)
-// }
-// }
-//
-// impl<'a> Deserialize<'a> for Sha1Digest {
-// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-// where
-// D: Deserializer<'a>,
-// {
-// deserializer.deserialize_any(Sha1DigestVisitor)
-// }
-// } \ No newline at end of file
+#[derive(Debug)]
+pub enum FileVerifyError {
+ Integrity(PathBuf, IntegrityError),
+ Open(PathBuf, tokio::io::Error),
+ Read(PathBuf, tokio::io::Error),
+}
+
+impl Display for FileVerifyError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ FileVerifyError::Integrity(path, e) => write!(f, "file integrity error {}: {}", path.display(), e),
+ FileVerifyError::Open(path, e) => write!(f, "error opening file {}: {}", path.display(), e),
+ FileVerifyError::Read(path, e) => write!(f, "error reading file {}: {}", path.display(), e)
+ }
+ }
+}
+
+impl Error for FileVerifyError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ FileVerifyError::Integrity(_, e) => Some(e),
+ FileVerifyError::Open(_, e) => Some(e),
+ FileVerifyError::Read(_, e) => Some(e)
+ }
+ }
+}
+
+pub async fn verify_file(path: impl AsRef<Path>, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> Result<(), FileVerifyError> {
+ let path = path.as_ref();
+
+ if expect_size.is_none() && expect_sha1.is_none() {
+ debug!("No size or sha1 for {}, have to assume it's good.", path.display());
+ return Ok(());
+ }
+
+ let mut file = File::open(path).await.map_err(|e| FileVerifyError::Open(path.to_owned(), e))?;
+
+ let mut tally = 0usize;
+ let mut st = Sha1::new();
+ let mut buf = [0u8; 4096];
+
+ loop {
+ let n = match file.read(&mut buf).await {
+ Ok(n) => n,
+ Err(e) => match e.kind() {
+ ErrorKind::Interrupted => continue,
+ _ => return Err(FileVerifyError::Read(path.to_owned(), e))
+ }
+ };
+
+ if n == 0 {
+ break;
+ }
+
+ st.update(&buf[..n]);
+ tally += n;
+ }
+
+ let dig = st.digest();
+
+ if expect_size.is_some_and(|sz| sz != tally) {
+ return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::SizeMismatch {
+ expect: expect_size.unwrap(),
+ actual: tally
+ }));
+ } else if expect_sha1.is_some_and(|exp_dig| exp_dig != dig) {
+ return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::Sha1Mismatch {
+ expect: expect_sha1.unwrap(),
+ actual: dig
+ }));
+ }
+
+ Ok(())
+}