use uv_pypi_types::{HashAlgorithm, HashDigest};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashPolicy<'a> {
None,
Generate(HashGeneration),
Any(&'a [HashDigest]),
All(&'a [HashDigest]),
}
impl HashPolicy<'_> {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
pub fn requires_validation(&self) -> bool {
matches!(self, Self::Any(_) | Self::All(_))
}
pub fn is_generate(&self, dist: &crate::BuiltDist) -> bool {
match self {
Self::Generate(HashGeneration::Url) => dist.file().is_none(),
Self::Generate(HashGeneration::All) => {
dist.file().is_none_or(|file| file.hashes.is_empty())
}
Self::Any(_) => false,
Self::All(_) => false,
Self::None => false,
}
}
pub fn algorithms(&self) -> Vec<HashAlgorithm> {
match self {
Self::None => vec![],
Self::Generate(_) => vec![HashAlgorithm::Sha256],
Self::Any(hashes) | Self::All(hashes) => {
let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>();
algorithms.sort();
algorithms.dedup();
algorithms
}
}
}
pub fn digests(&self) -> &[HashDigest] {
match self {
Self::None => &[],
Self::Generate(_) => &[],
Self::Any(hashes) | Self::All(hashes) => hashes,
}
}
pub fn matches(&self, hashes: &[HashDigest]) -> bool {
match self {
Self::None => true,
Self::Generate(_) => hashes
.iter()
.any(|hash| hash.algorithm == HashAlgorithm::Sha256),
Self::Any(required) => {
!required.is_empty() && hashes.iter().any(|hash| required.contains(hash))
}
Self::All(required) => {
!required.is_empty() && required.iter().all(|hash| hashes.contains(hash))
}
}
}
pub fn has_required_algorithms(&self, hashes: &[HashDigest]) -> bool {
match self {
Self::None => true,
Self::Generate(_) => hashes
.iter()
.any(|hash| hash.algorithm == HashAlgorithm::Sha256),
Self::Any(required) => {
!required.is_empty()
&& required
.iter()
.map(HashDigest::algorithm)
.any(|algorithm| hashes.iter().any(|hash| hash.algorithm == algorithm))
}
Self::All(required) => {
!required.is_empty()
&& required
.iter()
.map(HashDigest::algorithm)
.all(|algorithm| hashes.iter().any(|hash| hash.algorithm == algorithm))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashGeneration {
Url,
All,
}
pub trait Hashed {
fn hashes(&self) -> &[HashDigest];
fn satisfies(&self, hashes: HashPolicy) -> bool {
hashes.matches(self.hashes())
}
fn has_digests(&self, hashes: HashPolicy) -> bool {
hashes.has_required_algorithms(self.hashes())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use uv_pypi_types::HashDigest;
use super::HashPolicy;
#[test]
fn validate_all_requires_every_digest() {
let sha256 = HashDigest::from_str(
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
)
.unwrap();
let sha512 = HashDigest::from_str(
"sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
)
.unwrap();
let wrong_sha512 = HashDigest::from_str(
"sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
)
.unwrap();
let policy = HashPolicy::All(&[sha256.clone(), sha512.clone()]);
assert!(policy.matches(&[sha256.clone(), sha512]));
assert!(!policy.matches(std::slice::from_ref(&sha256)));
assert!(!policy.matches(&[sha256, wrong_sha512]));
}
#[test]
fn validate_any_requires_one_digest() {
let sha256 = HashDigest::from_str(
"sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
)
.unwrap();
let sha512 = HashDigest::from_str(
"sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
)
.unwrap();
let wrong_sha512 = HashDigest::from_str(
"sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
)
.unwrap();
let policy = HashPolicy::Any(&[sha256.clone(), sha512]);
assert!(policy.matches(&[sha256]));
assert!(!policy.matches(&[wrong_sha512]));
}
}