From 7a42235af574a3a8f07db8ccc3189ce735213ce8 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Tue, 14 Jan 2025 02:51:02 -0600 Subject: verify libraries when offline --- src/launcher.rs | 53 ++++++++++----- src/launcher/download.rs | 116 ++++++++++++--------------------- src/launcher/version.rs | 4 +- src/util.rs | 165 ++++++++++++++++++++++++++++------------------- 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 MultiDownloader { } } - pub async fn perform(&mut self) -> impl Stream>> { + pub async fn perform(&mut self) -> impl TryStream> { stream::iter(self.jobs.iter_mut()).map(|job| { let client = &self.client; @@ -135,25 +137,6 @@ impl MultiDownloader { } } -#[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, @@ -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 { + self.expect_size + } + + pub fn get_expect_sha1(&self) -> Option { + 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, Box> { - 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::()) -// } -// } -// -// impl Display for Sha1Digest { -// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { -// f.write_str(&self.0.encode_hex::()) -// } -// } -// -// impl Sha1Digest { -// pub fn from_hex(s: &str) -> Result { -// -// } -// - +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(self, v: &str) -> Result -// where -// E: Error, -// { -// Sha1DigestBytes::from_hex(v).map_err(|e| E::custom(e)).map(Sha1Digest) -// } -// } -// -// impl<'a> Deserialize<'a> for Sha1Digest { -// fn deserialize(deserializer: D) -> Result -// 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, expect_size: Option, expect_sha1: Option) -> 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(()) +} -- cgit v1.2.3-70-g09d2