summaryrefslogtreecommitdiffstats
path: root/src/launcher/version.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/launcher/version.rs')
-rw-r--r--src/launcher/version.rs398
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))
- }
-}