uv_distribution_types/
hash.rs1use uv_pypi_types::{HashAlgorithm, HashDigest};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum HashPolicy<'a> {
5 None,
7 Generate(HashGeneration),
9 Any(&'a [HashDigest]),
13 All(&'a [HashDigest]),
16}
17
18impl HashPolicy<'_> {
19 pub fn is_none(&self) -> bool {
21 matches!(self, Self::None)
22 }
23
24 pub fn requires_validation(&self) -> bool {
26 matches!(self, Self::Any(_) | Self::All(_))
27 }
28
29 pub fn is_generate(&self, dist: &crate::BuiltDist) -> bool {
31 match self {
32 Self::Generate(HashGeneration::Url) => dist.file().is_none(),
33 Self::Generate(HashGeneration::All) => {
34 dist.file().is_none_or(|file| file.hashes.is_empty())
35 }
36 Self::Any(_) => false,
37 Self::All(_) => false,
38 Self::None => false,
39 }
40 }
41
42 pub fn algorithms(&self) -> Vec<HashAlgorithm> {
44 match self {
45 Self::None => vec![],
46 Self::Generate(_) => vec![HashAlgorithm::Sha256],
47 Self::Any(hashes) | Self::All(hashes) => {
48 let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>();
49 algorithms.sort();
50 algorithms.dedup();
51 algorithms
52 }
53 }
54 }
55
56 pub fn digests(&self) -> &[HashDigest] {
58 match self {
59 Self::None => &[],
60 Self::Generate(_) => &[],
61 Self::Any(hashes) | Self::All(hashes) => hashes,
62 }
63 }
64
65 pub fn matches(&self, hashes: &[HashDigest]) -> bool {
67 match self {
68 Self::None => true,
69 Self::Generate(_) => hashes
70 .iter()
71 .any(|hash| hash.algorithm == HashAlgorithm::Sha256),
72 Self::Any(required) => {
73 !required.is_empty() && hashes.iter().any(|hash| required.contains(hash))
74 }
75 Self::All(required) => {
76 !required.is_empty() && required.iter().all(|hash| hashes.contains(hash))
77 }
78 }
79 }
80
81 pub fn has_required_algorithms(&self, hashes: &[HashDigest]) -> bool {
83 match self {
84 Self::None => true,
85 Self::Generate(_) => hashes
86 .iter()
87 .any(|hash| hash.algorithm == HashAlgorithm::Sha256),
88 Self::Any(required) => {
89 !required.is_empty()
90 && required
91 .iter()
92 .map(HashDigest::algorithm)
93 .any(|algorithm| hashes.iter().any(|hash| hash.algorithm == algorithm))
94 }
95 Self::All(required) => {
96 !required.is_empty()
97 && required
98 .iter()
99 .map(HashDigest::algorithm)
100 .all(|algorithm| hashes.iter().any(|hash| hash.algorithm == algorithm))
101 }
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum HashGeneration {
109 Url,
111 All,
114}
115
116pub trait Hashed {
117 fn hashes(&self) -> &[HashDigest];
119
120 fn satisfies(&self, hashes: HashPolicy) -> bool {
122 hashes.matches(self.hashes())
123 }
124
125 fn has_digests(&self, hashes: HashPolicy) -> bool {
127 hashes.has_required_algorithms(self.hashes())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use std::str::FromStr;
134
135 use uv_pypi_types::HashDigest;
136
137 use super::HashPolicy;
138
139 #[test]
140 fn validate_all_requires_every_digest() {
141 let sha256 = HashDigest::from_str(
142 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
143 )
144 .unwrap();
145 let sha512 = HashDigest::from_str(
146 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
147 )
148 .unwrap();
149 let wrong_sha512 = HashDigest::from_str(
150 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
151 )
152 .unwrap();
153
154 let policy = HashPolicy::All(&[sha256.clone(), sha512.clone()]);
155 assert!(policy.matches(&[sha256.clone(), sha512]));
156 assert!(!policy.matches(std::slice::from_ref(&sha256)));
157 assert!(!policy.matches(&[sha256, wrong_sha512]));
158 }
159
160 #[test]
161 fn validate_any_requires_one_digest() {
162 let sha256 = HashDigest::from_str(
163 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
164 )
165 .unwrap();
166 let sha512 = HashDigest::from_str(
167 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
168 )
169 .unwrap();
170 let wrong_sha512 = HashDigest::from_str(
171 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
172 )
173 .unwrap();
174
175 let policy = HashPolicy::Any(&[sha256.clone(), sha512]);
176 assert!(policy.matches(&[sha256]));
177 assert!(!policy.matches(&[wrong_sha512]));
178 }
179}