use core::fmt; use std::{collections::BTreeMap, convert::Infallible, marker::PhantomData, ops::Deref, str::FromStr}; use chrono::{DateTime, Utc}; use regex::Regex; use serde::{de::{self, Visitor}, Deserialize, Deserializer}; pub mod manifest; use manifest::*; #[derive(Deserialize, Debug, Clone, Copy)] #[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)] 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 std::fmt::Formatter) -> std::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)] pub struct OSRestriction { name: Option, version: Option, arch: Option } impl OSRestriction { pub fn get_name(&self) -> Option { self.name } pub fn get_version(&self) -> Option<&Regex> { self.version.as_deref() } pub fn get_arch(&self) -> Option<&Regex> { self.arch.as_deref() } } #[derive(Deserialize, Debug)] pub struct CompatibilityRule { action: RuleAction, features: Option>, os: Option } impl CompatibilityRule { pub fn get_action(&self) -> RuleAction { self.action } pub fn get_os(&self) -> Option<&OSRestriction> { self.os.as_ref() } pub fn get_features(&self) -> Option<&BTreeMap> { self.features.as_ref() } pub fn features_match(&self, checker: fn(&str) -> bool) -> bool { if let Some(m) = self.features.as_ref() { for (feat, expect) in m { if checker(feat) != *expect { return false; } } } true } } #[derive(Deserialize, Debug)] pub struct Argument { #[serde(default)] rules: Option>, #[serde(default)] #[serde(deserialize_with = "string_or_array")] value: Vec } impl Argument { fn get_rules(&self) -> Option<&Vec> { self.rules.as_ref() } fn get_value(&self) -> &Vec { &self.value } } #[derive(Debug)] 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)] pub struct Arguments { game: Option>, jvm: Option> } impl Arguments { fn get_game(&self) -> Option<&Vec> { self.game.as_ref() } fn get_jvm(&self) -> Option<&Vec> { self.jvm.as_ref() } } #[derive(Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum DownloadType { Client, ClientMappings, Server, ServerMappings, WindowsServer } #[derive(Deserialize, Debug)] pub struct DownloadInfo { sha1: Option, size: Option, total_size: Option, // available for asset index url: Option, // may not be present for libraries id: Option, path: Option } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct JavaVersionInfo { component: String, major_version: u32 } #[derive(Deserialize, Debug)] pub struct LibraryDownloads { artifact: Option, classifiers: Option> } #[derive(Deserialize, Debug)] pub struct LibraryExtractRule { #[serde(default)] exclude: Vec } #[derive(Deserialize, Debug)] pub struct Library { downloads: Option, name: String, extract: Option, natives: Option>, rules: Option>, url: Option // old format } #[derive(Deserialize, Debug)] pub struct ClientLogging { argument: String, r#type: String, file: DownloadInfo } #[derive(Deserialize, Debug)] pub struct Logging { client: ClientLogging // other fields unknown } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct CompleteVersion { arguments: Option, minecraft_arguments: Option, asset_index: Option, assets: Option, compliance_level: Option, java_version: Option, #[serde(default)] downloads: BTreeMap, #[serde(default)] libraries: Vec, id: String, jar: Option, // used as the jar filename if specified? (no longer used officially) logging: Option, main_class: Option, minimum_launcher_version: Option, release_time: Option>, time: Option>, #[serde(rename = "type")] version_type: Option, compatibility_rules: Option>, // incompatibility_reason: Option, // message shown when compatibility rules fail for this version 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.) */ } // 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 std::fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { Ok(FromStr::from_str(v).unwrap()) } fn visit_map(self, map: A) -> Result where A: serde::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 std::fmt::Formatter) -> std::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: de::SeqAccess<'de>, { Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq)) } } deserializer.deserialize_any(StringOrVec(PhantomData)) } #[cfg(test)] mod tests { use std::fs; use serde_json::json; 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); } }