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