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}