diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index acdc3b84e..404564290 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -1,6 +1,7 @@ use crate::{PackageRecord, VersionSpec}; +use rattler_digest::{serde::SerializableHash, Md5Hash, Sha256Hash}; use serde::Serialize; -use serde_with::skip_serializing_none; +use serde_with::{serde_as, skip_serializing_none}; use std::fmt::{Debug, Display, Formatter}; pub mod matcher; @@ -109,6 +110,7 @@ use matcher::StringMatcher; /// /// Alternatively, an exact spec is given by `*[sha256=01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b]`. #[skip_serializing_none] +#[serde_as] #[derive(Debug, Default, Clone, Serialize, Eq, PartialEq)] pub struct MatchSpec { /// The name of the package @@ -127,6 +129,12 @@ pub struct MatchSpec { pub subdir: Option, /// The namespace of the package (currently not used) pub namespace: Option, + /// The md5 hash of the package + #[serde_as(as = "Option>")] + pub md5: Option, + /// The sha256 hash of the package + #[serde_as(as = "Option>")] + pub sha256: Option, } impl Display for MatchSpec { @@ -140,25 +148,37 @@ impl Display for MatchSpec { write!(f, "/{}", subdir)?; } + match &self.name { + Some(name) => write!(f, "{name}")?, + None => write!(f, "*")?, + } + if let Some(namespace) = &self.namespace { write!(f, ":{}:", namespace)?; } else if self.channel.is_some() || self.subdir.is_some() { write!(f, "::")?; } - match &self.name { - Some(name) => write!(f, "{name}")?, - None => write!(f, "*")?, + if let Some(version) = &self.version { + write!(f, " {}", version)?; } - match &self.version { - Some(version) => write!(f, " {version}")?, - None => (), + if let Some(build) = &self.build { + write!(f, " {}", build)?; + } + + let mut keys = Vec::new(); + + if let Some(md5) = &self.md5 { + keys.push(format!("md5={md5:x}")); + } + + if let Some(sha256) = &self.sha256 { + keys.push(format!("sha256={sha256:x}")); } - match &self.build { - Some(build) => write!(f, " {build}")?, - None => (), + if !keys.is_empty() { + write!(f, "[{}]", keys.join(", "))?; } Ok(()) @@ -186,12 +206,25 @@ impl MatchSpec { } } + if let Some(md5_spec) = self.md5.as_ref() { + if Some(md5_spec) != record.md5.as_ref() { + return false; + } + } + + if let Some(sha256_spec) = self.sha256.as_ref() { + if Some(sha256_spec) != record.sha256.as_ref() { + return false; + } + } + true } } /// Similar to a [`MatchSpec`] but does not include the package name. This is useful in places /// where the package name is already known (e.g. `foo = "3.4.1 *cuda"`) +#[serde_as] #[skip_serializing_none] #[derive(Debug, Default, Clone, Serialize, Eq, PartialEq)] pub struct NamelessMatchSpec { @@ -209,6 +242,12 @@ pub struct NamelessMatchSpec { pub subdir: Option, /// The namespace of the package (currently not used) pub namespace: Option, + /// The md5 hash of the package + #[serde_as(as = "Option>")] + pub md5: Option, + /// The sha256 hash of the package + #[serde_as(as = "Option>")] + pub sha256: Option, } impl NamelessMatchSpec { @@ -226,6 +265,18 @@ impl NamelessMatchSpec { } } + if let Some(md5_spec) = self.md5.as_ref() { + if Some(md5_spec) != record.md5.as_ref() { + return false; + } + } + + if let Some(sha256_spec) = self.sha256.as_ref() { + if Some(sha256_spec) != record.sha256.as_ref() { + return false; + } + } + true } } @@ -237,12 +288,23 @@ impl Display for NamelessMatchSpec { None => write!(f, "*")?, } - match &self.build { - Some(build) => write!(f, " {build}")?, - None => (), + if let Some(build) = &self.build { + write!(f, " {}", build)?; + } + + let mut keys = Vec::new(); + + if let Some(md5) = &self.md5 { + keys.push(format!("md5={md5:x}")); + } + + if let Some(sha256) = &self.sha256 { + keys.push(format!("sha256={sha256:x}")); } - // TODO: Add any additional properties as bracket arguments (e.g. `[channel=..]`) + if !keys.is_empty() { + write!(f, "[{}]", keys.join(", "))?; + } Ok(()) } @@ -258,6 +320,8 @@ impl From for NamelessMatchSpec { channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, + md5: spec.md5, + sha256: spec.sha256, } } } @@ -274,6 +338,68 @@ impl MatchSpec { channel: spec.channel, subdir: spec.subdir, namespace: spec.namespace, + md5: spec.md5, + sha256: spec.sha256, } } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use rattler_digest::{parse_digest_from_hex, Md5, Sha256}; + + use crate::{MatchSpec, NamelessMatchSpec, PackageRecord, Version}; + + #[test] + fn test_matchspec_format_eq() { + let spec = MatchSpec::from_str("mamba[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97, md5=dede6252c964db3f3e41c7d30d07f6bf]").unwrap(); + let spec_as_string = spec.to_string(); + let rebuild_spec = MatchSpec::from_str(&spec_as_string).unwrap(); + + assert_eq!(spec, rebuild_spec) + } + + #[test] + fn test_nameless_matchspec_format_eq() { + let spec = NamelessMatchSpec::from_str("*[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97, md5=dede6252c964db3f3e41c7d30d07f6bf]").unwrap(); + let spec_as_string = spec.to_string(); + let rebuild_spec = NamelessMatchSpec::from_str(&spec_as_string).unwrap(); + + assert_eq!(spec, rebuild_spec) + } + + #[test] + fn test_digest_match() { + let record = PackageRecord { + name: "mamba".to_string(), + version: Version::from_str("1.0").unwrap(), + sha256: parse_digest_from_hex::( + "f44c4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97", + ), + md5: parse_digest_from_hex::("dede6252c964db3f3e41c7d30d07f6bf"), + ..PackageRecord::default() + }; + + let spec = MatchSpec::from_str("mamba[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]").unwrap(); + assert!(!spec.matches(&record)); + + let spec = MatchSpec::from_str("mamba[version==1.0, sha256=f44c4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]").unwrap(); + assert!(spec.matches(&record)); + + let spec = MatchSpec::from_str("mamba[version==1.0, md5=aaaa6252c964db3f3e41c7d30d07f6bf]") + .unwrap(); + assert!(!spec.matches(&record)); + + let spec = MatchSpec::from_str("mamba[version==1.0, md5=dede6252c964db3f3e41c7d30d07f6bf]") + .unwrap(); + assert!(spec.matches(&record)); + + let spec = MatchSpec::from_str("mamba[version==1.0, md5=dede6252c964db3f3e41c7d30d07f6bf, sha256=f44c4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]").unwrap(); + assert!(spec.matches(&record)); + + let spec = MatchSpec::from_str("mamba[version==1.0, md5=dede6252c964db3f3e41c7d30d07f6bf, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97]").unwrap(); + assert!(!spec.matches(&record)); + } +} diff --git a/crates/rattler_conda_types/src/match_spec/parse.rs b/crates/rattler_conda_types/src/match_spec/parse.rs index 1d5644f7a..5b72fbcc3 100644 --- a/crates/rattler_conda_types/src/match_spec/parse.rs +++ b/crates/rattler_conda_types/src/match_spec/parse.rs @@ -12,6 +12,7 @@ use nom::error::{context, ContextError, ParseError}; use nom::multi::{separated_list0, separated_list1}; use nom::sequence::{delimited, preceded, separated_pair, terminated}; use nom::{Finish, IResult}; +use rattler_digest::{parse_digest_from_hex, Md5, Sha256}; use smallvec::SmallVec; use std::borrow::Cow; use std::num::ParseIntError; @@ -54,6 +55,9 @@ pub enum ParseMatchSpecError { #[error("invalid build number: {0}")] InvalidBuildNumber(#[from] ParseIntError), + + #[error("Unable to parse hash digest from hex")] + InvalidHashDigest, } impl FromStr for MatchSpec { @@ -183,6 +187,18 @@ fn parse_bracket_vec_into_components( "version" => match_spec.version = Some(VersionSpec::from_str(value)?), "build" => match_spec.build = Some(StringMatcher::from_str(value)?), "build_number" => match_spec.build_number = Some(value.parse()?), + "sha256" => { + match_spec.sha256 = Some( + parse_digest_from_hex::(value) + .ok_or(ParseMatchSpecError::InvalidHashDigest)?, + ) + } + "md5" => { + match_spec.md5 = Some( + parse_digest_from_hex::(value) + .ok_or(ParseMatchSpecError::InvalidHashDigest)?, + ) + } "fn" => match_spec.file_name = Some(value.to_string()), _ => Err(ParseMatchSpecError::InvalidBracketKey(key.to_owned()))?, } @@ -425,6 +441,7 @@ fn parse(input: &str) -> Result { #[cfg(test)] mod tests { + use rattler_digest::{parse_digest_from_hex, Md5, Sha256}; use serde::Serialize; use std::collections::BTreeMap; use std::str::FromStr; @@ -541,6 +558,33 @@ mod tests { assert_eq!(spec.channel, Some("conda-forge".to_string())); } + #[test] + fn test_hash_spec() { + let spec = MatchSpec::from_str("conda-forge::foo[md5=1234567890]"); + assert_eq!(spec, Err(ParseMatchSpecError::InvalidHashDigest)); + + let spec = MatchSpec::from_str("conda-forge::foo[sha256=1234567890]"); + assert_eq!(spec, Err(ParseMatchSpecError::InvalidHashDigest)); + + let spec = MatchSpec::from_str("conda-forge::foo[sha256=315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3]").unwrap(); + assert_eq!( + spec.sha256, + Some( + parse_digest_from_hex::( + "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" + ) + .unwrap() + ) + ); + + let spec = + MatchSpec::from_str("conda-forge::foo[md5=8b1a9953c4611296a827abf8c47804d7]").unwrap(); + assert_eq!( + spec.md5, + Some(parse_digest_from_hex::("8b1a9953c4611296a827abf8c47804d7").unwrap()) + ); + } + #[test] fn test_parse_bracket_list() { assert_eq!( diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index 3da7c4312..76fa71fb5 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -62,7 +62,7 @@ pub struct ChannelInfo { #[serde_as] #[skip_serializing_none] #[sorted] -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)] +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default)] pub struct PackageRecord { /// Optionally the architecture the package supports pub arch: Option, diff --git a/crates/rattler_conda_types/src/version/mod.rs b/crates/rattler_conda_types/src/version/mod.rs index c2f64b19e..69b66aef2 100644 --- a/crates/rattler_conda_types/src/version/mod.rs +++ b/crates/rattler_conda_types/src/version/mod.rs @@ -128,7 +128,7 @@ const LOCAL_VERSION_OFFSET: u8 = 1; /// this problem by appending an underscore to plain version numbers: /// /// 1.0.1_ < 1.0.1a => True # ensure correct ordering for openssl -#[derive(Clone, Eq, Deserialize)] +#[derive(Clone, Eq, Deserialize, Default)] pub struct Version { /// A normed copy of the original version string trimmed and converted to lower case. /// Also dashes are replaced with underscores if the version string does not contain