1use std::collections::HashMap;
5use tracing::trace;
6
7use crate::storage::Store;
8use crate::DocumentText;
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_duration_subtraction)]
295 #![allow(clippy::useless_vec)]
296 #![allow(clippy::needless_pass_by_value)]
297 use super::*;
299 use tor_basic_utils::test_rng::testing_rng;
300
301 #[test]
302 fn doctype() {
303 assert_eq!(
304 DocId::LatestConsensus {
305 flavor: ConsensusFlavor::Microdesc,
306 cache_usage: CacheUsage::CacheOkay,
307 }
308 .doctype(),
309 DocType::Consensus(ConsensusFlavor::Microdesc)
310 );
311
312 let auth_id = AuthCertKeyIds {
313 id_fingerprint: [10; 20].into(),
314 sk_fingerprint: [12; 20].into(),
315 };
316 assert_eq!(DocId::AuthCert(auth_id).doctype(), DocType::AuthCert);
317
318 assert_eq!(DocId::Microdesc([22; 32]).doctype(), DocType::Microdesc);
319 #[cfg(feature = "routerdesc")]
320 assert_eq!(DocId::RouterDesc([42; 20]).doctype(), DocType::RouterDesc);
321 }
322
323 #[test]
324 fn partition_ids() {
325 let mut ids = Vec::new();
326 for byte in 0..=255 {
327 ids.push(DocId::Microdesc([byte; 32]));
328 #[cfg(feature = "routerdesc")]
329 ids.push(DocId::RouterDesc([byte; 20]));
330 ids.push(DocId::AuthCert(AuthCertKeyIds {
331 id_fingerprint: [byte; 20].into(),
332 sk_fingerprint: [33; 20].into(),
333 }));
334 }
335 let consensus_q = DocId::LatestConsensus {
336 flavor: ConsensusFlavor::Microdesc,
337 cache_usage: CacheUsage::CacheOkay,
338 };
339 ids.push(consensus_q);
340
341 let split = partition_by_type(ids);
342 #[cfg(feature = "routerdesc")]
343 assert_eq!(split.len(), 4); #[cfg(not(feature = "routerdesc"))]
345 assert_eq!(split.len(), 3); let q = split
348 .get(&DocType::Consensus(ConsensusFlavor::Microdesc))
349 .unwrap();
350 assert!(matches!(q, DocQuery::LatestConsensus { .. }));
351
352 let q = split.get(&DocType::Microdesc).unwrap();
353 assert!(matches!(q, DocQuery::Microdesc(v) if v.len() == 256));
354
355 #[cfg(feature = "routerdesc")]
356 {
357 let q = split.get(&DocType::RouterDesc).unwrap();
358 assert!(matches!(q, DocQuery::RouterDesc(v) if v.len() == 256));
359 }
360 let q = split.get(&DocType::AuthCert).unwrap();
361 assert!(matches!(q, DocQuery::AuthCert(v) if v.len() == 256));
362 }
363
364 #[test]
365 fn split_into_chunks() {
366 use std::collections::HashSet;
367 use rand::Rng;
369
370 let mut rng = testing_rng();
372 let ids: HashSet<MdDigest> = (0..3400).map(|_| rng.random()).collect();
373
374 let split = DocQuery::Microdesc(ids.clone().into_iter().collect()).split_for_download();
376 assert_eq!(split.len(), 7);
377 let mut found_ids = HashSet::new();
378 for q in split {
379 match q {
380 DocQuery::Microdesc(ids) => ids.into_iter().for_each(|id| {
381 found_ids.insert(id);
382 }),
383 _ => panic!("Wrong type."),
384 }
385 }
386 assert_eq!(found_ids.len(), 3400);
387 assert_eq!(found_ids, ids);
388
389 #[cfg(feature = "routerdesc")]
391 {
392 let ids: HashSet<RdDigest> = (0..1001).map(|_| rng.random()).collect();
393 let split =
394 DocQuery::RouterDesc(ids.clone().into_iter().collect()).split_for_download();
395 assert_eq!(split.len(), 3);
396 let mut found_ids = HashSet::new();
397 for q in split {
398 match q {
399 DocQuery::RouterDesc(ids) => ids.into_iter().for_each(|id| {
400 found_ids.insert(id);
401 }),
402 _ => panic!("Wrong type."),
403 }
404 }
405 assert_eq!(found_ids.len(), 1001);
406 assert_eq!(&found_ids, &ids);
407 }
408
409 let ids: HashSet<AuthCertKeyIds> = (0..2500)
411 .map(|_| {
412 let id_fingerprint = rng.random::<[u8; 20]>().into();
413 let sk_fingerprint = rng.random::<[u8; 20]>().into();
414 AuthCertKeyIds {
415 id_fingerprint,
416 sk_fingerprint,
417 }
418 })
419 .collect();
420 let split = DocQuery::AuthCert(ids.clone().into_iter().collect()).split_for_download();
421 assert_eq!(split.len(), 5);
422 let mut found_ids = HashSet::new();
423 for q in split {
424 match q {
425 DocQuery::AuthCert(ids) => ids.into_iter().for_each(|id| {
426 found_ids.insert(id);
427 }),
428 _ => panic!("Wrong type."),
429 }
430 }
431 assert_eq!(found_ids.len(), 2500);
432 assert_eq!(&found_ids, &ids);
433
434 let query = DocQuery::LatestConsensus {
436 flavor: ConsensusFlavor::Microdesc,
437 cache_usage: CacheUsage::CacheOkay,
438 };
439 let split = query.clone().split_for_download();
440 assert_eq!(split, vec![query]);
441 }
442
443 #[test]
444 fn into_query() {
445 let q: DocQuery = DocId::Microdesc([99; 32]).into();
446 assert_eq!(q, DocQuery::Microdesc(vec![[99; 32]]));
447 }
448
449 #[test]
450 fn pending_requirement() {
451 assert_eq!(CacheUsage::CacheOnly.pending_requirement(), Some(false));
454 assert_eq!(CacheUsage::CacheOkay.pending_requirement(), None);
457 assert_eq!(CacheUsage::MustDownload.pending_requirement(), None);
458 }
459}