Skip to main content

uv_types/
hash.rs

1use std::fmt::Display;
2use std::path::Path;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use rustc_hash::FxHashMap;
7
8use uv_configuration::HashCheckingMode;
9use uv_distribution_types::{
10    DistributionMetadata, HashGeneration, HashPolicy, Name, Requirement, RequirementSource,
11    Resolution, UnresolvedRequirement, VersionId,
12};
13use uv_normalize::PackageName;
14use uv_pep440::Version;
15use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, HashError, ResolverMarkerEnvironment};
16use uv_redacted::DisplaySafeUrl;
17
18/// Hash generation and verification policies for a resolution.
19///
20/// Verification takes precedence for distributions with trusted hashes. The generation policy
21/// applies to the remaining distributions.
22#[derive(Debug, Default, Clone)]
23pub struct HashStrategy {
24    generation: Option<HashGeneration>,
25    verification: HashVerification,
26}
27
28/// The trusted hashes to enforce when retrieving distributions.
29#[derive(Debug, Default, Clone)]
30pub enum HashVerification {
31    /// Hashes do not need to be validated.
32    #[default]
33    None,
34    /// Validate known hashes, without requiring hashes for other distributions.
35    IfPresent(Arc<FxHashMap<VersionId, Vec<HashDigest>>>),
36    /// Every distribution must have a matching trusted hash.
37    Required(Arc<FxHashMap<VersionId, Vec<HashDigest>>>),
38}
39
40impl HashStrategy {
41    /// Generate hashes according to the given policy.
42    pub fn generate(generation: HashGeneration) -> Self {
43        Self {
44            generation: Some(generation),
45            ..Self::default()
46        }
47    }
48
49    /// Validate hashes when present.
50    pub fn verify(hashes: Arc<FxHashMap<VersionId, Vec<HashDigest>>>) -> Self {
51        Self::default().with_verification(HashVerification::IfPresent(hashes))
52    }
53
54    /// Require a matching trusted hash for every distribution.
55    fn require(hashes: Arc<FxHashMap<VersionId, Vec<HashDigest>>>) -> Self {
56        Self::default().with_verification(HashVerification::Required(hashes))
57    }
58
59    /// Set verification independently of hash generation.
60    #[must_use]
61    fn with_verification(mut self, verification: HashVerification) -> Self {
62        self.verification = verification;
63        self
64    }
65
66    /// Return the hash generation policy.
67    pub fn generation(&self) -> Option<HashGeneration> {
68        self.generation
69    }
70
71    /// Return the hash verification policy.
72    pub fn verification(&self) -> &HashVerification {
73        &self.verification
74    }
75
76    /// Return the [`HashPolicy`] for the given distribution.
77    pub fn get<T: DistributionMetadata>(&self, distribution: &T) -> HashPolicy<'_> {
78        self.get_id(|| distribution.version_id())
79    }
80
81    /// Return the [`HashPolicy`] for the given registry-based package.
82    pub fn get_package(&self, name: &PackageName, version: &Version) -> HashPolicy<'_> {
83        self.get_id(|| VersionId::from_registry(name.clone(), version.clone()))
84    }
85
86    /// Return the [`HashPolicy`] for the given direct URL package.
87    ///
88    /// A direct URL identifies a single concrete artifact, so every provided digest must match.
89    pub fn get_url(&self, url: &DisplaySafeUrl) -> HashPolicy<'_> {
90        self.get_id(|| VersionId::from_url(url))
91    }
92
93    /// Construct an identity only when verification requires a lookup.
94    fn get_id(&self, id: impl FnOnce() -> VersionId) -> HashPolicy<'_> {
95        match &self.verification {
96            HashVerification::IfPresent(hashes) => {
97                let id = id();
98                if let Some(hashes) = hashes.get(&id) {
99                    return hash_policy(&id, hashes);
100                }
101            }
102            HashVerification::Required(hashes) => {
103                let id = id();
104                return hash_policy(&id, hashes.get(&id).map(Vec::as_slice).unwrap_or_default());
105            }
106            HashVerification::None => {}
107        }
108        self.generation
109            .map_or(HashPolicy::None, HashPolicy::Generate)
110    }
111
112    /// Returns `true` if the given registry-based package is allowed.
113    pub fn allows_package(&self, name: &PackageName, version: &Version) -> bool {
114        match &self.verification {
115            HashVerification::Required(hashes) => {
116                hashes.contains_key(&VersionId::from_registry(name.clone(), version.clone()))
117            }
118            HashVerification::None | HashVerification::IfPresent(_) => true,
119        }
120    }
121
122    /// Returns `true` if the given direct URL package is allowed.
123    pub fn allows_url(&self, url: &DisplaySafeUrl) -> bool {
124        match &self.verification {
125            HashVerification::Required(hashes) => hashes.contains_key(&VersionId::from_url(url)),
126            HashVerification::None | HashVerification::IfPresent(_) => true,
127        }
128    }
129
130    /// Return a [`HashStrategy`] augmented with archive URL hashes discovered in additional
131    /// requirements after the initial command-line parse.
132    pub fn augment_with_requirements<'a>(
133        mut self,
134        requirements: impl Iterator<Item = &'a Requirement>,
135    ) -> Result<Self, HashStrategyError> {
136        match &mut self.verification {
137            HashVerification::None => {}
138            HashVerification::IfPresent(existing) | HashVerification::Required(existing) => {
139                if let Some(hashes) = Self::augment_hashes(existing, requirements)? {
140                    *existing = Arc::new(hashes);
141                }
142            }
143        }
144        Ok(self)
145    }
146
147    /// Return a [`HashStrategy`] augmented with archive URL hashes discovered in distribution
148    /// metadata.
149    ///
150    /// Required-hash verification is intentionally a closed set. In that mode, distribution
151    /// untrusted metadata cannot authorize a requirement that was absent from the input hash set.
152    /// Explicit requirements, such as `build-system.requires` and user-provided metadata, can still
153    /// contribute hashes via [`Self::augment_with_requirements`].
154    pub fn augment_with_metadata_requirements<'a>(
155        self,
156        requirements: impl Iterator<Item = &'a Requirement>,
157    ) -> Result<Self, HashStrategyError> {
158        if matches!(&self.verification, HashVerification::Required(_)) {
159            return Ok(self);
160        }
161        self.augment_with_requirements(requirements)
162    }
163
164    /// Generate the required hashes from a set of [`UnresolvedRequirement`] entries.
165    ///
166    /// When the environment is not given, this treats all marker expressions
167    /// that reference the environment as true. In other words, it does
168    /// environment independent expression evaluation. (Which in turn devolves
169    /// to "only evaluate marker expressions that reference an extra name.")
170    pub fn from_requirements<'a>(
171        requirements: impl Iterator<Item = (&'a UnresolvedRequirement, &'a [String])>,
172        constraints: impl Iterator<Item = (&'a Requirement, &'a [String])>,
173        marker_env: Option<&ResolverMarkerEnvironment>,
174        mode: HashCheckingMode,
175    ) -> Result<Self, HashStrategyError> {
176        let mut constraint_hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
177
178        // First, index the constraints by name.
179        for (requirement, digests) in constraints {
180            if !requirement
181                .evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
182            {
183                continue;
184            }
185
186            // Every constraint must be a pinned version.
187            let Some(id) = Self::pin(requirement) else {
188                if mode.is_require() {
189                    return Err(HashStrategyError::UnpinnedRequirement(
190                        requirement.to_string(),
191                        mode,
192                    ));
193                }
194                continue;
195            };
196
197            // Parse the hashes provided directly on the requirement, then merge in any hashes from
198            // the URL fragment.
199            let mut digests = digests
200                .iter()
201                .map(|digest| HashDigest::from_str(digest))
202                .collect::<Result<Vec<_>, _>>()?;
203            if let Some(fragment_hashes) = requirement.hashes().map(HashDigests::from) {
204                merge_digests(&mut digests, fragment_hashes.iter(), requirement)?;
205            }
206
207            if mode.is_require() {
208                digests.retain(|digest| digest.algorithm() != HashAlgorithm::Md5);
209            }
210
211            if digests.is_empty() {
212                continue;
213            }
214
215            merge_hashes(&mut constraint_hashes, id, digests, requirement)?;
216        }
217
218        // For each requirement, map from hash identity to allowed hashes.
219        let mut requirement_hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
220        for (requirement, digests) in requirements {
221            if !requirement
222                .evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
223            {
224                continue;
225            }
226
227            // Every requirement must be either a pinned version or a direct URL.
228            let id = match &requirement {
229                UnresolvedRequirement::Named(requirement) => {
230                    if let Some(id) = Self::pin(requirement) {
231                        id
232                    } else {
233                        if mode.is_require() {
234                            return Err(HashStrategyError::UnpinnedRequirement(
235                                requirement.to_string(),
236                                mode,
237                            ));
238                        }
239                        continue;
240                    }
241                }
242                UnresolvedRequirement::Unnamed(requirement) => {
243                    // Direct URLs are always allowed.
244                    VersionId::from_parsed_url(requirement.url.parsed_url.clone())
245                }
246            };
247
248            // Parse the hashes provided directly on the requirement, then merge in any hashes from
249            // the URL fragment.
250            let mut digests = digests
251                .iter()
252                .map(|digest| HashDigest::from_str(digest))
253                .collect::<Result<Vec<_>, _>>()?;
254            if let Some(fragment_hashes) = requirement.hashes().map(HashDigests::from) {
255                merge_digests(&mut digests, fragment_hashes.iter(), requirement)?;
256            }
257
258            let has_md5 = mode.is_require()
259                && digests
260                    .iter()
261                    .any(|digest| digest.algorithm() == HashAlgorithm::Md5);
262            if mode.is_require() {
263                digests.retain(|digest| digest.algorithm() != HashAlgorithm::Md5);
264            }
265
266            let digests = if let Some(constraint) = constraint_hashes.remove(&id) {
267                if digests.is_empty() {
268                    // If there are _only_ hashes on the constraints, use them.
269                    constraint
270                } else if matches!(id, VersionId::ArchiveUrl { .. }) {
271                    let mut merged = digests;
272                    merge_digests(&mut merged, &constraint, requirement)?;
273                    merged
274                } else {
275                    // If there are constraint and requirement hashes, take the intersection.
276                    let intersection: Vec<_> = digests
277                        .into_iter()
278                        .filter(|digest| constraint.contains(digest))
279                        .collect();
280                    if intersection.is_empty() {
281                        return Err(HashStrategyError::NoIntersection(
282                            requirement.to_string(),
283                            mode,
284                        ));
285                    }
286                    intersection
287                }
288            } else {
289                digests
290            };
291
292            // Under `--require-hashes`, every requirement must include a hash.
293            if digests.is_empty() {
294                if mode.is_require() {
295                    if has_md5 {
296                        return Err(HashStrategyError::InsecureHashAlgorithm(
297                            requirement.to_string(),
298                            HashAlgorithm::Md5,
299                            mode,
300                        ));
301                    }
302                    return Err(HashStrategyError::MissingHashes(
303                        requirement.to_string(),
304                        mode,
305                    ));
306                }
307                continue;
308            }
309
310            merge_hashes(&mut requirement_hashes, id, digests, requirement)?;
311        }
312
313        // Merge the hashes, preferring requirements over constraints, since overlapping
314        // requirements were already merged.
315        let hashes: FxHashMap<VersionId, Vec<HashDigest>> = constraint_hashes
316            .into_iter()
317            .chain(requirement_hashes)
318            .collect();
319        match mode {
320            HashCheckingMode::Verify => Ok(Self::verify(Arc::new(hashes))),
321            HashCheckingMode::Require => Ok(Self::require(Arc::new(hashes))),
322        }
323    }
324
325    /// Generate the required hashes from a [`Resolution`].
326    pub fn from_resolution(
327        resolution: &Resolution,
328        mode: HashCheckingMode,
329    ) -> Result<Self, HashStrategyError> {
330        let mut hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
331
332        for (dist, digests) in resolution.hashes() {
333            if digests.is_empty() {
334                // Under `--require-hashes`, every requirement must include a hash.
335                if mode.is_require() {
336                    return Err(HashStrategyError::MissingHashes(
337                        dist.name().to_string(),
338                        mode,
339                    ));
340                }
341                continue;
342            }
343            hashes.insert(dist.version_id(), digests.to_vec());
344        }
345
346        match mode {
347            HashCheckingMode::Verify => Ok(Self::verify(Arc::new(hashes))),
348            HashCheckingMode::Require => Ok(Self::require(Arc::new(hashes))),
349        }
350    }
351
352    /// Augment an existing set of hashes with archive URL hashes discovered in additional
353    /// requirements.
354    ///
355    /// Archive URL requirements are keyed by a [`VersionId`] so that requirements that refer to
356    /// the same underlying archive but differ only in hash fragments are merged onto the same
357    /// digest set.
358    ///
359    /// Returns `Ok(None)` if no new hashes were added or updated.
360    fn augment_hashes<'a>(
361        existing: &FxHashMap<VersionId, Vec<HashDigest>>,
362        requirements: impl Iterator<Item = &'a Requirement>,
363    ) -> Result<Option<FxHashMap<VersionId, Vec<HashDigest>>>, HashStrategyError> {
364        let mut hashes = None;
365
366        for requirement in requirements {
367            let Some((id, digests)) = Self::requirement_hashes(requirement) else {
368                continue;
369            };
370            let current = hashes.as_ref().unwrap_or(existing);
371            let current_digests = current.get(&id);
372            let mut merged = current_digests.cloned().unwrap_or_default();
373            merge_digests(&mut merged, &digests, requirement)?;
374
375            if current_digests.map(Vec::as_slice) == Some(merged.as_slice()) {
376                continue;
377            }
378
379            hashes
380                .get_or_insert_with(|| existing.clone())
381                .insert(id, merged);
382        }
383
384        Ok(hashes)
385    }
386
387    /// Extract the archive URL hash target and digests for a requirement, if any.
388    fn requirement_hashes(requirement: &Requirement) -> Option<(VersionId, Vec<HashDigest>)> {
389        let mut digests = HashDigests::from(requirement.hashes()?).to_vec();
390        if digests.is_empty() {
391            return None;
392        }
393        digests.sort_unstable();
394        let id = Self::pin(requirement)?;
395        Some((id, digests))
396    }
397
398    /// Pin a [`Requirement`] to a [`VersionId`], if possible.
399    fn pin(requirement: &Requirement) -> Option<VersionId> {
400        match &requirement.source {
401            RequirementSource::Registry { specifier, .. } => {
402                // Must be a single specifier.
403                let [specifier] = specifier.as_ref() else {
404                    return None;
405                };
406
407                // Must be pinned to a specific version.
408                if *specifier.operator() != uv_pep440::Operator::Equal {
409                    return None;
410                }
411
412                Some(VersionId::from_registry(
413                    requirement.name.clone(),
414                    specifier.version().clone(),
415                ))
416            }
417            RequirementSource::Url {
418                location,
419                subdirectory,
420                ..
421            } => Some(VersionId::from_archive(
422                location.clone(),
423                subdirectory.clone().map(Path::into_path_buf),
424            )),
425            RequirementSource::GitDirectory {
426                git, subdirectory, ..
427            } => Some(VersionId::from_git(git, subdirectory.as_deref())),
428            RequirementSource::GitPath {
429                git, install_path, ..
430            } => Some(VersionId::from_git(git, Some(install_path))),
431            RequirementSource::Path { install_path, .. } => {
432                Some(VersionId::from_path(install_path))
433            }
434            RequirementSource::Directory { install_path, .. } => {
435                Some(VersionId::from_directory(install_path))
436            }
437        }
438    }
439}
440
441fn hash_policy<'a>(id: &VersionId, digests: &'a [HashDigest]) -> HashPolicy<'a> {
442    match id {
443        VersionId::NameVersion { .. } => HashPolicy::Any(digests),
444        VersionId::ArchiveUrl { .. }
445        | VersionId::Git { .. }
446        | VersionId::Path { .. }
447        | VersionId::Directory { .. }
448        | VersionId::Unknown { .. } => HashPolicy::All(digests),
449    }
450}
451
452/// Merge repeated hashes for a requirement or constraint into the hash map.
453fn merge_hashes(
454    hashes: &mut FxHashMap<VersionId, Vec<HashDigest>>,
455    id: VersionId,
456    incoming: Vec<HashDigest>,
457    requirement: impl Display,
458) -> Result<(), HashStrategyError> {
459    if incoming.is_empty() {
460        return Ok(());
461    }
462
463    if !matches!(&id, VersionId::ArchiveUrl { .. }) {
464        hashes.insert(id, incoming);
465        return Ok(());
466    }
467
468    if let Some(existing) = hashes.get_mut(&id) {
469        return merge_digests(existing, &incoming, requirement);
470    }
471
472    let mut merged = Vec::new();
473    merge_digests(&mut merged, &incoming, requirement)?;
474    hashes.insert(id, merged);
475    Ok(())
476}
477
478/// Merge `incoming` digests into `existing`.
479///
480/// Exact duplicates are ignored. Digests for different algorithms are accumulated. If the
481/// same algorithm appears with two different values, returns
482/// [`HashStrategyError::ConflictingArchiveUrlHashes`].
483fn merge_digests<'a>(
484    existing: &mut Vec<HashDigest>,
485    incoming: impl IntoIterator<Item = &'a HashDigest>,
486    requirement: impl Display,
487) -> Result<(), HashStrategyError> {
488    for digest in incoming {
489        match existing
490            .iter()
491            .find(|candidate| candidate.algorithm == digest.algorithm)
492        {
493            Some(candidate) if candidate == digest => {}
494            Some(conflict) => {
495                return Err(HashStrategyError::ConflictingArchiveUrlHashes(
496                    requirement.to_string(),
497                    conflict.clone(),
498                    digest.clone(),
499                ));
500            }
501            None => existing.push(digest.clone()),
502        }
503    }
504    existing.sort_unstable();
505
506    Ok(())
507}
508
509#[derive(thiserror::Error, Debug)]
510pub enum HashStrategyError {
511    #[error(transparent)]
512    Hash(#[from] HashError),
513    #[error("Conflicting archive URL hashes for `{0}`: `{1}` conflicts with `{2}`")]
514    ConflictingArchiveUrlHashes(String, HashDigest, HashDigest),
515    #[error(
516        "In `{1}` mode, all requirements must have their versions pinned with `==`, but found: {0}"
517    )]
518    UnpinnedRequirement(String, HashCheckingMode),
519    #[error(
520        "`{1}` hashes are insecure and cannot be used with `{2}` but no other hashes are available for: {0}"
521    )]
522    InsecureHashAlgorithm(String, HashAlgorithm, HashCheckingMode),
523    #[error("In `{1}` mode, all requirements must have a hash, but none were provided for: {0}")]
524    MissingHashes(String, HashCheckingMode),
525    #[error(
526        "In `{1}` mode, all requirements must have a hash, but there were no overlapping hashes between the requirements and constraints for: {0}"
527    )]
528    NoIntersection(String, HashCheckingMode),
529}
530
531#[cfg(test)]
532mod tests {
533    use std::slice;
534    use std::str::FromStr;
535    use std::sync::Arc;
536
537    use rustc_hash::FxHashMap;
538    use uv_configuration::HashCheckingMode;
539    use uv_distribution_filename::DistExtension;
540    use uv_distribution_types::{
541        HashGeneration, HashPolicy, Requirement, RequirementSource, UnresolvedRequirement,
542        VersionId,
543    };
544    use uv_normalize::PackageName;
545    use uv_pep440::Version;
546    use uv_pypi_types::HashDigest;
547    use uv_redacted::DisplaySafeUrl;
548
549    use super::{HashStrategy, HashVerification};
550
551    fn requirement(url: &str) -> Requirement {
552        Requirement {
553            name: "anyio".parse().unwrap(),
554            extras: Box::default(),
555            groups: Box::default(),
556            marker: "python_version >= '3.8'".parse().unwrap(),
557            source: RequirementSource::Url {
558                location: "https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl"
559                    .parse()
560                    .unwrap(),
561                subdirectory: None,
562                ext: DistExtension::Wheel,
563                url: url.parse().unwrap(),
564            },
565            origin: None,
566        }
567    }
568
569    #[test]
570    fn from_requirements_merges_direct_url_hashes_across_fragments() {
571        let first = UnresolvedRequirement::Named(requirement(
572            "https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl#sha256=cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
573        ));
574        let second = UnresolvedRequirement::Named(requirement(
575            "https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl#sha512=f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
576        ));
577
578        let hasher = HashStrategy::from_requirements(
579            [(&first, &[][..]), (&second, &[][..])].into_iter(),
580            std::iter::empty(),
581            None,
582            HashCheckingMode::Require,
583        )
584        .unwrap();
585
586        let mut expected = vec![
587            HashDigest::from_str(
588                "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
589            )
590            .unwrap(),
591            HashDigest::from_str(
592                "sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2",
593            )
594            .unwrap(),
595        ];
596        expected.sort_unstable();
597
598        for requirement in [&first, &second] {
599            let UnresolvedRequirement::Named(requirement) = requirement else {
600                panic!("expected named requirement");
601            };
602            let RequirementSource::Url { url, .. } = &requirement.source else {
603                panic!("expected direct URL requirement");
604            };
605            assert_eq!(hasher.get_url(url), HashPolicy::All(expected.as_slice()));
606        }
607    }
608
609    #[test]
610    fn generate_and_verify_validates_known_hashes_and_generates_unknown_hashes()
611    -> Result<(), Box<dyn std::error::Error>> {
612        let url: DisplaySafeUrl = "https://example.com/anyio-4.0.0.tar.gz".parse()?;
613        let unknown_url: DisplaySafeUrl = "https://example.com/anyio-4.1.0.tar.gz".parse()?;
614        let name: PackageName = "anyio".parse()?;
615        let version: Version = "4.0.0".parse()?;
616        let unknown_version: Version = "4.1.0".parse()?;
617        let digest = HashDigest::from_str(
618            "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f",
619        )?;
620        let hashes = FxHashMap::from_iter([
621            (VersionId::from_url(&url), vec![digest.clone()]),
622            (
623                VersionId::from_registry(name.clone(), version.clone()),
624                vec![digest.clone()],
625            ),
626        ]);
627        let strategy = HashStrategy::generate(HashGeneration::All)
628            .with_verification(HashVerification::IfPresent(Arc::new(hashes)));
629
630        assert_eq!(
631            strategy.get_url(&url),
632            HashPolicy::All(slice::from_ref(&digest))
633        );
634        assert_eq!(
635            strategy.get_url(&unknown_url),
636            HashPolicy::Generate(HashGeneration::All)
637        );
638        assert_eq!(
639            strategy.get_package(&name, &version),
640            HashPolicy::Any(slice::from_ref(&digest))
641        );
642        assert_eq!(
643            strategy.get_package(&name, &unknown_version),
644            HashPolicy::Generate(HashGeneration::All)
645        );
646
647        Ok(())
648    }
649
650    #[test]
651    fn required_hashes_take_precedence_over_generation() -> Result<(), Box<dyn std::error::Error>> {
652        let url: DisplaySafeUrl = "https://example.com/anyio-4.0.0.tar.gz".parse()?;
653        let name: PackageName = "anyio".parse()?;
654        let version: Version = "4.0.0".parse()?;
655        let strategy = HashStrategy::generate(HashGeneration::All)
656            .with_verification(HashVerification::Required(Arc::default()));
657
658        assert_eq!(strategy.get_url(&url), HashPolicy::All(&[]));
659        assert_eq!(strategy.get_package(&name, &version), HashPolicy::Any(&[]));
660        assert!(!strategy.allows_url(&url));
661        assert!(!strategy.allows_package(&name, &version));
662        Ok(())
663    }
664}