Skip to main content

tor_netdoc/doc/
netstatus.rs

1//! Parsing implementation for networkstatus documents.
2//!
3//! In Tor, a networkstatus documents describes a complete view of the
4//! relays in the network: how many there are, how to contact them,
5//! and so forth.
6//!
7//! A networkstatus document can either be a "votes" -- an authority's
8//! view of the network, used as input to the voting process -- or a
9//! "consensus" -- a combined view of the network based on multiple
10//! authorities' votes, and signed by multiple authorities.
11//!
12//! A consensus document can itself come in two different flavors: a
13//! plain (unflavoured) consensus has references to router descriptors, and
14//! a "microdesc"-flavored consensus ("md") has references to
15//! microdescriptors.
16//!
17//! To keep an up-to-date view of the network, clients download
18//! microdescriptor-flavored consensuses periodically, and then
19//! download whatever microdescriptors the consensus lists that the
20//! client doesn't already have.
21//!
22//! For full information about the network status format, see
23//! [dir-spec.txt](https://spec.torproject.org/dir-spec).
24//!
25//! # Limitations
26//!
27//! NOTE: The consensus format has changes time, using a
28//! "consensus-method" mechanism.  This module is does not yet handle all
29//! all historical consensus-methods.
30//!
31//! NOTE: This module _does_ parse some fields that are not in current
32//! use, like relay nicknames, and the "published" times on
33//! microdescriptors. We should probably decide whether we actually
34//! want to do this.
35//!
36//! TODO: This module doesn't implement vote parsing at all yet.
37//!
38//! TODO: This module doesn't implement plain consensuses.
39//!
40//! TODO: We need an object safe trait that combines the common operations found
41//! on netstatus documents, so we can store one in a `Box<dyn CommonNs>` or
42//! something similar; otherwise interfacing applications have a hard time to
43//! process netstatus documents in a flavor agnostic fashion.
44//!
45//! TODO: More testing is needed!
46//!
47//! TODO: There should be accessor functions for most of the fields here.
48//! As with the other tor-netdoc types, I'm deferring those till I know what
49//! they should be.
50
51mod dir_source;
52mod rs;
53
54pub mod md;
55pub mod plain;
56#[cfg(feature = "incomplete")]
57pub mod vote;
58
59#[cfg(feature = "build_docs")]
60mod build;
61
62pub use proto_statuses_parse2_encode::ProtoStatusesNetdocParseAccumulator;
63
64#[cfg(feature = "incomplete")]
65use crate::doc::authcert::EncodedAuthCert;
66
67use crate::doc::authcert::{AuthCert, AuthCertKeyIds};
68use crate::encode::{ItemValueEncodable, NetdocEncodable, NetdocEncoder};
69use crate::parse::keyword::Keyword;
70use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
71use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
72use crate::parse2::{
73    self, ArgumentStream, ErrorProblem, IsStructural, ItemStream, ItemValueParseable, KeywordRef,
74    NetdocParseable, StopAt,
75};
76use crate::types::misc::*;
77use crate::types::relay_flags::{self, DocRelayFlags};
78use crate::util::PeekableIterator;
79use crate::{Error, KeywordEncodable, NetdocErrorKind as EK, NormalItemArgument, Pos, Result};
80use std::collections::{BTreeSet, HashMap, HashSet};
81use std::fmt::{self, Display};
82use std::result::Result as StdResult;
83use std::str::FromStr;
84use std::sync::Arc;
85use std::{net, result, time};
86use tor_error::{Bug, HasKind, bad_api_usage, internal};
87use tor_protover::Protocols;
88use void::ResultVoidExt as _;
89
90use derive_deftly::{Deftly, define_derive_deftly};
91use digest::Digest;
92use std::sync::LazyLock;
93use tor_checkable::{ExternallySigned, timed::TimerangeBound};
94use tor_llcrypto as ll;
95use tor_llcrypto::pk::rsa::RsaIdentity;
96
97use serde::{Deserialize, Deserializer};
98
99#[cfg(feature = "build_docs")]
100pub use build::MdConsensusBuilder;
101#[cfg(feature = "build_docs")]
102pub use build::PlainConsensusBuilder;
103#[cfg(feature = "build_docs")]
104ns_export_each_flavor! {
105    ty: RouterStatusBuilder;
106}
107
108ns_export_each_variety! {
109    ty: RouterStatus, Preamble;
110}
111
112#[deprecated]
113pub use PlainConsensus as NsConsensus;
114#[deprecated]
115pub use PlainRouterStatus as NsRouterStatus;
116#[deprecated]
117pub use UncheckedPlainConsensus as UncheckedNsConsensus;
118#[deprecated]
119pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
120
121pub use rs::{RouterStatusMdDigestsVote, SoftwareVersion};
122
123pub use dir_source::{ConsensusAuthoritySection, DirSource, SupersededAuthorityKey};
124
125/// `publiscation` field in routerstatus entry intro item other than in votes
126///
127/// Two arguments which are both ignored.
128/// This used to be an ISO8601 timestamp in anomalous two-argument format.
129///
130/// Nowadays, according to the spec, it can be a dummy value.
131/// So it can be a unit type.
132///
133/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:r>,
134/// except in votes which use [`Iso8601TimeSp`] instead.
135///
136/// **Not the same as** the `published` item:
137/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
138#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
139#[allow(clippy::exhaustive_structs)]
140pub struct IgnoredPublicationTimeSp;
141
142/// The lifetime of a networkstatus document.
143///
144/// In a consensus, this type describes when the consensus may safely
145/// be used.  In a vote, this type describes the proposed lifetime for a
146/// consensus.
147///
148/// Aggregate of three netdoc preamble fields.
149#[derive(Clone, Debug, Deftly)]
150#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
151#[derive_deftly(Lifetime)]
152#[allow(clippy::exhaustive_structs)]
153pub struct Lifetime {
154    /// `valid-after` --- Time at which the document becomes valid
155    ///
156    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
157    ///
158    /// (You might see a consensus a little while before this time,
159    /// since voting tries to finish up before the.)
160    #[deftly(constructor)]
161    #[deftly(netdoc(single_arg))]
162    pub valid_after: Iso8601TimeSp,
163    /// `fresh-until` --- Time after which there is expected to be a better version
164    /// of this consensus
165    ///
166    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
167    ///
168    /// You can use the consensus after this time, but there is (or is
169    /// supposed to be) a better one by this point.
170    #[deftly(constructor)]
171    #[deftly(netdoc(single_arg))]
172    pub fresh_until: Iso8601TimeSp,
173    /// `valid-until` --- Time after which this consensus is expired.
174    ///
175    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
176    ///
177    /// You should try to get a better consensus after this time,
178    /// though it's okay to keep using this one if no more recent one
179    /// can be found.
180    #[deftly(constructor)]
181    #[deftly(netdoc(single_arg))]
182    pub valid_until: Iso8601TimeSp,
183
184    #[doc(hidden)]
185    #[deftly(netdoc(skip))]
186    pub __non_exhaustive: (),
187}
188
189define_derive_deftly! {
190    /// Bespoke derive for `Lifetime`, for `new` and accessors
191    Lifetime:
192
193    ${defcond FIELD not(approx_equal($fname, __non_exhaustive))}
194
195    impl Lifetime {
196        /// Construct a new Lifetime.
197        pub fn new(
198            $( ${when FIELD} $fname: time::SystemTime, )
199        ) -> Result<Self> {
200            // Make this now because otherwise literal `valid_after` here in the body
201            // has the wrong span - the compiler refuses to look at the argument.
202            // But we can refer to the field names.
203            let self_ = Lifetime {
204                $( ${when FIELD} $fname: $fname.into(), )
205                __non_exhaustive: (),
206            };
207            if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
208                Ok(self_)
209            } else {
210                Err(EK::InvalidLifetime.err())
211            }
212        }
213      $(
214        ${when FIELD}
215
216        ${fattrs doc}
217        pub fn $fname(&self) -> time::SystemTime {
218            *self.$fname
219        }
220      )
221        /// Return true if this consensus is officially valid at the provided time.
222        pub fn valid_at(&self, when: time::SystemTime) -> bool {
223            *self.valid_after <= when && when <= *self.valid_until
224        }
225
226        /// Return the voting period implied by this lifetime.
227        ///
228        /// (The "voting period" is the amount of time in between when a consensus first
229        /// becomes valid, and when the next consensus is expected to become valid)
230        pub fn voting_period(&self) -> time::Duration {
231            let valid_after = self.valid_after();
232            let fresh_until = self.fresh_until();
233            fresh_until
234                .duration_since(valid_after)
235                .expect("Mis-formed lifetime")
236        }
237    }
238}
239use derive_deftly_template_Lifetime;
240
241/// A single consensus method
242///
243/// These are integers, but we don't do arithmetic on them.
244///
245/// As defined here:
246/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
247/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavor:microdesc>
248///
249/// As used in a `consensus-method` item:
250/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-method>
251#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] //
252#[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
253pub struct ConsensusMethod(u32);
254impl NormalItemArgument for ConsensusMethod {}
255
256/// A set of consensus methods
257///
258/// Implements `ItemValueParseable` as required for `consensus-methods`,
259/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
260///
261/// There is also [`consensus_methods_comma_separated`] for `m` lines in votes.
262#[derive(Debug, Clone, Default, Eq, PartialEq, Deftly)]
263#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
264#[non_exhaustive]
265pub struct ConsensusMethods {
266    /// Consensus methods.
267    pub methods: BTreeSet<ConsensusMethod>,
268}
269
270/// Module for use with parse2's `with`, to parse one argument of comma-separated consensus methods
271///
272/// As found in an `m` item in a vote:
273/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:m>
274pub mod consensus_methods_comma_separated {
275    use super::*;
276    use parse2::ArgumentError as AE;
277    use std::result::Result;
278
279    /// Parse
280    pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
281        let mut methods = BTreeSet::new();
282        for ent in args.next().ok_or(AE::Missing)?.split(',') {
283            let ent = ent.parse().map_err(|_| AE::Invalid)?;
284            if !methods.insert(ent) {
285                return Err(AE::Invalid);
286            }
287        }
288        Ok(ConsensusMethods { methods })
289    }
290}
291
292/// A set of named network parameters.
293///
294/// These are used to describe current settings for the Tor network,
295/// current weighting parameters for path selection, and so on.  They're
296/// encoded with a space-separated K=V format.
297///
298/// A `NetParams<i32>` is part of the validated directory manager configuration,
299/// where it is built (in the builder-pattern sense) from a transparent HashMap.
300///
301/// As found in `params` in a network status:
302/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:params>
303///
304/// The same syntax is also used, and this type used for parsing, in various other places,
305/// for example routerstatus entry `w` items (bandwidth weights):
306/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:w>
307//
308// TODO DIRAUTH torspec#401 Replace `String` with a suitable newtype
309// Currently:
310//  - Our parser allows any keyword that makes it into a netdoc argument,
311//    but it splits on the *first* `=` so a `NetParams<i32>` cannot parse a keyword with a `=`.
312//  - We provide constructors that allow any `String`, even ones containing space, `=`,
313//    newline, etc.
314//  - Encoding throws `Bug` if the resulting document will be clearly garbage,
315//    forbidding `=`, whitespace, and controls.  If the supplied keywords are bizarre,
316//    it may generate surprising documents (eg, containing exciting Unicode).
317#[derive(Debug, Clone, Default, Eq, PartialEq)]
318pub struct NetParams<T> {
319    /// Map from keys to values.
320    params: HashMap<String, T>,
321}
322
323impl<T> NetParams<T> {
324    /// Create a new empty list of NetParams.
325    #[allow(unused)]
326    pub fn new() -> Self {
327        NetParams {
328            params: HashMap::new(),
329        }
330    }
331    /// Retrieve a given network parameter, if it is present.
332    pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
333        self.params.get(v.as_ref())
334    }
335    /// Return an iterator over all key value pairs in an arbitrary order.
336    pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
337        self.params.iter()
338    }
339    /// Set or replace the value of a network parameter.
340    pub fn set(&mut self, k: String, v: T) {
341        self.params.insert(k, v);
342    }
343}
344
345impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
346    fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
347        NetParams {
348            params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
349        }
350    }
351}
352
353impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
354    fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
355        self.params.extend(iter);
356    }
357}
358
359impl<'de, T> Deserialize<'de> for NetParams<T>
360where
361    T: Deserialize<'de>,
362{
363    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
364    where
365        D: Deserializer<'de>,
366    {
367        let params = HashMap::deserialize(deserializer)?;
368        Ok(NetParams { params })
369    }
370}
371
372/// A list of subprotocol versions that implementors should/must provide.
373///
374/// This struct represents a pair of (optional) items:
375/// `recommended-FOO-protocols` and `required-FOO-protocols`.
376///
377/// Each consensus has two of these: one for relays, and one for clients.
378///
379/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
380#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
381pub struct ProtoStatus {
382    /// Set of protocols that are recommended; if we're missing a protocol
383    /// in this list we should warn the user.
384    ///
385    /// `recommended-client-protocols` or `recommended-relay-protocols`
386    recommended: Protocols,
387    /// Set of protocols that are required; if we're missing a protocol
388    /// in this list we should refuse to start.
389    ///
390    /// `required-client-protocols` or `required-relay-protocols`
391    required: Protocols,
392}
393
394impl ProtoStatus {
395    /// Check whether the list of supported protocols
396    /// is sufficient to satisfy this list of recommendations and requirements.
397    ///
398    /// If any required protocol is missing, returns [`ProtocolSupportError::MissingRequired`].
399    ///
400    /// Otherwise, if no required protocol is missing, but some recommended protocol is missing,
401    /// returns [`ProtocolSupportError::MissingRecommended`].
402    ///
403    /// Otherwise, if no recommended or required protocol is missing, returns `Ok(())`.
404    pub fn check_protocols(
405        &self,
406        supported_protocols: &Protocols,
407    ) -> StdResult<(), ProtocolSupportError> {
408        // Required protocols take precedence, so we check them first.
409        let missing_required = self.required.difference(supported_protocols);
410        if !missing_required.is_empty() {
411            return Err(ProtocolSupportError::MissingRequired(missing_required));
412        }
413        let missing_recommended = self.recommended.difference(supported_protocols);
414        if !missing_recommended.is_empty() {
415            return Err(ProtocolSupportError::MissingRecommended(
416                missing_recommended,
417            ));
418        }
419
420        Ok(())
421    }
422}
423
424/// A subprotocol that is recommended or required in the consensus was not present.
425#[derive(Clone, Debug, thiserror::Error)]
426#[cfg_attr(test, derive(PartialEq))]
427#[non_exhaustive]
428pub enum ProtocolSupportError {
429    /// At least one required protocol was not in our list of supported protocols.
430    #[error("Required protocols are not implemented: {0}")]
431    MissingRequired(Protocols),
432
433    /// At least one recommended protocol was not in our list of supported protocols.
434    ///
435    /// Also implies that no _required_ protocols were missing.
436    #[error("Recommended protocols are not implemented: {0}")]
437    MissingRecommended(Protocols),
438}
439
440impl ProtocolSupportError {
441    /// Return true if the suggested behavior for this error is a shutdown.
442    pub fn should_shutdown(&self) -> bool {
443        matches!(self, Self::MissingRequired(_))
444    }
445}
446
447impl HasKind for ProtocolSupportError {
448    fn kind(&self) -> tor_error::ErrorKind {
449        tor_error::ErrorKind::SoftwareDeprecated
450    }
451}
452
453/// A set of recommended and required protocols when running
454/// in various scenarios.
455///
456/// Represents the collection of four items: `{recommended,required}-{client,relay}-protocols`.
457///
458/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
459#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
460pub struct ProtoStatuses {
461    /// Lists of recommended and required subprotocol versions for clients
462    client: ProtoStatus,
463    /// Lists of recommended and required subprotocol versions for relays
464    relay: ProtoStatus,
465}
466
467impl ProtoStatuses {
468    /// Return the list of recommended and required protocols for running as a client.
469    pub fn client(&self) -> &ProtoStatus {
470        &self.client
471    }
472
473    /// Return the list of recommended and required protocols for running as a relay.
474    pub fn relay(&self) -> &ProtoStatus {
475        &self.relay
476    }
477}
478
479/// A recognized 'flavor' of consensus document.
480///
481/// The enum is exhaustive because the addition/removal of a consensus flavor
482/// should indeed be a breaking change, as it would inevitable require
483/// interfacing code to think about the handling of it.
484///
485/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavors>
486#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
487#[allow(clippy::exhaustive_enums)]
488pub enum ConsensusFlavor {
489    /// A "microdesc"-flavored consensus.  This is the one that
490    /// clients and relays use today.
491    Microdesc,
492    /// A "networkstatus"-flavored consensus.  It's used for
493    /// historical and network-health purposes.  Instead of listing
494    /// microdescriptor digests, it lists digests of full relay
495    /// descriptors.
496    Plain,
497}
498
499impl ConsensusFlavor {
500    /// Return the name of this consensus flavor.
501    pub fn name(&self) -> &'static str {
502        match self {
503            ConsensusFlavor::Plain => "ns", // spec bug, now baked in
504            ConsensusFlavor::Microdesc => "microdesc",
505        }
506    }
507    /// Try to find the flavor whose name is `name`.
508    ///
509    /// For historical reasons, an unnamed flavor indicates an "Plain"
510    /// document.
511    pub fn from_opt_name(name: Option<&str>) -> Result<Self> {
512        match name {
513            Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
514            Some("ns") | None => Ok(ConsensusFlavor::Plain),
515            Some(other) => {
516                Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
517            }
518        }
519    }
520}
521
522define_directory_signature_hash_algo! {
523    #[derive_deftly_adhoc] // TODO DIRAUTH; suppresses complaints about attrs used only in poc
524}
525
526/// The signature of a single directory authority on a networkstatus document.
527#[derive(Debug, Clone)]
528#[non_exhaustive]
529pub struct Signature {
530    /// The name of the digest algorithm used to make the signature.
531    ///
532    /// Currently sha1 and sh256 are recognized.  Here we only support
533    /// sha256.
534    pub digest_algo: KeywordOrString<DirectorySignatureHashAlgo>,
535    /// Fingerprints of the keys for the authority that made
536    /// this signature.
537    pub key_ids: AuthCertKeyIds,
538    /// The signature itself.
539    pub signature: Vec<u8>,
540}
541
542/// A collection of signatures that can be checked on a networkstatus document
543#[derive(Debug, Clone)]
544#[non_exhaustive]
545pub struct SignatureGroup {
546    /// The sha256 of the document itself
547    pub sha256: Option<[u8; 32]>,
548    /// The sha1 of the document itself
549    pub sha1: Option<[u8; 20]>,
550    /// The signatures listed on the document.
551    pub signatures: Vec<Signature>,
552}
553
554/// A shared random value produced by the directory authorities.
555#[derive(
556    Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
557)]
558// (This doesn't need to use CtByteArray; we don't really need to compare these.)
559pub struct SharedRandVal([u8; 32]);
560
561/// A shared-random value produced by the directory authorities,
562/// along with meta-information about that value.
563#[derive(Debug, Clone, Deftly)]
564#[non_exhaustive]
565#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
566pub struct SharedRandStatus {
567    /// How many authorities revealed shares that contributed to this value.
568    pub n_reveals: u8,
569    /// The current random value.
570    ///
571    /// The properties of the secure shared-random system guarantee
572    /// that this value isn't predictable before it first becomes
573    /// live, and that a hostile party could not have forced it to
574    /// have any more than a small number of possible random values.
575    pub value: SharedRandVal,
576
577    /// The time when this SharedRandVal becomes (or became) the latest.
578    ///
579    /// (This is added per proposal 342, assuming that gets accepted.)
580    pub timestamp: Option<Iso8601TimeNoSp>,
581}
582
583/// Recognized weight fields on a single relay in a consensus
584#[non_exhaustive]
585#[derive(Debug, Clone, Copy)]
586pub enum RelayWeight {
587    /// An unmeasured weight for a relay.
588    Unmeasured(u32),
589    /// An measured weight for a relay.
590    Measured(u32),
591}
592
593impl RelayWeight {
594    /// Return true if this weight is the result of a successful measurement
595    pub fn is_measured(&self) -> bool {
596        matches!(self, RelayWeight::Measured(_))
597    }
598    /// Return true if this weight is nonzero
599    pub fn is_nonzero(&self) -> bool {
600        !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
601    }
602}
603
604/// Authority entry in a consensus - deprecated compatibility type alias
605#[deprecated = "renamed to ConsensusAuthorityEntry"]
606pub type ConsensusVoterInfo = ConsensusAuthorityEntry;
607
608/// Authority entry in a plain consensus - type alias provided for consistency
609pub type PlainAuthorityEntry = ConsensusAuthorityEntry;
610/// Authority entry in an md consensus - type alias provided for consistency
611pub type MdAuthorityEntry = ConsensusAuthorityEntry;
612
613/// An authority entry as found in a consensus
614///
615/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
616///
617/// See also [`VoteAuthorityEntry`]
618//
619// We don't use the `each_variety` system for this because:
620//  1. That avoids separating the two consensus authority entry types, which are identical
621//  2. The only common fields are `dir-source` and `contact`, so there is little duplication
622#[derive(Debug, Clone, Deftly)]
623#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
624#[allow(clippy::exhaustive_structs)]
625pub struct ConsensusAuthorityEntry {
626    /// Contents of the `dir-source` line about an authority
627    #[deftly(constructor)]
628    pub dir_source: DirSource,
629
630    /// Human-readable contact information about the authority
631    //
632    // If more non-intro fields get added that are the same in votes and cosensuses,
633    // consider using each_variety.rs or breaking those fields out into
634    // `AuthorityEntryCommon` implementing `NetdocParseableFields`, or something.
635    #[deftly(constructor)]
636    pub contact: ContactInfo,
637
638    /// Digest of the vote that the authority cast to contribute to
639    /// this consensus.
640    ///
641    /// This is not a fixed-length, fixed-algorithm field.
642    /// Bizarrely, the algorithm is supposed to be inferred from the length!
643    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-digest>
644    #[deftly(netdoc(single_arg))]
645    #[deftly(constructor)]
646    pub vote_digest: B16U,
647
648    #[doc(hidden)]
649    #[deftly(netdoc(skip))]
650    pub __non_exhaustive: (),
651}
652
653/// An authority entry as found in a vote
654///
655/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
656///
657/// See also [`ConsensusAuthorityEntry`]
658///
659/// TODO DIRAUTH not all fields are here yet.
660// They have individual comments, below.
661#[derive(Debug, Clone, Deftly)]
662#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
663#[allow(clippy::exhaustive_structs)]
664pub struct VoteAuthorityEntry {
665    /// Contents of the `dir-source` line about an authority
666    #[deftly(constructor)]
667    pub dir_source: DirSource,
668
669    /// Human-readable contact information about the authority
670    #[deftly(constructor)]
671    pub contact: ContactInfo,
672
673    // TODO DIRAUTH missing field legacy-dir-key
674    // TODO DIRAUTH missing field shared-rand-participate
675    // TODO DIRAUTH missing field shared-rand-commit
676    // TODO DIRAUTH missing field shared-rand-previous-value
677    // TODO DIRAUTH missing field shared-rand-current-value
678    //
679    #[doc(hidden)]
680    #[deftly(netdoc(skip))]
681    pub __non_exhaustive: (),
682}
683
684// For `ConsensusAuthoritySection`, see `dir_source.rs`.
685
686define_derive_deftly! {
687    /// Ad-hoc derive, `impl NetdocParseable for VoteAuthoritySection`
688    ///
689    /// We can't derive from `VoteAuthoritySection` with the normal macros, because
690    /// it's not a document, with its own intro item.  It's just a collection of sub-documents.
691    /// The netdoc derive macros don't have support for that - and it would be a fairly
692    /// confusing thing to support because you'd end up with nested multiplicities and a whole
693    /// variety of "intro item keywords" that were keywords for arbitrary sub-documents.
694    ///
695    /// Instead, we do that ad-hoc here.  It's less confusing because we don't need to
696    /// worry about multiplicity, and because we know what only the outer document is
697    /// that will contain this.
698    VoteAuthoritySection:
699
700    ${defcond F_NORMAL not(fmeta(netdoc(skip)))}
701
702    #[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
703    impl NetdocParseable for VoteAuthoritySection {
704        fn doctype_for_error() -> &'static str {
705            "vote.authority.section"
706        }
707        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
708            VoteAuthorityEntry::is_intro_item_keyword(kw)
709        }
710        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
711          $(
712            ${when F_NORMAL}
713            if let y @ Some(_) = $ftype::is_structural_keyword(kw) {
714                return y;
715            }
716          )
717            None
718        }
719        fn from_items<'s>(
720            input: &mut ItemStream<'s>,
721            stop_outer: stop_at!(),
722        ) -> StdResult<Self, ErrorProblem> {
723            let stop_inner = stop_outer
724              $(
725                ${when F_NORMAL}
726                | StopAt($ftype::is_intro_item_keyword)
727              )
728            ;
729            Ok(VoteAuthoritySection { $(
730                ${when F_NORMAL}
731                $fname: NetdocParseable::from_items(input, stop_inner)?,
732            )
733                __non_exhaustive: (),
734            })
735        }
736    }
737
738    #[cfg(feature = "incomplete")]
739    impl NetdocEncodable for VoteAuthoritySection {
740        fn encode_unsigned(&self, out: &mut NetdocEncoder) -> StdResult<(), Bug> {
741          $(
742            ${when F_NORMAL}
743            self.$fname.encode_unsigned(out)?;
744          )
745          Ok(())
746        }
747    }
748}
749
750/// An authority section in a vote
751///
752/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
753//
754// We have split this out to help encapsulate vote/consensus-specific
755// information in a forthcoming overall network status document type.
756#[derive(Deftly, Clone, Debug)]
757#[derive_deftly(VoteAuthoritySection, Constructor)]
758#[allow(clippy::exhaustive_structs)]
759#[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
760pub struct VoteAuthoritySection {
761    /// Authority entry
762    #[deftly(constructor)]
763    pub authority: VoteAuthorityEntry,
764
765    /// Authority key certificate
766    #[deftly(constructor)]
767    pub cert: EncodedAuthCert,
768
769    #[doc(hidden)]
770    #[deftly(netdoc(skip))]
771    pub __non_exhaustive: (),
772}
773
774/// The signed footer of a consensus netstatus.
775#[derive(Debug, Clone)]
776#[non_exhaustive]
777pub struct Footer {
778    /// Weights to be applied to certain classes of relays when choosing
779    /// for different roles.
780    ///
781    /// For example, we want to avoid choosing exits for non-exit
782    /// roles when overall the proportion of exits is small.
783    pub weights: NetParams<i32>,
784}
785
786/// A consensus document that lists relays along with their
787/// microdescriptor documents.
788pub type MdConsensus = md::Consensus;
789
790/// An MdConsensus that has been parsed and checked for timeliness,
791/// but not for signatures.
792pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
793
794/// An MdConsensus that has been parsed but not checked for signatures
795/// and timeliness.
796pub type UncheckedMdConsensus = md::UncheckedConsensus;
797
798/// A consensus document that lists relays along with their
799/// router descriptor documents.
800pub type PlainConsensus = plain::Consensus;
801
802/// An PlainConsensus that has been parsed and checked for timeliness,
803/// but not for signatures.
804pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
805
806/// An PlainConsensus that has been parsed but not checked for signatures
807/// and timeliness.
808pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
809
810decl_keyword! {
811    /// Keywords that can be used in votes and consensuses.
812    // TODO: This is public because otherwise we can't use it in the
813    // ParseRouterStatus crate.  But I'd rather find a way to make it
814    // private.
815    #[non_exhaustive]
816    #[allow(missing_docs)]
817    pub NetstatusKwd {
818        // Header
819        "network-status-version" => NETWORK_STATUS_VERSION,
820        "vote-status" => VOTE_STATUS,
821        "consensus-methods" => CONSENSUS_METHODS,
822        "consensus-method" => CONSENSUS_METHOD,
823        "published" => PUBLISHED,
824        "valid-after" => VALID_AFTER,
825        "fresh-until" => FRESH_UNTIL,
826        "valid-until" => VALID_UNTIL,
827        "voting-delay" => VOTING_DELAY,
828        "client-versions" => CLIENT_VERSIONS,
829        "server-versions" => SERVER_VERSIONS,
830        "known-flags" => KNOWN_FLAGS,
831        "flag-thresholds" => FLAG_THRESHOLDS,
832        "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
833        "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
834        "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
835        "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
836        "params" => PARAMS,
837        "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
838        "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
839        // "package" is now ignored.
840
841        // header in consensus, voter section in vote?
842        "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
843        "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
844
845        // Voter section (both)
846        "dir-source" => DIR_SOURCE,
847        "contact" => CONTACT,
848
849        // voter section (vote, but not consensus)
850        "legacy-dir-key" => LEGACY_DIR_KEY,
851        "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
852        "shared-rand-commit" => SHARED_RAND_COMMIT,
853
854        // voter section (consensus, but not vote)
855        "vote-digest" => VOTE_DIGEST,
856
857        // voter cert beginning (but only the beginning)
858        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
859
860        // routerstatus
861        "r" => RS_R,
862        "a" => RS_A,
863        "s" => RS_S,
864        "v" => RS_V,
865        "pr" => RS_PR,
866        "w" => RS_W,
867        "p" => RS_P,
868        "m" => RS_M,
869        "id" => RS_ID,
870
871        // footer
872        "directory-footer" => DIRECTORY_FOOTER,
873        "bandwidth-weights" => BANDWIDTH_WEIGHTS,
874        "directory-signature" => DIRECTORY_SIGNATURE,
875    }
876}
877
878/// Shared parts of rules for all kinds of netstatus headers
879static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
880    use NetstatusKwd::*;
881    let mut rules = SectionRules::builder();
882    rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
883    rules.add(VOTE_STATUS.rule().required().args(1..));
884    rules.add(VALID_AFTER.rule().required());
885    rules.add(FRESH_UNTIL.rule().required());
886    rules.add(VALID_UNTIL.rule().required());
887    rules.add(VOTING_DELAY.rule().args(2..));
888    rules.add(CLIENT_VERSIONS.rule());
889    rules.add(SERVER_VERSIONS.rule());
890    rules.add(KNOWN_FLAGS.rule().required());
891    rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
892    rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
893    rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
894    rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
895    rules.add(PARAMS.rule());
896    rules
897});
898/// Rules for parsing the header of a consensus.
899static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
900    use NetstatusKwd::*;
901    let mut rules = NS_HEADER_RULES_COMMON_.clone();
902    rules.add(CONSENSUS_METHOD.rule().args(1..=1));
903    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
904    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
905    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
906    rules.build()
907});
908/*
909/// Rules for parsing the header of a vote.
910static NS_HEADER_RULES_VOTE: SectionRules<NetstatusKwd> = {
911    use NetstatusKwd::*;
912    let mut rules = NS_HEADER_RULES_COMMON_.clone();
913    rules.add(CONSENSUS_METHODS.rule().args(1..));
914    rules.add(FLAG_THRESHOLDS.rule());
915    rules.add(BANDWIDTH_FILE_HEADERS.rule());
916    rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..));
917    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
918    rules
919};
920/// Rules for parsing a single voter's information in a vote.
921static NS_VOTERINFO_RULES_VOTE: SectionRules<NetstatusKwd> = {
922    use NetstatusKwd::*;
923    let mut rules = SectionRules::new();
924    rules.add(DIR_SOURCE.rule().required().args(6..));
925    rules.add(CONTACT.rule().required());
926    rules.add(LEGACY_DIR_KEY.rule().args(1..));
927    rules.add(SHARED_RAND_PARTICIPATE.rule().no_args());
928    rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..));
929    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
930    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
931    // then comes an entire cert: When we implement vote parsing,
932    // we should use the authcert code for handling that.
933    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
934    rules
935};
936 */
937/// Rules for parsing a single voter's information in a consensus
938static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
939    use NetstatusKwd::*;
940    let mut rules = SectionRules::builder();
941    rules.add(DIR_SOURCE.rule().required().args(6..));
942    rules.add(CONTACT.rule().required());
943    rules.add(VOTE_DIGEST.rule().required());
944    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
945    rules.build()
946});
947/// Shared rules for parsing a single routerstatus
948static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
949    LazyLock::new(|| {
950        use NetstatusKwd::*;
951        let mut rules = SectionRules::builder();
952        rules.add(RS_A.rule().may_repeat().args(1..));
953        rules.add(RS_S.rule().required());
954        rules.add(RS_V.rule());
955        rules.add(RS_PR.rule().required());
956        rules.add(RS_W.rule());
957        rules.add(RS_P.rule().args(2..));
958        rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
959        rules
960    });
961
962/// Rules for parsing a single routerstatus in an NS consensus
963static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
964    use NetstatusKwd::*;
965    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
966    rules.add(RS_R.rule().required().args(8..));
967    rules.build()
968});
969
970/*
971/// Rules for parsing a single routerstatus in a vote
972static NS_ROUTERSTATUS_RULES_VOTE: SectionRules<NetstatusKwd> = {
973    use NetstatusKwd::*;
974        let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
975        rules.add(RS_R.rule().required().args(8..));
976        rules.add(RS_M.rule().may_repeat().args(2..));
977        rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat?
978        rules
979    };
980*/
981/// Rules for parsing a single routerstatus in a microdesc consensus
982static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
983    use NetstatusKwd::*;
984    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
985    rules.add(RS_R.rule().required().args(6..));
986    rules.add(RS_M.rule().required().args(1..));
987    rules.build()
988});
989/// Rules for parsing consensus fields from a footer.
990static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
991    use NetstatusKwd::*;
992    let mut rules = SectionRules::builder();
993    rules.add(DIRECTORY_FOOTER.rule().required().no_args());
994    // consensus only
995    rules.add(BANDWIDTH_WEIGHTS.rule());
996    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
997    rules.build()
998});
999
1000impl ProtoStatus {
1001    /// Construct a ProtoStatus from two chosen keywords in a section.
1002    fn from_section(
1003        sec: &Section<'_, NetstatusKwd>,
1004        recommend_token: NetstatusKwd,
1005        required_token: NetstatusKwd,
1006    ) -> Result<ProtoStatus> {
1007        /// Helper: extract a Protocols entry from an item's arguments.
1008        fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> Result<Protocols> {
1009            if let Some(item) = t {
1010                item.args_as_str()
1011                    .parse::<Protocols>()
1012                    .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
1013            } else {
1014                Ok(Protocols::new())
1015            }
1016        }
1017
1018        let recommended = parse(sec.get(recommend_token))?;
1019        let required = parse(sec.get(required_token))?;
1020        Ok(ProtoStatus {
1021            recommended,
1022            required,
1023        })
1024    }
1025
1026    /// Return the protocols that are listed as "required" in this `ProtoStatus`.
1027    ///
1028    /// Implementations may assume that relays on the network implement all the
1029    /// protocols in the relays' required-protocols list.  Implementations should
1030    /// refuse to start if they do not implement all the protocols on their own
1031    /// (client or relay) required-protocols list.
1032    pub fn required_protocols(&self) -> &Protocols {
1033        &self.required
1034    }
1035
1036    /// Return the protocols that are listed as "recommended" in this `ProtoStatus`.
1037    ///
1038    /// Implementations should warn if they do not implement all the protocols
1039    /// on their own (client or relay) recommended-protocols list.
1040    pub fn recommended_protocols(&self) -> &Protocols {
1041        &self.recommended
1042    }
1043}
1044
1045impl<T> std::str::FromStr for NetParams<T>
1046where
1047    T: std::str::FromStr,
1048    T::Err: std::error::Error,
1049{
1050    type Err = Error;
1051    fn from_str(s: &str) -> Result<Self> {
1052        /// Helper: parse a single K=V pair.
1053        fn parse_pair<U>(p: &str) -> Result<(String, U)>
1054        where
1055            U: std::str::FromStr,
1056            U::Err: std::error::Error,
1057        {
1058            let parts: Vec<_> = p.splitn(2, '=').collect();
1059            if parts.len() != 2 {
1060                return Err(EK::BadArgument
1061                    .at_pos(Pos::at(p))
1062                    .with_msg("Missing = in key=value list"));
1063            }
1064            let num = parts[1].parse::<U>().map_err(|e| {
1065                EK::BadArgument
1066                    .at_pos(Pos::at(parts[1]))
1067                    .with_msg(e.to_string())
1068            })?;
1069            Ok((parts[0].to_string(), num))
1070        }
1071
1072        let params = s
1073            .split(' ')
1074            .filter(|p| !p.is_empty())
1075            .map(parse_pair)
1076            .collect::<Result<HashMap<_, _>>>()?;
1077        Ok(NetParams { params })
1078    }
1079}
1080
1081impl FromStr for SharedRandVal {
1082    type Err = Error;
1083    fn from_str(s: &str) -> Result<Self> {
1084        let val: B64 = s.parse()?;
1085        let val = SharedRandVal(val.into_array()?);
1086        Ok(val)
1087    }
1088}
1089impl Display for SharedRandVal {
1090    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1091        Display::fmt(&B64::from(Vec::from(self.0)), f)
1092    }
1093}
1094impl NormalItemArgument for SharedRandVal {}
1095
1096impl SharedRandStatus {
1097    /// Parse a current or previous shared rand value from a given
1098    /// SharedRandPreviousValue or SharedRandCurrentValue.
1099    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
1100        match item.kwd() {
1101            NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
1102            _ => {
1103                return Err(Error::from(internal!(
1104                    "wrong keyword {:?} on shared-random value",
1105                    item.kwd()
1106                ))
1107                .at_pos(item.pos()));
1108            }
1109        }
1110        let n_reveals: u8 = item.parse_arg(0)?;
1111        let value: SharedRandVal = item.parse_arg(1)?;
1112        // Added in proposal 342
1113        let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
1114        Ok(SharedRandStatus {
1115            n_reveals,
1116            value,
1117            timestamp,
1118        })
1119    }
1120
1121    /// Return the actual shared random value.
1122    pub fn value(&self) -> &SharedRandVal {
1123        &self.value
1124    }
1125
1126    /// Return the timestamp (if any) associated with this `SharedRandValue`.
1127    pub fn timestamp(&self) -> Option<std::time::SystemTime> {
1128        self.timestamp.map(|t| t.0)
1129    }
1130}
1131
1132impl DirSource {
1133    /// Parse a "dir-source" item
1134    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
1135        if item.kwd() != NetstatusKwd::DIR_SOURCE {
1136            return Err(
1137                Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
1138                    .at_pos(item.pos()),
1139            );
1140        }
1141        let nickname = item
1142            .required_arg(0)?
1143            .parse()
1144            .map_err(|e: InvalidNickname| {
1145                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1146            })?;
1147        let identity = item.parse_arg(1)?;
1148        let hostname = item
1149            .required_arg(2)?
1150            .parse()
1151            .map_err(|e: InvalidInternetHost| {
1152                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1153            })?;
1154        let ip = item.parse_arg(3)?;
1155        let dir_port = item.parse_arg(4)?;
1156        let or_port = item.parse_arg(5)?;
1157
1158        Ok(DirSource {
1159            nickname,
1160            identity,
1161            hostname,
1162            ip,
1163            dir_port,
1164            or_port,
1165            __non_exhaustive: (),
1166        })
1167    }
1168}
1169
1170impl ConsensusAuthorityEntry {
1171    /// Parse a single ConsensusAuthorityEntry from a voter info section.
1172    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<ConsensusAuthorityEntry> {
1173        use NetstatusKwd::*;
1174        // this unwrap should be safe because if there is not at least one
1175        // token in the section, the section is unparsable.
1176        #[allow(clippy::unwrap_used)]
1177        let first = sec.first_item().unwrap();
1178        if first.kwd() != DIR_SOURCE {
1179            return Err(Error::from(internal!(
1180                "Wrong keyword {:?} at start of voter info",
1181                first.kwd()
1182            ))
1183            .at_pos(first.pos()));
1184        }
1185        let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1186
1187        let contact = sec.required(CONTACT)?;
1188        // Ideally we would parse_args_as_str but that requires us to
1189        // impl From<InvalidContactInfo> for crate::Error which is wrong
1190        // because many it's a footgun which lets you just write ? here
1191        // resulting in lack of position information.
1192        // (This is a general problem with the error handling in crate::parse.)
1193        let contact = contact
1194            .args_as_str()
1195            .parse()
1196            .map_err(|err: InvalidContactInfo| {
1197                EK::BadArgument
1198                    .with_msg(err.to_string())
1199                    .at_pos(contact.pos())
1200            })?;
1201
1202        let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16U>(0)?;
1203
1204        Ok(ConsensusAuthorityEntry {
1205            dir_source,
1206            contact,
1207            vote_digest,
1208            __non_exhaustive: (),
1209        })
1210    }
1211}
1212
1213impl Default for RelayWeight {
1214    fn default() -> RelayWeight {
1215        RelayWeight::Unmeasured(0)
1216    }
1217}
1218
1219impl RelayWeight {
1220    /// Parse a routerweight from a "w" line.
1221    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<RelayWeight> {
1222        if item.kwd() != NetstatusKwd::RS_W {
1223            return Err(
1224                Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
1225                    .at_pos(item.pos()),
1226            );
1227        }
1228
1229        let params = item.args_as_str().parse()?;
1230
1231        Self::from_net_params(&params).map_err(|e| e.at_pos(item.pos()))
1232    }
1233
1234    /// Parse a routerweight from partially-parsed `w` line in the form of a `NetParams`
1235    ///
1236    /// This function is the common part shared between `parse2` and `parse`.
1237    fn from_net_params(params: &NetParams<u32>) -> Result<RelayWeight> {
1238        let bw = params.params.get("Bandwidth");
1239        let unmeas = params.params.get("Unmeasured");
1240
1241        let bw = match bw {
1242            None => return Ok(RelayWeight::Unmeasured(0)),
1243            Some(b) => *b,
1244        };
1245
1246        match unmeas {
1247            None | Some(0) => Ok(RelayWeight::Measured(bw)),
1248            Some(1) => Ok(RelayWeight::Unmeasured(bw)),
1249            _ => Err(EK::BadArgument.with_msg("unmeasured value")),
1250        }
1251    }
1252}
1253
1254/// `parse2` impls for types in this modulea
1255///
1256/// Separate module for a separate namespace.
1257mod parse2_impls {
1258    use super::*;
1259    pub(super) use parse2::{
1260        ArgumentError as AE, ArgumentStream, ErrorProblem as EP, ItemArgumentParseable,
1261        ItemValueParseable, NetdocParseableFields, UnparsedItem,
1262    };
1263    use std::result::Result;
1264
1265    impl ItemValueParseable for NetParams<i32> {
1266        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1267            item.check_no_object()?;
1268            item.args_copy()
1269                .into_remaining()
1270                .parse()
1271                .map_err(item.invalid_argument_handler("parameters"))
1272        }
1273    }
1274
1275    impl ItemValueParseable for RelayWeight {
1276        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1277            item.check_no_object()?;
1278            (|| {
1279                let params = item.args_copy().into_remaining().parse()?;
1280                Self::from_net_params(&params)
1281            })()
1282            .map_err(item.invalid_argument_handler("weights"))
1283        }
1284    }
1285
1286    impl ItemValueParseable for rs::SoftwareVersion {
1287        fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1288            item.check_no_object()?;
1289            item.args_mut()
1290                .into_remaining()
1291                .parse()
1292                .map_err(item.invalid_argument_handler("version"))
1293        }
1294    }
1295
1296    impl ItemArgumentParseable for IgnoredPublicationTimeSp {
1297        fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
1298            let mut next_arg = || a.next().ok_or(AE::Missing);
1299            let _: &str = next_arg()?;
1300            let _: &str = next_arg()?;
1301            Ok(IgnoredPublicationTimeSp)
1302        }
1303    }
1304}
1305
1306/// `encode` impls for types in this modulea
1307///
1308/// Separate module for a separate namespace.
1309mod encode_impls {
1310    use super::*;
1311    use std::result::Result;
1312    pub(crate) use {
1313        crate::encode::{ItemEncoder, ItemValueEncodable, NetdocEncodableFields},
1314        tor_error::Bug,
1315    };
1316
1317    impl ItemValueEncodable for NetParams<i32> {
1318        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
1319            for (k, v) in self.iter().collect::<BTreeSet<_>>() {
1320                if k.is_empty()
1321                    || k.chars()
1322                        .any(|c| c.is_whitespace() || c.is_control() || c == '=')
1323                {
1324                    // TODO torspec#401 see TODO in NetParams<T> definition
1325                    return Err(bad_api_usage!(
1326                        "tried to encode NetParms with unreasonable keyword {k:?}"
1327                    ));
1328                }
1329                out.args_raw_string(&format_args!("{k}={v}"));
1330            }
1331            Ok(())
1332        }
1333    }
1334}
1335
1336impl Footer {
1337    /// Parse a directory footer from a footer section.
1338    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<Footer> {
1339        use NetstatusKwd::*;
1340        sec.required(DIRECTORY_FOOTER)?;
1341
1342        let weights = sec
1343            .maybe(BANDWIDTH_WEIGHTS)
1344            .args_as_str()
1345            .unwrap_or("")
1346            .parse()?;
1347
1348        Ok(Footer { weights })
1349    }
1350}
1351
1352/// `ProtoStatuses` parsing and encoding
1353///
1354/// Separate module for separate namespace
1355mod proto_statuses_parse2_encode {
1356    use super::encode_impls::*;
1357    use super::parse2_impls::*;
1358    use super::*;
1359    use paste::paste;
1360    use std::result::Result;
1361
1362    /// Implements `NetdocParseableFields` for `ProtoStatuses`
1363    ///
1364    /// We have this macro so that it's impossible to write things like
1365    /// ```text
1366    ///      ProtoStatuses {
1367    ///          client: ProtoStatus {
1368    ///              recommended: something something recommended_relay_versions something,
1369    /// ```
1370    ///
1371    /// (The structure of `ProtoStatuses` means the normal parse2 derive won't work for it.
1372    /// Note the bug above: the recommended *relay* version info is put in the *client* field.
1373    /// Preventing this bug must involve: avoiding writing twice the field name elements,
1374    /// such as `relay` and `client`, during this kind of construction/conversion.)
1375    macro_rules! impl_proto_statuses { { $( $rr:ident $cr:ident; )* } => { paste! {
1376        #[derive(Deftly)]
1377        #[derive_deftly(NetdocParseableFields)]
1378        // Only ProtoStatusesParseNetdocParseAccumulator is exposed.
1379        #[allow(unreachable_pub)]
1380        pub struct ProtoStatusesParseHelper {
1381            $(
1382                #[deftly(netdoc(default))]
1383                [<$rr _ $cr _protocols>]: Protocols,
1384            )*
1385        }
1386
1387        /// Partially parsed `ProtoStatuses`
1388        pub use ProtoStatusesParseHelperNetdocParseAccumulator
1389            as ProtoStatusesNetdocParseAccumulator;
1390
1391        impl NetdocParseableFields for ProtoStatuses {
1392            type Accumulator = ProtoStatusesNetdocParseAccumulator;
1393            fn is_item_keyword(kw: KeywordRef<'_>) -> bool {
1394                ProtoStatusesParseHelper::is_item_keyword(kw)
1395            }
1396            fn accumulate_item(
1397                acc: &mut Self::Accumulator,
1398                item: UnparsedItem<'_>,
1399            ) -> Result<(), EP> {
1400                ProtoStatusesParseHelper::accumulate_item(acc, item)
1401            }
1402            fn finish(acc: Self::Accumulator) -> Result<Self, EP> {
1403                let parse = ProtoStatusesParseHelper::finish(acc)?;
1404                let mut out = ProtoStatuses::default();
1405                $(
1406                    out.$cr.$rr = parse.[< $rr _ $cr _protocols >];
1407                )*
1408                Ok(out)
1409            }
1410        }
1411
1412        impl NetdocEncodableFields for ProtoStatuses {
1413            fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
1414              $(
1415                self.$cr.$rr.write_item_value_onto(
1416                    out.item(stringify!([<$rr _ $cr _protocols>]))
1417                )?;
1418              )*
1419                Ok(())
1420            }
1421        }
1422    } } }
1423
1424    impl_proto_statuses! {
1425        required client;
1426        required relay;
1427        recommended client;
1428        recommended relay;
1429    }
1430}
1431
1432/// Result of checking a single authority signature.
1433enum SigCheckResult {
1434    /// The signature checks out.  Great!
1435    Valid,
1436    /// The signature is invalid; no additional information could make it
1437    /// valid.
1438    Invalid,
1439    /// We can't check the signature because we don't have a
1440    /// certificate with the right signing key.
1441    MissingCert,
1442}
1443
1444impl Signature {
1445    /// Parse a Signature from a directory-signature section
1446    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Signature> {
1447        if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
1448            return Err(Error::from(internal!(
1449                "Wrong keyword {:?} for directory signature",
1450                item.kwd()
1451            ))
1452            .at_pos(item.pos()));
1453        }
1454
1455        let (digest_algo, id_fp, sk_fp) = if item.n_args() > 2 {
1456            (
1457                item.required_arg(0)?,
1458                item.required_arg(1)?,
1459                item.required_arg(2)?,
1460            )
1461        } else {
1462            ("sha1", item.required_arg(0)?, item.required_arg(1)?)
1463        };
1464
1465        let digest_algo = digest_algo.to_string().parse().void_unwrap();
1466        let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
1467        let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
1468        let key_ids = AuthCertKeyIds {
1469            id_fingerprint,
1470            sk_fingerprint,
1471        };
1472        let signature = item.obj("SIGNATURE")?;
1473
1474        Ok(Signature {
1475            digest_algo,
1476            key_ids,
1477            signature,
1478        })
1479    }
1480
1481    /// Return true if this signature has the identity key and signing key
1482    /// that match a given cert.
1483    fn matches_cert(&self, cert: &AuthCert) -> bool {
1484        cert.key_ids() == self.key_ids
1485    }
1486
1487    /// If possible, find the right certificate for checking this signature
1488    /// from among a slice of certificates.
1489    fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
1490        certs.iter().find(|&c| self.matches_cert(c))
1491    }
1492
1493    /// Try to check whether this signature is a valid signature of a
1494    /// provided digest, given a slice of certificates that might contain
1495    /// its signing key.
1496    fn check_signature(&self, signed_digest: &[u8], certs: &[AuthCert]) -> SigCheckResult {
1497        match self.find_cert(certs) {
1498            None => SigCheckResult::MissingCert,
1499            Some(cert) => {
1500                let key = cert.signing_key();
1501                match key.verify(signed_digest, &self.signature[..]) {
1502                    Ok(()) => SigCheckResult::Valid,
1503                    Err(_) => SigCheckResult::Invalid,
1504                }
1505            }
1506        }
1507    }
1508}
1509
1510impl SignatureGroup {
1511    // TODO: these functions are pretty similar and could probably stand to be
1512    // refactored a lot.
1513
1514    /// Helper: Return a pair of the number of possible authorities'
1515    /// signatures in this object for which we _could_ find certs, and
1516    /// a list of the signatures we couldn't find certificates for.
1517    fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
1518        let mut ok: HashSet<RsaIdentity> = HashSet::new();
1519        let mut missing = Vec::new();
1520        for sig in &self.signatures {
1521            let id_fingerprint = &sig.key_ids.id_fingerprint;
1522            if ok.contains(id_fingerprint) {
1523                continue;
1524            }
1525            if sig.find_cert(certs).is_some() {
1526                ok.insert(*id_fingerprint);
1527                continue;
1528            }
1529
1530            missing.push(sig);
1531        }
1532        (ok.len(), missing)
1533    }
1534
1535    /// Given a list of authority identity key fingerprints, return true if
1536    /// this signature group is _potentially_ well-signed according to those
1537    /// authorities.
1538    fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
1539        let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
1540        for sig in &self.signatures {
1541            let id_fp = &sig.key_ids.id_fingerprint;
1542            if signed_by.contains(id_fp) {
1543                // Already found this in the list.
1544                continue;
1545            }
1546            if authorities.contains(&id_fp) {
1547                signed_by.insert(*id_fp);
1548            }
1549        }
1550
1551        signed_by.len() > (authorities.len() / 2)
1552    }
1553
1554    /// Return true if the signature group defines a valid signature.
1555    ///
1556    /// A signature is valid if it signed by more than half of the
1557    /// authorities.  This API requires that `n_authorities` is the number of
1558    /// authorities we believe in, and that every cert in `certs` belongs
1559    /// to a real authority.
1560    fn validate(&self, n_authorities: usize, certs: &[AuthCert]) -> bool {
1561        // A set of the authorities (by identity) who have have signed
1562        // this document.  We use a set here in case `certs` has more
1563        // than one certificate for a single authority.
1564        let mut ok: HashSet<RsaIdentity> = HashSet::new();
1565
1566        for sig in &self.signatures {
1567            let id_fingerprint = &sig.key_ids.id_fingerprint;
1568            if ok.contains(id_fingerprint) {
1569                // We already checked at least one signature using this
1570                // authority's identity fingerprint.
1571                continue;
1572            }
1573
1574            use DirectorySignatureHashAlgo as DSHA;
1575            use KeywordOrString as KOS;
1576
1577            let d: Option<&[u8]> = match sig.digest_algo {
1578                KOS::Known(DSHA::Sha256) => self.sha256.as_ref().map(|a| &a[..]),
1579                KOS::Known(DSHA::Sha1) => self.sha1.as_ref().map(|a| &a[..]),
1580                _ => None, // We don't know how to find this digest.
1581            };
1582            if d.is_none() {
1583                // We don't support this kind of digest for this kind
1584                // of document.
1585                continue;
1586            }
1587
1588            // Unwrap should be safe because of above `d.is_none()` check
1589            #[allow(clippy::unwrap_used)]
1590            match sig.check_signature(d.as_ref().unwrap(), certs) {
1591                SigCheckResult::Valid => {
1592                    ok.insert(*id_fingerprint);
1593                }
1594                _ => continue,
1595            }
1596        }
1597
1598        ok.len() > (n_authorities / 2)
1599    }
1600}
1601
1602#[cfg(test)]
1603mod test {
1604    // @@ begin test lint list maintained by maint/add_warning @@
1605    #![allow(clippy::bool_assert_comparison)]
1606    #![allow(clippy::clone_on_copy)]
1607    #![allow(clippy::dbg_macro)]
1608    #![allow(clippy::mixed_attributes_style)]
1609    #![allow(clippy::print_stderr)]
1610    #![allow(clippy::print_stdout)]
1611    #![allow(clippy::single_char_pattern)]
1612    #![allow(clippy::unwrap_used)]
1613    #![allow(clippy::unchecked_time_subtraction)]
1614    #![allow(clippy::useless_vec)]
1615    #![allow(clippy::needless_pass_by_value)]
1616    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1617    use super::*;
1618    use hex_literal::hex;
1619    #[cfg(feature = "incomplete")]
1620    use {
1621        crate::parse2::{NetdocUnverified as _, ParseInput, parse_netdoc},
1622        std::fs,
1623    };
1624
1625    const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
1626    const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
1627
1628    const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
1629    const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
1630
1631    fn read_bad(fname: &str) -> String {
1632        use std::fs;
1633        use std::path::PathBuf;
1634        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1635        path.push("testdata");
1636        path.push("bad-mdconsensus");
1637        path.push(fname);
1638
1639        fs::read_to_string(path).unwrap()
1640    }
1641
1642    #[test]
1643    fn parse_and_validate_md() -> Result<()> {
1644        use std::net::SocketAddr;
1645        use tor_checkable::{SelfSigned, Timebound};
1646        let mut certs = Vec::new();
1647        for cert in AuthCert::parse_multiple(CERTS)? {
1648            let cert = cert?.check_signature()?.dangerously_assume_timely();
1649            certs.push(cert);
1650        }
1651        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
1652
1653        assert_eq!(certs.len(), 3);
1654
1655        let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
1656        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1657
1658        // The set of authorities we know _could_ validate this cert.
1659        assert!(consensus.authorities_are_correct(&auth_ids));
1660        // A subset would also work.
1661        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1662        {
1663            // If we only believe in an authority that isn't listed,
1664            // that won't work.
1665            let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
1666            assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
1667        }
1668
1669        let missing = consensus.key_is_correct(&[]).err().unwrap();
1670        assert_eq!(3, missing.len());
1671        assert!(consensus.key_is_correct(&certs).is_ok());
1672        let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
1673        assert_eq!(2, missing.len());
1674
1675        // here is a trick that had better not work.
1676        let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
1677        let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
1678
1679        assert_eq!(2, missing.len());
1680        assert!(consensus.is_well_signed(&same_three_times).is_err());
1681
1682        assert!(consensus.key_is_correct(&certs).is_ok());
1683        let consensus = consensus.check_signature(&certs)?;
1684
1685        assert_eq!(6, consensus.relays().len());
1686        let r0 = &consensus.relays()[0];
1687        assert_eq!(
1688            r0.md_digest(),
1689            &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
1690        );
1691        assert_eq!(
1692            r0.rsa_identity().as_bytes(),
1693            &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
1694        );
1695        assert!(!r0.weight().is_measured());
1696        assert!(!r0.weight().is_nonzero());
1697        let pv = &r0.protovers();
1698        assert!(pv.supports_subver("HSDir", 2));
1699        assert!(!pv.supports_subver("HSDir", 3));
1700        let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
1701        let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
1702        assert!(r0.addrs().any(|a| a == ip4));
1703        assert!(r0.addrs().any(|a| a == ip6));
1704
1705        Ok(())
1706    }
1707
1708    #[test]
1709    fn parse_and_validate_ns() -> Result<()> {
1710        use tor_checkable::{SelfSigned, Timebound};
1711        let mut certs = Vec::new();
1712        for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
1713            let cert = cert?.check_signature()?.dangerously_assume_timely();
1714            certs.push(cert);
1715        }
1716        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
1717        assert_eq!(certs.len(), 4);
1718
1719        let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
1720        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
1721        // The set of authorities we know _could_ validate this cert.
1722        assert!(consensus.authorities_are_correct(&auth_ids));
1723        // A subset would also work.
1724        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
1725
1726        assert!(consensus.key_is_correct(&certs).is_ok());
1727
1728        let _consensus = consensus.check_signature(&certs)?;
1729
1730        Ok(())
1731    }
1732
1733    #[test]
1734    #[cfg(feature = "incomplete")]
1735    fn parse2_vote() -> anyhow::Result<()> {
1736        let file = "testdata2/v3-status-votes--1";
1737        let text = fs::read_to_string(file)?;
1738
1739        // TODO DIRAUTH replace the poc struct here when we have parsing of proper whole votes
1740        use crate::parse2::poc::netstatus::NetworkStatusUnverifiedVote;
1741
1742        let input = ParseInput::new(&text, file);
1743        let doc: NetworkStatusUnverifiedVote = parse_netdoc(&input)?;
1744
1745        println!("{doc:?}");
1746        println!("{:#?}", doc.inspect_unverified().0.r[0]);
1747
1748        Ok(())
1749    }
1750
1751    #[test]
1752    fn test_bad() {
1753        use crate::Pos;
1754        fn check(fname: &str, e: &Error) {
1755            let content = read_bad(fname);
1756            let res = MdConsensus::parse(&content);
1757            assert!(res.is_err());
1758            assert_eq!(&res.err().unwrap(), e);
1759        }
1760
1761        check(
1762            "bad-flags",
1763            &EK::BadArgument
1764                .at_pos(Pos::from_line(27, 1))
1765                .with_msg("Flags out of order"),
1766        );
1767        check(
1768            "bad-md-digest",
1769            &EK::BadArgument
1770                .at_pos(Pos::from_line(40, 3))
1771                .with_msg("Invalid base64"),
1772        );
1773        check(
1774            "bad-weight",
1775            &EK::BadArgument
1776                .at_pos(Pos::from_line(67, 141))
1777                .with_msg("invalid digit found in string"),
1778        );
1779        check(
1780            "bad-weights",
1781            &EK::BadArgument
1782                .at_pos(Pos::from_line(51, 13))
1783                .with_msg("invalid digit found in string"),
1784        );
1785        check(
1786            "wrong-order",
1787            &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
1788        );
1789        check(
1790            "wrong-start",
1791            &EK::UnexpectedToken
1792                .with_msg("vote-status")
1793                .at_pos(Pos::from_line(1, 1)),
1794        );
1795        check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
1796    }
1797
1798    fn gettok(s: &str) -> Result<Item<'_, NetstatusKwd>> {
1799        let mut reader = NetDocReader::new(s)?;
1800        let tok = reader.next().unwrap();
1801        assert!(reader.next().is_none());
1802        tok
1803    }
1804
1805    #[test]
1806    fn test_weight() {
1807        let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
1808        let w = RelayWeight::from_item(&w).unwrap();
1809        assert!(!w.is_measured());
1810        assert!(w.is_nonzero());
1811
1812        let w = gettok("w Bandwidth=10\n").unwrap();
1813        let w = RelayWeight::from_item(&w).unwrap();
1814        assert!(w.is_measured());
1815        assert!(w.is_nonzero());
1816
1817        let w = RelayWeight::default();
1818        assert!(!w.is_measured());
1819        assert!(!w.is_nonzero());
1820
1821        let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
1822        let w = RelayWeight::from_item(&w).unwrap();
1823        assert!(!w.is_measured());
1824        assert!(!w.is_nonzero());
1825
1826        let w = gettok("r foo\n").unwrap();
1827        let w = RelayWeight::from_item(&w);
1828        assert!(w.is_err());
1829
1830        let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
1831        let w = RelayWeight::from_item(&w);
1832        assert!(w.is_err());
1833
1834        let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
1835        let w = RelayWeight::from_item(&w);
1836        assert!(w.is_err());
1837    }
1838
1839    #[test]
1840    fn test_netparam() {
1841        let p = "Hello=600 Goodbye=5 Fred=7"
1842            .parse::<NetParams<u32>>()
1843            .unwrap();
1844        assert_eq!(p.get("Hello"), Some(&600_u32));
1845
1846        let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
1847        assert!(p.is_err());
1848
1849        let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
1850        assert!(p.is_err());
1851
1852        for bad_kw in ["What=The", "", "\n", "\0"] {
1853            let p = [(bad_kw, 42)].into_iter().collect::<NetParams<i32>>();
1854            let mut d = NetdocEncoder::new();
1855            let d = (|| {
1856                let i = d.item("bad-psrams");
1857                p.write_item_value_onto(i)?;
1858                d.finish()
1859            })();
1860            let _: tor_error::Bug = d.expect_err(bad_kw);
1861        }
1862    }
1863
1864    #[test]
1865    fn test_sharedrand() {
1866        let sr =
1867            gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
1868                .unwrap();
1869        let sr = SharedRandStatus::from_item(&sr).unwrap();
1870
1871        assert_eq!(sr.n_reveals, 9);
1872        assert_eq!(
1873            sr.value.0,
1874            hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
1875        );
1876        assert!(sr.timestamp.is_none());
1877
1878        let sr2 = gettok(
1879            "shared-rand-current-value 9 \
1880                    5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
1881        )
1882        .unwrap();
1883        let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
1884        assert_eq!(sr2.n_reveals, sr.n_reveals);
1885        assert_eq!(sr2.value.0, sr.value.0);
1886        assert_eq!(
1887            sr2.timestamp.unwrap().0,
1888            humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
1889        );
1890
1891        let sr = gettok("foo bar\n").unwrap();
1892        let sr = SharedRandStatus::from_item(&sr);
1893        assert!(sr.is_err());
1894    }
1895
1896    #[test]
1897    fn test_protostatus() {
1898        let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
1899
1900        let outcome = ProtoStatus {
1901            recommended: "Link=7".parse().unwrap(),
1902            required: "Desc=5".parse().unwrap(),
1903        }
1904        .check_protocols(&my_protocols);
1905        assert!(outcome.is_ok());
1906
1907        let outcome = ProtoStatus {
1908            recommended: "Microdesc=4 Link=7".parse().unwrap(),
1909            required: "Desc=5".parse().unwrap(),
1910        }
1911        .check_protocols(&my_protocols);
1912        assert_eq!(
1913            outcome,
1914            Err(ProtocolSupportError::MissingRecommended(
1915                "Microdesc=4".parse().unwrap()
1916            ))
1917        );
1918
1919        let outcome = ProtoStatus {
1920            recommended: "Microdesc=4 Link=7".parse().unwrap(),
1921            required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
1922        }
1923        .check_protocols(&my_protocols);
1924        assert_eq!(
1925            outcome,
1926            Err(ProtocolSupportError::MissingRequired(
1927                "Cons=6-12 Wombat=15".parse().unwrap()
1928            ))
1929        );
1930    }
1931
1932    #[test]
1933    fn serialize_protostatus() {
1934        let ps = ProtoStatuses {
1935            client: ProtoStatus {
1936                recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
1937                required: "Link=5 LinkAuth=3".parse().unwrap(),
1938            },
1939            relay: ProtoStatus {
1940                recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
1941                required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
1942            },
1943        };
1944        let json = serde_json::to_string(&ps).unwrap();
1945        let ps2 = serde_json::from_str(json.as_str()).unwrap();
1946        assert_eq!(ps, ps2);
1947
1948        let ps3: ProtoStatuses = serde_json::from_str(
1949            r#"{
1950            "client":{
1951                "required":"Link=5 LinkAuth=3",
1952                "recommended":"Link=1-5 LinkAuth=2-5"
1953            },
1954            "relay":{
1955                "required":"Wombat=20-22 Knish=25-27",
1956                "recommended":"Wombat=20-30 Knish=20-30"
1957            }
1958        }"#,
1959        )
1960        .unwrap();
1961        assert_eq!(ps, ps3);
1962    }
1963}