1use std::collections::HashMap;
93use std::fmt;
94use std::str::FromStr;
95
96use chrono::{DateTime, NaiveDateTime, Utc};
97
98use crate::Error;
99
100use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
101
102type ConnBiDirectResult = Result<(DateTime<Utc>, u32, u32, u32, u32, u32), Error>;
104
105type PaddingCountsResult = Result<(DateTime<Utc>, u32, HashMap<String, String>), Error>;
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub enum DirResponse {
135 Ok,
137 NotEnoughSigs,
139 Unavailable,
141 NotFound,
143 NotModified,
145 Busy,
147}
148
149impl FromStr for DirResponse {
150 type Err = Error;
151
152 fn from_str(s: &str) -> Result<Self, Self::Err> {
153 match s.to_lowercase().as_str() {
154 "ok" => Ok(DirResponse::Ok),
155 "not-enough-sigs" => Ok(DirResponse::NotEnoughSigs),
156 "unavailable" => Ok(DirResponse::Unavailable),
157 "not-found" => Ok(DirResponse::NotFound),
158 "not-modified" => Ok(DirResponse::NotModified),
159 "busy" => Ok(DirResponse::Busy),
160 _ => Err(Error::Parse {
161 location: "DirResponse".to_string(),
162 reason: format!("unknown dir response: {}", s),
163 }),
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Hash)]
196pub enum DirStat {
197 Complete,
199 Timeout,
201 Running,
203 Min,
205 Max,
207 D1,
209 D2,
211 D3,
213 D4,
215 D6,
217 D7,
219 D8,
221 D9,
223 Q1,
225 Q3,
227 Md,
229}
230
231impl FromStr for DirStat {
232 type Err = Error;
233
234 fn from_str(s: &str) -> Result<Self, Self::Err> {
235 match s.to_lowercase().as_str() {
236 "complete" => Ok(DirStat::Complete),
237 "timeout" => Ok(DirStat::Timeout),
238 "running" => Ok(DirStat::Running),
239 "min" => Ok(DirStat::Min),
240 "max" => Ok(DirStat::Max),
241 "d1" => Ok(DirStat::D1),
242 "d2" => Ok(DirStat::D2),
243 "d3" => Ok(DirStat::D3),
244 "d4" => Ok(DirStat::D4),
245 "d6" => Ok(DirStat::D6),
246 "d7" => Ok(DirStat::D7),
247 "d8" => Ok(DirStat::D8),
248 "d9" => Ok(DirStat::D9),
249 "q1" => Ok(DirStat::Q1),
250 "q3" => Ok(DirStat::Q3),
251 "md" => Ok(DirStat::Md),
252 _ => Err(Error::Parse {
253 location: "DirStat".to_string(),
254 reason: format!("unknown dir stat: {}", s),
255 }),
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
288pub struct BandwidthHistory {
289 pub end_time: DateTime<Utc>,
291
292 pub interval: u32,
294
295 pub values: Vec<i64>,
299}
300
301#[derive(Debug, Clone, PartialEq)]
322pub struct Transport {
323 pub name: String,
325
326 pub address: Option<String>,
328
329 pub port: Option<u16>,
331
332 pub args: Vec<String>,
334}
335
336#[derive(Debug, Clone, PartialEq)]
425pub struct ExtraInfoDescriptor {
426 pub nickname: String,
428
429 pub fingerprint: String,
433
434 pub published: DateTime<Utc>,
436
437 pub geoip_db_digest: Option<String>,
439
440 pub geoip6_db_digest: Option<String>,
442
443 pub transports: HashMap<String, Transport>,
447
448 pub read_history: Option<BandwidthHistory>,
450
451 pub write_history: Option<BandwidthHistory>,
453
454 pub dir_read_history: Option<BandwidthHistory>,
456
457 pub dir_write_history: Option<BandwidthHistory>,
459
460 pub conn_bi_direct_end: Option<DateTime<Utc>>,
462
463 pub conn_bi_direct_interval: Option<u32>,
465
466 pub conn_bi_direct_below: Option<u32>,
468
469 pub conn_bi_direct_read: Option<u32>,
471
472 pub conn_bi_direct_write: Option<u32>,
474
475 pub conn_bi_direct_both: Option<u32>,
477
478 pub cell_stats_end: Option<DateTime<Utc>>,
480
481 pub cell_stats_interval: Option<u32>,
483
484 pub cell_processed_cells: Vec<f64>,
486
487 pub cell_queued_cells: Vec<f64>,
489
490 pub cell_time_in_queue: Vec<f64>,
492
493 pub cell_circuits_per_decile: Option<u32>,
495
496 pub dir_stats_end: Option<DateTime<Utc>>,
498
499 pub dir_stats_interval: Option<u32>,
501
502 pub dir_v3_ips: HashMap<String, u32>,
504
505 pub dir_v3_requests: HashMap<String, u32>,
507
508 pub dir_v3_responses: HashMap<DirResponse, u32>,
510
511 pub dir_v3_responses_unknown: HashMap<String, u32>,
513
514 pub dir_v3_direct_dl: HashMap<DirStat, u32>,
516
517 pub dir_v3_direct_dl_unknown: HashMap<String, u32>,
519
520 pub dir_v3_tunneled_dl: HashMap<DirStat, u32>,
522
523 pub dir_v3_tunneled_dl_unknown: HashMap<String, u32>,
525
526 pub dir_v2_ips: HashMap<String, u32>,
528
529 pub dir_v2_requests: HashMap<String, u32>,
531
532 pub dir_v2_responses: HashMap<DirResponse, u32>,
534
535 pub dir_v2_responses_unknown: HashMap<String, u32>,
537
538 pub dir_v2_direct_dl: HashMap<DirStat, u32>,
540
541 pub dir_v2_direct_dl_unknown: HashMap<String, u32>,
543
544 pub dir_v2_tunneled_dl: HashMap<DirStat, u32>,
546
547 pub dir_v2_tunneled_dl_unknown: HashMap<String, u32>,
549
550 pub entry_stats_end: Option<DateTime<Utc>>,
552
553 pub entry_stats_interval: Option<u32>,
555
556 pub entry_ips: HashMap<String, u32>,
558
559 pub exit_stats_end: Option<DateTime<Utc>>,
561
562 pub exit_stats_interval: Option<u32>,
564
565 pub exit_kibibytes_written: HashMap<PortKey, u64>,
567
568 pub exit_kibibytes_read: HashMap<PortKey, u64>,
570
571 pub exit_streams_opened: HashMap<PortKey, u64>,
573
574 pub bridge_stats_end: Option<DateTime<Utc>>,
576
577 pub bridge_stats_interval: Option<u32>,
579
580 pub bridge_ips: HashMap<String, u32>,
582
583 pub ip_versions: HashMap<String, u32>,
585
586 pub ip_transports: HashMap<String, u32>,
588
589 pub hs_stats_end: Option<DateTime<Utc>>,
591
592 pub hs_rend_cells: Option<u64>,
594
595 pub hs_rend_cells_attr: HashMap<String, String>,
597
598 pub hs_dir_onions_seen: Option<u64>,
600
601 pub hs_dir_onions_seen_attr: HashMap<String, String>,
603
604 pub padding_counts_end: Option<DateTime<Utc>>,
606
607 pub padding_counts_interval: Option<u32>,
609
610 pub padding_counts: HashMap<String, String>,
612
613 pub ed25519_certificate: Option<String>,
615
616 pub ed25519_signature: Option<String>,
618
619 pub signature: Option<String>,
621
622 pub router_digest: Option<String>,
626
627 pub router_digest_sha256: Option<String>,
629
630 raw_content: Vec<u8>,
632
633 unrecognized_lines: Vec<String>,
635}
636
637#[derive(Debug, Clone, PartialEq, Eq, Hash)]
655pub enum PortKey {
656 Port(u16),
658
659 Other,
661}
662
663impl fmt::Display for PortKey {
664 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665 match self {
666 PortKey::Port(p) => write!(f, "{}", p),
667 PortKey::Other => write!(f, "other"),
668 }
669 }
670}
671
672impl Default for ExtraInfoDescriptor {
673 fn default() -> Self {
674 Self {
675 nickname: String::new(),
676 fingerprint: String::new(),
677 published: DateTime::from_timestamp(0, 0).unwrap(),
678 geoip_db_digest: None,
679 geoip6_db_digest: None,
680 transports: HashMap::new(),
681 read_history: None,
682 write_history: None,
683 dir_read_history: None,
684 dir_write_history: None,
685 conn_bi_direct_end: None,
686 conn_bi_direct_interval: None,
687 conn_bi_direct_below: None,
688 conn_bi_direct_read: None,
689 conn_bi_direct_write: None,
690 conn_bi_direct_both: None,
691 cell_stats_end: None,
692 cell_stats_interval: None,
693 cell_processed_cells: Vec::new(),
694 cell_queued_cells: Vec::new(),
695 cell_time_in_queue: Vec::new(),
696 cell_circuits_per_decile: None,
697 dir_stats_end: None,
698 dir_stats_interval: None,
699 dir_v3_ips: HashMap::new(),
700 dir_v3_requests: HashMap::new(),
701 dir_v3_responses: HashMap::new(),
702 dir_v3_responses_unknown: HashMap::new(),
703 dir_v3_direct_dl: HashMap::new(),
704 dir_v3_direct_dl_unknown: HashMap::new(),
705 dir_v3_tunneled_dl: HashMap::new(),
706 dir_v3_tunneled_dl_unknown: HashMap::new(),
707 dir_v2_ips: HashMap::new(),
708 dir_v2_requests: HashMap::new(),
709 dir_v2_responses: HashMap::new(),
710 dir_v2_responses_unknown: HashMap::new(),
711 dir_v2_direct_dl: HashMap::new(),
712 dir_v2_direct_dl_unknown: HashMap::new(),
713 dir_v2_tunneled_dl: HashMap::new(),
714 dir_v2_tunneled_dl_unknown: HashMap::new(),
715 entry_stats_end: None,
716 entry_stats_interval: None,
717 entry_ips: HashMap::new(),
718 exit_stats_end: None,
719 exit_stats_interval: None,
720 exit_kibibytes_written: HashMap::new(),
721 exit_kibibytes_read: HashMap::new(),
722 exit_streams_opened: HashMap::new(),
723 bridge_stats_end: None,
724 bridge_stats_interval: None,
725 bridge_ips: HashMap::new(),
726 ip_versions: HashMap::new(),
727 ip_transports: HashMap::new(),
728 hs_stats_end: None,
729 hs_rend_cells: None,
730 hs_rend_cells_attr: HashMap::new(),
731 hs_dir_onions_seen: None,
732 hs_dir_onions_seen_attr: HashMap::new(),
733 padding_counts_end: None,
734 padding_counts_interval: None,
735 padding_counts: HashMap::new(),
736 ed25519_certificate: None,
737 ed25519_signature: None,
738 signature: None,
739 router_digest: None,
740 router_digest_sha256: None,
741 raw_content: Vec::new(),
742 unrecognized_lines: Vec::new(),
743 }
744 }
745}
746
747impl ExtraInfoDescriptor {
748 fn parse_extra_info_line(line: &str) -> Result<(String, String), Error> {
749 let parts: Vec<&str> = line.split_whitespace().collect();
750 if parts.len() < 2 {
751 return Err(Error::Parse {
752 location: "extra-info".to_string(),
753 reason: "extra-info line requires nickname and fingerprint".to_string(),
754 });
755 }
756 let nickname = parts[0].to_string();
757 let fingerprint = parts[1].to_string();
758 if fingerprint.len() != 40 || !fingerprint.chars().all(|c| c.is_ascii_hexdigit()) {
759 return Err(Error::Parse {
760 location: "extra-info".to_string(),
761 reason: format!("invalid fingerprint: {}", fingerprint),
762 });
763 }
764 Ok((nickname, fingerprint))
765 }
766
767 fn parse_published_line(line: &str) -> Result<DateTime<Utc>, Error> {
768 let datetime =
769 NaiveDateTime::parse_from_str(line.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
770 Error::Parse {
771 location: "published".to_string(),
772 reason: format!("invalid datetime: {} - {}", line, e),
773 }
774 })?;
775 Ok(datetime.and_utc())
776 }
777
778 fn parse_history_line(line: &str) -> Result<BandwidthHistory, Error> {
779 let timestamp_re =
780 regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
781 location: "history".to_string(),
782 reason: format!("regex error: {}", e),
783 })?;
784
785 let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
786 location: "history".to_string(),
787 reason: format!("invalid history format: {}", line),
788 })?;
789
790 let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
791 let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
792 let values_str = caps.get(3).map(|m| m.as_str().trim()).unwrap_or("");
793
794 let end_time = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
795 .map_err(|e| Error::Parse {
796 location: "history".to_string(),
797 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
798 })?
799 .and_utc();
800
801 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
802 location: "history".to_string(),
803 reason: format!("invalid interval: {}", interval_str),
804 })?;
805
806 let values: Vec<i64> = if values_str.is_empty() {
807 Vec::new()
808 } else {
809 values_str
810 .split(',')
811 .filter(|s| !s.is_empty())
812 .map(|s| s.trim().parse::<i64>())
813 .collect::<Result<Vec<_>, _>>()
814 .map_err(|_| Error::Parse {
815 location: "history".to_string(),
816 reason: format!("invalid history values: {}", values_str),
817 })?
818 };
819
820 Ok(BandwidthHistory {
821 end_time,
822 interval,
823 values,
824 })
825 }
826
827 fn parse_timestamp_and_interval(line: &str) -> Result<(DateTime<Utc>, u32, String), Error> {
828 let timestamp_re =
829 regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
830 location: "timestamp".to_string(),
831 reason: format!("regex error: {}", e),
832 })?;
833
834 let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
835 location: "timestamp".to_string(),
836 reason: format!("invalid timestamp format: {}", line),
837 })?;
838
839 let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
840 let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
841 let remainder = caps
842 .get(3)
843 .map(|m| m.as_str().trim())
844 .unwrap_or("")
845 .to_string();
846
847 let timestamp = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
848 .map_err(|e| Error::Parse {
849 location: "timestamp".to_string(),
850 reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
851 })?
852 .and_utc();
853
854 let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
855 location: "timestamp".to_string(),
856 reason: format!("invalid interval: {}", interval_str),
857 })?;
858
859 Ok((timestamp, interval, remainder))
860 }
861
862 fn parse_geoip_to_count(value: &str) -> HashMap<String, u32> {
863 let mut result = HashMap::new();
864 if value.is_empty() {
865 return result;
866 }
867 for entry in value.split(',') {
868 if let Some(eq_pos) = entry.find('=') {
869 let locale = &entry[..eq_pos];
870 let count_str = &entry[eq_pos + 1..];
871 if let Ok(count) = count_str.parse::<u32>() {
872 result.insert(locale.to_string(), count);
873 }
874 }
875 }
876 result
877 }
878
879 fn parse_dirreq_resp(value: &str) -> (HashMap<DirResponse, u32>, HashMap<String, u32>) {
880 let mut recognized = HashMap::new();
881 let mut unrecognized = HashMap::new();
882 if value.is_empty() {
883 return (recognized, unrecognized);
884 }
885 for entry in value.split(',') {
886 if let Some(eq_pos) = entry.find('=') {
887 let status = &entry[..eq_pos];
888 let count_str = &entry[eq_pos + 1..];
889 if let Ok(count) = count_str.parse::<u32>() {
890 if let Ok(dir_resp) = DirResponse::from_str(status) {
891 recognized.insert(dir_resp, count);
892 } else {
893 unrecognized.insert(status.to_string(), count);
894 }
895 }
896 }
897 }
898 (recognized, unrecognized)
899 }
900
901 fn parse_dirreq_dl(value: &str) -> (HashMap<DirStat, u32>, HashMap<String, u32>) {
902 let mut recognized = HashMap::new();
903 let mut unrecognized = HashMap::new();
904 if value.is_empty() {
905 return (recognized, unrecognized);
906 }
907 for entry in value.split(',') {
908 if let Some(eq_pos) = entry.find('=') {
909 let stat = &entry[..eq_pos];
910 let count_str = &entry[eq_pos + 1..];
911 if let Ok(count) = count_str.parse::<u32>() {
912 if let Ok(dir_stat) = DirStat::from_str(stat) {
913 recognized.insert(dir_stat, count);
914 } else {
915 unrecognized.insert(stat.to_string(), count);
916 }
917 }
918 }
919 }
920 (recognized, unrecognized)
921 }
922
923 fn parse_port_count(value: &str) -> HashMap<PortKey, u64> {
924 let mut result = HashMap::new();
925 if value.is_empty() {
926 return result;
927 }
928 for entry in value.split(',') {
929 if let Some(eq_pos) = entry.find('=') {
930 let port_str = &entry[..eq_pos];
931 let count_str = &entry[eq_pos + 1..];
932 if let Ok(count) = count_str.parse::<u64>() {
933 let port_key = if port_str == "other" {
934 PortKey::Other
935 } else if let Ok(port) = port_str.parse::<u16>() {
936 PortKey::Port(port)
937 } else {
938 continue;
939 };
940 result.insert(port_key, count);
941 }
942 }
943 }
944 result
945 }
946
947 fn parse_cell_values(value: &str) -> Vec<f64> {
948 if value.is_empty() {
949 return Vec::new();
950 }
951 value
952 .split(',')
953 .filter_map(|s| s.trim().parse::<f64>().ok())
954 .collect()
955 }
956
957 fn parse_conn_bi_direct(value: &str) -> ConnBiDirectResult {
958 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
959 let stats: Vec<&str> = remainder.split(',').collect();
960 if stats.len() != 4 {
961 return Err(Error::Parse {
962 location: "conn-bi-direct".to_string(),
963 reason: format!("expected 4 values, got {}", stats.len()),
964 });
965 }
966 let below: u32 = stats[0].parse().map_err(|_| Error::Parse {
967 location: "conn-bi-direct".to_string(),
968 reason: "invalid below value".to_string(),
969 })?;
970 let read: u32 = stats[1].parse().map_err(|_| Error::Parse {
971 location: "conn-bi-direct".to_string(),
972 reason: "invalid read value".to_string(),
973 })?;
974 let write: u32 = stats[2].parse().map_err(|_| Error::Parse {
975 location: "conn-bi-direct".to_string(),
976 reason: "invalid write value".to_string(),
977 })?;
978 let both: u32 = stats[3].parse().map_err(|_| Error::Parse {
979 location: "conn-bi-direct".to_string(),
980 reason: "invalid both value".to_string(),
981 })?;
982 Ok((timestamp, interval, below, read, write, both))
983 }
984
985 fn parse_transport_line(value: &str) -> Transport {
986 let parts: Vec<&str> = value.split_whitespace().collect();
987 if parts.is_empty() {
988 return Transport {
989 name: String::new(),
990 address: None,
991 port: None,
992 args: Vec::new(),
993 };
994 }
995 let name = parts[0].to_string();
996 if parts.len() < 2 {
997 return Transport {
998 name,
999 address: None,
1000 port: None,
1001 args: Vec::new(),
1002 };
1003 }
1004 let addr_port = parts[1];
1005 let (address, port) = if let Some(colon_pos) = addr_port.rfind(':') {
1006 let addr = addr_port[..colon_pos]
1007 .trim_matches(|c| c == '[' || c == ']')
1008 .to_string();
1009 let port = addr_port[colon_pos + 1..].parse::<u16>().ok();
1010 (Some(addr), port)
1011 } else {
1012 (None, None)
1013 };
1014 let args: Vec<String> = parts.iter().skip(2).map(|s| s.to_string()).collect();
1015 Transport {
1016 name,
1017 address,
1018 port,
1019 args,
1020 }
1021 }
1022
1023 fn parse_hs_stats(value: &str) -> (Option<u64>, HashMap<String, String>) {
1024 let mut stat = None;
1025 let mut extra = HashMap::new();
1026 if value.is_empty() {
1027 return (stat, extra);
1028 }
1029 let parts: Vec<&str> = value.split_whitespace().collect();
1030 if let Some(first) = parts.first() {
1031 stat = first.parse::<u64>().ok();
1032 }
1033 for part in parts.iter().skip(1) {
1034 if let Some(eq_pos) = part.find('=') {
1035 let key = &part[..eq_pos];
1036 let val = &part[eq_pos + 1..];
1037 extra.insert(key.to_string(), val.to_string());
1038 }
1039 }
1040 (stat, extra)
1041 }
1042
1043 fn parse_padding_counts(value: &str) -> PaddingCountsResult {
1044 let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
1045 let mut counts = HashMap::new();
1046 for part in remainder.split_whitespace() {
1047 if let Some(eq_pos) = part.find('=') {
1048 let key = &part[..eq_pos];
1049 let val = &part[eq_pos + 1..];
1050 counts.insert(key.to_string(), val.to_string());
1051 }
1052 }
1053 Ok((timestamp, interval, counts))
1054 }
1055
1056 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1057 let mut block = String::new();
1058 let mut idx = start_idx;
1059 while idx < lines.len() {
1060 let line = lines[idx];
1061 block.push_str(line);
1062 block.push('\n');
1063 if line.starts_with("-----END ") {
1064 break;
1065 }
1066 idx += 1;
1067 }
1068 (block.trim_end().to_string(), idx)
1069 }
1070
1071 fn find_digest_content(content: &str) -> Option<&str> {
1076 let start_marker = "extra-info ";
1077 let end_marker = "\nrouter-signature\n";
1078 let start = content.find(start_marker)?;
1079 let end = content.find(end_marker)?;
1080 Some(&content[start..end + end_marker.len()])
1081 }
1082
1083 pub fn is_bridge(&self) -> bool {
1110 self.router_digest.is_some()
1111 }
1112}
1113
1114impl Descriptor for ExtraInfoDescriptor {
1115 fn parse(content: &str) -> Result<Self, Error> {
1116 let raw_content = content.as_bytes().to_vec();
1117 let lines: Vec<&str> = content.lines().collect();
1118 let mut desc = ExtraInfoDescriptor {
1119 raw_content,
1120 ..Default::default()
1121 };
1122
1123 let mut idx = 0;
1124 while idx < lines.len() {
1125 let line = lines[idx];
1126
1127 if line.starts_with("@type ") {
1128 idx += 1;
1129 continue;
1130 }
1131
1132 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1133 (&line[..space_pos], line[space_pos + 1..].trim())
1134 } else {
1135 (line, "")
1136 };
1137
1138 match keyword {
1139 "extra-info" => {
1140 let (nickname, fingerprint) = Self::parse_extra_info_line(value)?;
1141 desc.nickname = nickname;
1142 desc.fingerprint = fingerprint;
1143 }
1144 "published" => {
1145 desc.published = Self::parse_published_line(value)?;
1146 }
1147 "identity-ed25519" => {
1148 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1149 desc.ed25519_certificate = Some(block);
1150 idx = end_idx;
1151 }
1152 "router-sig-ed25519" => {
1153 desc.ed25519_signature = Some(value.to_string());
1154 }
1155 "router-signature" => {
1156 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1157 desc.signature = Some(block);
1158 idx = end_idx;
1159 }
1160 "router-digest" => {
1161 desc.router_digest = Some(value.to_string());
1162 }
1163 "router-digest-sha256" => {
1164 desc.router_digest_sha256 = Some(value.to_string());
1165 }
1166 "master-key-ed25519" => {
1167 desc.ed25519_certificate = Some(value.to_string());
1168 }
1169 "geoip-db-digest" => {
1170 desc.geoip_db_digest = Some(value.to_string());
1171 }
1172 "geoip6-db-digest" => {
1173 desc.geoip6_db_digest = Some(value.to_string());
1174 }
1175 "transport" => {
1176 let transport = Self::parse_transport_line(value);
1177 desc.transports.insert(transport.name.clone(), transport);
1178 }
1179 "read-history" => {
1180 desc.read_history = Some(Self::parse_history_line(value)?);
1181 }
1182 "write-history" => {
1183 desc.write_history = Some(Self::parse_history_line(value)?);
1184 }
1185 "dirreq-read-history" => {
1186 desc.dir_read_history = Some(Self::parse_history_line(value)?);
1187 }
1188 "dirreq-write-history" => {
1189 desc.dir_write_history = Some(Self::parse_history_line(value)?);
1190 }
1191 "conn-bi-direct" => {
1192 let (ts, interval, below, read, write, both) =
1193 Self::parse_conn_bi_direct(value)?;
1194 desc.conn_bi_direct_end = Some(ts);
1195 desc.conn_bi_direct_interval = Some(interval);
1196 desc.conn_bi_direct_below = Some(below);
1197 desc.conn_bi_direct_read = Some(read);
1198 desc.conn_bi_direct_write = Some(write);
1199 desc.conn_bi_direct_both = Some(both);
1200 }
1201 "cell-stats-end" => {
1202 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1203 desc.cell_stats_end = Some(ts);
1204 desc.cell_stats_interval = Some(interval);
1205 }
1206 "cell-processed-cells" => {
1207 desc.cell_processed_cells = Self::parse_cell_values(value);
1208 }
1209 "cell-queued-cells" => {
1210 desc.cell_queued_cells = Self::parse_cell_values(value);
1211 }
1212 "cell-time-in-queue" => {
1213 desc.cell_time_in_queue = Self::parse_cell_values(value);
1214 }
1215 "cell-circuits-per-decile" => {
1216 desc.cell_circuits_per_decile = value.parse().ok();
1217 }
1218 "dirreq-stats-end" => {
1219 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1220 desc.dir_stats_end = Some(ts);
1221 desc.dir_stats_interval = Some(interval);
1222 }
1223 "dirreq-v3-ips" => {
1224 desc.dir_v3_ips = Self::parse_geoip_to_count(value);
1225 }
1226 "dirreq-v3-reqs" => {
1227 desc.dir_v3_requests = Self::parse_geoip_to_count(value);
1228 }
1229 "dirreq-v3-resp" => {
1230 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1231 desc.dir_v3_responses = recognized;
1232 desc.dir_v3_responses_unknown = unrecognized;
1233 }
1234 "dirreq-v3-direct-dl" => {
1235 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1236 desc.dir_v3_direct_dl = recognized;
1237 desc.dir_v3_direct_dl_unknown = unrecognized;
1238 }
1239 "dirreq-v3-tunneled-dl" => {
1240 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1241 desc.dir_v3_tunneled_dl = recognized;
1242 desc.dir_v3_tunneled_dl_unknown = unrecognized;
1243 }
1244 "dirreq-v2-ips" => {
1245 desc.dir_v2_ips = Self::parse_geoip_to_count(value);
1246 }
1247 "dirreq-v2-reqs" => {
1248 desc.dir_v2_requests = Self::parse_geoip_to_count(value);
1249 }
1250 "dirreq-v2-resp" => {
1251 let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1252 desc.dir_v2_responses = recognized;
1253 desc.dir_v2_responses_unknown = unrecognized;
1254 }
1255 "dirreq-v2-direct-dl" => {
1256 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1257 desc.dir_v2_direct_dl = recognized;
1258 desc.dir_v2_direct_dl_unknown = unrecognized;
1259 }
1260 "dirreq-v2-tunneled-dl" => {
1261 let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1262 desc.dir_v2_tunneled_dl = recognized;
1263 desc.dir_v2_tunneled_dl_unknown = unrecognized;
1264 }
1265 "entry-stats-end" => {
1266 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1267 desc.entry_stats_end = Some(ts);
1268 desc.entry_stats_interval = Some(interval);
1269 }
1270 "entry-ips" => {
1271 desc.entry_ips = Self::parse_geoip_to_count(value);
1272 }
1273 "exit-stats-end" => {
1274 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1275 desc.exit_stats_end = Some(ts);
1276 desc.exit_stats_interval = Some(interval);
1277 }
1278 "exit-kibibytes-written" => {
1279 desc.exit_kibibytes_written = Self::parse_port_count(value);
1280 }
1281 "exit-kibibytes-read" => {
1282 desc.exit_kibibytes_read = Self::parse_port_count(value);
1283 }
1284 "exit-streams-opened" => {
1285 desc.exit_streams_opened = Self::parse_port_count(value);
1286 }
1287 "bridge-stats-end" => {
1288 let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1289 desc.bridge_stats_end = Some(ts);
1290 desc.bridge_stats_interval = Some(interval);
1291 }
1292 "bridge-ips" => {
1293 desc.bridge_ips = Self::parse_geoip_to_count(value);
1294 }
1295 "bridge-ip-versions" => {
1296 desc.ip_versions = Self::parse_geoip_to_count(value);
1297 }
1298 "bridge-ip-transports" => {
1299 desc.ip_transports = Self::parse_geoip_to_count(value);
1300 }
1301 "hidserv-stats-end" => {
1302 desc.hs_stats_end = Some(Self::parse_published_line(value)?);
1303 }
1304 "hidserv-rend-relayed-cells" => {
1305 let (stat, attr) = Self::parse_hs_stats(value);
1306 desc.hs_rend_cells = stat;
1307 desc.hs_rend_cells_attr = attr;
1308 }
1309 "hidserv-dir-onions-seen" => {
1310 let (stat, attr) = Self::parse_hs_stats(value);
1311 desc.hs_dir_onions_seen = stat;
1312 desc.hs_dir_onions_seen_attr = attr;
1313 }
1314 "padding-counts" => {
1315 let (ts, interval, counts) = Self::parse_padding_counts(value)?;
1316 desc.padding_counts_end = Some(ts);
1317 desc.padding_counts_interval = Some(interval);
1318 desc.padding_counts = counts;
1319 }
1320 _ => {
1321 if !line.is_empty() && !line.starts_with("-----") {
1322 desc.unrecognized_lines.push(line.to_string());
1323 }
1324 }
1325 }
1326 idx += 1;
1327 }
1328
1329 if desc.nickname.is_empty() {
1330 return Err(Error::Parse {
1331 location: "extra-info".to_string(),
1332 reason: "missing extra-info line".to_string(),
1333 });
1334 }
1335
1336 Ok(desc)
1337 }
1338
1339 fn to_descriptor_string(&self) -> String {
1340 let mut result = String::new();
1341
1342 result.push_str(&format!(
1343 "extra-info {} {}\n",
1344 self.nickname, self.fingerprint
1345 ));
1346 result.push_str(&format!(
1347 "published {}\n",
1348 self.published.format("%Y-%m-%d %H:%M:%S")
1349 ));
1350
1351 if let Some(ref history) = self.write_history {
1352 let values: String = history
1353 .values
1354 .iter()
1355 .map(|v| v.to_string())
1356 .collect::<Vec<_>>()
1357 .join(",");
1358 result.push_str(&format!(
1359 "write-history {} ({} s) {}\n",
1360 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1361 history.interval,
1362 values
1363 ));
1364 }
1365
1366 if let Some(ref history) = self.read_history {
1367 let values: String = history
1368 .values
1369 .iter()
1370 .map(|v| v.to_string())
1371 .collect::<Vec<_>>()
1372 .join(",");
1373 result.push_str(&format!(
1374 "read-history {} ({} s) {}\n",
1375 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1376 history.interval,
1377 values
1378 ));
1379 }
1380
1381 if let Some(ref history) = self.dir_write_history {
1382 let values: String = history
1383 .values
1384 .iter()
1385 .map(|v| v.to_string())
1386 .collect::<Vec<_>>()
1387 .join(",");
1388 result.push_str(&format!(
1389 "dirreq-write-history {} ({} s) {}\n",
1390 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1391 history.interval,
1392 values
1393 ));
1394 }
1395
1396 if let Some(ref history) = self.dir_read_history {
1397 let values: String = history
1398 .values
1399 .iter()
1400 .map(|v| v.to_string())
1401 .collect::<Vec<_>>()
1402 .join(",");
1403 result.push_str(&format!(
1404 "dirreq-read-history {} ({} s) {}\n",
1405 history.end_time.format("%Y-%m-%d %H:%M:%S"),
1406 history.interval,
1407 values
1408 ));
1409 }
1410
1411 if let Some(ref digest) = self.geoip_db_digest {
1412 result.push_str(&format!("geoip-db-digest {}\n", digest));
1413 }
1414
1415 if let Some(ref digest) = self.geoip6_db_digest {
1416 result.push_str(&format!("geoip6-db-digest {}\n", digest));
1417 }
1418
1419 if let Some(ref sig) = self.signature {
1420 result.push_str("router-signature\n");
1421 result.push_str(sig);
1422 result.push('\n');
1423 }
1424
1425 if let Some(ref digest) = self.router_digest {
1426 result.push_str(&format!("router-digest {}\n", digest));
1427 }
1428
1429 result
1430 }
1431
1432 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
1433 if self.is_bridge() {
1434 match (hash, encoding) {
1435 (DigestHash::Sha1, DigestEncoding::Hex) => {
1436 self.router_digest.clone().ok_or_else(|| Error::Parse {
1437 location: "digest".to_string(),
1438 reason: "bridge descriptor missing router-digest".to_string(),
1439 })
1440 }
1441 (DigestHash::Sha256, DigestEncoding::Base64) => self
1442 .router_digest_sha256
1443 .clone()
1444 .ok_or_else(|| Error::Parse {
1445 location: "digest".to_string(),
1446 reason: "bridge descriptor missing router-digest-sha256".to_string(),
1447 }),
1448 _ => Err(Error::Parse {
1449 location: "digest".to_string(),
1450 reason: "bridge extrainfo digests only available as sha1/hex or sha256/base64"
1451 .to_string(),
1452 }),
1453 }
1454 } else {
1455 let content_str = std::str::from_utf8(&self.raw_content).map_err(|_| Error::Parse {
1456 location: "digest".to_string(),
1457 reason: "invalid UTF-8 in raw content".to_string(),
1458 })?;
1459
1460 match hash {
1461 DigestHash::Sha1 => {
1462 let digest_content =
1463 Self::find_digest_content(content_str).ok_or_else(|| Error::Parse {
1464 location: "digest".to_string(),
1465 reason: "could not find digest content boundaries".to_string(),
1466 })?;
1467 Ok(compute_digest(digest_content.as_bytes(), hash, encoding))
1468 }
1469 DigestHash::Sha256 => Ok(compute_digest(&self.raw_content, hash, encoding)),
1470 }
1471 }
1472 }
1473
1474 fn raw_content(&self) -> &[u8] {
1475 &self.raw_content
1476 }
1477
1478 fn unrecognized_lines(&self) -> &[String] {
1479 &self.unrecognized_lines
1480 }
1481}
1482
1483impl FromStr for ExtraInfoDescriptor {
1484 type Err = Error;
1485
1486 fn from_str(s: &str) -> Result<Self, Self::Err> {
1487 Self::parse(s)
1488 }
1489}
1490
1491impl fmt::Display for ExtraInfoDescriptor {
1492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1493 write!(f, "{}", self.to_descriptor_string())
1494 }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499 use super::*;
1500
1501 const RELAY_EXTRA_INFO: &str = r#"@type extra-info 1.0
1502extra-info NINJA B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1503published 2012-05-05 17:03:50
1504write-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
1505read-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
1506dirreq-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
1507dirreq-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
1508router-signature
1509-----BEGIN SIGNATURE-----
1510K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
1511k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
15127LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
1513-----END SIGNATURE-----
1514"#;
1515
1516 const BRIDGE_EXTRA_INFO: &str = r#"@type bridge-extra-info 1.0
1517extra-info ec2bridgereaac65a3 1EC248422B57D9C0BD751892FE787585407479A4
1518published 2012-06-08 02:21:27
1519write-history 2012-06-08 02:10:38 (900 s) 343040,991232,5649408
1520read-history 2012-06-08 02:10:38 (900 s) 337920,437248,3995648
1521geoip-db-digest A27BE984989AB31C50D0861C7106B17A7EEC3756
1522dirreq-stats-end 2012-06-07 06:33:46 (86400 s)
1523dirreq-v3-ips
1524dirreq-v3-reqs
1525dirreq-v3-resp ok=72,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1526dirreq-v3-direct-dl complete=0,timeout=0,running=0
1527dirreq-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
1528bridge-stats-end 2012-06-07 06:33:53 (86400 s)
1529bridge-ips cn=16,ir=16,sy=16,us=16
1530router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1531"#;
1532
1533 const ED25519_EXTRA_INFO: &str = r#"@type extra-info 1.0
1534extra-info silverfoxden 4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E
1535identity-ed25519
1536-----BEGIN ED25519 CERT-----
1537AQQABhz0AQFcf5tGWLvPvr1sktoezBB95j6tAWSECa3Eo2ZuBtRNAQAgBABFAwSN
1538GcRlGIte4I1giLvQSTcXefT93rvx2PZ8wEDewxWdy6tzcLouPfE3Beu/eUyg8ntt
1539YuVlzi50WXzGlGnPmeounGLo0EDHTGzcLucFWpe0g/0ia6UDqgQiAySMBwI=
1540-----END ED25519 CERT-----
1541published 2015-08-22 19:21:12
1542write-history 2015-08-22 19:20:44 (14400 s) 14409728,23076864,7756800,6234112,7446528,12290048
1543read-history 2015-08-22 19:20:44 (14400 s) 20449280,23888896,9099264,7185408,8880128,13230080
1544geoip-db-digest 6882B8663F74C23E26E3C2274C24CAB2E82D67A2
1545geoip6-db-digest F063BD5247EB9829E6B9E586393D7036656DAF44
1546dirreq-stats-end 2015-08-22 11:58:30 (86400 s)
1547dirreq-v3-ips
1548dirreq-v3-reqs
1549dirreq-v3-resp ok=0,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1550dirreq-v3-direct-dl complete=0,timeout=0,running=0
1551dirreq-v3-tunneled-dl complete=0,timeout=0,running=0
1552router-sig-ed25519 g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw
1553router-signature
1554-----BEGIN SIGNATURE-----
1555R7kNaIWZrg3n3FWFBRMlEK2cbnha7gUIs8ToksLe+SF0dgoZiLyV3GKrnzdE/K6D
1556qdiOMN7eK04MOZVlgxkA5ayi61FTYVveK1HrDbJ+sEUwsviVGdif6kk/9DXOiyIJ
15577wP/tofgHj/aCbFZb1PGU0zrEVLa72hVJ6cCW8w/t1s=
1558-----END SIGNATURE-----
1559"#;
1560
1561 #[test]
1562 fn test_parse_relay_extra_info() {
1563 let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
1564
1565 assert_eq!(desc.nickname, "NINJA");
1566 assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
1567 assert_eq!(
1568 desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
1569 "2012-05-05 17:03:50"
1570 );
1571 assert!(!desc.is_bridge());
1572
1573 let write_history = desc.write_history.as_ref().unwrap();
1574 assert_eq!(write_history.interval, 900);
1575 assert_eq!(write_history.values.len(), 28);
1576 assert_eq!(write_history.values[0], 1082368);
1577
1578 let read_history = desc.read_history.as_ref().unwrap();
1579 assert_eq!(read_history.interval, 900);
1580 assert_eq!(read_history.values.len(), 28);
1581 assert_eq!(read_history.values[0], 3309568);
1582
1583 assert!(desc.signature.is_some());
1584 }
1585
1586 #[test]
1587 fn test_parse_bridge_extra_info() {
1588 let desc = ExtraInfoDescriptor::parse(BRIDGE_EXTRA_INFO).unwrap();
1589
1590 assert_eq!(desc.nickname, "ec2bridgereaac65a3");
1591 assert_eq!(desc.fingerprint, "1EC248422B57D9C0BD751892FE787585407479A4");
1592 assert!(desc.is_bridge());
1593 assert_eq!(
1594 desc.router_digest,
1595 Some("00A2AECCEAD3FEE033CFE29893387143146728EC".to_string())
1596 );
1597
1598 assert_eq!(
1599 desc.geoip_db_digest,
1600 Some("A27BE984989AB31C50D0861C7106B17A7EEC3756".to_string())
1601 );
1602
1603 assert_eq!(desc.dir_stats_interval, Some(86400));
1604 assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&72));
1605 assert_eq!(
1606 desc.dir_v3_responses.get(&DirResponse::NotEnoughSigs),
1607 Some(&0)
1608 );
1609
1610 assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&0));
1611 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Complete), Some(&68));
1612 assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Timeout), Some(&4));
1613
1614 assert_eq!(desc.bridge_stats_interval, Some(86400));
1615 assert_eq!(desc.bridge_ips.get("cn"), Some(&16));
1616 assert_eq!(desc.bridge_ips.get("us"), Some(&16));
1617 }
1618
1619 #[test]
1620 fn test_parse_ed25519_extra_info() {
1621 let desc = ExtraInfoDescriptor::parse(ED25519_EXTRA_INFO).unwrap();
1622
1623 assert_eq!(desc.nickname, "silverfoxden");
1624 assert_eq!(desc.fingerprint, "4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E");
1625 assert!(!desc.is_bridge());
1626
1627 assert!(desc.ed25519_certificate.is_some());
1628 assert!(desc
1629 .ed25519_certificate
1630 .as_ref()
1631 .unwrap()
1632 .contains("ED25519 CERT"));
1633
1634 assert!(desc.ed25519_signature.is_some());
1635 assert!(desc
1636 .ed25519_signature
1637 .as_ref()
1638 .unwrap()
1639 .starts_with("g6Zg7Er8K7C1"));
1640
1641 assert_eq!(
1642 desc.geoip_db_digest,
1643 Some("6882B8663F74C23E26E3C2274C24CAB2E82D67A2".to_string())
1644 );
1645 assert_eq!(
1646 desc.geoip6_db_digest,
1647 Some("F063BD5247EB9829E6B9E586393D7036656DAF44".to_string())
1648 );
1649
1650 let write_history = desc.write_history.as_ref().unwrap();
1651 assert_eq!(write_history.interval, 14400);
1652 assert_eq!(write_history.values.len(), 6);
1653 }
1654
1655 #[test]
1656 fn test_dir_response_parsing() {
1657 assert_eq!(DirResponse::from_str("ok").unwrap(), DirResponse::Ok);
1658 assert_eq!(
1659 DirResponse::from_str("not-enough-sigs").unwrap(),
1660 DirResponse::NotEnoughSigs
1661 );
1662 assert_eq!(
1663 DirResponse::from_str("unavailable").unwrap(),
1664 DirResponse::Unavailable
1665 );
1666 assert_eq!(
1667 DirResponse::from_str("not-found").unwrap(),
1668 DirResponse::NotFound
1669 );
1670 assert_eq!(
1671 DirResponse::from_str("not-modified").unwrap(),
1672 DirResponse::NotModified
1673 );
1674 assert_eq!(DirResponse::from_str("busy").unwrap(), DirResponse::Busy);
1675 }
1676
1677 #[test]
1678 fn test_dir_stat_parsing() {
1679 assert_eq!(DirStat::from_str("complete").unwrap(), DirStat::Complete);
1680 assert_eq!(DirStat::from_str("timeout").unwrap(), DirStat::Timeout);
1681 assert_eq!(DirStat::from_str("running").unwrap(), DirStat::Running);
1682 assert_eq!(DirStat::from_str("min").unwrap(), DirStat::Min);
1683 assert_eq!(DirStat::from_str("max").unwrap(), DirStat::Max);
1684 assert_eq!(DirStat::from_str("d1").unwrap(), DirStat::D1);
1685 assert_eq!(DirStat::from_str("q1").unwrap(), DirStat::Q1);
1686 assert_eq!(DirStat::from_str("md").unwrap(), DirStat::Md);
1687 }
1688
1689 #[test]
1690 fn test_history_parsing() {
1691 let history = ExtraInfoDescriptor::parse_history_line(
1692 "2012-05-05 17:02:45 (900 s) 1082368,19456,50176",
1693 )
1694 .unwrap();
1695
1696 assert_eq!(history.interval, 900);
1697 assert_eq!(history.values, vec![1082368, 19456, 50176]);
1698 }
1699
1700 #[test]
1701 fn test_geoip_to_count_parsing() {
1702 let result = ExtraInfoDescriptor::parse_geoip_to_count("cn=16,ir=16,us=8");
1703 assert_eq!(result.get("cn"), Some(&16));
1704 assert_eq!(result.get("ir"), Some(&16));
1705 assert_eq!(result.get("us"), Some(&8));
1706 }
1707
1708 #[test]
1709 fn test_port_count_parsing() {
1710 let result = ExtraInfoDescriptor::parse_port_count("80=1000,443=2000,other=500");
1711 assert_eq!(result.get(&PortKey::Port(80)), Some(&1000));
1712 assert_eq!(result.get(&PortKey::Port(443)), Some(&2000));
1713 assert_eq!(result.get(&PortKey::Other), Some(&500));
1714 }
1715
1716 #[test]
1717 fn test_missing_extra_info_line() {
1718 let content = "published 2012-05-05 17:03:50\n";
1719 let result = ExtraInfoDescriptor::parse(content);
1720 assert!(result.is_err());
1721 }
1722
1723 #[test]
1724 fn test_invalid_fingerprint() {
1725 let content = "extra-info NINJA INVALID\npublished 2012-05-05 17:03:50\n";
1726 let result = ExtraInfoDescriptor::parse(content);
1727 assert!(result.is_err());
1728 }
1729
1730 #[test]
1731 fn test_conn_bi_direct() {
1732 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1733published 2012-05-05 17:03:50
1734conn-bi-direct 2012-05-03 12:07:50 (500 s) 277431,12089,0,2134
1735"#;
1736 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1737 assert!(desc.conn_bi_direct_end.is_some());
1738 assert_eq!(desc.conn_bi_direct_interval, Some(500));
1739 assert_eq!(desc.conn_bi_direct_below, Some(277431));
1740 assert_eq!(desc.conn_bi_direct_read, Some(12089));
1741 assert_eq!(desc.conn_bi_direct_write, Some(0));
1742 assert_eq!(desc.conn_bi_direct_both, Some(2134));
1743 }
1744
1745 #[test]
1746 fn test_cell_circuits_per_decile() {
1747 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1748published 2012-05-05 17:03:50
1749cell-circuits-per-decile 25
1750"#;
1751 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1752 assert_eq!(desc.cell_circuits_per_decile, Some(25));
1753 }
1754
1755 #[test]
1756 fn test_hidden_service_stats() {
1757 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1758published 2012-05-05 17:03:50
1759hidserv-stats-end 2012-05-03 12:07:50
1760hidserv-rend-relayed-cells 345 spiffy=true snowmen=neat
1761hidserv-dir-onions-seen 123
1762"#;
1763 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1764 assert!(desc.hs_stats_end.is_some());
1765 assert_eq!(desc.hs_rend_cells, Some(345));
1766 assert_eq!(
1767 desc.hs_rend_cells_attr.get("spiffy"),
1768 Some(&"true".to_string())
1769 );
1770 assert_eq!(
1771 desc.hs_rend_cells_attr.get("snowmen"),
1772 Some(&"neat".to_string())
1773 );
1774 assert_eq!(desc.hs_dir_onions_seen, Some(123));
1775 }
1776
1777 #[test]
1778 fn test_padding_counts() {
1779 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1780published 2012-05-05 17:03:50
1781padding-counts 2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000
1782"#;
1783 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1784 assert!(desc.padding_counts_end.is_some());
1785 assert_eq!(desc.padding_counts_interval, Some(86400));
1786 assert_eq!(
1787 desc.padding_counts.get("bin-size"),
1788 Some(&"10000".to_string())
1789 );
1790 assert_eq!(
1791 desc.padding_counts.get("write-drop"),
1792 Some(&"0".to_string())
1793 );
1794 assert_eq!(
1795 desc.padding_counts.get("write-pad"),
1796 Some(&"10000".to_string())
1797 );
1798 }
1799
1800 #[test]
1801 fn test_transport_line() {
1802 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1803published 2012-05-05 17:03:50
1804transport obfs2 83.212.96.201:33570
1805"#;
1806 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1807 assert!(desc.transports.contains_key("obfs2"));
1808 let transport = desc.transports.get("obfs2").unwrap();
1809 assert_eq!(transport.address, Some("83.212.96.201".to_string()));
1810 assert_eq!(transport.port, Some(33570));
1811 }
1812
1813 #[test]
1814 fn test_bridge_ip_versions() {
1815 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1816published 2012-05-05 17:03:50
1817bridge-ip-versions v4=16,v6=40
1818router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1819"#;
1820 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1821 assert_eq!(desc.ip_versions.get("v4"), Some(&16));
1822 assert_eq!(desc.ip_versions.get("v6"), Some(&40));
1823 }
1824
1825 #[test]
1826 fn test_bridge_ip_transports() {
1827 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1828published 2012-05-05 17:03:50
1829bridge-ip-transports <OR>=16,<??>=40
1830router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1831"#;
1832 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1833 assert_eq!(desc.ip_transports.get("<OR>"), Some(&16));
1834 assert_eq!(desc.ip_transports.get("<??>"), Some(&40));
1835 }
1836
1837 #[test]
1838 fn test_exit_stats() {
1839 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1840published 2012-05-05 17:03:50
1841exit-stats-end 2012-05-03 12:07:50 (86400 s)
1842exit-kibibytes-written 80=115533759,443=1777,other=500
1843exit-kibibytes-read 80=100,443=200
1844exit-streams-opened 80=50,443=100
1845"#;
1846 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1847 assert!(desc.exit_stats_end.is_some());
1848 assert_eq!(desc.exit_stats_interval, Some(86400));
1849 assert_eq!(
1850 desc.exit_kibibytes_written.get(&PortKey::Port(80)),
1851 Some(&115533759)
1852 );
1853 assert_eq!(
1854 desc.exit_kibibytes_written.get(&PortKey::Port(443)),
1855 Some(&1777)
1856 );
1857 assert_eq!(desc.exit_kibibytes_written.get(&PortKey::Other), Some(&500));
1858 assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Port(80)), Some(&100));
1859 assert_eq!(desc.exit_streams_opened.get(&PortKey::Port(80)), Some(&50));
1860 }
1861
1862 #[test]
1863 fn test_entry_stats() {
1864 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1865published 2012-05-05 17:03:50
1866entry-stats-end 2012-05-03 12:07:50 (86400 s)
1867entry-ips uk=5,de=3,jp=2
1868"#;
1869 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1870 assert!(desc.entry_stats_end.is_some());
1871 assert_eq!(desc.entry_stats_interval, Some(86400));
1872 assert_eq!(desc.entry_ips.get("uk"), Some(&5));
1873 assert_eq!(desc.entry_ips.get("de"), Some(&3));
1874 assert_eq!(desc.entry_ips.get("jp"), Some(&2));
1875 }
1876
1877 #[test]
1878 fn test_cell_stats() {
1879 let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1880published 2012-05-05 17:03:50
1881cell-stats-end 2012-05-03 12:07:50 (86400 s)
1882cell-processed-cells 2.3,-4.6,8.9
1883cell-queued-cells 1.0,2.0,3.0
1884cell-time-in-queue 10.5,20.5,30.5
1885"#;
1886 let desc = ExtraInfoDescriptor::parse(content).unwrap();
1887 assert!(desc.cell_stats_end.is_some());
1888 assert_eq!(desc.cell_stats_interval, Some(86400));
1889 assert_eq!(desc.cell_processed_cells, vec![2.3, -4.6, 8.9]);
1890 assert_eq!(desc.cell_queued_cells, vec![1.0, 2.0, 3.0]);
1891 assert_eq!(desc.cell_time_in_queue, vec![10.5, 20.5, 30.5]);
1892 }
1893}