diff options
| author | 2025-02-01 23:06:37 -0600 | |
|---|---|---|
| committer | 2025-02-01 23:06:37 -0600 | |
| commit | c19a1077e85334a3e5ba885a60b03d76409a2b2e (patch) | |
| tree | 5e726e8180770ac9c2f6c415a0437d6d2c29c226 /src/launcher/version.rs | |
| parent | random changes (diff) | |
restructure project
Diffstat (limited to 'src/launcher/version.rs')
| -rw-r--r-- | src/launcher/version.rs | 398 |
1 files changed, 0 insertions, 398 deletions
diff --git a/src/launcher/version.rs b/src/launcher/version.rs deleted file mode 100644 index 0f55223..0000000 --- a/src/launcher/version.rs +++ /dev/null @@ -1,398 +0,0 @@ -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 log::{debug, info, warn}; -use sha1_smol::Digest; -use tokio::{fs, io}; -use crate::launcher::settings::ProfileVersion; -use crate::util; -use crate::version::{*, manifest::*}; - -use super::constants::*; - -#[derive(Debug)] -pub enum VersionError { - IO { what: String, error: io::Error }, - Request { what: String, error: reqwest::Error }, - MalformedObject { what: String, error: serde_json::Error }, - VersionIntegrity { id: String, expect: Digest, got: Digest } -} - -impl Display for VersionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VersionError::IO { what, error } => write!(f, "i/o error ({what}): {error}"), - VersionError::Request { what, error } => write!(f, "request error ({what}): {error}"), - VersionError::MalformedObject { what, error } => write!(f, "malformed {what}: {error}"), - VersionError::VersionIntegrity { id, expect, got } => write!(f, "version {id} integrity mismatch (expect {expect}, got {got})") - } - } -} - -impl Error for VersionError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - VersionError::IO { error, .. } => Some(error), - VersionError::Request { error, .. } => Some(error), - VersionError::MalformedObject { error, .. } => Some(error), - _ => None - } - } -} - -struct RemoteVersionList { - versions: HashMap<String, VersionManifestVersion>, - latest: LatestVersions -} - -impl RemoteVersionList { - async fn new() -> Result<RemoteVersionList, VersionError> { - debug!("Looking up remote version manifest."); - let text = reqwest::get(URL_VERSION_MANIFEST).await - .and_then(|r| r.error_for_status()) - .map_err(|e| VersionError::Request { what: "download version manifest".into(), error: e })? - .text().await.map_err(|e| VersionError::Request { what: "download version manifest (decode)".into(), error: e })?; - - debug!("Parsing version manifest."); - let manifest: VersionManifest = serde_json::from_str(text.as_str()).map_err(|e| VersionError::MalformedObject { what: "version manifest".into(), error: e })?; - - let mut versions = HashMap::new(); - for v in manifest.versions { - versions.insert(v.id.clone(), v); - } - - debug!("Done loading remote versions!"); - Ok(RemoteVersionList { - versions, - latest: manifest.latest - }) - } - - async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result<CompleteVersion, VersionError> { - // ensure parent directory exists - info!("Downloading version {}.", ver.id); - tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await - .inspect_err(|e| warn!("failed to create {} parent dirs: {e}", path.display())) - .map_err(|e| VersionError::IO { what: format!("creating version directory for {}", path.display()), error: e })?; - - // download it - let ver_text = reqwest::get(ver.url.as_str()).await - .and_then(|r| r.error_for_status()) - .map_err(|e| VersionError::Request { what: format!("download version {} from {}", ver.id, ver.url), error: e })? - .text().await.map_err(|e| VersionError::Request { what: format!("download version {} from {} (receive)", ver.id, ver.url), error: e })?; - - debug!("Validating downloaded {}...", ver.id); - // make sure it's valid - util::verify_sha1(ver.sha1, ver_text.as_str()) - .map_err(|e| VersionError::VersionIntegrity { - id: ver.id.clone(), - expect: ver.sha1, - got: e - })?; - - // make sure it's well-formed - let cver: CompleteVersion = serde_json::from_str(ver_text.as_str()).map_err(|e| VersionError::MalformedObject { what: format!("complete version {}", ver.id), error: e })?; - - debug!("Saving version {}...", ver.id); - - // write it out - tokio::fs::write(path, ver_text).await - .inspect_err(|e| warn!("Failed to save version {}: {}", ver.id, e)) - .map_err(|e| VersionError::IO { what: format!("writing version file at {}", path.display()), error: e })?; - - info!("Done downloading and verifying {}!", ver.id); - - Ok(cver) - } -} - -struct LocalVersionList { - versions: BTreeMap<String, CompleteVersion> -} - -#[derive(Debug)] -enum LocalVersionError { - Sha1Mismatch { exp: Digest, got: Digest }, - VersionMismatch { fname: String, json: String }, - Unknown(Box<dyn Error>) -} - -impl Display for LocalVersionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LocalVersionError::Sha1Mismatch { exp, got } => { - write!(f, "sha1 mismatch (exp {exp}, got {got})") - }, - LocalVersionError::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<Digest>) -> Result<CompleteVersion, LocalVersionError> { - // grumble grumble I don't like reading in the whole file at once - info!("Loading local version at {}.", path.display()); - let ver = tokio::fs::read_to_string(path).await.map_err(|e| LocalVersionError::Unknown(Box::new(e)))?; - if let Some(digest_exp) = sha1 { - debug!("Verifying local version {}.", path.display()); - util::verify_sha1(digest_exp, ver.as_str()) - .map_err(|got| { - warn!("Local version sha1 mismatch: {} (exp: {}, got: {})", path.display(), digest_exp, got); - LocalVersionError::Sha1Mismatch { exp: digest_exp.to_owned(), got } - })?; - } - - let ver: CompleteVersion = serde_json::from_str(ver.as_str()).map_err(|e| { - warn!("Invalid version JSON {}: {}", path.display(), 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() { - info!("Loaded local version {}.", ver.id); - Ok(ver) - } else { - warn!("Local version {} has a version ID conflict (filename: {}, json: {})!", path.display(), fname_id, ver.id); - Err(LocalVersionError::VersionMismatch { fname: fname_id.to_owned(), json: ver.id }) - } - } - - async fn load_versions(home: &Path, skip: impl Fn(&str) -> bool) -> Result<LocalVersionList, VersionError> { - info!("Loading local versions."); - let mut rd = tokio::fs::read_dir(home).await.map_err(|e| VersionError::IO { what: format!("open local versions directory {}", home.display()), error: e })?; - let mut versions = BTreeMap::new(); - - while let Some(ent) = rd.next_entry().await.map_err(|e| VersionError::IO { what: format!("read local versions directory {}", home.display()), error: e })? { - if !ent.file_type().await.map_err(|e| VersionError::IO { what: format!("version entry metadata {}", ent.path().display()), error: e} )?.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()); - } - } - } - - info!("Loaded {} local version(s).", versions.len()); - Ok(LocalVersionList { versions }) - } -} - -pub struct VersionList { - remote: Option<RemoteVersionList>, - 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<VersionResult<'a>>> From<Option<T>> for VersionResult<'a> { - fn from(value: Option<T>) -> Self { - value.map_or(VersionResult::None, |v| v.into()) - } -} - -#[derive(Debug)] -pub enum VersionResolveError { - InheritanceLoop(String), - MissingVersion(String), - VersionLoad(VersionError) -} - -impl Display for VersionResolveError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VersionResolveError::InheritanceLoop(s) => write!(f, "inheritance loop (saw {s} twice)"), - VersionResolveError::MissingVersion(s) => write!(f, "unknown version {s}"), - VersionResolveError::VersionLoad(err) => write!(f, "version load error: {err}") - } - } -} - -impl Error for VersionResolveError {} - -impl VersionList { - async fn create_dir_for(home: &Path) -> Result<(), io::Error> { - debug!("Creating versions directory."); - match fs::create_dir(home).await { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), - Err(e) => { - debug!("failed to create version home: {}", e); - Err(e) - } - } - } - - pub async fn online(home: &Path) -> Result<VersionList, VersionError> { - Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?; - - let remote = RemoteVersionList::new().await?; - let local = LocalVersionList::load_versions(home, |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<VersionList, VersionError> { - Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?; - - 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().and_then(|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>> { - match ver { - ProfileVersion::LatestRelease => self.remote.as_ref().map(|r| Cow::Owned(r.latest.release.clone())), - ProfileVersion::LatestSnapshot => self.remote.as_ref().map(|r| Cow::Owned(r.latest.snapshot.clone())), - ProfileVersion::Specific(ver) => Some(Cow::Borrowed(ver)) - } - } - - 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<CompleteVersion, VersionError> { - 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")); - - debug!("Loading local copy of remote version {}", ver.id); - - 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<Cow<'v, CompleteVersion>, VersionResolveError> { - let mut seen: HashSet<String> = HashSet::new(); - seen.insert(ver.id.clone()); - - let Some(inherit) = ver.inherits_from.as_ref() else { - return Ok(Cow::Borrowed(ver)); - }; - - if *inherit == ver.id { - warn!("Version {} directly inherits from itself!", ver.id); - return Err(VersionResolveError::InheritanceLoop(ver.id.clone())); - } - - debug!("Resolving version inheritance: {} (inherits from {})", ver.id, inherit); - - let mut ver = ver.clone(); - let mut inherit = inherit.clone(); - - loop { - if !seen.insert(inherit.clone()) { - warn!("Version inheritance loop detected in {}: {} transitively inherits from itself.", ver.id, inherit); - return Err(VersionResolveError::InheritanceLoop(inherit)); - } - - 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(VersionResolveError::VersionLoad)?), - VersionResult::None => { - warn!("Cannot resolve version {}, it inherits an unknown version {inherit}", ver.id); - return Err(VersionResolveError::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)) - } -} |
