summaryrefslogtreecommitdiffstats
path: root/src/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'src/launcher')
-rw-r--r--src/launcher/version.rs293
1 files changed, 293 insertions, 0 deletions
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<String, VersionManifestVersion>,
+ latest: LatestVersions
+}
+
+impl RemoteVersionList {
+ async fn new() -> Result<RemoteVersionList, Box<dyn Error>> {
+ 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<CompleteVersion, Box<dyn Error>> {
+ // 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::<Box<dyn Error>, _>(|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<String, CompleteVersion>
+}
+
+#[derive(Debug)]
+enum LocalVersionError {
+ Sha1Mismatch { exp: Sha1Digest, got: Sha1Digest },
+ 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 {
+ 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<CompleteVersion, LocalVersionError> {
+ // 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<LocalVersionList, Box<dyn Error>> {
+ 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<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())
+ }
+}
+
+pub enum VersionResolveError {
+ InheritanceLoop,
+ MissingVersion(String),
+ Unknown(Box<dyn Error>)
+}
+
+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<VersionList, Box<dyn Error>> {
+ 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<VersionList, Box<dyn Error>> {
+ 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<CompleteVersion, Box<dyn Error>> {
+ 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<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));
+ };
+
+ 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))
+ }
+}