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    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
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}