summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock120
-rw-r--r--ozone-cli/Cargo.toml6
-rw-r--r--ozone-cli/src/main.rs12
-rw-r--r--src/launcher.rs79
-rw-r--r--src/launcher/download.rs25
-rw-r--r--src/launcher/strsub.rs28
-rw-r--r--src/launcher/version.rs70
-rw-r--r--src/lib.rs4
8 files changed, 307 insertions, 37 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e32da57..6d2df19 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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));
}
diff --git a/src/lib.rs b/src/lib.rs
index 70041eb..c369ed5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,3 @@
mod util;
-mod version;
-mod launcher;
+pub mod version;
+pub mod launcher;