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 super::request::EasyFetch; use crate::util; use crate::version::{*, manifest::*}; use super::constants::*; struct RemoteVersionList { versions: HashMap, latest: LatestVersions } impl RemoteVersionList { async fn new() -> Result> { let text = EasyFetch::get(URL_VERSION_MANIFEST).await?.error_for_status()?.get_data_string(); 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 = EasyFetch::get(ver.url.as_str()).await?.error_for_status()?.get_data_string(); // make sure it's valid util::verify_sha1(ver.sha1, 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: Digest, got: Digest }, VersionMismatch { fname: String, json: String }, Unknown(Box) } 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) -> 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 { util::verify_sha1(digest_exp, ver.as_str()) .map_err(|got| LocalVersionError::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(LocalVersionError::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()) } } #[derive(Debug)] pub enum VersionResolveError { InheritanceLoop(String), MissingVersion(String), Unknown(Box) } 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::Unknown(err) => { write!(f, "unknown error: {err}") } } } } impl Error for VersionResolveError {} 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 { if seen.insert(inherit.clone()) { 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(|e| VersionResolveError::Unknown(e))?), 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)) } }