stem_rs/descriptor/
consensus.rs

1//! Network status consensus document parsing.
2//!
3//! This module provides types for parsing Tor network status consensus documents
4//! which describe the current state of the Tor network including all known relays.
5//!
6//! # Overview
7//!
8//! Network status documents are the authoritative source of information about
9//! the Tor network. They come in two forms:
10//!
11//! - **Votes**: Individual directory authority opinions about the network
12//! - **Consensus**: The agreed-upon view signed by multiple authorities
13//!
14//! Clients download the consensus to learn about available relays, their
15//! capabilities, and which relays are recommended for different purposes.
16//!
17//! # Document Types
18//!
19//! | Type | Description |
20//! |------|-------------|
21//! | Consensus | Agreed network status signed by authorities |
22//! | Vote | Individual authority's view before consensus |
23//! | Microdesc Consensus | Consensus using microdescriptor hashes |
24//!
25//! # Validity Times
26//!
27//! Consensus documents have three important timestamps:
28//!
29//! - **valid-after**: When the consensus becomes valid
30//! - **fresh-until**: When clients should fetch a new consensus
31//! - **valid-until**: When the consensus expires completely
32//!
33//! Clients should fetch a new consensus between `fresh-until` and `valid-until`.
34//!
35//! # Document Format
36//!
37//! ```text
38//! network-status-version 3 [microdesc]
39//! vote-status consensus|vote
40//! consensus-method <N>
41//! valid-after <YYYY-MM-DD HH:MM:SS>
42//! fresh-until <YYYY-MM-DD HH:MM:SS>
43//! valid-until <YYYY-MM-DD HH:MM:SS>
44//! voting-delay <vote-seconds> <dist-seconds>
45//! known-flags <flag> <flag> ...
46//! recommended-client-protocols <proto>=<versions> ...
47//! required-client-protocols <proto>=<versions> ...
48//! params <key>=<value> ...
49//! dir-source <nickname> <identity> <hostname> <address> <dirport> <orport>
50//! ...
51//! directory-footer
52//! bandwidth-weights <key>=<value> ...
53//! directory-signature <identity> <signing-key-digest>
54//! -----BEGIN SIGNATURE-----
55//! <base64 signature>
56//! -----END SIGNATURE-----
57//! ```
58//!
59//! # Example
60//!
61//! ```rust,no_run
62//! use stem_rs::descriptor::{NetworkStatusDocument, Descriptor};
63//!
64//! let content = std::fs::read_to_string("cached-consensus").unwrap();
65//! let consensus = NetworkStatusDocument::parse(&content).unwrap();
66//!
67//! println!("Consensus method: {:?}", consensus.consensus_method);
68//! println!("Valid after: {}", consensus.valid_after);
69//! println!("Valid until: {}", consensus.valid_until);
70//! println!("Known flags: {:?}", consensus.known_flags);
71//! println!("Authorities: {}", consensus.authorities.len());
72//! println!("Signatures: {}", consensus.signatures.len());
73//!
74//! // Check protocol requirements
75//! if let Some(versions) = consensus.required_client_protocols.get("Link") {
76//!     println!("Required Link protocol versions: {:?}", versions);
77//! }
78//! ```
79//!
80//! # Shared Randomness
81//!
82//! Modern consensus documents include shared randomness values used for
83//! hidden service directory assignment. These are computed collaboratively
84//! by the directory authorities.
85//!
86//! # See Also
87//!
88//! - [`RouterStatusEntry`](super::RouterStatusEntry) - Individual relay entries in consensus
89//! - [`DirectoryAuthority`] - Authority information
90//! - [Python Stem NetworkStatusDocument](https://stem.torproject.org/api/descriptor/networkstatus.html)
91
92use std::collections::HashMap;
93use std::fmt;
94use std::net::IpAddr;
95use std::str::FromStr;
96
97use chrono::{DateTime, NaiveDateTime, Utc};
98use derive_builder::Builder;
99
100use crate::version::Version;
101use crate::Error;
102
103use super::authority::DirectoryAuthority;
104use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
105
106/// Validates a relay fingerprint.
107///
108/// A valid fingerprint is exactly 40 hexadecimal characters (case-insensitive).
109fn is_valid_fingerprint(fingerprint: &str) -> bool {
110    fingerprint.len() == 40 && fingerprint.chars().all(|c| c.is_ascii_hexdigit())
111}
112
113/// Shared randomness value from directory authority collaboration.
114///
115/// Directory authorities collaboratively generate random values that are
116/// used for hidden service directory assignment. Each consensus includes
117/// the current and previous shared randomness values.
118///
119/// # Fields
120///
121/// - `num_reveals`: Number of authorities that revealed their commitment
122/// - `value`: The base64-encoded random value
123#[derive(Debug, Clone, PartialEq)]
124pub struct SharedRandomness {
125    /// Number of authorities that participated in the reveal phase.
126    pub num_reveals: u32,
127    /// The shared random value (base64-encoded).
128    pub value: String,
129}
130
131/// A signature on a network status document.
132///
133/// Each directory authority signs the consensus with their signing key.
134/// The signature covers the document from the beginning through the
135/// `directory-signature` line.
136///
137/// # Fields
138///
139/// - `identity`: The authority's identity key fingerprint
140/// - `signing_key_digest`: Digest of the signing key used
141/// - `signature`: The PEM-encoded signature
142/// - `algorithm`: Optional algorithm identifier (e.g., "sha256")
143#[derive(Debug, Clone, PartialEq)]
144pub struct DocumentSignature {
145    /// The signing authority's identity key fingerprint (40 hex chars).
146    pub identity: String,
147    /// Digest of the signing key used for this signature.
148    pub signing_key_digest: String,
149    /// The PEM-encoded signature block.
150    pub signature: String,
151    /// Algorithm used (e.g., "sha256"), if specified.
152    pub algorithm: Option<String>,
153}
154
155/// A network status consensus or vote document.
156///
157/// This is the primary document that describes the state of the Tor network.
158/// Clients download the consensus to learn about available relays and their
159/// capabilities.
160///
161/// # Document Types
162///
163/// - **Consensus** (`is_consensus = true`): The agreed-upon network view
164/// - **Vote** (`is_vote = true`): An individual authority's opinion
165/// - **Microdesc** (`is_microdescriptor = true`): Uses microdescriptor hashes
166///
167/// # Validity Times
168///
169/// The document has three important timestamps that control its lifecycle:
170///
171/// ```text
172/// valid-after -----> fresh-until -----> valid-until
173///     |                  |                  |
174///     |   Document is    |   Should fetch   |   Document
175///     |   fresh/current  |   new consensus  |   expired
176/// ```
177///
178/// # Example
179///
180/// ```rust,no_run
181/// use stem_rs::descriptor::{NetworkStatusDocument, Descriptor};
182///
183/// let content = std::fs::read_to_string("cached-consensus").unwrap();
184/// let doc = NetworkStatusDocument::parse(&content).unwrap();
185///
186/// // Check document type
187/// if doc.is_consensus {
188///     println!("This is a consensus document");
189/// }
190///
191/// // Check validity
192/// let now = chrono::Utc::now();
193/// if now > doc.valid_until {
194///     println!("Consensus has expired!");
195/// } else if now > doc.fresh_until {
196///     println!("Should fetch a new consensus");
197/// }
198///
199/// // Check required protocols
200/// for (proto, versions) in &doc.required_client_protocols {
201///     println!("Required {}: {:?}", proto, versions);
202/// }
203/// ```
204///
205/// # Thread Safety
206///
207/// `NetworkStatusDocument` is `Send` and `Sync` as it contains only owned data.
208#[derive(Debug, Clone, PartialEq, Builder)]
209#[builder(setter(into, strip_option))]
210pub struct NetworkStatusDocument {
211    /// Network status version (typically 3).
212    pub version: u32,
213    /// Version flavor (empty string or "microdesc").
214    pub version_flavor: String,
215    /// Whether this is a consensus document.
216    pub is_consensus: bool,
217    /// Whether this is a vote document.
218    pub is_vote: bool,
219    /// Whether this uses microdescriptor format.
220    pub is_microdescriptor: bool,
221    /// Consensus method used (consensus only).
222    #[builder(default)]
223    pub consensus_method: Option<u32>,
224    /// Supported consensus methods (vote only).
225    #[builder(default)]
226    pub consensus_methods: Option<Vec<u32>>,
227    /// When this vote was published (vote only).
228    #[builder(default)]
229    pub published: Option<DateTime<Utc>>,
230    /// When this document becomes valid.
231    pub valid_after: DateTime<Utc>,
232    /// When clients should fetch a new document.
233    pub fresh_until: DateTime<Utc>,
234    /// When this document expires.
235    pub valid_until: DateTime<Utc>,
236    /// Seconds authorities wait for votes.
237    #[builder(default)]
238    pub vote_delay: Option<u32>,
239    /// Seconds authorities wait for signatures.
240    #[builder(default)]
241    pub dist_delay: Option<u32>,
242    /// Recommended Tor versions for clients.
243    #[builder(default)]
244    pub client_versions: Vec<Version>,
245    /// Recommended Tor versions for relays.
246    #[builder(default)]
247    pub server_versions: Vec<Version>,
248    /// Flags that may appear on relay entries.
249    #[builder(default)]
250    pub known_flags: Vec<String>,
251    /// Recommended protocol versions for clients.
252    #[builder(default)]
253    pub recommended_client_protocols: HashMap<String, Vec<u32>>,
254    /// Recommended protocol versions for relays.
255    #[builder(default)]
256    pub recommended_relay_protocols: HashMap<String, Vec<u32>>,
257    /// Required protocol versions for clients.
258    #[builder(default)]
259    pub required_client_protocols: HashMap<String, Vec<u32>>,
260    /// Required protocol versions for relays.
261    #[builder(default)]
262    pub required_relay_protocols: HashMap<String, Vec<u32>>,
263    /// Consensus parameters (key=value pairs).
264    #[builder(default)]
265    pub params: HashMap<String, i32>,
266    /// Previous shared randomness value.
267    #[builder(default)]
268    pub shared_randomness_previous: Option<SharedRandomness>,
269    /// Current shared randomness value.
270    #[builder(default)]
271    pub shared_randomness_current: Option<SharedRandomness>,
272    /// Bandwidth weights for path selection.
273    #[builder(default)]
274    pub bandwidth_weights: HashMap<String, i32>,
275    /// Directory authorities that contributed to this document.
276    #[builder(default)]
277    pub authorities: Vec<DirectoryAuthority>,
278    /// Signatures from directory authorities.
279    #[builder(default)]
280    pub signatures: Vec<DocumentSignature>,
281    /// Raw bytes of the original document.
282    #[builder(default)]
283    raw_content: Vec<u8>,
284    /// Lines that were not recognized during parsing.
285    #[builder(default)]
286    unrecognized_lines: Vec<String>,
287}
288
289impl NetworkStatusDocument {
290    /// Validates the consensus document for correctness and consistency.
291    ///
292    /// Performs comprehensive validation including:
293    /// - Timestamp ordering (valid_after < fresh_until < valid_until)
294    /// - Authority fingerprint format (40 hex characters)
295    /// - Signature presence and format
296    /// - Version number validity
297    ///
298    /// # Returns
299    ///
300    /// `Ok(())` if validation passes, otherwise returns a descriptive error.
301    ///
302    /// # Example
303    ///
304    /// ```rust,no_run
305    /// use stem_rs::descriptor::{NetworkStatusDocument, Descriptor};
306    ///
307    /// let content = std::fs::read_to_string("consensus").unwrap();
308    /// let consensus = NetworkStatusDocument::parse(&content).unwrap();
309    ///
310    /// match consensus.validate() {
311    ///     Ok(()) => println!("Consensus is valid"),
312    ///     Err(e) => eprintln!("Validation failed: {}", e),
313    /// }
314    /// ```
315    pub fn validate(&self) -> Result<(), Error> {
316        use crate::descriptor::ConsensusError;
317
318        if self.valid_after >= self.fresh_until {
319            return Err(Error::Descriptor(
320                crate::descriptor::DescriptorError::Consensus(
321                    ConsensusError::TimestampOrderingViolation(format!(
322                        "valid-after ({}) must be before fresh-until ({})",
323                        self.valid_after.to_rfc3339(),
324                        self.fresh_until.to_rfc3339()
325                    )),
326                ),
327            ));
328        }
329
330        if self.fresh_until >= self.valid_until {
331            return Err(Error::Descriptor(
332                crate::descriptor::DescriptorError::Consensus(
333                    ConsensusError::TimestampOrderingViolation(format!(
334                        "fresh-until ({}) must be before valid-until ({})",
335                        self.fresh_until.to_rfc3339(),
336                        self.valid_until.to_rfc3339()
337                    )),
338                ),
339            ));
340        }
341
342        if self.version != 3 {
343            return Err(Error::Descriptor(
344                crate::descriptor::DescriptorError::Consensus(
345                    ConsensusError::InvalidNetworkStatusVersion(self.version.to_string()),
346                ),
347            ));
348        }
349
350        for authority in &self.authorities {
351            if !is_valid_fingerprint(&authority.v3ident) {
352                return Err(Error::Descriptor(
353                    crate::descriptor::DescriptorError::Consensus(
354                        ConsensusError::InvalidFingerprint(authority.v3ident.clone()),
355                    ),
356                ));
357            }
358        }
359
360        if self.signatures.is_empty() {
361            return Err(Error::Descriptor(
362                crate::descriptor::DescriptorError::Consensus(
363                    ConsensusError::MissingRequiredField("signatures".to_string()),
364                ),
365            ));
366        }
367
368        for signature in &self.signatures {
369            if !is_valid_fingerprint(&signature.identity) {
370                return Err(Error::Descriptor(
371                    crate::descriptor::DescriptorError::Consensus(
372                        ConsensusError::InvalidFingerprint(signature.identity.clone()),
373                    ),
374                ));
375            }
376
377            if !is_valid_fingerprint(&signature.signing_key_digest) {
378                return Err(Error::Descriptor(
379                    crate::descriptor::DescriptorError::Consensus(
380                        ConsensusError::InvalidFingerprint(signature.signing_key_digest.clone()),
381                    ),
382                ));
383            }
384
385            if signature.signature.is_empty() {
386                return Err(Error::Descriptor(
387                    crate::descriptor::DescriptorError::Consensus(
388                        ConsensusError::InvalidSignature("signature is empty".to_string()),
389                    ),
390                ));
391            }
392        }
393
394        Ok(())
395    }
396
397    fn parse_timestamp(value: &str) -> Result<DateTime<Utc>, Error> {
398        let datetime =
399            NaiveDateTime::parse_from_str(value.trim(), "%Y-%m-%d %H:%M:%S").map_err(|e| {
400                Error::Parse {
401                    location: "timestamp".to_string(),
402                    reason: format!("invalid datetime: {} - {}", value, e),
403                }
404            })?;
405        Ok(datetime.and_utc())
406    }
407
408    fn parse_network_status_version(
409        value: &str,
410        builder: &mut NetworkStatusDocumentBuilder,
411    ) -> Result<(), Error> {
412        let parts: Vec<&str> = value.split_whitespace().collect();
413        let version: u32 = parts.first().and_then(|v| v.parse().ok()).unwrap_or(3);
414        builder.version(version);
415
416        if let Some(flavor) = parts.get(1) {
417            builder.version_flavor(flavor.to_string());
418            builder.is_microdescriptor(*flavor == "microdesc");
419        } else {
420            builder.version_flavor("ns".to_string());
421            builder.is_microdescriptor(false);
422        }
423        Ok(())
424    }
425
426    fn parse_vote_status(
427        value: &str,
428        builder: &mut NetworkStatusDocumentBuilder,
429    ) -> Result<(), Error> {
430        builder.is_consensus(value == "consensus");
431        builder.is_vote(value == "vote");
432        Ok(())
433    }
434
435    fn parse_voting_delay(
436        value: &str,
437        builder: &mut NetworkStatusDocumentBuilder,
438    ) -> Result<(), Error> {
439        let parts: Vec<&str> = value.split_whitespace().collect();
440        if parts.len() >= 2 {
441            if let Ok(vote) = parts[0].parse::<u32>() {
442                builder.vote_delay(vote);
443            }
444            if let Ok(dist) = parts[1].parse::<u32>() {
445                builder.dist_delay(dist);
446            }
447        }
448        Ok(())
449    }
450
451    fn parse_directory_signature(
452        value: &str,
453        lines: &[&str],
454        idx: usize,
455        _builder: &mut NetworkStatusDocumentBuilder,
456    ) -> Result<(DocumentSignature, usize), Error> {
457        let parts: Vec<&str> = value.split_whitespace().collect();
458        let (algorithm, identity, signing_key_digest) = if parts.len() >= 3 {
459            (
460                Some(parts[0].to_string()),
461                parts[1].to_string(),
462                parts[2].to_string(),
463            )
464        } else if parts.len() >= 2 {
465            (None, parts[0].to_string(), parts[1].to_string())
466        } else {
467            (None, String::new(), String::new())
468        };
469        let (signature, end_idx) = Self::extract_pem_block(lines, idx + 1);
470        Ok((
471            DocumentSignature {
472                identity,
473                signing_key_digest,
474                signature,
475                algorithm,
476            },
477            end_idx,
478        ))
479    }
480
481    fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
482        let mut protocols = HashMap::new();
483        for entry in value.split_whitespace() {
484            if let Some(eq_pos) = entry.find('=') {
485                let proto_name = &entry[..eq_pos];
486                let versions_str = &entry[eq_pos + 1..];
487                let versions: Vec<u32> = versions_str
488                    .split(',')
489                    .filter_map(|v| {
490                        if let Some(dash) = v.find('-') {
491                            let start: u32 = v[..dash].parse().ok()?;
492                            let end: u32 = v[dash + 1..].parse().ok()?;
493                            Some((start..=end).collect::<Vec<_>>())
494                        } else {
495                            v.parse().ok().map(|n| vec![n])
496                        }
497                    })
498                    .flatten()
499                    .collect();
500                protocols.insert(proto_name.to_string(), versions);
501            }
502        }
503        protocols
504    }
505
506    fn parse_params(value: &str) -> HashMap<String, i32> {
507        let mut params = HashMap::new();
508        for entry in value.split_whitespace() {
509            if let Some(eq_pos) = entry.find('=') {
510                let key = &entry[..eq_pos];
511                let val_str = &entry[eq_pos + 1..];
512                if let Ok(val) = val_str.parse::<i32>() {
513                    params.insert(key.to_string(), val);
514                }
515            }
516        }
517        params
518    }
519
520    fn parse_shared_randomness(value: &str) -> Option<SharedRandomness> {
521        let parts: Vec<&str> = value.split_whitespace().collect();
522        if parts.len() >= 2 {
523            let num_reveals = parts[0].parse().ok()?;
524            let value = parts[1].to_string();
525            Some(SharedRandomness { num_reveals, value })
526        } else {
527            None
528        }
529    }
530
531    fn parse_versions(value: &str) -> Vec<Version> {
532        value
533            .split(',')
534            .filter_map(|v| {
535                let v = v.trim();
536                if v.is_empty() {
537                    None
538                } else {
539                    Version::parse(v).ok()
540                }
541            })
542            .collect()
543    }
544
545    fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
546        let mut block = String::new();
547        let mut idx = start_idx;
548        while idx < lines.len() {
549            let line = lines[idx];
550            block.push_str(line);
551            block.push('\n');
552            if line.starts_with("-----END ") {
553                break;
554            }
555            idx += 1;
556        }
557        (block.trim_end().to_string(), idx)
558    }
559
560    fn parse_dir_source(value: &str) -> Result<DirectoryAuthority, Error> {
561        let parts: Vec<&str> = value.split_whitespace().collect();
562        if parts.len() < 6 {
563            return Err(Error::Parse {
564                location: "dir-source".to_string(),
565                reason: "dir-source requires 6 fields".to_string(),
566            });
567        }
568        let nickname = parts[0].to_string();
569        let v3ident = parts[1].to_string();
570        let hostname = parts[2].to_string();
571        let address: IpAddr = parts[3].parse().map_err(|_| Error::Parse {
572            location: "dir-source".to_string(),
573            reason: format!("invalid address: {}", parts[3]),
574        })?;
575        let dir_port: Option<u16> = {
576            let port: u16 = parts[4].parse().map_err(|_| Error::Parse {
577                location: "dir-source".to_string(),
578                reason: format!("invalid dir_port: {}", parts[4]),
579            })?;
580            if port == 0 {
581                None
582            } else {
583                Some(port)
584            }
585        };
586        let or_port: u16 = parts[5].parse().map_err(|_| Error::Parse {
587            location: "dir-source".to_string(),
588            reason: format!("invalid or_port: {}", parts[5]),
589        })?;
590        let is_legacy = nickname.ends_with("-legacy");
591        Ok(DirectoryAuthority {
592            nickname,
593            v3ident,
594            hostname,
595            address,
596            dir_port,
597            or_port,
598            is_legacy,
599            contact: None,
600            vote_digest: None,
601            legacy_dir_key: None,
602            key_certificate: None,
603            is_shared_randomness_participate: false,
604            shared_randomness_commitments: Vec::new(),
605            shared_randomness_previous_reveal_count: None,
606            shared_randomness_previous_value: None,
607            shared_randomness_current_reveal_count: None,
608            shared_randomness_current_value: None,
609            raw_content: Vec::new(),
610            unrecognized_lines: Vec::new(),
611        })
612    }
613}
614
615impl Descriptor for NetworkStatusDocument {
616    fn parse(content: &str) -> Result<Self, Error> {
617        let raw_content = content.as_bytes().to_vec();
618        let lines: Vec<&str> = content.lines().collect();
619
620        let mut builder = NetworkStatusDocumentBuilder::default();
621        let mut unrecognized_lines: Vec<String> = Vec::new();
622        let mut current_authority: Option<DirectoryAuthority> = None;
623
624        let mut idx = 0;
625        while idx < lines.len() {
626            let line = lines[idx];
627
628            if line.starts_with("@type ") {
629                idx += 1;
630                continue;
631            }
632
633            let (keyword, value) = if let Some(space_pos) = line.find(' ') {
634                (&line[..space_pos], line[space_pos + 1..].trim())
635            } else {
636                (line, "")
637            };
638
639            match keyword {
640                "network-status-version" => {
641                    Self::parse_network_status_version(value, &mut builder)?;
642                }
643                "vote-status" => {
644                    Self::parse_vote_status(value, &mut builder)?;
645                }
646                "consensus-method" => {
647                    if let Ok(method) = value.parse::<u32>() {
648                        builder.consensus_method(method);
649                    }
650                }
651                "consensus-methods" => {
652                    let methods: Vec<u32> = value
653                        .split_whitespace()
654                        .filter_map(|v| v.parse().ok())
655                        .collect();
656                    builder.consensus_methods(methods);
657                }
658                "published" => {
659                    builder.published(Self::parse_timestamp(value)?);
660                }
661                "valid-after" => {
662                    builder.valid_after(Self::parse_timestamp(value)?);
663                }
664                "fresh-until" => {
665                    builder.fresh_until(Self::parse_timestamp(value)?);
666                }
667                "valid-until" => {
668                    builder.valid_until(Self::parse_timestamp(value)?);
669                }
670                "voting-delay" => {
671                    Self::parse_voting_delay(value, &mut builder)?;
672                }
673                "client-versions" => {
674                    builder.client_versions(Self::parse_versions(value));
675                }
676                "server-versions" => {
677                    builder.server_versions(Self::parse_versions(value));
678                }
679                "known-flags" => {
680                    let flags: Vec<String> =
681                        value.split_whitespace().map(|s| s.to_string()).collect();
682                    builder.known_flags(flags);
683                }
684                "recommended-client-protocols" => {
685                    builder.recommended_client_protocols(Self::parse_protocols(value));
686                }
687                "recommended-relay-protocols" => {
688                    builder.recommended_relay_protocols(Self::parse_protocols(value));
689                }
690                "required-client-protocols" => {
691                    builder.required_client_protocols(Self::parse_protocols(value));
692                }
693                "required-relay-protocols" => {
694                    builder.required_relay_protocols(Self::parse_protocols(value));
695                }
696                "params" => {
697                    builder.params(Self::parse_params(value));
698                }
699                "shared-rand-previous-value" => {
700                    if let Some(sr) = Self::parse_shared_randomness(value) {
701                        builder.shared_randomness_previous(sr);
702                    }
703                }
704                "shared-rand-current-value" => {
705                    if let Some(sr) = Self::parse_shared_randomness(value) {
706                        builder.shared_randomness_current(sr);
707                    }
708                }
709                "bandwidth-weights" => {
710                    builder.bandwidth_weights(Self::parse_params(value));
711                }
712                "dir-source" => {
713                    if let Some(auth) = current_authority.take() {
714                        let mut auths = builder.authorities.take().unwrap_or_default();
715                        auths.push(auth);
716                        builder.authorities(auths);
717                    }
718                    current_authority = Some(Self::parse_dir_source(value)?);
719                }
720                "contact" => {
721                    if let Some(ref mut auth) = current_authority {
722                        auth.contact = Some(value.to_string());
723                    }
724                }
725                "vote-digest" => {
726                    if let Some(ref mut auth) = current_authority {
727                        auth.vote_digest = Some(value.to_string());
728                    }
729                }
730                "legacy-dir-key" => {
731                    if let Some(ref mut auth) = current_authority {
732                        auth.legacy_dir_key = Some(value.to_string());
733                    }
734                }
735                "directory-signature" => {
736                    if let Some(auth) = current_authority.take() {
737                        let mut auths = builder.authorities.take().unwrap_or_default();
738                        auths.push(auth);
739                        builder.authorities(auths);
740                    }
741                    let (signature, end_idx) =
742                        Self::parse_directory_signature(value, &lines, idx, &mut builder)?;
743                    let mut sigs = builder.signatures.take().unwrap_or_default();
744                    sigs.push(signature);
745                    builder.signatures(sigs);
746                    idx = end_idx;
747                }
748                "r" | "s" | "v" | "pr" | "w" | "p" | "m" | "a" => {
749                    if let Some(auth) = current_authority.take() {
750                        let mut auths = builder.authorities.take().unwrap_or_default();
751                        auths.push(auth);
752                        builder.authorities(auths);
753                    }
754                }
755                "directory-footer" => {}
756                _ => {
757                    if !line.is_empty() && !line.starts_with("-----") {
758                        unrecognized_lines.push(line.to_string());
759                    }
760                }
761            }
762            idx += 1;
763        }
764
765        if let Some(auth) = current_authority.take() {
766            let mut auths = builder.authorities.take().unwrap_or_default();
767            auths.push(auth);
768            builder.authorities(auths);
769        }
770
771        builder.raw_content(raw_content);
772        builder.unrecognized_lines(unrecognized_lines);
773
774        builder.build().map_err(|e| Error::Parse {
775            location: "NetworkStatusDocument".to_string(),
776            reason: format!("builder error: {}", e),
777        })
778    }
779
780    fn to_descriptor_string(&self) -> String {
781        let mut result = String::new();
782
783        if self.is_microdescriptor {
784            result.push_str(&format!(
785                "network-status-version {} microdesc\n",
786                self.version
787            ));
788        } else {
789            result.push_str(&format!("network-status-version {}\n", self.version));
790        }
791
792        if self.is_consensus {
793            result.push_str("vote-status consensus\n");
794        } else {
795            result.push_str("vote-status vote\n");
796        }
797
798        if let Some(method) = self.consensus_method {
799            result.push_str(&format!("consensus-method {}\n", method));
800        }
801
802        if let Some(ref methods) = self.consensus_methods {
803            let methods_str: Vec<String> = methods.iter().map(|m| m.to_string()).collect();
804            result.push_str(&format!("consensus-methods {}\n", methods_str.join(" ")));
805        }
806
807        if let Some(published) = self.published {
808            result.push_str(&format!(
809                "published {}\n",
810                published.format("%Y-%m-%d %H:%M:%S")
811            ));
812        }
813
814        result.push_str(&format!(
815            "valid-after {}\n",
816            self.valid_after.format("%Y-%m-%d %H:%M:%S")
817        ));
818        result.push_str(&format!(
819            "fresh-until {}\n",
820            self.fresh_until.format("%Y-%m-%d %H:%M:%S")
821        ));
822        result.push_str(&format!(
823            "valid-until {}\n",
824            self.valid_until.format("%Y-%m-%d %H:%M:%S")
825        ));
826
827        if let (Some(vote), Some(dist)) = (self.vote_delay, self.dist_delay) {
828            result.push_str(&format!("voting-delay {} {}\n", vote, dist));
829        }
830
831        if !self.client_versions.is_empty() {
832            let versions: Vec<String> =
833                self.client_versions.iter().map(|v| v.to_string()).collect();
834            result.push_str(&format!("client-versions {}\n", versions.join(",")));
835        }
836
837        if !self.server_versions.is_empty() {
838            let versions: Vec<String> =
839                self.server_versions.iter().map(|v| v.to_string()).collect();
840            result.push_str(&format!("server-versions {}\n", versions.join(",")));
841        }
842
843        if !self.known_flags.is_empty() {
844            result.push_str(&format!("known-flags {}\n", self.known_flags.join(" ")));
845        }
846
847        result
848    }
849
850    fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
851        Ok(compute_digest(&self.raw_content, hash, encoding))
852    }
853
854    fn raw_content(&self) -> &[u8] {
855        &self.raw_content
856    }
857
858    fn unrecognized_lines(&self) -> &[String] {
859        &self.unrecognized_lines
860    }
861}
862
863impl FromStr for NetworkStatusDocument {
864    type Err = Error;
865
866    fn from_str(s: &str) -> Result<Self, Self::Err> {
867        Self::parse(s)
868    }
869}
870
871impl fmt::Display for NetworkStatusDocument {
872    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
873        write!(f, "{}", self.to_descriptor_string())
874    }
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880
881    const EXAMPLE_CONSENSUS: &str = r#"network-status-version 3
882vote-status consensus
883consensus-method 26
884valid-after 2017-05-25 04:46:30
885fresh-until 2017-05-25 04:46:40
886valid-until 2017-05-25 04:46:50
887voting-delay 2 2
888client-versions 
889server-versions 
890known-flags Authority Exit Fast Guard HSDir NoEdConsensus Running Stable V2Dir Valid
891recommended-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
892recommended-relay-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
893required-client-protocols Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=4 LinkAuth=1 Microdesc=1-2 Relay=2
894required-relay-protocols Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=3-4 LinkAuth=1 Microdesc=1 Relay=1-2
895dir-source test001a 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
896contact auth1@test.test
897vote-digest 2E7177224BBA39B505F7608FF376C07884CF926F
898dir-source test000a BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7000 5000
899contact auth0@test.test
900vote-digest 5DD41617166FFB82882A117EEFDA0353A2794DC5
901directory-footer
902bandwidth-weights Wbd=3333 Wbe=0 Wbg=0 Wbm=10000
903directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
904-----BEGIN SIGNATURE-----
905Ho0rLojfLHs9cSPFxe6znuGuFU8BvRr6gnH1gULTjUZO0NSQvo5N628KFeAsq+pT
906-----END SIGNATURE-----
907"#;
908
909    #[test]
910    fn test_parse_consensus() {
911        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
912        assert_eq!(doc.version, 3);
913        assert!(doc.is_consensus);
914        assert!(!doc.is_vote);
915        assert!(!doc.is_microdescriptor);
916        assert_eq!(doc.consensus_method, Some(26));
917        assert_eq!(doc.vote_delay, Some(2));
918        assert_eq!(doc.dist_delay, Some(2));
919    }
920
921    #[test]
922    fn test_parse_known_flags() {
923        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
924        assert!(doc.known_flags.contains(&"Authority".to_string()));
925        assert!(doc.known_flags.contains(&"Exit".to_string()));
926        assert!(doc.known_flags.contains(&"Fast".to_string()));
927        assert!(doc.known_flags.contains(&"Guard".to_string()));
928        assert!(doc.known_flags.contains(&"HSDir".to_string()));
929        assert!(doc.known_flags.contains(&"Running".to_string()));
930        assert!(doc.known_flags.contains(&"Stable".to_string()));
931        assert!(doc.known_flags.contains(&"Valid".to_string()));
932    }
933
934    #[test]
935    fn test_parse_protocols() {
936        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
937        assert_eq!(
938            doc.recommended_client_protocols.get("Cons"),
939            Some(&vec![1, 2])
940        );
941        assert_eq!(doc.recommended_client_protocols.get("Link"), Some(&vec![4]));
942        assert_eq!(doc.required_relay_protocols.get("Link"), Some(&vec![3, 4]));
943    }
944
945    #[test]
946    fn test_parse_authorities() {
947        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
948        assert_eq!(doc.authorities.len(), 2);
949        let auth1 = &doc.authorities[0];
950        assert_eq!(auth1.nickname, "test001a");
951        assert_eq!(auth1.v3ident, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
952        assert_eq!(auth1.contact, Some("auth1@test.test".to_string()));
953        assert_eq!(
954            auth1.vote_digest,
955            Some("2E7177224BBA39B505F7608FF376C07884CF926F".to_string())
956        );
957    }
958
959    #[test]
960    fn test_parse_bandwidth_weights() {
961        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
962        assert_eq!(doc.bandwidth_weights.get("Wbd"), Some(&3333));
963        assert_eq!(doc.bandwidth_weights.get("Wbe"), Some(&0));
964        assert_eq!(doc.bandwidth_weights.get("Wbm"), Some(&10000));
965    }
966
967    #[test]
968    fn test_parse_signatures() {
969        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
970        assert_eq!(doc.signatures.len(), 1);
971        let sig = &doc.signatures[0];
972        assert_eq!(sig.identity, "596CD48D61FDA4E868F4AA10FF559917BE3B1A35");
973        assert_eq!(
974            sig.signing_key_digest,
975            "9FBF54D6A62364320308A615BF4CF6B27B254FAD"
976        );
977        assert!(sig.signature.contains("BEGIN SIGNATURE"));
978    }
979
980    #[test]
981    fn test_parse_timestamps() {
982        let doc = NetworkStatusDocument::parse(EXAMPLE_CONSENSUS).unwrap();
983        assert_eq!(
984            doc.valid_after.format("%Y-%m-%d %H:%M:%S").to_string(),
985            "2017-05-25 04:46:30"
986        );
987        assert_eq!(
988            doc.fresh_until.format("%Y-%m-%d %H:%M:%S").to_string(),
989            "2017-05-25 04:46:40"
990        );
991        assert_eq!(
992            doc.valid_until.format("%Y-%m-%d %H:%M:%S").to_string(),
993            "2017-05-25 04:46:50"
994        );
995    }
996
997    #[test]
998    fn test_microdescriptor_consensus() {
999        let content = "network-status-version 3 microdesc
1000vote-status consensus
1001valid-after 2017-05-25 04:46:30
1002fresh-until 2017-05-25 04:46:40
1003valid-until 2017-05-25 04:46:50
1004";
1005        let doc = NetworkStatusDocument::parse(content).unwrap();
1006        assert!(doc.is_microdescriptor);
1007        assert_eq!(doc.version_flavor, "microdesc");
1008    }
1009
1010    use proptest::prelude::*;
1011
1012    fn valid_timestamp() -> impl Strategy<Value = DateTime<Utc>> {
1013        (
1014            2015u32..2025,
1015            1u32..13,
1016            1u32..29,
1017            0u32..24,
1018            0u32..60,
1019            0u32..60,
1020        )
1021            .prop_map(|(year, month, day, hour, min, sec)| {
1022                let naive = chrono::NaiveDate::from_ymd_opt(year as i32, month, day)
1023                    .unwrap()
1024                    .and_hms_opt(hour, min, sec)
1025                    .unwrap();
1026                naive.and_utc()
1027            })
1028    }
1029
1030    fn valid_flag() -> impl Strategy<Value = String> {
1031        prop_oneof![
1032            Just("Authority".to_string()),
1033            Just("Exit".to_string()),
1034            Just("Fast".to_string()),
1035            Just("Guard".to_string()),
1036            Just("HSDir".to_string()),
1037            Just("Running".to_string()),
1038            Just("Stable".to_string()),
1039            Just("Valid".to_string()),
1040        ]
1041    }
1042
1043    fn simple_consensus() -> impl Strategy<Value = NetworkStatusDocument> {
1044        (
1045            valid_timestamp(),
1046            valid_timestamp(),
1047            valid_timestamp(),
1048            proptest::collection::vec(valid_flag(), 1..5),
1049            1u32..30,
1050        )
1051            .prop_map(|(valid_after, fresh_until, valid_until, flags, method)| {
1052                let mut doc = NetworkStatusDocument {
1053                    version: 3,
1054                    version_flavor: String::new(),
1055                    is_consensus: true,
1056                    is_vote: false,
1057                    is_microdescriptor: false,
1058                    consensus_method: Some(method),
1059                    consensus_methods: None,
1060                    published: None,
1061                    valid_after,
1062                    fresh_until,
1063                    valid_until,
1064                    vote_delay: Some(2),
1065                    dist_delay: Some(2),
1066                    client_versions: Vec::new(),
1067                    server_versions: Vec::new(),
1068                    known_flags: flags,
1069                    recommended_client_protocols: HashMap::new(),
1070                    recommended_relay_protocols: HashMap::new(),
1071                    required_client_protocols: HashMap::new(),
1072                    required_relay_protocols: HashMap::new(),
1073                    params: HashMap::new(),
1074                    shared_randomness_previous: None,
1075                    shared_randomness_current: None,
1076                    bandwidth_weights: HashMap::new(),
1077                    authorities: Vec::new(),
1078                    signatures: Vec::new(),
1079                    raw_content: Vec::new(),
1080                    unrecognized_lines: Vec::new(),
1081                };
1082                doc.known_flags.sort();
1083                doc.known_flags.dedup();
1084                doc
1085            })
1086    }
1087
1088    proptest! {
1089        #![proptest_config(ProptestConfig::with_cases(100))]
1090
1091        #[test]
1092        fn prop_consensus_roundtrip(doc in simple_consensus()) {
1093            let serialized = doc.to_descriptor_string();
1094            let parsed = NetworkStatusDocument::parse(&serialized);
1095
1096            prop_assert!(parsed.is_ok(), "Failed to parse serialized consensus: {:?}", parsed.err());
1097
1098            let parsed = parsed.unwrap();
1099
1100            prop_assert_eq!(doc.version, parsed.version, "version mismatch");
1101            prop_assert_eq!(doc.is_consensus, parsed.is_consensus, "is_consensus mismatch");
1102        }
1103    }
1104}
1105
1106#[cfg(test)]
1107mod comprehensive_tests {
1108    use super::*;
1109
1110    #[test]
1111    fn test_edge_case_empty_client_versions() {
1112        let content = r#"network-status-version 3
1113vote-status consensus
1114valid-after 2017-05-25 04:46:30
1115fresh-until 2017-05-25 04:46:40
1116valid-until 2017-05-25 04:46:50
1117client-versions 
1118"#;
1119        let doc = NetworkStatusDocument::parse(content).unwrap();
1120        assert_eq!(doc.client_versions.len(), 0);
1121    }
1122
1123    #[test]
1124    fn test_edge_case_empty_server_versions() {
1125        let content = r#"network-status-version 3
1126vote-status consensus
1127valid-after 2017-05-25 04:46:30
1128fresh-until 2017-05-25 04:46:40
1129valid-until 2017-05-25 04:46:50
1130server-versions 
1131"#;
1132        let doc = NetworkStatusDocument::parse(content).unwrap();
1133        assert_eq!(doc.server_versions.len(), 0);
1134    }
1135
1136    #[test]
1137    fn test_edge_case_empty_known_flags() {
1138        let content = r#"network-status-version 3
1139vote-status consensus
1140valid-after 2017-05-25 04:46:30
1141fresh-until 2017-05-25 04:46:40
1142valid-until 2017-05-25 04:46:50
1143known-flags 
1144"#;
1145        let doc = NetworkStatusDocument::parse(content).unwrap();
1146        assert_eq!(doc.known_flags.len(), 0);
1147    }
1148
1149    #[test]
1150    fn test_edge_case_empty_params() {
1151        let content = r#"network-status-version 3
1152vote-status consensus
1153valid-after 2017-05-25 04:46:30
1154fresh-until 2017-05-25 04:46:40
1155valid-until 2017-05-25 04:46:50
1156params 
1157"#;
1158        let doc = NetworkStatusDocument::parse(content).unwrap();
1159        assert_eq!(doc.params.len(), 0);
1160    }
1161
1162    #[test]
1163    fn test_edge_case_empty_bandwidth_weights() {
1164        let content = r#"network-status-version 3
1165vote-status consensus
1166valid-after 2017-05-25 04:46:30
1167fresh-until 2017-05-25 04:46:40
1168valid-until 2017-05-25 04:46:50
1169directory-footer
1170bandwidth-weights 
1171"#;
1172        let doc = NetworkStatusDocument::parse(content).unwrap();
1173        assert_eq!(doc.bandwidth_weights.len(), 0);
1174    }
1175
1176    #[test]
1177    fn test_edge_case_multiple_authorities() {
1178        let content = r#"network-status-version 3
1179vote-status consensus
1180valid-after 2017-05-25 04:46:30
1181fresh-until 2017-05-25 04:46:40
1182valid-until 2017-05-25 04:46:50
1183dir-source auth1 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 127.0.0.1 127.0.0.1 7001 5001
1184dir-source auth2 BCB380A633592C218757BEE11E630511A485658A 127.0.0.1 127.0.0.1 7002 5002
1185dir-source auth3 ABC380A633592C218757BEE11E630511A485658B 127.0.0.1 127.0.0.1 7003 5003
1186"#;
1187        let doc = NetworkStatusDocument::parse(content).unwrap();
1188        assert_eq!(doc.authorities.len(), 3);
1189        assert_eq!(doc.authorities[0].nickname, "auth1");
1190        assert_eq!(doc.authorities[1].nickname, "auth2");
1191        assert_eq!(doc.authorities[2].nickname, "auth3");
1192    }
1193
1194    #[test]
1195    fn test_edge_case_multiple_signatures() {
1196        let content = r#"network-status-version 3
1197vote-status consensus
1198valid-after 2017-05-25 04:46:30
1199fresh-until 2017-05-25 04:46:40
1200valid-until 2017-05-25 04:46:50
1201directory-signature 596CD48D61FDA4E868F4AA10FF559917BE3B1A35 9FBF54D6A62364320308A615BF4CF6B27B254FAD
1202-----BEGIN SIGNATURE-----
1203sig1
1204-----END SIGNATURE-----
1205directory-signature BCB380A633592C218757BEE11E630511A485658A 8FBF54D6A62364320308A615BF4CF6B27B254FAE
1206-----BEGIN SIGNATURE-----
1207sig2
1208-----END SIGNATURE-----
1209"#;
1210        let doc = NetworkStatusDocument::parse(content).unwrap();
1211        assert_eq!(doc.signatures.len(), 2);
1212        assert_eq!(
1213            doc.signatures[0].identity,
1214            "596CD48D61FDA4E868F4AA10FF559917BE3B1A35"
1215        );
1216        assert_eq!(
1217            doc.signatures[1].identity,
1218            "BCB380A633592C218757BEE11E630511A485658A"
1219        );
1220    }
1221
1222    #[test]
1223    fn test_edge_case_shared_randomness_both() {
1224        let content = r#"network-status-version 3
1225vote-status consensus
1226valid-after 2017-05-25 04:46:30
1227fresh-until 2017-05-25 04:46:40
1228valid-until 2017-05-25 04:46:50
1229shared-rand-previous-value 9 abcdef1234567890
1230shared-rand-current-value 10 1234567890abcdef
1231"#;
1232        let doc = NetworkStatusDocument::parse(content).unwrap();
1233        assert!(doc.shared_randomness_previous.is_some());
1234        assert!(doc.shared_randomness_current.is_some());
1235        let prev = doc.shared_randomness_previous.unwrap();
1236        assert_eq!(prev.num_reveals, 9);
1237        assert_eq!(prev.value, "abcdef1234567890");
1238        let curr = doc.shared_randomness_current.unwrap();
1239        assert_eq!(curr.num_reveals, 10);
1240        assert_eq!(curr.value, "1234567890abcdef");
1241    }
1242
1243    #[test]
1244    fn test_edge_case_vote_document() {
1245        let content = r#"network-status-version 3
1246vote-status vote
1247consensus-methods 25 26 27
1248published 2017-05-25 04:46:20
1249valid-after 2017-05-25 04:46:30
1250fresh-until 2017-05-25 04:46:40
1251valid-until 2017-05-25 04:46:50
1252"#;
1253        let doc = NetworkStatusDocument::parse(content).unwrap();
1254        assert!(!doc.is_consensus);
1255        assert!(doc.is_vote);
1256        assert!(doc.consensus_methods.is_some());
1257        assert_eq!(doc.consensus_methods.unwrap(), vec![25, 26, 27]);
1258        assert!(doc.published.is_some());
1259    }
1260
1261    #[test]
1262    fn test_edge_case_protocol_ranges() {
1263        let content = r#"network-status-version 3
1264vote-status consensus
1265valid-after 2017-05-25 04:46:30
1266fresh-until 2017-05-25 04:46:40
1267valid-until 2017-05-25 04:46:50
1268recommended-client-protocols Cons=1-2 Link=1-5 Relay=1-3
1269"#;
1270        let doc = NetworkStatusDocument::parse(content).unwrap();
1271        assert_eq!(
1272            doc.recommended_client_protocols.get("Cons"),
1273            Some(&vec![1, 2])
1274        );
1275        assert_eq!(
1276            doc.recommended_client_protocols.get("Link"),
1277            Some(&vec![1, 2, 3, 4, 5])
1278        );
1279        assert_eq!(
1280            doc.recommended_client_protocols.get("Relay"),
1281            Some(&vec![1, 2, 3])
1282        );
1283    }
1284
1285    #[test]
1286    fn test_edge_case_protocol_mixed_ranges() {
1287        let content = r#"network-status-version 3
1288vote-status consensus
1289valid-after 2017-05-25 04:46:30
1290fresh-until 2017-05-25 04:46:40
1291valid-until 2017-05-25 04:46:50
1292required-relay-protocols Link=1-3,5,7-9
1293"#;
1294        let doc = NetworkStatusDocument::parse(content).unwrap();
1295        let link_protos = doc.required_relay_protocols.get("Link").unwrap();
1296        assert!(link_protos.contains(&1));
1297        assert!(link_protos.contains(&2));
1298        assert!(link_protos.contains(&3));
1299        assert!(link_protos.contains(&5));
1300        assert!(link_protos.contains(&7));
1301        assert!(link_protos.contains(&8));
1302        assert!(link_protos.contains(&9));
1303        assert!(!link_protos.contains(&4));
1304        assert!(!link_protos.contains(&6));
1305    }
1306
1307    #[test]
1308    fn test_edge_case_params_negative_values() {
1309        let content = r#"network-status-version 3
1310vote-status consensus
1311valid-after 2017-05-25 04:46:30
1312fresh-until 2017-05-25 04:46:40
1313valid-until 2017-05-25 04:46:50
1314params param1=100 param2=-50 param3=0
1315"#;
1316        let doc = NetworkStatusDocument::parse(content).unwrap();
1317        assert_eq!(doc.params.get("param1"), Some(&100));
1318        assert_eq!(doc.params.get("param2"), Some(&-50));
1319        assert_eq!(doc.params.get("param3"), Some(&0));
1320    }
1321
1322    #[test]
1323    fn test_edge_case_bandwidth_weights_negative() {
1324        let content = r#"network-status-version 3
1325vote-status consensus
1326valid-after 2017-05-25 04:46:30
1327fresh-until 2017-05-25 04:46:40
1328valid-until 2017-05-25 04:46:50
1329directory-footer
1330bandwidth-weights Wbd=3333 Wbe=-100 Wbg=0
1331"#;
1332        let doc = NetworkStatusDocument::parse(content).unwrap();
1333        assert_eq!(doc.bandwidth_weights.get("Wbd"), Some(&3333));
1334        assert_eq!(doc.bandwidth_weights.get("Wbe"), Some(&-100));
1335        assert_eq!(doc.bandwidth_weights.get("Wbg"), Some(&0));
1336    }
1337
1338    #[test]
1339    fn test_edge_case_unrecognized_lines() {
1340        let content = r#"network-status-version 3
1341vote-status consensus
1342valid-after 2017-05-25 04:46:30
1343fresh-until 2017-05-25 04:46:40
1344valid-until 2017-05-25 04:46:50
1345unknown-field some value
1346another-unknown another value
1347"#;
1348        let doc = NetworkStatusDocument::parse(content).unwrap();
1349        assert_eq!(doc.unrecognized_lines.len(), 2);
1350        assert!(doc
1351            .unrecognized_lines
1352            .contains(&"unknown-field some value".to_string()));
1353        assert!(doc
1354            .unrecognized_lines
1355            .contains(&"another-unknown another value".to_string()));
1356    }
1357
1358    #[test]
1359    fn test_validation_invalid_timestamp_ordering() {
1360        let doc = NetworkStatusDocument {
1361            version: 3,
1362            version_flavor: String::new(),
1363            is_consensus: true,
1364            is_vote: false,
1365            is_microdescriptor: false,
1366            consensus_method: Some(26),
1367            consensus_methods: None,
1368            published: None,
1369            valid_after: Utc::now(),
1370            fresh_until: Utc::now() - chrono::Duration::hours(1),
1371            valid_until: Utc::now() + chrono::Duration::hours(1),
1372            vote_delay: Some(2),
1373            dist_delay: Some(2),
1374            client_versions: Vec::new(),
1375            server_versions: Vec::new(),
1376            known_flags: Vec::new(),
1377            recommended_client_protocols: HashMap::new(),
1378            recommended_relay_protocols: HashMap::new(),
1379            required_client_protocols: HashMap::new(),
1380            required_relay_protocols: HashMap::new(),
1381            params: HashMap::new(),
1382            shared_randomness_previous: None,
1383            shared_randomness_current: None,
1384            bandwidth_weights: HashMap::new(),
1385            authorities: Vec::new(),
1386            signatures: Vec::new(),
1387            raw_content: Vec::new(),
1388            unrecognized_lines: Vec::new(),
1389        };
1390        let result = doc.validate();
1391        assert!(result.is_err());
1392        match result.unwrap_err() {
1393            Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1394                crate::descriptor::ConsensusError::TimestampOrderingViolation(_),
1395            )) => {}
1396            _ => panic!("Expected TimestampOrderingViolation error"),
1397        }
1398    }
1399
1400    #[test]
1401    fn test_validation_invalid_version() {
1402        let doc = NetworkStatusDocument {
1403            version: 2,
1404            version_flavor: String::new(),
1405            is_consensus: true,
1406            is_vote: false,
1407            is_microdescriptor: false,
1408            consensus_method: Some(26),
1409            consensus_methods: None,
1410            published: None,
1411            valid_after: Utc::now(),
1412            fresh_until: Utc::now() + chrono::Duration::hours(1),
1413            valid_until: Utc::now() + chrono::Duration::hours(2),
1414            vote_delay: Some(2),
1415            dist_delay: Some(2),
1416            client_versions: Vec::new(),
1417            server_versions: Vec::new(),
1418            known_flags: Vec::new(),
1419            recommended_client_protocols: HashMap::new(),
1420            recommended_relay_protocols: HashMap::new(),
1421            required_client_protocols: HashMap::new(),
1422            required_relay_protocols: HashMap::new(),
1423            params: HashMap::new(),
1424            shared_randomness_previous: None,
1425            shared_randomness_current: None,
1426            bandwidth_weights: HashMap::new(),
1427            authorities: Vec::new(),
1428            signatures: Vec::new(),
1429            raw_content: Vec::new(),
1430            unrecognized_lines: Vec::new(),
1431        };
1432        let result = doc.validate();
1433        assert!(result.is_err());
1434        match result.unwrap_err() {
1435            Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1436                crate::descriptor::ConsensusError::InvalidNetworkStatusVersion(_),
1437            )) => {}
1438            _ => panic!("Expected InvalidNetworkStatusVersion error"),
1439        }
1440    }
1441
1442    #[test]
1443    fn test_validation_invalid_authority_fingerprint() {
1444        let doc = NetworkStatusDocument {
1445            version: 3,
1446            version_flavor: String::new(),
1447            is_consensus: true,
1448            is_vote: false,
1449            is_microdescriptor: false,
1450            consensus_method: Some(26),
1451            consensus_methods: None,
1452            published: None,
1453            valid_after: Utc::now(),
1454            fresh_until: Utc::now() + chrono::Duration::hours(1),
1455            valid_until: Utc::now() + chrono::Duration::hours(2),
1456            vote_delay: Some(2),
1457            dist_delay: Some(2),
1458            client_versions: Vec::new(),
1459            server_versions: Vec::new(),
1460            known_flags: Vec::new(),
1461            recommended_client_protocols: HashMap::new(),
1462            recommended_relay_protocols: HashMap::new(),
1463            required_client_protocols: HashMap::new(),
1464            required_relay_protocols: HashMap::new(),
1465            params: HashMap::new(),
1466            shared_randomness_previous: None,
1467            shared_randomness_current: None,
1468            bandwidth_weights: HashMap::new(),
1469            authorities: vec![DirectoryAuthority {
1470                nickname: "test".to_string(),
1471                v3ident: "INVALID".to_string(),
1472                hostname: "test.test".to_string(),
1473                address: "127.0.0.1".parse().unwrap(),
1474                dir_port: Some(7000),
1475                or_port: 5000,
1476                is_legacy: false,
1477                contact: None,
1478                vote_digest: None,
1479                legacy_dir_key: None,
1480                key_certificate: None,
1481                is_shared_randomness_participate: false,
1482                shared_randomness_commitments: Vec::new(),
1483                shared_randomness_previous_reveal_count: None,
1484                shared_randomness_previous_value: None,
1485                shared_randomness_current_reveal_count: None,
1486                shared_randomness_current_value: None,
1487                raw_content: Vec::new(),
1488                unrecognized_lines: Vec::new(),
1489            }],
1490            signatures: Vec::new(),
1491            raw_content: Vec::new(),
1492            unrecognized_lines: Vec::new(),
1493        };
1494        let result = doc.validate();
1495        assert!(result.is_err());
1496        match result.unwrap_err() {
1497            Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1498                crate::descriptor::ConsensusError::InvalidFingerprint(_),
1499            )) => {}
1500            _ => panic!("Expected InvalidFingerprint error"),
1501        }
1502    }
1503
1504    #[test]
1505    fn test_validation_missing_signatures() {
1506        let doc = NetworkStatusDocument {
1507            version: 3,
1508            version_flavor: String::new(),
1509            is_consensus: true,
1510            is_vote: false,
1511            is_microdescriptor: false,
1512            consensus_method: Some(26),
1513            consensus_methods: None,
1514            published: None,
1515            valid_after: Utc::now(),
1516            fresh_until: Utc::now() + chrono::Duration::hours(1),
1517            valid_until: Utc::now() + chrono::Duration::hours(2),
1518            vote_delay: Some(2),
1519            dist_delay: Some(2),
1520            client_versions: Vec::new(),
1521            server_versions: Vec::new(),
1522            known_flags: Vec::new(),
1523            recommended_client_protocols: HashMap::new(),
1524            recommended_relay_protocols: HashMap::new(),
1525            required_client_protocols: HashMap::new(),
1526            required_relay_protocols: HashMap::new(),
1527            params: HashMap::new(),
1528            shared_randomness_previous: None,
1529            shared_randomness_current: None,
1530            bandwidth_weights: HashMap::new(),
1531            authorities: Vec::new(),
1532            signatures: Vec::new(),
1533            raw_content: Vec::new(),
1534            unrecognized_lines: Vec::new(),
1535        };
1536        let result = doc.validate();
1537        assert!(result.is_err());
1538        match result.unwrap_err() {
1539            Error::Descriptor(crate::descriptor::DescriptorError::Consensus(
1540                crate::descriptor::ConsensusError::MissingRequiredField(_),
1541            )) => {}
1542            _ => panic!("Expected MissingRequiredField error"),
1543        }
1544    }
1545
1546    #[test]
1547    fn test_validation_valid_consensus() {
1548        let doc = NetworkStatusDocument {
1549            version: 3,
1550            version_flavor: String::new(),
1551            is_consensus: true,
1552            is_vote: false,
1553            is_microdescriptor: false,
1554            consensus_method: Some(26),
1555            consensus_methods: None,
1556            published: None,
1557            valid_after: Utc::now(),
1558            fresh_until: Utc::now() + chrono::Duration::hours(1),
1559            valid_until: Utc::now() + chrono::Duration::hours(2),
1560            vote_delay: Some(2),
1561            dist_delay: Some(2),
1562            client_versions: Vec::new(),
1563            server_versions: Vec::new(),
1564            known_flags: Vec::new(),
1565            recommended_client_protocols: HashMap::new(),
1566            recommended_relay_protocols: HashMap::new(),
1567            required_client_protocols: HashMap::new(),
1568            required_relay_protocols: HashMap::new(),
1569            params: HashMap::new(),
1570            shared_randomness_previous: None,
1571            shared_randomness_current: None,
1572            bandwidth_weights: HashMap::new(),
1573            authorities: vec![DirectoryAuthority {
1574                nickname: "test".to_string(),
1575                v3ident: "596CD48D61FDA4E868F4AA10FF559917BE3B1A35".to_string(),
1576                hostname: "test.test".to_string(),
1577                address: "127.0.0.1".parse().unwrap(),
1578                dir_port: Some(7000),
1579                or_port: 5000,
1580                is_legacy: false,
1581                contact: None,
1582                vote_digest: None,
1583                legacy_dir_key: None,
1584                key_certificate: None,
1585                is_shared_randomness_participate: false,
1586                shared_randomness_commitments: Vec::new(),
1587                shared_randomness_previous_reveal_count: None,
1588                shared_randomness_previous_value: None,
1589                shared_randomness_current_reveal_count: None,
1590                shared_randomness_current_value: None,
1591                raw_content: Vec::new(),
1592                unrecognized_lines: Vec::new(),
1593            }],
1594            signatures: vec![DocumentSignature {
1595                identity: "596CD48D61FDA4E868F4AA10FF559917BE3B1A35".to_string(),
1596                signing_key_digest: "9FBF54D6A62364320308A615BF4CF6B27B254FAD".to_string(),
1597                signature: "test".to_string(),
1598                algorithm: None,
1599            }],
1600            raw_content: Vec::new(),
1601            unrecognized_lines: Vec::new(),
1602        };
1603        let result = doc.validate();
1604        assert!(result.is_ok());
1605    }
1606
1607    #[test]
1608    fn test_builder_basic() {
1609        let now = Utc::now();
1610        let doc = NetworkStatusDocumentBuilder::default()
1611            .version(3_u32)
1612            .version_flavor("")
1613            .is_consensus(true)
1614            .is_vote(false)
1615            .is_microdescriptor(false)
1616            .valid_after(now)
1617            .fresh_until(now + chrono::Duration::hours(1))
1618            .valid_until(now + chrono::Duration::hours(2))
1619            .build()
1620            .expect("Failed to build");
1621
1622        assert_eq!(doc.version, 3);
1623        assert!(doc.is_consensus);
1624        assert!(!doc.is_vote);
1625    }
1626
1627    #[test]
1628    fn test_builder_with_optional_fields() {
1629        let now = Utc::now();
1630        let doc = NetworkStatusDocumentBuilder::default()
1631            .version(3_u32)
1632            .version_flavor("microdesc")
1633            .is_consensus(true)
1634            .is_vote(false)
1635            .is_microdescriptor(true)
1636            .consensus_method(26_u32)
1637            .valid_after(now)
1638            .fresh_until(now + chrono::Duration::hours(1))
1639            .valid_until(now + chrono::Duration::hours(2))
1640            .vote_delay(2_u32)
1641            .dist_delay(2_u32)
1642            .build()
1643            .expect("Failed to build");
1644
1645        assert_eq!(doc.version_flavor, "microdesc");
1646        assert!(doc.is_microdescriptor);
1647        assert_eq!(doc.consensus_method, Some(26));
1648        assert_eq!(doc.vote_delay, Some(2));
1649        assert_eq!(doc.dist_delay, Some(2));
1650    }
1651
1652    #[test]
1653    fn test_round_trip_serialization() {
1654        let content = r#"network-status-version 3
1655vote-status consensus
1656consensus-method 26
1657valid-after 2017-05-25 04:46:30
1658fresh-until 2017-05-25 04:46:40
1659valid-until 2017-05-25 04:46:50
1660voting-delay 2 2
1661known-flags Authority Exit Fast Guard
1662"#;
1663        let doc1 = NetworkStatusDocument::parse(content).unwrap();
1664        let serialized = doc1.to_descriptor_string();
1665        let doc2 = NetworkStatusDocument::parse(&serialized).unwrap();
1666
1667        assert_eq!(doc1.version, doc2.version);
1668        assert_eq!(doc1.is_consensus, doc2.is_consensus);
1669        assert_eq!(doc1.consensus_method, doc2.consensus_method);
1670        assert_eq!(doc1.vote_delay, doc2.vote_delay);
1671        assert_eq!(doc1.dist_delay, doc2.dist_delay);
1672        assert_eq!(doc1.known_flags, doc2.known_flags);
1673    }
1674
1675    #[test]
1676    fn test_display_implementation() {
1677        let now = Utc::now();
1678        let doc = NetworkStatusDocument {
1679            version: 3,
1680            version_flavor: String::new(),
1681            is_consensus: true,
1682            is_vote: false,
1683            is_microdescriptor: false,
1684            consensus_method: Some(26),
1685            consensus_methods: None,
1686            published: None,
1687            valid_after: now,
1688            fresh_until: now + chrono::Duration::hours(1),
1689            valid_until: now + chrono::Duration::hours(2),
1690            vote_delay: Some(2),
1691            dist_delay: Some(2),
1692            client_versions: Vec::new(),
1693            server_versions: Vec::new(),
1694            known_flags: Vec::new(),
1695            recommended_client_protocols: HashMap::new(),
1696            recommended_relay_protocols: HashMap::new(),
1697            required_client_protocols: HashMap::new(),
1698            required_relay_protocols: HashMap::new(),
1699            params: HashMap::new(),
1700            shared_randomness_previous: None,
1701            shared_randomness_current: None,
1702            bandwidth_weights: HashMap::new(),
1703            authorities: Vec::new(),
1704            signatures: Vec::new(),
1705            raw_content: Vec::new(),
1706            unrecognized_lines: Vec::new(),
1707        };
1708        let display_str = format!("{}", doc);
1709        assert!(display_str.contains("network-status-version"));
1710        assert!(display_str.contains("vote-status"));
1711    }
1712
1713    #[test]
1714    fn test_from_str_implementation() {
1715        let content = r#"network-status-version 3
1716vote-status consensus
1717valid-after 2017-05-25 04:46:30
1718fresh-until 2017-05-25 04:46:40
1719valid-until 2017-05-25 04:46:50
1720"#;
1721        let doc: NetworkStatusDocument = content.parse().unwrap();
1722        assert_eq!(doc.version, 3);
1723        assert!(doc.is_consensus);
1724    }
1725}