summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2024-12-22 23:48:39 -0600
committerLibravatar bigfoot547 <[email protected]>2024-12-22 23:48:39 -0600
commit1f5693c5531fa7ddf7bfdb8e27dc48d9765b97ca (patch)
tree72bd95bb067306a9cf4d133a510fd10590f54d12 /src
parentuse paths instead of dumb strings (diff)
when the
Diffstat (limited to 'src')
-rw-r--r--src/launcher.rs156
-rw-r--r--src/launcher/version.rs293
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs2
-rw-r--r--src/util.rs68
-rw-r--r--src/version.rs172
-rw-r--r--src/version/manifest.rs5
7 files changed, 525 insertions, 172 deletions
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))
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 5f506fc..70041eb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
}