1use std::collections::HashMap;
5use tracing::trace;
6
7use crate::DocumentText;
8use crate::storage::Store;
9use tor_dirclient::request;
10#[cfg(feature = "routerdesc")]
11use tor_netdoc::doc::routerdesc::RdDigest;
12use tor_netdoc::doc::{authcert::AuthCertKeyIds, microdesc::MdDigest, netstatus::ConsensusFlavor};
13
14#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
17#[non_exhaustive]
18pub enum DocId {
19 LatestConsensus {
21 flavor: ConsensusFlavor,
23 cache_usage: CacheUsage,
25 },
26 AuthCert(AuthCertKeyIds),
29 Microdesc(MdDigest),
31 #[cfg(feature = "routerdesc")]
34 RouterDesc(RdDigest),
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
42#[non_exhaustive]
43pub(crate) enum DocType {
44 Consensus(ConsensusFlavor),
46 AuthCert,
48 Microdesc,
50 #[cfg(feature = "routerdesc")]
52 RouterDesc,
53}
54
55impl DocId {
56 pub(crate) fn doctype(&self) -> DocType {
58 use DocId::*;
59 use DocType as T;
60 match self {
61 LatestConsensus { flavor: f, .. } => T::Consensus(*f),
62 AuthCert(_) => T::AuthCert,
63 Microdesc(_) => T::Microdesc,
64 #[cfg(feature = "routerdesc")]
65 RouterDesc(_) => T::RouterDesc,
66 }
67 }
68}
69
70#[derive(Clone, Debug)]
73pub(crate) enum ClientRequest {
74 Consensus(request::ConsensusRequest),
76 AuthCert(request::AuthCertRequest),
78 Microdescs(request::MicrodescRequest),
80 #[cfg(feature = "routerdesc")]
82 RouterDescs(request::RouterDescRequest),
83}
84
85impl ClientRequest {
86 pub(crate) fn as_requestable(&self) -> &(dyn request::Requestable + Send + Sync) {
88 use ClientRequest::*;
89 match self {
90 Consensus(a) => a,
91 AuthCert(a) => a,
92 Microdescs(a) => a,
93 #[cfg(feature = "routerdesc")]
94 RouterDescs(a) => a,
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
101pub enum CacheUsage {
102 CacheOnly,
106 CacheOkay,
110 MustDownload,
113}
114
115impl CacheUsage {
116 pub(crate) fn pending_requirement(&self) -> Option<bool> {
119 match self {
120 CacheUsage::CacheOnly => Some(false),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
131pub(crate) enum DocQuery {
132 LatestConsensus {
134 flavor: ConsensusFlavor,
136 cache_usage: CacheUsage,
138 },
139 AuthCert(Vec<AuthCertKeyIds>),
141 Microdesc(Vec<MdDigest>),
143 #[cfg(feature = "routerdesc")]
145 RouterDesc(Vec<RdDigest>),
146}
147
148impl DocQuery {
149 pub(crate) fn empty_from_docid(id: &DocId) -> Self {
151 match *id {
152 DocId::LatestConsensus {
153 flavor,
154 cache_usage,
155 } => Self::LatestConsensus {
156 flavor,
157 cache_usage,
158 },
159 DocId::AuthCert(_) => Self::AuthCert(Vec::new()),
160 DocId::Microdesc(_) => Self::Microdesc(Vec::new()),
161 #[cfg(feature = "routerdesc")]
162 DocId::RouterDesc(_) => Self::RouterDesc(Vec::new()),
163 }
164 }
165
166 fn push(&mut self, id: DocId) {
168 match (self, id) {
169 (Self::LatestConsensus { .. }, DocId::LatestConsensus { .. }) => {}
170 (Self::AuthCert(ids), DocId::AuthCert(id)) => ids.push(id),
171 (Self::Microdesc(ids), DocId::Microdesc(id)) => ids.push(id),
172 #[cfg(feature = "routerdesc")]
173 (Self::RouterDesc(ids), DocId::RouterDesc(id)) => ids.push(id),
174 (_, _) => panic!(),
175 }
176 }
177
178 pub(crate) fn split_for_download(self) -> Vec<Self> {
181 use DocQuery::*;
182 const N: usize = 500;
184 match self {
185 LatestConsensus { .. } => vec![self],
186 AuthCert(mut v) => {
187 v.sort_unstable();
188 v[..].chunks(N).map(|s| AuthCert(s.to_vec())).collect()
189 }
190 Microdesc(mut v) => {
191 v.sort_unstable();
192 v[..].chunks(N).map(|s| Microdesc(s.to_vec())).collect()
193 }
194 #[cfg(feature = "routerdesc")]
195 RouterDesc(mut v) => {
196 v.sort_unstable();
197 v[..].chunks(N).map(|s| RouterDesc(s.to_vec())).collect()
198 }
199 }
200 }
201
202 pub(crate) fn load_from_store_into(
209 &self,
210 result: &mut HashMap<DocId, DocumentText>,
211 store: &dyn Store,
212 ) -> crate::Result<()> {
213 use DocQuery::*;
214 match self {
215 LatestConsensus {
216 flavor,
217 cache_usage,
218 } => {
219 if *cache_usage == CacheUsage::MustDownload {
220 trace!("MustDownload is set; not checking for cached consensus.");
222 } else if let Some(c) =
223 store.latest_consensus(*flavor, cache_usage.pending_requirement())?
224 {
225 trace!("Found a reasonable consensus in the cache");
226 let id = DocId::LatestConsensus {
227 flavor: *flavor,
228 cache_usage: *cache_usage,
229 };
230 result.insert(id, c.into());
231 }
232 }
233 AuthCert(ids) => result.extend(
234 store
235 .authcerts(ids)?
236 .into_iter()
237 .map(|(id, c)| (DocId::AuthCert(id), DocumentText::from_string(c))),
238 ),
239 Microdesc(digests) => {
240 result.extend(
241 store
242 .microdescs(digests)?
243 .into_iter()
244 .map(|(id, md)| (DocId::Microdesc(id), DocumentText::from_string(md))),
245 );
246 }
247 #[cfg(feature = "routerdesc")]
248 RouterDesc(digests) => result.extend(
249 store
250 .routerdescs(digests)?
251 .into_iter()
252 .map(|(id, rd)| (DocId::RouterDesc(id), DocumentText::from_string(rd))),
253 ),
254 }
255 Ok(())
256 }
257}
258
259impl From<DocId> for DocQuery {
260 fn from(d: DocId) -> DocQuery {
261 let mut result = DocQuery::empty_from_docid(&d);
262 result.push(d);
263 result
264 }
265}
266
267pub(crate) fn partition_by_type<T>(collection: T) -> HashMap<DocType, DocQuery>
269where
270 T: IntoIterator<Item = DocId>,
271{
272 let mut result = HashMap::new();
273 for item in collection.into_iter() {
274 let tp = item.doctype();
275 result
276 .entry(tp)
277 .or_insert_with(|| DocQuery::empty_from_docid(&item))
278 .push(item);
279 }
280 result
281}
282
283#[cfg(test)]
284mod test {
285 #![allow(clippy::bool_assert_comparison)]
287 #![allow(clippy::clone_on_copy)]
288 #![allow(clippy::dbg_macro)]
289 #![allow(clippy::mixed_attributes_style)]
290 #![allow(clippy::print_stderr)]
291 #![allow(clippy::print_stdout)]
292 #![allow(clippy::single_char_pattern)]
293 #![allow(clippy::unwrap_used)]
294 #![allow(clippy::unchecked_time_subtraction)]
295 #![allow(clippy::useless_vec)]
296 #![allow(clippy::needless_pass_by_value)]
297 #![allow(clippy::string_slice)] use super::*;
300 use tor_basic_utils::test_rng::testing_rng;
301
302 #[test]
303 fn doctype() {
304 assert_eq!(
305 DocId::LatestConsensus {
306 flavor: ConsensusFlavor::Microdesc,
307 cache_usage: CacheUsage::CacheOkay,
308 }
309 .doctype(),
310 DocType::Consensus(ConsensusFlavor::Microdesc)
311 );
312
313 let auth_id = AuthCertKeyIds {
314 id_fingerprint: [10; 20].into(),
315 sk_fingerprint: [12; 20].into(),
316 };
317 assert_eq!(DocId::AuthCert(auth_id).doctype(), DocType::AuthCert);
318
319 assert_eq!(DocId::Microdesc([22; 32]).doctype(), DocType::Microdesc);
320 #[cfg(feature = "routerdesc")]
321 assert_eq!(DocId::RouterDesc([42; 20]).doctype(), DocType::RouterDesc);
322 }
323
324 #[test]
325 fn partition_ids() {
326 let mut ids = Vec::new();
327 for byte in 0..=255 {
328 ids.push(DocId::Microdesc([byte; 32]));
329 #[cfg(feature = "routerdesc")]
330 ids.push(DocId::RouterDesc([byte; 20]));
331 ids.push(DocId::AuthCert(AuthCertKeyIds {
332 id_fingerprint: [byte; 20].into(),
333 sk_fingerprint: [33; 20].into(),
334 }));
335 }
336 let consensus_q = DocId::LatestConsensus {
337 flavor: ConsensusFlavor::Microdesc,
338 cache_usage: CacheUsage::CacheOkay,
339 };
340 ids.push(consensus_q);
341
342 let split = partition_by_type(ids);
343 #[cfg(feature = "routerdesc")]
344 assert_eq!(split.len(), 4); #[cfg(not(feature = "routerdesc"))]
346 assert_eq!(split.len(), 3); let q = split
349 .get(&DocType::Consensus(ConsensusFlavor::Microdesc))
350 .unwrap();
351 assert!(matches!(q, DocQuery::LatestConsensus { .. }));
352
353 let q = split.get(&DocType::Microdesc).unwrap();
354 assert!(matches!(q, DocQuery::Microdesc(v) if v.len() == 256));
355
356 #[cfg(feature = "routerdesc")]
357 {
358 let q = split.get(&DocType::RouterDesc).unwrap();
359 assert!(matches!(q, DocQuery::RouterDesc(v) if v.len() == 256));
360 }
361 let q = split.get(&DocType::AuthCert).unwrap();
362 assert!(matches!(q, DocQuery::AuthCert(v) if v.len() == 256));
363 }
364
365 #[test]
366 fn split_into_chunks() {
367 use std::collections::HashSet;
368 use rand::RngExt;
370
371 let mut rng = testing_rng();
373 let ids: HashSet<MdDigest> = (0..3400).map(|_| rng.random()).collect();
374
375 let split = DocQuery::Microdesc(ids.clone().into_iter().collect()).split_for_download();
377 assert_eq!(split.len(), 7);
378 let mut found_ids = HashSet::new();
379 for q in split {
380 match q {
381 DocQuery::Microdesc(ids) => ids.into_iter().for_each(|id| {
382 found_ids.insert(id);
383 }),
384 _ => panic!("Wrong type."),
385 }
386 }
387 assert_eq!(found_ids.len(), 3400);
388 assert_eq!(found_ids, ids);
389
390 #[cfg(feature = "routerdesc")]
392 {
393 let ids: HashSet<RdDigest> = (0..1001).map(|_| rng.random()).collect();
394 let split =
395 DocQuery::RouterDesc(ids.clone().into_iter().collect()).split_for_download();
396 assert_eq!(split.len(), 3);
397 let mut found_ids = HashSet::new();
398 for q in split {
399 match q {
400 DocQuery::RouterDesc(ids) => ids.into_iter().for_each(|id| {
401 found_ids.insert(id);
402 }),
403 _ => panic!("Wrong type."),
404 }
405 }
406 assert_eq!(found_ids.len(), 1001);
407 assert_eq!(&found_ids, &ids);
408 }
409
410 let ids: HashSet<AuthCertKeyIds> = (0..2500)
412 .map(|_| {
413 let id_fingerprint = rng.random::<[u8; 20]>().into();
414 let sk_fingerprint = rng.random::<[u8; 20]>().into();
415 AuthCertKeyIds {
416 id_fingerprint,
417 sk_fingerprint,
418 }
419 })
420 .collect();
421 let split = DocQuery::AuthCert(ids.clone().into_iter().collect()).split_for_download();
422 assert_eq!(split.len(), 5);
423 let mut found_ids = HashSet::new();
424 for q in split {
425 match q {
426 DocQuery::AuthCert(ids) => ids.into_iter().for_each(|id| {
427 found_ids.insert(id);
428 }),
429 _ => panic!("Wrong type."),
430 }
431 }
432 assert_eq!(found_ids.len(), 2500);
433 assert_eq!(&found_ids, &ids);
434
435 let query = DocQuery::LatestConsensus {
437 flavor: ConsensusFlavor::Microdesc,
438 cache_usage: CacheUsage::CacheOkay,
439 };
440 let split = query.clone().split_for_download();
441 assert_eq!(split, vec![query]);
442 }
443
444 #[test]
445 fn into_query() {
446 let q: DocQuery = DocId::Microdesc([99; 32]).into();
447 assert_eq!(q, DocQuery::Microdesc(vec![[99; 32]]));
448 }
449
450 #[test]
451 fn pending_requirement() {
452 assert_eq!(CacheUsage::CacheOnly.pending_requirement(), Some(false));
455 assert_eq!(CacheUsage::CacheOkay.pending_requirement(), None);
458 assert_eq!(CacheUsage::MustDownload.pending_requirement(), None);
459 }
460}