Skip to main content

uv_distribution_types/
hash.rs

1use uv_pypi_types::{HashAlgorithm, HashDigest};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum HashPolicy<'a> {
5    /// No hash policy is specified.
6    None,
7    /// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
8    Generate(HashGeneration),
9    /// Hashes should be validated against a pre-defined list of hashes, and any matching digest is
10    /// sufficient. If necessary, hashes should be generated so as to ensure that the archive is
11    /// valid.
12    Any(&'a [HashDigest]),
13    /// Hashes should be validated against a pre-defined list of hashes, and every digest must
14    /// match. If necessary, hashes should be generated so as to ensure that the archive is valid.
15    All(&'a [HashDigest]),
16}
17
18impl HashPolicy<'_> {
19    /// Returns `true` if the hash policy is `None`.
20    pub fn is_none(&self) -> bool {
21        matches!(self, Self::None)
22    }
23
24    /// Returns `true` if the hash policy is `Any` or `All`.
25    pub fn requires_validation(&self) -> bool {
26        matches!(self, Self::Any(_) | Self::All(_))
27    }
28
29    /// Returns `true` if the hash policy indicates that hashes should be generated.
30    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    /// Return the algorithms used in the hash policy.
43    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    /// Return the digests used in the hash policy.
57    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    /// Returns `true` if the given hashes satisfy the policy.
66    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    /// Returns `true` if the given hashes include the algorithms required by the policy.
82    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/// The context in which hashes should be generated.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum HashGeneration {
109    /// Generate hashes for direct URL distributions.
110    Url,
111    /// Generate hashes for direct URL distributions, along with any distributions that are hosted
112    /// on a registry that does _not_ provide hashes.
113    All,
114}
115
116pub trait Hashed {
117    /// Return the [`HashDigest`]s for the archive.
118    fn hashes(&self) -> &[HashDigest];
119
120    /// Returns `true` if the archive satisfies the given hash policy.
121    fn satisfies(&self, hashes: HashPolicy) -> bool {
122        hashes.matches(self.hashes())
123    }
124
125    /// Returns `true` if the archive includes the algorithms required by the given hash policy.
126    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}