use core::fmt;
use miden_core::{LexicographicWord, Word};
use pubgrub::VersionSet as _;
use smallvec::{SmallVec, smallvec};
use super::pubgrub_compat::SemverPubgrub;
use crate::{SemVer, Version, VersionReq, VersionRequirement};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionSet {
range: SemverPubgrub,
digests: SmallVec<[LexicographicWord; 1]>,
}
#[derive(Debug)]
pub enum VersionSetFilter<'a> {
Any,
Digest(&'a [LexicographicWord]),
}
impl VersionSetFilter<'_> {
pub fn matches(&self, version: &Version) -> bool {
match self {
Self::Any => true,
Self::Digest(digests) => {
version.digest.as_ref().is_some_and(|digest| digests.contains(digest))
},
}
}
}
impl VersionSet {
pub fn range(&self) -> &SemverPubgrub {
&self.range
}
pub fn filter(&self) -> VersionSetFilter<'_> {
if self.digests.is_empty() {
VersionSetFilter::Any
} else {
VersionSetFilter::Digest(&self.digests)
}
}
pub fn is_digest_only(&self) -> bool {
!self.digests.is_empty() && self.range == SemverPubgrub::full()
}
pub fn from_available_digest<'a>(
digest: Word,
versions: impl IntoIterator<Item = &'a Version>,
) -> Self {
let digest = LexicographicWord::new(digest);
let mut range = SemverPubgrub::empty();
let mut matched = false;
for version in versions {
if version.digest.as_ref() == Some(&digest) {
range = range.union(&SemverPubgrub::singleton(version.version.clone()));
matched = true;
}
}
if !matched {
Self::empty()
} else {
Self { range, digests: smallvec![digest] }
}
}
}
impl Default for VersionSet {
#[inline]
fn default() -> Self {
Self::empty()
}
}
impl From<Word> for VersionSet {
fn from(value: Word) -> Self {
Self {
range: SemverPubgrub::full(),
digests: smallvec![LexicographicWord::new(value)],
}
}
}
impl From<SemVer> for VersionSet {
fn from(value: SemVer) -> Self {
Self {
range: SemverPubgrub::singleton(value),
digests: smallvec![],
}
}
}
impl From<Version> for VersionSet {
fn from(value: Version) -> Self {
let Version { version, digest } = value;
Self {
range: SemverPubgrub::singleton(version),
digests: SmallVec::from_iter(digest),
}
}
}
impl From<VersionReq> for VersionSet {
fn from(value: VersionReq) -> Self {
Self {
range: SemverPubgrub::from(&value),
digests: smallvec![],
}
}
}
impl From<&VersionReq> for VersionSet {
fn from(value: &VersionReq) -> Self {
Self {
range: SemverPubgrub::from(value),
digests: smallvec![],
}
}
}
impl From<VersionRequirement> for VersionSet {
fn from(value: VersionRequirement) -> Self {
match value {
VersionRequirement::Digest(digest) => Self::from(digest.into_inner()),
VersionRequirement::Semantic(req) => Self::from(req.inner()),
VersionRequirement::Exact(version) => Self::from(version),
}
}
}
impl From<SemverPubgrub> for VersionSet {
fn from(range: SemverPubgrub) -> Self {
Self { range, digests: smallvec![] }
}
}
impl fmt::Display for VersionSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.digests.as_slice() {
[] => write!(f, "{}", &self.range),
[digest] => write!(f, "{} in {}", digest.inner(), &self.range),
digests => {
f.write_str("any of ")?;
for (i, digest) in digests.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{}", &digest.inner())?;
}
write!(f, " in {}", &self.range)
},
}
}
}
impl pubgrub::VersionSet for VersionSet {
type V = Version;
fn empty() -> Self {
Self::from(SemverPubgrub::empty())
}
fn singleton(v: Self::V) -> Self {
Self::from(v)
}
fn full() -> Self {
Self::from(SemverPubgrub::full())
}
fn complement(&self) -> Self {
Self::from(self.range.complement())
}
fn intersection(&self, other: &Self) -> Self {
use alloc::collections::BTreeSet;
if self.digests.is_empty() && other.digests.is_empty() {
return Self::from(self.range.intersection(&other.range));
}
let digests = match (self.digests.is_empty(), other.digests.is_empty()) {
(true, false) => other.digests.clone(),
(false, true) => self.digests.clone(),
(false, false) => {
let ldigests = BTreeSet::from_iter(self.digests.iter());
let rdigests = BTreeSet::from_iter(other.digests.iter());
ldigests.intersection(&rdigests).map(|d| **d).collect::<SmallVec<[_; _]>>()
},
(true, true) => SmallVec::new(),
};
if digests.is_empty() && !self.digests.is_empty() && !other.digests.is_empty() {
return Self::empty();
}
let range = self.range.intersection(&other.range);
if range.is_empty() {
return Self::empty();
}
Self { range, digests }
}
fn contains(&self, v: &Self::V) -> bool {
if self.digests.is_empty() {
self.range.contains(&v.version)
} else if let Some(digest) = v.digest.as_ref() {
self.digests.contains(digest) && self.range.contains(&v.version)
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use miden_core::crypto::hash::Rpo256;
use super::*;
#[test]
fn version_set_contains_requires_both_digest_and_semver() {
let digest = Rpo256::hash(b"same-digest");
let set = VersionSet::from(Version::new("1.0.0".parse().unwrap(), digest));
let candidate = Version::new("9.9.9".parse().unwrap(), digest);
assert!(
!<VersionSet as pubgrub::VersionSet>::contains(&set, &candidate),
"set {set} unexpectedly contains {candidate}"
);
}
#[test]
fn digest_and_semantic_intersection_preserves_both_constraints() {
let digest = Rpo256::hash(b"shared-digest");
let set = <VersionSet as pubgrub::VersionSet>::intersection(
&VersionSet::from(digest),
&VersionSet::from("^1.0.0".parse::<VersionReq>().unwrap()),
);
let matching = Version::new("1.2.3".parse().unwrap(), digest);
let wrong_semver = Version::new("2.0.0".parse().unwrap(), digest);
let wrong_digest = Version::new("1.2.3".parse().unwrap(), Rpo256::hash(b"other-digest"));
assert!(
<VersionSet as pubgrub::VersionSet>::contains(&set, &matching),
"set {set} should contain {matching}"
);
assert!(
!<VersionSet as pubgrub::VersionSet>::contains(&set, &wrong_semver),
"set {set} unexpectedly contains {wrong_semver}"
);
assert!(
!<VersionSet as pubgrub::VersionSet>::contains(&set, &wrong_digest),
"set {set} unexpectedly contains {wrong_digest}"
);
}
#[test]
fn semantic_range_contains_digest_qualified_versions() {
let set = VersionSet::from("^1.0.0".parse::<VersionReq>().unwrap());
let digest = Rpo256::hash(b"same-version-digest");
let candidate = Version::new("1.2.3".parse().unwrap(), digest);
assert!(
<VersionSet as pubgrub::VersionSet>::contains(&set, &candidate),
"semantic set {set} should contain {candidate}"
);
}
#[test]
fn empty_digest_constrained_intersection_stays_empty() {
let digest = Rpo256::hash(b"revived-digest");
let impossible = <VersionSet as pubgrub::VersionSet>::intersection(
&VersionSet::from(Version::new("1.0.0".parse().unwrap(), digest)),
&VersionSet::from("=2.0.0".parse::<VersionReq>().unwrap()),
);
let revived = <VersionSet as pubgrub::VersionSet>::intersection(
&impossible,
&VersionSet::from("^3.0.0".parse::<VersionReq>().unwrap()),
);
let candidate = Version::new("3.1.0".parse().unwrap(), digest);
assert_eq!(revived, VersionSet::empty());
assert!(
!<VersionSet as pubgrub::VersionSet>::contains(&revived, &candidate),
"set {revived} unexpectedly revived impossible candidate {candidate}"
);
}
}