1use crate::registry::REGISTRY;
25use crate::types::{Class, Entry, ImplStatus, Purpose};
26
27const MAX_PURPOSES: usize = 4;
29
30#[derive(Debug, Clone, Copy, Default)]
32pub struct Query {
33 class: Option<Class>,
34 purposes: [Option<Purpose>; MAX_PURPOSES],
35 purpose_count: usize,
36 min_classical: u16,
37 min_quantum: u16,
38 fips_only: bool,
39 available_only: bool,
40 family: Option<&'static str>,
41}
42
43impl Query {
44 pub const fn new() -> Self {
46 Self {
47 class: None,
48 purposes: [None; MAX_PURPOSES],
49 purpose_count: 0,
50 min_classical: 0,
51 min_quantum: 0,
52 fips_only: false,
53 available_only: false,
54 family: None,
55 }
56 }
57
58 pub const fn class(mut self, class: Class) -> Self {
60 self.class = Some(class);
61 self
62 }
63
64 pub const fn purpose(mut self, purpose: Purpose) -> Self {
68 if self.purpose_count < MAX_PURPOSES {
69 self.purposes[self.purpose_count] = Some(purpose);
70 self.purpose_count += 1;
71 }
72 self
73 }
74
75 pub const fn min_classical_bits(mut self, bits: u16) -> Self {
77 self.min_classical = bits;
78 self
79 }
80
81 pub const fn min_quantum_bits(mut self, bits: u16) -> Self {
86 self.min_quantum = bits;
87 self
88 }
89
90 pub const fn fips_approved_only(mut self) -> Self {
92 self.fips_only = true;
93 self
94 }
95
96 pub const fn available_only(mut self) -> Self {
98 self.available_only = true;
99 self
100 }
101
102 pub const fn family(mut self, family: &'static str) -> Self {
104 self.family = Some(family);
105 self
106 }
107
108 pub fn matches(&self, e: &Entry) -> bool {
110 if let Some(c) = self.class {
111 if e.class != c {
112 return false;
113 }
114 }
115 for p in self.purposes.iter().flatten() {
116 if !e.serves(*p) {
117 return false;
118 }
119 }
120 if e.strength.classical < self.min_classical || e.strength.quantum < self.min_quantum {
121 return false;
122 }
123 if self.fips_only && !e.fips.permitted_in_approved_mode() {
124 return false;
125 }
126 if self.available_only && e.status != ImplStatus::Available {
127 return false;
128 }
129 if let Some(f) = self.family {
130 if e.family != f {
131 return false;
132 }
133 }
134 true
135 }
136
137 pub fn run(self) -> impl Iterator<Item = &'static Entry> {
139 REGISTRY.iter().filter(move |e| self.matches(e))
140 }
141
142 pub fn count(self) -> usize {
144 self.run().count()
145 }
146}
147
148pub fn get(name: &str) -> Option<&'static Entry> {
150 REGISTRY.iter().find(|e| e.matches_name(name))
151}
152
153pub fn all() -> &'static [Entry] {
155 REGISTRY
156}
157
158pub fn related(id: &str) -> impl Iterator<Item = &'static Entry> + '_ {
164 REGISTRY.iter().filter(move |e| {
165 if e.id == id {
166 return false;
167 }
168 let outgoing = get(id)
169 .map(|src| src.edges.iter().any(|edge| edge.target == e.id))
170 .unwrap_or(false);
171 let incoming = e.edges.iter().any(|edge| edge.target == id);
172 outgoing || incoming
173 })
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn lookup_handles_ids_names_and_aliases() {
182 assert_eq!(get("sha2-256").unwrap().id, "sha2-256");
183 assert_eq!(get("SHA-256").unwrap().id, "sha2-256");
184 assert_eq!(get("sha256").unwrap().id, "sha2-256");
185 assert_eq!(get("SHA256").unwrap().id, "sha2-256");
186 assert!(get("not-a-real-algorithm").is_none());
187 }
188
189 #[test]
190 fn authenticated_encryption_query_finds_the_aeads() {
191 let hits: Vec<_> = Query::new()
192 .purpose(Purpose::Confidentiality)
193 .purpose(Purpose::Authentication)
194 .available_only()
195 .run()
196 .map(|e| e.id)
197 .collect();
198 assert!(hits.contains(&"aes-256-gcm"));
199 assert!(hits.contains(&"chacha20-poly1305"));
200 assert!(!hits.contains(&"aes-256"));
202 }
203
204 #[test]
205 fn fips_filter_excludes_unapproved_algorithms() {
206 let hits: Vec<_> = Query::new()
207 .class(Class::Aead)
208 .fips_approved_only()
209 .run()
210 .map(|e| e.id)
211 .collect();
212 assert!(hits.contains(&"aes-256-gcm"));
213 assert!(!hits.contains(&"chacha20-poly1305"));
214 }
215
216 #[test]
217 fn availability_filter_excludes_what_is_not_implemented() {
218 let kems: Vec<&str> = Query::new()
225 .class(Class::Kem)
226 .available_only()
227 .run()
228 .map(|e| e.id)
229 .collect();
230 assert_eq!(
231 kems,
232 [
233 "hpke-x25519-sha256",
234 "hpke-p384-sha384",
235 "ml-kem-768",
236 "ml-kem-512",
237 "ml-kem-1024"
238 ]
239 );
240
241 for id in ["md5", "sha-1", "3des"] {
245 assert!(
246 crate::get(id).is_some(),
247 "{id} should still be listed, so a request for it is answered"
248 );
249 assert!(
250 !Query::new().available_only().run().any(|e| e.id == id),
251 "{id} is broken and must not appear as available"
252 );
253 }
254 }
255
256 #[test]
257 fn quantum_filter_removes_classical_asymmetric_schemes() {
258 let hits: Vec<_> = Query::new()
259 .min_quantum_bits(128)
260 .run()
261 .map(|e| e.id)
262 .collect();
263 assert!(!hits.contains(&"x25519"), "Shor breaks X25519");
264 assert!(!hits.contains(&"ed25519"));
265 assert!(
266 hits.contains(&"aes-256-gcm"),
267 "Grover leaves AES-256 at 128 bits"
268 );
269 assert!(hits.contains(&"ml-kem-768"));
270 }
271
272 #[test]
273 fn strength_filter_is_inclusive_at_the_boundary() {
274 let hits: Vec<_> = Query::new()
275 .class(Class::Aead)
276 .min_classical_bits(256)
277 .run()
278 .map(|e| e.id)
279 .collect();
280 assert!(hits.contains(&"aes-256-gcm"));
281 assert!(!hits.contains(&"aes-128-gcm"));
282 }
283
284 #[test]
285 fn family_filter_groups_related_entries() {
286 let n = Query::new().family("SHA-2").count();
287 assert_eq!(n, 6, "six published SHA-2 output variants");
288 }
289
290 #[test]
291 fn empty_query_matches_everything() {
292 assert_eq!(Query::new().count(), all().len());
293 }
294
295 #[test]
296 fn relations_traverse_both_directions() {
297 let from_hmac: Vec<_> = related("hmac-sha2-256").map(|e| e.id).collect();
299 assert!(from_hmac.contains(&"sha2-256"));
300 let from_sha: Vec<_> = related("sha2-256").map(|e| e.id).collect();
302 assert!(from_sha.contains(&"hmac-sha2-256"));
303 }
304
305 #[test]
306 fn purpose_limit_does_not_silently_drop_filters() {
307 let q = Query::new()
310 .purpose(Purpose::Confidentiality)
311 .purpose(Purpose::Authentication)
312 .purpose(Purpose::Integrity)
313 .purpose(Purpose::KeyDerivation)
314 .purpose(Purpose::RandomGeneration);
315 assert_eq!(q.purpose_count, 4);
316 assert_eq!(q.count(), 0, "nothing serves all four of those purposes");
317 }
318}