stem_rs/descriptor/
router_status.rs

1//! Router status entry parsing for Tor network status documents.
2//!
3//! This module provides types for parsing router status entries, which describe
4//! individual relays within a network status consensus document. Router status
5//! entries are the core building blocks of consensus documents, containing
6//! essential information about each relay in the Tor network.
7//!
8//! # Overview
9//!
10//! Router status entries appear in several contexts:
11//!
12//! - **Network status consensus documents** - The authoritative list of relays
13//! - **Control port responses** - Via `GETINFO ns/*` and `GETINFO md/*` queries
14//! - **Cached consensus files** - Local copies of network status documents
15//!
16//! Each entry contains information about a single relay including its identity,
17//! network address, capabilities (flags), bandwidth, and exit policy summary.
18//!
19//! # Entry Types
20//!
21//! Different versions and flavors of network status documents use different
22//! entry formats:
23//!
24//! | Type | Description | Digest Field |
25//! |------|-------------|--------------|
26//! | [`RouterStatusEntryType::V2`] | Legacy v2 network status | SHA-1 hex digest |
27//! | [`RouterStatusEntryType::V3`] | Standard v3 consensus | SHA-1 hex digest |
28//! | [`RouterStatusEntryType::MicroV3`] | Microdescriptor consensus | Base64 microdesc digest |
29//! | [`RouterStatusEntryType::Bridge`] | Bridge network status | SHA-1 hex digest |
30//!
31//! # Entry Format
32//!
33//! Router status entries consist of several lines, each starting with a keyword:
34//!
35//! - `r` - Router identity (nickname, fingerprint, address, ports, publication time)
36//! - `a` - Additional OR addresses (IPv6 addresses)
37//! - `s` - Flags assigned by directory authorities
38//! - `v` - Tor version string
39//! - `w` - Bandwidth weights for path selection
40//! - `p` - Exit policy summary (accept/reject port ranges)
41//! - `pr` - Protocol versions supported
42//! - `m` - Microdescriptor digest (for microdescriptor consensus)
43//! - `id` - Ed25519 identity key (in votes)
44//!
45//! # Example
46//!
47//! ```rust
48//! use stem_rs::descriptor::router_status::RouterStatusEntry;
49//!
50//! let entry_content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww oQZFLYe9e4A7bOkWKR7TaNxb0JE 2024-01-15 12:00:00 192.0.2.1 9001 0
51//! s Fast Guard Running Stable Valid
52//! v Tor 0.4.8.10
53//! w Bandwidth=5000
54//! p reject 1-65535"#;
55//!
56//! let entry = RouterStatusEntry::parse(entry_content).unwrap();
57//! assert_eq!(entry.nickname, "example");
58//! assert!(entry.flags.contains(&"Guard".to_string()));
59//! ```
60//!
61//! # Flags
62//!
63//! Directory authorities assign flags to relays based on their behavior and
64//! capabilities. Common flags include:
65//!
66//! - `Authority` - A directory authority
67//! - `BadExit` - Believed to be useless as an exit node
68//! - `Exit` - Suitable for exit traffic
69//! - `Fast` - Suitable for high-bandwidth circuits
70//! - `Guard` - Suitable as an entry guard
71//! - `HSDir` - Hidden service directory
72//! - `Running` - Currently usable
73//! - `Stable` - Suitable for long-lived circuits
74//! - `Valid` - Has been validated
75//! - `V2Dir` - Supports v2 directory protocol
76//!
77//! # Bandwidth Weights
78//!
79//! The `w` line contains bandwidth information used for path selection:
80//!
81//! - `Bandwidth` - Consensus bandwidth weight (arbitrary units, typically KB/s)
82//! - `Measured` - Bandwidth measured by bandwidth authorities
83//! - `Unmeasured` - Set to 1 if bandwidth is not based on measurements
84//!
85//! # See Also
86//!
87//! - [`crate::descriptor::consensus`] - Network status consensus documents
88//! - [`crate::descriptor::micro`] - Microdescriptor parsing
89//! - [`crate::descriptor::server`] - Full server descriptors
90//! - [`crate::exit_policy`] - Exit policy evaluation
91//!
92//! # See Also
93//!
94//! - [Tor Directory Protocol Specification](https://spec.torproject.org/dir-spec)
95//! - Python Stem's `RouterStatusEntry` class
96
97use std::collections::HashMap;
98use std::fmt;
99use std::net::IpAddr;
100use std::str::FromStr;
101
102use chrono::{DateTime, NaiveDateTime, Utc};
103
104use crate::exit_policy::MicroExitPolicy;
105use crate::version::Version;
106use crate::Error;
107
108/// The type of router status entry, determining its format and available fields.
109///
110/// Different versions and flavors of network status documents use different
111/// entry formats. The entry type affects which fields are present and how
112/// certain lines (like the `r` line) are parsed.
113///
114/// # Variants
115///
116/// Each variant corresponds to a specific document type:
117///
118/// - [`V2`](Self::V2) - Legacy network status v2 documents
119/// - [`V3`](Self::V3) - Standard network status v3 consensus
120/// - [`MicroV3`](Self::MicroV3) - Microdescriptor-flavored v3 consensus
121/// - [`Bridge`](Self::Bridge) - Bridge network status documents
122///
123/// # Format Differences
124///
125/// The main difference between entry types is the `r` line format:
126///
127/// - **V2/V3/Bridge**: `r nickname identity digest published address or_port dir_port`
128/// - **MicroV3**: `r nickname identity published address or_port dir_port` (no digest)
129///
130/// MicroV3 entries use the `m` line for the microdescriptor digest instead.
131///
132/// # Example
133///
134/// ```rust
135/// use stem_rs::descriptor::router_status::RouterStatusEntryType;
136///
137/// let entry_type = RouterStatusEntryType::V3;
138/// assert_eq!(entry_type, RouterStatusEntryType::V3);
139/// ```
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum RouterStatusEntryType {
142    /// Legacy network status v2 entry format.
143    ///
144    /// Used in older network status documents. Contains a SHA-1 digest
145    /// of the relay's server descriptor.
146    V2,
147
148    /// Standard network status v3 entry format.
149    ///
150    /// The most common format, used in regular consensus documents.
151    /// Contains a SHA-1 digest of the relay's server descriptor.
152    V3,
153
154    /// Microdescriptor-flavored v3 entry format.
155    ///
156    /// Used in microdescriptor consensus documents. Does not contain
157    /// a server descriptor digest in the `r` line; instead uses the
158    /// `m` line for the microdescriptor digest.
159    MicroV3,
160
161    /// Bridge network status entry format.
162    ///
163    /// Used for bridge relay status entries. Similar to V2 format
164    /// but specific to bridge authority documents.
165    Bridge,
166}
167
168/// Microdescriptor hash information from vote documents.
169///
170/// In directory authority votes, the `m` line contains microdescriptor
171/// digests computed using different consensus methods. This allows
172/// authorities to vote on which microdescriptor digest should be used
173/// for each relay.
174///
175/// # Fields
176///
177/// - `methods` - Consensus method numbers that produce this digest
178/// - `hashes` - Mapping of hash algorithm names to digest values
179///
180/// # Format
181///
182/// The `m` line format in votes is:
183/// ```text
184/// m methods algorithm=digest [algorithm=digest ...]
185/// ```
186///
187/// For example:
188/// ```text
189/// m 13,14,15 sha256=uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc
190/// ```
191///
192/// # Example
193///
194/// ```rust
195/// use stem_rs::descriptor::router_status::MicrodescriptorHash;
196/// use std::collections::HashMap;
197///
198/// let hash = MicrodescriptorHash {
199///     methods: vec![13, 14, 15],
200///     hashes: {
201///         let mut h = HashMap::new();
202///         h.insert("sha256".to_string(), "uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc".to_string());
203///         h
204///     },
205/// };
206///
207/// assert_eq!(hash.methods, vec![13, 14, 15]);
208/// ```
209#[derive(Debug, Clone, PartialEq)]
210pub struct MicrodescriptorHash {
211    /// Consensus method numbers that produce this microdescriptor digest.
212    ///
213    /// Different consensus methods may produce different microdescriptor
214    /// digests for the same relay. This field lists which methods
215    /// correspond to the digests in the `hashes` field.
216    pub methods: Vec<u32>,
217
218    /// Mapping of hash algorithm names to digest values.
219    ///
220    /// The key is the algorithm name (e.g., "sha256") and the value
221    /// is the base64-encoded digest.
222    pub hashes: HashMap<String, String>,
223}
224
225/// Information about an individual relay in a network status document.
226///
227/// A `RouterStatusEntry` contains the essential information about a single
228/// relay as recorded in a network status consensus or vote. This includes
229/// the relay's identity, network location, capabilities, and performance
230/// characteristics.
231///
232/// # Overview
233///
234/// Router status entries are the building blocks of consensus documents.
235/// Each entry describes one relay and contains:
236///
237/// - **Identity**: Nickname, fingerprint, and optional Ed25519 identity
238/// - **Network**: IP address, OR port, directory port, additional addresses
239/// - **Capabilities**: Flags assigned by directory authorities
240/// - **Performance**: Bandwidth weights for path selection
241/// - **Policy**: Exit policy summary
242/// - **Version**: Tor software version
243///
244/// # Entry Types
245///
246/// The [`entry_type`](Self::entry_type) field determines the format:
247///
248/// | Type | Digest Field | Microdescriptor |
249/// |------|--------------|-----------------|
250/// | V2 | `digest` (hex) | N/A |
251/// | V3 | `digest` (hex) | Via `m` line in votes |
252/// | MicroV3 | N/A | `microdescriptor_digest` |
253/// | Bridge | `digest` (hex) | N/A |
254///
255/// # Parsing
256///
257/// Use the appropriate parsing method for your entry type:
258///
259/// - [`parse()`](Self::parse) - Standard V3 consensus entries
260/// - [`parse_micro()`](Self::parse_micro) - Microdescriptor consensus entries
261/// - [`parse_v2()`](Self::parse_v2) - Legacy V2 entries
262/// - [`parse_vote()`](Self::parse_vote) - Vote document entries
263///
264/// # Example
265///
266/// ```rust
267/// use stem_rs::descriptor::router_status::RouterStatusEntry;
268///
269/// let content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww oQZFLYe9e4A7bOkWKR7TaNxb0JE 2024-01-15 12:00:00 192.0.2.1 9001 9030
270/// s Fast Guard Running Stable Valid
271/// v Tor 0.4.8.10
272/// w Bandwidth=5000 Measured=4800
273/// p accept 80,443"#;
274///
275/// let entry = RouterStatusEntry::parse(content).unwrap();
276///
277/// // Access relay identity
278/// assert_eq!(entry.nickname, "example");
279/// assert!(!entry.fingerprint.is_empty());
280///
281/// // Check flags
282/// assert!(entry.flags.contains(&"Guard".to_string()));
283/// assert!(entry.flags.contains(&"Running".to_string()));
284///
285/// // Check bandwidth
286/// assert_eq!(entry.bandwidth, Some(5000));
287/// assert_eq!(entry.measured, Some(4800));
288/// ```
289///
290/// # Flags
291///
292/// The `flags` field contains strings assigned by directory authorities.
293/// Common flags include:
294///
295/// - `Authority` - A directory authority
296/// - `BadExit` - Believed to be useless as an exit
297/// - `Exit` - Suitable for exit traffic
298/// - `Fast` - Suitable for high-bandwidth circuits
299/// - `Guard` - Suitable as an entry guard
300/// - `HSDir` - Hidden service directory
301/// - `Running` - Currently usable
302/// - `Stable` - Suitable for long-lived circuits
303/// - `Valid` - Has been validated
304///
305/// # Thread Safety
306///
307/// `RouterStatusEntry` is `Send` and `Sync`, making it safe to share
308/// across threads.
309///
310/// # See Also
311///
312/// - [`crate::descriptor::consensus::NetworkStatusDocument`] - Contains router status entries
313/// - [`crate::Flag`] - Enum of standard relay flags
314#[derive(Debug, Clone, PartialEq)]
315pub struct RouterStatusEntry {
316    /// The type of this router status entry.
317    ///
318    /// Determines the format and which fields are available.
319    pub entry_type: RouterStatusEntryType,
320
321    /// The relay's nickname (1-19 alphanumeric characters).
322    ///
323    /// Nicknames are not unique identifiers; use `fingerprint` for
324    /// reliable relay identification.
325    pub nickname: String,
326
327    /// The relay's identity fingerprint as uppercase hexadecimal.
328    ///
329    /// This is a 40-character hex string representing the SHA-1 hash
330    /// of the relay's identity key. This is the authoritative identifier
331    /// for a relay.
332    pub fingerprint: String,
333
334    /// SHA-1 digest of the relay's server descriptor (hex, uppercase).
335    ///
336    /// Present in V2, V3, and Bridge entries. Not present in MicroV3
337    /// entries (use `microdescriptor_digest` instead).
338    pub digest: Option<String>,
339
340    /// When the relay's descriptor was published (UTC).
341    pub published: DateTime<Utc>,
342
343    /// The relay's primary IP address.
344    pub address: IpAddr,
345
346    /// The relay's OR (onion router) port for relay traffic.
347    pub or_port: u16,
348
349    /// The relay's directory port, if it serves directory information.
350    ///
351    /// `None` if the relay doesn't serve directory information (port 0).
352    pub dir_port: Option<u16>,
353
354    /// Additional OR addresses (typically IPv6).
355    ///
356    /// Each tuple contains (address, port, is_ipv6). The primary address
357    /// is in the `address` field; this contains additional addresses
358    /// from `a` lines.
359    pub or_addresses: Vec<(IpAddr, u16, bool)>,
360
361    /// Flags assigned to this relay by directory authorities.
362    ///
363    /// Common flags: "Authority", "BadExit", "Exit", "Fast", "Guard",
364    /// "HSDir", "Running", "Stable", "Valid", "V2Dir".
365    pub flags: Vec<String>,
366
367    /// The raw version line from the entry.
368    ///
369    /// Typically starts with "Tor " followed by the version number.
370    pub version_line: Option<String>,
371
372    /// Parsed Tor version, if the version line was parseable.
373    ///
374    /// `None` if the relay uses a non-standard version format.
375    pub version: Option<Version>,
376
377    /// Consensus bandwidth weight (arbitrary units, typically KB/s).
378    ///
379    /// Used for path selection weighting. Higher values indicate
380    /// more bandwidth capacity.
381    pub bandwidth: Option<u64>,
382
383    /// Bandwidth measured by bandwidth authorities.
384    ///
385    /// More accurate than self-reported bandwidth. Used when available.
386    pub measured: Option<u64>,
387
388    /// Whether the bandwidth value is unmeasured.
389    ///
390    /// `true` if the bandwidth is not based on actual measurements
391    /// (fewer than 3 measurements available).
392    pub is_unmeasured: bool,
393
394    /// Unrecognized entries from the `w` (bandwidth) line.
395    ///
396    /// Contains any bandwidth-related key=value pairs that weren't
397    /// recognized during parsing.
398    pub unrecognized_bandwidth_entries: Vec<String>,
399
400    /// Exit policy summary from the `p` line.
401    ///
402    /// A compact representation of the relay's exit policy.
403    pub exit_policy: Option<MicroExitPolicy>,
404
405    /// Protocol versions supported by this relay.
406    ///
407    /// Maps protocol names (e.g., "Link", "Relay") to lists of
408    /// supported version numbers.
409    pub protocols: HashMap<String, Vec<u32>>,
410
411    /// Microdescriptor digest (base64) for MicroV3 entries.
412    ///
413    /// Used to fetch the corresponding microdescriptor.
414    pub microdescriptor_digest: Option<String>,
415
416    /// Microdescriptor hashes from vote documents.
417    ///
418    /// Contains digests computed using different consensus methods.
419    /// Only present in vote documents, not consensus documents.
420    pub microdescriptor_hashes: Vec<MicrodescriptorHash>,
421
422    /// Ed25519 identity key type (typically "ed25519").
423    ///
424    /// Present in vote documents when the relay has an Ed25519 key.
425    pub identifier_type: Option<String>,
426
427    /// Ed25519 identity key value (base64).
428    ///
429    /// The value "none" indicates the relay doesn't have an Ed25519 key.
430    pub identifier: Option<String>,
431
432    /// Lines that weren't recognized during parsing.
433    ///
434    /// Useful for forward compatibility with new entry fields.
435    unrecognized_lines: Vec<String>,
436}
437
438impl RouterStatusEntry {
439    /// Creates a new router status entry with minimal required fields.
440    ///
441    /// This constructor creates an entry with only the essential fields
442    /// populated. Optional fields are set to their default values.
443    ///
444    /// # Arguments
445    ///
446    /// * `entry_type` - The type of entry (V2, V3, MicroV3, or Bridge)
447    /// * `nickname` - The relay's nickname (1-19 alphanumeric characters)
448    /// * `fingerprint` - The relay's identity fingerprint (40 hex characters)
449    /// * `published` - When the relay's descriptor was published
450    /// * `address` - The relay's primary IP address
451    /// * `or_port` - The relay's OR port
452    ///
453    /// # Example
454    ///
455    /// ```rust
456    /// use stem_rs::descriptor::router_status::{RouterStatusEntry, RouterStatusEntryType};
457    /// use chrono::Utc;
458    /// use std::net::IpAddr;
459    ///
460    /// let entry = RouterStatusEntry::new(
461    ///     RouterStatusEntryType::V3,
462    ///     "example".to_string(),
463    ///     "AABBCCDD".repeat(5),
464    ///     Utc::now(),
465    ///     "192.0.2.1".parse().unwrap(),
466    ///     9001,
467    /// );
468    ///
469    /// assert_eq!(entry.nickname, "example");
470    /// assert_eq!(entry.or_port, 9001);
471    /// ```
472    pub fn new(
473        entry_type: RouterStatusEntryType,
474        nickname: String,
475        fingerprint: String,
476        published: DateTime<Utc>,
477        address: IpAddr,
478        or_port: u16,
479    ) -> Self {
480        Self {
481            entry_type,
482            nickname,
483            fingerprint,
484            digest: None,
485            published,
486            address,
487            or_port,
488            dir_port: None,
489            or_addresses: Vec::new(),
490            flags: Vec::new(),
491            version_line: None,
492            version: None,
493            bandwidth: None,
494            measured: None,
495            is_unmeasured: false,
496            unrecognized_bandwidth_entries: Vec::new(),
497            exit_policy: None,
498            protocols: HashMap::new(),
499            microdescriptor_digest: None,
500            microdescriptor_hashes: Vec::new(),
501            identifier_type: None,
502            identifier: None,
503            unrecognized_lines: Vec::new(),
504        }
505    }
506
507    /// Parses a V3 router status entry from a string.
508    ///
509    /// This is the standard parsing method for entries from network status
510    /// v3 consensus documents.
511    ///
512    /// # Arguments
513    ///
514    /// * `content` - The entry content as a multi-line string
515    ///
516    /// # Returns
517    ///
518    /// A parsed `RouterStatusEntry` on success.
519    ///
520    /// # Errors
521    ///
522    /// Returns [`Error::Parse`] if:
523    /// - The `r` line is missing or malformed
524    /// - Required fields cannot be parsed
525    /// - The fingerprint cannot be decoded from base64
526    ///
527    /// # Example
528    ///
529    /// ```rust
530    /// use stem_rs::descriptor::router_status::RouterStatusEntry;
531    ///
532    /// let content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww oQZFLYe9e4A7bOkWKR7TaNxb0JE 2024-01-15 12:00:00 192.0.2.1 9001 0
533    /// s Fast Running Valid"#;
534    ///
535    /// let entry = RouterStatusEntry::parse(content).unwrap();
536    /// assert_eq!(entry.nickname, "example");
537    /// ```
538    pub fn parse(content: &str) -> Result<Self, Error> {
539        Self::parse_with_type(content, RouterStatusEntryType::V3, false)
540    }
541
542    /// Parses a microdescriptor-flavored V3 router status entry.
543    ///
544    /// Use this method for entries from microdescriptor consensus documents.
545    /// The main difference from [`parse()`](Self::parse) is that the `r` line
546    /// does not contain a server descriptor digest.
547    ///
548    /// # Arguments
549    ///
550    /// * `content` - The entry content as a multi-line string
551    ///
552    /// # Returns
553    ///
554    /// A parsed `RouterStatusEntry` with `entry_type` set to `MicroV3`.
555    ///
556    /// # Errors
557    ///
558    /// Returns [`Error::Parse`] if the entry is malformed.
559    ///
560    /// # Example
561    ///
562    /// ```rust
563    /// use stem_rs::descriptor::router_status::RouterStatusEntry;
564    ///
565    /// let content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww 2024-01-15 12:00:00 192.0.2.1 9001 0
566    /// m aiUklwBrua82obG5AsTX+iEpkjQA2+AQHxZ7GwMfY70
567    /// s Fast Running Valid"#;
568    ///
569    /// let entry = RouterStatusEntry::parse_micro(content).unwrap();
570    /// assert!(entry.digest.is_none());
571    /// assert!(entry.microdescriptor_digest.is_some());
572    /// ```
573    pub fn parse_micro(content: &str) -> Result<Self, Error> {
574        Self::parse_with_type(content, RouterStatusEntryType::MicroV3, false)
575    }
576
577    /// Parses a legacy V2 router status entry.
578    ///
579    /// Use this method for entries from older network status v2 documents.
580    ///
581    /// # Arguments
582    ///
583    /// * `content` - The entry content as a multi-line string
584    ///
585    /// # Returns
586    ///
587    /// A parsed `RouterStatusEntry` with `entry_type` set to `V2`.
588    ///
589    /// # Errors
590    ///
591    /// Returns [`Error::Parse`] if the entry is malformed.
592    pub fn parse_v2(content: &str) -> Result<Self, Error> {
593        Self::parse_with_type(content, RouterStatusEntryType::V2, false)
594    }
595
596    /// Parses a router status entry from a vote document.
597    ///
598    /// Vote documents may contain additional fields not present in
599    /// consensus documents, such as Ed25519 identity keys (`id` line)
600    /// and microdescriptor hashes (`m` lines with method numbers).
601    ///
602    /// # Arguments
603    ///
604    /// * `content` - The entry content as a multi-line string
605    ///
606    /// # Returns
607    ///
608    /// A parsed `RouterStatusEntry` with vote-specific fields populated.
609    ///
610    /// # Errors
611    ///
612    /// Returns [`Error::Parse`] if the entry is malformed.
613    ///
614    /// # Example
615    ///
616    /// ```rust
617    /// use stem_rs::descriptor::router_status::RouterStatusEntry;
618    ///
619    /// let content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww oQZFLYe9e4A7bOkWKR7TaNxb0JE 2024-01-15 12:00:00 192.0.2.1 9001 0
620    /// s Fast Running Valid
621    /// id ed25519 8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8
622    /// m 13,14,15 sha256=uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc"#;
623    ///
624    /// let entry = RouterStatusEntry::parse_vote(content).unwrap();
625    /// assert_eq!(entry.identifier_type, Some("ed25519".to_string()));
626    /// assert!(!entry.microdescriptor_hashes.is_empty());
627    /// ```
628    pub fn parse_vote(content: &str) -> Result<Self, Error> {
629        Self::parse_with_type(content, RouterStatusEntryType::V3, true)
630    }
631
632    /// Parses a router status entry with explicit type and vote flag.
633    ///
634    /// This is the general-purpose parsing method that other parse methods
635    /// delegate to. Use this when you need explicit control over the entry
636    /// type and whether vote-specific parsing should be enabled.
637    ///
638    /// # Arguments
639    ///
640    /// * `content` - The entry content as a multi-line string
641    /// * `entry_type` - The type of entry to parse as
642    /// * `is_vote` - Whether to enable vote-specific parsing (for `m` and `id` lines)
643    ///
644    /// # Returns
645    ///
646    /// A parsed `RouterStatusEntry` on success.
647    ///
648    /// # Errors
649    ///
650    /// Returns [`Error::Parse`] if:
651    /// - The `r` line is missing or has insufficient fields
652    /// - The fingerprint or digest cannot be decoded from base64
653    /// - The IP address is invalid
654    /// - The ports are invalid
655    /// - The publication timestamp is malformed
656    ///
657    /// # Line Parsing
658    ///
659    /// The following lines are recognized:
660    ///
661    /// | Keyword | Description |
662    /// |---------|-------------|
663    /// | `r` | Router identity (required) |
664    /// | `a` | Additional OR addresses |
665    /// | `s` | Flags |
666    /// | `v` | Version |
667    /// | `w` | Bandwidth weights |
668    /// | `p` | Exit policy summary |
669    /// | `pr` | Protocol versions |
670    /// | `m` | Microdescriptor digest/hashes |
671    /// | `id` | Ed25519 identity (votes only) |
672    pub fn parse_with_type(
673        content: &str,
674        entry_type: RouterStatusEntryType,
675        is_vote: bool,
676    ) -> Result<Self, Error> {
677        let lines: Vec<&str> = content.lines().collect();
678        let is_micro = entry_type == RouterStatusEntryType::MicroV3;
679
680        let mut nickname = String::new();
681        let mut fingerprint = String::new();
682        let mut digest: Option<String> = None;
683        let mut published: Option<DateTime<Utc>> = None;
684        let mut address: Option<IpAddr> = None;
685        let mut or_port: u16 = 0;
686        let mut dir_port: Option<u16> = None;
687        let mut or_addresses: Vec<(IpAddr, u16, bool)> = Vec::new();
688        let mut flags: Vec<String> = Vec::new();
689        let mut version_line: Option<String> = None;
690        let mut version: Option<Version> = None;
691        let mut bandwidth: Option<u64> = None;
692        let mut measured: Option<u64> = None;
693        let mut is_unmeasured = false;
694        let mut unrecognized_bandwidth_entries: Vec<String> = Vec::new();
695        let mut exit_policy: Option<MicroExitPolicy> = None;
696        let mut protocols: HashMap<String, Vec<u32>> = HashMap::new();
697        let mut microdescriptor_digest: Option<String> = None;
698        let mut microdescriptor_hashes: Vec<MicrodescriptorHash> = Vec::new();
699        let mut identifier_type: Option<String> = None;
700        let mut identifier: Option<String> = None;
701        let mut unrecognized_lines: Vec<String> = Vec::new();
702
703        for line in lines {
704            if line.is_empty() {
705                continue;
706            }
707
708            let (keyword, value) = if let Some(space_pos) = line.find(' ') {
709                (&line[..space_pos], line[space_pos + 1..].trim())
710            } else {
711                (line, "")
712            };
713
714            match keyword {
715                "r" => {
716                    let parts: Vec<&str> = value.split_whitespace().collect();
717                    let min_parts = if is_micro { 7 } else { 8 };
718                    if parts.len() < min_parts {
719                        return Err(Error::Parse {
720                            location: "r".to_string(),
721                            reason: format!("r line requires {} fields", min_parts),
722                        });
723                    }
724                    nickname = parts[0].to_string();
725                    fingerprint = Self::base64_to_hex(parts[1])?;
726                    let (date_idx, time_idx, addr_idx, or_idx, dir_idx) = if is_micro {
727                        (2, 3, 4, 5, 6)
728                    } else {
729                        digest = Some(Self::base64_to_hex(parts[2])?);
730                        (3, 4, 5, 6, 7)
731                    };
732                    let datetime_str = format!("{} {}", parts[date_idx], parts[time_idx]);
733                    published = Some(Self::parse_timestamp(&datetime_str)?);
734                    address = Some(parts[addr_idx].parse().map_err(|_| Error::Parse {
735                        location: "r".to_string(),
736                        reason: format!("invalid address: {}", parts[addr_idx]),
737                    })?);
738                    or_port = parts[or_idx].parse().map_err(|_| Error::Parse {
739                        location: "r".to_string(),
740                        reason: format!("invalid or_port: {}", parts[or_idx]),
741                    })?;
742                    let dp: u16 = parts[dir_idx].parse().map_err(|_| Error::Parse {
743                        location: "r".to_string(),
744                        reason: format!("invalid dir_port: {}", parts[dir_idx]),
745                    })?;
746                    dir_port = if dp == 0 { None } else { Some(dp) };
747                }
748                "a" => {
749                    if let Ok(addr) = Self::parse_or_address(value) {
750                        or_addresses.push(addr);
751                    }
752                }
753                "s" => {
754                    flags = value.split_whitespace().map(|s| s.to_string()).collect();
755                }
756                "v" => {
757                    version_line = Some(value.to_string());
758                    if let Some(stripped) = value.strip_prefix("Tor ") {
759                        version = Version::parse(stripped).ok();
760                    }
761                }
762                "w" => {
763                    for entry in value.split_whitespace() {
764                        if let Some(eq_pos) = entry.find('=') {
765                            let key = &entry[..eq_pos];
766                            let val = &entry[eq_pos + 1..];
767                            match key {
768                                "Bandwidth" => bandwidth = val.parse().ok(),
769                                "Measured" => measured = val.parse().ok(),
770                                "Unmeasured" => is_unmeasured = val == "1",
771                                _ => unrecognized_bandwidth_entries.push(entry.to_string()),
772                            }
773                        } else {
774                            unrecognized_bandwidth_entries.push(entry.to_string());
775                        }
776                    }
777                }
778                "p" => {
779                    exit_policy = MicroExitPolicy::parse(value).ok();
780                }
781                "pr" => {
782                    protocols = Self::parse_protocols(value);
783                }
784                "m" => {
785                    if is_micro {
786                        microdescriptor_digest = Some(value.to_string());
787                    } else if is_vote {
788                        if let Ok(hash) = Self::parse_microdescriptor_hash(value) {
789                            microdescriptor_hashes.push(hash);
790                        }
791                    } else {
792                        microdescriptor_digest = Some(value.to_string());
793                    }
794                }
795                "id" => {
796                    let parts: Vec<&str> = value.split_whitespace().collect();
797                    if parts.len() >= 2 {
798                        identifier_type = Some(parts[0].to_string());
799                        identifier = Some(parts[1].to_string());
800                    }
801                }
802                _ => {
803                    if !line.is_empty() {
804                        unrecognized_lines.push(line.to_string());
805                    }
806                }
807            }
808        }
809
810        let address = address.ok_or_else(|| Error::Parse {
811            location: "r".to_string(),
812            reason: "missing r line".to_string(),
813        })?;
814        let published = published.ok_or_else(|| Error::Parse {
815            location: "r".to_string(),
816            reason: "missing published time".to_string(),
817        })?;
818
819        Ok(Self {
820            entry_type,
821            nickname,
822            fingerprint,
823            digest,
824            published,
825            address,
826            or_port,
827            dir_port,
828            or_addresses,
829            flags,
830            version_line,
831            version,
832            bandwidth,
833            measured,
834            is_unmeasured,
835            unrecognized_bandwidth_entries,
836            exit_policy,
837            protocols,
838            microdescriptor_digest,
839            microdescriptor_hashes,
840            identifier_type,
841            identifier,
842            unrecognized_lines,
843        })
844    }
845
846    /// Parses a microdescriptor hash line from a vote document.
847    ///
848    /// The `m` line in votes has the format:
849    /// ```text
850    /// m methods algorithm=digest [algorithm=digest ...]
851    /// ```
852    ///
853    /// # Arguments
854    ///
855    /// * `value` - The value portion of the `m` line (after "m ")
856    ///
857    /// # Returns
858    ///
859    /// A [`MicrodescriptorHash`] containing the parsed methods and digests.
860    ///
861    /// # Errors
862    ///
863    /// Returns [`Error::Parse`] if the line is empty.
864    fn parse_microdescriptor_hash(value: &str) -> Result<MicrodescriptorHash, Error> {
865        let parts: Vec<&str> = value.split_whitespace().collect();
866        if parts.is_empty() {
867            return Err(Error::Parse {
868                location: "m".to_string(),
869                reason: "empty m line".to_string(),
870            });
871        }
872
873        let methods: Vec<u32> = parts[0].split(',').filter_map(|s| s.parse().ok()).collect();
874
875        let mut hashes = HashMap::new();
876        for entry in parts.iter().skip(1) {
877            if let Some(eq_pos) = entry.find('=') {
878                let algo = &entry[..eq_pos];
879                let digest = &entry[eq_pos + 1..];
880                hashes.insert(algo.to_string(), digest.to_string());
881            }
882        }
883
884        Ok(MicrodescriptorHash { methods, hashes })
885    }
886
887    /// Parses a timestamp string in "YYYY-MM-DD HH:MM:SS" format.
888    ///
889    /// # Arguments
890    ///
891    /// * `value` - The timestamp string to parse
892    ///
893    /// # Returns
894    ///
895    /// A UTC `DateTime` on success.
896    ///
897    /// # Errors
898    ///
899    /// Returns [`Error::Parse`] if the timestamp format is invalid.
900    fn parse_timestamp(value: &str) -> Result<DateTime<Utc>, Error> {
901        let datetime =
902            NaiveDateTime::parse_from_str(value.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
903                Error::Parse {
904                    location: "timestamp".to_string(),
905                    reason: format!("invalid datetime: {} - {}", value, e),
906                }
907            })?;
908        Ok(datetime.and_utc())
909    }
910
911    /// Converts a base64-encoded identity to uppercase hexadecimal.
912    ///
913    /// Used to decode relay fingerprints and digests from the compact
914    /// base64 format used in router status entries.
915    ///
916    /// # Arguments
917    ///
918    /// * `input` - Base64-encoded string (without padding)
919    ///
920    /// # Returns
921    ///
922    /// Uppercase hexadecimal string representation.
923    ///
924    /// # Errors
925    ///
926    /// Returns [`Error::Parse`] if the base64 decoding fails.
927    ///
928    /// # Example
929    ///
930    /// ```rust,ignore
931    /// let hex = RouterStatusEntry::base64_to_hex("p1aag7VwarGxqctS7/fS0y5FU+s").unwrap();
932    /// assert_eq!(hex, "A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB");
933    /// ```
934    fn base64_to_hex(input: &str) -> Result<String, Error> {
935        let decoded = Self::base64_decode(input)?;
936        Ok(decoded.iter().map(|b| format!("{:02X}", b)).collect())
937    }
938
939    /// Decodes a base64 string to bytes.
940    ///
941    /// Handles base64 strings with or without padding.
942    ///
943    /// # Arguments
944    ///
945    /// * `input` - Base64-encoded string
946    ///
947    /// # Returns
948    ///
949    /// Decoded bytes on success.
950    ///
951    /// # Errors
952    ///
953    /// Returns [`Error::Parse`] if the input contains invalid base64 characters.
954    fn base64_decode(input: &str) -> Result<Vec<u8>, Error> {
955        const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
956        let input = input.trim_end_matches('=');
957        let mut result = Vec::new();
958        let chars: Vec<u8> = input
959            .chars()
960            .filter_map(|c| ALPHABET.iter().position(|&x| x == c as u8).map(|p| p as u8))
961            .collect();
962
963        let mut i = 0;
964        while i < chars.len() {
965            let n = chars.len() - i;
966            if n >= 4 {
967                let a = chars[i] as u32;
968                let b = chars[i + 1] as u32;
969                let c = chars[i + 2] as u32;
970                let d = chars[i + 3] as u32;
971                let triple = (a << 18) | (b << 12) | (c << 6) | d;
972                result.push((triple >> 16) as u8);
973                result.push((triple >> 8) as u8);
974                result.push(triple as u8);
975                i += 4;
976            } else if n == 3 {
977                let a = chars[i] as u32;
978                let b = chars[i + 1] as u32;
979                let c = chars[i + 2] as u32;
980                let triple = (a << 18) | (b << 12) | (c << 6);
981                result.push((triple >> 16) as u8);
982                result.push((triple >> 8) as u8);
983                i += 3;
984            } else if n == 2 {
985                let a = chars[i] as u32;
986                let b = chars[i + 1] as u32;
987                let triple = (a << 18) | (b << 12);
988                result.push((triple >> 16) as u8);
989                i += 2;
990            } else {
991                break;
992            }
993        }
994        Ok(result)
995    }
996
997    /// Parses an OR address from an `a` line.
998    ///
999    /// Handles both IPv4 and IPv6 addresses. IPv6 addresses are expected
1000    /// to be enclosed in brackets: `[address]:port`.
1001    ///
1002    /// # Arguments
1003    ///
1004    /// * `line` - The address:port string (e.g., "192.0.2.1:9001" or "[::1]:9001")
1005    ///
1006    /// # Returns
1007    ///
1008    /// A tuple of (address, port, is_ipv6).
1009    ///
1010    /// # Errors
1011    ///
1012    /// Returns [`Error::Parse`] if the address or port is invalid.
1013    fn parse_or_address(line: &str) -> Result<(IpAddr, u16, bool), Error> {
1014        let line = line.trim();
1015        if line.starts_with('[') {
1016            if let Some(bracket_end) = line.find(']') {
1017                let ipv6_str = &line[1..bracket_end];
1018                let port_str = &line[bracket_end + 2..];
1019                let addr: IpAddr = ipv6_str.parse().map_err(|_| Error::Parse {
1020                    location: "a".to_string(),
1021                    reason: format!("invalid IPv6 address: {}", ipv6_str),
1022                })?;
1023                let port: u16 = port_str.parse().map_err(|_| Error::Parse {
1024                    location: "a".to_string(),
1025                    reason: format!("invalid port: {}", port_str),
1026                })?;
1027                return Ok((addr, port, true));
1028            }
1029        }
1030        if let Some(colon_pos) = line.rfind(':') {
1031            let addr_str = &line[..colon_pos];
1032            let port_str = &line[colon_pos + 1..];
1033            let addr: IpAddr = addr_str.parse().map_err(|_| Error::Parse {
1034                location: "a".to_string(),
1035                reason: format!("invalid address: {}", addr_str),
1036            })?;
1037            let port: u16 = port_str.parse().map_err(|_| Error::Parse {
1038                location: "a".to_string(),
1039                reason: format!("invalid port: {}", port_str),
1040            })?;
1041            let is_ipv6 = addr.is_ipv6();
1042            return Ok((addr, port, is_ipv6));
1043        }
1044        Err(Error::Parse {
1045            location: "a".to_string(),
1046            reason: format!("invalid or-address format: {}", line),
1047        })
1048    }
1049
1050    /// Parses protocol versions from a `pr` line.
1051    ///
1052    /// The `pr` line format is:
1053    /// ```text
1054    /// pr Protocol=versions [Protocol=versions ...]
1055    /// ```
1056    ///
1057    /// Versions can be individual numbers or ranges (e.g., "1-4").
1058    ///
1059    /// # Arguments
1060    ///
1061    /// * `value` - The value portion of the `pr` line
1062    ///
1063    /// # Returns
1064    ///
1065    /// A map of protocol names to lists of supported version numbers.
1066    ///
1067    /// # Example
1068    ///
1069    /// ```rust,ignore
1070    /// let protocols = RouterStatusEntry::parse_protocols("Link=1-4 Relay=1-2");
1071    /// assert_eq!(protocols.get("Link"), Some(&vec![1, 2, 3, 4]));
1072    /// ```
1073    fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
1074        let mut protocols = HashMap::new();
1075        for entry in value.split_whitespace() {
1076            if let Some(eq_pos) = entry.find('=') {
1077                let proto_name = &entry[..eq_pos];
1078                let versions_str = &entry[eq_pos + 1..];
1079                let versions: Vec<u32> = versions_str
1080                    .split(',')
1081                    .filter_map(|v| {
1082                        if let Some(dash) = v.find('-') {
1083                            let start: u32 = v[..dash].parse().ok()?;
1084                            let end: u32 = v[dash + 1..].parse().ok()?;
1085                            Some((start..=end).collect::<Vec<_>>())
1086                        } else {
1087                            v.parse().ok().map(|n| vec![n])
1088                        }
1089                    })
1090                    .flatten()
1091                    .collect();
1092                protocols.insert(proto_name.to_string(), versions);
1093            }
1094        }
1095        protocols
1096    }
1097
1098    /// Returns lines that weren't recognized during parsing.
1099    ///
1100    /// This is useful for forward compatibility when new fields are added
1101    /// to the router status entry format.
1102    ///
1103    /// # Returns
1104    ///
1105    /// A slice of unrecognized line strings.
1106    pub fn unrecognized_lines(&self) -> &[String] {
1107        &self.unrecognized_lines
1108    }
1109}
1110
1111/// Formats the router status entry back to its string representation.
1112///
1113/// The output follows the standard router status entry format and can
1114/// be parsed back using the appropriate `parse_*` method.
1115impl fmt::Display for RouterStatusEntry {
1116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1117        let is_micro = self.entry_type == RouterStatusEntryType::MicroV3;
1118        if is_micro {
1119            writeln!(
1120                f,
1121                "r {} {} {} {} {} {}",
1122                self.nickname,
1123                Self::hex_to_base64(&self.fingerprint),
1124                self.published.format("%Y-%m-%d %H:%M:%S"),
1125                self.address,
1126                self.or_port,
1127                self.dir_port.unwrap_or(0)
1128            )?;
1129        } else {
1130            writeln!(
1131                f,
1132                "r {} {} {} {} {} {} {}",
1133                self.nickname,
1134                Self::hex_to_base64(&self.fingerprint),
1135                self.digest
1136                    .as_ref()
1137                    .map(|d| Self::hex_to_base64(d))
1138                    .unwrap_or_default(),
1139                self.published.format("%Y-%m-%d %H:%M:%S"),
1140                self.address,
1141                self.or_port,
1142                self.dir_port.unwrap_or(0)
1143            )?;
1144        }
1145        for (addr, port, is_ipv6) in &self.or_addresses {
1146            if *is_ipv6 {
1147                writeln!(f, "a [{}]:{}", addr, port)?;
1148            } else {
1149                writeln!(f, "a {}:{}", addr, port)?;
1150            }
1151        }
1152        if !self.flags.is_empty() {
1153            writeln!(f, "s {}", self.flags.join(" "))?;
1154        }
1155        if let Some(ref v) = self.version_line {
1156            writeln!(f, "v {}", v)?;
1157        }
1158        if let Some(bw) = self.bandwidth {
1159            let mut w_parts = vec![format!("Bandwidth={}", bw)];
1160            if let Some(m) = self.measured {
1161                w_parts.push(format!("Measured={}", m));
1162            }
1163            if self.is_unmeasured {
1164                w_parts.push("Unmeasured=1".to_string());
1165            }
1166            for entry in &self.unrecognized_bandwidth_entries {
1167                w_parts.push(entry.clone());
1168            }
1169            writeln!(f, "w {}", w_parts.join(" "))?;
1170        }
1171        if let Some(ref policy) = self.exit_policy {
1172            writeln!(f, "p {}", policy)?;
1173        }
1174        if !self.protocols.is_empty() {
1175            let proto_str: Vec<String> = self
1176                .protocols
1177                .iter()
1178                .map(|(k, v)| {
1179                    let versions: Vec<String> = v.iter().map(|n| n.to_string()).collect();
1180                    format!("{}={}", k, versions.join(","))
1181                })
1182                .collect();
1183            writeln!(f, "pr {}", proto_str.join(" "))?;
1184        }
1185        if let (Some(ref id_type), Some(ref id)) = (&self.identifier_type, &self.identifier) {
1186            writeln!(f, "id {} {}", id_type, id)?;
1187        }
1188        for hash in &self.microdescriptor_hashes {
1189            let methods: Vec<String> = hash.methods.iter().map(|m| m.to_string()).collect();
1190            let hashes: Vec<String> = hash
1191                .hashes
1192                .iter()
1193                .map(|(k, v)| format!("{}={}", k, v))
1194                .collect();
1195            if hashes.is_empty() {
1196                writeln!(f, "m {}", methods.join(","))?;
1197            } else {
1198                writeln!(f, "m {} {}", methods.join(","), hashes.join(" "))?;
1199            }
1200        }
1201        if let Some(ref m) = self.microdescriptor_digest {
1202            if self.microdescriptor_hashes.is_empty() {
1203                writeln!(f, "m {}", m)?;
1204            }
1205        }
1206        Ok(())
1207    }
1208}
1209
1210impl RouterStatusEntry {
1211    /// Converts a hexadecimal string to base64 encoding.
1212    ///
1213    /// Used when formatting router status entries back to their string
1214    /// representation, as fingerprints and digests are stored in hex
1215    /// but displayed in base64.
1216    ///
1217    /// # Arguments
1218    ///
1219    /// * `hex` - Uppercase hexadecimal string
1220    ///
1221    /// # Returns
1222    ///
1223    /// Base64-encoded string (without padding).
1224    fn hex_to_base64(hex: &str) -> String {
1225        const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1226        let bytes: Vec<u8> = (0..hex.len())
1227            .step_by(2)
1228            .filter_map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
1229            .collect();
1230        let mut result = String::new();
1231        let mut i = 0;
1232        while i < bytes.len() {
1233            let b0 = bytes[i] as u32;
1234            let b1 = bytes.get(i + 1).map(|&b| b as u32).unwrap_or(0);
1235            let b2 = bytes.get(i + 2).map(|&b| b as u32).unwrap_or(0);
1236            let triple = (b0 << 16) | (b1 << 8) | b2;
1237            result.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
1238            result.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
1239            if i + 1 < bytes.len() {
1240                result.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
1241            }
1242            if i + 2 < bytes.len() {
1243                result.push(ALPHABET[(triple & 0x3F) as usize] as char);
1244            }
1245            i += 3;
1246        }
1247        result
1248    }
1249}
1250
1251/// Parses a router status entry from a string.
1252///
1253/// This implementation delegates to [`RouterStatusEntry::parse()`],
1254/// parsing the content as a V3 consensus entry.
1255///
1256/// # Example
1257///
1258/// ```rust
1259/// use stem_rs::descriptor::router_status::RouterStatusEntry;
1260/// use std::str::FromStr;
1261///
1262/// let content = r#"r example ARIJF2zbqirB9IwsW0mQznccWww oQZFLYe9e4A7bOkWKR7TaNxb0JE 2024-01-15 12:00:00 192.0.2.1 9001 0
1263/// s Running Valid"#;
1264///
1265/// let entry = RouterStatusEntry::from_str(content).unwrap();
1266/// assert_eq!(entry.nickname, "example");
1267/// ```
1268impl FromStr for RouterStatusEntry {
1269    type Err = Error;
1270
1271    fn from_str(s: &str) -> Result<Self, Self::Err> {
1272        Self::parse(s)
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279
1280    const EXAMPLE_V3_ENTRY: &str = r#"r test002r NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1281s Exit Fast Guard HSDir Running Stable V2Dir Valid
1282v Tor 0.3.0.7
1283pr Cons=1-2 Desc=1-2 DirCache=1 HSDir=1-2 HSIntro=3-4 HSRend=1-2 Link=1-4 LinkAuth=1,3 Microdesc=1-2 Relay=1-2
1284w Bandwidth=0 Unmeasured=1
1285p accept 1-65535"#;
1286
1287    const EXAMPLE_MICRO_ENTRY: &str = r#"r test002r NIIl+DyFR5ay3WNk5lyxibM71pY 2017-05-25 04:46:11 127.0.0.1 5002 7002
1288s Exit Fast Guard HSDir Running Stable V2Dir Valid
1289v Tor 0.3.0.7
1290w Bandwidth=0 Unmeasured=1
1291p accept 1-65535
1292m uhCGfIM6RbeD1Z/C6e9ct41+NIl9EbpgP8wG7uZT2Rw"#;
1293
1294    const ENTRY_WITHOUT_ED25519: &str = r#"r seele AAoQ1DAR6kkoo19hBAX5K0QztNw m0ynPuwzSextzsiXYJYA0Hce+Cs 2015-08-23 00:26:35 73.15.150.172 9001 0
1295s Running Stable Valid
1296v Tor 0.2.6.10
1297w Bandwidth=102 Measured=31
1298p reject 1-65535
1299id ed25519 none
1300m 13,14,15 sha256=uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc
1301m 16,17 sha256=G6FmPe/ehgfb6tsRzFKDCwvvae+RICeP1MaP0vWDGyI
1302m 18,19,20,21 sha256=/XhIMOnhElo2UiKjL2S10uRka/fhg1CFfNd+9wgUwEE"#;
1303
1304    const ENTRY_WITH_ED25519: &str = r#"r PDrelay1 AAFJ5u9xAqrKlpDW6N0pMhJLlKs yrJ6b/73pmHBiwsREgw+inf8WFw 2015-08-23 16:52:37 95.215.44.189 8080 0
1305s Fast Running Stable Valid
1306v Tor 0.2.7.2-alpha-dev
1307w Bandwidth=608 Measured=472
1308p reject 1-65535
1309id ed25519 8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8
1310m 13 sha256=PTSHzE7RKnRGZMRmBddSzDiZio254FUhv9+V4F5zq8s
1311m 14,15 sha256=0wsEwBbxJ8RtPmGYwilHQTVEw2pWzUBEVlSgEO77OyU
1312m 16,17 sha256=JK2xhYr/VsCF60px+LsT990BCpfKfQTeMxRbD63o2vE
1313m 18,19,20 sha256=AkZH3gIvz3wunsroqh5izBJizdYuR7kn2oVbsvqgML8
1314m 21 sha256=AVp41YVxKEJCaoEf0+77Cdvyw5YgpyDXdob0+LSv/pE"#;
1315
1316    const ENTRY_WITH_IPV6: &str = r#"r MYLEX AQt3KEVEEfSFzinUx5oUU0FRwsQ 2018-07-15 16:38:10 77.123.42.148 444 800
1317a [2001:470:71:9b9:f66d:4ff:fee7:954c]:444
1318m GWb+xjav0fsuwPwPNnUvW9Q1Ivk5nz8m1McECM4KY8A
1319s Fast Guard HSDir Running Stable V2Dir Valid
1320v Tor 0.2.5.16
1321pr Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2
1322w Bandwidth=4950"#;
1323
1324    #[test]
1325    fn test_parse_v3_entry() {
1326        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1327        assert_eq!(entry.nickname, "test002r");
1328        assert_eq!(
1329            entry.fingerprint,
1330            "348225F83C854796B2DD6364E65CB189B33BD696"
1331        );
1332        assert!(entry.digest.is_some());
1333        assert_eq!(entry.address.to_string(), "127.0.0.1");
1334        assert_eq!(entry.or_port, 5002);
1335        assert_eq!(entry.dir_port, Some(7002));
1336    }
1337
1338    #[test]
1339    fn test_parse_flags() {
1340        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1341        assert!(entry.flags.contains(&"Exit".to_string()));
1342        assert!(entry.flags.contains(&"Fast".to_string()));
1343        assert!(entry.flags.contains(&"Guard".to_string()));
1344        assert!(entry.flags.contains(&"Running".to_string()));
1345        assert!(entry.flags.contains(&"Stable".to_string()));
1346        assert!(entry.flags.contains(&"Valid".to_string()));
1347    }
1348
1349    #[test]
1350    fn test_parse_version() {
1351        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1352        assert_eq!(entry.version_line, Some("Tor 0.3.0.7".to_string()));
1353        assert!(entry.version.is_some());
1354    }
1355
1356    #[test]
1357    fn test_parse_bandwidth() {
1358        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1359        assert_eq!(entry.bandwidth, Some(0));
1360        assert!(entry.is_unmeasured);
1361    }
1362
1363    #[test]
1364    fn test_parse_exit_policy() {
1365        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1366        assert!(entry.exit_policy.is_some());
1367        let policy = entry.exit_policy.unwrap();
1368        assert!(policy.is_accept);
1369    }
1370
1371    #[test]
1372    fn test_parse_protocols() {
1373        let entry = RouterStatusEntry::parse(EXAMPLE_V3_ENTRY).unwrap();
1374        assert_eq!(entry.protocols.get("Cons"), Some(&vec![1, 2]));
1375        assert_eq!(entry.protocols.get("Link"), Some(&vec![1, 2, 3, 4]));
1376    }
1377
1378    #[test]
1379    fn test_parse_micro_entry() {
1380        let entry = RouterStatusEntry::parse_micro(EXAMPLE_MICRO_ENTRY).unwrap();
1381        assert_eq!(entry.nickname, "test002r");
1382        assert_eq!(
1383            entry.fingerprint,
1384            "348225F83C854796B2DD6364E65CB189B33BD696"
1385        );
1386        assert!(entry.digest.is_none());
1387        assert_eq!(
1388            entry.microdescriptor_digest,
1389            Some("uhCGfIM6RbeD1Z/C6e9ct41+NIl9EbpgP8wG7uZT2Rw".to_string())
1390        );
1391    }
1392
1393    #[test]
1394    fn test_parse_or_addresses() {
1395        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1396a [2001:6b0:7:125::242]:9001
1397a 10.0.0.1:9002"#;
1398        let entry = RouterStatusEntry::parse(content).unwrap();
1399        assert_eq!(entry.or_addresses.len(), 2);
1400        let (addr1, port1, is_ipv6_1) = &entry.or_addresses[0];
1401        assert_eq!(addr1.to_string(), "2001:6b0:7:125::242");
1402        assert_eq!(*port1, 9001);
1403        assert!(*is_ipv6_1);
1404        let (addr2, port2, is_ipv6_2) = &entry.or_addresses[1];
1405        assert_eq!(addr2.to_string(), "10.0.0.1");
1406        assert_eq!(*port2, 9002);
1407        assert!(!*is_ipv6_2);
1408    }
1409
1410    #[test]
1411    fn test_base64_to_hex() {
1412        let hex = RouterStatusEntry::base64_to_hex("NIIl+DyFR5ay3WNk5lyxibM71pY").unwrap();
1413        assert_eq!(hex, "348225F83C854796B2DD6364E65CB189B33BD696");
1414    }
1415
1416    #[test]
1417    fn test_fingerprint_decoding() {
1418        let test_values = [
1419            (
1420                "p1aag7VwarGxqctS7/fS0y5FU+s",
1421                "A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB",
1422            ),
1423            (
1424                "IbhGa8T+8tyy/MhxCk/qI+EI2LU",
1425                "21B8466BC4FEF2DCB2FCC8710A4FEA23E108D8B5",
1426            ),
1427            (
1428                "20wYcbFGwFfMktmuffYj6Z1RM9k",
1429                "DB4C1871B146C057CC92D9AE7DF623E99D5133D9",
1430            ),
1431            (
1432                "nTv9AG1cZeFW2hXiSIEAF6JLRJ4",
1433                "9D3BFD006D5C65E156DA15E248810017A24B449E",
1434            ),
1435            (
1436                "/UKsQiOSGPi/6es0/ha1prNTeDI",
1437                "FD42AC42239218F8BFE9EB34FE16B5A6B3537832",
1438            ),
1439            (
1440                "/nHdqoKZ6bKZixxAPzYt9Qen+Is",
1441                "FE71DDAA8299E9B2998B1C403F362DF507A7F88B",
1442            ),
1443        ];
1444        for (input, expected) in test_values {
1445            let result = RouterStatusEntry::base64_to_hex(input).unwrap();
1446            assert_eq!(result, expected);
1447        }
1448    }
1449
1450    #[test]
1451    fn test_without_ed25519() {
1452        let entry = RouterStatusEntry::parse_vote(ENTRY_WITHOUT_ED25519).unwrap();
1453        assert_eq!(entry.nickname, "seele");
1454        assert_eq!(
1455            entry.fingerprint,
1456            "000A10D43011EA4928A35F610405F92B4433B4DC"
1457        );
1458        assert_eq!(entry.address.to_string(), "73.15.150.172");
1459        assert_eq!(entry.or_port, 9001);
1460        assert_eq!(entry.dir_port, None);
1461        assert!(entry.flags.contains(&"Running".to_string()));
1462        assert!(entry.flags.contains(&"Stable".to_string()));
1463        assert!(entry.flags.contains(&"Valid".to_string()));
1464        assert_eq!(entry.version_line, Some("Tor 0.2.6.10".to_string()));
1465        assert_eq!(entry.bandwidth, Some(102));
1466        assert_eq!(entry.measured, Some(31));
1467        assert!(!entry.is_unmeasured);
1468        assert_eq!(entry.identifier_type, Some("ed25519".to_string()));
1469        assert_eq!(entry.identifier, Some("none".to_string()));
1470        assert_eq!(
1471            entry.digest,
1472            Some("9B4CA73EEC3349EC6DCEC897609600D0771EF82B".to_string())
1473        );
1474        assert_eq!(entry.microdescriptor_hashes.len(), 3);
1475        assert_eq!(entry.microdescriptor_hashes[0].methods, vec![13, 14, 15]);
1476        assert_eq!(
1477            entry.microdescriptor_hashes[0].hashes.get("sha256"),
1478            Some(&"uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc".to_string())
1479        );
1480    }
1481
1482    #[test]
1483    fn test_with_ed25519() {
1484        let entry = RouterStatusEntry::parse_vote(ENTRY_WITH_ED25519).unwrap();
1485        assert_eq!(entry.nickname, "PDrelay1");
1486        assert_eq!(
1487            entry.fingerprint,
1488            "000149E6EF7102AACA9690D6E8DD2932124B94AB"
1489        );
1490        assert_eq!(entry.address.to_string(), "95.215.44.189");
1491        assert_eq!(entry.or_port, 8080);
1492        assert_eq!(entry.dir_port, None);
1493        assert!(entry.flags.contains(&"Fast".to_string()));
1494        assert!(entry.flags.contains(&"Running".to_string()));
1495        assert!(entry.flags.contains(&"Stable".to_string()));
1496        assert!(entry.flags.contains(&"Valid".to_string()));
1497        assert_eq!(
1498            entry.version_line,
1499            Some("Tor 0.2.7.2-alpha-dev".to_string())
1500        );
1501        assert_eq!(entry.bandwidth, Some(608));
1502        assert_eq!(entry.measured, Some(472));
1503        assert!(!entry.is_unmeasured);
1504        assert_eq!(entry.identifier_type, Some("ed25519".to_string()));
1505        assert_eq!(
1506            entry.identifier,
1507            Some("8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8".to_string())
1508        );
1509        assert_eq!(
1510            entry.digest,
1511            Some("CAB27A6FFEF7A661C18B0B11120C3E8A77FC585C".to_string())
1512        );
1513        assert_eq!(entry.microdescriptor_hashes.len(), 5);
1514        assert_eq!(entry.microdescriptor_hashes[0].methods, vec![13]);
1515        assert_eq!(entry.microdescriptor_hashes[1].methods, vec![14, 15]);
1516    }
1517
1518    #[test]
1519    fn test_with_ipv6() {
1520        let entry = RouterStatusEntry::parse_micro(ENTRY_WITH_IPV6).unwrap();
1521        assert_eq!(entry.nickname, "MYLEX");
1522        assert_eq!(
1523            entry.fingerprint,
1524            "010B7728454411F485CE29D4C79A14534151C2C4"
1525        );
1526        assert_eq!(entry.address.to_string(), "77.123.42.148");
1527        assert_eq!(entry.or_port, 444);
1528        assert_eq!(entry.dir_port, Some(800));
1529        assert!(entry.flags.contains(&"Fast".to_string()));
1530        assert!(entry.flags.contains(&"Guard".to_string()));
1531        assert!(entry.flags.contains(&"HSDir".to_string()));
1532        assert!(entry.flags.contains(&"Running".to_string()));
1533        assert!(entry.flags.contains(&"Stable".to_string()));
1534        assert!(entry.flags.contains(&"V2Dir".to_string()));
1535        assert!(entry.flags.contains(&"Valid".to_string()));
1536        assert_eq!(entry.version_line, Some("Tor 0.2.5.16".to_string()));
1537        assert_eq!(entry.or_addresses.len(), 1);
1538        let (addr, port, is_ipv6) = &entry.or_addresses[0];
1539        assert_eq!(addr.to_string(), "2001:470:71:9b9:f66d:4ff:fee7:954c");
1540        assert_eq!(*port, 444);
1541        assert!(*is_ipv6);
1542        assert_eq!(entry.bandwidth, Some(4950));
1543        assert_eq!(entry.measured, None);
1544        assert!(!entry.is_unmeasured);
1545        assert_eq!(entry.protocols.len(), 10);
1546        assert_eq!(
1547            entry.microdescriptor_digest,
1548            Some("GWb+xjav0fsuwPwPNnUvW9Q1Ivk5nz8m1McECM4KY8A".to_string())
1549        );
1550    }
1551
1552    #[test]
1553    fn test_unrecognized_bandwidth_entries() {
1554        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1555s Running Valid
1556w Bandwidth=11111 Measured=482 Blarg!"#;
1557        let entry = RouterStatusEntry::parse(content).unwrap();
1558        assert_eq!(entry.bandwidth, Some(11111));
1559        assert_eq!(entry.measured, Some(482));
1560        assert_eq!(entry.unrecognized_bandwidth_entries, vec!["Blarg!"]);
1561    }
1562
1563    #[test]
1564    fn test_blank_lines() {
1565        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1566
1567s Running Valid
1568
1569v Tor 0.2.2.35
1570
1571"#;
1572        let entry = RouterStatusEntry::parse(content).unwrap();
1573        assert_eq!(entry.version_line, Some("Tor 0.2.2.35".to_string()));
1574    }
1575
1576    #[test]
1577    fn test_unrecognized_lines() {
1578        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1579s Running Valid
1580z New tor feature: sparkly unicorns!"#;
1581        let entry = RouterStatusEntry::parse(content).unwrap();
1582        assert_eq!(
1583            entry.unrecognized_lines(),
1584            &["z New tor feature: sparkly unicorns!"]
1585        );
1586    }
1587
1588    #[test]
1589    fn test_ipv6_addresses_multiple() {
1590        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1591a [2607:fcd0:daaa:101::602c:bd62]:443
1592a [1148:fcd0:daaa:101::602c:bd62]:80"#;
1593        let entry = RouterStatusEntry::parse(content).unwrap();
1594        assert_eq!(entry.or_addresses.len(), 2);
1595        let (addr1, port1, is_ipv6_1) = &entry.or_addresses[0];
1596        assert_eq!(addr1.to_string(), "2607:fcd0:daaa:101::602c:bd62");
1597        assert_eq!(*port1, 443);
1598        assert!(*is_ipv6_1);
1599        let (addr2, port2, is_ipv6_2) = &entry.or_addresses[1];
1600        assert_eq!(addr2.to_string(), "1148:fcd0:daaa:101::602c:bd62");
1601        assert_eq!(*port2, 80);
1602        assert!(*is_ipv6_2);
1603    }
1604
1605    #[test]
1606    fn test_versions() {
1607        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1608v Tor 0.2.2.35"#;
1609        let entry = RouterStatusEntry::parse(content).unwrap();
1610        assert_eq!(entry.version_line, Some("Tor 0.2.2.35".to_string()));
1611        assert!(entry.version.is_some());
1612
1613        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1614v Torr new_stuff"#;
1615        let entry = RouterStatusEntry::parse(content).unwrap();
1616        assert_eq!(entry.version_line, Some("Torr new_stuff".to_string()));
1617        assert!(entry.version.is_none());
1618    }
1619
1620    #[test]
1621    fn test_bandwidth_variations() {
1622        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1623w Bandwidth=63138"#;
1624        let entry = RouterStatusEntry::parse(content).unwrap();
1625        assert_eq!(entry.bandwidth, Some(63138));
1626        assert_eq!(entry.measured, None);
1627        assert!(!entry.is_unmeasured);
1628
1629        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1630w Bandwidth=11111 Measured=482"#;
1631        let entry = RouterStatusEntry::parse(content).unwrap();
1632        assert_eq!(entry.bandwidth, Some(11111));
1633        assert_eq!(entry.measured, Some(482));
1634        assert!(!entry.is_unmeasured);
1635
1636        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1637w Bandwidth=11111 Measured=482 Unmeasured=1"#;
1638        let entry = RouterStatusEntry::parse(content).unwrap();
1639        assert_eq!(entry.bandwidth, Some(11111));
1640        assert_eq!(entry.measured, Some(482));
1641        assert!(entry.is_unmeasured);
1642    }
1643
1644    #[test]
1645    fn test_exit_policy_variations() {
1646        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1647p reject 1-65535"#;
1648        let entry = RouterStatusEntry::parse(content).unwrap();
1649        assert!(entry.exit_policy.is_some());
1650        let policy = entry.exit_policy.unwrap();
1651        assert!(!policy.is_accept);
1652
1653        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1654p accept 80,110,143,443"#;
1655        let entry = RouterStatusEntry::parse(content).unwrap();
1656        assert!(entry.exit_policy.is_some());
1657        let policy = entry.exit_policy.unwrap();
1658        assert!(policy.is_accept);
1659    }
1660
1661    #[test]
1662    fn test_flags_variations() {
1663        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1664s "#;
1665        let entry = RouterStatusEntry::parse(content).unwrap();
1666        assert!(entry.flags.is_empty() || entry.flags.iter().all(|f| f.is_empty()));
1667
1668        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1669s Fast"#;
1670        let entry = RouterStatusEntry::parse(content).unwrap();
1671        assert!(entry.flags.contains(&"Fast".to_string()));
1672
1673        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1674s Fast Valid"#;
1675        let entry = RouterStatusEntry::parse(content).unwrap();
1676        assert!(entry.flags.contains(&"Fast".to_string()));
1677        assert!(entry.flags.contains(&"Valid".to_string()));
1678
1679        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1680s Ugabuga"#;
1681        let entry = RouterStatusEntry::parse(content).unwrap();
1682        assert!(entry.flags.contains(&"Ugabuga".to_string()));
1683    }
1684
1685    #[test]
1686    fn test_microdescriptor_hashes_variations() {
1687        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1688m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"#;
1689        let entry = RouterStatusEntry::parse_vote(content).unwrap();
1690        assert_eq!(entry.microdescriptor_hashes.len(), 1);
1691        assert_eq!(
1692            entry.microdescriptor_hashes[0].methods,
1693            vec![8, 9, 10, 11, 12]
1694        );
1695        assert_eq!(
1696            entry.microdescriptor_hashes[0].hashes.get("sha256"),
1697            Some(&"g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs".to_string())
1698        );
1699
1700        let content = r#"r test NIIl+DyFR5ay3WNk5lyxibM71pY UzQp+EE8G0YCKtNlZVy+3h5tv0Q 2017-05-25 04:46:11 127.0.0.1 5002 7002
1701m 11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
1702m 31,32 sha512=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"#;
1703        let entry = RouterStatusEntry::parse_vote(content).unwrap();
1704        assert_eq!(entry.microdescriptor_hashes.len(), 2);
1705        assert_eq!(entry.microdescriptor_hashes[0].methods, vec![11, 12]);
1706        assert_eq!(entry.microdescriptor_hashes[1].methods, vec![31, 32]);
1707    }
1708}