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 timestamp_re =
815 regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
816 location: "history".to_string(),
817 reason: format!("regex error: {}", e),
818 })?;
819
820 let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
821 location: "history".to_string(),
822 reason: format!("invalid history format: {}", line),
823 })?;
824
825 let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
826 let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
827 let values_str = caps.get(3).map(|m| m.as_str().trim()).unwrap_or("");
828
829 let end_time = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
830 .map_err(|e| Error::Parse {
831 location: "history".to_string(),
832 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
833 })?
834 .and_utc();
835
836 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
837 location: "history".to_string(),
838 reason: format!("invalid interval: {}", interval_str),
839 })?;
840
841 let values: Vec<i64> = if values_str.is_empty() {
842 Vec::new()
843 } else {
844 values_str
845 .split(',')
846 .filter(|s| !s.is_empty())
847 .map(|s| s.trim().parse::<i64>())
848 .collect::<Result<Vec<_>, _>>()
849 .map_err(|_| Error::Parse {
850 location: "history".to_string(),
851 reason: format!("invalid history values: {}", values_str),
852 })?
853 };
854
855 Ok(BandwidthHistory {
856 end_time,
857 interval,
858 values,
859 })
860 }
861
862 fn parse_timestamp_and_interval(line: &str) -> Result<(DateTime<Utc>, u32, String), Error> {
863 let timestamp_re =
864 regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
865 location: "timestamp".to_string(),
866 reason: format!("regex error: {}", e),
867 })?;
868
869 let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
870 location: "timestamp".to_string(),
871 reason: format!("invalid timestamp format: {}", line),
872 })?;
873
874 let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
875 let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
876 let remainder = caps
877 .get(3)
878 .map(|m| m.as_str().trim())
879 .unwrap_or("")
880 .to_string();
881
882 let timestamp = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
883 .map_err(|e| Error::Parse {
884 location: "timestamp".to_string(),
885 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
886 })?
887 .and_utc();
888
889 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
890 location: "timestamp".to_string(),
891 reason: format!("invalid interval: {}", interval_str),
892 })?;
893
894 Ok((timestamp, interval, remainder))
895 }
896
897 fn parse_geoip_to_count(value: &str) -> HashMap<String, u32> {
898 let mut result = HashMap::new();
899 if value.is_empty() {
900 return result;
901 }
902 for entry in value.split(',') {
903 if let Some(eq_pos) = entry.find('=') {
904 let locale = &entry[..eq_pos];
905 let count_str = &entry[eq_pos + 1..];
906 if let Ok(count) = count_str.parse::<u32>() {
907 result.insert(locale.to_string(), count);
908 }
909 }
910 }
911 result
912 }
913
914 fn parse_dirreq_resp(value: &str) -> (HashMap<DirResponse, u32>, HashMap<String, u32>) {
915 let mut recognized = HashMap::new();
916 let mut unrecognized = HashMap::new();
917 if value.is_empty() {
918 return (recognized, unrecognized);
919 }
920 for entry in value.split(',') {
921 if let Some(eq_pos) = entry.find('=') {
922 let status = &entry[..eq_pos];
923 let count_str = &entry[eq_pos + 1..];
924 if let Ok(count) = count_str.parse::<u32>() {
925 if let Ok(dir_resp) = DirResponse::from_str(status) {
926 recognized.insert(dir_resp, count);
927 } else {
928 unrecognized.insert(status.to_string(), count);
929 }
930 }
931 }
932 }
933 (recognized, unrecognized)
934 }
935
936 fn parse_dirreq_dl(value: &str) -> (HashMap<DirStat, 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 stat = &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_stat) = DirStat::from_str(stat) {
948 recognized.insert(dir_stat, count);
949 } else {
950 unrecognized.insert(stat.to_string(), count);
951 }
952 }
953 }
954 }
955 (recognized, unrecognized)
956 }
957
958 fn parse_port_count(value: &str) -> HashMap<PortKey, u64> {
959 let mut result = HashMap::new();
960 if value.is_empty() {
961 return result;
962 }
963 for entry in value.split(',') {
964 if let Some(eq_pos) = entry.find('=') {
965 let port_str = &entry[..eq_pos];
966 let count_str = &entry[eq_pos + 1..];
967 if let Ok(count) = count_str.parse::<u64>() {
968 let port_key = if port_str == "other" {
969 PortKey::Other
970 } else if let Ok(port) = port_str.parse::<u16>() {
971 PortKey::Port(port)
972 } else {
973 continue;
974 };
975 result.insert(port_key, count);
976 }
977 }
978 }
979 result
980 }
981
982 fn parse_cell_values(value: &str) -> Vec<f64> {
983 if value.is_empty() {
984 return Vec::new();
985 }
986 value
987 .split(',')
988 .filter_map(|s| s.trim().parse::<f64>().ok())
989 .collect()
990 }
991
992 fn parse_conn_bi_direct(value: &str) -> ConnBiDirectResult {
993 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
994 let stats: Vec<&str> = remainder.split(',').collect();
995 if stats.len() != 4 {
996 return Err(Error::Parse {
997 location: "conn-bi-direct".to_string(),
998 reason: format!("expected 4 values, got {}", stats.len()),
999 });
1000 }
1001 let below: u32 = stats[0].parse().map_err(|_| Error::Parse {
1002 location: "conn-bi-direct".to_string(),
1003 reason: "invalid below value".to_string(),
1004 })?;
1005 let read: u32 = stats[1].parse().map_err(|_| Error::Parse {
1006 location: "conn-bi-direct".to_string(),
1007 reason: "invalid read value".to_string(),
1008 })?;
1009 let write: u32 = stats[2].parse().map_err(|_| Error::Parse {
1010 location: "conn-bi-direct".to_string(),
1011 reason: "invalid write value".to_string(),
1012 })?;
1013 let both: u32 = stats[3].parse().map_err(|_| Error::Parse {
1014 location: "conn-bi-direct".to_string(),
1015 reason: "invalid both value".to_string(),
1016 })?;
1017 Ok((timestamp, interval, below, read, write, both))
1018 }
1019
1020 fn parse_transport_line(value: &str) -> Transport {
1021 let parts: Vec<&str> = value.split_whitespace().collect();
1022 if parts.is_empty() {
1023 return Transport {
1024 name: String::new(),
1025 address: None,
1026 port: None,
1027 args: Vec::new(),
1028 };
1029 }
1030 let name = parts[0].to_string();
1031 if parts.len() < 2 {
1032 return Transport {
1033 name,
1034 address: None,
1035 port: None,
1036 args: Vec::new(),
1037 };
1038 }
1039 let addr_port = parts[1];
1040 let (address, port) = if let Some(colon_pos) = addr_port.rfind(':') {
1041 let addr = addr_port[..colon_pos]
1042 .trim_matches(|c| c == '[' || c == ']')
1043 .to_string();
1044 let port = addr_port[colon_pos + 1..].parse::<u16>().ok();
1045 (Some(addr), port)
1046 } else {
1047 (None, None)
1048 };
1049 let args: Vec<String> = parts.iter().skip(2).map(|s| s.to_string()).collect();
1050 Transport {
1051 name,
1052 address,
1053 port,
1054 args,
1055 }
1056 }
1057
1058 fn parse_hs_stats(value: &str) -> (Option<u64>, HashMap<String, String>) {
1059 let mut stat = None;
1060 let mut extra = HashMap::new();
1061 if value.is_empty() {
1062 return (stat, extra);
1063 }
1064 let parts: Vec<&str> = value.split_whitespace().collect();
1065 if let Some(first) = parts.first() {
1066 stat = first.parse::<u64>().ok();
1067 }
1068 for part in parts.iter().skip(1) {
1069 if let Some(eq_pos) = part.find('=') {
1070 let key = &part[..eq_pos];
1071 let val = &part[eq_pos + 1..];
1072 extra.insert(key.to_string(), val.to_string());
1073 }
1074 }
1075 (stat, extra)
1076 }
1077
1078 fn parse_padding_counts(value: &str) -> PaddingCountsResult {
1079 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
1080 let mut counts = HashMap::new();
1081 for part in remainder.split_whitespace() {
1082 if let Some(eq_pos) = part.find('=') {
1083 let key = &part[..eq_pos];
1084 let val = &part[eq_pos + 1..];
1085 counts.insert(key.to_string(), val.to_string());
1086 }
1087 }
1088 Ok((timestamp, interval, counts))
1089 }
1090
1091 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1092 let mut block = String::new();
1093 let mut idx = start_idx;
1094 while idx < lines.len() {
1095 let line = lines[idx];
1096 block.push_str(line);
1097 block.push('\n');
1098 if line.starts_with("-----END ") {
1099 break;
1100 }
1101 idx += 1;
1102 }
1103 (block.trim_end().to_string(), idx)
1104 }
1105
1106 fn find_digest_content(content: &str) -> Option<&str> {
1111 let start_marker = "extra-info ";
1112 let end_marker = "\nrouter-signature\n";
1113 let start = content.find(start_marker)?;
1114 let end = content.find(end_marker)?;
1115 Some(&content[start..end + end_marker.len()])
1116 }
1117
1118 pub fn is_bridge(&self) -> bool {
1145 self.router_digest.is_some()
1146 }
1147}
1148
1149impl Descriptor for ExtraInfoDescriptor {
1150 fn parse(content: &str) -> Result<Self, Error> {
1151 let raw_content = content.as_bytes().to_vec();
1152 let lines: Vec<&str> = content.lines().collect();
1153 let mut desc = ExtraInfoDescriptor {
1154 raw_content,
1155 ..Default::default()
1156 };
1157
1158 let mut idx = 0;
1159 while idx < lines.len() {
1160 let line = lines[idx];
1161
1162 if line.starts_with("@type ") {
1163 idx += 1;
1164 continue;
1165 }
1166
1167 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1168 (&line[..space_pos], line[space_pos + 1..].trim())
1169 } else {
1170 (line, "")
1171 };
1172
1173 match keyword {
1174 "extra-info" => {
1175 let (nickname, fingerprint) = Self::parse_extra_info_line(value)?;
1176 desc.nickname = nickname;
1177 desc.fingerprint = fingerprint;
1178 }
1179 "published" => {
1180 desc.published = Self::parse_published_line(value)?;
1181 }
1182 "identity-ed25519" => {
1183 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1184 desc.ed25519_certificate = Some(block);
1185 idx = end_idx;
1186 }
1187 "router-sig-ed25519" => {
1188 desc.ed25519_signature = Some(value.to_string());
1189 }
1190 "router-signature" => {
1191 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1192 desc.signature = Some(block);
1193 idx = end_idx;
1194 }
1195 "router-digest" => {
1196 desc.router_digest = Some(value.to_string());
1197 }
1198 "router-digest-sha256" => {
1199 desc.router_digest_sha256 = Some(value.to_string());
1200 }
1201 "master-key-ed25519" => {
1202 desc.ed25519_certificate = Some(value.to_string());
1203 }
1204 "geoip-db-digest" => {
1205 desc.geoip_db_digest = Some(value.to_string());
1206 }
1207 "geoip6-db-digest" => {
1208 desc.geoip6_db_digest = Some(value.to_string());
1209 }
1210 "transport" => {
1211 let transport = Self::parse_transport_line(value);
1212 desc.transports.insert(transport.name.clone(), transport);
1213 }
1214 "read-history" => {
1215 desc.read_history = Some(Self::parse_history_line(value)?);
1216 }
1217 "write-history" => {
1218 desc.write_history = Some(Self::parse_history_line(value)?);
1219 }
1220 "dirreq-read-history" => {
1221 desc.dir_read_history = Some(Self::parse_history_line(value)?);
1222 }
1223 "dirreq-write-history" => {
1224 desc.dir_write_history = Some(Self::parse_history_line(value)?);
1225 }
1226 "conn-bi-direct" => {
1227 let (ts, interval, below, read, write, both) =
1228 Self::parse_conn_bi_direct(value)?;
1229 desc.conn_bi_direct_end = Some(ts);
1230 desc.conn_bi_direct_interval = Some(interval);
1231 desc.conn_bi_direct_below = Some(below);
1232 desc.conn_bi_direct_read = Some(read);
1233 desc.conn_bi_direct_write = Some(write);
1234 desc.conn_bi_direct_both = Some(both);
1235 }
1236 "cell-stats-end" => {
1237 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1238 desc.cell_stats_end = Some(ts);
1239 desc.cell_stats_interval = Some(interval);
1240 }
1241 "cell-processed-cells" => {
1242 desc.cell_processed_cells = Self::parse_cell_values(value);
1243 }
1244 "cell-queued-cells" => {
1245 desc.cell_queued_cells = Self::parse_cell_values(value);
1246 }
1247 "cell-time-in-queue" => {
1248 desc.cell_time_in_queue = Self::parse_cell_values(value);
1249 }
1250 "cell-circuits-per-decile" => {
1251 desc.cell_circuits_per_decile = value.parse().ok();
1252 }
1253 "dirreq-stats-end" => {
1254 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1255 desc.dir_stats_end = Some(ts);
1256 desc.dir_stats_interval = Some(interval);
1257 }
1258 "dirreq-v3-ips" => {
1259 desc.dir_v3_ips = Self::parse_geoip_to_count(value);
1260 }
1261 "dirreq-v3-reqs" => {
1262 desc.dir_v3_requests = Self::parse_geoip_to_count(value);
1263 }
1264 "dirreq-v3-resp" => {
1265 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1266 desc.dir_v3_responses = recognized;
1267 desc.dir_v3_responses_unknown = unrecognized;
1268 }
1269 "dirreq-v3-direct-dl" => {
1270 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1271 desc.dir_v3_direct_dl = recognized;
1272 desc.dir_v3_direct_dl_unknown = unrecognized;
1273 }
1274 "dirreq-v3-tunneled-dl" => {
1275 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1276 desc.dir_v3_tunneled_dl = recognized;
1277 desc.dir_v3_tunneled_dl_unknown = unrecognized;
1278 }
1279 "dirreq-v2-ips" => {
1280 desc.dir_v2_ips = Self::parse_geoip_to_count(value);
1281 }
1282 "dirreq-v2-reqs" => {
1283 desc.dir_v2_requests = Self::parse_geoip_to_count(value);
1284 }
1285 "dirreq-v2-resp" => {
1286 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1287 desc.dir_v2_responses = recognized;
1288 desc.dir_v2_responses_unknown = unrecognized;
1289 }
1290 "dirreq-v2-direct-dl" => {
1291 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1292 desc.dir_v2_direct_dl = recognized;
1293 desc.dir_v2_direct_dl_unknown = unrecognized;
1294 }
1295 "dirreq-v2-tunneled-dl" => {
1296 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1297 desc.dir_v2_tunneled_dl = recognized;
1298 desc.dir_v2_tunneled_dl_unknown = unrecognized;
1299 }
1300 "entry-stats-end" => {
1301 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1302 desc.entry_stats_end = Some(ts);
1303 desc.entry_stats_interval = Some(interval);
1304 }
1305 "entry-ips" => {
1306 desc.entry_ips = Self::parse_geoip_to_count(value);
1307 }
1308 "exit-stats-end" => {
1309 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1310 desc.exit_stats_end = Some(ts);
1311 desc.exit_stats_interval = Some(interval);
1312 }
1313 "exit-kibibytes-written" => {
1314 desc.exit_kibibytes_written = Self::parse_port_count(value);
1315 }
1316 "exit-kibibytes-read" => {
1317 desc.exit_kibibytes_read = Self::parse_port_count(value);
1318 }
1319 "exit-streams-opened" => {
1320 desc.exit_streams_opened = Self::parse_port_count(value);
1321 }
1322 "bridge-stats-end" => {
1323 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1324 desc.bridge_stats_end = Some(ts);
1325 desc.bridge_stats_interval = Some(interval);
1326 }
1327 "bridge-ips" => {
1328 desc.bridge_ips = Self::parse_geoip_to_count(value);
1329 }
1330 "bridge-ip-versions" => {
1331 desc.ip_versions = Self::parse_geoip_to_count(value);
1332 }
1333 "bridge-ip-transports" => {
1334 desc.ip_transports = Self::parse_geoip_to_count(value);
1335 }
1336 "hidserv-stats-end" => {
1337 desc.hs_stats_end = Some(Self::parse_published_line(value)?);
1338 }
1339 "hidserv-rend-relayed-cells" => {
1340 let (stat, attr) = Self::parse_hs_stats(value);
1341 desc.hs_rend_cells = stat;
1342 desc.hs_rend_cells_attr = attr;
1343 }
1344 "hidserv-dir-onions-seen" => {
1345 let (stat, attr) = Self::parse_hs_stats(value);
1346 desc.hs_dir_onions_seen = stat;
1347 desc.hs_dir_onions_seen_attr = attr;
1348 }
1349 "padding-counts" => {
1350 let (ts, interval, counts) = Self::parse_padding_counts(value)?;
1351 desc.padding_counts_end = Some(ts);
1352 desc.padding_counts_interval = Some(interval);
1353 desc.padding_counts = counts;
1354 }
1355 _ => {
1356 if !line.is_empty() && !line.starts_with("-----") {
1357 desc.unrecognized_lines.push(line.to_string());
1358 }
1359 }
1360 }
1361 idx += 1;
1362 }
1363
1364 if desc.nickname.is_empty() {
1365 return Err(Error::Parse {
1366 location: "extra-info".to_string(),
1367 reason: "missing extra-info line".to_string(),
1368 });
1369 }
1370
1371 Ok(desc)
1372 }
1373
1374 fn to_descriptor_string(&self) -> String {
1375 let mut result = String::new();
1376
1377 result.push_str(&format!(
1378 "extra-info {} {}\n",
1379 self.nickname, self.fingerprint
1380 ));
1381 result.push_str(&format!(
1382 "published {}\n",
1383 self.published.format("%Y-%m-%d %H:%M:%S")
1384 ));
1385
1386 if let Some(ref history) = self.write_history {
1387 let values: String = history
1388 .values
1389 .iter()
1390 .map(|v| v.to_string())
1391 .collect::<Vec<_>>()
1392 .join(",");
1393 result.push_str(&format!(
1394 "write-history {} ({} s) {}\n",
1395 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1396 history.interval,
1397 values
1398 ));
1399 }
1400
1401 if let Some(ref history) = self.read_history {
1402 let values: String = history
1403 .values
1404 .iter()
1405 .map(|v| v.to_string())
1406 .collect::<Vec<_>>()
1407 .join(",");
1408 result.push_str(&format!(
1409 "read-history {} ({} s) {}\n",
1410 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1411 history.interval,
1412 values
1413 ));
1414 }
1415
1416 if let Some(ref history) = self.dir_write_history {
1417 let values: String = history
1418 .values
1419 .iter()
1420 .map(|v| v.to_string())
1421 .collect::<Vec<_>>()
1422 .join(",");
1423 result.push_str(&format!(
1424 "dirreq-write-history {} ({} s) {}\n",
1425 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1426 history.interval,
1427 values
1428 ));
1429 }
1430
1431 if let Some(ref history) = self.dir_read_history {
1432 let values: String = history
1433 .values
1434 .iter()
1435 .map(|v| v.to_string())
1436 .collect::<Vec<_>>()
1437 .join(",");
1438 result.push_str(&format!(
1439 "dirreq-read-history {} ({} s) {}\n",
1440 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1441 history.interval,
1442 values
1443 ));
1444 }
1445
1446 if let Some(ref digest) = self.geoip_db_digest {
1447 result.push_str(&format!("geoip-db-digest {}\n", digest));
1448 }
1449
1450 if let Some(ref digest) = self.geoip6_db_digest {
1451 result.push_str(&format!("geoip6-db-digest {}\n", digest));
1452 }
1453
1454 if let Some(ref sig) = self.signature {
1455 result.push_str("router-signature\n");
1456 result.push_str(sig);
1457 result.push('\n');
1458 }
1459
1460 if let Some(ref digest) = self.router_digest {
1461 result.push_str(&format!("router-digest {}\n", digest));
1462 }
1463
1464 result
1465 }
1466
1467 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
1468 if self.is_bridge() {
1469 match (hash, encoding) {
1470 (DigestHash::Sha1, DigestEncoding::Hex) => {
1471 self.router_digest.clone().ok_or_else(|| Error::Parse {
1472 location: "digest".to_string(),
1473 reason: "bridge descriptor missing router-digest".to_string(),
1474 })
1475 }
1476 (DigestHash::Sha256, DigestEncoding::Base64) => self
1477 .router_digest_sha256
1478 .clone()
1479 .ok_or_else(|| Error::Parse {
1480 location: "digest".to_string(),
1481 reason: "bridge descriptor missing router-digest-sha256".to_string(),
1482 }),
1483 _ => Err(Error::Parse {
1484 location: "digest".to_string(),
1485 reason: "bridge extrainfo digests only available as sha1/hex or sha256/base64"
1486 .to_string(),
1487 }),
1488 }
1489 } else {
1490 let content_str = std::str::from_utf8(&self.raw_content).map_err(|_| Error::Parse {
1491 location: "digest".to_string(),
1492 reason: "invalid UTF-8 in raw content".to_string(),
1493 })?;
1494
1495 match hash {
1496 DigestHash::Sha1 => {
1497 let digest_content =
1498 Self::find_digest_content(content_str).ok_or_else(|| Error::Parse {
1499 location: "digest".to_string(),
1500 reason: "could not find digest content boundaries".to_string(),
1501 })?;
1502 Ok(compute_digest(digest_content.as_bytes(), hash, encoding))
1503 }
1504 DigestHash::Sha256 => Ok(compute_digest(&self.raw_content, hash, encoding)),
1505 }
1506 }
1507 }
1508
1509 fn raw_content(&self) -> &[u8] {
1510 &self.raw_content
1511 }
1512
1513 fn unrecognized_lines(&self) -> &[String] {
1514 &self.unrecognized_lines
1515 }
1516}
1517
1518impl FromStr for ExtraInfoDescriptor {
1519 type Err = Error;
1520
1521 fn from_str(s: &str) -> Result<Self, Self::Err> {
1522 Self::parse(s)
1523 }
1524}
1525
1526impl fmt::Display for ExtraInfoDescriptor {
1527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1528 write!(f, "{}", self.to_descriptor_string())
1529 }
1530}
1531
1532#[cfg(test)]
1533mod tests {
1534 use super::*;
1535
1536 const RELAY_EXTRA_INFO: &str = r#"@type extra-info 1.0
1537extra-info NINJA B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1538published 2012-05-05 17:03:50
1539write-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
1540read-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
1541dirreq-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
1542dirreq-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
1543router-signature
1544-----BEGIN SIGNATURE-----
1545K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
1546k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
15477LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
1548-----END SIGNATURE-----
1549"#;
1550
1551 const BRIDGE_EXTRA_INFO: &str = r#"@type bridge-extra-info 1.0
1552extra-info ec2bridgereaac65a3 1EC248422B57D9C0BD751892FE787585407479A4
1553published 2012-06-08 02:21:27
1554write-history 2012-06-08 02:10:38 (900 s) 343040,991232,5649408
1555read-history 2012-06-08 02:10:38 (900 s) 337920,437248,3995648
1556geoip-db-digest A27BE984989AB31C50D0861C7106B17A7EEC3756
1557dirreq-stats-end 2012-06-07 06:33:46 (86400 s)
1558dirreq-v3-ips
1559dirreq-v3-reqs
1560dirreq-v3-resp ok=72,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1561dirreq-v3-direct-dl complete=0,timeout=0,running=0
1562dirreq-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
1563bridge-stats-end 2012-06-07 06:33:53 (86400 s)
1564bridge-ips cn=16,ir=16,sy=16,us=16
1565router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1566"#;
1567
1568 const ED25519_EXTRA_INFO: &str = r#"@type extra-info 1.0
1569extra-info silverfoxden 4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E
1570identity-ed25519
1571-----BEGIN ED25519 CERT-----
1572AQQABhz0AQFcf5tGWLvPvr1sktoezBB95j6tAWSECa3Eo2ZuBtRNAQAgBABFAwSN
1573GcRlGIte4I1giLvQSTcXefT93rvx2PZ8wEDewxWdy6tzcLouPfE3Beu/eUyg8ntt
1574YuVlzi50WXzGlGnPmeounGLo0EDHTGzcLucFWpe0g/0ia6UDqgQiAySMBwI=
1575-----END ED25519 CERT-----
1576published 2015-08-22 19:21:12
1577write-history 2015-08-22 19:20:44 (14400 s) 14409728,23076864,7756800,6234112,7446528,12290048
1578read-history 2015-08-22 19:20:44 (14400 s) 20449280,23888896,9099264,7185408,8880128,13230080
1579geoip-db-digest 6882B8663F74C23E26E3C2274C24CAB2E82D67A2
1580geoip6-db-digest F063BD5247EB9829E6B9E586393D7036656DAF44
1581dirreq-stats-end 2015-08-22 11:58:30 (86400 s)
1582dirreq-v3-ips
1583dirreq-v3-reqs
1584dirreq-v3-resp ok=0,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1585dirreq-v3-direct-dl complete=0,timeout=0,running=0
1586dirreq-v3-tunneled-dl complete=0,timeout=0,running=0
1587router-sig-ed25519 g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw
1588router-signature
1589-----BEGIN SIGNATURE-----
1590R7kNaIWZrg3n3FWFBRMlEK2cbnha7gUIs8ToksLe+SF0dgoZiLyV3GKrnzdE/K6D
1591qdiOMN7eK04MOZVlgxkA5ayi61FTYVveK1HrDbJ+sEUwsviVGdif6kk/9DXOiyIJ
15927wP/tofgHj/aCbFZb1PGU0zrEVLa72hVJ6cCW8w/t1s=
1593-----END SIGNATURE-----
1594"#;
1595
1596 #[test]
1597 fn test_parse_relay_extra_info() {
1598 let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
1599
1600 assert_eq!(desc.nickname, "NINJA");
1601 assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
1602 assert_eq!(
1603 desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
1604 "2012-05-05 17:03:50"
1605 );
1606 assert!(!desc.is_bridge());
1607
1608 let write_history = desc.write_history.as_ref().unwrap();
1609 assert_eq!(write_history.interval, 900);
1610 assert_eq!(write_history.values.len(), 28);
1611 assert_eq!(write_history.values[0], 1082368);
1612
1613 let read_history = desc.read_history.as_ref().unwrap();
1614 assert_eq!(read_history.interval, 900);
1615 assert_eq!(read_history.values.len(), 28);
1616 assert_eq!(read_history.values[0], 3309568);
1617
1618 assert!(desc.signature.is_some());
1619 }
1620
1621 #[test]
1622 fn test_parse_bridge_extra_info() {
1623 let desc = ExtraInfoDescriptor::parse(BRIDGE_EXTRA_INFO).unwrap();
1624
1625 assert_eq!(desc.nickname, "ec2bridgereaac65a3");
1626 assert_eq!(desc.fingerprint, "1EC248422B57D9C0BD751892FE787585407479A4");
1627 assert!(desc.is_bridge());
1628 assert_eq!(
1629 desc.router_digest,
1630 Some("00A2AECCEAD3FEE033CFE29893387143146728EC".to_string())
1631 );
1632
1633 assert_eq!(
1634 desc.geoip_db_digest,
1635 Some("A27BE984989AB31C50D0861C7106B17A7EEC3756".to_string())
1636 );
1637
1638 assert_eq!(desc.dir_stats_interval, Some(86400));
1639 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&72));
1640 assert_eq!(
1641 desc.dir_v3_responses.get(&DirResponse::NotEnoughSigs),
1642 Some(&0)
1643 );
1644
1645 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&0));
1646 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Complete), Some(&68));
1647 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Timeout), Some(&4));
1648
1649 assert_eq!(desc.bridge_stats_interval, Some(86400));
1650 assert_eq!(desc.bridge_ips.get("cn"), Some(&16));
1651 assert_eq!(desc.bridge_ips.get("us"), Some(&16));
1652 }
1653
1654 #[test]
1655 fn test_parse_ed25519_extra_info() {
1656 let desc = ExtraInfoDescriptor::parse(ED25519_EXTRA_INFO).unwrap();
1657
1658 assert_eq!(desc.nickname, "silverfoxden");
1659 assert_eq!(desc.fingerprint, "4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E");
1660 assert!(!desc.is_bridge());
1661
1662 assert!(desc.ed25519_certificate.is_some());
1663 assert!(desc
1664 .ed25519_certificate
1665 .as_ref()
1666 .unwrap()
1667 .contains("ED25519 CERT"));
1668
1669 assert!(desc.ed25519_signature.is_some());
1670 assert!(desc
1671 .ed25519_signature
1672 .as_ref()
1673 .unwrap()
1674 .starts_with("g6Zg7Er8K7C1"));
1675
1676 assert_eq!(
1677 desc.geoip_db_digest,
1678 Some("6882B8663F74C23E26E3C2274C24CAB2E82D67A2".to_string())
1679 );
1680 assert_eq!(
1681 desc.geoip6_db_digest,
1682 Some("F063BD5247EB9829E6B9E586393D7036656DAF44".to_string())
1683 );
1684
1685 let write_history = desc.write_history.as_ref().unwrap();
1686 assert_eq!(write_history.interval, 14400);
1687 assert_eq!(write_history.values.len(), 6);
1688 }
1689
1690 #[test]
1691 fn test_dir_response_parsing() {
1692 assert_eq!(DirResponse::from_str("ok").unwrap(), DirResponse::Ok);
1693 assert_eq!(
1694 DirResponse::from_str("not-enough-sigs").unwrap(),
1695 DirResponse::NotEnoughSigs
1696 );
1697 assert_eq!(
1698 DirResponse::from_str("unavailable").unwrap(),
1699 DirResponse::Unavailable
1700 );
1701 assert_eq!(
1702 DirResponse::from_str("not-found").unwrap(),
1703 DirResponse::NotFound
1704 );
1705 assert_eq!(
1706 DirResponse::from_str("not-modified").unwrap(),
1707 DirResponse::NotModified
1708 );
1709 assert_eq!(DirResponse::from_str("busy").unwrap(), DirResponse::Busy);
1710 }
1711
1712 #[test]
1713 fn test_dir_stat_parsing() {
1714 assert_eq!(DirStat::from_str("complete").unwrap(), DirStat::Complete);
1715 assert_eq!(DirStat::from_str("timeout").unwrap(), DirStat::Timeout);
1716 assert_eq!(DirStat::from_str("running").unwrap(), DirStat::Running);
1717 assert_eq!(DirStat::from_str("min").unwrap(), DirStat::Min);
1718 assert_eq!(DirStat::from_str("max").unwrap(), DirStat::Max);
1719 assert_eq!(DirStat::from_str("d1").unwrap(), DirStat::D1);
1720 assert_eq!(DirStat::from_str("q1").unwrap(), DirStat::Q1);
1721 assert_eq!(DirStat::from_str("md").unwrap(), DirStat::Md);
1722 }
1723
1724 #[test]
1725 fn test_history_parsing() {
1726 let history = ExtraInfoDescriptor::parse_history_line(
1727 "2012-05-05 17:02:45 (900 s) 1082368,19456,50176",
1728 )
1729 .unwrap();
1730
1731 assert_eq!(history.interval, 900);
1732 assert_eq!(history.values, vec![1082368, 19456, 50176]);
1733 }
1734
1735 #[test]
1736 fn test_geoip_to_count_parsing() {
1737 let result = ExtraInfoDescriptor::parse_geoip_to_count("cn=16,ir=16,us=8");
1738 assert_eq!(result.get("cn"), Some(&16));
1739 assert_eq!(result.get("ir"), Some(&16));
1740 assert_eq!(result.get("us"), Some(&8));
1741 }
1742
1743 #[test]
1744 fn test_port_count_parsing() {
1745 let result = ExtraInfoDescriptor::parse_port_count("80=1000,443=2000,other=500");
1746 assert_eq!(result.get(&PortKey::Port(80)), Some(&1000));
1747 assert_eq!(result.get(&PortKey::Port(443)), Some(&2000));
1748 assert_eq!(result.get(&PortKey::Other), Some(&500));
1749 }
1750
1751 #[test]
1752 fn test_missing_extra_info_line() {
1753 let content = "published 2012-05-05 17:03:50\n";
1754 let result = ExtraInfoDescriptor::parse(content);
1755 assert!(result.is_err());
1756 }
1757
1758 #[test]
1759 fn test_invalid_fingerprint() {
1760 let content = "extra-info NINJA INVALID\npublished 2012-05-05 17:03:50\n";
1761 let result = ExtraInfoDescriptor::parse(content);
1762 assert!(result.is_err());
1763 }
1764
1765 #[test]
1766 fn test_conn_bi_direct() {
1767 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1768published 2012-05-05 17:03:50
1769conn-bi-direct 2012-05-03 12:07:50 (500 s) 277431,12089,0,2134
1770"#;
1771 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1772 assert!(desc.conn_bi_direct_end.is_some());
1773 assert_eq!(desc.conn_bi_direct_interval, Some(500));
1774 assert_eq!(desc.conn_bi_direct_below, Some(277431));
1775 assert_eq!(desc.conn_bi_direct_read, Some(12089));
1776 assert_eq!(desc.conn_bi_direct_write, Some(0));
1777 assert_eq!(desc.conn_bi_direct_both, Some(2134));
1778 }
1779
1780 #[test]
1781 fn test_cell_circuits_per_decile() {
1782 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1783published 2012-05-05 17:03:50
1784cell-circuits-per-decile 25
1785"#;
1786 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1787 assert_eq!(desc.cell_circuits_per_decile, Some(25));
1788 }
1789
1790 #[test]
1791 fn test_hidden_service_stats() {
1792 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1793published 2012-05-05 17:03:50
1794hidserv-stats-end 2012-05-03 12:07:50
1795hidserv-rend-relayed-cells 345 spiffy=true snowmen=neat
1796hidserv-dir-onions-seen 123
1797"#;
1798 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1799 assert!(desc.hs_stats_end.is_some());
1800 assert_eq!(desc.hs_rend_cells, Some(345));
1801 assert_eq!(
1802 desc.hs_rend_cells_attr.get("spiffy"),
1803 Some(&"true".to_string())
1804 );
1805 assert_eq!(
1806 desc.hs_rend_cells_attr.get("snowmen"),
1807 Some(&"neat".to_string())
1808 );
1809 assert_eq!(desc.hs_dir_onions_seen, Some(123));
1810 }
1811
1812 #[test]
1813 fn test_padding_counts() {
1814 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1815published 2012-05-05 17:03:50
1816padding-counts 2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000
1817"#;
1818 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1819 assert!(desc.padding_counts_end.is_some());
1820 assert_eq!(desc.padding_counts_interval, Some(86400));
1821 assert_eq!(
1822 desc.padding_counts.get("bin-size"),
1823 Some(&"10000".to_string())
1824 );
1825 assert_eq!(
1826 desc.padding_counts.get("write-drop"),
1827 Some(&"0".to_string())
1828 );
1829 assert_eq!(
1830 desc.padding_counts.get("write-pad"),
1831 Some(&"10000".to_string())
1832 );
1833 }
1834
1835 #[test]
1836 fn test_transport_line() {
1837 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1838published 2012-05-05 17:03:50
1839transport obfs2 83.212.96.201:33570
1840"#;
1841 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1842 assert!(desc.transports.contains_key("obfs2"));
1843 let transport = desc.transports.get("obfs2").unwrap();
1844 assert_eq!(transport.address, Some("83.212.96.201".to_string()));
1845 assert_eq!(transport.port, Some(33570));
1846 }
1847
1848 #[test]
1849 fn test_bridge_ip_versions() {
1850 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1851published 2012-05-05 17:03:50
1852bridge-ip-versions v4=16,v6=40
1853router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1854"#;
1855 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1856 assert_eq!(desc.ip_versions.get("v4"), Some(&16));
1857 assert_eq!(desc.ip_versions.get("v6"), Some(&40));
1858 }
1859
1860 #[test]
1861 fn test_bridge_ip_transports() {
1862 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1863published 2012-05-05 17:03:50
1864bridge-ip-transports <OR>=16,<??>=40
1865router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1866"#;
1867 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1868 assert_eq!(desc.ip_transports.get("<OR>"), Some(&16));
1869 assert_eq!(desc.ip_transports.get("<??>"), Some(&40));
1870 }
1871
1872 #[test]
1873 fn test_exit_stats() {
1874 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1875published 2012-05-05 17:03:50
1876exit-stats-end 2012-05-03 12:07:50 (86400 s)
1877exit-kibibytes-written 80=115533759,443=1777,other=500
1878exit-kibibytes-read 80=100,443=200
1879exit-streams-opened 80=50,443=100
1880"#;
1881 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1882 assert!(desc.exit_stats_end.is_some());
1883 assert_eq!(desc.exit_stats_interval, Some(86400));
1884 assert_eq!(
1885 desc.exit_kibibytes_written.get(&PortKey::Port(80)),
1886 Some(&115533759)
1887 );
1888 assert_eq!(
1889 desc.exit_kibibytes_written.get(&PortKey::Port(443)),
1890 Some(&1777)
1891 );
1892 assert_eq!(desc.exit_kibibytes_written.get(&PortKey::Other), Some(&500));
1893 assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Port(80)), Some(&100));
1894 assert_eq!(desc.exit_streams_opened.get(&PortKey::Port(80)), Some(&50));
1895 }
1896
1897 #[test]
1898 fn test_entry_stats() {
1899 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1900published 2012-05-05 17:03:50
1901entry-stats-end 2012-05-03 12:07:50 (86400 s)
1902entry-ips uk=5,de=3,jp=2
1903"#;
1904 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1905 assert!(desc.entry_stats_end.is_some());
1906 assert_eq!(desc.entry_stats_interval, Some(86400));
1907 assert_eq!(desc.entry_ips.get("uk"), Some(&5));
1908 assert_eq!(desc.entry_ips.get("de"), Some(&3));
1909 assert_eq!(desc.entry_ips.get("jp"), Some(&2));
1910 }
1911
1912 #[test]
1913 fn test_cell_stats() {
1914 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1915published 2012-05-05 17:03:50
1916cell-stats-end 2012-05-03 12:07:50 (86400 s)
1917cell-processed-cells 2.3,-4.6,8.9
1918cell-queued-cells 1.0,2.0,3.0
1919cell-time-in-queue 10.5,20.5,30.5
1920"#;
1921 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1922 assert!(desc.cell_stats_end.is_some());
1923 assert_eq!(desc.cell_stats_interval, Some(86400));
1924 assert_eq!(desc.cell_processed_cells, vec![2.3, -4.6, 8.9]);
1925 assert_eq!(desc.cell_queued_cells, vec![1.0, 2.0, 3.0]);
1926 assert_eq!(desc.cell_time_in_queue, vec![10.5, 20.5, 30.5]);
1927 }
1928
1929 #[test]
1930 fn test_empty_history_values() {
1931 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1932published 2012-05-05 17:03:50
1933write-history 2012-05-05 17:02:45 (900 s)
1934read-history 2012-05-05 17:02:45 (900 s)
1935"#;
1936 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1937 assert!(desc.write_history.is_some());
1938 assert!(desc.read_history.is_some());
1939 assert_eq!(desc.write_history.as_ref().unwrap().values.len(), 0);
1940 assert_eq!(desc.read_history.as_ref().unwrap().values.len(), 0);
1941 }
1942
1943 #[test]
1944 fn test_empty_geoip_counts() {
1945 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1946published 2012-05-05 17:03:50
1947dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
1948dirreq-v3-ips
1949dirreq-v3-reqs
1950"#;
1951 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1952 assert!(desc.dir_stats_end.is_some());
1953 assert_eq!(desc.dir_v3_ips.len(), 0);
1954 assert_eq!(desc.dir_v3_requests.len(), 0);
1955 }
1956
1957 #[test]
1958 fn test_negative_bandwidth_values() {
1959 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1960published 2012-05-05 17:03:50
1961write-history 2012-05-05 17:02:45 (900 s) -100,200,-300,400
1962"#;
1963 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1964 let history = desc.write_history.as_ref().unwrap();
1965 assert_eq!(history.values, vec![-100, 200, -300, 400]);
1966 }
1967
1968 #[test]
1969 fn test_large_bandwidth_values() {
1970 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1971published 2012-05-05 17:03:50
1972write-history 2012-05-05 17:02:45 (900 s) 9223372036854775807,1000000000000
1973"#;
1974 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1975 let history = desc.write_history.as_ref().unwrap();
1976 assert_eq!(history.values.len(), 2);
1977 assert_eq!(history.values[0], 9223372036854775807);
1978 assert_eq!(history.values[1], 1000000000000);
1979 }
1980
1981 #[test]
1982 fn test_unrecognized_lines_captured() {
1983 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1984published 2012-05-05 17:03:50
1985unknown-keyword some value here
1986another-unknown-line with data
1987"#;
1988 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1989 assert_eq!(desc.unrecognized_lines.len(), 2);
1990 assert!(desc
1991 .unrecognized_lines
1992 .contains(&"unknown-keyword some value here".to_string()));
1993 assert!(desc
1994 .unrecognized_lines
1995 .contains(&"another-unknown-line with data".to_string()));
1996 }
1997
1998 #[test]
1999 fn test_round_trip_serialization() {
2000 let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
2001 let serialized = desc.to_descriptor_string();
2002 let reparsed = ExtraInfoDescriptor::parse(&serialized).unwrap();
2003
2004 assert_eq!(desc.nickname, reparsed.nickname);
2005 assert_eq!(desc.fingerprint, reparsed.fingerprint);
2006 assert_eq!(
2007 desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
2008 reparsed.published.format("%Y-%m-%d %H:%M:%S").to_string()
2009 );
2010
2011 if let (Some(ref orig), Some(ref new)) = (&desc.write_history, &reparsed.write_history) {
2012 assert_eq!(orig.interval, new.interval);
2013 assert_eq!(orig.values, new.values);
2014 }
2015
2016 if let (Some(ref orig), Some(ref new)) = (&desc.read_history, &reparsed.read_history) {
2017 assert_eq!(orig.interval, new.interval);
2018 assert_eq!(orig.values, new.values);
2019 }
2020 }
2021
2022 #[test]
2023 fn test_transport_with_ipv6_address() {
2024 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2025published 2012-05-05 17:03:50
2026transport obfs4 [2001:db8::1]:9001 cert=abc123
2027"#;
2028 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2029 assert!(desc.transports.contains_key("obfs4"));
2030 let transport = desc.transports.get("obfs4").unwrap();
2031 assert_eq!(transport.address, Some("2001:db8::1".to_string()));
2032 assert_eq!(transport.port, Some(9001));
2033 assert_eq!(transport.args, vec!["cert=abc123".to_string()]);
2034 }
2035
2036 #[test]
2037 fn test_transport_without_address() {
2038 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2039published 2012-05-05 17:03:50
2040transport snowflake
2041"#;
2042 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2043 assert!(desc.transports.contains_key("snowflake"));
2044 let transport = desc.transports.get("snowflake").unwrap();
2045 assert_eq!(transport.address, None);
2046 assert_eq!(transport.port, None);
2047 assert_eq!(transport.args.len(), 0);
2048 }
2049
2050 #[test]
2051 fn test_multiple_transports() {
2052 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2053published 2012-05-05 17:03:50
2054transport obfs2 192.168.1.1:9001
2055transport obfs3 192.168.1.1:9002
2056transport obfs4 192.168.1.1:9003 cert=xyz
2057"#;
2058 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2059 assert_eq!(desc.transports.len(), 3);
2060 assert!(desc.transports.contains_key("obfs2"));
2061 assert!(desc.transports.contains_key("obfs3"));
2062 assert!(desc.transports.contains_key("obfs4"));
2063 }
2064
2065 #[test]
2066 fn test_dirreq_response_with_unknown_status() {
2067 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2068published 2012-05-05 17:03:50
2069dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
2070dirreq-v3-resp ok=100,unknown-status=50,busy=25
2071"#;
2072 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2073 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&100));
2074 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Busy), Some(&25));
2075 assert_eq!(
2076 desc.dir_v3_responses_unknown.get("unknown-status"),
2077 Some(&50)
2078 );
2079 }
2080
2081 #[test]
2082 fn test_dirreq_dl_with_unknown_stat() {
2083 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2084published 2012-05-05 17:03:50
2085dirreq-stats-end 2012-05-03 12:07:50 (86400 s)
2086dirreq-v3-direct-dl complete=100,unknown-stat=50,timeout=25
2087"#;
2088 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2089 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&100));
2090 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Timeout), Some(&25));
2091 assert_eq!(desc.dir_v3_direct_dl_unknown.get("unknown-stat"), Some(&50));
2092 }
2093
2094 #[test]
2095 fn test_hidden_service_stats_without_attributes() {
2096 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2097published 2012-05-05 17:03:50
2098hidserv-stats-end 2012-05-03 12:07:50
2099hidserv-rend-relayed-cells 12345
2100hidserv-dir-onions-seen 678
2101"#;
2102 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2103 assert_eq!(desc.hs_rend_cells, Some(12345));
2104 assert_eq!(desc.hs_rend_cells_attr.len(), 0);
2105 assert_eq!(desc.hs_dir_onions_seen, Some(678));
2106 assert_eq!(desc.hs_dir_onions_seen_attr.len(), 0);
2107 }
2108
2109 #[test]
2110 fn test_padding_counts_multiple_attributes() {
2111 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2112published 2012-05-05 17:03:50
2113padding-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
2114"#;
2115 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2116 assert_eq!(desc.padding_counts.len(), 7);
2117 assert_eq!(
2118 desc.padding_counts.get("bin-size"),
2119 Some(&"10000".to_string())
2120 );
2121 assert_eq!(
2122 desc.padding_counts.get("write-total"),
2123 Some(&"20000".to_string())
2124 );
2125 assert_eq!(
2126 desc.padding_counts.get("read-total"),
2127 Some(&"25000".to_string())
2128 );
2129 }
2130
2131 #[test]
2132 fn test_minimal_valid_descriptor() {
2133 let content = r#"extra-info minimal B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2134published 2012-05-05 17:03:50
2135"#;
2136 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2137 assert_eq!(desc.nickname, "minimal");
2138 assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
2139 assert!(!desc.is_bridge());
2140 assert_eq!(desc.transports.len(), 0);
2141 assert_eq!(desc.unrecognized_lines.len(), 0);
2142 }
2143
2144 #[test]
2145 fn test_type_annotation_ignored() {
2146 let content = r#"@type extra-info 1.0
2147@type bridge-extra-info 1.1
2148extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2149published 2012-05-05 17:03:50
2150"#;
2151 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2152 assert_eq!(desc.nickname, "test");
2153 assert_eq!(desc.unrecognized_lines.len(), 0);
2154 }
2155
2156 #[test]
2157 fn test_port_key_display() {
2158 assert_eq!(format!("{}", PortKey::Port(80)), "80");
2159 assert_eq!(format!("{}", PortKey::Port(443)), "443");
2160 assert_eq!(format!("{}", PortKey::Other), "other");
2161 }
2162
2163 #[test]
2164 fn test_bandwidth_history_with_single_value() {
2165 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2166published 2012-05-05 17:03:50
2167write-history 2012-05-05 17:02:45 (900 s) 1234567890
2168"#;
2169 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2170 let history = desc.write_history.as_ref().unwrap();
2171 assert_eq!(history.values.len(), 1);
2172 assert_eq!(history.values[0], 1234567890);
2173 }
2174
2175 #[test]
2176 fn test_conn_bi_direct_with_zeros() {
2177 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2178published 2012-05-05 17:03:50
2179conn-bi-direct 2012-05-03 12:07:50 (500 s) 0,0,0,0
2180"#;
2181 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2182 assert_eq!(desc.conn_bi_direct_below, Some(0));
2183 assert_eq!(desc.conn_bi_direct_read, Some(0));
2184 assert_eq!(desc.conn_bi_direct_write, Some(0));
2185 assert_eq!(desc.conn_bi_direct_both, Some(0));
2186 }
2187
2188 #[test]
2189 fn test_exit_stats_with_only_other_port() {
2190 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2191published 2012-05-05 17:03:50
2192exit-stats-end 2012-05-03 12:07:50 (86400 s)
2193exit-kibibytes-written other=1000000
2194exit-kibibytes-read other=500000
2195exit-streams-opened other=1000
2196"#;
2197 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2198 assert_eq!(
2199 desc.exit_kibibytes_written.get(&PortKey::Other),
2200 Some(&1000000)
2201 );
2202 assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Other), Some(&500000));
2203 assert_eq!(desc.exit_streams_opened.get(&PortKey::Other), Some(&1000));
2204 assert_eq!(desc.exit_kibibytes_written.len(), 1);
2205 }
2206
2207 #[test]
2208 fn test_geoip_with_special_country_codes() {
2209 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
2210published 2012-05-05 17:03:50
2211bridge-stats-end 2012-05-03 12:07:50 (86400 s)
2212bridge-ips ??=100,a1=50,zz=25
2213router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
2214"#;
2215 let desc = ExtraInfoDescriptor::parse(content).unwrap();
2216 assert_eq!(desc.bridge_ips.get("??"), Some(&100));
2217 assert_eq!(desc.bridge_ips.get("a1"), Some(&50));
2218 assert_eq!(desc.bridge_ips.get("zz"), Some(&25));
2219 }
2220}