uv_distribution_types/
hash.rs1use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, Hashes};
2use uv_redacted::SafeUrl;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ArchiveHashPolicy<'a> {
7 None,
9 Generate,
11 Any(&'a [HashDigest]),
15 All(&'a [HashDigest]),
18}
19
20impl<'a> ArchiveHashPolicy<'a> {
21 #[must_use]
26 pub fn with_index_hashes(self, hashes: &'a [HashDigest]) -> Self {
27 match self {
28 Self::Any(_) | Self::All(_) => self,
29 Self::None | Self::Generate => {
30 if hashes.is_empty() {
31 self
32 } else {
33 Self::All(hashes)
34 }
35 }
36 }
37 }
38
39 pub fn is_none(&self) -> bool {
41 matches!(self, Self::None)
42 }
43
44 pub fn requires_validation(&self) -> bool {
46 matches!(self, Self::Any(_) | Self::All(_))
47 }
48
49 pub fn algorithms(&self) -> Vec<HashAlgorithm> {
51 match self {
52 Self::None => vec![],
53 Self::Generate => vec![HashAlgorithm::Sha256],
54 Self::Any(hashes) | Self::All(hashes) => {
55 let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>();
56 algorithms.sort();
57 algorithms.dedup();
58 algorithms
59 }
60 }
61 }
62
63 pub fn digests(&self) -> &[HashDigest] {
65 match self {
66 Self::None => &[],
67 Self::Generate => &[],
68 Self::Any(hashes) | Self::All(hashes) => hashes,
69 }
70 }
71
72 pub fn matches(&self, hashes: &[HashDigest]) -> bool {
74 match self {
75 Self::None => true,
76 Self::Generate => hashes
77 .iter()
78 .any(|hash| hash.algorithm() == HashAlgorithm::Sha256),
79 Self::Any(required) => {
80 !required.is_empty() && hashes.iter().any(|hash| required.contains(hash))
81 }
82 Self::All(required) => {
83 !required.is_empty() && required.iter().all(|hash| hashes.contains(hash))
84 }
85 }
86 }
87
88 fn has_required_algorithms(&self, hashes: &[HashDigest]) -> bool {
90 match self {
91 Self::None => true,
92 Self::Generate => hashes
93 .iter()
94 .any(|hash| hash.algorithm() == HashAlgorithm::Sha256),
95 Self::Any(required) => {
96 !required.is_empty()
97 && required
98 .iter()
99 .map(HashDigest::algorithm)
100 .any(|algorithm| hashes.iter().any(|hash| hash.algorithm() == algorithm))
101 }
102 Self::All(required) => {
103 !required.is_empty()
104 && required
105 .iter()
106 .map(HashDigest::algorithm)
107 .all(|algorithm| hashes.iter().any(|hash| hash.algorithm() == algorithm))
108 }
109 }
110 }
111}
112
113#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
127pub struct MetadataHashPolicy<'a> {
128 pub collection: HashCollection,
130 pub validation: HashValidation<'a>,
132}
133
134#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
136pub enum HashValidation<'a> {
137 #[default]
139 None,
140 Any(&'a [HashDigest]),
142 All(&'a [HashDigest]),
144}
145
146impl<'a> From<HashValidation<'a>> for ArchiveHashPolicy<'a> {
147 fn from(validation: HashValidation<'a>) -> Self {
148 match validation {
149 HashValidation::None => Self::None,
150 HashValidation::Any(hashes) => Self::Any(hashes),
151 HashValidation::All(hashes) => Self::All(hashes),
152 }
153 }
154}
155
156#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
160pub enum HashCollection {
161 #[default]
163 None,
164 Url,
166 All,
168}
169
170pub fn parse_url_hashes(url: &SafeUrl) -> Option<HashDigests> {
172 let hashes = url
173 .fragment()?
174 .split('&')
175 .find_map(|fragment| Hashes::parse_fragment(fragment).ok())?;
176 let hashes = HashDigests::from(hashes);
177 let contains_md5 = hashes.iter().any(|hash| matches!(hash, HashDigest::Md5(_)));
178 (!contains_md5).then_some(hashes)
179}
180
181pub trait Hashed {
182 fn hashes(&self) -> &[HashDigest];
184
185 fn satisfies(&self, hashes: ArchiveHashPolicy) -> bool {
187 hashes.matches(self.hashes())
188 }
189
190 fn has_digests(&self, hashes: ArchiveHashPolicy) -> bool {
192 hashes.has_required_algorithms(self.hashes())
193 }
194}
195
196impl Hashed for Vec<HashDigest> {
197 fn hashes(&self) -> &[HashDigest] {
198 self
199 }
200}
201
202impl Hashed for &[HashDigest] {
203 fn hashes(&self) -> &[HashDigest] {
204 self
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use std::str::FromStr;
211
212 use uv_pypi_types::{HashDigest, HashError};
213
214 use super::ArchiveHashPolicy;
215
216 #[test]
219 fn index_hashes_preserve_empty_required_policies() -> Result<(), HashError> {
220 let index_hashes = [HashDigest::from_str(
221 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
222 )?];
223 assert_eq!(
224 ArchiveHashPolicy::Any(&[]).with_index_hashes(&index_hashes),
225 ArchiveHashPolicy::Any(&[])
226 );
227 assert_eq!(
228 ArchiveHashPolicy::All(&[]).with_index_hashes(&index_hashes),
229 ArchiveHashPolicy::All(&[])
230 );
231
232 Ok(())
233 }
234
235 #[test]
236 fn validate_all_requires_every_digest() {
237 let sha256 = HashDigest::from_str(
238 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
239 )
240 .unwrap();
241 let sha512 = HashDigest::from_str(
242 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
243 )
244 .unwrap();
245 let wrong_sha512 = HashDigest::from_str(
246 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
247 )
248 .unwrap();
249
250 let policy = ArchiveHashPolicy::All(&[sha256.clone(), sha512.clone()]);
251 assert!(policy.matches(&[sha256.clone(), sha512]));
252 assert!(!policy.matches(std::slice::from_ref(&sha256)));
253 assert!(!policy.matches(&[sha256, wrong_sha512]));
254 }
255
256 #[test]
257 fn validate_any_requires_one_digest() {
258 let sha256 = HashDigest::from_str(
259 "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
260 )
261 .unwrap();
262 let sha512 = HashDigest::from_str(
263 "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
264 )
265 .unwrap();
266 let wrong_sha512 = HashDigest::from_str(
267 "sha512:e30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
268 )
269 .unwrap();
270
271 let policy = ArchiveHashPolicy::Any(&[sha256.clone(), sha512]);
272 assert!(policy.matches(&[sha256]));
273 assert!(!policy.matches(&[wrong_sha512]));
274 }
275}