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 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
131impl Hashed for Vec<HashDigest> {
132 fn hashes(&self) -> &[HashDigest] {
133 self
134 }
135}
136
137impl Hashed for &[HashDigest] {
138 fn hashes(&self) -> &[HashDigest] {
139 self
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::str::FromStr;
146
147 use uv_pypi_types::HashDigest;
148
149 use super::HashPolicy;
150
151 #[test]
152 fn validate_all_requires_every_digest() {
153 let sha256 = HashDigest::from_str(
154 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
155 )
156 .unwrap();
157 let sha512 = HashDigest::from_str(
158 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
159 )
160 .unwrap();
161 let wrong_sha512 = HashDigest::from_str(
162 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
163 )
164 .unwrap();
165
166 let policy = HashPolicy::All(&[sha256.clone(), sha512.clone()]);
167 assert!(policy.matches(&[sha256.clone(), sha512]));
168 assert!(!policy.matches(std::slice::from_ref(&sha256)));
169 assert!(!policy.matches(&[sha256, wrong_sha512]));
170 }
171
172 #[test]
173 fn validate_any_requires_one_digest() {
174 let sha256 = HashDigest::from_str(
175 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
176 )
177 .unwrap();
178 let sha512 = HashDigest::from_str(
179 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
180 )
181 .unwrap();
182 let wrong_sha512 = HashDigest::from_str(
183 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
184 )
185 .unwrap();
186
187 let policy = HashPolicy::Any(&[sha256.clone(), sha512]);
188 assert!(policy.matches(&[sha256]));
189 assert!(!policy.matches(&[wrong_sha512]));
190 }
191}