From 1f5693c5531fa7ddf7bfdb8e27dc48d9765b97ca Mon Sep 17 00:00:00 2001 From: bigfoot547 Date: Sun, 22 Dec 2024 23:48:39 -0600 Subject: when the --- src/launcher.rs | 156 ++------------------------ src/launcher/version.rs | 293 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 2 +- src/util.rs | 68 +++++++++++ src/version.rs | 172 ++++++++++++++++++++++++---- src/version/manifest.rs | 5 +- 7 files changed, 525 insertions(+), 172 deletions(-) create mode 100644 src/launcher/version.rs create mode 100644 src/util.rs (limited to 'src') 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 -} - -#[derive(Serialize, Deserialize, Debug)] -struct RemoteVersionIndex { - versions: BTreeMap -} - -impl Default for RemoteVersionIndex { - fn default() -> Self { - RemoteVersionIndex { - versions: BTreeMap::new() - } - } -} - -struct RemoteVersionList { - versions: HashMap, - index: RemoteVersionIndex -} - -impl RemoteVersionList { - async fn new(home: impl AsRef) -> Result> { - 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 -} - -impl LocalVersionList { - async fn load_version(path: &PathBuf) -> Result> { - serde_json::from_str(tokio::fs::read_to_string(path).await?)?; - } - - async fn load_versions(home: impl AsRef, skip: impl Fn(&str) -> bool) -> Result> { - 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, - local: LocalVersionList +pub struct Launcher { + pub versions: VersionList } -impl VersionList { - pub async fn online(home: impl AsRef) -> Result> { - 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) -> Result> { - let local = LocalVersionList::load_versions(home, |_| false).await?; - - Ok(VersionList { - offline: true, - remote: None, - local - }) - } -} +impl Launcher { + pub async fn new() -> Result> { + 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, + latest: LatestVersions +} + +impl RemoteVersionList { + async fn new() -> Result> { + 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> { + // 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::, _>(|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 +} + +#[derive(Debug)] +enum LocalVersionError { + Sha1Mismatch { exp: Sha1Digest, got: Sha1Digest }, + VersionMismatch { fname: String, json: String }, + Unknown(Box) +} + +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 { + // 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> { + 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, + 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>> From> for VersionResult<'a> { + fn from(value: Option) -> Self { + value.map_or(VersionResult::None, |v| v.into()) + } +} + +pub enum VersionResolveError { + InheritanceLoop, + MissingVersion(String), + Unknown(Box) +} + +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> { + 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> { + 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> { + 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, VersionResolveError> { + let mut seen: HashSet = 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::()) + } +} + +impl Display for Sha1Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.encode_hex::()) + } +} + +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(self, v: &str) -> Result + where + E: Error, + { + Sha1DigestBytes::from_hex(v).map_err(|e| E::custom(e)).map(Sha1Digest) + } +} + +impl<'a> Deserialize<'a> for Sha1Digest { + fn deserialize(deserializer: D) -> Result + 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, @@ -71,7 +74,7 @@ pub struct OSRestriction { pub arch: Option } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct CompatibilityRule { pub action: RuleAction, pub features: Option>, @@ -92,7 +95,7 @@ impl CompatibilityRule { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Argument { #[serde(default)] pub rules: Option>, @@ -102,7 +105,7 @@ pub struct Argument { pub value: Vec } -#[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>, pub jvm: Option> } -#[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, + pub sha1: Option, pub size: Option, pub total_size: Option, // available for asset index pub url: Option, // may not be present for libraries @@ -158,26 +177,26 @@ pub struct DownloadInfo { pub path: Option } -#[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, pub classifiers: Option> } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct LibraryExtractRule { #[serde(default)] pub exclude: Vec } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct Library { pub downloads: Option, pub name: String, @@ -187,7 +206,7 @@ pub struct Library { pub url: Option // 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, @@ -217,8 +236,8 @@ pub struct CompleteVersion { #[serde(default)] pub downloads: BTreeMap, - #[serde(default)] - pub libraries: Vec, + #[serde(default, deserialize_with = "deserialize_libraries")] + pub libraries: HashMap, pub id: String, pub jar: Option, // 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::>() + .join(":") +} + +fn deserialize_libraries<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de> +{ + struct LibrariesVisitor; + + impl<'de> Visitor<'de> for LibrariesVisitor { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of libraries") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut map = HashMap::new(); + + while let Some(lib) = seq.next_element::()? { + 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 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(self, v: &str) -> Result where - E: serde::de::Error, { + E: de::Error, { Ok(FromStr::from_str(v).unwrap()) } fn visit_map(self, map: A) -> Result 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(self, seq: A) -> Result 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, pub release_time: DateTime, - pub sha1: String, + pub sha1: Sha1Digest, pub compliance_level: u32 } -- cgit v1.2.3-70-g09d2