stem_rs/descriptor/
extra_info.rs

1//! Extra-info descriptor parsing for Tor relay and bridge extra-info documents.
2//!
3//! Extra-info descriptors contain non-vital but interesting information about
4//! Tor relays such as usage statistics, bandwidth history, and directory
5//! request statistics. Unlike server descriptors, these are not required for
6//! Tor clients to function and are not fetched by default.
7//!
8//! # Overview
9//!
10//! Extra-info descriptors are published by relays whenever their server
11//! descriptor is published. They contain detailed statistics about:
12//!
13//! - **Bandwidth history** - Read/write traffic over time
14//! - **Directory statistics** - Request counts, response types, download speeds
15//! - **Cell statistics** - Circuit cell processing metrics
16//! - **Exit statistics** - Traffic per port for exit relays
17//! - **Bridge statistics** - Client connection data for bridges
18//! - **Hidden service statistics** - Onion service activity metrics
19//!
20//! # Descriptor Types
21//!
22//! | Type | Description | Signature |
23//! |------|-------------|-----------|
24//! | Relay | Standard relay extra-info | RSA signature |
25//! | Bridge | Bridge relay extra-info | No signature (has router-digest) |
26//!
27//! # Sources
28//!
29//! Extra-info descriptors are available from:
30//!
31//! - **Control port** - Via `GETINFO extra-info/digest/*` (requires `DownloadExtraInfo 1`)
32//! - **Data directory** - The `cached-extrainfo` file
33//! - **CollecTor** - Archived descriptors from metrics.torproject.org
34//! - **Directory authorities** - Via DirPort requests
35//!
36//! # Example
37//!
38//! ```rust
39//! use stem_rs::descriptor::extra_info::ExtraInfoDescriptor;
40//! use stem_rs::descriptor::Descriptor;
41//!
42//! let content = r#"extra-info example B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
43//! published 2024-01-15 12:00:00
44//! write-history 2024-01-15 12:00:00 (900 s) 1000000,2000000,3000000
45//! read-history 2024-01-15 12:00:00 (900 s) 500000,1000000,1500000
46//! "#;
47//!
48//! let desc = ExtraInfoDescriptor::parse(content).unwrap();
49//! assert_eq!(desc.nickname, "example");
50//! assert!(desc.write_history.is_some());
51//! ```
52//!
53//! # Statistics Categories
54//!
55//! ## Bandwidth History
56//!
57//! The `read-history` and `write-history` lines record bytes transferred
58//! over time intervals (typically 900 seconds = 15 minutes).
59//!
60//! ## Directory Statistics
61//!
62//! Directory mirrors report request statistics including:
63//! - Client IP counts by country (`dirreq-v3-ips`)
64//! - Request counts by country (`dirreq-v3-reqs`)
65//! - Response status counts (`dirreq-v3-resp`)
66//! - Download speed statistics (`dirreq-v3-direct-dl`, `dirreq-v3-tunneled-dl`)
67//!
68//! ## Exit Statistics
69//!
70//! Exit relays report traffic per destination port:
71//! - `exit-kibibytes-written` - Outbound traffic
72//! - `exit-kibibytes-read` - Inbound traffic
73//! - `exit-streams-opened` - Connection counts
74//!
75//! ## Bridge Statistics
76//!
77//! Bridges report client connection data:
78//! - `bridge-ips` - Client counts by country
79//! - `bridge-ip-versions` - IPv4 vs IPv6 client counts
80//! - `bridge-ip-transports` - Pluggable transport usage
81//!
82//! # See Also
83//!
84//! - [`crate::descriptor::server`] - Server descriptors (published alongside extra-info)
85//! - [`crate::descriptor::consensus`] - Network status documents
86//!
87//! # See Also
88//!
89//! - [Tor Directory Protocol Specification, Section 2.1.2](https://spec.torproject.org/dir-spec)
90//! - Python Stem's `ExtraInfoDescriptor` class
91
92use 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
103/// Result type for parsing bi-directional connection statistics.
104type ConnBiDirectResult = Result<(DateTime<Utc>, u32, u32, u32, u32, u32), Error>;
105
106/// Result type for parsing padding count statistics.
107type PaddingCountsResult = Result<(DateTime<Utc>, u32, HashMap<String, String>), Error>;
108
109/// Response status for directory requests.
110///
111/// These statuses indicate the outcome of network status requests
112/// made to directory servers.
113///
114/// # Variants
115///
116/// | Status | Description |
117/// |--------|-------------|
118/// | `Ok` | Request completed successfully |
119/// | `NotEnoughSigs` | Network status wasn't signed by enough authorities |
120/// | `Unavailable` | Requested network status was unavailable |
121/// | `NotFound` | Requested network status was not found |
122/// | `NotModified` | Network status unmodified since If-Modified-Since time |
123/// | `Busy` | Directory server was too busy to respond |
124///
125/// # Example
126///
127/// ```rust
128/// use stem_rs::descriptor::extra_info::DirResponse;
129/// use std::str::FromStr;
130///
131/// let status = DirResponse::from_str("ok").unwrap();
132/// assert_eq!(status, DirResponse::Ok);
133/// ```
134#[derive(Debug, Clone, PartialEq, Eq, Hash)]
135pub enum DirResponse {
136    /// Request completed successfully.
137    Ok,
138    /// Network status wasn't signed by enough authorities.
139    NotEnoughSigs,
140    /// Requested network status was unavailable.
141    Unavailable,
142    /// Requested network status was not found.
143    NotFound,
144    /// Network status unmodified since If-Modified-Since time.
145    NotModified,
146    /// Directory server was too busy to respond.
147    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/// Download statistics for directory requests.
170///
171/// These statistics measure the performance of directory downloads,
172/// including completion rates and speed percentiles.
173///
174/// # Variants
175///
176/// | Stat | Description |
177/// |------|-------------|
178/// | `Complete` | Requests that completed successfully |
179/// | `Timeout` | Requests that didn't complete within timeout |
180/// | `Running` | Requests still in progress when measured |
181/// | `Min` | Minimum download rate (B/s) |
182/// | `Max` | Maximum download rate (B/s) |
183/// | `D1`-`D9` | Decile download rates (10th-90th percentile) |
184/// | `Q1`, `Q3` | Quartile download rates (25th, 75th percentile) |
185/// | `Md` | Median download rate |
186///
187/// # Example
188///
189/// ```rust
190/// use stem_rs::descriptor::extra_info::DirStat;
191/// use std::str::FromStr;
192///
193/// let stat = DirStat::from_str("complete").unwrap();
194/// assert_eq!(stat, DirStat::Complete);
195/// ```
196#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub enum DirStat {
198    /// Requests that completed successfully.
199    Complete,
200    /// Requests that timed out (10 minute default).
201    Timeout,
202    /// Requests still running when measurement was taken.
203    Running,
204    /// Minimum download rate in bytes per second.
205    Min,
206    /// Maximum download rate in bytes per second.
207    Max,
208    /// 10th percentile download rate.
209    D1,
210    /// 20th percentile download rate.
211    D2,
212    /// 30th percentile download rate.
213    D3,
214    /// 40th percentile download rate.
215    D4,
216    /// 60th percentile download rate.
217    D6,
218    /// 70th percentile download rate.
219    D7,
220    /// 80th percentile download rate.
221    D8,
222    /// 90th percentile download rate.
223    D9,
224    /// First quartile (25th percentile) download rate.
225    Q1,
226    /// Third quartile (75th percentile) download rate.
227    Q3,
228    /// Median download rate.
229    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/// Bandwidth history data for a time period.
262///
263/// Records bytes transferred over a series of fixed-length intervals.
264/// This is used for read/write history and directory request history.
265///
266/// # Format
267///
268/// The history line format is:
269/// ```text
270/// keyword YYYY-MM-DD HH:MM:SS (INTERVAL s) VALUE,VALUE,...
271/// ```
272///
273/// # Example
274///
275/// ```rust
276/// use stem_rs::descriptor::extra_info::BandwidthHistory;
277/// use chrono::Utc;
278///
279/// let history = BandwidthHistory {
280///     end_time: Utc::now(),
281///     interval: 900,  // 15 minutes
282///     values: vec![1000000, 2000000, 3000000],
283/// };
284///
285/// assert_eq!(history.interval, 900);
286/// assert_eq!(history.values.len(), 3);
287/// ```
288#[derive(Debug, Clone, PartialEq)]
289pub struct BandwidthHistory {
290    /// End time of the most recent interval (UTC).
291    pub end_time: DateTime<Utc>,
292
293    /// Length of each interval in seconds (typically 900 = 15 minutes).
294    pub interval: u32,
295
296    /// Bytes transferred during each interval, oldest first.
297    ///
298    /// Values can be negative in some cases due to historical bugs.
299    pub values: Vec<i64>,
300}
301
302/// Pluggable transport information.
303///
304/// Describes a pluggable transport method available on a bridge.
305/// In published bridge descriptors, the address and port are typically
306/// scrubbed for privacy.
307///
308/// # Example
309///
310/// ```rust
311/// use stem_rs::descriptor::extra_info::Transport;
312///
313/// let transport = Transport {
314///     name: "obfs4".to_string(),
315///     address: Some("192.0.2.1".to_string()),
316///     port: Some(443),
317///     args: vec!["cert=...".to_string()],
318/// };
319///
320/// assert_eq!(transport.name, "obfs4");
321/// ```
322#[derive(Debug, Clone, PartialEq)]
323pub struct Transport {
324    /// Transport method name (e.g., "obfs4", "snowflake").
325    pub name: String,
326
327    /// Transport address (may be scrubbed in published descriptors).
328    pub address: Option<String>,
329
330    /// Transport port (may be scrubbed in published descriptors).
331    pub port: Option<u16>,
332
333    /// Additional transport arguments.
334    pub args: Vec<String>,
335}
336
337/// Extra-info descriptor containing relay statistics and metadata.
338///
339/// Extra-info descriptors are published alongside server descriptors and
340/// contain detailed statistics about relay operation. They are not required
341/// for Tor clients to function but provide valuable metrics for network
342/// analysis.
343///
344/// # Overview
345///
346/// The descriptor contains several categories of information:
347///
348/// - **Identity** - Nickname, fingerprint, publication time
349/// - **Bandwidth history** - Read/write traffic over time
350/// - **Directory statistics** - Request counts and download speeds
351/// - **Cell statistics** - Circuit cell processing metrics
352/// - **Exit statistics** - Traffic per destination port
353/// - **Bridge statistics** - Client connection data
354/// - **Hidden service statistics** - Onion service activity
355/// - **Cryptographic data** - Ed25519 certificates and signatures
356///
357/// # Relay vs Bridge
358///
359/// Use [`is_bridge()`](Self::is_bridge) to distinguish between relay and
360/// bridge extra-info descriptors:
361///
362/// - **Relay**: Has `router-signature` line with RSA signature
363/// - **Bridge**: Has `router-digest` line instead of signature
364///
365/// # Example
366///
367/// ```rust
368/// use stem_rs::descriptor::extra_info::ExtraInfoDescriptor;
369/// use stem_rs::descriptor::Descriptor;
370///
371/// let content = r#"extra-info MyRelay B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
372/// published 2024-01-15 12:00:00
373/// write-history 2024-01-15 12:00:00 (900 s) 1000000,2000000
374/// read-history 2024-01-15 12:00:00 (900 s) 500000,1000000
375/// "#;
376///
377/// let desc = ExtraInfoDescriptor::parse(content).unwrap();
378///
379/// // Check identity
380/// assert_eq!(desc.nickname, "MyRelay");
381/// assert_eq!(desc.fingerprint.len(), 40);
382///
383/// // Check bandwidth history
384/// if let Some(ref history) = desc.write_history {
385///     println!("Write interval: {} seconds", history.interval);
386///     println!("Values: {:?}", history.values);
387/// }
388/// ```
389///
390/// # Statistics Fields
391///
392/// ## Bandwidth History
393///
394/// - `read_history` / `write_history` - Relay traffic
395/// - `dir_read_history` / `dir_write_history` - Directory traffic
396///
397/// ## Directory Statistics
398///
399/// - `dir_v3_ips` / `dir_v3_requests` - Client counts by country
400/// - `dir_v3_responses` - Response status counts
401/// - `dir_v3_direct_dl` / `dir_v3_tunneled_dl` - Download speed stats
402///
403/// ## Exit Statistics
404///
405/// - `exit_kibibytes_written` / `exit_kibibytes_read` - Traffic per port
406/// - `exit_streams_opened` - Connection counts per port
407///
408/// ## Bridge Statistics
409///
410/// - `bridge_ips` - Client counts by country
411/// - `ip_versions` - IPv4 vs IPv6 client counts
412/// - `ip_transports` - Pluggable transport usage
413///
414/// # Thread Safety
415///
416/// `ExtraInfoDescriptor` is `Send` and `Sync`, making it safe to share
417/// across threads.
418///
419/// # See Also
420///
421/// - [`crate::descriptor::server::ServerDescriptor`] - Published alongside extra-info
422/// - [`BandwidthHistory`] - Bandwidth history data structure
423/// - [`DirResponse`] - Directory response status codes
424/// - [`DirStat`] - Directory download statistics
425#[derive(Debug, Clone, PartialEq, Builder)]
426#[builder(setter(into, strip_option))]
427pub struct ExtraInfoDescriptor {
428    /// The relay's nickname (1-19 alphanumeric characters).
429    pub nickname: String,
430
431    /// The relay's identity fingerprint (40 uppercase hex characters).
432    ///
433    /// This is the SHA-1 hash of the relay's identity key.
434    pub fingerprint: String,
435
436    /// When this descriptor was published (UTC).
437    pub published: DateTime<Utc>,
438
439    /// SHA-1 digest of the GeoIP database for IPv4 addresses.
440    #[builder(default)]
441    pub geoip_db_digest: Option<String>,
442
443    /// SHA-1 digest of the GeoIP database for IPv6 addresses.
444    #[builder(default)]
445    pub geoip6_db_digest: Option<String>,
446
447    /// Pluggable transports available on this relay (bridges only).
448    ///
449    /// Maps transport name to transport details.
450    pub transports: HashMap<String, Transport>,
451
452    /// Bytes read by the relay over time.
453    #[builder(default)]
454    pub read_history: Option<BandwidthHistory>,
455
456    /// Bytes written by the relay over time.
457    #[builder(default)]
458    pub write_history: Option<BandwidthHistory>,
459
460    /// Bytes read for directory requests over time.
461    #[builder(default)]
462    pub dir_read_history: Option<BandwidthHistory>,
463
464    /// Bytes written for directory requests over time.
465    #[builder(default)]
466    pub dir_write_history: Option<BandwidthHistory>,
467
468    /// End time for bi-directional connection statistics.
469    #[builder(default)]
470    pub conn_bi_direct_end: Option<DateTime<Utc>>,
471
472    /// Interval for bi-directional connection statistics (seconds).
473    #[builder(default)]
474    pub conn_bi_direct_interval: Option<u32>,
475
476    /// Connections that read/wrote less than 20 KiB.
477    #[builder(default)]
478    pub conn_bi_direct_below: Option<u32>,
479
480    /// Connections that read at least 10x more than wrote.
481    #[builder(default)]
482    pub conn_bi_direct_read: Option<u32>,
483
484    /// Connections that wrote at least 10x more than read.
485    #[builder(default)]
486    pub conn_bi_direct_write: Option<u32>,
487
488    /// Connections with balanced read/write (remaining).
489    #[builder(default)]
490    pub conn_bi_direct_both: Option<u32>,
491
492    /// End time for cell statistics collection.
493    #[builder(default)]
494    pub cell_stats_end: Option<DateTime<Utc>>,
495
496    /// Interval for cell statistics (seconds).
497    #[builder(default)]
498    pub cell_stats_interval: Option<u32>,
499
500    /// Mean processed cells per circuit, by decile.
501    pub cell_processed_cells: Vec<f64>,
502
503    /// Mean queued cells per circuit, by decile.
504    pub cell_queued_cells: Vec<f64>,
505
506    /// Mean time cells spent in queue (milliseconds), by decile.
507    pub cell_time_in_queue: Vec<f64>,
508
509    /// Mean number of circuits in a decile.
510    #[builder(default)]
511    pub cell_circuits_per_decile: Option<u32>,
512
513    /// End time for directory statistics collection.
514    #[builder(default)]
515    pub dir_stats_end: Option<DateTime<Utc>>,
516
517    /// Interval for directory statistics (seconds).
518    #[builder(default)]
519    pub dir_stats_interval: Option<u32>,
520
521    /// V3 directory request client IPs by country code.
522    pub dir_v3_ips: HashMap<String, u32>,
523
524    /// V3 directory request counts by country code.
525    pub dir_v3_requests: HashMap<String, u32>,
526
527    /// V3 directory response status counts.
528    pub dir_v3_responses: HashMap<DirResponse, u32>,
529
530    /// Unrecognized V3 directory response statuses.
531    pub dir_v3_responses_unknown: HashMap<String, u32>,
532
533    /// V3 direct download statistics (via DirPort).
534    pub dir_v3_direct_dl: HashMap<DirStat, u32>,
535
536    /// Unrecognized V3 direct download statistics.
537    pub dir_v3_direct_dl_unknown: HashMap<String, u32>,
538
539    /// V3 tunneled download statistics (via ORPort).
540    pub dir_v3_tunneled_dl: HashMap<DirStat, u32>,
541
542    /// Unrecognized V3 tunneled download statistics.
543    pub dir_v3_tunneled_dl_unknown: HashMap<String, u32>,
544
545    /// V2 directory request client IPs by country code (deprecated).
546    pub dir_v2_ips: HashMap<String, u32>,
547
548    /// V2 directory request counts by country code (deprecated).
549    pub dir_v2_requests: HashMap<String, u32>,
550
551    /// V2 directory response status counts (deprecated).
552    pub dir_v2_responses: HashMap<DirResponse, u32>,
553
554    /// Unrecognized V2 directory response statuses (deprecated).
555    pub dir_v2_responses_unknown: HashMap<String, u32>,
556
557    /// V2 direct download statistics (deprecated).
558    pub dir_v2_direct_dl: HashMap<DirStat, u32>,
559
560    /// Unrecognized V2 direct download statistics (deprecated).
561    pub dir_v2_direct_dl_unknown: HashMap<String, u32>,
562
563    /// V2 tunneled download statistics (deprecated).
564    pub dir_v2_tunneled_dl: HashMap<DirStat, u32>,
565
566    /// Unrecognized V2 tunneled download statistics (deprecated).
567    pub dir_v2_tunneled_dl_unknown: HashMap<String, u32>,
568
569    /// End time for entry guard statistics.
570    #[builder(default)]
571    pub entry_stats_end: Option<DateTime<Utc>>,
572
573    /// Interval for entry guard statistics (seconds).
574    #[builder(default)]
575    pub entry_stats_interval: Option<u32>,
576
577    /// Entry guard client IPs by country code.
578    pub entry_ips: HashMap<String, u32>,
579
580    /// End time for exit statistics.
581    #[builder(default)]
582    pub exit_stats_end: Option<DateTime<Utc>>,
583
584    /// Interval for exit statistics (seconds).
585    #[builder(default)]
586    pub exit_stats_interval: Option<u32>,
587
588    /// Kibibytes written per destination port.
589    pub exit_kibibytes_written: HashMap<PortKey, u64>,
590
591    /// Kibibytes read per destination port.
592    pub exit_kibibytes_read: HashMap<PortKey, u64>,
593
594    /// Streams opened per destination port.
595    pub exit_streams_opened: HashMap<PortKey, u64>,
596
597    /// End time for bridge statistics.
598    #[builder(default)]
599    pub bridge_stats_end: Option<DateTime<Utc>>,
600
601    /// Interval for bridge statistics (seconds).
602    #[builder(default)]
603    pub bridge_stats_interval: Option<u32>,
604
605    /// Bridge client IPs by country code.
606    pub bridge_ips: HashMap<String, u32>,
607
608    /// Bridge client counts by IP version (v4, v6).
609    pub ip_versions: HashMap<String, u32>,
610
611    /// Bridge client counts by transport method.
612    pub ip_transports: HashMap<String, u32>,
613
614    /// End time for hidden service statistics.
615    #[builder(default)]
616    pub hs_stats_end: Option<DateTime<Utc>>,
617
618    /// Rounded count of RENDEZVOUS1 cells relayed.
619    #[builder(default)]
620    pub hs_rend_cells: Option<u64>,
621
622    /// Additional attributes for hs_rend_cells.
623    pub hs_rend_cells_attr: HashMap<String, String>,
624
625    /// Rounded count of unique onion service identities seen.
626    #[builder(default)]
627    pub hs_dir_onions_seen: Option<u64>,
628
629    /// Additional attributes for hs_dir_onions_seen.
630    pub hs_dir_onions_seen_attr: HashMap<String, String>,
631
632    /// End time for padding count statistics.
633    #[builder(default)]
634    pub padding_counts_end: Option<DateTime<Utc>>,
635
636    /// Interval for padding count statistics (seconds).
637    #[builder(default)]
638    pub padding_counts_interval: Option<u32>,
639
640    /// Padding-related statistics.
641    pub padding_counts: HashMap<String, String>,
642
643    /// Ed25519 certificate (PEM-encoded).
644    #[builder(default)]
645    pub ed25519_certificate: Option<String>,
646
647    /// Ed25519 signature of the descriptor.
648    #[builder(default)]
649    pub ed25519_signature: Option<String>,
650
651    /// RSA signature of the descriptor (relay extra-info only).
652    #[builder(default)]
653    pub signature: Option<String>,
654
655    /// Router digest for bridge extra-info descriptors.
656    ///
657    /// Present only in bridge descriptors; indicates this is a bridge.
658    #[builder(default)]
659    pub router_digest: Option<String>,
660
661    /// SHA-256 router digest (base64).
662    #[builder(default)]
663    pub router_digest_sha256: Option<String>,
664
665    /// Raw descriptor content for digest computation.
666    raw_content: Vec<u8>,
667
668    /// Lines not recognized during parsing.
669    unrecognized_lines: Vec<String>,
670}
671
672/// Key for port-based statistics in exit traffic data.
673///
674/// Exit statistics are grouped by destination port. The special "other"
675/// category aggregates traffic to ports not individually tracked.
676///
677/// # Example
678///
679/// ```rust
680/// use stem_rs::descriptor::extra_info::PortKey;
681///
682/// let http = PortKey::Port(80);
683/// let https = PortKey::Port(443);
684/// let other = PortKey::Other;
685///
686/// assert_eq!(format!("{}", http), "80");
687/// assert_eq!(format!("{}", other), "other");
688/// ```
689#[derive(Debug, Clone, PartialEq, Eq, Hash)]
690pub enum PortKey {
691    /// A specific port number.
692    Port(u16),
693
694    /// Aggregate of all other ports not individually tracked.
695    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    /// Finds the content to be hashed for digest computation.
1107    ///
1108    /// For relay extra-info descriptors, the digest is computed over
1109    /// the content from "extra-info " through "router-signature\n".
1110    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    /// Returns whether this is a bridge extra-info descriptor.
1119    ///
1120    /// Bridge descriptors have a `router-digest` line instead of a
1121    /// `router-signature` line.
1122    ///
1123    /// # Example
1124    ///
1125    /// ```rust
1126    /// use stem_rs::descriptor::extra_info::ExtraInfoDescriptor;
1127    /// use stem_rs::descriptor::Descriptor;
1128    ///
1129    /// let relay_content = r#"extra-info relay B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1130    /// published 2024-01-15 12:00:00
1131    /// "#;
1132    ///
1133    /// let bridge_content = r#"extra-info bridge B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1134    /// published 2024-01-15 12:00:00
1135    /// router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1136    /// "#;
1137    ///
1138    /// let relay = ExtraInfoDescriptor::parse(relay_content).unwrap();
1139    /// let bridge = ExtraInfoDescriptor::parse(bridge_content).unwrap();
1140    ///
1141    /// assert!(!relay.is_bridge());
1142    /// assert!(bridge.is_bridge());
1143    /// ```
1144    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}