diff options
| author | 2024-12-22 23:48:39 -0600 | |
|---|---|---|
| committer | 2024-12-22 23:48:39 -0600 | |
| commit | 1f5693c5531fa7ddf7bfdb8e27dc48d9765b97ca (patch) | |
| tree | 72bd95bb067306a9cf4d133a510fd10590f54d12 | |
| parent | use paths instead of dumb strings (diff) | |
when the
| -rw-r--r-- | .idea/jsonSchemas.xml | 27 | ||||
| -rw-r--r-- | Cargo.lock | 132 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/launcher.rs | 156 | ||||
| -rw-r--r-- | src/launcher/version.rs | 293 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | src/util.rs | 68 | ||||
| -rw-r--r-- | src/version.rs | 172 | ||||
| -rw-r--r-- | src/version/manifest.rs | 5 |
10 files changed, 684 insertions, 174 deletions
diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml new file mode 100644 index 0000000..7d1fb3b --- /dev/null +++ b/.idea/jsonSchemas.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JsonSchemaMappingsProjectConfiguration"> + <state> + <map> + <entry key="No JSON schema"> + <value> + <SchemaInfo> + <option name="ignoredFile" value="true" /> + <option name="name" value="No JSON schema" /> + <option name="relativePathToSchema" value="" /> + <option name="applicationDefined" value="true" /> + <option name="patterns"> + <list> + <Item> + <option name="path" value="file://$PROJECT_DIR$/test_stuff/versions/1.2.1.json" /> + </Item> + </list> + </option> + <option name="isIgnoredFile" value="true" /> + </SchemaInfo> + </value> + </entry> + </map> + </state> + </component> +</project>
\ No newline at end of file @@ -209,6 +209,12 @@ dependencies = [ ] [[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -248,6 +254,12 @@ dependencies = [ ] [[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -255,7 +267,7 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -290,6 +302,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] name = "http" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -647,7 +668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -682,9 +703,11 @@ name = "o3launcher" version = "0.1.0" dependencies = [ "chrono", + "hex", "log", "regex", "reqwest", + "rust-crypto", "serde", "serde_json", "tokio", @@ -792,6 +815,53 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -879,12 +949,31 @@ dependencies = [ ] [[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + +[[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + +[[package]] name = "rustix" version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1133,6 +1222,17 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1273,6 +1373,12 @@ dependencies = [ [[package]] name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" @@ -1355,6 +1461,28 @@ dependencies = [ ] [[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5,9 +5,11 @@ edition = "2021" [dependencies] chrono = { version = "0.4.39", default-features = false, features = ["std", "alloc", "clock", "now", "serde"] } +hex = { version = "0.4.3", features = ["serde"] } log = "0.4.22" regex = "1.11.1" reqwest = "0.12.9" +rust-crypto = "0.2.36" serde = { version = "1.0.216", features = ["derive"] } serde_json = "1.0.133" tokio = { version = "1.42.0", features = ["fs"] } diff --git a/src/launcher.rs b/src/launcher.rs index 82abef0..d99a294 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -1,151 +1,15 @@ -use std::{collections::{BTreeMap, HashMap}, error::Error, fmt::Write, io::ErrorKind};
-use std::path::{Path, PathBuf};
-use chrono::{DateTime, Utc};
-use serde::{Serialize, Deserialize};
-
-use log::{debug, warn};
-
-use super::version::{*, manifest::*};
-
mod constants;
-use constants::*;
-
-#[derive(Serialize, Deserialize, Debug)]
-struct RemoteVersionIndexEntry {
- last_update: DateTime<Utc>
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct RemoteVersionIndex {
- versions: BTreeMap<String, RemoteVersionIndexEntry>
-}
-
-impl Default for RemoteVersionIndex {
- fn default() -> Self {
- RemoteVersionIndex {
- versions: BTreeMap::new()
- }
- }
-}
-
-struct RemoteVersionList {
- versions: HashMap<String, VersionManifestVersion>,
- index: RemoteVersionIndex
-}
-
-impl RemoteVersionList {
- async fn new(home: impl AsRef<Path>) -> 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 fname = home.as_ref().to_path_buf();
- fname.push(".version.json");
-
- let index: RemoteVersionIndex = match tokio::fs::read_to_string(fname).await {
- Ok(s) => serde_json::from_str(s.as_str())?,
- Err(e) => {
- if e.kind() == ErrorKind::NotFound {
- RemoteVersionIndex::default()
- } else {
- return Err(Box::new(e));
- }
- }
- };
-
- let mut versions = HashMap::new();
- for v in manifest.versions {
- versions.insert(v.id.clone(), v);
- }
-
- Ok(RemoteVersionList {
- versions,
- index
- })
- }
-}
+mod version;
-struct LocalVersionList {
- versions: BTreeMap<String, CompleteVersion>
-}
-
-impl LocalVersionList {
- async fn load_version(path: &PathBuf) -> Result<CompleteVersion, Box<dyn Error>> {
- serde_json::from_str(tokio::fs::read_to_string(path).await?)?;
- }
-
- async fn load_versions(home: impl AsRef<Path>, skip: impl Fn(&str) -> bool) -> Result<LocalVersionList, Box<dyn Error>> {
- let mut rd = tokio::fs::read_dir(home).await?;
- let 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"));
-
-
- },
+use std::error::Error;
+pub use version::VersionList;
- /* 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.");
- continue
- }
- };
-
- match Self::load_version(path).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 })
- }
-}
-
-struct VersionList {
- offline: bool,
- remote: Option<RemoteVersionList>,
- local: LocalVersionList
+pub struct Launcher {
+ pub versions: VersionList
}
-impl VersionList {
- pub async fn online(home: impl AsRef<Path>) -> Result<VersionList, Box<dyn Error>> {
- let remote = RemoteVersionList::new(home).await?;
- let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?;
-
- Ok(VersionList {
- offline: false,
- remote: Some(remote),
- local
- })
- }
-
- pub async fn offline(home: impl AsRef<Path>) -> Result<VersionList, Box<dyn Error>> {
- let local = LocalVersionList::load_versions(home, |_| false).await?;
-
- Ok(VersionList {
- offline: true,
- remote: None,
- local
- })
- }
-}
+impl Launcher {
+ pub async fn new() -> Result<Launcher, Box<dyn Error>> {
+ todo!()
+ }
+}
\ No newline at end of file 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)) + } +} @@ -1,2 +1,3 @@ +mod util; mod version; mod launcher; diff --git a/src/main.rs b/src/main.rs index e7a11a9..2a778e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,3 @@ fn main() { - println!("Hello, world!"); + } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..428990c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,68 @@ +use std::fmt::{Debug, Display, Formatter, Pointer}; +use std::ops::Deref; +use crypto::digest::Digest; +use crypto::sha1::Sha1; +use serde::{Deserialize, Deserializer}; +use serde::de::{Error, Visitor}; +use hex::{FromHex, ToHex}; + +// sha1 digests are 20 bytes long +pub const SHA1_DIGEST_BYTES: usize = 20; +pub type Sha1DigestBytes = [u8; SHA1_DIGEST_BYTES]; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct Sha1Digest(pub Sha1DigestBytes); + +impl Debug for Sha1Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Sha1Digest {{{}}}", self.0.encode_hex::<String>()) + } +} + +impl Display for Sha1Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.encode_hex::<String>()) + } +} + +impl Sha1Digest { + pub fn verify(&self, s: &str) -> Result<(), Sha1Digest> { + let mut st = Sha1::new(); + let mut dig = [0u8; SHA1_DIGEST_BYTES]; + + st.input_str(s); + st.result(&mut dig); + + if self.0 == dig { + return Ok(()); + } + + Err(Sha1Digest(dig)) + } +} + +struct Sha1DigestVisitor; + +impl <'a> Visitor<'a> for Sha1DigestVisitor { + type Value = Sha1Digest; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "a valid SHA-1 digest (40-character hex string)") + } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + Sha1DigestBytes::from_hex(v).map_err(|e| E::custom(e)).map(Sha1Digest) + } +} + +impl<'a> Deserialize<'a> for Sha1Digest { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'a>, + { + deserializer.deserialize_any(Sha1DigestVisitor) + } +}
\ No newline at end of file diff --git a/src/version.rs b/src/version.rs index 0c1e65f..190dd72 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,12 +1,15 @@ use core::fmt; use std::{collections::BTreeMap, convert::Infallible, marker::PhantomData, ops::Deref, str::FromStr}; - +use std::ascii::AsciiExt; +use std::collections::HashMap; use chrono::{DateTime, Utc}; use regex::Regex; use serde::{de::{self, Visitor}, Deserialize, Deserializer}; +use serde::de::SeqAccess; pub mod manifest; use manifest::*; +use crate::util::Sha1Digest; #[derive(Deserialize, Debug, Clone, Copy)] #[serde(rename_all = "lowercase")] @@ -29,7 +32,7 @@ pub enum OperatingSystem { Unknown // (not used in official jsons) } -#[derive(Debug)] +#[derive(Debug, Clone)] struct WrappedRegex(Regex); impl Deref for WrappedRegex { @@ -63,7 +66,7 @@ impl<'de> Deserialize<'de> for WrappedRegex { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct OSRestriction { pub name: Option<OperatingSystem>, @@ -71,7 +74,7 @@ pub struct OSRestriction { pub arch: Option<WrappedRegex> } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct CompatibilityRule { pub action: RuleAction, pub features: Option<BTreeMap<String, bool>>, @@ -92,7 +95,7 @@ impl CompatibilityRule { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Argument { #[serde(default)] pub rules: Option<Vec<CompatibilityRule>>, @@ -102,7 +105,7 @@ pub struct Argument { pub value: Vec<String> } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WrappedArgument(Argument); impl FromStr for Argument { @@ -132,13 +135,29 @@ impl<'de> Deserialize<'de> for WrappedArgument { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Arguments { pub game: Option<Vec<WrappedArgument>>, pub jvm: Option<Vec<WrappedArgument>> } -#[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] +impl Arguments { + fn apply_child(&mut self, other: &Arguments) { + if self.game.is_none() { + if let Some(game) = other.game.as_ref() { + self.game.replace(game.to_owned()); + } + } + + if self.jvm.is_none() { + if let Some(jvm) = other.jvm.as_ref() { + self.jvm.replace(jvm.to_owned()); + } + } + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum DownloadType { Client, @@ -148,9 +167,9 @@ pub enum DownloadType { WindowsServer } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct DownloadInfo { - pub sha1: Option<String>, + pub sha1: Option<Sha1Digest>, pub size: Option<usize>, pub total_size: Option<usize>, // available for asset index pub url: Option<String>, // may not be present for libraries @@ -158,26 +177,26 @@ pub struct DownloadInfo { pub path: Option<String> } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct JavaVersionInfo { pub component: String, pub major_version: u32 } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct LibraryDownloads { pub artifact: Option<DownloadInfo>, pub classifiers: Option<BTreeMap<String, DownloadInfo>> } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct LibraryExtractRule { #[serde(default)] pub exclude: Vec<String> } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Library { pub downloads: Option<LibraryDownloads>, pub name: String, @@ -187,7 +206,7 @@ pub struct Library { pub url: Option<String> // old format } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct ClientLogging { pub argument: String, @@ -196,12 +215,12 @@ pub struct ClientLogging { pub file: DownloadInfo } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Logging { pub client: ClientLogging // other fields unknown } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct CompleteVersion { pub arguments: Option<Arguments>, @@ -217,8 +236,8 @@ pub struct CompleteVersion { #[serde(default)] pub downloads: BTreeMap<DownloadType, DownloadInfo>, - #[serde(default)] - pub libraries: Vec<Library>, + #[serde(default, deserialize_with = "deserialize_libraries")] + pub libraries: HashMap<String, Library>, pub id: String, pub jar: Option<String>, // used as the jar filename if specified? (no longer used officially) @@ -243,6 +262,104 @@ pub struct CompleteVersion { */ } +impl CompleteVersion { + pub fn get_jar(&self) -> &String { + &self.jar.as_ref().unwrap_or(&self.id) + } + + pub fn apply_child(&mut self, other: &CompleteVersion) { + macro_rules! replace_missing { + ($name:ident) => { + if self.$name.is_none() { + if let Some($name) = other.$name.as_ref() { + self.$name.replace($name.to_owned()); + } + } + }; + } + + if let Some(arguments) = other.arguments.as_ref() { + if let Some(my_args) = self.arguments.as_mut() { + my_args.apply_child(arguments); + } else { + self.arguments.replace(arguments.to_owned()); + } + } + + replace_missing!(minecraft_arguments); + replace_missing!(asset_index); + replace_missing!(assets); + replace_missing!(compliance_level); + replace_missing!(java_version); + + for (dltype, dl) in other.downloads.iter().by_ref() { + self.downloads.entry(*dltype).or_insert_with(|| dl.clone()); + } + + for (name, lib) in other.libraries.iter().by_ref() { + self.libraries.entry(name.to_owned()).or_insert_with(|| lib.clone()); + } + + replace_missing!(logging); + replace_missing!(main_class); + replace_missing!(minimum_launcher_version); + replace_missing!(release_time); + replace_missing!(time); + replace_missing!(version_type); + + if let Some(rules) = other.compatibility_rules.as_ref() { + if let Some(my_rules) = self.compatibility_rules.as_mut() { + for rule in rules { + my_rules.push(rule.to_owned()); + } + } else { + self.compatibility_rules.replace(rules.to_owned()); + } + } + + replace_missing!(incompatibility_reason); + } +} + +fn canonicalize_library_name(name: &str) -> String { + name.split(':') + .enumerate() + .filter(|(i, _)| *i != 2) + .map(|(_, s)| s.to_ascii_lowercase()) + .collect::<Vec<_>>() + .join(":") +} + +fn deserialize_libraries<'de, D>(deserializer: D) -> Result<HashMap<String, Library>, D::Error> +where + D: Deserializer<'de> +{ + struct LibrariesVisitor; + + impl<'de> Visitor<'de> for LibrariesVisitor { + type Value = HashMap<String, Library>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of libraries") + } + + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> + where + A: SeqAccess<'de>, + { + let mut map = HashMap::new(); + + while let Some(lib) = seq.next_element::<Library>()? { + map.insert(canonicalize_library_name(lib.name.as_str()), lib); + } + + Ok(map) + } + } + + deserializer.deserialize_seq(LibrariesVisitor) +} + // https://serde.rs/string-or-struct.html fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error> where @@ -257,19 +374,19 @@ where { type Value = T; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where - E: serde::de::Error, { + E: de::Error, { Ok(FromStr::from_str(v).unwrap()) } fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error> where - A: serde::de::MapAccess<'de>, { + A: de::MapAccess<'de>, { // wizardry (check comment in link) Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) } @@ -304,7 +421,7 @@ where fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error> where - A: de::SeqAccess<'de>, { + A: SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) } } @@ -333,4 +450,13 @@ mod tests { let arg: VersionManifest = serde_json::from_str(s.unwrap().as_str()).unwrap(); dbg!(arg); } + + #[test] + fn test_it3() { + assert_eq!(canonicalize_library_name("group:artifact:version"), String::from("group:artifact")); + assert_eq!(canonicalize_library_name("group:artifact:version:specifier"), String::from("group:artifact:specifier")); + assert_eq!(canonicalize_library_name("not_enough:fields"), String::from("not_enough:fields")); + assert_eq!(canonicalize_library_name("word"), String::from("word")); + assert_eq!(canonicalize_library_name(""), String::from("")); + } } diff --git a/src/version/manifest.rs b/src/version/manifest.rs index 1fa8041..437daf6 100644 --- a/src/version/manifest.rs +++ b/src/version/manifest.rs @@ -2,6 +2,7 @@ use core::fmt; use chrono::{DateTime, Utc}; use serde::{de::Visitor, Deserialize}; +use crate::util::Sha1Digest; #[derive(Deserialize, Debug)] pub struct LatestVersions { @@ -9,7 +10,7 @@ pub struct LatestVersions { pub snapshot: String } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum VersionType { Snapshot, Release, @@ -58,7 +59,7 @@ pub struct VersionManifestVersion { pub url: String, pub time: DateTime<Utc>, pub release_time: DateTime<Utc>, - pub sha1: String, + pub sha1: Sha1Digest, pub compliance_level: u32 } |
