1use std::collections::HashMap;
93use std::fmt;
94use std::str::FromStr;
95
96use chrono::{DateTime, NaiveDateTime, Utc};
97use derive_builder::Builder;
98
99use crate::Error;
100
101use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
102
103type ConnBiDirectResult = Result<(DateTime<Utc>, u32, u32, u32, u32, u32), Error>;
105
106type PaddingCountsResult = Result<(DateTime<Utc>, u32, HashMap<String, String>), Error>;
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
135pub enum DirResponse {
136 Ok,
138 NotEnoughSigs,
140 Unavailable,
142 NotFound,
144 NotModified,
146 Busy,
148}
149
150impl FromStr for DirResponse {
151 type Err = Error;
152
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 match s.to_lowercase().as_str() {
155 "ok" => Ok(DirResponse::Ok),
156 "not-enough-sigs" => Ok(DirResponse::NotEnoughSigs),
157 "unavailable" => Ok(DirResponse::Unavailable),
158 "not-found" => Ok(DirResponse::NotFound),
159 "not-modified" => Ok(DirResponse::NotModified),
160 "busy" => Ok(DirResponse::Busy),
161 _ => Err(Error::Parse {
162 location: "DirResponse".to_string(),
163 reason: format!("unknown dir response: {}", s),
164 }),
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub enum DirStat {
198 Complete,
200 Timeout,
202 Running,
204 Min,
206 Max,
208 D1,
210 D2,
212 D3,
214 D4,
216 D6,
218 D7,
220 D8,
222 D9,
224 Q1,
226 Q3,
228 Md,
230}
231
232impl FromStr for DirStat {
233 type Err = Error;
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 match s.to_lowercase().as_str() {
237 "complete" => Ok(DirStat::Complete),
238 "timeout" => Ok(DirStat::Timeout),
239 "running" => Ok(DirStat::Running),
240 "min" => Ok(DirStat::Min),
241 "max" => Ok(DirStat::Max),
242 "d1" => Ok(DirStat::D1),
243 "d2" => Ok(DirStat::D2),
244 "d3" => Ok(DirStat::D3),
245 "d4" => Ok(DirStat::D4),
246 "d6" => Ok(DirStat::D6),
247 "d7" => Ok(DirStat::D7),
248 "d8" => Ok(DirStat::D8),
249 "d9" => Ok(DirStat::D9),
250 "q1" => Ok(DirStat::Q1),
251 "q3" => Ok(DirStat::Q3),
252 "md" => Ok(DirStat::Md),
253 _ => Err(Error::Parse {
254 location: "DirStat".to_string(),
255 reason: format!("unknown dir stat: {}", s),
256 }),
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq)]
289pub struct BandwidthHistory {
290 pub end_time: DateTime<Utc>,
292
293 pub interval: u32,
295
296 pub values: Vec<i64>,
300}
301
302#[derive(Debug, Clone, PartialEq)]
323pub struct Transport {
324 pub name: String,
326
327 pub address: Option<String>,
329
330 pub port: Option<u16>,
332
333 pub args: Vec<String>,
335}
336
337#[derive(Debug, Clone, PartialEq, Builder)]
426#[builder(setter(into, strip_option))]
427pub struct ExtraInfoDescriptor {
428 pub nickname: String,
430
431 pub fingerprint: String,
435
436 pub published: DateTime<Utc>,
438
439 #[builder(default)]
441 pub geoip_db_digest: Option<String>,
442
443 #[builder(default)]
445 pub geoip6_db_digest: Option<String>,
446
447 pub transports: HashMap<String, Transport>,
451
452 #[builder(default)]
454 pub read_history: Option<BandwidthHistory>,
455
456 #[builder(default)]
458 pub write_history: Option<BandwidthHistory>,
459
460 #[builder(default)]
462 pub dir_read_history: Option<BandwidthHistory>,
463
464 #[builder(default)]
466 pub dir_write_history: Option<BandwidthHistory>,
467
468 #[builder(default)]
470 pub conn_bi_direct_end: Option<DateTime<Utc>>,
471
472 #[builder(default)]
474 pub conn_bi_direct_interval: Option<u32>,
475
476 #[builder(default)]
478 pub conn_bi_direct_below: Option<u32>,
479
480 #[builder(default)]
482 pub conn_bi_direct_read: Option<u32>,
483
484 #[builder(default)]
486 pub conn_bi_direct_write: Option<u32>,
487
488 #[builder(default)]
490 pub conn_bi_direct_both: Option<u32>,
491
492 #[builder(default)]
494 pub cell_stats_end: Option<DateTime<Utc>>,
495
496 #[builder(default)]
498 pub cell_stats_interval: Option<u32>,
499
500 pub cell_processed_cells: Vec<f64>,
502
503 pub cell_queued_cells: Vec<f64>,
505
506 pub cell_time_in_queue: Vec<f64>,
508
509 #[builder(default)]
511 pub cell_circuits_per_decile: Option<u32>,
512
513 #[builder(default)]
515 pub dir_stats_end: Option<DateTime<Utc>>,
516
517 #[builder(default)]
519 pub dir_stats_interval: Option<u32>,
520
521 pub dir_v3_ips: HashMap<String, u32>,
523
524 pub dir_v3_requests: HashMap<String, u32>,
526
527 pub dir_v3_responses: HashMap<DirResponse, u32>,
529
530 pub dir_v3_responses_unknown: HashMap<String, u32>,
532
533 pub dir_v3_direct_dl: HashMap<DirStat, u32>,
535
536 pub dir_v3_direct_dl_unknown: HashMap<String, u32>,
538
539 pub dir_v3_tunneled_dl: HashMap<DirStat, u32>,
541
542 pub dir_v3_tunneled_dl_unknown: HashMap<String, u32>,
544
545 pub dir_v2_ips: HashMap<String, u32>,
547
548 pub dir_v2_requests: HashMap<String, u32>,
550
551 pub dir_v2_responses: HashMap<DirResponse, u32>,
553
554 pub dir_v2_responses_unknown: HashMap<String, u32>,
556
557 pub dir_v2_direct_dl: HashMap<DirStat, u32>,
559
560 pub dir_v2_direct_dl_unknown: HashMap<String, u32>,
562
563 pub dir_v2_tunneled_dl: HashMap<DirStat, u32>,
565
566 pub dir_v2_tunneled_dl_unknown: HashMap<String, u32>,
568
569 #[builder(default)]
571 pub entry_stats_end: Option<DateTime<Utc>>,
572
573 #[builder(default)]
575 pub entry_stats_interval: Option<u32>,
576
577 pub entry_ips: HashMap<String, u32>,
579
580 #[builder(default)]
582 pub exit_stats_end: Option<DateTime<Utc>>,
583
584 #[builder(default)]
586 pub exit_stats_interval: Option<u32>,
587
588 pub exit_kibibytes_written: HashMap<PortKey, u64>,
590
591 pub exit_kibibytes_read: HashMap<PortKey, u64>,
593
594 pub exit_streams_opened: HashMap<PortKey, u64>,
596
597 #[builder(default)]
599 pub bridge_stats_end: Option<DateTime<Utc>>,
600
601 #[builder(default)]
603 pub bridge_stats_interval: Option<u32>,
604
605 pub bridge_ips: HashMap<String, u32>,
607
608 pub ip_versions: HashMap<String, u32>,
610
611 pub ip_transports: HashMap<String, u32>,
613
614 #[builder(default)]
616 pub hs_stats_end: Option<DateTime<Utc>>,
617
618 #[builder(default)]
620 pub hs_rend_cells: Option<u64>,
621
622 pub hs_rend_cells_attr: HashMap<String, String>,
624
625 #[builder(default)]
627 pub hs_dir_onions_seen: Option<u64>,
628
629 pub hs_dir_onions_seen_attr: HashMap<String, String>,
631
632 #[builder(default)]
634 pub padding_counts_end: Option<DateTime<Utc>>,
635
636 #[builder(default)]
638 pub padding_counts_interval: Option<u32>,
639
640 pub padding_counts: HashMap<String, String>,
642
643 #[builder(default)]
645 pub ed25519_certificate: Option<String>,
646
647 #[builder(default)]
649 pub ed25519_signature: Option<String>,
650
651 #[builder(default)]
653 pub signature: Option<String>,
654
655 #[builder(default)]
659 pub router_digest: Option<String>,
660
661 #[builder(default)]
663 pub router_digest_sha256: Option<String>,
664
665 raw_content: Vec<u8>,
667
668 unrecognized_lines: Vec<String>,
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, Hash)]
690pub enum PortKey {
691 Port(u16),
693
694 Other,
696}
697
698impl fmt::Display for PortKey {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 match self {
701 PortKey::Port(p) => write!(f, "{}", p),
702 PortKey::Other => write!(f, "other"),
703 }
704 }
705}
706
707impl Default for ExtraInfoDescriptor {
708 fn default() -> Self {
709 Self {
710 nickname: String::new(),
711 fingerprint: String::new(),
712 published: DateTime::from_timestamp(0, 0).unwrap(),
713 geoip_db_digest: None,
714 geoip6_db_digest: None,
715 transports: HashMap::new(),
716 read_history: None,
717 write_history: None,
718 dir_read_history: None,
719 dir_write_history: None,
720 conn_bi_direct_end: None,
721 conn_bi_direct_interval: None,
722 conn_bi_direct_below: None,
723 conn_bi_direct_read: None,
724 conn_bi_direct_write: None,
725 conn_bi_direct_both: None,
726 cell_stats_end: None,
727 cell_stats_interval: None,
728 cell_processed_cells: Vec::new(),
729 cell_queued_cells: Vec::new(),
730 cell_time_in_queue: Vec::new(),
731 cell_circuits_per_decile: None,
732 dir_stats_end: None,
733 dir_stats_interval: None,
734 dir_v3_ips: HashMap::new(),
735 dir_v3_requests: HashMap::new(),
736 dir_v3_responses: HashMap::new(),
737 dir_v3_responses_unknown: HashMap::new(),
738 dir_v3_direct_dl: HashMap::new(),
739 dir_v3_direct_dl_unknown: HashMap::new(),
740 dir_v3_tunneled_dl: HashMap::new(),
741 dir_v3_tunneled_dl_unknown: HashMap::new(),
742 dir_v2_ips: HashMap::new(),
743 dir_v2_requests: HashMap::new(),
744 dir_v2_responses: HashMap::new(),
745 dir_v2_responses_unknown: HashMap::new(),
746 dir_v2_direct_dl: HashMap::new(),
747 dir_v2_direct_dl_unknown: HashMap::new(),
748 dir_v2_tunneled_dl: HashMap::new(),
749 dir_v2_tunneled_dl_unknown: HashMap::new(),
750 entry_stats_end: None,
751 entry_stats_interval: None,
752 entry_ips: HashMap::new(),
753 exit_stats_end: None,
754 exit_stats_interval: None,
755 exit_kibibytes_written: HashMap::new(),
756 exit_kibibytes_read: HashMap::new(),
757 exit_streams_opened: HashMap::new(),
758 bridge_stats_end: None,
759 bridge_stats_interval: None,
760 bridge_ips: HashMap::new(),
761 ip_versions: HashMap::new(),
762 ip_transports: HashMap::new(),
763 hs_stats_end: None,
764 hs_rend_cells: None,
765 hs_rend_cells_attr: HashMap::new(),
766 hs_dir_onions_seen: None,
767 hs_dir_onions_seen_attr: HashMap::new(),
768 padding_counts_end: None,
769 padding_counts_interval: None,
770 padding_counts: HashMap::new(),
771 ed25519_certificate: None,
772 ed25519_signature: None,
773 signature: None,
774 router_digest: None,
775 router_digest_sha256: None,
776 raw_content: Vec::new(),
777 unrecognized_lines: Vec::new(),
778 }
779 }
780}
781
782impl ExtraInfoDescriptor {
783 fn parse_extra_info_line(line: &str) -> Result<(String, String), Error> {
784 let parts: Vec<&str> = line.split_whitespace().collect();
785 if parts.len() < 2 {
786 return Err(Error::Parse {
787 location: "extra-info".to_string(),
788 reason: "extra-info line requires nickname and fingerprint".to_string(),
789 });
790 }
791 let nickname = parts[0].to_string();
792 let fingerprint = parts[1].to_string();
793 if fingerprint.len() != 40 || !fingerprint.chars().all(|c| c.is_ascii_hexdigit()) {
794 return Err(Error::Parse {
795 location: "extra-info".to_string(),
796 reason: format!("invalid fingerprint: {}", fingerprint),
797 });
798 }
799 Ok((nickname, fingerprint))
800 }
801
802 fn parse_published_line(line: &str) -> Result<DateTime<Utc>, Error> {
803 let datetime =
804 NaiveDateTime::parse_from_str(line.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
805 Error::Parse {
806 location: "published".to_string(),
807 reason: format!("invalid datetime: {} - {}", line, e),
808 }
809 })?;
810 Ok(datetime.and_utc())
811 }
812
813 fn parse_history_line(line: &str) -> Result<BandwidthHistory, Error> {
814 let paren_start = line.find(" (").ok_or_else(|| Error::Parse {
817 location: "history".to_string(),
818 reason: format!("invalid history format: missing '(': {}", line),
819 })?;
820
821 let timestamp_str = &line[..paren_start];
822 let rest = &line[paren_start + 2..]; let paren_end = rest.find(')').ok_or_else(|| Error::Parse {
826 location: "history".to_string(),
827 reason: format!("invalid history format: missing ')': {}", line),
828 })?;
829
830 let interval_part = &rest[..paren_end];
831 let values_str = rest[paren_end + 1..].trim();
832
833 let interval_str = interval_part
835 .trim()
836 .strip_suffix(" s")
837 .ok_or_else(|| Error::Parse {
838 location: "history".to_string(),
839 reason: format!("invalid interval format: {}", interval_part),
840 })?;
841
842 let end_time = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
843 .map_err(|e| Error::Parse {
844 location: "history".to_string(),
845 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
846 })?
847 .and_utc();
848
849 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
850 location: "history".to_string(),
851 reason: format!("invalid interval: {}", interval_str),
852 })?;
853
854 let values: Vec<i64> = if values_str.is_empty() {
855 Vec::new()
856 } else {
857 values_str
858 .split(',')
859 .filter(|s| !s.is_empty())
860 .map(|s| s.trim().parse::<i64>())
861 .collect::<Result<Vec<_>, _>>()
862 .map_err(|_| Error::Parse {
863 location: "history".to_string(),
864 reason: format!("invalid history values: {}", values_str),
865 })?
866 };
867
868 Ok(BandwidthHistory {
869 end_time,
870 interval,
871 values,
872 })
873 }
874
875 fn parse_timestamp_and_interval(line: &str) -> Result<(DateTime<Utc>, u32, String), Error> {
876 let paren_start = line.find(" (").ok_or_else(|| Error::Parse {
879 location: "timestamp".to_string(),
880 reason: format!("invalid timestamp format: missing '(': {}", line),
881 })?;
882
883 let timestamp_str = &line[..paren_start];
884 let rest = &line[paren_start + 2..]; let paren_end = rest.find(')').ok_or_else(|| Error::Parse {
888 location: "timestamp".to_string(),
889 reason: format!("invalid timestamp format: missing ')': {}", line),
890 })?;
891
892 let interval_part = &rest[..paren_end];
893 let remainder = rest[paren_end + 1..].trim().to_string();
894
895 let interval_str = interval_part
897 .trim()
898 .strip_suffix(" s")
899 .ok_or_else(|| Error::Parse {
900 location: "timestamp".to_string(),
901 reason: format!("invalid interval format: {}", interval_part),
902 })?;
903
904 let timestamp = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
905 .map_err(|e| Error::Parse {
906 location: "timestamp".to_string(),
907 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
908 })?
909 .and_utc();
910
911 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
912 location: "timestamp".to_string(),
913 reason: format!("invalid interval: {}", interval_str),
914 })?;
915
916 Ok((timestamp, interval, remainder))
917 }
918
919 fn parse_geoip_to_count(value: &str) -> HashMap<String, u32> {
920 let mut result = HashMap::new();
921 if value.is_empty() {
922 return result;
923 }
924 for entry in value.split(',') {
925 if let Some(eq_pos) = entry.find('=') {
926 let locale = &entry[..eq_pos];
927 let count_str = &entry[eq_pos + 1..];
928 if let Ok(count) = count_str.parse::<u32>() {
929 result.insert(locale.to_string(), count);
930 }
931 }
932 }
933 result
934 }
935
936 fn parse_dirreq_resp(value: &str) -> (HashMap<DirResponse, u32>, HashMap<String, u32>) {
937 let mut recognized = HashMap::new();
938 let mut unrecognized = HashMap::new();
939 if value.is_empty() {
940 return (recognized, unrecognized);
941 }
942 for entry in value.split(',') {
943 if let Some(eq_pos) = entry.find('=') {
944 let status = &entry[..eq_pos];
945 let count_str = &entry[eq_pos + 1..];
946 if let Ok(count) = count_str.parse::<u32>() {
947 if let Ok(dir_resp) = DirResponse::from_str(status) {
948 recognized.insert(dir_resp, count);
949 } else {
950 unrecognized.insert(status.to_string(), count);
951 }
952 }
953 }
954 }
955 (recognized, unrecognized)
956 }
957
958 fn parse_dirreq_dl(value: &str) -> (HashMap<DirStat, u32>, HashMap<String, u32>) {
959 let mut recognized = HashMap::new();
960 let mut unrecognized = HashMap::new();
961 if value.is_empty() {
962 return (recognized, unrecognized);
963 }
964 for entry in value.split(',') {
965 if let Some(eq_pos) = entry.find('=') {
966 let stat = &entry[..eq_pos];
967 let count_str = &entry[eq_pos + 1..];
968 if let Ok(count) = count_str.parse::<u32>() {
969 if let Ok(dir_stat) = DirStat::from_str(stat) {
970 recognized.insert(dir_stat, count);
971 } else {
972 unrecognized.insert(stat.to_string(), count);
973 }
974 }
975 }
976 }
977 (recognized, unrecognized)
978 }
979
980 fn parse_port_count(value: &str) -> HashMap<PortKey, u64> {
981 let mut result = HashMap::new();
982 if value.is_empty() {
983 return result;
984 }
985 for entry in value.split(',') {
986 if let Some(eq_pos) = entry.find('=') {
987 let port_str = &entry[..eq_pos];
988 let count_str = &entry[eq_pos + 1..];
989 if let Ok(count) = count_str.parse::<u64>() {
990 let port_key = if port_str == "other" {
991 PortKey::Other
992 } else if let Ok(port) = port_str.parse::<u16>() {
993 PortKey::Port(port)
994 } else {
995 continue;
996 };
997 result.insert(port_key, count);
998 }
999 }
1000 }
1001 result
1002 }
1003
1004 fn parse_cell_values(value: &str) -> Vec<f64> {
1005 if value.is_empty() {
1006 return Vec::new();
1007 }
1008 value
1009 .split(',')
1010 .filter_map(|s| s.trim().parse::<f64>().ok())
1011 .collect()
1012 }
1013
1014 fn parse_conn_bi_direct(value: &str) -> ConnBiDirectResult {
1015 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
1016 let stats: Vec<&str> = remainder.split(',').collect();
1017 if stats.len() != 4 {
1018 return Err(Error::Parse {
1019 location: "conn-bi-direct".to_string(),
1020 reason: format!("expected 4 values, got {}", stats.len()),
1021 });
1022 }
1023 let below: u32 = stats[0].parse().map_err(|_| Error::Parse {
1024 location: "conn-bi-direct".to_string(),
1025 reason: "invalid below value".to_string(),
1026 })?;
1027 let read: u32 = stats[1].parse().map_err(|_| Error::Parse {
1028 location: "conn-bi-direct".to_string(),
1029 reason: "invalid read value".to_string(),
1030 })?;
1031 let write: u32 = stats[2].parse().map_err(|_| Error::Parse {
1032 location: "conn-bi-direct".to_string(),
1033 reason: "invalid write value".to_string(),
1034 })?;
1035 let both: u32 = stats[3].parse().map_err(|_| Error::Parse {
1036 location: "conn-bi-direct".to_string(),
1037 reason: "invalid both value".to_string(),
1038 })?;
1039 Ok((timestamp, interval, below, read, write, both))
1040 }
1041
1042 fn parse_transport_line(value: &str) -> Transport {
1043 let parts: Vec<&str> = value.split_whitespace().collect();
1044 if parts.is_empty() {
1045 return Transport {
1046 name: String::new(),
1047 address: None,
1048 port: None,
1049 args: Vec::new(),
1050 };
1051 }
1052 let name = parts[0].to_string();
1053 if parts.len() < 2 {
1054 return Transport {
1055 name,
1056 address: None,
1057 port: None,
1058 args: Vec::new(),
1059 };
1060 }
1061 let addr_port = parts[1];
1062 let (address, port) = if let Some(colon_pos) = addr_port.rfind(':') {
1063 let addr = addr_port[..colon_pos]
1064 .trim_matches(|c| c == '[' || c == ']')
1065 .to_string();
1066 let port = addr_port[colon_pos + 1..].parse::<u16>().ok();
1067 (Some(addr), port)
1068 } else {
1069 (None, None)
1070 };
1071 let args: Vec<String> = parts.iter().skip(2).map(|s| s.to_string()).collect();
1072 Transport {
1073 name,
1074 address,
1075 port,
1076 args,
1077 }
1078 }
1079
1080 fn parse_hs_stats(value: &str) -> (Option<u64>, HashMap<String, String>) {
1081 let mut stat = None;
1082 let mut extra = HashMap::new();
1083 if value.is_empty() {
1084 return (stat, extra);
1085 }
1086 let parts: Vec<&str> = value.split_whitespace().collect();
1087 if let Some(first) = parts.first() {
1088 stat = first.parse::<u64>().ok();
1089 }
1090 for part in parts.iter().skip(1) {
1091 if let Some(eq_pos) = part.find('=') {
1092 let key = &part[..eq_pos];
1093 let val = &part[eq_pos + 1..];
1094 extra.insert(key.to_string(), val.to_string());
1095 }
1096 }
1097 (stat, extra)
1098 }
1099
1100 fn parse_padding_counts(value: &str) -> PaddingCountsResult {
1101 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
1102 let mut counts = HashMap::new();
1103 for part in remainder.split_whitespace() {
1104 if let Some(eq_pos) = part.find('=') {
1105 let key = &part[..eq_pos];
1106 let val = &part[eq_pos + 1..];
1107 counts.insert(key.to_string(), val.to_string());
1108 }
1109 }
1110 Ok((timestamp, interval, counts))
1111 }
1112
1113 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1114 let mut block = String::new();
1115 let mut idx = start_idx;
1116 while idx < lines.len() {
1117 let line = lines[idx];
1118 block.push_str(line);
1119 block.push('\n');
1120 if line.starts_with("-----END ") {
1121 break;
1122 }
1123 idx += 1;
1124 }
1125 (block.trim_end().to_string(), idx)
1126 }
1127
1128 fn find_digest_content(content: &str) -> Option<&str> {
1133 let start_marker = "extra-info ";
1134 let end_marker = "\nrouter-signature\n";
1135 let start = content.find(start_marker)?;
1136 let end = content.find(end_marker)?;
1137 Some(&content[start..end + end_marker.len()])
1138 }
1139
1140 pub fn is_bridge(&self) -> bool {
1167 self.router_digest.is_some()
1168 }
1169}
1170
1171impl Descriptor for ExtraInfoDescriptor {
1172 fn parse(content: &str) -> Result<Self, Error> {
1173 let raw_content = content.as_bytes().to_vec();
1174 let lines: Vec<&str> = content.lines().collect();
1175 let mut desc = ExtraInfoDescriptor {
1176 raw_content,
1177 ..Default::default()
1178 };
1179
1180 let mut idx = 0;
1181 while idx < lines.len() {
1182 let line = lines[idx];
1183
1184 if line.starts_with("@type ") {
1185 idx += 1;
1186 continue;
1187 }
1188
1189 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1190 (&line[..space_pos], line[space_pos + 1..].trim())
1191 } else {
1192 (line, "")
1193 };
1194
1195 match keyword {
1196 "extra-info" => {
1197 let (nickname, fingerprint) = Self::parse_extra_info_line(value)?;
1198 desc.nickname = nickname;
1199 desc.fingerprint = fingerprint;
1200 }
1201 "published" => {
1202 desc.published = Self::parse_published_line(value)?;
1203 }
1204 "identity-ed25519" => {
1205 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1206 desc.ed25519_certificate = Some(block);
1207 idx = end_idx;
1208 }
1209 "router-sig-ed25519" => {
1210 desc.ed25519_signature = Some(value.to_string());
1211 }
1212 "router-signature" => {
1213 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1214 desc.signature = Some(block);
1215 idx = end_idx;
1216 }
1217 "router-digest" => {
1218 desc.router_digest = Some(value.to_string());
1219 }
1220 "router-digest-sha256" => {
1221 desc.router_digest_sha256 = Some(value.to_string());
1222 }
1223 "master-key-ed25519" => {
1224 desc.ed25519_certificate = Some(value.to_string());
1225 }
1226 "geoip-db-digest" => {
1227 desc.geoip_db_digest = Some(value.to_string());
1228 }
1229 "geoip6-db-digest" => {
1230 desc.geoip6_db_digest = Some(value.to_string());
1231 }
1232 "transport" => {
1233 let transport = Self::parse_transport_line(value);
1234 desc.transports.insert(transport.name.clone(), transport);
1235 }
1236 "read-history" => {
1237 desc.read_history = Some(Self::parse_history_line(value)?);
1238 }
1239 "write-history" => {
1240 desc.write_history = Some(Self::parse_history_line(value)?);
1241 }
1242 "dirreq-read-history" => {
1243 desc.dir_read_history = Some(Self::parse_history_line(value)?);
1244 }
1245 "dirreq-write-history" => {
1246 desc.dir_write_history = Some(Self::parse_history_line(value)?);
1247 }
1248 "conn-bi-direct" => {
1249 let (ts, interval, below, read, write, both) =
1250 Self::parse_conn_bi_direct(value)?;
1251 desc.conn_bi_direct_end = Some(ts);
1252 desc.conn_bi_direct_interval = Some(interval);
1253 desc.conn_bi_direct_below = Some(below);
1254 desc.conn_bi_direct_read = Some(read);
1255 desc.conn_bi_direct_write = Some(write);
1256 desc.conn_bi_direct_both = Some(both);
1257 }
1258 "cell-stats-end" => {
1259 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1260 desc.cell_stats_end = Some(ts);
1261 desc.cell_stats_interval = Some(interval);
1262 }
1263 "cell-processed-cells" => {
1264 desc.cell_processed_cells = Self::parse_cell_values(value);
1265 }
1266 "cell-queued-cells" => {
1267 desc.cell_queued_cells = Self::parse_cell_values(value);
1268 }
1269 "cell-time-in-queue" => {
1270 desc.cell_time_in_queue = Self::parse_cell_values(value);
1271 }
1272 "cell-circuits-per-decile" => {
1273 desc.cell_circuits_per_decile = value.parse().ok();
1274 }
1275 "dirreq-stats-end" => {
1276 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1277 desc.dir_stats_end = Some(ts);
1278 desc.dir_stats_interval = Some(interval);
1279 }
1280 "dirreq-v3-ips" => {
1281 desc.dir_v3_ips = Self::parse_geoip_to_count(value);
1282 }
1283 "dirreq-v3-reqs" => {
1284 desc.dir_v3_requests = Self::parse_geoip_to_count(value);
1285 }
1286 "dirreq-v3-resp" => {
1287 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1288 desc.dir_v3_responses = recognized;
1289 desc.dir_v3_responses_unknown = unrecognized;
1290 }
1291 "dirreq-v3-direct-dl" => {
1292 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1293 desc.dir_v3_direct_dl = recognized;
1294 desc.dir_v3_direct_dl_unknown = unrecognized;
1295 }
1296 "dirreq-v3-tunneled-dl" => {
1297 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1298 desc.dir_v3_tunneled_dl = recognized;
1299 desc.dir_v3_tunneled_dl_unknown = unrecognized;
1300 }
1301 "dirreq-v2-ips" => {
1302 desc.dir_v2_ips = Self::parse_geoip_to_count(value);
1303 }
1304 "dirreq-v2-reqs" => {
1305 desc.dir_v2_requests = Self::parse_geoip_to_count(value);
1306 }
1307 "dirreq-v2-resp" => {
1308 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1309 desc.dir_v2_responses = recognized;
1310 desc.dir_v2_responses_unknown = unrecognized;
1311 }
1312 "dirreq-v2-direct-dl" => {
1313 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1314 desc.dir_v2_direct_dl = recognized;
1315 desc.dir_v2_direct_dl_unknown = unrecognized;
1316 }
1317 "dirreq-v2-tunneled-dl" => {
1318 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1319 desc.dir_v2_tunneled_dl = recognized;
1320 desc.dir_v2_tunneled_dl_unknown = unrecognized;
1321 }
1322 "entry-stats-end" => {
1323 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1324 desc.entry_stats_end = Some(ts);
1325 desc.entry_stats_interval = Some(interval);
1326 }
1327 "entry-ips" => {
1328 desc.entry_ips = Self::parse_geoip_to_count(value);
1329 }
1330 "exit-stats-end" => {
1331 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1332 desc.exit_stats_end = Some(ts);
1333 desc.exit_stats_interval = Some(interval);
1334 }
1335 "exit-kibibytes-written" => {
1336 desc.exit_kibibytes_written = Self::parse_port_count(value);
1337 }
1338 "exit-kibibytes-read" => {
1339 desc.exit_kibibytes_read = Self::parse_port_count(value);
1340 }
1341 "exit-streams-opened" => {
1342 desc.exit_streams_opened = Self::parse_port_count(value);
1343 }
1344 "bridge-stats-end" => {
1345 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1346 desc.bridge_stats_end = Some(ts);
1347 desc.bridge_stats_interval = Some(interval);
1348 }
1349 "bridge-ips" => {
1350 desc.bridge_ips = Self::parse_geoip_to_count(value);
1351 }
1352 "bridge-ip-versions" => {
1353 desc.ip_versions = Self::parse_geoip_to_count(value);
1354 }
1355 "bridge-ip-transports" => {
1356 desc.ip_transports = Self::parse_geoip_to_count(value);
1357 }
1358 "hidserv-stats-end" => {
1359 desc.hs_stats_end = Some(Self::parse_published_line(value)?);
1360 }
1361 "hidserv-rend-relayed-cells" => {
1362 let (stat, attr) = Self::parse_hs_stats(value);
1363 desc.hs_rend_cells = stat;
1364 desc.hs_rend_cells_attr = attr;
1365 }
1366 "hidserv-dir-onions-seen" => {
1367 let (stat, attr) = Self::parse_hs_stats(value);
1368 desc.hs_dir_onions_seen = stat;
1369 desc.hs_dir_onions_seen_attr = attr;
1370 }
1371 "padding-counts" => {
1372 let (ts, interval, counts) = Self::parse_padding_counts(value)?;
1373 desc.padding_counts_end = Some(ts);
1374 desc.padding_counts_interval = Some(interval);
1375 desc.padding_counts = counts;
1376 }
1377 _ => {
1378 if !line.is_empty() && !line.starts_with("-----") {
1379 desc.unrecognized_lines.push(line.to_string());
1380 }
1381 }
1382 }
1383 idx += 1;
1384 }
1385
1386 if desc.nickname.is_empty() {
1387 return Err(Error::Parse {
1388 location: "extra-info".to_string(),
1389 reason: "missing extra-info line".to_string(),
1390 });
1391 }
1392
1393 Ok(desc)
1394 }
1395
1396 fn to_descriptor_string(&self) -> String {
1397 let mut result = String::new();
1398
1399 result.push_str(&format!(
1400 "extra-info {} {}\n",
1401 self.nickname, self.fingerprint
1402 ));
1403 result.push_str(&format!(
1404 "published {}\n",
1405 self.published.format("%Y-%m-%d %H:%M:%S")
1406 ));
1407
1408 if let Some(ref history) = self.write_history {
1409 let values: String = history
1410 .values
1411 .iter()
1412 .map(|v| v.to_string())
1413 .collect::<Vec<_>>()
1414 .join(",");
1415 result.push_str(&format!(
1416 "write-history {} ({} s) {}\n",
1417 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1418 history.interval,
1419 values
1420 ));
1421 }
1422
1423 if let Some(ref history) = self.read_history {
1424 let values: String = history
1425 .values
1426 .iter()
1427 .map(|v| v.to_string())
1428 .collect::<Vec<_>>()
1429 .join(",");
1430 result.push_str(&format!(
1431 "read-history {} ({} s) {}\n",
1432 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1433 history.interval,
1434 values
1435 ));
1436 }
1437
1438 if let Some(ref history) = self.dir_write_history {
1439 let values: String = history
1440 .values
1441 .iter()
1442 .map(|v| v.to_string())
1443 .collect::<Vec<_>>()
1444 .join(",");
1445 result.push_str(&format!(
1446 "dirreq-write-history {} ({} s) {}\n",
1447 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1448 history.interval,
1449 values
1450 ));
1451 }
1452
1453 if let Some(ref history) = self.dir_read_history {
1454 let values: String = history
1455 .values
1456 .iter()
1457 .map(|v| v.to_string())
1458 .collect::<Vec<_>>()
1459 .join(",");
1460 result.push_str(&format!(
1461 "dirreq-read-history {} ({} s) {}\n",
1462 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1463 history.interval,
1464 values
1465 ));
1466 }
1467
1468 if let Some(ref digest) = self.geoip_db_digest {
1469 result.push_str(&format!("geoip-db-digest {}\n", digest));
1470 }
1471
1472 if let Some(ref digest) = self.geoip6_db_digest {
1473 result.push_str(&format!("geoip6-db-digest {}\n", digest));
1474 }
1475
1476 if let Some(ref sig) = self.signature {
1477 result.push_str("router-signature\n");
1478 result.push_str(sig);
1479 result.push('\n');
1480 }
1481
1482 if let Some(ref digest) = self.router_digest {
1483 result.push_str(&format!("router-digest {}\n", digest));
1484 }
1485
1486 result
1487 }
1488
1489 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
1490 if self.is_bridge() {
1491 match (hash, encoding) {
1492 (DigestHash::Sha1, DigestEncoding::Hex) => {
1493 self.router_digest.clone().ok_or_else(|| Error::Parse {
1494 location: "digest".to_string(),
1495 reason: "bridge descriptor missing router-digest".to_string(),
1496 })
1497 }
1498 (DigestHash::Sha256, DigestEncoding::Base64) => self
1499 .router_digest_sha256
1500 .clone()
1501 .ok_or_else(|| Error::Parse {
1502 location: "digest".to_string(),
1503 reason: "bridge descriptor missing router-digest-sha256".to_string(),
1504 }),
1505 _ => Err(Error::Parse {
1506 location: "digest".to_string(),
1507 reason: "bridge extrainfo digests only available as sha1/hex or sha256/base64"
1508 .to_string(),
1509 }),
1510 }
1511 } else {
1512 let content_str = std::str::from_utf8(&self.raw_content).map_err(|_| Error::Parse {
1513 location: "digest".to_string(),
1514 reason: "invalid UTF-8 in raw content".to_string(),
1515 })?;
1516
1517 match hash {
1518 DigestHash::Sha1 => {
1519 let digest_content =
1520 Self::find_digest_content(content_str).ok_or_else(|| Error::Parse {
1521 location: "digest".to_string(),
1522 reason: "could not find digest content boundaries".to_string(),
1523 })?;
1524 Ok(compute_digest(digest_content.as_bytes(), hash, encoding))
1525 }
1526 DigestHash::Sha256 => Ok(compute_digest(&self.raw_content, hash, encoding)),
1527 }
1528 }
1529 }
1530
1531 fn raw_content(&self) -> &[u8] {
1532 &self.raw_content
1533 }
1534
1535 fn unrecognized_lines(&self) -> &[String] {
1536 &self.unrecognized_lines
1537 }
1538}
1539
1540impl FromStr for ExtraInfoDescriptor {
1541 type Err = Error;
1542
1543 fn from_str(s: &str) -> Result<Self, Self::Err> {
1544 Self::parse(s)
1545 }
1546}
1547
1548impl fmt::Display for ExtraInfoDescriptor {
1549 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1550 write!(f, "{}", self.to_descriptor_string())
1551 }
1552}
1553
1554#[cfg(test)]
1555mod tests {
1556 use super::*;
1557
1558 const RELAY_EXTRA_INFO: &str = r#"@type extra-info 1.0
1559extra-info NINJA B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1560published 2012-05-05 17:03:50
1561write-history 2012-05-05 17:02:45 (900 s) 1082368,19456,50176,272384,485376,1850368,1132544,1790976,2459648,4091904,6310912,13701120,3209216,3871744,7873536,5440512,7287808,10561536,9979904,11247616,11982848,7590912,10611712,20728832,38534144,6839296,3173376,16678912
1562read-history 2012-05-05 17:02:45 (900 s) 3309568,9216,41984,27648,123904,2004992,364544,576512,1607680,3808256,4672512,12783616,2938880,2562048,7348224,3574784,6488064,10954752,9359360,4438016,6286336,6438912,4502528,10720256,38165504,1524736,2336768,8186880
1563dirreq-write-history 2012-05-05 17:02:45 (900 s) 0,0,0,227328,349184,382976,738304,1171456,850944,657408,1675264,987136,702464,1335296,587776,1941504,893952,533504,695296,6828032,6326272,1287168,6310912,10085376,1048576,5372928,894976,8610816
1564dirreq-read-history 2012-05-05 17:02:45 (900 s) 0,0,0,0,33792,27648,48128,46080,60416,51200,63488,64512,45056,27648,37888,48128,57344,34816,46080,50176,37888,51200,25600,33792,39936,32768,28672,30720
1565router-signature
1566-----BEGIN SIGNATURE-----
1567K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
1568k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
15697LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
1570-----END SIGNATURE-----
1571"#;
1572
1573 const BRIDGE_EXTRA_INFO: &str = r#"@type bridge-extra-info 1.0
1574extra-info ec2bridgereaac65a3 1EC248422B57D9C0BD751892FE787585407479A4
1575published 2012-06-08 02:21:27
1576write-history 2012-06-08 02:10:38 (900 s) 343040,991232,5649408
1577read-history 2012-06-08 02:10:38 (900 s) 337920,437248,3995648
1578geoip-db-digest A27BE984989AB31C50D0861C7106B17A7EEC3756
1579dirreq-stats-end 2012-06-07 06:33:46 (86400 s)
1580dirreq-v3-ips
1581dirreq-v3-reqs
1582dirreq-v3-resp ok=72,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1583dirreq-v3-direct-dl complete=0,timeout=0,running=0
1584dirreq-v3-tunneled-dl complete=68,timeout=4,running=0,min=2626,d1=7795,d2=14369,q1=18695,d3=29117,d4=52562,md=70626,d6=102271,d7=164175,q3=181522,d8=271682,d9=563791,max=32136142
1585bridge-stats-end 2012-06-07 06:33:53 (86400 s)
1586bridge-ips cn=16,ir=16,sy=16,us=16
1587router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1588"#;
1589
1590 const ED25519_EXTRA_INFO: &str = r#"@type extra-info 1.0
1591extra-info silverfoxden 4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E
1592identity-ed25519
1593-----BEGIN ED25519 CERT-----
1594AQQABhz0AQFcf5tGWLvPvr1sktoezBB95j6tAWSECa3Eo2ZuBtRNAQAgBABFAwSN
1595GcRlGIte4I1giLvQSTcXefT93rvx2PZ8wEDewxWdy6tzcLouPfE3Beu/eUyg8ntt
1596YuVlzi50WXzGlGnPmeounGLo0EDHTGzcLucFWpe0g/0ia6UDqgQiAySMBwI=
1597-----END ED25519 CERT-----
1598published 2015-08-22 19:21:12
1599write-history 2015-08-22 19:20:44 (14400 s) 14409728,23076864,7756800,6234112,7446528,12290048
1600read-history 2015-08-22 19:20:44 (14400 s) 20449280,23888896,9099264,7185408,8880128,13230080
1601geoip-db-digest 6882B8663F74C23E26E3C2274C24CAB2E82D67A2
1602geoip6-db-digest F063BD5247EB9829E6B9E586393D7036656DAF44
1603dirreq-stats-end 2015-08-22 11:58:30 (86400 s)
1604dirreq-v3-ips
1605dirreq-v3-reqs
1606dirreq-v3-resp ok=0,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1607dirreq-v3-direct-dl complete=0,timeout=0,running=0
1608dirreq-v3-tunneled-dl complete=0,timeout=0,running=0
1609router-sig-ed25519 g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw
1610router-signature
1611-----BEGIN SIGNATURE-----
1612R7kNaIWZrg3n3FWFBRMlEK2cbnha7gUIs8ToksLe+SF0dgoZiLyV3GKrnzdE/K6D
1613qdiOMN7eK04MOZVlgxkA5ayi61FTYVveK1HrDbJ+sEUwsviVGdif6kk/9DXOiyIJ
16147wP/tofgHj/aCbFZb1PGU0zrEVLa72hVJ6cCW8w/t1s=
1615-----END SIGNATURE-----
1616"#;
1617
1618 #[test]
1619 fn test_parse_relay_extra_info() {
1620 let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
1621
1622 assert_eq!(desc.nickname, "NINJA");
1623 assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
1624 assert_eq!(
1625 desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
1626 "2012-05-05 17:03:50"
1627 );
1628 assert!(!desc.is_bridge());
1629
1630 let write_history = desc.write_history.as_ref().unwrap();
1631 assert_eq!(write_history.interval, 900);
1632 assert_eq!(write_history.values.len(), 28);
1633 assert_eq!(write_history.values[0], 1082368);
1634
1635 let read_history = desc.read_history.as_ref().unwrap();
1636 assert_eq!(read_history.interval, 900);
1637 assert_eq!(read_history.values.len(), 28);
1638 assert_eq!(read_history.values[0], 3309568);
1639
1640 assert!(desc.signature.is_some());
1641 }
1642
1643 #[test]
1644 fn test_parse_bridge_extra_info() {
1645 let desc = ExtraInfoDescriptor::parse(BRIDGE_EXTRA_INFO).unwrap();
1646
1647 assert_eq!(desc.nickname, "ec2bridgereaac65a3");
1648 assert_eq!(desc.fingerprint, "1EC248422B57D9C0BD751892FE787585407479A4");
1649 assert!(desc.is_bridge());
1650 assert_eq!(
1651 desc.router_digest,
1652 Some("00A2AECCEAD3FEE033CFE29893387143146728EC".to_string())
1653 );
1654
1655 assert_eq!(
1656 desc.geoip_db_digest,
1657 Some("A27BE984989AB31C50D0861C7106B17A7EEC3756".to_string())
1658 );
1659
1660 assert_eq!(desc.dir_stats_interval, Some(86400));
1661 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&72));
1662 assert_eq!(
1663 desc.dir_v3_responses.get(&DirResponse::NotEnoughSigs),
1664 Some(&0)
1665 );
1666
1667 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&0));
1668 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Complete), Some(&68));
1669 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Timeout), Some(&4));
1670
1671 assert_eq!(desc.bridge_stats_interval, Some(86400));
1672 assert_eq!(desc.bridge_ips.get("cn"), Some(&16));
1673 assert_eq!(desc.bridge_ips.get("us"), Some(&16));
1674 }
1675
1676 #[test]
1677 fn test_parse_ed25519_extra_info() {
1678 let desc = ExtraInfoDescriptor::parse(ED25519_EXTRA_INFO).unwrap();
1679
1680 assert_eq!(desc.nickname, "silverfoxden");
1681 assert_eq!(desc.fingerprint, "4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E");
1682 assert!(!desc.is_bridge());
1683
1684 assert!(desc.ed25519_certificate.is_some());
1685 assert!(desc
1686 .ed25519_certificate
1687 .as_ref()
1688 .unwrap()
1689 .contains("ED25519 CERT"));
1690
1691 assert!(desc.ed25519_signature.is_some());
1692 assert!(desc
1693 .ed25519_signature
1694 .as_ref()
1695 .unwrap()
1696 .starts_with("g6Zg7Er8K7C1"));
1697
1698 assert_eq!(
1699 desc.geoip_db_digest,
1700 Some("6882B8663F74C23E26E3C2274C24CAB2E82D67A2".to_string())
1701 );
1702 assert_eq!(
1703 desc.geoip6_db_digest,
1704 Some("F063BD5247EB9829E6B9E586393D7036656DAF44".to_string())
1705 );
1706
1707 let write_history = desc.write_history.as_ref().unwrap();
1708 assert_eq!(write_history.interval, 14400);
1709 assert_eq!(write_history.values.len(), 6);
1710 }
1711
1712 #[test]
1713 fn test_dir_response_parsing() {
1714 assert_eq!(DirResponse::from_str("ok").unwrap(), DirResponse::Ok);
1715 assert_eq!(
1716 DirResponse::from_str("not-enough-sigs").unwrap(),
1717 DirResponse::NotEnoughSigs
1718 );
1719 assert_eq!(
1720 DirResponse::from_str("unavailable").unwrap(),
1721 DirResponse::Unavailable
1722 );
1723 assert_eq!(
1724 DirResponse::from_str("not-found").unwrap(),
1725 DirResponse::NotFound
1726 );
1727 assert_eq!(
1728 DirResponse::from_str("not-modified").unwrap(),
1729 DirResponse::NotModified
1730 );
1731 assert_eq!(DirResponse::from_str("busy").unwrap(), DirResponse::Busy);
1732 }
1733
1734 #[test]
1735 fn test_dir_stat_parsing() {
1736 assert_eq!(DirStat::from_str("complete").unwrap(), DirStat::Complete);
1737 assert_eq!(DirStat::from_str("timeout").unwrap(), DirStat::Timeout);
1738 assert_eq!(DirStat::from_str("running").unwrap(), DirStat::Running);
1739 assert_eq!(DirStat::from_str("min").unwrap(), DirStat::Min);
1740 assert_eq!(DirStat::from_str("max").unwrap(), DirStat::Max);
1741 assert_eq!(DirStat::from_str("d1").unwrap(), DirStat::D1);
1742 assert_eq!(DirStat::from_str("q1").unwrap(), DirStat::Q1);
1743 assert_eq!(DirStat::from_str("md").unwrap(), DirStat::Md);
1744 }
1745
1746 #[test]
1747 fn test_history_parsing() {
1748 let history = ExtraInfoDescriptor::parse_history_line(
1749 "2012-05-05 17:02:45 (900 s) 1082368,19456,50176",
1750 )
1751 .unwrap();
1752
1753 assert_eq!(history.interval, 900);
1754 assert_eq!(history.values, vec![1082368, 19456, 50176]);
1755 }
1756
1757 #[test]
1758 fn test_geoip_to_count_parsing() {
1759 let result = ExtraInfoDescriptor::parse_geoip_to_count("cn=16,ir=16,us=8");
1760 assert_eq!(result.get("cn"), Some(&16));
1761 assert_eq!(result.get("ir"), Some(&16));
1762 assert_eq!(result.get("us"), Some(&8));
1763 }
1764
1765 #[test]
1766 fn test_port_count_parsing() {
1767 let result = ExtraInfoDescriptor::parse_port_count("80=1000,443=2000,other=500");
1768 assert_eq!(result.get(&PortKey::Port(80)), Some(&1000));
1769 assert_eq!(result.get(&PortKey::Port(443)), Some(&2000));
1770 assert_eq!(result.get(&PortKey::Other), Some(&500));
1771 }
1772
1773 #[test]
1774 fn test_missing_extra_info_line() {
1775 let content = "published 2012-05-05 17:03:50\n";
1776 let result = ExtraInfoDescriptor::parse(content);
1777 assert!(result.is_err());
1778 }
1779
1780 #[test]
1781 fn test_invalid_fingerprint() {
1782 let content = "extra-info NINJA INVALID\npublished 2012-05-05 17:03:50\n";
1783 let result = ExtraInfoDescriptor::parse(content);
1784 assert!(result.is_err());
1785 }
1786
1787 #[test]
1788 fn test_conn_bi_direct() {
1789 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1790published 2012-05-05 17:03:50
1791conn-bi-direct 2012-05-03 12:07:50 (500 s) 277431,12089,0,2134
1792"#;
1793 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1794 assert!(desc.conn_bi_direct_end.is_some());
1795 assert_eq!(desc.conn_bi_direct_interval, Some(500));
1796 assert_eq!(desc.conn_bi_direct_below, Some(277431));
1797 assert_eq!(desc.conn_bi_direct_read, Some(12089));
1798 assert_eq!(desc.conn_bi_direct_write, Some(0));
1799 assert_eq!(desc.conn_bi_direct_both, Some(2134));
1800 }
1801
1802 #[test]
1803 fn test_cell_circuits_per_decile() {
1804 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1805published 2012-05-05 17:03:50
1806cell-circuits-per-decile 25
1807"#;
1808 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1809 assert_eq!(desc.cell_circuits_per_decile, Some(25));
1810 }
1811
1812 #[test]
1813 fn test_hidden_service_stats() {
1814 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1815published 2012-05-05 17:03:50
1816hidserv-stats-end 2012-05-03 12:07:50
1817hidserv-rend-relayed-cells 345 spiffy=true snowmen=neat
1818hidserv-dir-onions-seen 123
1819"#;
1820 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1821 assert!(desc.hs_stats_end.is_some());
1822 assert_eq!(desc.hs_rend_cells, Some(345));
1823 assert_eq!(
1824 desc.hs_rend_cells_attr.get("spiffy"),
1825 Some(&"true".to_string())
1826 );
1827 assert_eq!(
1828 desc.hs_rend_cells_attr.get("snowmen"),
1829 Some(&"neat".to_string())
1830 );
1831 assert_eq!(desc.hs_dir_onions_seen, Some(123));
1832 }
1833
1834 #[test]
1835 fn test_padding_counts() {
1836 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1837published 2012-05-05 17:03:50
1838padding-counts 2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000
1839"#;
1840 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1841 assert!(desc.padding_counts_end.is_some());
1842 assert_eq!(desc.padding_counts_interval, Some(86400));
1843 assert_eq!(
1844 desc.padding_counts.get("bin-size"),
1845 Some(&"10000".to_string())
1846 );
1847 assert_eq!(
1848 desc.padding_counts.get("write-drop"),
1849 Some(&"0".to_string())
1850 );
1851 assert_eq!(
1852 desc.padding_counts.get("write-pad"),
1853 Some(&"10000".to_string())
1854 );
1855 }
1856
1857 #[test]
1858 fn test_transport_line() {
1859 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1860published 2012-05-05 17:03:50
1861transport obfs2 83.212.96.201:33570
1862"#;
1863 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1864 assert!(desc.transports.contains_key("obfs2"));
1865 let transport = desc.transports.get("obfs2").unwrap();
1866 assert_eq!(transport.address, Some("83.212.96.201".to_string()));
1867 assert_eq!(transport.port, Some(33570));
1868 }
1869
1870 #[test]
1871 fn test_bridge_ip_versions() {
1872 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1873published 2012-05-05 17:03:50
1874bridge-ip-versions v4=16,v6=40
1875router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1876"#;
1877 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1878 assert_eq!(desc.ip_versions.get("v4"), Some(&16));
1879 assert_eq!(desc.ip_versions.get("v6"), Some(&40));
1880 }
1881
1882 #[test]
1883 fn test_bridge_ip_transports() {
1884 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1885published 2012-05-05 17:03:50
1886bridge-ip-transports <OR>=16,<??>=40
1887router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1888"#;
1889 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1890 assert_eq!(desc.ip_transports.get("<OR>"), Some(&16));
1891 assert_eq!(desc.ip_transports.get("<??>"), Some(&40));
1892 }
1893
1894 #[test]
1895 fn test_exit_stats() {
1896 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1897published 2012-05-05 17:03:50
1898exit-stats-end 2012-05-03 12:07:50 (86400 s)
1899exit-kibibytes-written 80=115533759,443=1777,other=500
1900exit-kibibytes-read 80=100,443=200
1901exit-streams-opened 80=50,443=100
1902"#;
1903 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1904 assert!(desc.exit_stats_end.is_some());
1905 assert_eq!(desc.exit_stats_interval, Some(86400));
1906 assert_eq!(
1907 desc.exit_kibibytes_written.get(&PortKey::Port(80)),
1908 Some(&115533759)
1909 );
1910 assert_eq!(
1911 desc.exit_kibibytes_written.get(&PortKey::Port(443)),
1912 Some(&1777)
1913 );
1914 assert_eq!(desc.exit_kibibytes_written.get(&PortKey::Other), Some(&500));
1915 assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Port(80)), Some(&100));
1916 assert_eq!(desc.exit_streams_opened.get(&PortKey::Port(80)), Some(&50));
1917 }
1918
1919 #[test]
1920 fn test_entry_stats() {
1921 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1922published 2012-05-05 17:03:50
1923entry-stats-end 2012-05-03 12:07:50 (86400 s)
1924entry-ips uk=5,de=3,jp=2
1925"#;
1926 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1927 assert!(desc.entry_stats_end.is_some());
1928 assert_eq!(desc.entry_stats_interval, Some(86400));
1929 assert_eq!(desc.entry_ips.get("uk"), Some(&5));
1930 assert_eq!(desc.entry_ips.get("de"), Some(&3));
1931 assert_eq!(desc.entry_ips.get("jp"), Some(&2));
1932 }
1933
1934 #[test]
1935 fn test_cell_stats() {
1936 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1937published 2012-05-05 17:03:50
1938cell-stats-end 2012-05-03 12:07:50 (86400 s)
1939cell-processed-cells 2.3,-4.6,8.9
1940cell-queued-cells 1.0,2.0,3.0
1941cell-time-in-queue 10.5,20.5,30.5
1942"#;
1943 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1944 assert!(desc.cell_stats_end.is_some());
1945 assert_eq!(desc.cell_stats_interval, Some(86400));
1946 assert_eq!(desc.cell_processed_cells, vec![2.3, -4.6, 8.9]);
1947 assert_eq!(desc.cell_queued_cells, vec![1.0, 2.0, 3.0]);
1948 assert_eq!(desc.cell_time_in_queue, vec![10.5, 20.5, 30.5]);
1949 }
1950
1951 #[test]
1952 fn test_empty_history_values() {
1953 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1954published 2012-05-05 17:03:50
1955write-history 2012-05-05 17:02:45 (900 s)
1956read-history 2012-05-05 17:02:45 (900 s)
1957"#;
1958 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1959 assert!(desc.write_history.is_some());
1960 assert!(desc.read_history.is_some());
1961 assert_eq!(desc.write_history.as_ref().unwrap().values.len(), 0);
1962 assert_eq!(desc.read_history.as_ref().unwrap().values.len(), 0);
1963 }
1964
1965 #[test]
1966 fn test_empty_geoip_counts() {
1967 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1968published 2012-05-05 17:03:50
1969dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
1970dirreq-v3-ips
1971dirreq-v3-reqs
1972"#;
1973 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1974 assert!(desc.dir_stats_end.is_some());
1975 assert_eq!(desc.dir_v3_ips.len(), 0);
1976 assert_eq!(desc.dir_v3_requests.len(), 0);
1977 }
1978
1979 #[test]
1980 fn test_negative_bandwidth_values() {
1981 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1982published 2012-05-05 17:03:50
1983write-history 2012-05-05 17:02:45 (900 s) -100,200,-300,400
1984"#;
1985 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1986 let history = desc.write_history.as_ref().unwrap();
1987 assert_eq!(history.values, vec![-100, 200, -300, 400]);
1988 }
1989
1990 #[test]
1991 fn test_large_bandwidth_values() {
1992 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1993published 2012-05-05 17:03:50
1994write-history 2012-05-05 17:02:45 (900 s) 9223372036854775807,1000000000000
1995"#;
1996 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1997 let history = desc.write_history.as_ref().unwrap();
1998 assert_eq!(history.values.len(), 2);
1999 assert_eq!(history.values[0], 9223372036854775807);
2000 assert_eq!(history.values[1], 1000000000000);
2001 }
2002
2003 #[test]
2004 fn test_unrecognized_lines_captured() {
2005 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2006published 2012-05-05 17:03:50
2007unknown-keyword some value here
2008another-unknown-line with data
2009"#;
2010 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2011 assert_eq!(desc.unrecognized_lines.len(), 2);
2012 assert!(desc
2013 .unrecognized_lines
2014 .contains(&"unknown-keyword some value here".to_string()));
2015 assert!(desc
2016 .unrecognized_lines
2017 .contains(&"another-unknown-line with data".to_string()));
2018 }
2019
2020 #[test]
2021 fn test_round_trip_serialization() {
2022 let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
2023 let serialized = desc.to_descriptor_string();
2024 let reparsed = ExtraInfoDescriptor::parse(&serialized).unwrap();
2025
2026 assert_eq!(desc.nickname, reparsed.nickname);
2027 assert_eq!(desc.fingerprint, reparsed.fingerprint);
2028 assert_eq!(
2029 desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
2030 reparsed.published.format("%Y-%m-%d %H:%M:%S").to_string()
2031 );
2032
2033 if let (Some(ref orig), Some(ref new)) = (&desc.write_history, &reparsed.write_history) {
2034 assert_eq!(orig.interval, new.interval);
2035 assert_eq!(orig.values, new.values);
2036 }
2037
2038 if let (Some(ref orig), Some(ref new)) = (&desc.read_history, &reparsed.read_history) {
2039 assert_eq!(orig.interval, new.interval);
2040 assert_eq!(orig.values, new.values);
2041 }
2042 }
2043
2044 #[test]
2045 fn test_transport_with_ipv6_address() {
2046 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2047published 2012-05-05 17:03:50
2048transport obfs4 [2001:db8::1]:9001 cert=abc123
2049"#;
2050 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2051 assert!(desc.transports.contains_key("obfs4"));
2052 let transport = desc.transports.get("obfs4").unwrap();
2053 assert_eq!(transport.address, Some("2001:db8::1".to_string()));
2054 assert_eq!(transport.port, Some(9001));
2055 assert_eq!(transport.args, vec!["cert=abc123".to_string()]);
2056 }
2057
2058 #[test]
2059 fn test_transport_without_address() {
2060 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2061published 2012-05-05 17:03:50
2062transport snowflake
2063"#;
2064 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2065 assert!(desc.transports.contains_key("snowflake"));
2066 let transport = desc.transports.get("snowflake").unwrap();
2067 assert_eq!(transport.address, None);
2068 assert_eq!(transport.port, None);
2069 assert_eq!(transport.args.len(), 0);
2070 }
2071
2072 #[test]
2073 fn test_multiple_transports() {
2074 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2075published 2012-05-05 17:03:50
2076transport obfs2 192.168.1.1:9001
2077transport obfs3 192.168.1.1:9002
2078transport obfs4 192.168.1.1:9003 cert=xyz
2079"#;
2080 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2081 assert_eq!(desc.transports.len(), 3);
2082 assert!(desc.transports.contains_key("obfs2"));
2083 assert!(desc.transports.contains_key("obfs3"));
2084 assert!(desc.transports.contains_key("obfs4"));
2085 }
2086
2087 #[test]
2088 fn test_dirreq_response_with_unknown_status() {
2089 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2090published 2012-05-05 17:03:50
2091dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
2092dirreq-v3-resp ok=100,unknown-status=50,busy=25
2093"#;
2094 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2095 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&100));
2096 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Busy), Some(&25));
2097 assert_eq!(
2098 desc.dir_v3_responses_unknown.get("unknown-status"),
2099 Some(&50)
2100 );
2101 }
2102
2103 #[test]
2104 fn test_dirreq_dl_with_unknown_stat() {
2105 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2106published 2012-05-05 17:03:50
2107dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
2108dirreq-v3-direct-dl complete=100,unknown-stat=50,timeout=25
2109"#;
2110 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2111 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&100));
2112 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Timeout), Some(&25));
2113 assert_eq!(desc.dir_v3_direct_dl_unknown.get("unknown-stat"), Some(&50));
2114 }
2115
2116 #[test]
2117 fn test_hidden_service_stats_without_attributes() {
2118 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2119published 2012-05-05 17:03:50
2120hidserv-stats-end 2012-05-03 12:07:50
2121hidserv-rend-relayed-cells 12345
2122hidserv-dir-onions-seen 678
2123"#;
2124 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2125 assert_eq!(desc.hs_rend_cells, Some(12345));
2126 assert_eq!(desc.hs_rend_cells_attr.len(), 0);
2127 assert_eq!(desc.hs_dir_onions_seen, Some(678));
2128 assert_eq!(desc.hs_dir_onions_seen_attr.len(), 0);
2129 }
2130
2131 #[test]
2132 fn test_padding_counts_multiple_attributes() {
2133 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2134published 2012-05-05 17:03:50
2135padding-counts 2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000 write-total=20000 read-drop=5 read-pad=15000 read-total=25000
2136"#;
2137 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2138 assert_eq!(desc.padding_counts.len(), 7);
2139 assert_eq!(
2140 desc.padding_counts.get("bin-size"),
2141 Some(&"10000".to_string())
2142 );
2143 assert_eq!(
2144 desc.padding_counts.get("write-total"),
2145 Some(&"20000".to_string())
2146 );
2147 assert_eq!(
2148 desc.padding_counts.get("read-total"),
2149 Some(&"25000".to_string())
2150 );
2151 }
2152
2153 #[test]
2154 fn test_minimal_valid_descriptor() {
2155 let content = r#"extra-info minimal B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2156published 2012-05-05 17:03:50
2157"#;
2158 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2159 assert_eq!(desc.nickname, "minimal");
2160 assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
2161 assert!(!desc.is_bridge());
2162 assert_eq!(desc.transports.len(), 0);
2163 assert_eq!(desc.unrecognized_lines.len(), 0);
2164 }
2165
2166 #[test]
2167 fn test_type_annotation_ignored() {
2168 let content = r#"@type extra-info 1.0
2169@type bridge-extra-info 1.1
2170extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2171published 2012-05-05 17:03:50
2172"#;
2173 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2174 assert_eq!(desc.nickname, "test");
2175 assert_eq!(desc.unrecognized_lines.len(), 0);
2176 }
2177
2178 #[test]
2179 fn test_port_key_display() {
2180 assert_eq!(format!("{}", PortKey::Port(80)), "80");
2181 assert_eq!(format!("{}", PortKey::Port(443)), "443");
2182 assert_eq!(format!("{}", PortKey::Other), "other");
2183 }
2184
2185 #[test]
2186 fn test_bandwidth_history_with_single_value() {
2187 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2188published 2012-05-05 17:03:50
2189write-history 2012-05-05 17:02:45 (900 s) 1234567890
2190"#;
2191 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2192 let history = desc.write_history.as_ref().unwrap();
2193 assert_eq!(history.values.len(), 1);
2194 assert_eq!(history.values[0], 1234567890);
2195 }
2196
2197 #[test]
2198 fn test_conn_bi_direct_with_zeros() {
2199 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2200published 2012-05-05 17:03:50
2201conn-bi-direct 2012-05-03 12:07:50 (500 s) 0,0,0,0
2202"#;
2203 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2204 assert_eq!(desc.conn_bi_direct_below, Some(0));
2205 assert_eq!(desc.conn_bi_direct_read, Some(0));
2206 assert_eq!(desc.conn_bi_direct_write, Some(0));
2207 assert_eq!(desc.conn_bi_direct_both, Some(0));
2208 }
2209
2210 #[test]
2211 fn test_exit_stats_with_only_other_port() {
2212 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2213published 2012-05-05 17:03:50
2214exit-stats-end 2012-05-03 12:07:50 (86400 s)
2215exit-kibibytes-written other=1000000
2216exit-kibibytes-read other=500000
2217exit-streams-opened other=1000
2218"#;
2219 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2220 assert_eq!(
2221 desc.exit_kibibytes_written.get(&PortKey::Other),
2222 Some(&1000000)
2223 );
2224 assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Other), Some(&500000));
2225 assert_eq!(desc.exit_streams_opened.get(&PortKey::Other), Some(&1000));
2226 assert_eq!(desc.exit_kibibytes_written.len(), 1);
2227 }
2228
2229 #[test]
2230 fn test_geoip_with_special_country_codes() {
2231 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2232published 2012-05-05 17:03:50
2233bridge-stats-end 2012-05-03 12:07:50 (86400 s)
2234bridge-ips ??=100,a1=50,zz=25
2235router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
2236"#;
2237 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2238 assert_eq!(desc.bridge_ips.get("??"), Some(&100));
2239 assert_eq!(desc.bridge_ips.get("a1"), Some(&50));
2240 assert_eq!(desc.bridge_ips.get("zz"), Some(&25));
2241 }
2242}