1use std::str::FromStr;
2use std::sync::Arc;
3
4use rustc_hash::FxHashMap;
5
6use uv_configuration::HashCheckingMode;
7use uv_distribution_types::{
8 DistributionMetadata, HashGeneration, HashPolicy, Name, Requirement, RequirementSource,
9 Resolution, UnresolvedRequirement, VersionId,
10};
11use uv_normalize::PackageName;
12use uv_pep440::Version;
13use uv_pypi_types::{HashDigest, HashDigests, HashError, ResolverMarkerEnvironment};
14use uv_redacted::DisplaySafeUrl;
15
16#[derive(Debug, Default, Clone)]
17pub enum HashStrategy {
18 #[default]
20 None,
21 Generate(HashGeneration),
23 Verify(Arc<FxHashMap<VersionId, Vec<HashDigest>>>),
27 Require(Arc<FxHashMap<VersionId, Vec<HashDigest>>>),
31}
32
33impl HashStrategy {
34 pub fn get<T: DistributionMetadata>(&self, distribution: &T) -> HashPolicy<'_> {
36 match self {
37 Self::None => HashPolicy::None,
38 Self::Generate(mode) => HashPolicy::Generate(*mode),
39 Self::Verify(hashes) => {
40 if let Some(hashes) = hashes.get(&distribution.version_id()) {
41 HashPolicy::Validate(hashes.as_slice())
42 } else {
43 HashPolicy::None
44 }
45 }
46 Self::Require(hashes) => HashPolicy::Validate(
47 hashes
48 .get(&distribution.version_id())
49 .map(Vec::as_slice)
50 .unwrap_or_default(),
51 ),
52 }
53 }
54
55 pub fn get_package(&self, name: &PackageName, version: &Version) -> HashPolicy<'_> {
57 match self {
58 Self::None => HashPolicy::None,
59 Self::Generate(mode) => HashPolicy::Generate(*mode),
60 Self::Verify(hashes) => {
61 if let Some(hashes) =
62 hashes.get(&VersionId::from_registry(name.clone(), version.clone()))
63 {
64 HashPolicy::Validate(hashes.as_slice())
65 } else {
66 HashPolicy::None
67 }
68 }
69 Self::Require(hashes) => HashPolicy::Validate(
70 hashes
71 .get(&VersionId::from_registry(name.clone(), version.clone()))
72 .map(Vec::as_slice)
73 .unwrap_or_default(),
74 ),
75 }
76 }
77
78 pub fn get_url(&self, url: &DisplaySafeUrl) -> HashPolicy<'_> {
80 match self {
81 Self::None => HashPolicy::None,
82 Self::Generate(mode) => HashPolicy::Generate(*mode),
83 Self::Verify(hashes) => {
84 if let Some(hashes) = hashes.get(&VersionId::from_url(url)) {
85 HashPolicy::Validate(hashes.as_slice())
86 } else {
87 HashPolicy::None
88 }
89 }
90 Self::Require(hashes) => HashPolicy::Validate(
91 hashes
92 .get(&VersionId::from_url(url))
93 .map(Vec::as_slice)
94 .unwrap_or_default(),
95 ),
96 }
97 }
98
99 pub fn allows_package(&self, name: &PackageName, version: &Version) -> bool {
101 match self {
102 Self::None => true,
103 Self::Generate(_) => true,
104 Self::Verify(_) => true,
105 Self::Require(hashes) => {
106 hashes.contains_key(&VersionId::from_registry(name.clone(), version.clone()))
107 }
108 }
109 }
110
111 pub fn allows_url(&self, url: &DisplaySafeUrl) -> bool {
113 match self {
114 Self::None => true,
115 Self::Generate(_) => true,
116 Self::Verify(_) => true,
117 Self::Require(hashes) => hashes.contains_key(&VersionId::from_url(url)),
118 }
119 }
120
121 pub fn from_requirements<'a>(
128 requirements: impl Iterator<Item = (&'a UnresolvedRequirement, &'a [String])>,
129 constraints: impl Iterator<Item = (&'a Requirement, &'a [String])>,
130 marker_env: Option<&ResolverMarkerEnvironment>,
131 mode: HashCheckingMode,
132 ) -> Result<Self, HashStrategyError> {
133 let mut constraint_hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
134
135 for (requirement, digests) in constraints {
137 if !requirement
138 .evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
139 {
140 continue;
141 }
142
143 let Some(id) = Self::pin(requirement) else {
145 if mode.is_require() {
146 return Err(HashStrategyError::UnpinnedRequirement(
147 requirement.to_string(),
148 mode,
149 ));
150 }
151 continue;
152 };
153
154 let digests = if digests.is_empty() {
155 requirement
158 .hashes()
159 .map(HashDigests::from)
160 .map(|hashes| hashes.to_vec())
161 .unwrap_or_default()
162 } else {
163 digests
165 .iter()
166 .map(|digest| HashDigest::from_str(digest))
167 .collect::<Result<Vec<_>, _>>()?
168 };
169
170 if digests.is_empty() {
171 continue;
172 }
173
174 constraint_hashes.insert(id, digests);
175 }
176
177 let mut requirement_hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
180 for (requirement, digests) in requirements {
181 if !requirement
182 .evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
183 {
184 continue;
185 }
186
187 let id = match &requirement {
189 UnresolvedRequirement::Named(requirement) => {
190 if let Some(id) = Self::pin(requirement) {
191 id
192 } else {
193 if mode.is_require() {
194 return Err(HashStrategyError::UnpinnedRequirement(
195 requirement.to_string(),
196 mode,
197 ));
198 }
199 continue;
200 }
201 }
202 UnresolvedRequirement::Unnamed(requirement) => {
203 VersionId::from_url(&requirement.url.verbatim)
205 }
206 };
207
208 let digests = if digests.is_empty() {
209 requirement
212 .hashes()
213 .map(HashDigests::from)
214 .map(|hashes| hashes.to_vec())
215 .unwrap_or_default()
216 } else {
217 digests
219 .iter()
220 .map(|digest| HashDigest::from_str(digest))
221 .collect::<Result<Vec<_>, _>>()?
222 };
223
224 let digests = if let Some(constraint) = constraint_hashes.remove(&id) {
225 if digests.is_empty() {
226 constraint
228 } else {
229 let intersection: Vec<_> = digests
231 .into_iter()
232 .filter(|digest| constraint.contains(digest))
233 .collect();
234 if intersection.is_empty() {
235 return Err(HashStrategyError::NoIntersection(
236 requirement.to_string(),
237 mode,
238 ));
239 }
240 intersection
241 }
242 } else {
243 digests
244 };
245
246 if digests.is_empty() {
248 if mode.is_require() {
249 return Err(HashStrategyError::MissingHashes(
250 requirement.to_string(),
251 mode,
252 ));
253 }
254 continue;
255 }
256
257 requirement_hashes.insert(id, digests);
258 }
259
260 let hashes: FxHashMap<VersionId, Vec<HashDigest>> = constraint_hashes
263 .into_iter()
264 .chain(requirement_hashes)
265 .collect();
266 match mode {
267 HashCheckingMode::Verify => Ok(Self::Verify(Arc::new(hashes))),
268 HashCheckingMode::Require => Ok(Self::Require(Arc::new(hashes))),
269 }
270 }
271
272 pub fn from_resolution(
274 resolution: &Resolution,
275 mode: HashCheckingMode,
276 ) -> Result<Self, HashStrategyError> {
277 let mut hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
278
279 for (dist, digests) in resolution.hashes() {
280 if digests.is_empty() {
281 if mode.is_require() {
283 return Err(HashStrategyError::MissingHashes(
284 dist.name().to_string(),
285 mode,
286 ));
287 }
288 continue;
289 }
290 hashes.insert(dist.version_id(), digests.to_vec());
291 }
292
293 match mode {
294 HashCheckingMode::Verify => Ok(Self::Verify(Arc::new(hashes))),
295 HashCheckingMode::Require => Ok(Self::Require(Arc::new(hashes))),
296 }
297 }
298
299 fn pin(requirement: &Requirement) -> Option<VersionId> {
301 match &requirement.source {
302 RequirementSource::Registry { specifier, .. } => {
303 let [specifier] = specifier.as_ref() else {
305 return None;
306 };
307
308 if *specifier.operator() != uv_pep440::Operator::Equal {
310 return None;
311 }
312
313 Some(VersionId::from_registry(
314 requirement.name.clone(),
315 specifier.version().clone(),
316 ))
317 }
318 RequirementSource::Url { url, .. }
319 | RequirementSource::Git { url, .. }
320 | RequirementSource::Path { url, .. }
321 | RequirementSource::Directory { url, .. } => Some(VersionId::from_url(url)),
322 }
323 }
324}
325
326#[derive(thiserror::Error, Debug)]
327pub enum HashStrategyError {
328 #[error(transparent)]
329 Hash(#[from] HashError),
330 #[error(
331 "In `{1}` mode, all requirements must have their versions pinned with `==`, but found: {0}"
332 )]
333 UnpinnedRequirement(String, HashCheckingMode),
334 #[error("In `{1}` mode, all requirements must have a hash, but none were provided for: {0}")]
335 MissingHashes(String, HashCheckingMode),
336 #[error(
337 "In `{1}` mode, all requirements must have a hash, but there were no overlapping hashes between the requirements and constraints for: {0}"
338 )]
339 NoIntersection(String, HashCheckingMode),
340}