diff options
| -rw-r--r-- | Cargo.lock | 120 | ||||
| -rw-r--r-- | ozone-cli/Cargo.toml | 6 | ||||
| -rw-r--r-- | ozone-cli/src/main.rs | 12 | ||||
| -rw-r--r-- | src/launcher.rs | 79 | ||||
| -rw-r--r-- | src/launcher/download.rs | 25 | ||||
| -rw-r--r-- | src/launcher/strsub.rs | 28 | ||||
| -rw-r--r-- | src/launcher/version.rs | 70 | ||||
| -rw-r--r-- | src/lib.rs | 4 |
8 files changed, 307 insertions, 37 deletions
@@ -558,6 +558,16 @@ dependencies = [ ] [[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] name = "com" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -829,6 +839,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" [[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] name = "detect-desktop-environment" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2057,6 +2076,12 @@ dependencies = [ ] [[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2313,6 +2338,12 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2353,6 +2384,15 @@ dependencies = [ ] [[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] name = "o3launcher" version = "0.1.0" dependencies = [ @@ -2692,7 +2732,11 @@ dependencies = [ name = "ozone-cli" version = "0.1.0" dependencies = [ + "o3launcher", + "simple_logger", "sysinfo", + "tokio", + "tokio-macros", ] [[package]] @@ -2912,6 +2956,12 @@ dependencies = [ ] [[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3458,6 +3508,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] +name = "simple_logger" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + +[[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3769,6 +3831,39 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] name = "tiny-skia" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3834,9 +3929,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -3844,10 +3939,22 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] [[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4617,6 +4724,15 @@ dependencies = [ [[package]] name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" diff --git a/ozone-cli/Cargo.toml b/ozone-cli/Cargo.toml index c4b3dd0..730a37f 100644 --- a/ozone-cli/Cargo.toml +++ b/ozone-cli/Cargo.toml @@ -4,4 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -sysinfo = { version = "0.33.1", features = ["system", "multithread"] }
\ No newline at end of file +sysinfo = { version = "0.33.1", features = ["system", "multithread"] } +o3launcher = { path = ".." } +tokio = { version = "1.43.0", features = ["rt", "rt-multi-thread", "macros"] } +tokio-macros = "2.5.0" +simple_logger = { version = "5.0.0", features = ["colors"] } diff --git a/ozone-cli/src/main.rs b/ozone-cli/src/main.rs index b3f91f1..dbe591a 100644 --- a/ozone-cli/src/main.rs +++ b/ozone-cli/src/main.rs @@ -1,8 +1,18 @@ use std::env::consts::{ARCH, OS}; +use std::error::Error; +use std::path::PathBuf; use sysinfo::System; -fn main() { +#[tokio::main] +async fn main() -> Result<(), Box<dyn Error>> { + simple_logger::SimpleLogger::new().init().unwrap(); + println!("Hello, world!"); println!("stuff: {:?} {:?} {:?} {:?} {:?}", System::name(), System::os_version(), System::long_os_version(), System::kernel_version(), System::cpu_arch()); println!("stuff: {:?} {:?} {:?} {}", System::distribution_id(), OS, ARCH, size_of::<*const i32>()); + + let launcher = o3launcher::launcher::Launcher::new(PathBuf::from("./work").as_path(), true).await?; + println!("ok"); + + Ok(()) } diff --git a/src/launcher.rs b/src/launcher.rs index e207281..a7ef8c9 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -11,22 +11,64 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; +use const_format::formatcp; +use futures::TryFutureExt; +use log::{debug, warn}; use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; use sysinfo::System; use tokio::fs::File; +use tokio::{fs, io}; use tokio::io::AsyncReadExt; use version::VersionList; use profile::{Instance, Profile}; use crate::launcher::version::{VersionResolveError, VersionResult}; use crate::version::{Library, OSRestriction, OperatingSystem}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { profiles: HashMap<String, Profile>, instances: HashMap<String, Instance> } +#[derive(Debug)] +enum SettingsLoadError { + IO(io::Error), + Format(serde_json::Error) +} + +impl Display for SettingsLoadError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SettingsLoadError::IO(err) => write!(f, "I/O error loading settings: {}", err), + SettingsLoadError::Format(err) => write!(f, "settings format error: {}", err), + } + } +} + +impl Error for SettingsLoadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + SettingsLoadError::IO(err) => Some(err), + SettingsLoadError::Format(err) => Some(err), + } + } +} + +impl Settings { + async fn load(path: impl AsRef<Path>) -> Result<Settings, SettingsLoadError> { + let data = match fs::read_to_string(&path).await { + Ok(data) => data, + Err(e) => return match e.kind() { + ErrorKind::NotFound => Ok(Settings::default()), + _ => Err(SettingsLoadError::IO(e)) + } + }; + + serde_json::from_str(data.as_str()).map_err(SettingsLoadError::Format) + } +} + struct SystemInfo { os: OperatingSystem, os_version: String, @@ -84,7 +126,19 @@ impl Launcher { let home = home.to_owned(); let versions_home = home.join("versions"); let versions; - + + match tokio::fs::create_dir_all(&home).await { + Ok(_) => (), + Err(e) => match e.kind() { + ErrorKind::AlreadyExists => (), + _ => { + warn!("Failed to create launcher home directory: {}", e); + return Err(e.into()); + } + } + } + + debug!("Version list online?: {online}"); if online { versions = VersionList::online(versions_home.as_ref()).await?; } else { @@ -92,7 +146,7 @@ impl Launcher { } let settings_path = home.join("ozone.json"); - let settings = serde_json::from_str(tokio::fs::read_to_string(&settings_path).await?.as_str())?; + let settings = Settings::load(&settings_path).await?; Ok(Launcher { online, @@ -162,7 +216,16 @@ impl Display for LibraryError { impl Error for LibraryError {} +const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS); + impl LibraryRepository { + fn lib_replace(key: &str) -> Option<Cow<'static, str>> { + match key { + "arch" => Some(Cow::Borrowed(ARCH_BITS)), + _ => None + } + } + fn get_artifact_base_dir(name: &str) -> Option<PathBuf> { let end_of_gid = name.find(':')?; @@ -174,14 +237,14 @@ impl LibraryRepository { if let Some(classifier) = classifier { match n.len() { - 3 => Some(PathBuf::from(format!("{}-{}-{}.jar", n[1], n[2], classifier))), - 4 => Some(PathBuf::from(format!("{}-{}-{}-{}.jar", n[1], n[2], classifier, n[3]))), + 3 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}-{}.jar", n[1], n[2], classifier), Self::lib_replace))), + 4 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}-{}-{}.jar", n[1], n[2], classifier, n[3]), Self::lib_replace))), _ => None } } else { match n.len() { - 3 => Some(PathBuf::from(format!("{}-{}.jar", n[1], n[2]))), - 4 => Some(PathBuf::from(format!("{}-{}-{}.jar", n[1], n[2], n[3]))), + 3 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}.jar", n[1], n[2]), Self::lib_replace))), + 4 => Some(PathBuf::from(strsub::replace_thru(format!("{}-{}-{}.jar", n[1], n[2], n[3]), Self::lib_replace))), _ => None } } @@ -238,4 +301,4 @@ impl SystemInfo { && restriction.version.as_deref().is_none_or(|pat| pat.is_match(&self.os_version)) && restriction.arch.as_deref().is_none_or(|pat| pat.is_match(&self.arch)) } -}
\ No newline at end of file +} diff --git a/src/launcher/download.rs b/src/launcher/download.rs index 28c3b86..8c24cae 100644 --- a/src/launcher/download.rs +++ b/src/launcher/download.rs @@ -3,6 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use futures::{stream, Stream, StreamExt}; +use log::debug; use reqwest::{Client, IntoUrl, Method, RequestBuilder}; use sha1_smol::{Digest, Sha1}; use tokio::fs; @@ -101,7 +102,7 @@ impl<T: Download> MultiDownloader<T> { pub async fn perform(&mut self) -> impl Stream<Item = Result<(), PhaseDownloadError<T>>> { stream::iter(self.jobs.iter_mut()).map(|job| { let client = &self.client; - + macro_rules! map_err { ($result:expr, $phase:expr, $job:expr) => { match $result { @@ -117,7 +118,7 @@ impl<T: Download> MultiDownloader<T> { .header(reqwest::header::USER_AGENT, USER_AGENT)).await, Phase::Prepare, job) else { return Ok(()) }; - + let mut data = map_err!(map_err!(rq.send().await, Phase::Send, job).error_for_status(), Phase::Send, job).bytes_stream(); while let Some(bytes) = data.next().await { @@ -220,13 +221,21 @@ impl Download for VerifiedDownload { Ok(file) => file, Err(e) => return if e.kind() == ErrorKind::NotFound { // assume the parent folder exists (responsibility of the caller to ensure this) + debug!("File {} does not exist, downloading it.", self.path.to_string_lossy()); self.open_output().await?; Ok(Some(req)) } else { + debug!("Error opening {}: {}", self.path.to_string_lossy(), e); Err(e.into()) } }; + // short-circuit this + if self.expect_size.is_none() && self.expect_sha1.is_none() { + debug!("No size or sha1 for {}, have to assume it's good.", self.path.to_string_lossy()); + return Ok(None); + } + let mut tally = 0usize; let mut sha1 = Sha1::new(); @@ -236,7 +245,10 @@ impl Download for VerifiedDownload { Ok(n) => n, Err(e) => match e.kind() { ErrorKind::Interrupted => continue, - _ => return Err(e.into()) + _ => { + debug!("Error reading {}: {}", self.path.to_string_lossy(), e); + return Err(e.into()); + } } }; @@ -248,6 +260,7 @@ impl Download for VerifiedDownload { if self.expect_sha1.is_none_or(|d| d == sha1.digest()) && self.expect_size.is_none_or(|s| s == tally) { + debug!("Not downloading {}, sha1 and size match.", self.path.to_string_lossy()); return Ok(None); } @@ -255,6 +268,8 @@ impl Download for VerifiedDownload { // potentially racy to close the file and reopen it... :/ self.open_output().await?; + + debug!("Downloading {} because sha1 or size does not match.", self.path.to_string_lossy()); Ok(Some(req)) } @@ -271,13 +286,17 @@ impl Download for VerifiedDownload { if let Some(d) = self.expect_sha1 { if d != digest { + debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.to_string_lossy(), d, digest); return Err(IntegrityError::Sha1Mismatch { expect: d, actual: digest }.into()); } } else if let Some(s) = self.expect_size { if s != self.tally { + debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.to_string_lossy(), s, self.tally); return Err(IntegrityError::SizeMismatch { expect: s, actual: self.tally }.into()); } } + + debug!("Successfully downloaded {} ({} bytes).", self.path.to_string_lossy(), self.tally); // release the file descriptor (don't want to wait until it's dropped automatically because idk when that would be) drop(self.file.take().unwrap()); diff --git a/src/launcher/strsub.rs b/src/launcher/strsub.rs index cdf395b..e01ef80 100644 --- a/src/launcher/strsub.rs +++ b/src/launcher/strsub.rs @@ -12,23 +12,35 @@ fn prev_char(slice: &str, idx: usize) -> Option<(usize, char)> { slice[..idx].char_indices().rev().next() } -// basically the same thing as replace_string, but it creates the String itself and returns it. -pub fn replace_str<'rep, T>(input: &str, sub: T) -> String +pub trait SubFunc<'rep>: Fn(&str) -> Option<Cow<'rep, str>> { + fn substitute(&self, key: &str) -> Option<Cow<'rep, str>>; +} + +impl<'rep, F> SubFunc<'rep> for F where - T: Fn(/*key: */ &str) -> Option<Cow<'rep, str>> + F: Fn(&str) -> Option<Cow<'rep, str>> { + fn substitute(&self, key: &str) -> Option<Cow<'rep, str>> { + self(key) + } +} + +// basically the same thing as replace_string, but it creates the String itself and returns it. +pub fn replace_str<'rep>(input: &str, sub: impl SubFunc<'rep>) -> String { let mut input = String::from(input); replace_string(&mut input, sub); input } +pub fn replace_thru<'rep>(mut input: String, sub: impl SubFunc<'rep>) -> String { + replace_string(&mut input, sub); + input +} + // handles ${replacements} on this string IN-PLACE. Calls the "sub" function for each key it receives. // if "sub" returns None, it will use a default value or ignore the ${substitution}. // There are no "invalid inputs" and this function should never panic unless "sub" panics. -pub fn replace_string<'rep, T>(input: &mut String, sub: T) -where - T: Fn(/*key: */ &str) -> Option<Cow<'rep, str>> -{ +pub fn replace_string<'rep>(input: &mut String, sub: impl SubFunc<'rep>) { let mut cursor = input.len(); while let Some(idx) = input[..cursor].rfind(VAR_BEGIN) { // note: for some reason, apache processes escapes BEFORE checking if it's even a valid @@ -58,7 +70,7 @@ where def_opt = None; } - if let Some(sub_val) = sub(name).map_or_else(|| def_opt.map(|d| Cow::Owned(d.to_owned())), |v| Some(v)) { + if let Some(sub_val) = sub.substitute(name).map_or_else(|| def_opt.map(|d| Cow::Owned(d.to_owned())), |v| Some(v)) { input.replace_range(idx..(endidx + VAR_END.len()), sub_val.as_ref()); } diff --git a/src/launcher/version.rs b/src/launcher/version.rs index f4cdd6c..411ac59 100644 --- a/src/launcher/version.rs +++ b/src/launcher/version.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use log::{debug, info, warn}; use sha1_smol::Digest; +use tokio::{fs, io}; use crate::util; use crate::version::{*, manifest::*}; @@ -18,7 +19,9 @@ struct RemoteVersionList { impl RemoteVersionList { async fn new() -> Result<RemoteVersionList, Box<dyn Error>> { + debug!("Looking up remote version manifest."); let text = reqwest::get(URL_VERSION_MANIFEST).await?.error_for_status()?.text().await?; + debug!("Parsing version manifest."); let manifest: VersionManifest = serde_json::from_str(text.as_str())?; let mut versions = HashMap::new(); @@ -26,6 +29,7 @@ impl RemoteVersionList { versions.insert(v.id.clone(), v); } + debug!("Done loading remote versions!"); Ok(RemoteVersionList { versions, latest: manifest.latest @@ -34,6 +38,7 @@ impl RemoteVersionList { async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result<CompleteVersion, Box<dyn Error>> { // ensure parent directory exists + info!("Downloading version {}.", ver.id); match tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await { Err(e) => { if e.kind() != ErrorKind::AlreadyExists { @@ -47,6 +52,7 @@ impl RemoteVersionList { // download it let ver_text = reqwest::get(ver.url.as_str()).await?.error_for_status()?.text().await?; + debug!("Validating downloaded {}...", ver.id); // make sure it's valid util::verify_sha1(ver.sha1, 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())?; @@ -54,9 +60,13 @@ impl RemoteVersionList { // make sure it's well-formed let cver: CompleteVersion = serde_json::from_str(ver.url.as_str())?; + debug!("Saving version {}...", ver.id); + // write it out tokio::fs::write(path, ver_text).await?; + info!("Done downloading and verifying {}!", ver.id); + Ok(cver) } } @@ -93,13 +103,21 @@ 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| LocalVersionError::Sha1Mismatch { exp: digest_exp.to_owned(), got })?; + .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| LocalVersionError::Unknown(Box::new(e)))?; + 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 @@ -107,13 +125,16 @@ impl LocalVersionList { .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, Box<dyn Error>> { + info!("Loading local versions."); let mut rd = tokio::fs::read_dir(home).await?; let mut versions = BTreeMap::new(); @@ -156,6 +177,7 @@ impl LocalVersionList { } } + info!("Loaded {} local version(s).", versions.len()); Ok(LocalVersionList { versions }) } } @@ -200,23 +222,35 @@ pub enum VersionResolveError { 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}") - } + 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 { + async fn create_dir_for(home: &Path) -> Result<(), io::Error> { + debug!("Creating versions directory."); + match fs::create_dir(home).await { + Ok(_) => Ok(()), + Err(e) => match e.kind() { + ErrorKind::AlreadyExists => Ok(()), + _ => { + debug!("failed to create version home: {}", e); + Err(e) + } + } + } + } + pub async fn online(home: &Path) -> Result<VersionList, Box<dyn Error>> { + Self::create_dir_for(home).await?; + let remote = RemoteVersionList::new().await?; let local = LocalVersionList::load_versions(home.as_ref(), |s| remote.versions.contains_key(s)).await?; @@ -228,6 +262,8 @@ impl VersionList { } pub async fn offline(home: &Path) -> Result<VersionList, Box<dyn Error>> { + Self::create_dir_for(home).await?; + let local = LocalVersionList::load_versions(home, |_| false).await?; Ok(VersionList { @@ -259,10 +295,12 @@ impl VersionList { 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}"); + info!("Redownloading {id}, since the local copy could not be loaded: {e}"); } } @@ -277,11 +315,19 @@ impl VersionList { 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)); } @@ -1,3 +1,3 @@ mod util; -mod version; -mod launcher; +pub mod version; +pub mod launcher; |
