From e5d13bf03a3b7e8444ae367689852fcd6633e221 Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Wed, 15 Jan 2025 22:36:17 -0600 Subject: assets done --- src/launcher.rs | 54 +++++++++------- src/launcher/assets.rs | 162 ++++++++++++++++++++++++++++++++++++++++++----- src/launcher/download.rs | 13 +++- 3 files changed, 191 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/launcher.rs b/src/launcher.rs index 184bcae..ccb84cb 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -10,6 +10,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; +use std::ffi::OsStr; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; @@ -28,6 +29,7 @@ use version::{VersionList, VersionResolveError, VersionResult}; use crate::version::{Logging, Library, OSRestriction, OperatingSystem}; pub use profile::{Instance, Profile}; +use crate::launcher::assets::{AssetError, AssetRepository}; use crate::util; use crate::util::{FileVerifyError, IntegrityError}; @@ -135,7 +137,8 @@ pub struct Launcher { system_info: SystemInfo, - libraries: LibraryRepository + libraries: LibraryRepository, + assets: AssetRepository } #[derive(Debug)] @@ -152,7 +155,10 @@ pub enum LaunchError { LibraryDownloadError, // log errors - LogConfig(LogConfigError) + LogConfig(LogConfigError), + + // asset errors + Assets(AssetError) } impl Display for LaunchError { @@ -165,7 +171,8 @@ impl Display for LaunchError { 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 - LaunchError::LogConfig(e) => write!(f, "failed to configure logger: {}", e) + LaunchError::LogConfig(e) => write!(f, "failed to configure logger: {}", e), + LaunchError::Assets(e) => write!(f, "failed to fetch assets: {}", e) } } } @@ -179,6 +186,7 @@ impl Error for LaunchError { LaunchError::LibraryDirError(_, e) => Some(e), LaunchError::LibraryVerifyError(e) => Some(e), LaunchError::LogConfig(e) => Some(e), + LaunchError::Assets(e) => Some(e), _ => None } } @@ -212,6 +220,8 @@ impl Launcher { let settings_path = home.join("ozone.json"); let settings = Settings::load(&settings_path).await?; + let assets_path = home.join("assets"); + Ok(Launcher { online, home: home.to_owned(), @@ -221,7 +231,8 @@ impl Launcher { system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries"), - } + }, + assets: AssetRepository::new(online, &assets_path).await? }) } @@ -350,16 +361,16 @@ impl Launcher { * - (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) + * - (done) (if offline mode and there are libraries to download, then explode violently) * - extract natives * - (done) logging * - (done) download the config if present and necessary * - (done) (explode if offline mode and we need to download stuff) * - assets - * - get asset index (check if our local copy is good and redownload if not) - * - check what ones are good and what needs to be downloaded - * - download them - * - (if offline mode, explode) + * - (done) get asset index (check if our local copy is good and redownload if not) + * - (done) check what ones are good and what needs to be downloaded + * - (done) download them + * - (done) (if offline mode, explode) * - if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) * - the actual client jar * - check integriddy and download if needed @@ -408,19 +419,11 @@ impl Launcher { .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(8) - .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) - })?; + download::verify_files(downloads).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) + })?; } let log_arg; @@ -433,6 +436,13 @@ impl Launcher { dbg!(log_arg); + if let Some(idx_download) = ver.asset_index.as_ref() { + let asset_idx = self.assets.load_index(idx_download, ver.assets.as_ref().map(|s| s.as_ref())).await + .map_err(|e| LaunchError::Assets(e))?; + + self.assets.ensure_assets(&asset_idx).await.map_err(|e| LaunchError::Assets(e))?; + } + //todo!() Ok(()) } diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs index 020885f..7d883d8 100644 --- a/src/launcher/assets.rs +++ b/src/launcher/assets.rs @@ -1,13 +1,17 @@ use std::error::Error; -use std::fmt::{Display, Formatter, Write}; +use std::ffi::OsStr; +use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::path::Component::Normal; +use futures::{stream, TryFutureExt, TryStreamExt}; use log::{debug, info, warn}; +use sha1_smol::Sha1; use tokio::{fs, io}; -use crate::assets::AssetIndex; +use crate::assets::{Asset, AssetIndex}; +use crate::launcher::download::{MultiDownloader, VerifiedDownload}; use crate::util; -use crate::util::FileVerifyError; +use crate::util::{FileVerifyError, IntegrityError}; use crate::version::DownloadInfo; const INDEX_PATH: &'static str = "indexes"; @@ -21,7 +25,14 @@ pub struct AssetRepository { #[derive(Debug)] pub enum AssetError { InvalidId(Option), - IO { what: &'static str, error: io::Error } + IO { what: &'static str, error: io::Error }, + IndexParse(serde_json::Error), + Offline, + MissingURL, + DownloadIndex(reqwest::Error), + Integrity(IntegrityError), + AssetObjectDownload, + AssetVerifyError(FileVerifyError) } impl Display for AssetError { @@ -29,7 +40,14 @@ impl Display for AssetError { match self { AssetError::InvalidId(None) => f.write_str("missing asset index id"), AssetError::InvalidId(Some(id)) => write!(f, "invalid asset index id: {}", id), - AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error) + AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), + AssetError::IndexParse(error) => write!(f, "error parsing asset index: {}", error), + AssetError::Offline => f.write_str("cannot download asset index while offline"), + AssetError::MissingURL => f.write_str("missing asset index URL"), + AssetError::DownloadIndex(e) => write!(f, "error downloading asset index: {}", e), + AssetError::Integrity(e) => write!(f, "asset index integrity error: {}", e), + AssetError::AssetObjectDownload => f.write_str("asset object download failed"), + AssetError::AssetVerifyError(e) => write!(f, "error verifying asset object: {e}") } } } @@ -38,6 +56,10 @@ impl Error for AssetError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { AssetError::IO { error, .. } => Some(error), + AssetError::IndexParse(error) => Some(error), + AssetError::DownloadIndex(error) => Some(error), + AssetError::Integrity(error) => Some(error), + AssetError::AssetVerifyError(error) => Some(error), _ => None } } @@ -68,15 +90,17 @@ impl AssetRepository { } fn get_index_path(&self, id: &str) -> Result { - let indexes_path: &Path = (&self.home, INDEX_PATH).as_ref(); + let mut indexes_path: PathBuf = [self.home.as_ref(), OsStr::new(INDEX_PATH)].iter().collect(); let Some(Normal(path)) = Path::new(id).components().last() else { - return Err(AssetError::InvalidId(id.into())); + return Err(AssetError::InvalidId(Some(id.into()))); }; - let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy())))?; + let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?; // FIXME: change this once "add_extension" is stabilized - Ok((indexes_path, format!("{}.json", path)).into()) + indexes_path.push(format!("{}.json", path)); + + Ok(indexes_path) } pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result { @@ -87,28 +111,136 @@ impl AssetRepository { info!("Loading asset index {}", id); let path = self.get_index_path(id)?; - debug!("Asset index {} is located at {}", id, path); + debug!("Asset index {} is located at {}", id, path.display()); match util::verify_file(&path, index.size, index.sha1).await { - Ok(_) => todo!(), // load local index + Ok(_) => { + debug!("Asset index {} verified on disk. Loading it.", id); + let idx_data = fs::read_to_string(&path).await.map_err(|e| AssetError::IO { + what: "reading asset index", + error: e + })?; + + return Ok(serde_json::from_str(&idx_data).map_err(|e| AssetError::IndexParse(e))?); + }, Err(FileVerifyError::Open(_, e)) => match e.kind() { - ErrorKind::NotFound => (), // download + ErrorKind::NotFound => { + debug!("Asset index {} not found on disk. Must download it.", id); + }, _ => return Err(("opening asset index", e).into()) }, Err(FileVerifyError::Integrity(_, e)) => { info!("Asset index {} has mismatched integrity: {}, must download it.", id, e); let _ = fs::remove_file(&path).await.map_err(|e| warn!("Error deleting modified index {}: {} (ignored)", id, e)); - // download }, Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into()) } if !self.online { - warn!("Must redownload asset index {}, but the launcher is in offline mode. Please try again in online mode.", id); - return todo!(); + warn!("Must download asset index {}, but the launcher is in offline mode. Please try again in online mode.", id); + return Err(AssetError::Offline); + } + + let Some(url) = index.url.as_ref() else { + return Err(AssetError::MissingURL); + }; + + debug!("Downloading asset index {} from {}", id, url); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await.map_err(|e| AssetError::IO { + what: "creating asset index folder", + error: e + })?; + } + + let idx_text = reqwest::get(url).await + .map_err(|e| AssetError::DownloadIndex(e))? + .text().await + .map_err(|e| AssetError::DownloadIndex(e))?; + + if index.size.is_some_and(|s| s != idx_text.len()) { + return Err(AssetError::Integrity(IntegrityError::SizeMismatch { + expect: index.size.unwrap(), + actual: idx_text.len() + })); + } + + if let Some(expect) = index.sha1 { + let actual = Sha1::from(&idx_text).digest(); + return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual })); } + debug!("Saving downloaded asset index to {}", path.display()); + fs::write(&path, &idx_text).await.map_err(|e| AssetError::IO { + what: "writing asset index", + error: e + })?; + + Ok(serde_json::from_str(&idx_text).map_err(|e| AssetError::IndexParse(e))?) + } + + fn get_object_url(obj: &Asset) -> String { + format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash) + } + + async fn download_assets(&self, index: &AssetIndex) -> Result<(), AssetError> { todo!() } + + async fn ensure_dir(path: impl AsRef) -> Result<(), io::Error> { + match fs::create_dir(path).await { + Ok(_) => Ok(()), + Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e) + } + } + + pub async fn ensure_assets(&self, index: &AssetIndex) -> Result<(), AssetError> { + let mut downloads = Vec::new(); + let objects_path = [self.home.as_ref(), OsStr::new("objects")].iter().collect::(); + + Self::ensure_dir(&objects_path).await.map_err(|e| AssetError::IO { + what: "creating objects directory", + error: e + })?; + + for object in index.objects.iter() { + let hex_digest = object.hash.to_string(); + let path = [objects_path.as_ref(), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect::(); + + Self::ensure_dir(path.parent().unwrap()).await.map_err(|error| AssetError::IO { error, what: "creating directory for object" })?; + + downloads.push(VerifiedDownload::new(&Self::get_object_url(object), &path, Some(object.size), Some(object.hash))); + } + + if self.online { + info!("Downloading {} asset objects...", downloads.len()); + let mut multi = MultiDownloader::new(downloads); + multi.perform().await + .inspect_err(|e| warn!("asset download failed: {e}")) + .try_fold((), |_, _| async {Ok(())}) + .await + .map_err(|_| AssetError::AssetObjectDownload)?; + } else { + info!("Verifying {} asset objects...", downloads.len()); + super::download::verify_files(downloads).await.map_err(|e| AssetError::AssetVerifyError(e))?; + } + + Ok(()) + } } +mod tests { + use super::*; + + #[test] + fn test_it() { + let digest_str = "ad1115931887a73cd596300f2c93f84adf39521d"; + assert_eq!(AssetRepository::get_object_url(&Asset { + name: String::from("test"), + hash: digest_str.parse().unwrap(), + size: 0usize + }), "https://resources.download.minecraft.net/ad/ad1115931887a73cd596300f2c93f84adf39521d"); + } +} diff --git a/src/launcher/download.rs b/src/launcher/download.rs index 7d9be73..d8d05f7 100644 --- a/src/launcher/download.rs +++ b/src/launcher/download.rs @@ -2,7 +2,7 @@ use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; -use futures::{stream, StreamExt, TryStream}; +use futures::{stream, StreamExt, TryStream, TryStreamExt}; use log::{debug, warn}; use reqwest::{Client, IntoUrl, Method, RequestBuilder}; use sha1_smol::{Digest, Sha1}; @@ -281,3 +281,14 @@ impl Download for VerifiedDownload { Ok(()) } } + +pub async fn verify_files(files: Vec) -> Result<(), FileVerifyError> { + stream::iter(files) + .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(8) + .try_fold((), |_, _| async {Ok(())}) + .await +} -- cgit v1.2.3-70-g09d2