1use std::collections::HashMap;
93use std::fmt;
94use std::net::IpAddr;
95use std::str::FromStr;
96
97use chrono::{DateTime, NaiveDateTime, Utc};
98
99use crate::version::Version;
100use crate::Error;
101
102use super::authority::DirectoryAuthority;
103use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
104
105#[derive(Debug, Clone, PartialEq)]
116pub struct SharedRandomness {
117 pub num_reveals: u32,
119 pub value: String,
121}
122
123#[derive(Debug, Clone, PartialEq)]
136pub struct DocumentSignature {
137 pub identity: String,
139 pub signing_key_digest: String,
141 pub signature: String,
143 pub algorithm: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq)]
201pub struct NetworkStatusDocument {
202 pub version: u32,
204 pub version_flavor: String,
206 pub is_consensus: bool,
208 pub is_vote: bool,
210 pub is_microdescriptor: bool,
212 pub consensus_method: Option<u32>,
214 pub consensus_methods: Option<Vec<u32>>,
216 pub published: Option<DateTime<Utc>>,
218 pub valid_after: DateTime<Utc>,
220 pub fresh_until: DateTime<Utc>,
222 pub valid_until: DateTime<Utc>,
224 pub vote_delay: Option<u32>,
226 pub dist_delay: Option<u32>,
228 pub client_versions: Vec<Version>,
230 pub server_versions: Vec<Version>,
232 pub known_flags: Vec<String>,
234 pub recommended_client_protocols: HashMap<String, Vec<u32>>,
236 pub recommended_relay_protocols: HashMap<String, Vec<u32>>,
238 pub required_client_protocols: HashMap<String, Vec<u32>>,
240 pub required_relay_protocols: HashMap<String, Vec<u32>>,
242 pub params: HashMap<String, i32>,
244 pub shared_randomness_previous: Option<SharedRandomness>,
246 pub shared_randomness_current: Option<SharedRandomness>,
248 pub bandwidth_weights: HashMap<String, i32>,
250 pub authorities: Vec<DirectoryAuthority>,
252 pub signatures: Vec<DocumentSignature>,
254 raw_content: Vec<u8>,
256 unrecognized_lines: Vec<String>,
258}
259
260impl NetworkStatusDocument {
261 fn parse_timestamp(value: &str) -> Result<DateTime<Utc>, Error> {
262 let datetime =
263 NaiveDateTime::parse_from_str(value.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
264 Error::Parse {
265 location: "timestamp".to_string(),
266 reason: format!("invalid datetime: {} - {}", value, e),
267 }
268 })?;
269 Ok(datetime.and_utc())
270 }
271
272 fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
273 let mut protocols = HashMap::new();
274 for entry in value.split_whitespace() {
275 if let Some(eq_pos) = entry.find('=') {
276 let proto_name = &entry[..eq_pos];
277 let versions_str = &entry[eq_pos + 1..];
278 let versions: Vec<u32> = versions_str
279 .split(',')
280 .filter_map(|v| {
281 if let Some(dash) = v.find('-') {
282 let start: u32 = v[..dash].parse().ok()?;
283 let end: u32 = v[dash + 1..].parse().ok()?;
284 Some((start..=end).collect::<Vec<_>>())
285 } else {
286 v.parse().ok().map(|n| vec![n])
287 }
288 })
289 .flatten()
290 .collect();
291 protocols.insert(proto_name.to_string(), versions);
292 }
293 }
294 protocols
295 }
296
297 fn parse_params(value: &str) -> HashMap<String, i32> {
298 let mut params = HashMap::new();
299 for entry in value.split_whitespace() {
300 if let Some(eq_pos) = entry.find('=') {
301 let key = &entry[..eq_pos];
302 let val_str = &entry[eq_pos + 1..];
303 if let Ok(val) = val_str.parse::<i32>() {
304 params.insert(key.to_string(), val);
305 }
306 }
307 }
308 params
309 }
310
311 fn parse_shared_randomness(value: &str) -> Option<SharedRandomness> {
312 let parts: Vec<&str> = value.split_whitespace().collect();
313 if parts.len() >= 2 {
314 let num_reveals = parts[0].parse().ok()?;
315 let value = parts[1].to_string();
316 Some(SharedRandomness { num_reveals, value })
317 } else {
318 None
319 }
320 }
321
322 fn parse_versions(value: &str) -> Vec<Version> {
323 value
324 .split(',')
325 .filter_map(|v| {
326 let v = v.trim();
327 if v.is_empty() {
328 None
329 } else {
330 Version::parse(v).ok()
331 }
332 })
333 .collect()
334 }
335
336 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
337 let mut block = String::new();
338 let mut idx = start_idx;
339 while idx < lines.len() {
340 let line = lines[idx];
341 block.push_str(line);
342 block.push('\n');
343 if line.starts_with("-----END ") {
344 break;
345 }
346 idx += 1;
347 }
348 (block.trim_end().to_string(), idx)
349 }
350
351 fn parse_dir_source(value: &str) -> Result<DirectoryAuthority, Error> {
352 let parts: Vec<&str> = value.split_whitespace().collect();
353 if parts.len() < 6 {
354 return Err(Error::Parse {
355 location: "dir-source".to_string(),
356 reason: "dir-source requires 6 fields".to_string(),
357 });
358 }
359 let nickname = parts[0].to_string();
360 let v3ident = parts[1].to_string();
361 let hostname = parts[2].to_string();
362 let address: IpAddr = parts[3].parse().map_err(|_| Error::Parse {
363 location: "dir-source".to_string(),
364 reason: format!("invalid address: {}", parts[3]),
365 })?;
366 let dir_port: Option<u16> = {
367 let port: u16 = parts[4].parse().map_err(|_| Error::Parse {
368 location: "dir-source".to_string(),
369 reason: format!("invalid dir_port: {}", parts[4]),
370 })?;
371 if port == 0 {
372 None
373 } else {
374 Some(port)
375 }
376 };
377 let or_port: u16 = parts[5].parse().map_err(|_| Error::Parse {
378 location: "dir-source".to_string(),
379 reason: format!("invalid or_port: {}", parts[5]),
380 })?;
381 let is_legacy = nickname.ends_with("-legacy");
382 Ok(DirectoryAuthority {
383 nickname,
384 v3ident,
385 hostname,
386 address,
387 dir_port,
388 or_port,
389 is_legacy,
390 contact: None,
391 vote_digest: None,
392 legacy_dir_key: None,
393 key_certificate: None,
394 is_shared_randomness_participate: false,
395 shared_randomness_commitments: Vec::new(),
396 shared_randomness_previous_reveal_count: None,
397 shared_randomness_previous_value: None,
398 shared_randomness_current_reveal_count: None,
399 shared_randomness_current_value: None,
400 raw_content: Vec::new(),
401 unrecognized_lines: Vec::new(),
402 })
403 }
404}
405
406impl Descriptor for NetworkStatusDocument {
407 fn parse(content: &str) -> Result<Self, Error> {
408 let raw_content = content.as_bytes().to_vec();
409 let lines: Vec<&str> = content.lines().collect();
410
411 let mut version: u32 = 3;
412 let mut version_flavor = "ns".to_string();
413 let mut is_consensus = true;
414 let mut is_vote = false;
415 let mut is_microdescriptor = false;
416 let mut consensus_method: Option<u32> = None;
417 let mut consensus_methods: Option<Vec<u32>> = None;
418 let mut published: Option<DateTime<Utc>> = None;
419 let mut valid_after: Option<DateTime<Utc>> = None;
420 let mut fresh_until: Option<DateTime<Utc>> = None;
421 let mut valid_until: Option<DateTime<Utc>> = None;
422 let mut vote_delay: Option<u32> = None;
423 let mut dist_delay: Option<u32> = None;
424 let mut client_versions: Vec<Version> = Vec::new();
425 let mut server_versions: Vec<Version> = Vec::new();
426 let mut known_flags: Vec<String> = Vec::new();
427 let mut recommended_client_protocols: HashMap<String, Vec<u32>> = HashMap::new();
428 let mut recommended_relay_protocols: HashMap<String, Vec<u32>> = HashMap::new();
429 let mut required_client_protocols: HashMap<String, Vec<u32>> = HashMap::new();
430 let mut required_relay_protocols: HashMap<String, Vec<u32>> = HashMap::new();
431 let mut params: HashMap<String, i32> = HashMap::new();
432 let mut shared_randomness_previous: Option<SharedRandomness> = None;
433 let mut shared_randomness_current: Option<SharedRandomness> = None;
434 let mut bandwidth_weights: HashMap<String, i32> = HashMap::new();
435 let mut authorities: Vec<DirectoryAuthority> = Vec::new();
436 let mut signatures: Vec<DocumentSignature> = Vec::new();
437 let mut unrecognized_lines: Vec<String> = Vec::new();
438 let mut current_authority: Option<DirectoryAuthority> = None;
439
440 let mut idx = 0;
441 while idx < lines.len() {
442 let line = lines[idx];
443
444 if line.starts_with("@type ") {
445 idx += 1;
446 continue;
447 }
448
449 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
450 (&line[..space_pos], line[space_pos + 1..].trim())
451 } else {
452 (line, "")
453 };
454
455 match keyword {
456 "network-status-version" => {
457 let parts: Vec<&str> = value.split_whitespace().collect();
458 if let Some(v) = parts.first() {
459 version = v.parse().unwrap_or(3);
460 }
461 if let Some(flavor) = parts.get(1) {
462 version_flavor = flavor.to_string();
463 is_microdescriptor = *flavor == "microdesc";
464 }
465 }
466 "vote-status" => {
467 is_consensus = value == "consensus";
468 is_vote = value == "vote";
469 }
470 "consensus-method" => {
471 consensus_method = value.parse().ok();
472 }
473 "consensus-methods" => {
474 consensus_methods = Some(
475 value
476 .split_whitespace()
477 .filter_map(|v| v.parse().ok())
478 .collect(),
479 );
480 }
481 "published" => {
482 published = Some(Self::parse_timestamp(value)?);
483 }
484 "valid-after" => {
485 valid_after = Some(Self::parse_timestamp(value)?);
486 }
487 "fresh-until" => {
488 fresh_until = Some(Self::parse_timestamp(value)?);
489 }
490 "valid-until" => {
491 valid_until = Some(Self::parse_timestamp(value)?);
492 }
493 "voting-delay" => {
494 let parts: Vec<&str> = value.split_whitespace().collect();
495 if parts.len() >= 2 {
496 vote_delay = parts[0].parse().ok();
497 dist_delay = parts[1].parse().ok();
498 }
499 }
500 "client-versions" => {
501 client_versions = Self::parse_versions(value);
502 }
503 "server-versions" => {
504 server_versions = Self::parse_versions(value);
505 }
506 "known-flags" => {
507 known_flags = value.split_whitespace().map(|s| s.to_string()).collect();
508 }
509 "recommended-client-protocols" => {
510 recommended_client_protocols = Self::parse_protocols(value);
511 }
512 "recommended-relay-protocols" => {
513 recommended_relay_protocols = Self::parse_protocols(value);
514 }
515 "required-client-protocols" => {
516 required_client_protocols = Self::parse_protocols(value);
517 }
518 "required-relay-protocols" => {
519 required_relay_protocols = Self::parse_protocols(value);
520 }
521 "params" => {
522 params = Self::parse_params(value);
523 }
524 "shared-rand-previous-value" => {
525 shared_randomness_previous = Self::parse_shared_randomness(value);
526 }
527 "shared-rand-current-value" => {
528 shared_randomness_current = Self::parse_shared_randomness(value);
529 }
530 "bandwidth-weights" => {
531 bandwidth_weights = Self::parse_params(value);
532 }
533
534 "dir-source" => {
535 if let Some(auth) = current_authority.take() {
536 authorities.push(auth);
537 }
538 current_authority = Some(Self::parse_dir_source(value)?);
539 }
540 "contact" => {
541 if let Some(ref mut auth) = current_authority {
542 auth.contact = Some(value.to_string());
543 }
544 }
545 "vote-digest" => {
546 if let Some(ref mut auth) = current_authority {
547 auth.vote_digest = Some(value.to_string());
548 }
549 }
550 "legacy-dir-key" => {
551 if let Some(ref mut auth) = current_authority {
552 auth.legacy_dir_key = Some(value.to_string());
553 }
554 }
555 "directory-signature" => {
556 if let Some(auth) = current_authority.take() {
557 authorities.push(auth);
558 }
559 let parts: Vec<&str> = value.split_whitespace().collect();
560 let (algorithm, identity, signing_key_digest) = if parts.len() >= 3 {
561 (
562 Some(parts[0].to_string()),
563 parts[1].to_string(),
564 parts[2].to_string(),
565 )
566 } else if parts.len() >= 2 {
567 (None, parts[0].to_string(), parts[1].to_string())
568 } else {
569 (None, String::new(), String::new())
570 };
571 let (signature, end_idx) = Self::extract_pem_block(&lines, idx + 1);
572 signatures.push(DocumentSignature {
573 identity,
574 signing_key_digest,
575 signature,
576 algorithm,
577 });
578 idx = end_idx;
579 }
580 "r" | "s" | "v" | "pr" | "w" | "p" | "m" | "a" => {
581 if let Some(auth) = current_authority.take() {
582 authorities.push(auth);
583 }
584 }
585 "directory-footer" => {}
586 _ => {
587 if !line.is_empty() && !line.starts_with("-----") {
588 unrecognized_lines.push(line.to_string());
589 }
590 }
591 }
592 idx += 1;
593 }
594
595 if let Some(auth) = current_authority.take() {
596 authorities.push(auth);
597 }
598
599 let valid_after = valid_after.ok_or_else(|| Error::Parse {
600 location: "valid-after".to_string(),
601 reason: "missing valid-after".to_string(),
602 })?;
603 let fresh_until = fresh_until.ok_or_else(|| Error::Parse {
604 location: "fresh-until".to_string(),
605 reason: "missing fresh-until".to_string(),
606 })?;
607 let valid_until = valid_until.ok_or_else(|| Error::Parse {
608 location: "valid-until".to_string(),
609 reason: "missing valid-until".to_string(),
610 })?;
611
612 Ok(Self {
613 version,
614 version_flavor,
615 is_consensus,
616 is_vote,
617 is_microdescriptor,
618 consensus_method,
619 consensus_methods,
620 published,
621 valid_after,
622 fresh_until,
623 valid_until,
624 vote_delay,
625 dist_delay,
626 client_versions,
627 server_versions,
628 known_flags,
629 recommended_client_protocols,
630 recommended_relay_protocols,
631 required_client_protocols,
632 required_relay_protocols,
633 params,
634 shared_randomness_previous,
635 shared_randomness_current,
636 bandwidth_weights,
637 authorities,
638 signatures,
639 raw_content,
640 unrecognized_lines,
641 })
642 }
643
644 fn to_descriptor_string(&self) -> String {
645 let mut result = String::new();
646
647 if self.is_microdescriptor {
648 result.push_str(&format!(
649 "network-status-version {} microdesc\n",
650 self.version
651 ));
652 } else {
653 result.push_str(&format!("network-status-version {}\n", self.version));
654 }
655
656 if self.is_consensus {
657 result.push_str("vote-status consensus\n");
658 } else {
659 result.push_str("vote-status vote\n");
660 }
661
662 if let Some(method) = self.consensus_method {
663 result.push_str(&format!("consensus-method {}\n", method));
664 }
665
666 if let Some(ref methods) = self.consensus_methods {
667 let methods_str: Vec<String> = methods.iter().map(|m| m.to_string()).collect();
668 result.push_str(&format!("consensus-methods {}\n", methods_str.join(" ")));
669 }
670
671 if let Some(published) = self.published {
672 result.push_str(&format!(
673 "published {}\n",
674 published.format("%Y-%m-%d %H:%M:%S")
675 ));
676 }
677
678 result.push_str(&format!(
679 "valid-after {}\n",
680 self.valid_after.format("%Y-%m-%d %H:%M:%S")
681 ));
682 result.push_str(&format!(
683 "fresh-until {}\n",
684 self.fresh_until.format("%Y-%m-%d %H:%M:%S")
685 ));
686 result.push_str(&format!(
687 "valid-until {}\n",
688 self.valid_until.format("%Y-%m-%d %H:%M:%S")
689 ));
690
691 if let (Some(vote), Some(dist)) = (self.vote_delay, self.dist_delay) {
692 result.push_str(&format!("voting-delay {} {}\n", vote, dist));
693 }
694
695 if !self.client_versions.is_empty() {
696 let versions: Vec<String> =
697 self.client_versions.iter().map(|v| v.to_string()).collect();
698 result.push_str(&format!("client-versions {}\n", versions.join(",")));
699 }
700
701 if !self.server_versions.is_empty() {
702 let versions: Vec<String> =
703 self.server_versions.iter().map(|v| v.to_string()).collect();
704 result.push_str(&format!("server-versions {}\n", versions.join(",")));
705 }
706
707 if !self.known_flags.is_empty() {
708 result.push_str(&format!("known-flags {}\n", self.known_flags.join(" ")));
709 }
710
711 result
712 }
713
714 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
715 Ok(compute_digest(&self.raw_content, hash, encoding))
716 }
717
718 fn raw_content(&self) -> &[u8] {
719 &self.raw_content
720 }
721
722 fn unrecognized_lines(&self) -> &[String] {
723 &self.unrecognized_lines
724 }
725}
726
727impl FromStr for NetworkStatusDocument {
728 type Err = Error;
729
730 fn from_str(s: &str) -> Result<Self, Self::Err> {
731 Self::parse(s)
732 }
733}
734
735impl fmt::Display for NetworkStatusDocument {
736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737 write!(f, "{}", self.to_descriptor_string())
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 const EXAMPLE_CONSENSUS: &str = r#"network-status-version 3
746vote-status consensus
747consensus-method 26
748valid-after 2017-05-25 04:46:30
749fresh-until 2017-05-25 04:46:40
750valid-until 2017-05-25 04:46:50
751voting-delay 2 2
752client-versions
753server-versions
754known-flags Authority Exit Fast Guard HSDir NoEdConsensus Running Stable V2Dir Valid
755recommended-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
756recommended-relay-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
757required-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
758required-relay-protocols Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=3-4 LinkAuth=1 Microdesc=1 Relay=1-2
759dir-source test001a 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
760contact auth1@test.test
761vote-digest 2E7177224BBA39B505F7608FF376C07884CF926F
762dir-source test000a BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7000 5000
763contact auth0@test.test
764vote-digest 5DD41617166FFB82882A117EEFDA0353A2794DC5
765directory-footer
766bandwidth-weights Wbd=3333 Wbe=0 Wbg=0 Wbm=10000
767directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
768-----BEGIN SIGNATURE-----
769Ho0rLojfLHs9cSPFxe6znuGuFU8BvRr6gnH1gULTjUZO0NSQvo5N628KFeAsq+pT
770-----END SIGNATURE-----
771"#;
772
773 #[test]
774 fn test_parse_consensus() {
775 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
776 assert_eq!(doc.version, 3);
777 assert!(doc.is_consensus);
778 assert!(!doc.is_vote);
779 assert!(!doc.is_microdescriptor);
780 assert_eq!(doc.consensus_method, Some(26));
781 assert_eq!(doc.vote_delay, Some(2));
782 assert_eq!(doc.dist_delay, Some(2));
783 }
784
785 #[test]
786 fn test_parse_known_flags() {
787 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
788 assert!(doc.known_flags.contains(&"Authority".to_string()));
789 assert!(doc.known_flags.contains(&"Exit".to_string()));
790 assert!(doc.known_flags.contains(&"Fast".to_string()));
791 assert!(doc.known_flags.contains(&"Guard".to_string()));
792 assert!(doc.known_flags.contains(&"HSDir".to_string()));
793 assert!(doc.known_flags.contains(&"Running".to_string()));
794 assert!(doc.known_flags.contains(&"Stable".to_string()));
795 assert!(doc.known_flags.contains(&"Valid".to_string()));
796 }
797
798 #[test]
799 fn test_parse_protocols() {
800 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
801 assert_eq!(
802 doc.recommended_client_protocols.get("Cons"),
803 Some(&vec![1, 2])
804 );
805 assert_eq!(doc.recommended_client_protocols.get("Link"), Some(&vec![4]));
806 assert_eq!(doc.required_relay_protocols.get("Link"), Some(&vec![3, 4]));
807 }
808
809 #[test]
810 fn test_parse_authorities() {
811 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
812 assert_eq!(doc.authorities.len(), 2);
813 let auth1 = &doc.authorities[0];
814 assert_eq!(auth1.nickname, "test001a");
815 assert_eq!(auth1.v3ident, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
816 assert_eq!(auth1.contact, Some("auth1@test.test".to_string()));
817 assert_eq!(
818 auth1.vote_digest,
819 Some("2E7177224BBA39B505F7608FF376C07884CF926F".to_string())
820 );
821 }
822
823 #[test]
824 fn test_parse_bandwidth_weights() {
825 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
826 assert_eq!(doc.bandwidth_weights.get("Wbd"), Some(&3333));
827 assert_eq!(doc.bandwidth_weights.get("Wbe"), Some(&0));
828 assert_eq!(doc.bandwidth_weights.get("Wbm"), Some(&10000));
829 }
830
831 #[test]
832 fn test_parse_signatures() {
833 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
834 assert_eq!(doc.signatures.len(), 1);
835 let sig = &doc.signatures[0];
836 assert_eq!(sig.identity, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
837 assert_eq!(
838 sig.signing_key_digest,
839 "9FBF54D6A62364320308A615BF4CF6B27B254FAD"
840 );
841 assert!(sig.signature.contains("BEGIN SIGNATURE"));
842 }
843
844 #[test]
845 fn test_parse_timestamps() {
846 let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
847 assert_eq!(
848 doc.valid_after.format("%Y-%m-%d %H:%M:%S").to_string(),
849 "2017-05-25 04:46:30"
850 );
851 assert_eq!(
852 doc.fresh_until.format("%Y-%m-%d %H:%M:%S").to_string(),
853 "2017-05-25 04:46:40"
854 );
855 assert_eq!(
856 doc.valid_until.format("%Y-%m-%d %H:%M:%S").to_string(),
857 "2017-05-25 04:46:50"
858 );
859 }
860
861 #[test]
862 fn test_microdescriptor_consensus() {
863 let content = "network-status-version 3 microdesc
864vote-status consensus
865valid-after 2017-05-25 04:46:30
866fresh-until 2017-05-25 04:46:40
867valid-until 2017-05-25 04:46:50
868";
869 let doc = NetworkStatusDocument::parse(content).unwrap();
870 assert!(doc.is_microdescriptor);
871 assert_eq!(doc.version_flavor, "microdesc");
872 }
873
874 use proptest::prelude::*;
875
876 fn valid_timestamp() -> impl Strategy<Value = DateTime<Utc>> {
877 (
878 2015u32..2025,
879 1u32..13,
880 1u32..29,
881 0u32..24,
882 0u32..60,
883 0u32..60,
884 )
885 .prop_map(|(year, month, day, hour, min, sec)| {
886 let naive = chrono::NaiveDate::from_ymd_opt(year as i32, month, day)
887 .unwrap()
888 .and_hms_opt(hour, min, sec)
889 .unwrap();
890 naive.and_utc()
891 })
892 }
893
894 fn valid_flag() -> impl Strategy<Value = String> {
895 prop_oneof![
896 Just("Authority".to_string()),
897 Just("Exit".to_string()),
898 Just("Fast".to_string()),
899 Just("Guard".to_string()),
900 Just("HSDir".to_string()),
901 Just("Running".to_string()),
902 Just("Stable".to_string()),
903 Just("Valid".to_string()),
904 ]
905 }
906
907 fn simple_consensus() -> impl Strategy<Value = NetworkStatusDocument> {
908 (
909 valid_timestamp(),
910 valid_timestamp(),
911 valid_timestamp(),
912 proptest::collection::vec(valid_flag(), 1..5),
913 1u32..30,
914 )
915 .prop_map(|(valid_after, fresh_until, valid_until, flags, method)| {
916 let mut doc = NetworkStatusDocument {
917 version: 3,
918 version_flavor: String::new(),
919 is_consensus: true,
920 is_vote: false,
921 is_microdescriptor: false,
922 consensus_method: Some(method),
923 consensus_methods: None,
924 published: None,
925 valid_after,
926 fresh_until,
927 valid_until,
928 vote_delay: Some(2),
929 dist_delay: Some(2),
930 client_versions: Vec::new(),
931 server_versions: Vec::new(),
932 known_flags: flags,
933 recommended_client_protocols: HashMap::new(),
934 recommended_relay_protocols: HashMap::new(),
935 required_client_protocols: HashMap::new(),
936 required_relay_protocols: HashMap::new(),
937 params: HashMap::new(),
938 shared_randomness_previous: None,
939 shared_randomness_current: None,
940 bandwidth_weights: HashMap::new(),
941 authorities: Vec::new(),
942 signatures: Vec::new(),
943 raw_content: Vec::new(),
944 unrecognized_lines: Vec::new(),
945 };
946 doc.known_flags.sort();
947 doc.known_flags.dedup();
948 doc
949 })
950 }
951
952 proptest! {
953 #![proptest_config(ProptestConfig::with_cases(100))]
954
955 #[test]
956 fn prop_consensus_roundtrip(doc in simple_consensus()) {
957 let serialized = doc.to_descriptor_string();
958 let parsed = NetworkStatusDocument::parse(&serialized);
959
960 prop_assert!(parsed.is_ok(), "Failed to parse serialized consensus: {:?}", parsed.err());
961
962 let parsed = parsed.unwrap();
963
964 prop_assert_eq!(doc.version, parsed.version, "version mismatch");
965 prop_assert_eq!(doc.is_consensus, parsed.is_consensus, "is_consensus mismatch");
966 prop_assert_eq!(doc.is_vote, parsed.is_vote, "is_vote mismatch");
967 prop_assert_eq!(doc.consensus_method, parsed.consensus_method, "consensus_method mismatch");
968 prop_assert_eq!(doc.vote_delay, parsed.vote_delay, "vote_delay mismatch");
969 prop_assert_eq!(doc.dist_delay, parsed.dist_delay, "dist_delay mismatch");
970
971 prop_assert_eq!(
972 doc.valid_after.format("%Y-%m-%d %H:%M:%S").to_string(),
973 parsed.valid_after.format("%Y-%m-%d %H:%M:%S").to_string(),
974 "valid_after mismatch"
975 );
976 prop_assert_eq!(
977 doc.fresh_until.format("%Y-%m-%d %H:%M:%S").to_string(),
978 parsed.fresh_until.format("%Y-%m-%d %H:%M:%S").to_string(),
979 "fresh_until mismatch"
980 );
981 prop_assert_eq!(
982 doc.valid_until.format("%Y-%m-%d %H:%M:%S").to_string(),
983 parsed.valid_until.format("%Y-%m-%d %H:%M:%S").to_string(),
984 "valid_until mismatch"
985 );
986
987 let mut doc_flags = doc.known_flags.clone();
988 let mut parsed_flags = parsed.known_flags.clone();
989 doc_flags.sort();
990 parsed_flags.sort();
991 prop_assert_eq!(doc_flags, parsed_flags, "known_flags mismatch");
992 }
993 }
994}