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};
97
98use crate::Error;
99
100use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
101
102/// Result type for parsing bi-directional connection statistics.
103type ConnBiDirectResult = Result<(DateTime<Utc>, u32, u32, u32, u32, u32), Error>;
104
105/// Result type for parsing padding count statistics.
106type PaddingCountsResult = Result<(DateTime<Utc>, u32, HashMap<String, String>), Error>;
107
108/// Response status for directory requests.
109///
110/// These statuses indicate the outcome of network status requests
111/// made to directory servers.
112///
113/// # Variants
114///
115/// | Status | Description |
116/// |--------|-------------|
117/// | `Ok` | Request completed successfully |
118/// | `NotEnoughSigs` | Network status wasn't signed by enough authorities |
119/// | `Unavailable` | Requested network status was unavailable |
120/// | `NotFound` | Requested network status was not found |
121/// | `NotModified` | Network status unmodified since If-Modified-Since time |
122/// | `Busy` | Directory server was too busy to respond |
123///
124/// # Example
125///
126/// ```rust
127/// use stem_rs::descriptor::extra_info::DirResponse;
128/// use std::str::FromStr;
129///
130/// let status = DirResponse::from_str("ok").unwrap();
131/// assert_eq!(status, DirResponse::Ok);
132/// ```
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134pub enum DirResponse {
135    /// Request completed successfully.
136    Ok,
137    /// Network status wasn't signed by enough authorities.
138    NotEnoughSigs,
139    /// Requested network status was unavailable.
140    Unavailable,
141    /// Requested network status was not found.
142    NotFound,
143    /// Network status unmodified since If-Modified-Since time.
144    NotModified,
145    /// Directory server was too busy to respond.
146    Busy,
147}
148
149impl FromStr for DirResponse {
150    type Err = Error;
151
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        match s.to_lowercase().as_str() {
154            "ok" => Ok(DirResponse::Ok),
155            "not-enough-sigs" => Ok(DirResponse::NotEnoughSigs),
156            "unavailable" => Ok(DirResponse::Unavailable),
157            "not-found" => Ok(DirResponse::NotFound),
158            "not-modified" => Ok(DirResponse::NotModified),
159            "busy" => Ok(DirResponse::Busy),
160            _ => Err(Error::Parse {
161                location: "DirResponse".to_string(),
162                reason: format!("unknown dir response: {}", s),
163            }),
164        }
165    }
166}
167
168/// Download statistics for directory requests.
169///
170/// These statistics measure the performance of directory downloads,
171/// including completion rates and speed percentiles.
172///
173/// # Variants
174///
175/// | Stat | Description |
176/// |------|-------------|
177/// | `Complete` | Requests that completed successfully |
178/// | `Timeout` | Requests that didn't complete within timeout |
179/// | `Running` | Requests still in progress when measured |
180/// | `Min` | Minimum download rate (B/s) |
181/// | `Max` | Maximum download rate (B/s) |
182/// | `D1`-`D9` | Decile download rates (10th-90th percentile) |
183/// | `Q1`, `Q3` | Quartile download rates (25th, 75th percentile) |
184/// | `Md` | Median download rate |
185///
186/// # Example
187///
188/// ```rust
189/// use stem_rs::descriptor::extra_info::DirStat;
190/// use std::str::FromStr;
191///
192/// let stat = DirStat::from_str("complete").unwrap();
193/// assert_eq!(stat, DirStat::Complete);
194/// ```
195#[derive(Debug, Clone, PartialEq, Eq, Hash)]
196pub enum DirStat {
197    /// Requests that completed successfully.
198    Complete,
199    /// Requests that timed out (10 minute default).
200    Timeout,
201    /// Requests still running when measurement was taken.
202    Running,
203    /// Minimum download rate in bytes per second.
204    Min,
205    /// Maximum download rate in bytes per second.
206    Max,
207    /// 10th percentile download rate.
208    D1,
209    /// 20th percentile download rate.
210    D2,
211    /// 30th percentile download rate.
212    D3,
213    /// 40th percentile download rate.
214    D4,
215    /// 60th percentile download rate.
216    D6,
217    /// 70th percentile download rate.
218    D7,
219    /// 80th percentile download rate.
220    D8,
221    /// 90th percentile download rate.
222    D9,
223    /// First quartile (25th percentile) download rate.
224    Q1,
225    /// Third quartile (75th percentile) download rate.
226    Q3,
227    /// Median download rate.
228    Md,
229}
230
231impl FromStr for DirStat {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        match s.to_lowercase().as_str() {
236            "complete" => Ok(DirStat::Complete),
237            "timeout" => Ok(DirStat::Timeout),
238            "running" => Ok(DirStat::Running),
239            "min" => Ok(DirStat::Min),
240            "max" => Ok(DirStat::Max),
241            "d1" => Ok(DirStat::D1),
242            "d2" => Ok(DirStat::D2),
243            "d3" => Ok(DirStat::D3),
244            "d4" => Ok(DirStat::D4),
245            "d6" => Ok(DirStat::D6),
246            "d7" => Ok(DirStat::D7),
247            "d8" => Ok(DirStat::D8),
248            "d9" => Ok(DirStat::D9),
249            "q1" => Ok(DirStat::Q1),
250            "q3" => Ok(DirStat::Q3),
251            "md" => Ok(DirStat::Md),
252            _ => Err(Error::Parse {
253                location: "DirStat".to_string(),
254                reason: format!("unknown dir stat: {}", s),
255            }),
256        }
257    }
258}
259
260/// Bandwidth history data for a time period.
261///
262/// Records bytes transferred over a series of fixed-length intervals.
263/// This is used for read/write history and directory request history.
264///
265/// # Format
266///
267/// The history line format is:
268/// ```text
269/// keyword YYYY-MM-DD HH:MM:SS (INTERVAL s) VALUE,VALUE,...
270/// ```
271///
272/// # Example
273///
274/// ```rust
275/// use stem_rs::descriptor::extra_info::BandwidthHistory;
276/// use chrono::Utc;
277///
278/// let history = BandwidthHistory {
279///     end_time: Utc::now(),
280///     interval: 900,  // 15 minutes
281///     values: vec![1000000, 2000000, 3000000],
282/// };
283///
284/// assert_eq!(history.interval, 900);
285/// assert_eq!(history.values.len(), 3);
286/// ```
287#[derive(Debug, Clone, PartialEq)]
288pub struct BandwidthHistory {
289    /// End time of the most recent interval (UTC).
290    pub end_time: DateTime<Utc>,
291
292    /// Length of each interval in seconds (typically 900 = 15 minutes).
293    pub interval: u32,
294
295    /// Bytes transferred during each interval, oldest first.
296    ///
297    /// Values can be negative in some cases due to historical bugs.
298    pub values: Vec<i64>,
299}
300
301/// Pluggable transport information.
302///
303/// Describes a pluggable transport method available on a bridge.
304/// In published bridge descriptors, the address and port are typically
305/// scrubbed for privacy.
306///
307/// # Example
308///
309/// ```rust
310/// use stem_rs::descriptor::extra_info::Transport;
311///
312/// let transport = Transport {
313///     name: "obfs4".to_string(),
314///     address: Some("192.0.2.1".to_string()),
315///     port: Some(443),
316///     args: vec!["cert=...".to_string()],
317/// };
318///
319/// assert_eq!(transport.name, "obfs4");
320/// ```
321#[derive(Debug, Clone, PartialEq)]
322pub struct Transport {
323    /// Transport method name (e.g., "obfs4", "snowflake").
324    pub name: String,
325
326    /// Transport address (may be scrubbed in published descriptors).
327    pub address: Option<String>,
328
329    /// Transport port (may be scrubbed in published descriptors).
330    pub port: Option<u16>,
331
332    /// Additional transport arguments.
333    pub args: Vec<String>,
334}
335
336/// Extra-info descriptor containing relay statistics and metadata.
337///
338/// Extra-info descriptors are published alongside server descriptors and
339/// contain detailed statistics about relay operation. They are not required
340/// for Tor clients to function but provide valuable metrics for network
341/// analysis.
342///
343/// # Overview
344///
345/// The descriptor contains several categories of information:
346///
347/// - **Identity** - Nickname, fingerprint, publication time
348/// - **Bandwidth history** - Read/write traffic over time
349/// - **Directory statistics** - Request counts and download speeds
350/// - **Cell statistics** - Circuit cell processing metrics
351/// - **Exit statistics** - Traffic per destination port
352/// - **Bridge statistics** - Client connection data
353/// - **Hidden service statistics** - Onion service activity
354/// - **Cryptographic data** - Ed25519 certificates and signatures
355///
356/// # Relay vs Bridge
357///
358/// Use [`is_bridge()`](Self::is_bridge) to distinguish between relay and
359/// bridge extra-info descriptors:
360///
361/// - **Relay**: Has `router-signature` line with RSA signature
362/// - **Bridge**: Has `router-digest` line instead of signature
363///
364/// # Example
365///
366/// ```rust
367/// use stem_rs::descriptor::extra_info::ExtraInfoDescriptor;
368/// use stem_rs::descriptor::Descriptor;
369///
370/// let content = r#"extra-info MyRelay B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
371/// published 2024-01-15 12:00:00
372/// write-history 2024-01-15 12:00:00 (900 s) 1000000,2000000
373/// read-history 2024-01-15 12:00:00 (900 s) 500000,1000000
374/// "#;
375///
376/// let desc = ExtraInfoDescriptor::parse(content).unwrap();
377///
378/// // Check identity
379/// assert_eq!(desc.nickname, "MyRelay");
380/// assert_eq!(desc.fingerprint.len(), 40);
381///
382/// // Check bandwidth history
383/// if let Some(ref history) = desc.write_history {
384///     println!("Write interval: {} seconds", history.interval);
385///     println!("Values: {:?}", history.values);
386/// }
387/// ```
388///
389/// # Statistics Fields
390///
391/// ## Bandwidth History
392///
393/// - `read_history` / `write_history` - Relay traffic
394/// - `dir_read_history` / `dir_write_history` - Directory traffic
395///
396/// ## Directory Statistics
397///
398/// - `dir_v3_ips` / `dir_v3_requests` - Client counts by country
399/// - `dir_v3_responses` - Response status counts
400/// - `dir_v3_direct_dl` / `dir_v3_tunneled_dl` - Download speed stats
401///
402/// ## Exit Statistics
403///
404/// - `exit_kibibytes_written` / `exit_kibibytes_read` - Traffic per port
405/// - `exit_streams_opened` - Connection counts per port
406///
407/// ## Bridge Statistics
408///
409/// - `bridge_ips` - Client counts by country
410/// - `ip_versions` - IPv4 vs IPv6 client counts
411/// - `ip_transports` - Pluggable transport usage
412///
413/// # Thread Safety
414///
415/// `ExtraInfoDescriptor` is `Send` and `Sync`, making it safe to share
416/// across threads.
417///
418/// # See Also
419///
420/// - [`crate::descriptor::server::ServerDescriptor`] - Published alongside extra-info
421/// - [`BandwidthHistory`] - Bandwidth history data structure
422/// - [`DirResponse`] - Directory response status codes
423/// - [`DirStat`] - Directory download statistics
424#[derive(Debug, Clone, PartialEq)]
425pub struct ExtraInfoDescriptor {
426    /// The relay's nickname (1-19 alphanumeric characters).
427    pub nickname: String,
428
429    /// The relay's identity fingerprint (40 uppercase hex characters).
430    ///
431    /// This is the SHA-1 hash of the relay's identity key.
432    pub fingerprint: String,
433
434    /// When this descriptor was published (UTC).
435    pub published: DateTime<Utc>,
436
437    /// SHA-1 digest of the GeoIP database for IPv4 addresses.
438    pub geoip_db_digest: Option<String>,
439
440    /// SHA-1 digest of the GeoIP database for IPv6 addresses.
441    pub geoip6_db_digest: Option<String>,
442
443    /// Pluggable transports available on this relay (bridges only).
444    ///
445    /// Maps transport name to transport details.
446    pub transports: HashMap<String, Transport>,
447
448    /// Bytes read by the relay over time.
449    pub read_history: Option<BandwidthHistory>,
450
451    /// Bytes written by the relay over time.
452    pub write_history: Option<BandwidthHistory>,
453
454    /// Bytes read for directory requests over time.
455    pub dir_read_history: Option<BandwidthHistory>,
456
457    /// Bytes written for directory requests over time.
458    pub dir_write_history: Option<BandwidthHistory>,
459
460    /// End time for bi-directional connection statistics.
461    pub conn_bi_direct_end: Option<DateTime<Utc>>,
462
463    /// Interval for bi-directional connection statistics (seconds).
464    pub conn_bi_direct_interval: Option<u32>,
465
466    /// Connections that read/wrote less than 20 KiB.
467    pub conn_bi_direct_below: Option<u32>,
468
469    /// Connections that read at least 10x more than wrote.
470    pub conn_bi_direct_read: Option<u32>,
471
472    /// Connections that wrote at least 10x more than read.
473    pub conn_bi_direct_write: Option<u32>,
474
475    /// Connections with balanced read/write (remaining).
476    pub conn_bi_direct_both: Option<u32>,
477
478    /// End time for cell statistics collection.
479    pub cell_stats_end: Option<DateTime<Utc>>,
480
481    /// Interval for cell statistics (seconds).
482    pub cell_stats_interval: Option<u32>,
483
484    /// Mean processed cells per circuit, by decile.
485    pub cell_processed_cells: Vec<f64>,
486
487    /// Mean queued cells per circuit, by decile.
488    pub cell_queued_cells: Vec<f64>,
489
490    /// Mean time cells spent in queue (milliseconds), by decile.
491    pub cell_time_in_queue: Vec<f64>,
492
493    /// Mean number of circuits in a decile.
494    pub cell_circuits_per_decile: Option<u32>,
495
496    /// End time for directory statistics collection.
497    pub dir_stats_end: Option<DateTime<Utc>>,
498
499    /// Interval for directory statistics (seconds).
500    pub dir_stats_interval: Option<u32>,
501
502    /// V3 directory request client IPs by country code.
503    pub dir_v3_ips: HashMap<String, u32>,
504
505    /// V3 directory request counts by country code.
506    pub dir_v3_requests: HashMap<String, u32>,
507
508    /// V3 directory response status counts.
509    pub dir_v3_responses: HashMap<DirResponse, u32>,
510
511    /// Unrecognized V3 directory response statuses.
512    pub dir_v3_responses_unknown: HashMap<String, u32>,
513
514    /// V3 direct download statistics (via DirPort).
515    pub dir_v3_direct_dl: HashMap<DirStat, u32>,
516
517    /// Unrecognized V3 direct download statistics.
518    pub dir_v3_direct_dl_unknown: HashMap<String, u32>,
519
520    /// V3 tunneled download statistics (via ORPort).
521    pub dir_v3_tunneled_dl: HashMap<DirStat, u32>,
522
523    /// Unrecognized V3 tunneled download statistics.
524    pub dir_v3_tunneled_dl_unknown: HashMap<String, u32>,
525
526    /// V2 directory request client IPs by country code (deprecated).
527    pub dir_v2_ips: HashMap<String, u32>,
528
529    /// V2 directory request counts by country code (deprecated).
530    pub dir_v2_requests: HashMap<String, u32>,
531
532    /// V2 directory response status counts (deprecated).
533    pub dir_v2_responses: HashMap<DirResponse, u32>,
534
535    /// Unrecognized V2 directory response statuses (deprecated).
536    pub dir_v2_responses_unknown: HashMap<String, u32>,
537
538    /// V2 direct download statistics (deprecated).
539    pub dir_v2_direct_dl: HashMap<DirStat, u32>,
540
541    /// Unrecognized V2 direct download statistics (deprecated).
542    pub dir_v2_direct_dl_unknown: HashMap<String, u32>,
543
544    /// V2 tunneled download statistics (deprecated).
545    pub dir_v2_tunneled_dl: HashMap<DirStat, u32>,
546
547    /// Unrecognized V2 tunneled download statistics (deprecated).
548    pub dir_v2_tunneled_dl_unknown: HashMap<String, u32>,
549
550    /// End time for entry guard statistics.
551    pub entry_stats_end: Option<DateTime<Utc>>,
552
553    /// Interval for entry guard statistics (seconds).
554    pub entry_stats_interval: Option<u32>,
555
556    /// Entry guard client IPs by country code.
557    pub entry_ips: HashMap<String, u32>,
558
559    /// End time for exit statistics.
560    pub exit_stats_end: Option<DateTime<Utc>>,
561
562    /// Interval for exit statistics (seconds).
563    pub exit_stats_interval: Option<u32>,
564
565    /// Kibibytes written per destination port.
566    pub exit_kibibytes_written: HashMap<PortKey, u64>,
567
568    /// Kibibytes read per destination port.
569    pub exit_kibibytes_read: HashMap<PortKey, u64>,
570
571    /// Streams opened per destination port.
572    pub exit_streams_opened: HashMap<PortKey, u64>,
573
574    /// End time for bridge statistics.
575    pub bridge_stats_end: Option<DateTime<Utc>>,
576
577    /// Interval for bridge statistics (seconds).
578    pub bridge_stats_interval: Option<u32>,
579
580    /// Bridge client IPs by country code.
581    pub bridge_ips: HashMap<String, u32>,
582
583    /// Bridge client counts by IP version (v4, v6).
584    pub ip_versions: HashMap<String, u32>,
585
586    /// Bridge client counts by transport method.
587    pub ip_transports: HashMap<String, u32>,
588
589    /// End time for hidden service statistics.
590    pub hs_stats_end: Option<DateTime<Utc>>,
591
592    /// Rounded count of RENDEZVOUS1 cells relayed.
593    pub hs_rend_cells: Option<u64>,
594
595    /// Additional attributes for hs_rend_cells.
596    pub hs_rend_cells_attr: HashMap<String, String>,
597
598    /// Rounded count of unique onion service identities seen.
599    pub hs_dir_onions_seen: Option<u64>,
600
601    /// Additional attributes for hs_dir_onions_seen.
602    pub hs_dir_onions_seen_attr: HashMap<String, String>,
603
604    /// End time for padding count statistics.
605    pub padding_counts_end: Option<DateTime<Utc>>,
606
607    /// Interval for padding count statistics (seconds).
608    pub padding_counts_interval: Option<u32>,
609
610    /// Padding-related statistics.
611    pub padding_counts: HashMap<String, String>,
612
613    /// Ed25519 certificate (PEM-encoded).
614    pub ed25519_certificate: Option<String>,
615
616    /// Ed25519 signature of the descriptor.
617    pub ed25519_signature: Option<String>,
618
619    /// RSA signature of the descriptor (relay extra-info only).
620    pub signature: Option<String>,
621
622    /// Router digest for bridge extra-info descriptors.
623    ///
624    /// Present only in bridge descriptors; indicates this is a bridge.
625    pub router_digest: Option<String>,
626
627    /// SHA-256 router digest (base64).
628    pub router_digest_sha256: Option<String>,
629
630    /// Raw descriptor content for digest computation.
631    raw_content: Vec<u8>,
632
633    /// Lines not recognized during parsing.
634    unrecognized_lines: Vec<String>,
635}
636
637/// Key for port-based statistics in exit traffic data.
638///
639/// Exit statistics are grouped by destination port. The special "other"
640/// category aggregates traffic to ports not individually tracked.
641///
642/// # Example
643///
644/// ```rust
645/// use stem_rs::descriptor::extra_info::PortKey;
646///
647/// let http = PortKey::Port(80);
648/// let https = PortKey::Port(443);
649/// let other = PortKey::Other;
650///
651/// assert_eq!(format!("{}", http), "80");
652/// assert_eq!(format!("{}", other), "other");
653/// ```
654#[derive(Debug, Clone, PartialEq, Eq, Hash)]
655pub enum PortKey {
656    /// A specific port number.
657    Port(u16),
658
659    /// Aggregate of all other ports not individually tracked.
660    Other,
661}
662
663impl fmt::Display for PortKey {
664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665        match self {
666            PortKey::Port(p) => write!(f, "{}", p),
667            PortKey::Other => write!(f, "other"),
668        }
669    }
670}
671
672impl Default for ExtraInfoDescriptor {
673    fn default() -> Self {
674        Self {
675            nickname: String::new(),
676            fingerprint: String::new(),
677            published: DateTime::from_timestamp(0, 0).unwrap(),
678            geoip_db_digest: None,
679            geoip6_db_digest: None,
680            transports: HashMap::new(),
681            read_history: None,
682            write_history: None,
683            dir_read_history: None,
684            dir_write_history: None,
685            conn_bi_direct_end: None,
686            conn_bi_direct_interval: None,
687            conn_bi_direct_below: None,
688            conn_bi_direct_read: None,
689            conn_bi_direct_write: None,
690            conn_bi_direct_both: None,
691            cell_stats_end: None,
692            cell_stats_interval: None,
693            cell_processed_cells: Vec::new(),
694            cell_queued_cells: Vec::new(),
695            cell_time_in_queue: Vec::new(),
696            cell_circuits_per_decile: None,
697            dir_stats_end: None,
698            dir_stats_interval: None,
699            dir_v3_ips: HashMap::new(),
700            dir_v3_requests: HashMap::new(),
701            dir_v3_responses: HashMap::new(),
702            dir_v3_responses_unknown: HashMap::new(),
703            dir_v3_direct_dl: HashMap::new(),
704            dir_v3_direct_dl_unknown: HashMap::new(),
705            dir_v3_tunneled_dl: HashMap::new(),
706            dir_v3_tunneled_dl_unknown: HashMap::new(),
707            dir_v2_ips: HashMap::new(),
708            dir_v2_requests: HashMap::new(),
709            dir_v2_responses: HashMap::new(),
710            dir_v2_responses_unknown: HashMap::new(),
711            dir_v2_direct_dl: HashMap::new(),
712            dir_v2_direct_dl_unknown: HashMap::new(),
713            dir_v2_tunneled_dl: HashMap::new(),
714            dir_v2_tunneled_dl_unknown: HashMap::new(),
715            entry_stats_end: None,
716            entry_stats_interval: None,
717            entry_ips: HashMap::new(),
718            exit_stats_end: None,
719            exit_stats_interval: None,
720            exit_kibibytes_written: HashMap::new(),
721            exit_kibibytes_read: HashMap::new(),
722            exit_streams_opened: HashMap::new(),
723            bridge_stats_end: None,
724            bridge_stats_interval: None,
725            bridge_ips: HashMap::new(),
726            ip_versions: HashMap::new(),
727            ip_transports: HashMap::new(),
728            hs_stats_end: None,
729            hs_rend_cells: None,
730            hs_rend_cells_attr: HashMap::new(),
731            hs_dir_onions_seen: None,
732            hs_dir_onions_seen_attr: HashMap::new(),
733            padding_counts_end: None,
734            padding_counts_interval: None,
735            padding_counts: HashMap::new(),
736            ed25519_certificate: None,
737            ed25519_signature: None,
738            signature: None,
739            router_digest: None,
740            router_digest_sha256: None,
741            raw_content: Vec::new(),
742            unrecognized_lines: Vec::new(),
743        }
744    }
745}
746
747impl ExtraInfoDescriptor {
748    fn parse_extra_info_line(line: &str) -> Result<(String, String), Error> {
749        let parts: Vec<&str> = line.split_whitespace().collect();
750        if parts.len() < 2 {
751            return Err(Error::Parse {
752                location: "extra-info".to_string(),
753                reason: "extra-info line requires nickname and fingerprint".to_string(),
754            });
755        }
756        let nickname = parts[0].to_string();
757        let fingerprint = parts[1].to_string();
758        if fingerprint.len() != 40 || !fingerprint.chars().all(|c| c.is_ascii_hexdigit()) {
759            return Err(Error::Parse {
760                location: "extra-info".to_string(),
761                reason: format!("invalid fingerprint: {}", fingerprint),
762            });
763        }
764        Ok((nickname, fingerprint))
765    }
766
767    fn parse_published_line(line: &str) -> Result<DateTime<Utc>, Error> {
768        let datetime =
769            NaiveDateTime::parse_from_str(line.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
770                Error::Parse {
771                    location: "published".to_string(),
772                    reason: format!("invalid datetime: {} - {}", line, e),
773                }
774            })?;
775        Ok(datetime.and_utc())
776    }
777
778    fn parse_history_line(line: &str) -> Result<BandwidthHistory, Error> {
779        let timestamp_re =
780            regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
781                location: "history".to_string(),
782                reason: format!("regex error: {}", e),
783            })?;
784
785        let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
786            location: "history".to_string(),
787            reason: format!("invalid history format: {}", line),
788        })?;
789
790        let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
791        let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
792        let values_str = caps.get(3).map(|m| m.as_str().trim()).unwrap_or("");
793
794        let end_time = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
795            .map_err(|e| Error::Parse {
796                location: "history".to_string(),
797                reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
798            })?
799            .and_utc();
800
801        let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
802            location: "history".to_string(),
803            reason: format!("invalid interval: {}", interval_str),
804        })?;
805
806        let values: Vec<i64> = if values_str.is_empty() {
807            Vec::new()
808        } else {
809            values_str
810                .split(',')
811                .filter(|s| !s.is_empty())
812                .map(|s| s.trim().parse::<i64>())
813                .collect::<Result<Vec<_>, _>>()
814                .map_err(|_| Error::Parse {
815                    location: "history".to_string(),
816                    reason: format!("invalid history values: {}", values_str),
817                })?
818        };
819
820        Ok(BandwidthHistory {
821            end_time,
822            interval,
823            values,
824        })
825    }
826
827    fn parse_timestamp_and_interval(line: &str) -> Result<(DateTime<Utc>, u32, String), Error> {
828        let timestamp_re =
829            regex::Regex::new(r"^(.+?) \((\d+) s\)(.*)$").map_err(|e| Error::Parse {
830                location: "timestamp".to_string(),
831                reason: format!("regex error: {}", e),
832            })?;
833
834        let caps = timestamp_re.captures(line).ok_or_else(|| Error::Parse {
835            location: "timestamp".to_string(),
836            reason: format!("invalid timestamp format: {}", line),
837        })?;
838
839        let timestamp_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
840        let interval_str = caps.get(2).map(|m| m.as_str()).unwrap_or("0");
841        let remainder = caps
842            .get(3)
843            .map(|m| m.as_str().trim())
844            .unwrap_or("")
845            .to_string();
846
847        let timestamp = NaiveDateTime::parse_from_str(timestamp_str.trim(), "%Y-%m-%d %H:%M:%S")
848            .map_err(|e| Error::Parse {
849                location: "timestamp".to_string(),
850                reason: format!("invalid timestamp: {} - {}", timestamp_str, e),
851            })?
852            .and_utc();
853
854        let interval: u32 = interval_str.parse().map_err(|_| Error::Parse {
855            location: "timestamp".to_string(),
856            reason: format!("invalid interval: {}", interval_str),
857        })?;
858
859        Ok((timestamp, interval, remainder))
860    }
861
862    fn parse_geoip_to_count(value: &str) -> HashMap<String, u32> {
863        let mut result = HashMap::new();
864        if value.is_empty() {
865            return result;
866        }
867        for entry in value.split(',') {
868            if let Some(eq_pos) = entry.find('=') {
869                let locale = &entry[..eq_pos];
870                let count_str = &entry[eq_pos + 1..];
871                if let Ok(count) = count_str.parse::<u32>() {
872                    result.insert(locale.to_string(), count);
873                }
874            }
875        }
876        result
877    }
878
879    fn parse_dirreq_resp(value: &str) -> (HashMap<DirResponse, u32>, HashMap<String, u32>) {
880        let mut recognized = HashMap::new();
881        let mut unrecognized = HashMap::new();
882        if value.is_empty() {
883            return (recognized, unrecognized);
884        }
885        for entry in value.split(',') {
886            if let Some(eq_pos) = entry.find('=') {
887                let status = &entry[..eq_pos];
888                let count_str = &entry[eq_pos + 1..];
889                if let Ok(count) = count_str.parse::<u32>() {
890                    if let Ok(dir_resp) = DirResponse::from_str(status) {
891                        recognized.insert(dir_resp, count);
892                    } else {
893                        unrecognized.insert(status.to_string(), count);
894                    }
895                }
896            }
897        }
898        (recognized, unrecognized)
899    }
900
901    fn parse_dirreq_dl(value: &str) -> (HashMap<DirStat, u32>, HashMap<String, u32>) {
902        let mut recognized = HashMap::new();
903        let mut unrecognized = HashMap::new();
904        if value.is_empty() {
905            return (recognized, unrecognized);
906        }
907        for entry in value.split(',') {
908            if let Some(eq_pos) = entry.find('=') {
909                let stat = &entry[..eq_pos];
910                let count_str = &entry[eq_pos + 1..];
911                if let Ok(count) = count_str.parse::<u32>() {
912                    if let Ok(dir_stat) = DirStat::from_str(stat) {
913                        recognized.insert(dir_stat, count);
914                    } else {
915                        unrecognized.insert(stat.to_string(), count);
916                    }
917                }
918            }
919        }
920        (recognized, unrecognized)
921    }
922
923    fn parse_port_count(value: &str) -> HashMap<PortKey, u64> {
924        let mut result = HashMap::new();
925        if value.is_empty() {
926            return result;
927        }
928        for entry in value.split(',') {
929            if let Some(eq_pos) = entry.find('=') {
930                let port_str = &entry[..eq_pos];
931                let count_str = &entry[eq_pos + 1..];
932                if let Ok(count) = count_str.parse::<u64>() {
933                    let port_key = if port_str == "other" {
934                        PortKey::Other
935                    } else if let Ok(port) = port_str.parse::<u16>() {
936                        PortKey::Port(port)
937                    } else {
938                        continue;
939                    };
940                    result.insert(port_key, count);
941                }
942            }
943        }
944        result
945    }
946
947    fn parse_cell_values(value: &str) -> Vec<f64> {
948        if value.is_empty() {
949            return Vec::new();
950        }
951        value
952            .split(',')
953            .filter_map(|s| s.trim().parse::<f64>().ok())
954            .collect()
955    }
956
957    fn parse_conn_bi_direct(value: &str) -> ConnBiDirectResult {
958        let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
959        let stats: Vec<&str> = remainder.split(',').collect();
960        if stats.len() != 4 {
961            return Err(Error::Parse {
962                location: "conn-bi-direct".to_string(),
963                reason: format!("expected 4 values, got {}", stats.len()),
964            });
965        }
966        let below: u32 = stats[0].parse().map_err(|_| Error::Parse {
967            location: "conn-bi-direct".to_string(),
968            reason: "invalid below value".to_string(),
969        })?;
970        let read: u32 = stats[1].parse().map_err(|_| Error::Parse {
971            location: "conn-bi-direct".to_string(),
972            reason: "invalid read value".to_string(),
973        })?;
974        let write: u32 = stats[2].parse().map_err(|_| Error::Parse {
975            location: "conn-bi-direct".to_string(),
976            reason: "invalid write value".to_string(),
977        })?;
978        let both: u32 = stats[3].parse().map_err(|_| Error::Parse {
979            location: "conn-bi-direct".to_string(),
980            reason: "invalid both value".to_string(),
981        })?;
982        Ok((timestamp, interval, below, read, write, both))
983    }
984
985    fn parse_transport_line(value: &str) -> Transport {
986        let parts: Vec<&str> = value.split_whitespace().collect();
987        if parts.is_empty() {
988            return Transport {
989                name: String::new(),
990                address: None,
991                port: None,
992                args: Vec::new(),
993            };
994        }
995        let name = parts[0].to_string();
996        if parts.len() < 2 {
997            return Transport {
998                name,
999                address: None,
1000                port: None,
1001                args: Vec::new(),
1002            };
1003        }
1004        let addr_port = parts[1];
1005        let (address, port) = if let Some(colon_pos) = addr_port.rfind(':') {
1006            let addr = addr_port[..colon_pos]
1007                .trim_matches(|c| c == '[' || c == ']')
1008                .to_string();
1009            let port = addr_port[colon_pos + 1..].parse::<u16>().ok();
1010            (Some(addr), port)
1011        } else {
1012            (None, None)
1013        };
1014        let args: Vec<String> = parts.iter().skip(2).map(|s| s.to_string()).collect();
1015        Transport {
1016            name,
1017            address,
1018            port,
1019            args,
1020        }
1021    }
1022
1023    fn parse_hs_stats(value: &str) -> (Option<u64>, HashMap<String, String>) {
1024        let mut stat = None;
1025        let mut extra = HashMap::new();
1026        if value.is_empty() {
1027            return (stat, extra);
1028        }
1029        let parts: Vec<&str> = value.split_whitespace().collect();
1030        if let Some(first) = parts.first() {
1031            stat = first.parse::<u64>().ok();
1032        }
1033        for part in parts.iter().skip(1) {
1034            if let Some(eq_pos) = part.find('=') {
1035                let key = &part[..eq_pos];
1036                let val = &part[eq_pos + 1..];
1037                extra.insert(key.to_string(), val.to_string());
1038            }
1039        }
1040        (stat, extra)
1041    }
1042
1043    fn parse_padding_counts(value: &str) -> PaddingCountsResult {
1044        let (timestamp, interval, remainder) = Self::parse_timestamp_and_interval(value)?;
1045        let mut counts = HashMap::new();
1046        for part in remainder.split_whitespace() {
1047            if let Some(eq_pos) = part.find('=') {
1048                let key = &part[..eq_pos];
1049                let val = &part[eq_pos + 1..];
1050                counts.insert(key.to_string(), val.to_string());
1051            }
1052        }
1053        Ok((timestamp, interval, counts))
1054    }
1055
1056    fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1057        let mut block = String::new();
1058        let mut idx = start_idx;
1059        while idx < lines.len() {
1060            let line = lines[idx];
1061            block.push_str(line);
1062            block.push('\n');
1063            if line.starts_with("-----END ") {
1064                break;
1065            }
1066            idx += 1;
1067        }
1068        (block.trim_end().to_string(), idx)
1069    }
1070
1071    /// Finds the content to be hashed for digest computation.
1072    ///
1073    /// For relay extra-info descriptors, the digest is computed over
1074    /// the content from "extra-info " through "router-signature\n".
1075    fn find_digest_content(content: &str) -> Option<&str> {
1076        let start_marker = "extra-info ";
1077        let end_marker = "\nrouter-signature\n";
1078        let start = content.find(start_marker)?;
1079        let end = content.find(end_marker)?;
1080        Some(&content[start..end + end_marker.len()])
1081    }
1082
1083    /// Returns whether this is a bridge extra-info descriptor.
1084    ///
1085    /// Bridge descriptors have a `router-digest` line instead of a
1086    /// `router-signature` line.
1087    ///
1088    /// # Example
1089    ///
1090    /// ```rust
1091    /// use stem_rs::descriptor::extra_info::ExtraInfoDescriptor;
1092    /// use stem_rs::descriptor::Descriptor;
1093    ///
1094    /// let relay_content = r#"extra-info relay B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1095    /// published 2024-01-15 12:00:00
1096    /// "#;
1097    ///
1098    /// let bridge_content = r#"extra-info bridge B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1099    /// published 2024-01-15 12:00:00
1100    /// router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1101    /// "#;
1102    ///
1103    /// let relay = ExtraInfoDescriptor::parse(relay_content).unwrap();
1104    /// let bridge = ExtraInfoDescriptor::parse(bridge_content).unwrap();
1105    ///
1106    /// assert!(!relay.is_bridge());
1107    /// assert!(bridge.is_bridge());
1108    /// ```
1109    pub fn is_bridge(&self) -> bool {
1110        self.router_digest.is_some()
1111    }
1112}
1113
1114impl Descriptor for ExtraInfoDescriptor {
1115    fn parse(content: &str) -> Result<Self, Error> {
1116        let raw_content = content.as_bytes().to_vec();
1117        let lines: Vec<&str> = content.lines().collect();
1118        let mut desc = ExtraInfoDescriptor {
1119            raw_content,
1120            ..Default::default()
1121        };
1122
1123        let mut idx = 0;
1124        while idx < lines.len() {
1125            let line = lines[idx];
1126
1127            if line.starts_with("@type ") {
1128                idx += 1;
1129                continue;
1130            }
1131
1132            let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1133                (&line[..space_pos], line[space_pos + 1..].trim())
1134            } else {
1135                (line, "")
1136            };
1137
1138            match keyword {
1139                "extra-info" => {
1140                    let (nickname, fingerprint) = Self::parse_extra_info_line(value)?;
1141                    desc.nickname = nickname;
1142                    desc.fingerprint = fingerprint;
1143                }
1144                "published" => {
1145                    desc.published = Self::parse_published_line(value)?;
1146                }
1147                "identity-ed25519" => {
1148                    let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1149                    desc.ed25519_certificate = Some(block);
1150                    idx = end_idx;
1151                }
1152                "router-sig-ed25519" => {
1153                    desc.ed25519_signature = Some(value.to_string());
1154                }
1155                "router-signature" => {
1156                    let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
1157                    desc.signature = Some(block);
1158                    idx = end_idx;
1159                }
1160                "router-digest" => {
1161                    desc.router_digest = Some(value.to_string());
1162                }
1163                "router-digest-sha256" => {
1164                    desc.router_digest_sha256 = Some(value.to_string());
1165                }
1166                "master-key-ed25519" => {
1167                    desc.ed25519_certificate = Some(value.to_string());
1168                }
1169                "geoip-db-digest" => {
1170                    desc.geoip_db_digest = Some(value.to_string());
1171                }
1172                "geoip6-db-digest" => {
1173                    desc.geoip6_db_digest = Some(value.to_string());
1174                }
1175                "transport" => {
1176                    let transport = Self::parse_transport_line(value);
1177                    desc.transports.insert(transport.name.clone(), transport);
1178                }
1179                "read-history" => {
1180                    desc.read_history = Some(Self::parse_history_line(value)?);
1181                }
1182                "write-history" => {
1183                    desc.write_history = Some(Self::parse_history_line(value)?);
1184                }
1185                "dirreq-read-history" => {
1186                    desc.dir_read_history = Some(Self::parse_history_line(value)?);
1187                }
1188                "dirreq-write-history" => {
1189                    desc.dir_write_history = Some(Self::parse_history_line(value)?);
1190                }
1191                "conn-bi-direct" => {
1192                    let (ts, interval, below, read, write, both) =
1193                        Self::parse_conn_bi_direct(value)?;
1194                    desc.conn_bi_direct_end = Some(ts);
1195                    desc.conn_bi_direct_interval = Some(interval);
1196                    desc.conn_bi_direct_below = Some(below);
1197                    desc.conn_bi_direct_read = Some(read);
1198                    desc.conn_bi_direct_write = Some(write);
1199                    desc.conn_bi_direct_both = Some(both);
1200                }
1201                "cell-stats-end" => {
1202                    let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1203                    desc.cell_stats_end = Some(ts);
1204                    desc.cell_stats_interval = Some(interval);
1205                }
1206                "cell-processed-cells" => {
1207                    desc.cell_processed_cells = Self::parse_cell_values(value);
1208                }
1209                "cell-queued-cells" => {
1210                    desc.cell_queued_cells = Self::parse_cell_values(value);
1211                }
1212                "cell-time-in-queue" => {
1213                    desc.cell_time_in_queue = Self::parse_cell_values(value);
1214                }
1215                "cell-circuits-per-decile" => {
1216                    desc.cell_circuits_per_decile = value.parse().ok();
1217                }
1218                "dirreq-stats-end" => {
1219                    let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1220                    desc.dir_stats_end = Some(ts);
1221                    desc.dir_stats_interval = Some(interval);
1222                }
1223                "dirreq-v3-ips" => {
1224                    desc.dir_v3_ips = Self::parse_geoip_to_count(value);
1225                }
1226                "dirreq-v3-reqs" => {
1227                    desc.dir_v3_requests = Self::parse_geoip_to_count(value);
1228                }
1229                "dirreq-v3-resp" => {
1230                    let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1231                    desc.dir_v3_responses = recognized;
1232                    desc.dir_v3_responses_unknown = unrecognized;
1233                }
1234                "dirreq-v3-direct-dl" => {
1235                    let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1236                    desc.dir_v3_direct_dl = recognized;
1237                    desc.dir_v3_direct_dl_unknown = unrecognized;
1238                }
1239                "dirreq-v3-tunneled-dl" => {
1240                    let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1241                    desc.dir_v3_tunneled_dl = recognized;
1242                    desc.dir_v3_tunneled_dl_unknown = unrecognized;
1243                }
1244                "dirreq-v2-ips" => {
1245                    desc.dir_v2_ips = Self::parse_geoip_to_count(value);
1246                }
1247                "dirreq-v2-reqs" => {
1248                    desc.dir_v2_requests = Self::parse_geoip_to_count(value);
1249                }
1250                "dirreq-v2-resp" => {
1251                    let (recognized, unrecognized) = Self::parse_dirreq_resp(value);
1252                    desc.dir_v2_responses = recognized;
1253                    desc.dir_v2_responses_unknown = unrecognized;
1254                }
1255                "dirreq-v2-direct-dl" => {
1256                    let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1257                    desc.dir_v2_direct_dl = recognized;
1258                    desc.dir_v2_direct_dl_unknown = unrecognized;
1259                }
1260                "dirreq-v2-tunneled-dl" => {
1261                    let (recognized, unrecognized) = Self::parse_dirreq_dl(value);
1262                    desc.dir_v2_tunneled_dl = recognized;
1263                    desc.dir_v2_tunneled_dl_unknown = unrecognized;
1264                }
1265                "entry-stats-end" => {
1266                    let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1267                    desc.entry_stats_end = Some(ts);
1268                    desc.entry_stats_interval = Some(interval);
1269                }
1270                "entry-ips" => {
1271                    desc.entry_ips = Self::parse_geoip_to_count(value);
1272                }
1273                "exit-stats-end" => {
1274                    let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1275                    desc.exit_stats_end = Some(ts);
1276                    desc.exit_stats_interval = Some(interval);
1277                }
1278                "exit-kibibytes-written" => {
1279                    desc.exit_kibibytes_written = Self::parse_port_count(value);
1280                }
1281                "exit-kibibytes-read" => {
1282                    desc.exit_kibibytes_read = Self::parse_port_count(value);
1283                }
1284                "exit-streams-opened" => {
1285                    desc.exit_streams_opened = Self::parse_port_count(value);
1286                }
1287                "bridge-stats-end" => {
1288                    let (ts, interval, _) = Self::parse_timestamp_and_interval(value)?;
1289                    desc.bridge_stats_end = Some(ts);
1290                    desc.bridge_stats_interval = Some(interval);
1291                }
1292                "bridge-ips" => {
1293                    desc.bridge_ips = Self::parse_geoip_to_count(value);
1294                }
1295                "bridge-ip-versions" => {
1296                    desc.ip_versions = Self::parse_geoip_to_count(value);
1297                }
1298                "bridge-ip-transports" => {
1299                    desc.ip_transports = Self::parse_geoip_to_count(value);
1300                }
1301                "hidserv-stats-end" => {
1302                    desc.hs_stats_end = Some(Self::parse_published_line(value)?);
1303                }
1304                "hidserv-rend-relayed-cells" => {
1305                    let (stat, attr) = Self::parse_hs_stats(value);
1306                    desc.hs_rend_cells = stat;
1307                    desc.hs_rend_cells_attr = attr;
1308                }
1309                "hidserv-dir-onions-seen" => {
1310                    let (stat, attr) = Self::parse_hs_stats(value);
1311                    desc.hs_dir_onions_seen = stat;
1312                    desc.hs_dir_onions_seen_attr = attr;
1313                }
1314                "padding-counts" => {
1315                    let (ts, interval, counts) = Self::parse_padding_counts(value)?;
1316                    desc.padding_counts_end = Some(ts);
1317                    desc.padding_counts_interval = Some(interval);
1318                    desc.padding_counts = counts;
1319                }
1320                _ => {
1321                    if !line.is_empty() && !line.starts_with("-----") {
1322                        desc.unrecognized_lines.push(line.to_string());
1323                    }
1324                }
1325            }
1326            idx += 1;
1327        }
1328
1329        if desc.nickname.is_empty() {
1330            return Err(Error::Parse {
1331                location: "extra-info".to_string(),
1332                reason: "missing extra-info line".to_string(),
1333            });
1334        }
1335
1336        Ok(desc)
1337    }
1338
1339    fn to_descriptor_string(&self) -> String {
1340        let mut result = String::new();
1341
1342        result.push_str(&format!(
1343            "extra-info {} {}\n",
1344            self.nickname, self.fingerprint
1345        ));
1346        result.push_str(&format!(
1347            "published {}\n",
1348            self.published.format("%Y-%m-%d %H:%M:%S")
1349        ));
1350
1351        if let Some(ref history) = self.write_history {
1352            let values: String = history
1353                .values
1354                .iter()
1355                .map(|v| v.to_string())
1356                .collect::<Vec<_>>()
1357                .join(",");
1358            result.push_str(&format!(
1359                "write-history {} ({} s) {}\n",
1360                history.end_time.format("%Y-%m-%d %H:%M:%S"),
1361                history.interval,
1362                values
1363            ));
1364        }
1365
1366        if let Some(ref history) = self.read_history {
1367            let values: String = history
1368                .values
1369                .iter()
1370                .map(|v| v.to_string())
1371                .collect::<Vec<_>>()
1372                .join(",");
1373            result.push_str(&format!(
1374                "read-history {} ({} s) {}\n",
1375                history.end_time.format("%Y-%m-%d %H:%M:%S"),
1376                history.interval,
1377                values
1378            ));
1379        }
1380
1381        if let Some(ref history) = self.dir_write_history {
1382            let values: String = history
1383                .values
1384                .iter()
1385                .map(|v| v.to_string())
1386                .collect::<Vec<_>>()
1387                .join(",");
1388            result.push_str(&format!(
1389                "dirreq-write-history {} ({} s) {}\n",
1390                history.end_time.format("%Y-%m-%d %H:%M:%S"),
1391                history.interval,
1392                values
1393            ));
1394        }
1395
1396        if let Some(ref history) = self.dir_read_history {
1397            let values: String = history
1398                .values
1399                .iter()
1400                .map(|v| v.to_string())
1401                .collect::<Vec<_>>()
1402                .join(",");
1403            result.push_str(&format!(
1404                "dirreq-read-history {} ({} s) {}\n",
1405                history.end_time.format("%Y-%m-%d %H:%M:%S"),
1406                history.interval,
1407                values
1408            ));
1409        }
1410
1411        if let Some(ref digest) = self.geoip_db_digest {
1412            result.push_str(&format!("geoip-db-digest {}\n", digest));
1413        }
1414
1415        if let Some(ref digest) = self.geoip6_db_digest {
1416            result.push_str(&format!("geoip6-db-digest {}\n", digest));
1417        }
1418
1419        if let Some(ref sig) = self.signature {
1420            result.push_str("router-signature\n");
1421            result.push_str(sig);
1422            result.push('\n');
1423        }
1424
1425        if let Some(ref digest) = self.router_digest {
1426            result.push_str(&format!("router-digest {}\n", digest));
1427        }
1428
1429        result
1430    }
1431
1432    fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
1433        if self.is_bridge() {
1434            match (hash, encoding) {
1435                (DigestHash::Sha1, DigestEncoding::Hex) => {
1436                    self.router_digest.clone().ok_or_else(|| Error::Parse {
1437                        location: "digest".to_string(),
1438                        reason: "bridge descriptor missing router-digest".to_string(),
1439                    })
1440                }
1441                (DigestHash::Sha256, DigestEncoding::Base64) => self
1442                    .router_digest_sha256
1443                    .clone()
1444                    .ok_or_else(|| Error::Parse {
1445                        location: "digest".to_string(),
1446                        reason: "bridge descriptor missing router-digest-sha256".to_string(),
1447                    }),
1448                _ => Err(Error::Parse {
1449                    location: "digest".to_string(),
1450                    reason: "bridge extrainfo digests only available as sha1/hex or sha256/base64"
1451                        .to_string(),
1452                }),
1453            }
1454        } else {
1455            let content_str = std::str::from_utf8(&self.raw_content).map_err(|_| Error::Parse {
1456                location: "digest".to_string(),
1457                reason: "invalid UTF-8 in raw content".to_string(),
1458            })?;
1459
1460            match hash {
1461                DigestHash::Sha1 => {
1462                    let digest_content =
1463                        Self::find_digest_content(content_str).ok_or_else(|| Error::Parse {
1464                            location: "digest".to_string(),
1465                            reason: "could not find digest content boundaries".to_string(),
1466                        })?;
1467                    Ok(compute_digest(digest_content.as_bytes(), hash, encoding))
1468                }
1469                DigestHash::Sha256 => Ok(compute_digest(&self.raw_content, hash, encoding)),
1470            }
1471        }
1472    }
1473
1474    fn raw_content(&self) -> &[u8] {
1475        &self.raw_content
1476    }
1477
1478    fn unrecognized_lines(&self) -> &[String] {
1479        &self.unrecognized_lines
1480    }
1481}
1482
1483impl FromStr for ExtraInfoDescriptor {
1484    type Err = Error;
1485
1486    fn from_str(s: &str) -> Result<Self, Self::Err> {
1487        Self::parse(s)
1488    }
1489}
1490
1491impl fmt::Display for ExtraInfoDescriptor {
1492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1493        write!(f, "{}", self.to_descriptor_string())
1494    }
1495}
1496
1497#[cfg(test)]
1498mod tests {
1499    use super::*;
1500
1501    const RELAY_EXTRA_INFO: &str = r#"@type extra-info 1.0
1502extra-info NINJA B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1503published 2012-05-05 17:03:50
1504write-history 2012-05-05 17:02:45 (900 s) 1082368,19456,50176,272384,485376,1850368,1132544,1790976,2459648,4091904,6310912,13701120,3209216,3871744,7873536,5440512,7287808,10561536,9979904,11247616,11982848,7590912,10611712,20728832,38534144,6839296,3173376,16678912
1505read-history 2012-05-05 17:02:45 (900 s) 3309568,9216,41984,27648,123904,2004992,364544,576512,1607680,3808256,4672512,12783616,2938880,2562048,7348224,3574784,6488064,10954752,9359360,4438016,6286336,6438912,4502528,10720256,38165504,1524736,2336768,8186880
1506dirreq-write-history 2012-05-05 17:02:45 (900 s) 0,0,0,227328,349184,382976,738304,1171456,850944,657408,1675264,987136,702464,1335296,587776,1941504,893952,533504,695296,6828032,6326272,1287168,6310912,10085376,1048576,5372928,894976,8610816
1507dirreq-read-history 2012-05-05 17:02:45 (900 s) 0,0,0,0,33792,27648,48128,46080,60416,51200,63488,64512,45056,27648,37888,48128,57344,34816,46080,50176,37888,51200,25600,33792,39936,32768,28672,30720
1508router-signature
1509-----BEGIN SIGNATURE-----
1510K5FSywk7qvw/boA4DQcqkls6Ize5vcBYfhQ8JnOeRQC9+uDxbnpm3qaYN9jZ8myj
1511k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
15127LZqklu+gVvhMKREpchVqlAwXkWR44VENm24Hs+mT3M=
1513-----END SIGNATURE-----
1514"#;
1515
1516    const BRIDGE_EXTRA_INFO: &str = r#"@type bridge-extra-info 1.0
1517extra-info ec2bridgereaac65a3 1EC248422B57D9C0BD751892FE787585407479A4
1518published 2012-06-08 02:21:27
1519write-history 2012-06-08 02:10:38 (900 s) 343040,991232,5649408
1520read-history 2012-06-08 02:10:38 (900 s) 337920,437248,3995648
1521geoip-db-digest A27BE984989AB31C50D0861C7106B17A7EEC3756
1522dirreq-stats-end 2012-06-07 06:33:46 (86400 s)
1523dirreq-v3-ips 
1524dirreq-v3-reqs 
1525dirreq-v3-resp ok=72,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1526dirreq-v3-direct-dl complete=0,timeout=0,running=0
1527dirreq-v3-tunneled-dl complete=68,timeout=4,running=0,min=2626,d1=7795,d2=14369,q1=18695,d3=29117,d4=52562,md=70626,d6=102271,d7=164175,q3=181522,d8=271682,d9=563791,max=32136142
1528bridge-stats-end 2012-06-07 06:33:53 (86400 s)
1529bridge-ips cn=16,ir=16,sy=16,us=16
1530router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1531"#;
1532
1533    const ED25519_EXTRA_INFO: &str = r#"@type extra-info 1.0
1534extra-info silverfoxden 4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E
1535identity-ed25519
1536-----BEGIN ED25519 CERT-----
1537AQQABhz0AQFcf5tGWLvPvr1sktoezBB95j6tAWSECa3Eo2ZuBtRNAQAgBABFAwSN
1538GcRlGIte4I1giLvQSTcXefT93rvx2PZ8wEDewxWdy6tzcLouPfE3Beu/eUyg8ntt
1539YuVlzi50WXzGlGnPmeounGLo0EDHTGzcLucFWpe0g/0ia6UDqgQiAySMBwI=
1540-----END ED25519 CERT-----
1541published 2015-08-22 19:21:12
1542write-history 2015-08-22 19:20:44 (14400 s) 14409728,23076864,7756800,6234112,7446528,12290048
1543read-history 2015-08-22 19:20:44 (14400 s) 20449280,23888896,9099264,7185408,8880128,13230080
1544geoip-db-digest 6882B8663F74C23E26E3C2274C24CAB2E82D67A2
1545geoip6-db-digest F063BD5247EB9829E6B9E586393D7036656DAF44
1546dirreq-stats-end 2015-08-22 11:58:30 (86400 s)
1547dirreq-v3-ips 
1548dirreq-v3-reqs 
1549dirreq-v3-resp ok=0,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
1550dirreq-v3-direct-dl complete=0,timeout=0,running=0
1551dirreq-v3-tunneled-dl complete=0,timeout=0,running=0
1552router-sig-ed25519 g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw
1553router-signature
1554-----BEGIN SIGNATURE-----
1555R7kNaIWZrg3n3FWFBRMlEK2cbnha7gUIs8ToksLe+SF0dgoZiLyV3GKrnzdE/K6D
1556qdiOMN7eK04MOZVlgxkA5ayi61FTYVveK1HrDbJ+sEUwsviVGdif6kk/9DXOiyIJ
15577wP/tofgHj/aCbFZb1PGU0zrEVLa72hVJ6cCW8w/t1s=
1558-----END SIGNATURE-----
1559"#;
1560
1561    #[test]
1562    fn test_parse_relay_extra_info() {
1563        let desc = ExtraInfoDescriptor::parse(RELAY_EXTRA_INFO).unwrap();
1564
1565        assert_eq!(desc.nickname, "NINJA");
1566        assert_eq!(desc.fingerprint, "B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48");
1567        assert_eq!(
1568            desc.published.format("%Y-%m-%d %H:%M:%S").to_string(),
1569            "2012-05-05 17:03:50"
1570        );
1571        assert!(!desc.is_bridge());
1572
1573        let write_history = desc.write_history.as_ref().unwrap();
1574        assert_eq!(write_history.interval, 900);
1575        assert_eq!(write_history.values.len(), 28);
1576        assert_eq!(write_history.values[0], 1082368);
1577
1578        let read_history = desc.read_history.as_ref().unwrap();
1579        assert_eq!(read_history.interval, 900);
1580        assert_eq!(read_history.values.len(), 28);
1581        assert_eq!(read_history.values[0], 3309568);
1582
1583        assert!(desc.signature.is_some());
1584    }
1585
1586    #[test]
1587    fn test_parse_bridge_extra_info() {
1588        let desc = ExtraInfoDescriptor::parse(BRIDGE_EXTRA_INFO).unwrap();
1589
1590        assert_eq!(desc.nickname, "ec2bridgereaac65a3");
1591        assert_eq!(desc.fingerprint, "1EC248422B57D9C0BD751892FE787585407479A4");
1592        assert!(desc.is_bridge());
1593        assert_eq!(
1594            desc.router_digest,
1595            Some("00A2AECCEAD3FEE033CFE29893387143146728EC".to_string())
1596        );
1597
1598        assert_eq!(
1599            desc.geoip_db_digest,
1600            Some("A27BE984989AB31C50D0861C7106B17A7EEC3756".to_string())
1601        );
1602
1603        assert_eq!(desc.dir_stats_interval, Some(86400));
1604        assert_eq!(desc.dir_v3_responses.get(&DirResponse::Ok), Some(&72));
1605        assert_eq!(
1606            desc.dir_v3_responses.get(&DirResponse::NotEnoughSigs),
1607            Some(&0)
1608        );
1609
1610        assert_eq!(desc.dir_v3_direct_dl.get(&DirStat::Complete), Some(&0));
1611        assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Complete), Some(&68));
1612        assert_eq!(desc.dir_v3_tunneled_dl.get(&DirStat::Timeout), Some(&4));
1613
1614        assert_eq!(desc.bridge_stats_interval, Some(86400));
1615        assert_eq!(desc.bridge_ips.get("cn"), Some(&16));
1616        assert_eq!(desc.bridge_ips.get("us"), Some(&16));
1617    }
1618
1619    #[test]
1620    fn test_parse_ed25519_extra_info() {
1621        let desc = ExtraInfoDescriptor::parse(ED25519_EXTRA_INFO).unwrap();
1622
1623        assert_eq!(desc.nickname, "silverfoxden");
1624        assert_eq!(desc.fingerprint, "4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E");
1625        assert!(!desc.is_bridge());
1626
1627        assert!(desc.ed25519_certificate.is_some());
1628        assert!(desc
1629            .ed25519_certificate
1630            .as_ref()
1631            .unwrap()
1632            .contains("ED25519 CERT"));
1633
1634        assert!(desc.ed25519_signature.is_some());
1635        assert!(desc
1636            .ed25519_signature
1637            .as_ref()
1638            .unwrap()
1639            .starts_with("g6Zg7Er8K7C1"));
1640
1641        assert_eq!(
1642            desc.geoip_db_digest,
1643            Some("6882B8663F74C23E26E3C2274C24CAB2E82D67A2".to_string())
1644        );
1645        assert_eq!(
1646            desc.geoip6_db_digest,
1647            Some("F063BD5247EB9829E6B9E586393D7036656DAF44".to_string())
1648        );
1649
1650        let write_history = desc.write_history.as_ref().unwrap();
1651        assert_eq!(write_history.interval, 14400);
1652        assert_eq!(write_history.values.len(), 6);
1653    }
1654
1655    #[test]
1656    fn test_dir_response_parsing() {
1657        assert_eq!(DirResponse::from_str("ok").unwrap(), DirResponse::Ok);
1658        assert_eq!(
1659            DirResponse::from_str("not-enough-sigs").unwrap(),
1660            DirResponse::NotEnoughSigs
1661        );
1662        assert_eq!(
1663            DirResponse::from_str("unavailable").unwrap(),
1664            DirResponse::Unavailable
1665        );
1666        assert_eq!(
1667            DirResponse::from_str("not-found").unwrap(),
1668            DirResponse::NotFound
1669        );
1670        assert_eq!(
1671            DirResponse::from_str("not-modified").unwrap(),
1672            DirResponse::NotModified
1673        );
1674        assert_eq!(DirResponse::from_str("busy").unwrap(), DirResponse::Busy);
1675    }
1676
1677    #[test]
1678    fn test_dir_stat_parsing() {
1679        assert_eq!(DirStat::from_str("complete").unwrap(), DirStat::Complete);
1680        assert_eq!(DirStat::from_str("timeout").unwrap(), DirStat::Timeout);
1681        assert_eq!(DirStat::from_str("running").unwrap(), DirStat::Running);
1682        assert_eq!(DirStat::from_str("min").unwrap(), DirStat::Min);
1683        assert_eq!(DirStat::from_str("max").unwrap(), DirStat::Max);
1684        assert_eq!(DirStat::from_str("d1").unwrap(), DirStat::D1);
1685        assert_eq!(DirStat::from_str("q1").unwrap(), DirStat::Q1);
1686        assert_eq!(DirStat::from_str("md").unwrap(), DirStat::Md);
1687    }
1688
1689    #[test]
1690    fn test_history_parsing() {
1691        let history = ExtraInfoDescriptor::parse_history_line(
1692            "2012-05-05 17:02:45 (900 s) 1082368,19456,50176",
1693        )
1694        .unwrap();
1695
1696        assert_eq!(history.interval, 900);
1697        assert_eq!(history.values, vec![1082368, 19456, 50176]);
1698    }
1699
1700    #[test]
1701    fn test_geoip_to_count_parsing() {
1702        let result = ExtraInfoDescriptor::parse_geoip_to_count("cn=16,ir=16,us=8");
1703        assert_eq!(result.get("cn"), Some(&16));
1704        assert_eq!(result.get("ir"), Some(&16));
1705        assert_eq!(result.get("us"), Some(&8));
1706    }
1707
1708    #[test]
1709    fn test_port_count_parsing() {
1710        let result = ExtraInfoDescriptor::parse_port_count("80=1000,443=2000,other=500");
1711        assert_eq!(result.get(&PortKey::Port(80)), Some(&1000));
1712        assert_eq!(result.get(&PortKey::Port(443)), Some(&2000));
1713        assert_eq!(result.get(&PortKey::Other), Some(&500));
1714    }
1715
1716    #[test]
1717    fn test_missing_extra_info_line() {
1718        let content = "published 2012-05-05 17:03:50\n";
1719        let result = ExtraInfoDescriptor::parse(content);
1720        assert!(result.is_err());
1721    }
1722
1723    #[test]
1724    fn test_invalid_fingerprint() {
1725        let content = "extra-info NINJA INVALID\npublished 2012-05-05 17:03:50\n";
1726        let result = ExtraInfoDescriptor::parse(content);
1727        assert!(result.is_err());
1728    }
1729
1730    #[test]
1731    fn test_conn_bi_direct() {
1732        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1733published 2012-05-05 17:03:50
1734conn-bi-direct 2012-05-03 12:07:50 (500 s) 277431,12089,0,2134
1735"#;
1736        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1737        assert!(desc.conn_bi_direct_end.is_some());
1738        assert_eq!(desc.conn_bi_direct_interval, Some(500));
1739        assert_eq!(desc.conn_bi_direct_below, Some(277431));
1740        assert_eq!(desc.conn_bi_direct_read, Some(12089));
1741        assert_eq!(desc.conn_bi_direct_write, Some(0));
1742        assert_eq!(desc.conn_bi_direct_both, Some(2134));
1743    }
1744
1745    #[test]
1746    fn test_cell_circuits_per_decile() {
1747        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1748published 2012-05-05 17:03:50
1749cell-circuits-per-decile 25
1750"#;
1751        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1752        assert_eq!(desc.cell_circuits_per_decile, Some(25));
1753    }
1754
1755    #[test]
1756    fn test_hidden_service_stats() {
1757        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1758published 2012-05-05 17:03:50
1759hidserv-stats-end 2012-05-03 12:07:50
1760hidserv-rend-relayed-cells 345 spiffy=true snowmen=neat
1761hidserv-dir-onions-seen 123
1762"#;
1763        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1764        assert!(desc.hs_stats_end.is_some());
1765        assert_eq!(desc.hs_rend_cells, Some(345));
1766        assert_eq!(
1767            desc.hs_rend_cells_attr.get("spiffy"),
1768            Some(&"true".to_string())
1769        );
1770        assert_eq!(
1771            desc.hs_rend_cells_attr.get("snowmen"),
1772            Some(&"neat".to_string())
1773        );
1774        assert_eq!(desc.hs_dir_onions_seen, Some(123));
1775    }
1776
1777    #[test]
1778    fn test_padding_counts() {
1779        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1780published 2012-05-05 17:03:50
1781padding-counts 2017-05-17 11:02:58 (86400 s) bin-size=10000 write-drop=0 write-pad=10000
1782"#;
1783        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1784        assert!(desc.padding_counts_end.is_some());
1785        assert_eq!(desc.padding_counts_interval, Some(86400));
1786        assert_eq!(
1787            desc.padding_counts.get("bin-size"),
1788            Some(&"10000".to_string())
1789        );
1790        assert_eq!(
1791            desc.padding_counts.get("write-drop"),
1792            Some(&"0".to_string())
1793        );
1794        assert_eq!(
1795            desc.padding_counts.get("write-pad"),
1796            Some(&"10000".to_string())
1797        );
1798    }
1799
1800    #[test]
1801    fn test_transport_line() {
1802        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1803published 2012-05-05 17:03:50
1804transport obfs2 83.212.96.201:33570
1805"#;
1806        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1807        assert!(desc.transports.contains_key("obfs2"));
1808        let transport = desc.transports.get("obfs2").unwrap();
1809        assert_eq!(transport.address, Some("83.212.96.201".to_string()));
1810        assert_eq!(transport.port, Some(33570));
1811    }
1812
1813    #[test]
1814    fn test_bridge_ip_versions() {
1815        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1816published 2012-05-05 17:03:50
1817bridge-ip-versions v4=16,v6=40
1818router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1819"#;
1820        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1821        assert_eq!(desc.ip_versions.get("v4"), Some(&16));
1822        assert_eq!(desc.ip_versions.get("v6"), Some(&40));
1823    }
1824
1825    #[test]
1826    fn test_bridge_ip_transports() {
1827        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1828published 2012-05-05 17:03:50
1829bridge-ip-transports <OR>=16,<??>=40
1830router-digest 00A2AECCEAD3FEE033CFE29893387143146728EC
1831"#;
1832        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1833        assert_eq!(desc.ip_transports.get("<OR>"), Some(&16));
1834        assert_eq!(desc.ip_transports.get("<??>"), Some(&40));
1835    }
1836
1837    #[test]
1838    fn test_exit_stats() {
1839        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1840published 2012-05-05 17:03:50
1841exit-stats-end 2012-05-03 12:07:50 (86400 s)
1842exit-kibibytes-written 80=115533759,443=1777,other=500
1843exit-kibibytes-read 80=100,443=200
1844exit-streams-opened 80=50,443=100
1845"#;
1846        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1847        assert!(desc.exit_stats_end.is_some());
1848        assert_eq!(desc.exit_stats_interval, Some(86400));
1849        assert_eq!(
1850            desc.exit_kibibytes_written.get(&PortKey::Port(80)),
1851            Some(&115533759)
1852        );
1853        assert_eq!(
1854            desc.exit_kibibytes_written.get(&PortKey::Port(443)),
1855            Some(&1777)
1856        );
1857        assert_eq!(desc.exit_kibibytes_written.get(&PortKey::Other), Some(&500));
1858        assert_eq!(desc.exit_kibibytes_read.get(&PortKey::Port(80)), Some(&100));
1859        assert_eq!(desc.exit_streams_opened.get(&PortKey::Port(80)), Some(&50));
1860    }
1861
1862    #[test]
1863    fn test_entry_stats() {
1864        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1865published 2012-05-05 17:03:50
1866entry-stats-end 2012-05-03 12:07:50 (86400 s)
1867entry-ips uk=5,de=3,jp=2
1868"#;
1869        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1870        assert!(desc.entry_stats_end.is_some());
1871        assert_eq!(desc.entry_stats_interval, Some(86400));
1872        assert_eq!(desc.entry_ips.get("uk"), Some(&5));
1873        assert_eq!(desc.entry_ips.get("de"), Some(&3));
1874        assert_eq!(desc.entry_ips.get("jp"), Some(&2));
1875    }
1876
1877    #[test]
1878    fn test_cell_stats() {
1879        let content = r#"extra-info test B2289C3EAB83ECD6EB916A2F481A02E6B76A0A48
1880published 2012-05-05 17:03:50
1881cell-stats-end 2012-05-03 12:07:50 (86400 s)
1882cell-processed-cells 2.3,-4.6,8.9
1883cell-queued-cells 1.0,2.0,3.0
1884cell-time-in-queue 10.5,20.5,30.5
1885"#;
1886        let desc = ExtraInfoDescriptor::parse(content).unwrap();
1887        assert!(desc.cell_stats_end.is_some());
1888        assert_eq!(desc.cell_stats_interval, Some(86400));
1889        assert_eq!(desc.cell_processed_cells, vec![2.3, -4.6, 8.9]);
1890        assert_eq!(desc.cell_queued_cells, vec![1.0, 2.0, 3.0]);
1891        assert_eq!(desc.cell_time_in_queue, vec![10.5, 20.5, 30.5]);
1892    }
1893}