use core::fmt; use std::{collections::BTreeMap, convert::Infallible, marker::PhantomData, ops::Deref, str::FromStr}; use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::format::ParseErrorKind; use regex::Regex; use serde::{de::{self, Visitor}, Deserialize, Deserializer}; use serde::de::{Error, SeqAccess}; use sha1_smol::Digest; pub mod manifest; use manifest::*; #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum RuleAction { Allow, Disallow } // must derive an order on this because it's used as a key for a btreemap #[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[serde(rename_all = "lowercase")] pub enum OperatingSystem { Linux, // "linux" Windows, // "windows" #[serde(alias = "osx")] // not technically correct but it works MacOS, // "osx" #[serde(other)] Unknown // (not used in official jsons) } #[derive(Debug, Clone)] pub struct WrappedRegex(Regex); impl Deref for WrappedRegex { type Target = Regex; fn deref(&self) -> &Self::Target { &self.0 } } struct RegexVisitor; impl<'de> Visitor<'de> for RegexVisitor { type Value = WrappedRegex; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a valid regular expression") } fn visit_str(self, v: &str) -> Result where E: de::Error, { Regex::new(v).map_err(de::Error::custom).map(|r| WrappedRegex(r)) } } impl<'de> Deserialize<'de> for WrappedRegex { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { deserializer.deserialize_any(RegexVisitor) } } #[derive(Deserialize, Debug, Clone)] pub struct OSRestriction { #[serde(rename = "name")] pub os: Option, pub version: Option, pub arch: Option } #[derive(Deserialize, Debug, Clone)] pub struct CompatibilityRule { pub action: RuleAction, pub features: Option>, pub os: Option } pub trait FeatureMatcher { fn matches(&self, feature: &str) -> bool; } impl CompatibilityRule { pub fn features_match(&self, checker: &impl FeatureMatcher) -> bool { if let Some(m) = self.features.as_ref() { for (feat, expect) in m { if checker.matches(feat) != *expect { return false; } } } true } } #[derive(Deserialize, Debug, Clone)] pub struct Argument { #[serde(default)] pub rules: Option>, #[serde(default)] #[serde(deserialize_with = "string_or_array")] pub value: Vec } #[derive(Debug, Clone)] pub struct WrappedArgument(Argument); impl FromStr for Argument { type Err = Infallible; fn from_str(s: &str) -> Result { Ok(Argument { value: vec![s.to_owned()], rules: None }) } } impl Deref for WrappedArgument { type Target = Argument; fn deref(&self) -> &Self::Target { &self.0 } } impl<'de> Deserialize<'de> for WrappedArgument { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { Ok(WrappedArgument(string_or_struct(deserializer)?)) } } #[derive(Deserialize, Debug, Clone)] pub struct Arguments { pub game: Option>, pub jvm: Option> } impl Arguments { fn apply_child(&mut self, other: &Arguments) { if let Some(game) = other.game.as_ref() { self.game.get_or_insert_default().splice(0..0, game.iter().cloned()); } if let Some(jvm) = other.jvm.as_ref() { self.jvm.get_or_insert_default().splice(0..0, jvm.iter().cloned()); } } } #[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum DownloadType { Client, ClientMappings, Server, ServerMappings, WindowsServer } #[derive(Deserialize, Debug, Clone)] pub struct DownloadInfo { pub sha1: Option, pub size: Option, pub total_size: Option, // available for asset index pub url: Option, // may not be present for libraries pub id: Option, pub path: Option } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct JavaVersionInfo { pub component: String, pub major_version: u32 } #[derive(Deserialize, Debug, Clone)] pub struct LibraryDownloads { pub artifact: Option, pub classifiers: Option> } #[derive(Deserialize, Debug, Clone)] pub struct LibraryExtractRule { #[serde(default)] pub exclude: Vec } #[derive(Deserialize, Debug, Clone)] pub struct Library { pub downloads: Option, pub name: String, pub extract: Option, pub natives: Option>, pub rules: Option>, // old format pub url: Option, pub size: Option, pub sha1: Option } impl Library { pub fn get_canonical_name(&self) -> String { canonicalize_library_name(self.name.as_str(), self.natives.as_ref().map_or(None, |_| Some("__ozone_natives"))) } } impl LibraryDownloads { pub fn get_download_info(&self, classifier: Option<&str>) -> Option<&DownloadInfo> { if let Some(classifier) = classifier { self.classifiers.as_ref()?.get(classifier) } else { self.artifact.as_ref() } } } #[derive(Deserialize, Debug, Clone)] pub struct ClientLogging { pub argument: String, #[serde(rename = "type")] pub log_type: String, pub file: DownloadInfo } #[derive(Deserialize, Debug, Clone)] pub struct Logging { pub client: Option // other fields unknown } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct CompleteVersion { pub arguments: Option, pub minecraft_arguments: Option, pub asset_index: Option, pub assets: Option, pub compliance_level: Option, pub java_version: Option, #[serde(default)] pub downloads: BTreeMap, #[serde(default)] pub libraries: Vec, pub id: String, pub jar: Option, // used as the jar filename if specified? (no longer used officially) pub logging: Option, pub main_class: Option, pub minimum_launcher_version: Option, #[serde(deserialize_with = "deserialize_datetime_lenient")] pub release_time: Option>, #[serde(deserialize_with = "deserialize_datetime_lenient")] pub time: Option>, #[serde(rename = "type")] pub version_type: Option, pub compatibility_rules: Option>, // pub incompatibility_reason: Option, // message shown when compatibility rules fail for this version pub inherits_from: Option /* omitting field `savableVersion' because it seems like a vestigial part from old launcher versions * (also it isn't even a string that is present in modern liblauncher.so, so I assume it will never be used.) */ } 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()); } // we use extend here instead of splice for library resolution priority reasons // (libraries earlier in the list will override libraries later in the list) self.libraries.extend(other.libraries.iter().cloned()); 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() { my_rules.splice(0..0, rules.iter().cloned()); } else { self.compatibility_rules.replace(rules.to_owned()); } } replace_missing!(incompatibility_reason); } } fn canonicalize_library_name(name: &str, suffix: Option<&str>) -> String { name.split(':') .enumerate() .filter(|(i, _)| *i != 2) .map(|(_, s)| s.to_ascii_lowercase()) .chain(suffix.into_iter().map(|s| s.to_owned())) .collect::>() .join(":") } fn deserialize_datetime_lenient<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de> { struct DateTimeVisitor; impl<'de> Visitor<'de> for DateTimeVisitor { type Value = Option>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a valid datetime") } fn visit_str(self, value: &str) -> Result where E: Error { match value.parse::>() { Ok(dt) => Ok(Some(dt)), Err(e) if e.kind() == ParseErrorKind::TooShort => { // this probably just doesn't have an offset for some reason match value.parse::() { Ok(ndt) => Ok(Some(ndt.and_utc())), Err(e) => Err(Error::custom(e)) } }, Err(e) => Err(Error::custom(e)) } } } deserializer.deserialize_str(DateTimeVisitor) } // https://serde.rs/string-or-struct.html fn string_or_struct<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de> + FromStr, D: Deserializer<'de>, { struct StringOrStruct(PhantomData T>); impl<'de, T> Visitor<'de> for StringOrStruct where T: Deserialize<'de> + FromStr, { type Value = T; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str(self, v: &str) -> Result where E: de::Error, { Ok(FromStr::from_str(v).unwrap()) } fn visit_map(self, map: A) -> Result where A: de::MapAccess<'de>, { // wizardry (check comment in link) Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) } } deserializer.deserialize_any(StringOrStruct(PhantomData)) } // adapted from above fn string_or_array<'de, T, D>(deserializer: D) -> Result, D::Error> where T: Deserialize<'de> + FromStr, D: Deserializer<'de>, { struct StringOrVec(PhantomData T>); impl<'de, T> Visitor<'de> for StringOrVec where T: Deserialize<'de> + FromStr, { type Value = Vec; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or array") } fn visit_str(self, v: &str) -> Result where E: de::Error, { Ok(vec![FromStr::from_str(v).unwrap()]) } fn visit_seq(self, seq: A) -> Result where A: SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) } } deserializer.deserialize_any(StringOrVec(PhantomData)) } #[cfg(test)] mod tests { use std::fs; use super::*; #[test] fn test_it() { let s = fs::read_to_string("./test_stuff/versions/1.7.10.json"); let arg: CompleteVersion = serde_json::from_str(s.unwrap().as_str()).unwrap(); dbg!(arg); } #[test] fn test_it2() { let s = fs::read_to_string("./test_stuff/version_manifest_v2.json"); 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", None), String::from("group:artifact")); assert_eq!(canonicalize_library_name("group:artifact:version:specifier", None), String::from("group:artifact:specifier")); assert_eq!(canonicalize_library_name("not_enough:fields", None), String::from("not_enough:fields")); assert_eq!(canonicalize_library_name("word", None), String::from("word")); assert_eq!(canonicalize_library_name("", None), String::from("")); assert_eq!(canonicalize_library_name("group:artifact:version", Some("suffix")), String::from("group:artifact:suffix")); } }