From 1f5693c5531fa7ddf7bfdb8e27dc48d9765b97ca Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Sun, 22 Dec 2024 23:48:39 -0600 Subject: when the --- src/launcher/version.rs | 293 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 src/launcher/version.rs (limited to 'src/launcher') diff --git a/src/launcher/version.rs b/src/launcher/version.rs new file mode 100644 index 0000000..378e008 --- /dev/null +++ b/src/launcher/version.rs @@ -0,0 +1,293 @@ +use std::{collections::{BTreeMap, HashMap}, error::Error, io::ErrorKind}; +use std::borrow::Cow; +use std::collections::HashSet; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use chrono::{DateTime, Utc}; +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use serde::{Serialize, Deserialize}; + +use log::{debug, info, warn}; + +use crate::version::{*, manifest::*}; + +use super::constants::*; + +struct RemoteVersionList { + versions: HashMap, + latest: LatestVersions +} + +impl RemoteVersionList { + async fn new() -> Result> { + let text = reqwest::get(URL_VERSION_MANIFEST).await?.error_for_status()?.text().await?; + let manifest: VersionManifest = serde_json::from_str(text.as_str())?; + + let mut versions = HashMap::new(); + for v in manifest.versions { + versions.insert(v.id.clone(), v); + } + + Ok(RemoteVersionList { + versions, + latest: manifest.latest + }) + } + + async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result> { + // ensure parent directory exists + 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()); + return Err(e.into()); + } + }, + Ok(()) => {} + } + + // download it + let ver_text = reqwest::get(ver.url.as_str()).await?.error_for_status()?.text().await?; + + // make sure it's valid + ver.sha1.verify(ver_text.as_str()) + .map_err::, _>(|e| format!("downloaded version {} has wrong hash! (expect {}, got {})", ver.id.as_str(), &ver.sha1, e).as_str().into())?; + + // make sure it's well-formed + let cver: CompleteVersion = serde_json::from_str(ver.url.as_str())?; + + // write it out + tokio::fs::write(path, ver_text).await?; + + Ok(cver) + } +} + +struct LocalVersionList { + versions: BTreeMap +} + +#[derive(Debug)] +enum LocalVersionError { + Sha1Mismatch { exp: Sha1Digest, got: Sha1Digest }, + VersionMismatch { fname: String, json: String }, + Unknown(Box) +} + +impl Display for LocalVersionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Sha1Mismatch { exp, got } => { + write!(f, "sha1 mismatch (exp {exp}, got {got})") + }, + VersionMismatch { fname, json } => { + write!(f, "version ID mismatch (filename {fname}, json {json})") + }, + LocalVersionError::Unknown(err) => { + write!(f, "unknown version error: {err}") + } + } + } +} + +impl Error for LocalVersionError {} + +impl LocalVersionList { + async fn load_version(path: &Path, sha1: Option<&Sha1Digest>) -> Result { + // grumble grumble I don't like reading in the whole file at once + let ver = tokio::fs::read_to_string(path).await.map_err(|e| LocalVersionError::Unknown(Box::new(e)))?; + if let Some(digest_exp) = sha1 { + digest_exp.verify(ver.as_str()) + .map_err(|got| Sha1Mismatch { exp: digest_exp.to_owned(), got })?; + } + + let ver: CompleteVersion = serde_json::from_str(ver.as_str()).map_err(|e| LocalVersionError::Unknown(Box::new(e)))?; + + let fname_id = path.file_stem() + .expect("tried to load a local version with no path") // should be impossible + .to_str() + .expect("tried to load a local version with invalid UTF-8 filename"); // we already checked if the filename is valid UTF-8 at this point + + if fname_id == ver.id.as_str() { + Ok(ver) + } else { + Err(VersionMismatch { fname: fname_id.to_owned(), json: ver.id }) + } + } + + async fn load_versions(home: &Path, skip: impl Fn(&str) -> bool) -> Result> { + let mut rd = tokio::fs::read_dir(home).await?; + let mut versions = BTreeMap::new(); + + while let Some(ent) = rd.next_entry().await? { + if !ent.file_type().await?.is_dir() { continue; } + + // when the code is fugly + let path = match ent.file_name().to_str() { + Some(s) => { + if skip(s) { + debug!("Skipping local version {s} because (I assume) it is remotely tracked."); + continue + } + + /* FIXME: once https://github.com/rust-lang/rust/issues/127292 is closed, + * use add_extension to avoid extra heap allocations (they hurt my feelings) */ + let mut path = ent.path(); + + // can't use set_extension since s might contain a . (like 1.8.9) + path.push(format!("{s}.json")); + path + }, + + /* We just ignore directories with names that contain invalid unicode. Unfortunately, the laucher + * will not be supporting such custom versions. Name your version something sensible please. */ + None => { + warn!("Ignoring a local version {} because its id contains invalid unicode.", ent.file_name().to_string_lossy()); + continue + } + }; + + match Self::load_version(&path, None).await { + Ok(v) => { + versions.insert(v.id.clone(), v); + }, + Err(e) => { + // FIXME: just display the filename without to_string_lossy when https://github.com/rust-lang/rust/issues/120048 is closed + warn!("Ignoring local version {}: {e}", ent.file_name().to_string_lossy()); + } + } + } + + Ok(LocalVersionList { versions }) + } +} + +pub struct VersionList { + remote: Option, + local: LocalVersionList, + home: PathBuf +} + +pub enum VersionResult<'a> { + Complete(&'a CompleteVersion), + Remote(&'a VersionManifestVersion), + None +} + +impl<'a> From<&'a CompleteVersion> for VersionResult<'a> { + fn from(value: &'a CompleteVersion) -> Self { + Self::Complete(value) + } +} + +impl<'a> From<&'a VersionManifestVersion> for VersionResult<'a> { + fn from(value: &'a VersionManifestVersion) -> Self { + Self::Remote(value) + } +} + +impl<'a, T: Into>> From> for VersionResult<'a> { + fn from(value: Option) -> Self { + value.map_or(VersionResult::None, |v| v.into()) + } +} + +pub enum VersionResolveError { + InheritanceLoop, + MissingVersion(String), + Unknown(Box) +} + +use crate::util::{Sha1Digest, SHA1_DIGEST_BYTES}; +use crate::launcher::version::LocalVersionError::{Sha1Mismatch, VersionMismatch}; +use crate::launcher::version::VersionResolveError::MissingVersion; + +impl VersionList { + pub async fn online(home: &Path) -> Result> { + let remote = RemoteVersionList::new().await?; + let local = LocalVersionList::load_versions(home.as_ref(), |s| remote.versions.contains_key(s)).await?; + + Ok(VersionList { + remote: Some(remote), + local, + home: home.to_path_buf() + }) + } + + pub async fn offline(home: &Path) -> Result> { + let local = LocalVersionList::load_versions(home, |_| false).await?; + + Ok(VersionList { + remote: None, + local, + home: home.to_path_buf() + }) + } + + pub fn is_online(&self) -> bool { + self.remote.is_some() + } + + 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()) + } + + pub fn get_remote_version(&self, id: &str) -> Option<&VersionManifestVersion> { + let remote = self.remote.as_ref().expect("get_remote_version called in offline mode!"); + + remote.versions.get(id) + } + + pub async fn load_remote_version(&self, ver: &VersionManifestVersion) -> Result> { + let remote = self.remote.as_ref().expect("load_remote_version called in offline mode!"); + + let id = ver.id.as_str(); + let mut ver_path = self.home.join(id); + ver_path.push(format!("{id}.json")); + + match LocalVersionList::load_version(ver_path.as_path(), Some(&ver.sha1)).await { + Ok(v) => return Ok(v), + Err(e) => { + info!("redownloading {id}, since the local copy could not be loaded: {e}"); + } + } + + remote.download_version(ver, ver_path.as_path()).await + } + + pub async fn resolve_version<'v>(&self, ver: &'v CompleteVersion) -> Result, VersionResolveError> { + let mut seen: HashSet = HashSet::new(); + seen.insert(ver.id.clone()); + + let Some(inherit) = ver.inherits_from.as_ref() else { + return Ok(Cow::Borrowed(ver)); + }; + + let mut ver = ver.clone(); + let mut inherit = inherit.clone(); + + loop { + let inherited_ver = match self.get_version_lazy(inherit.as_str()) { + VersionResult::Complete(v) => Cow::Borrowed(v), + VersionResult::Remote(v) => + Cow::Owned(self.load_remote_version(v).await.map_err(|e| VersionResolveError::Unknown(e))?), + VersionResult::None => { + warn!("Cannot resolve version {}, it inherits an unknown version {inherit}", ver.id); + return Err(MissingVersion(inherit)); + } + }; + + ver.apply_child(inherited_ver.as_ref()); + + let Some(new_inherit) = inherited_ver.inherits_from.as_ref() else { + break + }; + + inherit.replace_range(.., new_inherit.as_str()); + } + + Ok(Cow::Owned(ver)) + } +} -- cgit v1.2.3-70-g09d2