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::{self, AuthCert, AuthCertKeyIds};
68use crate::encode::{
69    ItemArgument, ItemEncoder, ItemValueEncodable, NetdocEncodable, NetdocEncoder,
70};
71use crate::parse::keyword::Keyword;
72use crate::parse::parser::{Section, SectionRules, SectionRulesBuilder};
73use crate::parse::tokenize::{Item, ItemResult, NetDocReader};
74use crate::parse2::{
75    self, ArgumentError, ArgumentStream, ErrorProblem, IsStructural, ItemArgumentParseable,
76    ItemStream, ItemValueParseable, KeywordRef, NetdocParseable, SignatureHashInputs,
77    SignatureItemParseable, StopAt, UnparsedItem,
78};
79use crate::types::relay_flags::{self, DocRelayFlags};
80use crate::types::{self, *};
81use crate::util::PeekableIterator;
82use crate::{Error, KeywordEncodable, NetdocErrorKind as EK, NormalItemArgument, Pos, Result};
83use std::collections::{BTreeSet, HashMap, HashSet};
84use std::fmt::{self, Display};
85use std::result::Result as StdResult;
86use std::str::FromStr;
87use std::sync::Arc;
88use std::{net, result, time};
89use tor_error::{Bug, HasKind, bad_api_usage, internal};
90use tor_protover::Protocols;
91use void::ResultVoidExt as _;
92
93use derive_deftly::{Deftly, define_derive_deftly};
94use digest::Digest;
95use itertools::Itertools;
96use std::sync::LazyLock;
97use tor_checkable::{ExternallySigned, timed::TimerangeBound};
98use tor_llcrypto as ll;
99use tor_llcrypto::pk::rsa::RsaIdentity;
100
101use serde::{Deserialize, Deserializer};
102
103#[cfg(feature = "build_docs")]
104pub use build::MdConsensusBuilder;
105#[cfg(feature = "build_docs")]
106pub use build::PlainConsensusBuilder;
107#[cfg(feature = "build_docs")]
108ns_export_each_flavor! {
109    ty: RouterStatusBuilder;
110}
111
112ns_export_each_variety! {
113    ty: Footer, RouterStatus, Preamble;
114}
115
116#[deprecated]
117pub use PlainConsensus as NsConsensus;
118#[deprecated]
119pub use PlainRouterStatus as NsRouterStatus;
120#[deprecated]
121pub use UncheckedPlainConsensus as UncheckedNsConsensus;
122#[deprecated]
123pub use UnvalidatedPlainConsensus as UnvalidatedNsConsensus;
124
125pub use rs::{RouterStatusMdDigestsVote, SoftwareVersion};
126
127pub use dir_source::{ConsensusAuthoritySection, DirSource, SupersededAuthorityKey};
128
129define_constant_string! {
130    /// `network-status-version` version value
131    ///
132    /// This is the fixed string `3`.
133    ///
134    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:network-status-version>
135    //
136    // IMO this is nicer than the formulation with an enum.
137    // In practice we are not going to support other versions with the same parsing approach;
138    // probably not even with the same code.
139    NetworkStatusVersion = "3";
140}
141
142define_constant_string! {
143    /// The `status` value in a `vote-status` line in a consensus
144    ///
145    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
146    VoteStatusConsensus = "consensus";
147}
148
149define_constant_string! {
150    /// The `vote` value in a `vote-status` line in a vote
151    ///
152    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-status>
153    VoteStatusVote = "vote";
154}
155
156/// `publiscation` field in routerstatus entry intro item other than in votes
157///
158/// Two arguments which are both ignored.
159/// This used to be an ISO8601 timestamp in anomalous two-argument format.
160///
161/// Nowadays, according to the spec, it can be a dummy value.
162/// So it can be a unit type.
163///
164/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:r>,
165/// except in votes which use [`Iso8601TimeSp`] instead.
166///
167/// **Not the same as** the `published` item:
168/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
169#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
170#[allow(clippy::exhaustive_structs)]
171pub struct IgnoredPublicationTimeSp;
172
173/// The lifetime of a networkstatus document.
174///
175/// In a consensus, this type describes when the consensus may safely
176/// be used.  In a vote, this type describes the proposed lifetime for a
177/// consensus.
178///
179/// Aggregate of three netdoc preamble fields.
180#[derive(Clone, Debug, Deftly)]
181#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
182#[derive_deftly(Lifetime)]
183#[allow(clippy::exhaustive_structs)]
184pub struct Lifetime {
185    /// `valid-after` --- Time at which the document becomes valid
186    ///
187    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
188    ///
189    /// (You might see a consensus a little while before this time,
190    /// since voting tries to finish up before the.)
191    #[deftly(constructor)]
192    #[deftly(netdoc(single_arg))]
193    pub valid_after: Iso8601TimeSp,
194    /// `fresh-until` --- Time after which there is expected to be a better version
195    /// of this consensus
196    ///
197    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
198    ///
199    /// You can use the consensus after this time, but there is (or is
200    /// supposed to be) a better one by this point.
201    #[deftly(constructor)]
202    #[deftly(netdoc(single_arg))]
203    pub fresh_until: Iso8601TimeSp,
204    /// `valid-until` --- Time after which this consensus is expired.
205    ///
206    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:published>
207    ///
208    /// You should try to get a better consensus after this time,
209    /// though it's okay to keep using this one if no more recent one
210    /// can be found.
211    #[deftly(constructor)]
212    #[deftly(netdoc(single_arg))]
213    pub valid_until: Iso8601TimeSp,
214
215    #[doc(hidden)]
216    #[deftly(netdoc(skip))]
217    pub __non_exhaustive: (),
218}
219
220define_derive_deftly! {
221    /// Bespoke derive for `Lifetime`, for `new` and accessors
222    Lifetime:
223
224    ${defcond FIELD not(approx_equal($fname, __non_exhaustive))}
225
226    impl Lifetime {
227        /// Construct a new Lifetime.
228        pub fn new(
229            $( ${when FIELD} $fname: time::SystemTime, )
230        ) -> Result<Self> {
231            // Make this now because otherwise literal `valid_after` here in the body
232            // has the wrong span - the compiler refuses to look at the argument.
233            // But we can refer to the field names.
234            let self_ = Lifetime {
235                $( ${when FIELD} $fname: $fname.into(), )
236                __non_exhaustive: (),
237            };
238            if self_.valid_after < self_.fresh_until && self_.fresh_until < self_.valid_until {
239                Ok(self_)
240            } else {
241                Err(EK::InvalidLifetime.err())
242            }
243        }
244      $(
245        ${when FIELD}
246
247        ${fattrs doc}
248        pub fn $fname(&self) -> time::SystemTime {
249            *self.$fname
250        }
251      )
252        /// Return true if this consensus is officially valid at the provided time.
253        pub fn valid_at(&self, when: time::SystemTime) -> bool {
254            *self.valid_after <= when && when <= *self.valid_until
255        }
256
257        /// Return the voting period implied by this lifetime.
258        ///
259        /// (The "voting period" is the amount of time in between when a consensus first
260        /// becomes valid, and when the next consensus is expected to become valid)
261        pub fn voting_period(&self) -> time::Duration {
262            let valid_after = self.valid_after();
263            let fresh_until = self.fresh_until();
264            fresh_until
265                .duration_since(valid_after)
266                .expect("Mis-formed lifetime")
267        }
268    }
269}
270use derive_deftly_template_Lifetime;
271
272/// A single consensus method
273///
274/// These are integers, but we don't do arithmetic on them.
275///
276/// As defined here:
277/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
278/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavor:microdesc>
279///
280/// As used in a `consensus-method` item:
281/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-method>
282#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)] //
283#[derive(derive_more::From, derive_more::Into, derive_more::Display, derive_more::FromStr)]
284pub struct ConsensusMethod(u32);
285impl NormalItemArgument for ConsensusMethod {}
286
287/// A set of consensus methods
288///
289/// Implements `ItemValueParseable` as required for `consensus-methods`,
290/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:consensus-methods>
291///
292/// There is also [`consensus_methods_comma_separated`] for `m` lines in votes.
293#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Deftly)]
294#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
295#[non_exhaustive]
296pub struct ConsensusMethods {
297    /// Consensus methods.
298    pub methods: BTreeSet<ConsensusMethod>,
299}
300
301/// Module for use with parse2's `with`, to parse one argument of comma-separated consensus methods
302///
303/// As found in an `m` item in a vote:
304/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:m>
305pub mod consensus_methods_comma_separated {
306    use super::*;
307    use parse2::ArgumentError as AE;
308    use std::result::Result;
309
310    /// Parse
311    pub fn from_args<'s>(args: &mut ArgumentStream<'s>) -> Result<ConsensusMethods, AE> {
312        let mut methods = BTreeSet::new();
313        for ent in args.next().ok_or(AE::Missing)?.split(',') {
314            let ent = ent.parse().map_err(|_| AE::Invalid)?;
315            if !methods.insert(ent) {
316                return Err(AE::Invalid);
317            }
318        }
319        Ok(ConsensusMethods { methods })
320    }
321
322    /// Encode
323    #[cfg(feature = "incomplete")] // untested
324    pub fn write_arg_onto(self_: &ConsensusMethods, out: &mut ItemEncoder) -> Result<(), Bug> {
325        for s in Itertools::intersperse(self_.methods.iter().map(|v| v as &dyn Display), &",") {
326            out.args_raw_string(s);
327        }
328        Ok(())
329    }
330}
331
332/// A set of named network parameters.
333///
334/// These are used to describe current settings for the Tor network,
335/// current weighting parameters for path selection, and so on.  They're
336/// encoded with a space-separated K=V format.
337///
338/// A `NetParams<i32>` is part of the validated directory manager configuration,
339/// where it is built (in the builder-pattern sense) from a transparent HashMap.
340///
341/// As found in `params` in a network status:
342/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:params>
343///
344/// The same syntax is also used, and this type used for parsing, in various other places,
345/// for example routerstatus entry `w` items (bandwidth weights):
346/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:w>
347//
348// TODO DIRAUTH torspec#401 Replace `String` with a suitable newtype
349// Currently:
350//  - Our parser allows any keyword that makes it into a netdoc argument,
351//    but it splits on the *first* `=` so a `NetParams<i32>` cannot parse a keyword with a `=`.
352//  - We provide constructors that allow any `String`, even ones containing space, `=`,
353//    newline, etc.
354//  - Encoding throws `Bug` if the resulting document will be clearly garbage,
355//    forbidding `=`, whitespace, and controls.  If the supplied keywords are bizarre,
356//    it may generate surprising documents (eg, containing exciting Unicode).
357#[derive(Debug, Clone, Default, Eq, PartialEq)]
358pub struct NetParams<T> {
359    /// Map from keys to values.
360    params: HashMap<String, T>,
361}
362
363impl<T> NetParams<T> {
364    /// Create a new empty list of NetParams.
365    #[allow(unused)]
366    pub fn new() -> Self {
367        NetParams {
368            params: HashMap::new(),
369        }
370    }
371    /// Retrieve a given network parameter, if it is present.
372    pub fn get<A: AsRef<str>>(&self, v: A) -> Option<&T> {
373        self.params.get(v.as_ref())
374    }
375    /// Return an iterator over all key value pairs in an arbitrary order.
376    pub fn iter(&self) -> impl Iterator<Item = (&String, &T)> {
377        self.params.iter()
378    }
379    /// Set or replace the value of a network parameter.
380    pub fn set(&mut self, k: String, v: T) {
381        self.params.insert(k, v);
382    }
383}
384
385impl<K: Into<String>, T> FromIterator<(K, T)> for NetParams<T> {
386    fn from_iter<I: IntoIterator<Item = (K, T)>>(i: I) -> Self {
387        NetParams {
388            params: i.into_iter().map(|(k, v)| (k.into(), v)).collect(),
389        }
390    }
391}
392
393impl<T> std::iter::Extend<(String, T)> for NetParams<T> {
394    fn extend<I: IntoIterator<Item = (String, T)>>(&mut self, iter: I) {
395        self.params.extend(iter);
396    }
397}
398
399impl<'de, T> Deserialize<'de> for NetParams<T>
400where
401    T: Deserialize<'de>,
402{
403    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
404    where
405        D: Deserializer<'de>,
406    {
407        let params = HashMap::deserialize(deserializer)?;
408        Ok(NetParams { params })
409    }
410}
411
412/// A list of subprotocol versions that implementors should/must provide.
413///
414/// This struct represents a pair of (optional) items:
415/// `recommended-FOO-protocols` and `required-FOO-protocols`.
416///
417/// Each consensus has two of these: one for relays, and one for clients.
418///
419/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
420#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
421pub struct ProtoStatus {
422    /// Set of protocols that are recommended; if we're missing a protocol
423    /// in this list we should warn the user.
424    ///
425    /// `recommended-client-protocols` or `recommended-relay-protocols`
426    recommended: Protocols,
427    /// Set of protocols that are required; if we're missing a protocol
428    /// in this list we should refuse to start.
429    ///
430    /// `required-client-protocols` or `required-relay-protocols`
431    required: Protocols,
432}
433
434impl ProtoStatus {
435    /// Check whether the list of supported protocols
436    /// is sufficient to satisfy this list of recommendations and requirements.
437    ///
438    /// If any required protocol is missing, returns [`ProtocolSupportError::MissingRequired`].
439    ///
440    /// Otherwise, if no required protocol is missing, but some recommended protocol is missing,
441    /// returns [`ProtocolSupportError::MissingRecommended`].
442    ///
443    /// Otherwise, if no recommended or required protocol is missing, returns `Ok(())`.
444    pub fn check_protocols(
445        &self,
446        supported_protocols: &Protocols,
447    ) -> StdResult<(), ProtocolSupportError> {
448        // Required protocols take precedence, so we check them first.
449        let missing_required = self.required.difference(supported_protocols);
450        if !missing_required.is_empty() {
451            return Err(ProtocolSupportError::MissingRequired(missing_required));
452        }
453        let missing_recommended = self.recommended.difference(supported_protocols);
454        if !missing_recommended.is_empty() {
455            return Err(ProtocolSupportError::MissingRecommended(
456                missing_recommended,
457            ));
458        }
459
460        Ok(())
461    }
462}
463
464/// A subprotocol that is recommended or required in the consensus was not present.
465#[derive(Clone, Debug, thiserror::Error)]
466#[cfg_attr(test, derive(PartialEq))]
467#[non_exhaustive]
468pub enum ProtocolSupportError {
469    /// At least one required protocol was not in our list of supported protocols.
470    #[error("Required protocols are not implemented: {0}")]
471    MissingRequired(Protocols),
472
473    /// At least one recommended protocol was not in our list of supported protocols.
474    ///
475    /// Also implies that no _required_ protocols were missing.
476    #[error("Recommended protocols are not implemented: {0}")]
477    MissingRecommended(Protocols),
478}
479
480impl ProtocolSupportError {
481    /// Return true if the suggested behavior for this error is a shutdown.
482    pub fn should_shutdown(&self) -> bool {
483        matches!(self, Self::MissingRequired(_))
484    }
485}
486
487impl HasKind for ProtocolSupportError {
488    fn kind(&self) -> tor_error::ErrorKind {
489        tor_error::ErrorKind::SoftwareDeprecated
490    }
491}
492
493/// A set of recommended and required protocols when running
494/// in various scenarios.
495///
496/// Represents the collection of four items: `{recommended,required}-{client,relay}-protocols`.
497///
498/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:required-relay-protocols>
499#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
500pub struct ProtoStatuses {
501    /// Lists of recommended and required subprotocol versions for clients
502    client: ProtoStatus,
503    /// Lists of recommended and required subprotocol versions for relays
504    relay: ProtoStatus,
505}
506
507impl ProtoStatuses {
508    /// Return the list of recommended and required protocols for running as a client.
509    pub fn client(&self) -> &ProtoStatus {
510        &self.client
511    }
512
513    /// Return the list of recommended and required protocols for running as a relay.
514    pub fn relay(&self) -> &ProtoStatus {
515        &self.relay
516    }
517}
518
519/// A recognized 'flavor' of consensus document.
520///
521/// The enum is exhaustive because the addition/removal of a consensus flavor
522/// should indeed be a breaking change, as it would inevitable require
523/// interfacing code to think about the handling of it.
524///
525/// <https://spec.torproject.org/dir-spec/computing-consensus.html#flavors>
526#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
527#[allow(clippy::exhaustive_enums)]
528pub enum ConsensusFlavor {
529    /// A "microdesc"-flavored consensus.  This is the one that
530    /// clients and relays use today.
531    Microdesc,
532    /// A "networkstatus"-flavored consensus.  It's used for
533    /// historical and network-health purposes.  Instead of listing
534    /// microdescriptor digests, it lists digests of full relay
535    /// descriptors.
536    Plain,
537}
538
539impl ConsensusFlavor {
540    /// Return the name of this consensus flavor.
541    pub fn name(&self) -> &'static str {
542        match self {
543            ConsensusFlavor::Plain => "ns", // spec bug, now baked in
544            ConsensusFlavor::Microdesc => "microdesc",
545        }
546    }
547    /// Try to find the flavor whose name is `name`.
548    ///
549    /// For historical reasons, an unnamed flavor indicates an "Plain"
550    /// document.
551    pub fn from_opt_name(name: Option<&str>) -> Result<Self> {
552        match name {
553            Some("microdesc") => Ok(ConsensusFlavor::Microdesc),
554            Some("ns") | None => Ok(ConsensusFlavor::Plain),
555            Some(other) => {
556                Err(EK::BadDocumentType.with_msg(format!("unrecognized flavor {:?}", other)))
557            }
558        }
559    }
560}
561
562define_derive_deftly! {
563    /// Bespoke derives applied to [`DirectorySignatureHashAlgo`]
564    ///
565    /// Generates:
566    ///
567    ///  * [`DirectorySignaturesHashesAccu`]
568    ///  * [`DirectorySignaturesHashesAccu::update_from`]
569    ///  * [`DirectorySignaturesHashesAccu::hash_slice_for_verification`]
570    DirectorySignaturesHashesAccu:
571
572    ${define FNAME ${paste ${snake_case $vname}} }
573
574    /// `directory-signature`a hash algorithm argument
575    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, Deftly)]
576    #[derive_deftly(AsMutSelf)]
577    #[non_exhaustive]
578    pub struct DirectorySignaturesHashesAccu {
579      $(
580        ${vattrs doc}
581        pub $FNAME: Option<[u8; ${vmeta(hash_len) as expr}]>,
582      )
583
584      /// `sha1` but without the algorithm name
585      ///
586      /// This is needed because the hash includes the whole signature item keyword line,
587      /// and therefore a signature with the `sha1` explicitly stated,
588      /// and one without, have different hashes!
589      ///
590      /// So we mustn't use the `sha1` field for both implicit and explicit use of SHA-1,
591      /// or multiple signatures with different syntax would overwrite each others'
592      /// different hashes.
593      pub sha1_unnamed: Option<[u8; 20]>,
594    }
595
596    impl DirectorySignaturesHashesAccu {
597        /// Calculate the hash for a signature item and update this accumulator
598        fn update_from(
599            &mut self,
600            algo: &DigestAlgoInSignature,
601            body: &SignatureHashInputs,
602        ) {
603            // Update the hash in self.$UPDATE according to algorithm $AGLO
604            // (uses dynamic bindings of those parameters)
605            ${define HASH {
606                // Avoid recalculating if we don't need to
607                self.$UPDATE.get_or_insert_with(|| {
608                    let mut h = tor_llcrypto::d::$ALGO::new();
609                    h.update(body.body().body());
610                    h.update(body.signature_item_kw_spc);
611                    h.finalize().into()
612                });
613            }}
614
615            match &**algo {
616              $(
617                Some(KeywordOrString::Known($vtype)) => {
618                    ${define UPDATE $FNAME}
619                    ${define ALGO $vname}
620                    $HASH
621                }
622              )
623                None => {
624                    ${define UPDATE sha1_unnamed}
625                    ${define ALGO Sha1}
626                    $HASH
627                }
628                Some(KeywordOrString::Unknown(..)) => {}
629            }
630        }
631
632        /// Return the hash value for a specific algorithm, as a slice
633        ///
634        /// `None` if the value wasn't computed.
635        /// That shouldn't happen.
636        // TODO DIRAUTH make private when poc's verification is abolished
637        pub(crate) fn hash_slice_for_verification(
638            &self,
639            algo: &DigestAlgoInSignature,
640        ) -> Option<&[u8]> {
641            match &**algo {
642              $(
643                Some(KeywordOrString::Known($vtype)) => Some(self.$FNAME.as_ref()?),
644              )
645                None => Some(self.sha1_unnamed.as_ref()?),
646                Some(KeywordOrString::Unknown(..)) => None,
647            }
648        }
649    }
650}
651
652/// `directory-signature` hash algorithm argument
653#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display, strum::EnumString, Deftly)]
654#[derive_deftly(DirectorySignaturesHashesAccu)]
655#[non_exhaustive]
656#[strum(serialize_all = "snake_case")]
657pub enum DirectorySignatureHashAlgo {
658    /// SHA-1
659    #[deftly(hash_len = "20")]
660    Sha1,
661    /// SHA-256
662    #[deftly(hash_len = "32")]
663    Sha256,
664}
665
666/// `algorithm` field in a `directory-signature` item
667///
668/// This is extremely bizarre: it's an *optional item at the start of the arguments*!
669// TODO SPEC #350
670///
671/// So we parse it with some kind of nightmarish lookahead.
672///
673/// Additionally, to be able to convey the signatures accurately, without breaking them,
674/// we must remember whether the argument was present.
675///
676/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:directory-signature>
677#[derive(Debug, Clone, derive_more::Deref, derive_more::DerefMut)]
678#[allow(clippy::exhaustive_structs)]
679pub struct DigestAlgoInSignature(pub Option<KeywordOrString<DirectorySignatureHashAlgo>>);
680
681impl ItemArgumentParseable for DigestAlgoInSignature {
682    fn from_args<'s>(args: &mut ArgumentStream<'s>) -> StdResult<Self, ArgumentError> {
683        let v = if args
684            .clone()
685            .next()
686            // Treat it as a fingerprint if it doesn't have any non-hex characters
687            // (including lowercase ones).  If we reuse this item for new algorithms
688            // they should have at least one letter g-z in their name.
689            .and_then(|s| s.chars().all(|c| c.is_ascii_hexdigit()).then_some(()))
690            .is_some()
691        {
692            // next argument looks enough like a fingerprint that we don't treat as an algo name
693            None
694        } else {
695            Some(KeywordOrString::from_args(args)?)
696        };
697        Ok(DigestAlgoInSignature(v))
698    }
699}
700impl ItemArgument for DigestAlgoInSignature {
701    fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> StdResult<(), Bug> {
702        if let Some(y) = &self.0 {
703            y.write_arg_onto(out)?;
704        }
705        Ok(())
706    }
707}
708impl DigestAlgoInSignature {
709    /// Return the actual algorithm
710    ///
711    /// This handles the defaulting, where an absent argument means `sha1`.
712    pub fn algorithm(&self) -> &KeywordOrString<DirectorySignatureHashAlgo> {
713        self.as_ref()
714            .unwrap_or(&KeywordOrString::Known(DirectorySignatureHashAlgo::Sha1))
715    }
716}
717
718impl NormalItemArgument for DirectorySignatureHashAlgo {}
719
720/// The signature of a single directory authority on a networkstatus document.
721///
722/// Implements `ItemValueParseable` which parses without hashing anything;
723/// this is mostly useful for use by the `SignatureItemParseable` implementation.
724#[derive(Debug, Clone, Deftly)]
725#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
726#[non_exhaustive]
727pub struct Signature {
728    /// The name of the digest algorithm used to make the signature.
729    ///
730    /// Currently sha1 and sh256 are recognized.  Here we only support
731    /// sha256.
732    pub digest_algo: DigestAlgoInSignature,
733    /// Fingerprints of the keys for the authority that made
734    /// this signature.
735    #[deftly(netdoc(with = authcert::keyids_directory_signature_args))]
736    pub key_ids: AuthCertKeyIds,
737    /// The signature itself.
738    #[deftly(netdoc(object(label = "SIGNATURE"), with = types::raw_data_object))]
739    pub signature: Vec<u8>,
740}
741
742impl SignatureItemParseable for Signature {
743    type HashAccu = DirectorySignaturesHashesAccu;
744
745    fn from_unparsed_and_body(
746        item: UnparsedItem,
747        body: &SignatureHashInputs<'_>,
748        hash: &mut Self::HashAccu,
749    ) -> StdResult<Self, ErrorProblem> {
750        let signature = Signature::from_unparsed(item)?;
751        hash.update_from(&signature.digest_algo, body);
752        Ok(signature)
753    }
754}
755
756/// A collection of signatures that can be checked on a networkstatus document
757///
758/// This is derived from the signatures section of a netstatus,
759/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:signature>,
760/// but it is not isomorphic to it, and is not directly parseable.
761#[derive(Debug, Clone)]
762#[non_exhaustive]
763pub struct SignatureGroup {
764    /// The document hashes of the signed part of the document
765    ///
766    /// The pre-parse2 parser always sets `hashes.sha1` and `hashes.sha1_unnamed`
767    /// to the same value, which is wrong. which is
768    /// [bug #2530](https://gitlab.torproject.org/tpo/core/arti/-/work_items/2530)
769    pub hashes: DirectorySignaturesHashesAccu,
770    /// The signatures listed on the document.
771    pub signatures: Vec<Signature>,
772}
773
774/// A shared random value produced by the directory authorities.
775#[derive(
776    Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into, derive_more::AsRef,
777)]
778// (This doesn't need to use CtByteArray; we don't really need to compare these.)
779pub struct SharedRandVal([u8; 32]);
780
781/// A shared-random value produced by the directory authorities,
782/// along with meta-information about that value.
783#[derive(Debug, Clone, Deftly)]
784#[non_exhaustive]
785#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
786pub struct SharedRandStatus {
787    /// How many authorities revealed shares that contributed to this value.
788    pub n_reveals: u8,
789    /// The current random value.
790    ///
791    /// The properties of the secure shared-random system guarantee
792    /// that this value isn't predictable before it first becomes
793    /// live, and that a hostile party could not have forced it to
794    /// have any more than a small number of possible random values.
795    pub value: SharedRandVal,
796
797    /// The time when this SharedRandVal becomes (or became) the latest.
798    ///
799    /// (This is added per proposal 342, assuming that gets accepted.)
800    pub timestamp: Option<Iso8601TimeNoSp>,
801}
802
803/// The two shared random values, `shared-rand-*-value`
804///
805/// As found in the consensus preamble
806/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-current-value>
807/// and a vote's authority section
808/// <https://spec.torproject.org/dir-spec/consensus-formats.html#authority-item-shared-rand-value>
809#[derive(Debug, Clone, Default, Deftly)]
810#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
811#[allow(clippy::exhaustive_structs)]
812pub struct SharedRandStatuses {
813    /// Global shared-random value for the previous shared-random period.
814    pub shared_rand_previous_value: Option<SharedRandStatus>,
815
816    /// Global shared-random value for the current shared-random period.
817    pub shared_rand_current_value: Option<SharedRandStatus>,
818
819    #[doc(hidden)]
820    #[deftly(netdoc(skip))]
821    pub __non_exhaustive: (),
822}
823
824/// Relay weight information - `w` item in routerstatus
825///
826/// This is a combination of two representations of (subsets of) the same information,
827/// from an optional `w` in the document.
828///
829///  * [`effective`](RelayWeightsItem::effective):
830///
831///    Always contains the effective weight, as [`RelayWeight`].
832///    This is what is used by clients.
833///    It does not record whether a `w` line was actually present.
834///
835///  * [`params`](RelayWeightsItem::params):
836///
837///    Can represent the presence and whole contents of the `w` line,
838///    including all the known and unknown parameters.
839///    This is within [`Unknown`], so it is only present with crate `feature = "retain-unknown"`,
840///    and only some constructors/parsers record it.
841///
842/// # Parsing
843///
844/// Parsing is done with `NetdocParseableFields` rather than `ItemValueParseable`.
845/// The `params` are [`Retained`](Unknown::Retained) if `retain_unknown_values` is
846/// selected in [`parse2::ParseOptions`].
847//
848// We use NetdocParseableFields because the containing document, RouterStatus,
849// contains `RelayWeightsItem` rather than `Option<RelayWeightsItem>`.
850// The item parsing multiplicity machinery would see plain `RelayWeightsItem` as a required item.
851//
852// This representation also means so that if retaining unknown information is compiled out
853// (ie, in clients) each routerstatus entry stored in memory does not need to record
854// whether `w` was present, merely what the implications were.
855//
856// We can't use ItemValueParseable with #[deftly(netdoc(default))]
857// because `RelayWeightsItem::default()` is a RelayWeightsItem that definitively
858// contains no pazrameters, ie with `Unknown::Retained`,
859// and is therefore only conditionally available.
860/// # Encoding
861///
862/// Encoding requires knowing whether a `w` line is to be included, and its contents,
863/// so is implemented only with if `effective` is `Unknown::Retained`.
864/// The encoding impl is only compiled in with `"retain-unknown"`,
865/// and throws [`Bug`] if applied to a `RelayWeightsItem` whose `params` are `Discarded`.
866///
867/// # Constructors
868///
869/// An "empty" `RelayWeightsItem` can be constructed with [`RelayWeightsItem::new_no_info`].
870///
871/// A `RelayWeightsItem` containing only the effective `RelayWeight`
872/// can be constructed using [`RelayWeightsItem::from_effective`].
873///
874/// With `"retain-unknown"`:
875/// a `RelayWeightsItem` can be constructed from a [`NetParams<u32>`] using `TryFrom`;
876/// and, implements `Default`, which yields a `RelayWeightsItem`
877/// representing the (known) absence of a `w` line.
878//
879// Fields are private to maintain the invariant.
880#[derive(Debug, Clone)]
881pub struct RelayWeightsItem {
882    /// The effective relay weight
883    effective: RelayWeight,
884
885    /// The complete parameter set, if available and `w` was present.
886    params: Unknown<Option<NetParams<u32>>>,
887}
888
889/// Recognized weight fields on a single relay in a consensus
890///
891/// The part of a `w` item that we understand as a client.
892#[non_exhaustive]
893#[derive(Debug, Clone, Copy)]
894pub enum RelayWeight {
895    /// An unmeasured weight for a relay.
896    Unmeasured(u32),
897    /// An measured weight for a relay.
898    Measured(u32),
899}
900
901/// Error processing a `w` line's netparams into an effective relay weight
902#[derive(Debug, Clone, thiserror::Error)]
903#[non_exhaustive]
904pub enum InvalidRelayWeights {
905    /// Invalid value for `Unmeasured`
906    #[error("invalid value for Unmeasured")]
907    InvalidUnmeasured,
908}
909
910/// Authority entry in a consensus - deprecated compatibility type alias
911#[deprecated = "renamed to ConsensusAuthorityEntry"]
912pub type ConsensusVoterInfo = ConsensusAuthorityEntry;
913
914/// Authority entry in a plain consensus - type alias provided for consistency
915pub type PlainAuthorityEntry = ConsensusAuthorityEntry;
916/// Authority entry in an md consensus - type alias provided for consistency
917pub type MdAuthorityEntry = ConsensusAuthorityEntry;
918
919/// An authority entry as found in a consensus
920///
921/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
922///
923/// See also [`VoteAuthorityEntry`]
924//
925// We don't use the `each_variety` system for this because:
926//  1. That avoids separating the two consensus authority entry types, which are identical
927//  2. The only common fields are `dir-source` and `contact`, so there is little duplication
928#[derive(Debug, Clone, Deftly)]
929#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
930#[allow(clippy::exhaustive_structs)]
931pub struct ConsensusAuthorityEntry {
932    /// Contents of the `dir-source` line about an authority
933    #[deftly(constructor)]
934    pub dir_source: DirSource,
935
936    /// Human-readable contact information about the authority
937    //
938    // If more non-intro fields get added that are the same in votes and cosensuses,
939    // consider using each_variety.rs or breaking those fields out into
940    // `AuthorityEntryCommon` implementing `NetdocParseableFields`, or something.
941    #[deftly(constructor)]
942    pub contact: ContactInfo,
943
944    /// Digest of the vote that the authority cast to contribute to
945    /// this consensus.
946    ///
947    /// This is not a fixed-length, fixed-algorithm field.
948    /// Bizarrely, the algorithm is supposed to be inferred from the length!
949    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:vote-digest>
950    #[deftly(netdoc(single_arg))]
951    #[deftly(constructor)]
952    pub vote_digest: B16U,
953
954    #[doc(hidden)]
955    #[deftly(netdoc(skip))]
956    pub __non_exhaustive: (),
957}
958
959/// An authority entry as found in a vote
960///
961/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority-entry>
962///
963/// See also [`ConsensusAuthorityEntry`]
964#[derive(Debug, Clone, Deftly)]
965#[derive_deftly(Constructor, NetdocEncodable, NetdocParseable)]
966#[allow(clippy::exhaustive_structs)]
967pub struct VoteAuthorityEntry {
968    /// Contents of the `dir-source` line about an authority
969    #[deftly(constructor)]
970    pub dir_source: DirSource,
971
972    /// Human-readable contact information about the authority
973    #[deftly(constructor)]
974    pub contact: ContactInfo,
975
976    /// `legacy-dir-key` - superseded authority identity key
977    ///
978    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:legacy-dir-key>
979    #[deftly(netdoc(single_arg))]
980    pub legacy_dir_key: Option<Fingerprint>,
981
982    /// `shared-rand-participate` - Indicate shared random participation
983    ///
984    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
985    pub shared_rand_participate: Option<SharedRandParticipate>,
986
987    /// `shared-rand-commit` - Shared random commitment
988    ///
989    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
990    pub shared_rand_commit: Vec<SharedRandCommit>,
991
992    /// Global shared-random values
993    #[deftly(netdoc(flatten))]
994    pub shared_rand: SharedRandStatuses,
995
996    #[doc(hidden)]
997    #[deftly(netdoc(skip))]
998    pub __non_exhaustive: (),
999}
1000
1001/// `shared-rand-participate` in a vote authority entry
1002///
1003/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-participate>
1004//
1005// We could have done `shared_rand_participate: Option<()>` in VoteAuthorityEntry,
1006// but then we might end up with variables of type `&Option<()>` etc.
1007// whose meaning has been detached from its type.
1008//
1009// TODO DIRAUTH rework this according to the API design conclusion from !3977 when there is one
1010#[derive(Debug, Clone, Deftly)]
1011#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
1012#[allow(clippy::exhaustive_structs)]
1013pub struct SharedRandParticipate {
1014    #[doc(hidden)]
1015    #[deftly(netdoc(skip))]
1016    pub __non_exhaustive: (),
1017}
1018
1019/// `shared-rand-commit` in a vote authority entry
1020///
1021/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
1022#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
1023// If new protocols use this item with a different version, we'll call it an API break.
1024#[allow(clippy::exhaustive_enums)]
1025pub enum SharedRandCommit {
1026    /// Version 1, the only one supported
1027    V1(SharedRandCommitV1),
1028
1029    /// Other versions.  Cannot be encoded.
1030    // It's not clear that future versions will use this version mechanism.  torspec#408.
1031    Unknown {},
1032}
1033
1034/// `shared-rand-commit` in a vote authority entry
1035///
1036/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:shared-rand-commit>
1037///
1038/// Version and hash are not explicitly represented.  See torspec#407.
1039///
1040/// `ItemValueEncodable` and `ItemValueParseable` impls do not include the fixed arguments;
1041/// in a netdoc, this type should be used within `SharedRandCommit::V1`.
1042#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deftly)]
1043#[derive_deftly(Constructor, ItemValueEncodable, ItemValueParseable)]
1044#[allow(clippy::exhaustive_structs)]
1045pub struct SharedRandCommitV1 {
1046    /// Authority id key, recapitulated.
1047    // TODO this field shouldn't here at all torspec#407
1048    #[deftly(constructor)]
1049    h_kp_auth_id_rsa: Fingerprint,
1050
1051    /// Commitment
1052    ///
1053    /// `TIMESTAMP || SHA3_256(REVEAL)`, as per
1054    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
1055    //
1056    // TOOD we would like to replace this with a type that separates out the pieces!
1057    // But that would need a FixedB64 generic over some tor-bytes trait, or something.
1058    #[deftly(constructor)]
1059    commit: FixedB64<40>,
1060
1061    /// Reveal
1062    ///
1063    /// `TIMESTAMP || random number`, as per
1064    /// <https://spec.torproject.org/srv-spec/specification.html#COMMITREVEAL>
1065    reveal: Option<FixedB64<40>>,
1066
1067    #[doc(hidden)]
1068    #[deftly(netdoc(skip))]
1069    pub __non_exhaustive: (),
1070}
1071
1072impl SharedRandCommitV1 {
1073    /// The fixed arguments that precede the actual value in `shared-rand-commit 1 ...`
1074    const FIXED_ARGUMENTS: &[&str] = &["1", "sha3-256"];
1075}
1076impl ItemValueEncodable for SharedRandCommit {
1077    fn write_item_value_onto(&self, mut out: ItemEncoder) -> StdResult<(), Bug> {
1078        match self {
1079            SharedRandCommit::V1(values) => {
1080                for fixed in SharedRandCommitV1::FIXED_ARGUMENTS {
1081                    out.args_raw_string(fixed);
1082                }
1083                values.write_item_value_onto(out)
1084            }
1085            SharedRandCommit::Unknown {} => Err(internal!("encoding SharedRandCommit::Unknown")),
1086        }
1087    }
1088}
1089impl ItemValueParseable for SharedRandCommit {
1090    fn from_unparsed(mut item: UnparsedItem<'_>) -> StdResult<Self, ErrorProblem> {
1091        let mut fixed = SharedRandCommitV1::FIXED_ARGUMENTS.iter().copied();
1092        let args = item.args_mut();
1093        let version = args
1094            .next()
1095            .ok_or_else(|| args.handle_error("version", ArgumentError::Missing))?;
1096        if version != fixed.next().expect("nonempty") {
1097            return Ok(SharedRandCommit::Unknown {});
1098        }
1099        for exp in fixed {
1100            let got = args
1101                .next()
1102                .ok_or_else(|| args.handle_error(exp, ArgumentError::Missing))?;
1103            if got != exp {
1104                return Err(args.handle_error(exp, ArgumentError::Invalid))?;
1105            }
1106        }
1107        let values = SharedRandCommitV1::from_unparsed(item)?;
1108        Ok(SharedRandCommit::V1(values))
1109    }
1110}
1111
1112// For `ConsensusAuthoritySection`, see `dir_source.rs`.
1113
1114define_derive_deftly! {
1115    /// Ad-hoc derive, `impl NetdocParseable for VoteAuthoritySection`
1116    ///
1117    /// We can't derive from `VoteAuthoritySection` with the normal macros, because
1118    /// it's not a document, with its own intro item.  It's just a collection of sub-documents.
1119    /// The netdoc derive macros don't have support for that - and it would be a fairly
1120    /// confusing thing to support because you'd end up with nested multiplicities and a whole
1121    /// variety of "intro item keywords" that were keywords for arbitrary sub-documents.
1122    ///
1123    /// Instead, we do that ad-hoc here.  It's less confusing because we don't need to
1124    /// worry about multiplicity, and because we know what only the outer document is
1125    /// that will contain this.
1126    VoteAuthoritySection:
1127
1128    ${defcond F_NORMAL not(fmeta(netdoc(skip)))}
1129
1130    #[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
1131    impl NetdocParseable for VoteAuthoritySection {
1132        fn doctype_for_error() -> &'static str {
1133            "vote.authority.section"
1134        }
1135        fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
1136            VoteAuthorityEntry::is_intro_item_keyword(kw)
1137        }
1138        fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
1139          $(
1140            ${when F_NORMAL}
1141            if let y @ Some(_) = $ftype::is_structural_keyword(kw) {
1142                return y;
1143            }
1144          )
1145            None
1146        }
1147        fn from_items<'s>(
1148            input: &mut ItemStream<'s>,
1149            stop_outer: stop_at!(),
1150        ) -> StdResult<Self, ErrorProblem> {
1151            let stop_inner = stop_outer
1152              $(
1153                ${when F_NORMAL}
1154                | StopAt($ftype::is_intro_item_keyword)
1155              )
1156            ;
1157            Ok(VoteAuthoritySection { $(
1158                ${when F_NORMAL}
1159                $fname: NetdocParseable::from_items(input, stop_inner)?,
1160            )
1161                __non_exhaustive: (),
1162            })
1163        }
1164    }
1165
1166    #[cfg(feature = "incomplete")]
1167    impl NetdocEncodable for VoteAuthoritySection {
1168        fn encode_unsigned(&self, out: &mut NetdocEncoder) -> StdResult<(), Bug> {
1169          $(
1170            ${when F_NORMAL}
1171            self.$fname.encode_unsigned(out)?;
1172          )
1173          Ok(())
1174        }
1175    }
1176}
1177
1178/// An authority section in a vote
1179///
1180/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:authority>
1181//
1182// We have split this out to help encapsulate vote/consensus-specific
1183// information in a forthcoming overall network status document type.
1184#[derive(Deftly, Clone, Debug)]
1185#[derive_deftly(VoteAuthoritySection, Constructor)]
1186#[allow(clippy::exhaustive_structs)]
1187#[cfg(feature = "incomplete")] // needs EncodedAuthCert, otherwise complete
1188pub struct VoteAuthoritySection {
1189    /// Authority entry
1190    #[deftly(constructor)]
1191    pub authority: VoteAuthorityEntry,
1192
1193    /// Authority key certificate
1194    #[deftly(constructor)]
1195    pub cert: EncodedAuthCert,
1196
1197    #[doc(hidden)]
1198    #[deftly(netdoc(skip))]
1199    pub __non_exhaustive: (),
1200}
1201
1202/// Fields in the footer of a consensus
1203///
1204/// <https://spec.torproject.org/dir-spec/consensus-formats.html#section:footer>
1205///
1206/// Not the whole footer, because it lacks the `directory-footer` item.
1207#[derive(Debug, Clone, Deftly)]
1208#[derive_deftly(Constructor, NetdocEncodableFields, NetdocParseableFields)]
1209#[allow(clippy::exhaustive_structs)]
1210pub struct ConsensusFooterFields {
1211    /// `bandwidth-weights`
1212    ///
1213    /// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:bandwidth-weights>
1214    #[deftly(netdoc(default))]
1215    pub bandwidth_weights: NetParams<i32>,
1216
1217    #[doc(hidden)]
1218    #[deftly(netdoc(skip))]
1219    pub __non_exhaustive: (),
1220}
1221
1222/// A consensus document that lists relays along with their
1223/// microdescriptor documents.
1224pub type MdConsensus = md::Consensus;
1225
1226/// An MdConsensus that has been parsed and checked for timeliness,
1227/// but not for signatures.
1228pub type UnvalidatedMdConsensus = md::UnvalidatedConsensus;
1229
1230/// An MdConsensus that has been parsed but not checked for signatures
1231/// and timeliness.
1232pub type UncheckedMdConsensus = md::UncheckedConsensus;
1233
1234/// A consensus document that lists relays along with their
1235/// router descriptor documents.
1236pub type PlainConsensus = plain::Consensus;
1237
1238/// An PlainConsensus that has been parsed and checked for timeliness,
1239/// but not for signatures.
1240pub type UnvalidatedPlainConsensus = plain::UnvalidatedConsensus;
1241
1242/// An PlainConsensus that has been parsed but not checked for signatures
1243/// and timeliness.
1244pub type UncheckedPlainConsensus = plain::UncheckedConsensus;
1245
1246decl_keyword! {
1247    /// Keywords that can be used in votes and consensuses.
1248    // TODO: This is public because otherwise we can't use it in the
1249    // ParseRouterStatus crate.  But I'd rather find a way to make it
1250    // private.
1251    #[non_exhaustive]
1252    #[allow(missing_docs)]
1253    pub NetstatusKwd {
1254        // Header
1255        "network-status-version" => NETWORK_STATUS_VERSION,
1256        "vote-status" => VOTE_STATUS,
1257        "consensus-methods" => CONSENSUS_METHODS,
1258        "consensus-method" => CONSENSUS_METHOD,
1259        "published" => PUBLISHED,
1260        "valid-after" => VALID_AFTER,
1261        "fresh-until" => FRESH_UNTIL,
1262        "valid-until" => VALID_UNTIL,
1263        "voting-delay" => VOTING_DELAY,
1264        "client-versions" => CLIENT_VERSIONS,
1265        "server-versions" => SERVER_VERSIONS,
1266        "known-flags" => KNOWN_FLAGS,
1267        "flag-thresholds" => FLAG_THRESHOLDS,
1268        "recommended-client-protocols" => RECOMMENDED_CLIENT_PROTOCOLS,
1269        "required-client-protocols" => REQUIRED_CLIENT_PROTOCOLS,
1270        "recommended-relay-protocols" => RECOMMENDED_RELAY_PROTOCOLS,
1271        "required-relay-protocols" => REQUIRED_RELAY_PROTOCOLS,
1272        "params" => PARAMS,
1273        "bandwidth-file-headers" => BANDWIDTH_FILE_HEADERS,
1274        "bandwidth-file-digest" => BANDWIDTH_FILE_DIGEST,
1275        // "package" is now ignored.
1276
1277        // header in consensus, voter section in vote?
1278        "shared-rand-previous-value" => SHARED_RAND_PREVIOUS_VALUE,
1279        "shared-rand-current-value" => SHARED_RAND_CURRENT_VALUE,
1280
1281        // Voter section (both)
1282        "dir-source" => DIR_SOURCE,
1283        "contact" => CONTACT,
1284
1285        // voter section (vote, but not consensus)
1286        "legacy-dir-key" => LEGACY_DIR_KEY,
1287        "shared-rand-participate" => SHARED_RAND_PARTICIPATE,
1288        "shared-rand-commit" => SHARED_RAND_COMMIT,
1289
1290        // voter section (consensus, but not vote)
1291        "vote-digest" => VOTE_DIGEST,
1292
1293        // voter cert beginning (but only the beginning)
1294        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
1295
1296        // routerstatus
1297        "r" => RS_R,
1298        "a" => RS_A,
1299        "s" => RS_S,
1300        "v" => RS_V,
1301        "pr" => RS_PR,
1302        "w" => RS_W,
1303        "p" => RS_P,
1304        "m" => RS_M,
1305        "id" => RS_ID,
1306
1307        // footer
1308        "directory-footer" => DIRECTORY_FOOTER,
1309        "bandwidth-weights" => BANDWIDTH_WEIGHTS,
1310        "directory-signature" => DIRECTORY_SIGNATURE,
1311    }
1312}
1313
1314/// Shared parts of rules for all kinds of netstatus headers
1315static NS_HEADER_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> = LazyLock::new(|| {
1316    use NetstatusKwd::*;
1317    let mut rules = SectionRules::builder();
1318    rules.add(NETWORK_STATUS_VERSION.rule().required().args(1..=2));
1319    rules.add(VOTE_STATUS.rule().required().args(1..));
1320    rules.add(VALID_AFTER.rule().required());
1321    rules.add(FRESH_UNTIL.rule().required());
1322    rules.add(VALID_UNTIL.rule().required());
1323    rules.add(VOTING_DELAY.rule().args(2..));
1324    rules.add(CLIENT_VERSIONS.rule());
1325    rules.add(SERVER_VERSIONS.rule());
1326    rules.add(KNOWN_FLAGS.rule().required());
1327    rules.add(RECOMMENDED_CLIENT_PROTOCOLS.rule().args(1..));
1328    rules.add(RECOMMENDED_RELAY_PROTOCOLS.rule().args(1..));
1329    rules.add(REQUIRED_CLIENT_PROTOCOLS.rule().args(1..));
1330    rules.add(REQUIRED_RELAY_PROTOCOLS.rule().args(1..));
1331    rules.add(PARAMS.rule());
1332    rules
1333});
1334/// Rules for parsing the header of a consensus.
1335static NS_HEADER_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1336    use NetstatusKwd::*;
1337    let mut rules = NS_HEADER_RULES_COMMON_.clone();
1338    rules.add(CONSENSUS_METHOD.rule().args(1..=1));
1339    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
1340    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
1341    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1342    rules.build()
1343});
1344/*
1345/// Rules for parsing the header of a vote.
1346static NS_HEADER_RULES_VOTE: SectionRules<NetstatusKwd> = {
1347    use NetstatusKwd::*;
1348    let mut rules = NS_HEADER_RULES_COMMON_.clone();
1349    rules.add(CONSENSUS_METHODS.rule().args(1..));
1350    rules.add(FLAG_THRESHOLDS.rule());
1351    rules.add(BANDWIDTH_FILE_HEADERS.rule());
1352    rules.add(BANDWIDTH_FILE_DIGEST.rule().args(1..));
1353    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1354    rules
1355};
1356/// Rules for parsing a single voter's information in a vote.
1357static NS_VOTERINFO_RULES_VOTE: SectionRules<NetstatusKwd> = {
1358    use NetstatusKwd::*;
1359    let mut rules = SectionRules::new();
1360    rules.add(DIR_SOURCE.rule().required().args(6..));
1361    rules.add(CONTACT.rule().required());
1362    rules.add(LEGACY_DIR_KEY.rule().args(1..));
1363    rules.add(SHARED_RAND_PARTICIPATE.rule().no_args());
1364    rules.add(SHARED_RAND_COMMIT.rule().may_repeat().args(4..));
1365    rules.add(SHARED_RAND_PREVIOUS_VALUE.rule().args(2..));
1366    rules.add(SHARED_RAND_CURRENT_VALUE.rule().args(2..));
1367    // then comes an entire cert: When we implement vote parsing,
1368    // we should use the authcert code for handling that.
1369    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1370    rules
1371};
1372 */
1373/// Rules for parsing a single voter's information in a consensus
1374static NS_VOTERINFO_RULES_CONSENSUS: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1375    use NetstatusKwd::*;
1376    let mut rules = SectionRules::builder();
1377    rules.add(DIR_SOURCE.rule().required().args(6..));
1378    rules.add(CONTACT.rule().required());
1379    rules.add(VOTE_DIGEST.rule().required());
1380    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1381    rules.build()
1382});
1383/// Shared rules for parsing a single routerstatus
1384static NS_ROUTERSTATUS_RULES_COMMON_: LazyLock<SectionRulesBuilder<NetstatusKwd>> =
1385    LazyLock::new(|| {
1386        use NetstatusKwd::*;
1387        let mut rules = SectionRules::builder();
1388        rules.add(RS_A.rule().may_repeat().args(1..));
1389        rules.add(RS_S.rule().required());
1390        rules.add(RS_V.rule());
1391        rules.add(RS_PR.rule().required());
1392        rules.add(RS_W.rule());
1393        rules.add(RS_P.rule().args(2..));
1394        rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1395        rules
1396    });
1397
1398/// Rules for parsing a single routerstatus in an NS consensus
1399static NS_ROUTERSTATUS_RULES_PLAIN: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1400    use NetstatusKwd::*;
1401    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1402    rules.add(RS_R.rule().required().args(8..));
1403    rules.build()
1404});
1405
1406/*
1407/// Rules for parsing a single routerstatus in a vote
1408static NS_ROUTERSTATUS_RULES_VOTE: SectionRules<NetstatusKwd> = {
1409    use NetstatusKwd::*;
1410        let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1411        rules.add(RS_R.rule().required().args(8..));
1412        rules.add(RS_M.rule().may_repeat().args(2..));
1413        rules.add(RS_ID.rule().may_repeat().args(2..)); // may-repeat?
1414        rules
1415    };
1416*/
1417/// Rules for parsing a single routerstatus in a microdesc consensus
1418static NS_ROUTERSTATUS_RULES_MDCON: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1419    use NetstatusKwd::*;
1420    let mut rules = NS_ROUTERSTATUS_RULES_COMMON_.clone();
1421    rules.add(RS_R.rule().required().args(6..));
1422    rules.add(RS_M.rule().required().args(1..));
1423    rules.build()
1424});
1425/// Rules for parsing consensus fields from a footer.
1426static NS_FOOTER_RULES: LazyLock<SectionRules<NetstatusKwd>> = LazyLock::new(|| {
1427    use NetstatusKwd::*;
1428    let mut rules = SectionRules::builder();
1429    rules.add(DIRECTORY_FOOTER.rule().required().no_args());
1430    // consensus only
1431    rules.add(BANDWIDTH_WEIGHTS.rule());
1432    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
1433    rules.build()
1434});
1435
1436impl ProtoStatus {
1437    /// Construct a ProtoStatus from two chosen keywords in a section.
1438    fn from_section(
1439        sec: &Section<'_, NetstatusKwd>,
1440        recommend_token: NetstatusKwd,
1441        required_token: NetstatusKwd,
1442    ) -> Result<ProtoStatus> {
1443        /// Helper: extract a Protocols entry from an item's arguments.
1444        fn parse(t: Option<&Item<'_, NetstatusKwd>>) -> Result<Protocols> {
1445            if let Some(item) = t {
1446                item.args_as_str()
1447                    .parse::<Protocols>()
1448                    .map_err(|e| EK::BadArgument.at_pos(item.pos()).with_source(e))
1449            } else {
1450                Ok(Protocols::new())
1451            }
1452        }
1453
1454        let recommended = parse(sec.get(recommend_token))?;
1455        let required = parse(sec.get(required_token))?;
1456        Ok(ProtoStatus {
1457            recommended,
1458            required,
1459        })
1460    }
1461
1462    /// Return the protocols that are listed as "required" in this `ProtoStatus`.
1463    ///
1464    /// Implementations may assume that relays on the network implement all the
1465    /// protocols in the relays' required-protocols list.  Implementations should
1466    /// refuse to start if they do not implement all the protocols on their own
1467    /// (client or relay) required-protocols list.
1468    pub fn required_protocols(&self) -> &Protocols {
1469        &self.required
1470    }
1471
1472    /// Return the protocols that are listed as "recommended" in this `ProtoStatus`.
1473    ///
1474    /// Implementations should warn if they do not implement all the protocols
1475    /// on their own (client or relay) recommended-protocols list.
1476    pub fn recommended_protocols(&self) -> &Protocols {
1477        &self.recommended
1478    }
1479}
1480
1481impl<T> std::str::FromStr for NetParams<T>
1482where
1483    T: std::str::FromStr,
1484    T::Err: std::error::Error,
1485{
1486    type Err = Error;
1487    fn from_str(s: &str) -> Result<Self> {
1488        /// Helper: parse a single K=V pair.
1489        fn parse_pair<U>(p: &str) -> Result<(String, U)>
1490        where
1491            U: std::str::FromStr,
1492            U::Err: std::error::Error,
1493        {
1494            let parts: Vec<_> = p.splitn(2, '=').collect();
1495            if parts.len() != 2 {
1496                return Err(EK::BadArgument
1497                    .at_pos(Pos::at(p))
1498                    .with_msg("Missing = in key=value list"));
1499            }
1500            let num = parts[1].parse::<U>().map_err(|e| {
1501                EK::BadArgument
1502                    .at_pos(Pos::at(parts[1]))
1503                    .with_msg(e.to_string())
1504            })?;
1505            Ok((parts[0].to_string(), num))
1506        }
1507
1508        let params = s
1509            .split(' ')
1510            .filter(|p| !p.is_empty())
1511            .map(parse_pair)
1512            .collect::<Result<HashMap<_, _>>>()?;
1513        Ok(NetParams { params })
1514    }
1515}
1516
1517impl FromStr for SharedRandVal {
1518    type Err = Error;
1519    fn from_str(s: &str) -> Result<Self> {
1520        let val: B64 = s.parse()?;
1521        let val = SharedRandVal(val.into_array()?);
1522        Ok(val)
1523    }
1524}
1525impl Display for SharedRandVal {
1526    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1527        Display::fmt(&B64::from(Vec::from(self.0)), f)
1528    }
1529}
1530impl NormalItemArgument for SharedRandVal {}
1531
1532impl SharedRandStatus {
1533    /// Parse a current or previous shared rand value from a given
1534    /// SharedRandPreviousValue or SharedRandCurrentValue.
1535    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
1536        match item.kwd() {
1537            NetstatusKwd::SHARED_RAND_PREVIOUS_VALUE | NetstatusKwd::SHARED_RAND_CURRENT_VALUE => {}
1538            _ => {
1539                return Err(Error::from(internal!(
1540                    "wrong keyword {:?} on shared-random value",
1541                    item.kwd()
1542                ))
1543                .at_pos(item.pos()));
1544            }
1545        }
1546        let n_reveals: u8 = item.parse_arg(0)?;
1547        let value: SharedRandVal = item.parse_arg(1)?;
1548        // Added in proposal 342
1549        let timestamp = item.parse_optional_arg::<Iso8601TimeNoSp>(2)?;
1550        Ok(SharedRandStatus {
1551            n_reveals,
1552            value,
1553            timestamp,
1554        })
1555    }
1556
1557    /// Return the actual shared random value.
1558    pub fn value(&self) -> &SharedRandVal {
1559        &self.value
1560    }
1561
1562    /// Return the timestamp (if any) associated with this `SharedRandValue`.
1563    pub fn timestamp(&self) -> Option<std::time::SystemTime> {
1564        self.timestamp.map(|t| t.0)
1565    }
1566}
1567
1568impl DirSource {
1569    /// Parse a "dir-source" item
1570    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Self> {
1571        if item.kwd() != NetstatusKwd::DIR_SOURCE {
1572            return Err(
1573                Error::from(internal!("Bad keyword {:?} on dir-source", item.kwd()))
1574                    .at_pos(item.pos()),
1575            );
1576        }
1577        let nickname = item
1578            .required_arg(0)?
1579            .parse()
1580            .map_err(|e: InvalidNickname| {
1581                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1582            })?;
1583        let identity = item.parse_arg(1)?;
1584        let hostname = item
1585            .required_arg(2)?
1586            .parse()
1587            .map_err(|e: InvalidInternetHost| {
1588                EK::BadArgument.at_pos(item.pos()).with_msg(e.to_string())
1589            })?;
1590        let ip = item.parse_arg(3)?;
1591        let dir_port = item.parse_arg(4)?;
1592        let or_port = item.parse_arg(5)?;
1593
1594        Ok(DirSource {
1595            nickname,
1596            identity,
1597            hostname,
1598            ip,
1599            dir_port,
1600            or_port,
1601            __non_exhaustive: (),
1602        })
1603    }
1604}
1605
1606impl ConsensusAuthorityEntry {
1607    /// Parse a single ConsensusAuthorityEntry from a voter info section.
1608    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<ConsensusAuthorityEntry> {
1609        use NetstatusKwd::*;
1610        // this unwrap should be safe because if there is not at least one
1611        // token in the section, the section is unparsable.
1612        #[allow(clippy::unwrap_used)]
1613        let first = sec.first_item().unwrap();
1614        if first.kwd() != DIR_SOURCE {
1615            return Err(Error::from(internal!(
1616                "Wrong keyword {:?} at start of voter info",
1617                first.kwd()
1618            ))
1619            .at_pos(first.pos()));
1620        }
1621        let dir_source = DirSource::from_item(sec.required(DIR_SOURCE)?)?;
1622
1623        let contact = sec.required(CONTACT)?;
1624        // Ideally we would parse_args_as_str but that requires us to
1625        // impl From<InvalidContactInfo> for crate::Error which is wrong
1626        // because many it's a footgun which lets you just write ? here
1627        // resulting in lack of position information.
1628        // (This is a general problem with the error handling in crate::parse.)
1629        let contact = contact
1630            .args_as_str()
1631            .parse()
1632            .map_err(|err: InvalidContactInfo| {
1633                EK::BadArgument
1634                    .with_msg(err.to_string())
1635                    .at_pos(contact.pos())
1636            })?;
1637
1638        let vote_digest = sec.required(VOTE_DIGEST)?.parse_arg::<B16U>(0)?;
1639
1640        Ok(ConsensusAuthorityEntry {
1641            dir_source,
1642            contact,
1643            vote_digest,
1644            __non_exhaustive: (),
1645        })
1646    }
1647}
1648
1649impl RelayWeightsItem {
1650    /// Return a new `RelayWeightsItem` containing no information
1651    ///
1652    /// As if parsed from a document with no `w` line, discarding unknown information.
1653    pub fn new_no_info() -> Self {
1654        RelayWeightsItem {
1655            effective: RelayWeight::default(),
1656            params: Unknown::new_discard(),
1657        }
1658    }
1659
1660    /// Return a new `RelayWeightsItem` containing only the effective weight
1661    pub fn from_effective(effective: RelayWeight) -> Self {
1662        RelayWeightsItem {
1663            effective,
1664            params: Unknown::new_discard(),
1665        }
1666    }
1667
1668    /// Get the effective relay weight (bandwidth estimate) for path selection.
1669    ///
1670    /// Invariant: consistent with from [`params`](RelayWeightsItem::params),
1671    /// if `parsed` isn't [`Discarded`](Unknown::Discarded).
1672    //
1673    // We open-code this rather than deriving it so we can provide better docs.
1674    pub fn effective(&self) -> RelayWeight {
1675        self.effective
1676    }
1677
1678    /// Get the complete parameter set, if this information is available.
1679    ///
1680    /// After parsing, this is the parsed but not interpreted `w` item,
1681    /// or `None` if the document contained no `w` item.
1682    //
1683    // We open-code this rather than deriving it because we want to return
1684    // `Unknown<&...>` rather than `&Unknown<..>`, which the user would just have to .as_ref().
1685    pub fn params(&self) -> Unknown<&Option<NetParams<u32>>> {
1686        self.params.as_ref()
1687    }
1688
1689    /// Parse a routerweight from a "w" line.
1690    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<RelayWeightsItem> {
1691        if item.kwd() != NetstatusKwd::RS_W {
1692            return Err(
1693                Error::from(internal!("Wrong keyword {:?} on W line", item.kwd()))
1694                    .at_pos(item.pos()),
1695            );
1696        }
1697
1698        let params = item.args_as_str().parse()?;
1699        let effective = RelayWeight::from_net_params(&params).map_err(|e| e.at_pos(item.pos()))?;
1700
1701        Ok(RelayWeightsItem {
1702            effective,
1703            params: Unknown::new_discard(),
1704        })
1705    }
1706
1707    /// The keyword for parsing and encoding
1708    const KEYWORD: &str = "w";
1709}
1710
1711#[cfg(feature = "retain-unknown")]
1712impl Default for RelayWeightsItem {
1713    fn default() -> Self {
1714        RelayWeightsItem {
1715            effective: RelayWeight::default(),
1716            params: Unknown::Retained(None),
1717        }
1718    }
1719}
1720
1721impl RelayWeight {
1722    /// Return true if this weight is the result of a successful measurement
1723    pub fn is_measured(&self) -> bool {
1724        matches!(self, RelayWeight::Measured(_))
1725    }
1726
1727    /// Return true if this weight is nonzero
1728    pub fn is_nonzero(&self) -> bool {
1729        !matches!(self, RelayWeight::Unmeasured(0) | RelayWeight::Measured(0))
1730    }
1731
1732    /// Parse a routerweight from partially-parsed `w` line in the form of a `NetParams`
1733    ///
1734    /// This function is the common part shared between `parse2` and `parse`.
1735    fn from_net_params(params: &NetParams<u32>) -> Result<RelayWeight> {
1736        params
1737            .try_into()
1738            .map_err(|e: InvalidRelayWeights| EK::BadArgument.with_msg(e.to_string()))
1739    }
1740}
1741
1742impl Default for RelayWeight {
1743    fn default() -> RelayWeight {
1744        RelayWeight::Unmeasured(0)
1745    }
1746}
1747
1748impl TryFrom<&NetParams<u32>> for RelayWeight {
1749    type Error = InvalidRelayWeights;
1750
1751    fn try_from(params: &NetParams<u32>) -> StdResult<RelayWeight, InvalidRelayWeights> {
1752        let bw = params.params.get("Bandwidth");
1753        let unmeas = params.params.get("Unmeasured");
1754
1755        let bw = match bw {
1756            None => return Ok(RelayWeight::Unmeasured(0)),
1757            Some(b) => *b,
1758        };
1759
1760        match unmeas {
1761            None | Some(0) => Ok(RelayWeight::Measured(bw)),
1762            Some(1) => Ok(RelayWeight::Unmeasured(bw)),
1763            _ => Err(InvalidRelayWeights::InvalidUnmeasured),
1764        }
1765    }
1766}
1767
1768#[cfg(feature = "retain-unknown")]
1769impl TryFrom<NetParams<u32>> for RelayWeightsItem {
1770    type Error = InvalidRelayWeights;
1771
1772    fn try_from(params: NetParams<u32>) -> StdResult<RelayWeightsItem, InvalidRelayWeights> {
1773        Ok(RelayWeightsItem {
1774            effective: (&params).try_into()?,
1775            params: Unknown::Retained(Some(params)),
1776        })
1777    }
1778}
1779
1780/// `parse2` impls for types in this modulea
1781///
1782/// Separate module for a separate namespace.
1783mod parse2_impls {
1784    use super::*;
1785    pub(super) use parse2::{
1786        ArgumentError as AE, ArgumentStream, ErrorProblem as EP, ItemArgumentParseable,
1787        ItemValueParseable, NetdocParseableFields,
1788    };
1789    use std::result::Result;
1790
1791    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
1792    impl<T: FromStr + NormalItemArgument> ItemValueParseable for NetParams<T>
1793    where
1794        T::Err: std::error::Error,
1795    {
1796        fn from_unparsed(item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1797            item.check_no_object()?;
1798            item.args_copy()
1799                .into_remaining()
1800                .parse()
1801                .map_err(item.invalid_argument_handler("parameters"))
1802        }
1803    }
1804
1805    impl NetdocParseableFields for RelayWeightsItem {
1806        type Accumulator = Option<NetParams<u32>>;
1807
1808        fn is_item_keyword(kw: KeywordRef) -> bool {
1809            kw == Self::KEYWORD
1810        }
1811
1812        fn accumulate_item(acc: &mut Self::Accumulator, item: UnparsedItem) -> Result<(), EP> {
1813            if acc.is_some() {
1814                return Err(EP::ItemRepeated);
1815            }
1816            item.check_no_object()?;
1817            let params = NetParams::from_unparsed(item)?;
1818            *acc = Some(params);
1819            Ok(())
1820        }
1821
1822        fn finish(params: Self::Accumulator, items: &ItemStream) -> Result<Self, EP> {
1823            let effective = params
1824                .as_ref()
1825                .map(TryFrom::try_from)
1826                .transpose()
1827                .map_err(|_| EP::OtherBadDocument("invalid information in `w` item"))?
1828                .unwrap_or_default();
1829
1830            let params = items.parse_options().retain_unknown_values.map(|()| params);
1831
1832            Ok(RelayWeightsItem { effective, params })
1833        }
1834    }
1835
1836    impl ItemValueParseable for rs::SoftwareVersion {
1837        fn from_unparsed(mut item: parse2::UnparsedItem<'_>) -> Result<Self, EP> {
1838            item.check_no_object()?;
1839            item.args_mut()
1840                .into_remaining()
1841                .parse()
1842                .map_err(item.invalid_argument_handler("version"))
1843        }
1844    }
1845
1846    impl ItemArgumentParseable for IgnoredPublicationTimeSp {
1847        fn from_args(a: &mut ArgumentStream) -> Result<IgnoredPublicationTimeSp, AE> {
1848            let mut next_arg = || a.next().ok_or(AE::Missing);
1849            let _: &str = next_arg()?;
1850            let _: &str = next_arg()?;
1851            Ok(IgnoredPublicationTimeSp)
1852        }
1853    }
1854}
1855
1856/// `encode` impls for types in this modulea
1857///
1858/// Separate module for a separate namespace.
1859mod encode_impls {
1860    use super::*;
1861    use std::result::Result;
1862    pub(crate) use {
1863        crate::encode::{ItemEncoder, ItemValueEncodable, NetdocEncodableFields},
1864        tor_error::Bug,
1865    };
1866
1867    #[cfg(feature = "incomplete")] // untested
1868    impl NetdocEncodableFields for RelayWeightsItem {
1869        fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
1870            if let Some(w) = self.params.as_ref().into_retained()? {
1871                w.write_item_value_onto(out.item(Self::KEYWORD))?;
1872            }
1873            Ok(())
1874        }
1875    }
1876
1877    // The NormalItemArgument bound ensures that this is applied only to sane types eg integers
1878    impl<T: NormalItemArgument + Ord + Display> ItemValueEncodable for NetParams<T> {
1879        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
1880            for (k, v) in self.iter().collect::<BTreeSet<_>>() {
1881                if k.is_empty()
1882                    || k.chars()
1883                        .any(|c| c.is_whitespace() || c.is_control() || c == '=')
1884                {
1885                    // TODO torspec#401 see TODO in NetParams<T> definition
1886                    return Err(bad_api_usage!(
1887                        "tried to encode NetParms with unreasonable keyword {k:?}"
1888                    ));
1889                }
1890                out.args_raw_string(&format_args!("{k}={v}"));
1891            }
1892            Ok(())
1893        }
1894    }
1895
1896    impl ItemValueEncodable for rs::SoftwareVersion {
1897        fn write_item_value_onto(&self, mut out: ItemEncoder) -> Result<(), Bug> {
1898            out.args_raw_string(self);
1899            Ok(())
1900        }
1901    }
1902
1903    impl ItemArgument for IgnoredPublicationTimeSp {
1904        fn write_arg_onto(&self, out: &mut ItemEncoder) -> Result<(), Bug> {
1905            out.args_raw_string(&"2000-01-01 00:00:01");
1906            Ok(())
1907        }
1908    }
1909}
1910
1911impl ConsensusFooterFields {
1912    /// Parse a directory footer from a footer section.
1913    fn from_section(sec: &Section<'_, NetstatusKwd>) -> Result<ConsensusFooterFields> {
1914        use NetstatusKwd::*;
1915        sec.required(DIRECTORY_FOOTER)?;
1916
1917        let bandwidth_weights = sec
1918            .maybe(BANDWIDTH_WEIGHTS)
1919            .args_as_str()
1920            .unwrap_or("")
1921            .parse()?;
1922
1923        Ok(ConsensusFooterFields {
1924            bandwidth_weights,
1925            __non_exhaustive: (),
1926        })
1927    }
1928}
1929
1930/// `ProtoStatuses` parsing and encoding
1931///
1932/// Separate module for separate namespace
1933mod proto_statuses_parse2_encode {
1934    use super::encode_impls::*;
1935    use super::parse2_impls::*;
1936    use super::*;
1937    use paste::paste;
1938    use std::result::Result;
1939
1940    /// Implements `NetdocParseableFields` for `ProtoStatuses`
1941    ///
1942    /// We have this macro so that it's impossible to write things like
1943    /// ```text
1944    ///      ProtoStatuses {
1945    ///          client: ProtoStatus {
1946    ///              recommended: something something recommended_relay_versions something,
1947    /// ```
1948    ///
1949    /// (The structure of `ProtoStatuses` means the normal parse2 derive won't work for it.
1950    /// Note the bug above: the recommended *relay* version info is put in the *client* field.
1951    /// Preventing this bug must involve: avoiding writing twice the field name elements,
1952    /// such as `relay` and `client`, during this kind of construction/conversion.)
1953    macro_rules! impl_proto_statuses { { $( $rr:ident $cr:ident; )* } => { paste! {
1954        #[derive(Deftly)]
1955        #[derive_deftly(NetdocParseableFields)]
1956        // Only ProtoStatusesParseNetdocParseAccumulator is exposed.
1957        #[allow(unreachable_pub)]
1958        pub struct ProtoStatusesParseHelper {
1959            $(
1960                #[deftly(netdoc(default))]
1961                [<$rr _ $cr _protocols>]: Protocols,
1962            )*
1963        }
1964
1965        /// Partially parsed `ProtoStatuses`
1966        pub use ProtoStatusesParseHelperNetdocParseAccumulator
1967            as ProtoStatusesNetdocParseAccumulator;
1968
1969        impl NetdocParseableFields for ProtoStatuses {
1970            type Accumulator = ProtoStatusesNetdocParseAccumulator;
1971            fn is_item_keyword(kw: KeywordRef<'_>) -> bool {
1972                ProtoStatusesParseHelper::is_item_keyword(kw)
1973            }
1974            fn accumulate_item(
1975                acc: &mut Self::Accumulator,
1976                item: UnparsedItem<'_>,
1977            ) -> Result<(), EP> {
1978                ProtoStatusesParseHelper::accumulate_item(acc, item)
1979            }
1980            fn finish(acc: Self::Accumulator, items: &ItemStream<'_>) -> Result<Self, EP> {
1981                let parse = ProtoStatusesParseHelper::finish(acc, items)?;
1982                let mut out = ProtoStatuses::default();
1983                $(
1984                    out.$cr.$rr = parse.[< $rr _ $cr _protocols >];
1985                )*
1986                Ok(out)
1987            }
1988        }
1989
1990        impl NetdocEncodableFields for ProtoStatuses {
1991            fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
1992              $(
1993                self.$cr.$rr.write_item_value_onto(
1994                    out.item(stringify!([<$rr _ $cr _protocols>]))
1995                )?;
1996              )*
1997                Ok(())
1998            }
1999        }
2000    } } }
2001
2002    impl_proto_statuses! {
2003        required client;
2004        required relay;
2005        recommended client;
2006        recommended relay;
2007    }
2008}
2009
2010/// Result of checking a single authority signature.
2011enum SigCheckResult {
2012    /// The signature checks out.  Great!
2013    Valid,
2014    /// The signature is invalid; no additional information could make it
2015    /// valid.
2016    Invalid,
2017    /// We can't check the signature because we don't have a
2018    /// certificate with the right signing key.
2019    MissingCert,
2020}
2021
2022impl Signature {
2023    /// Parse a Signature from a directory-signature section
2024    fn from_item(item: &Item<'_, NetstatusKwd>) -> Result<Signature> {
2025        if item.kwd() != NetstatusKwd::DIRECTORY_SIGNATURE {
2026            return Err(Error::from(internal!(
2027                "Wrong keyword {:?} for directory signature",
2028                item.kwd()
2029            ))
2030            .at_pos(item.pos()));
2031        }
2032
2033        let (digest_algo, id_fp, sk_fp) = if item.n_args() > 2 {
2034            (
2035                item.required_arg(0)?,
2036                item.required_arg(1)?,
2037                item.required_arg(2)?,
2038            )
2039        } else {
2040            ("sha1", item.required_arg(0)?, item.required_arg(1)?)
2041        };
2042
2043        let digest_algo = digest_algo.to_string().parse().void_unwrap();
2044        let digest_algo = DigestAlgoInSignature(Some(digest_algo));
2045        let id_fingerprint = id_fp.parse::<Fingerprint>()?.into();
2046        let sk_fingerprint = sk_fp.parse::<Fingerprint>()?.into();
2047        let key_ids = AuthCertKeyIds {
2048            id_fingerprint,
2049            sk_fingerprint,
2050        };
2051        let signature = item.obj("SIGNATURE")?;
2052
2053        Ok(Signature {
2054            digest_algo,
2055            key_ids,
2056            signature,
2057        })
2058    }
2059
2060    /// Return true if this signature has the identity key and signing key
2061    /// that match a given cert.
2062    fn matches_cert(&self, cert: &AuthCert) -> bool {
2063        cert.key_ids() == self.key_ids
2064    }
2065
2066    /// If possible, find the right certificate for checking this signature
2067    /// from among a slice of certificates.
2068    fn find_cert<'a>(&self, certs: &'a [AuthCert]) -> Option<&'a AuthCert> {
2069        certs.iter().find(|&c| self.matches_cert(c))
2070    }
2071
2072    /// Try to check whether this signature is a valid signature of a
2073    /// provided digest, given a slice of certificates that might contain
2074    /// its signing key.
2075    fn check_signature(&self, signed_digest: &[u8], certs: &[AuthCert]) -> SigCheckResult {
2076        match self.find_cert(certs) {
2077            None => SigCheckResult::MissingCert,
2078            Some(cert) => {
2079                let key = cert.signing_key();
2080                match key.verify(signed_digest, &self.signature[..]) {
2081                    Ok(()) => SigCheckResult::Valid,
2082                    Err(_) => SigCheckResult::Invalid,
2083                }
2084            }
2085        }
2086    }
2087}
2088
2089impl SignatureGroup {
2090    // TODO: these functions are pretty similar and could probably stand to be
2091    // refactored a lot.
2092
2093    /// Helper: Return a pair of the number of possible authorities'
2094    /// signatures in this object for which we _could_ find certs, and
2095    /// a list of the signatures we couldn't find certificates for.
2096    fn list_missing(&self, certs: &[AuthCert]) -> (usize, Vec<&Signature>) {
2097        let mut ok: HashSet<RsaIdentity> = HashSet::new();
2098        let mut missing = Vec::new();
2099        for sig in &self.signatures {
2100            let id_fingerprint = &sig.key_ids.id_fingerprint;
2101            if ok.contains(id_fingerprint) {
2102                continue;
2103            }
2104            if sig.find_cert(certs).is_some() {
2105                ok.insert(*id_fingerprint);
2106                continue;
2107            }
2108
2109            missing.push(sig);
2110        }
2111        (ok.len(), missing)
2112    }
2113
2114    /// Given a list of authority identity key fingerprints, return true if
2115    /// this signature group is _potentially_ well-signed according to those
2116    /// authorities.
2117    fn could_validate(&self, authorities: &[&RsaIdentity]) -> bool {
2118        let mut signed_by: HashSet<RsaIdentity> = HashSet::new();
2119        for sig in &self.signatures {
2120            let id_fp = &sig.key_ids.id_fingerprint;
2121            if signed_by.contains(id_fp) {
2122                // Already found this in the list.
2123                continue;
2124            }
2125            if authorities.contains(&id_fp) {
2126                signed_by.insert(*id_fp);
2127            }
2128        }
2129
2130        signed_by.len() > (authorities.len() / 2)
2131    }
2132
2133    /// Return true if the signature group defines a valid signature.
2134    ///
2135    /// A signature is valid if it signed by more than half of the
2136    /// authorities.  This API requires that `n_authorities` is the number of
2137    /// authorities we believe in, and that every cert in `certs` belongs
2138    /// to a real authority.
2139    fn validate(&self, n_authorities: usize, certs: &[AuthCert]) -> bool {
2140        // A set of the authorities (by identity) who have have signed
2141        // this document.  We use a set here in case `certs` has more
2142        // than one certificate for a single authority.
2143        let mut ok: HashSet<RsaIdentity> = HashSet::new();
2144
2145        for sig in &self.signatures {
2146            let id_fingerprint = &sig.key_ids.id_fingerprint;
2147            if ok.contains(id_fingerprint) {
2148                // We already checked at least one signature using this
2149                // authority's identity fingerprint.
2150                continue;
2151            }
2152
2153            use DirectorySignatureHashAlgo as DSHA;
2154            use KeywordOrString as KOS;
2155
2156            let d: Option<&[u8]> = match sig.digest_algo.algorithm() {
2157                KOS::Known(DSHA::Sha256) => self.hashes.sha256.as_ref().map(|a| &a[..]),
2158                // TODO #2530 this needs to depend on whether `sha1` was stated (!)
2159                KOS::Known(DSHA::Sha1) => self.hashes.sha1.as_ref().map(|a| &a[..]),
2160                _ => None, // We don't know how to find this digest.
2161            };
2162            if d.is_none() {
2163                // We don't support this kind of digest for this kind
2164                // of document.
2165                continue;
2166            }
2167
2168            // Unwrap should be safe because of above `d.is_none()` check
2169            #[allow(clippy::unwrap_used)]
2170            match sig.check_signature(d.as_ref().unwrap(), certs) {
2171                SigCheckResult::Valid => {
2172                    ok.insert(*id_fingerprint);
2173                }
2174                _ => continue,
2175            }
2176        }
2177
2178        ok.len() > (n_authorities / 2)
2179    }
2180}
2181
2182#[cfg(test)]
2183mod test {
2184    // @@ begin test lint list maintained by maint/add_warning @@
2185    #![allow(clippy::bool_assert_comparison)]
2186    #![allow(clippy::clone_on_copy)]
2187    #![allow(clippy::dbg_macro)]
2188    #![allow(clippy::mixed_attributes_style)]
2189    #![allow(clippy::print_stderr)]
2190    #![allow(clippy::print_stdout)]
2191    #![allow(clippy::single_char_pattern)]
2192    #![allow(clippy::unwrap_used)]
2193    #![allow(clippy::unchecked_time_subtraction)]
2194    #![allow(clippy::useless_vec)]
2195    #![allow(clippy::needless_pass_by_value)]
2196    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
2197    use super::*;
2198    use hex_literal::hex;
2199    #[cfg(feature = "incomplete")]
2200    use {
2201        crate::parse2::{NetdocUnverified as _, ParseInput, parse_netdoc},
2202        std::fs,
2203    };
2204
2205    const CERTS: &str = include_str!("../../testdata/authcerts2.txt");
2206    const CONSENSUS: &str = include_str!("../../testdata/mdconsensus1.txt");
2207
2208    const PLAIN_CERTS: &str = include_str!("../../testdata2/cached-certs");
2209    const PLAIN_CONSENSUS: &str = include_str!("../../testdata2/cached-consensus");
2210
2211    fn read_bad(fname: &str) -> String {
2212        use std::fs;
2213        use std::path::PathBuf;
2214        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2215        path.push("testdata");
2216        path.push("bad-mdconsensus");
2217        path.push(fname);
2218
2219        fs::read_to_string(path).unwrap()
2220    }
2221
2222    #[test]
2223    fn parse_and_validate_md() -> Result<()> {
2224        use std::net::SocketAddr;
2225        use tor_checkable::{SelfSigned, Timebound};
2226        let mut certs = Vec::new();
2227        for cert in AuthCert::parse_multiple(CERTS)? {
2228            let cert = cert?.check_signature()?.dangerously_assume_timely();
2229            certs.push(cert);
2230        }
2231        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
2232
2233        assert_eq!(certs.len(), 3);
2234
2235        let (_, _, consensus) = MdConsensus::parse(CONSENSUS)?;
2236        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
2237
2238        // The set of authorities we know _could_ validate this cert.
2239        assert!(consensus.authorities_are_correct(&auth_ids));
2240        // A subset would also work.
2241        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
2242        {
2243            // If we only believe in an authority that isn't listed,
2244            // that won't work.
2245            let bad_auth_id = (*b"xxxxxxxxxxxxxxxxxxxx").into();
2246            assert!(!consensus.authorities_are_correct(&[&bad_auth_id]));
2247        }
2248
2249        let missing = consensus.key_is_correct(&[]).err().unwrap();
2250        assert_eq!(3, missing.len());
2251        assert!(consensus.key_is_correct(&certs).is_ok());
2252        let missing = consensus.key_is_correct(&certs[0..1]).err().unwrap();
2253        assert_eq!(2, missing.len());
2254
2255        // here is a trick that had better not work.
2256        let same_three_times = vec![certs[0].clone(), certs[0].clone(), certs[0].clone()];
2257        let missing = consensus.key_is_correct(&same_three_times).err().unwrap();
2258
2259        assert_eq!(2, missing.len());
2260        assert!(consensus.is_well_signed(&same_three_times).is_err());
2261
2262        assert!(consensus.key_is_correct(&certs).is_ok());
2263        let consensus = consensus.check_signature(&certs)?;
2264
2265        assert_eq!(6, consensus.relays().len());
2266        let r0 = &consensus.relays()[0];
2267        assert_eq!(
2268            r0.md_digest(),
2269            &hex!("73dabe0a0468f4f7a67810a18d11e36731bb1d2ec3634db459100609f3b3f535")
2270        );
2271        assert_eq!(
2272            r0.rsa_identity().as_bytes(),
2273            &hex!("0a3057af2910415794d8ea430309d9ac5f5d524b")
2274        );
2275        assert!(!r0.weight().is_measured());
2276        assert!(!r0.weight().is_nonzero());
2277        let pv = &r0.protovers();
2278        assert!(pv.supports_subver("HSDir", 2));
2279        assert!(!pv.supports_subver("HSDir", 3));
2280        let ip4 = "127.0.0.1:5002".parse::<SocketAddr>().unwrap();
2281        let ip6 = "[::1]:5002".parse::<SocketAddr>().unwrap();
2282        assert!(r0.addrs().any(|a| a == ip4));
2283        assert!(r0.addrs().any(|a| a == ip6));
2284
2285        Ok(())
2286    }
2287
2288    #[test]
2289    fn parse_and_validate_ns() -> Result<()> {
2290        use tor_checkable::{SelfSigned, Timebound};
2291        let mut certs = Vec::new();
2292        for cert in AuthCert::parse_multiple(PLAIN_CERTS)? {
2293            let cert = cert?.check_signature()?.dangerously_assume_timely();
2294            certs.push(cert);
2295        }
2296        let auth_ids: Vec<_> = certs.iter().map(|c| c.id_fingerprint()).collect();
2297        assert_eq!(certs.len(), 4);
2298
2299        let (_, _, consensus) = PlainConsensus::parse(PLAIN_CONSENSUS)?;
2300        let consensus = consensus.dangerously_assume_timely().set_n_authorities(3);
2301        // The set of authorities we know _could_ validate this cert.
2302        assert!(consensus.authorities_are_correct(&auth_ids));
2303        // A subset would also work.
2304        assert!(consensus.authorities_are_correct(&auth_ids[0..1]));
2305
2306        assert!(consensus.key_is_correct(&certs).is_ok());
2307
2308        let _consensus = consensus.check_signature(&certs)?;
2309
2310        Ok(())
2311    }
2312
2313    #[test]
2314    #[cfg(feature = "incomplete")]
2315    fn parse2_vote() -> anyhow::Result<()> {
2316        let file = "testdata2/v3-status-votes--1";
2317        let text = fs::read_to_string(file)?;
2318
2319        // TODO DIRAUTH replace the poc struct here when we have parsing of proper whole votes
2320        use crate::parse2::poc::netstatus::NetworkStatusUnverifiedVote;
2321
2322        let input = ParseInput::new(&text, file);
2323        let doc: NetworkStatusUnverifiedVote = parse_netdoc(&input)?;
2324
2325        println!("{doc:?}");
2326        println!("{:#?}", doc.inspect_unverified().0.r[0]);
2327
2328        Ok(())
2329    }
2330
2331    #[test]
2332    fn test_bad() {
2333        use crate::Pos;
2334        fn check(fname: &str, e: &Error) {
2335            let content = read_bad(fname);
2336            let res = MdConsensus::parse(&content);
2337            assert!(res.is_err());
2338            assert_eq!(&res.err().unwrap(), e);
2339        }
2340
2341        check(
2342            "bad-flags",
2343            &EK::BadArgument
2344                .at_pos(Pos::from_line(27, 1))
2345                .with_msg("Flags out of order"),
2346        );
2347        check(
2348            "bad-md-digest",
2349            &EK::BadArgument
2350                .at_pos(Pos::from_line(40, 3))
2351                .with_msg("Invalid base64"),
2352        );
2353        check(
2354            "bad-weight",
2355            &EK::BadArgument
2356                .at_pos(Pos::from_line(67, 141))
2357                .with_msg("invalid digit found in string"),
2358        );
2359        check(
2360            "bad-weights",
2361            &EK::BadArgument
2362                .at_pos(Pos::from_line(51, 13))
2363                .with_msg("invalid digit found in string"),
2364        );
2365        check(
2366            "wrong-order",
2367            &EK::WrongSortOrder.at_pos(Pos::from_line(52, 1)),
2368        );
2369        check(
2370            "wrong-start",
2371            &EK::UnexpectedToken
2372                .with_msg("vote-status")
2373                .at_pos(Pos::from_line(1, 1)),
2374        );
2375        check("wrong-version", &EK::BadDocumentVersion.with_msg("10"));
2376    }
2377
2378    fn gettok(s: &str) -> Result<Item<'_, NetstatusKwd>> {
2379        let mut reader = NetDocReader::new(s)?;
2380        let tok = reader.next().unwrap();
2381        assert!(reader.next().is_none());
2382        tok
2383    }
2384
2385    #[test]
2386    fn test_weight() {
2387        let w = gettok("w Unmeasured=1 Bandwidth=6\n").unwrap();
2388        let w = RelayWeightsItem::from_item(&w).unwrap();
2389        assert!(!w.effective.is_measured());
2390        assert!(w.effective.is_nonzero());
2391
2392        let w = gettok("w Bandwidth=10\n").unwrap();
2393        let w = RelayWeightsItem::from_item(&w).unwrap();
2394        assert!(w.effective.is_measured());
2395        assert!(w.effective.is_nonzero());
2396
2397        let w = RelayWeightsItem::new_no_info();
2398        assert!(!w.effective.is_measured());
2399        assert!(!w.effective.is_nonzero());
2400
2401        let w = gettok("w Mustelid=66 Cheato=7 Unmeasured=1\n").unwrap();
2402        let w = RelayWeightsItem::from_item(&w).unwrap();
2403        assert!(!w.effective.is_measured());
2404        assert!(!w.effective.is_nonzero());
2405
2406        let w = gettok("r foo\n").unwrap();
2407        let w = RelayWeightsItem::from_item(&w);
2408        assert!(w.is_err());
2409
2410        let w = gettok("r Bandwidth=6 Unmeasured=Frog\n").unwrap();
2411        let w = RelayWeightsItem::from_item(&w);
2412        assert!(w.is_err());
2413
2414        let w = gettok("r Bandwidth=6 Unmeasured=3\n").unwrap();
2415        let w = RelayWeightsItem::from_item(&w);
2416        assert!(w.is_err());
2417    }
2418
2419    #[test]
2420    fn test_netparam() {
2421        let p = "Hello=600 Goodbye=5 Fred=7"
2422            .parse::<NetParams<u32>>()
2423            .unwrap();
2424        assert_eq!(p.get("Hello"), Some(&600_u32));
2425
2426        let p = "Hello=Goodbye=5 Fred=7".parse::<NetParams<u32>>();
2427        assert!(p.is_err());
2428
2429        let p = "Hello=Goodbye Fred=7".parse::<NetParams<u32>>();
2430        assert!(p.is_err());
2431
2432        for bad_kw in ["What=The", "", "\n", "\0"] {
2433            let p = [(bad_kw, 42)].into_iter().collect::<NetParams<i32>>();
2434            let mut d = NetdocEncoder::new();
2435            let d = (|| {
2436                let i = d.item("bad-psrams");
2437                p.write_item_value_onto(i)?;
2438                d.finish()
2439            })();
2440            let _: tor_error::Bug = d.expect_err(bad_kw);
2441        }
2442    }
2443
2444    #[test]
2445    fn test_sharedrand() {
2446        let sr =
2447            gettok("shared-rand-previous-value 9 5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4\n")
2448                .unwrap();
2449        let sr = SharedRandStatus::from_item(&sr).unwrap();
2450
2451        assert_eq!(sr.n_reveals, 9);
2452        assert_eq!(
2453            sr.value.0,
2454            hex!("e4ba1d638c96c458532adc6957dc0080d03d37c7e5854087d0da90bf5ff4e72e")
2455        );
2456        assert!(sr.timestamp.is_none());
2457
2458        let sr2 = gettok(
2459            "shared-rand-current-value 9 \
2460                    5LodY4yWxFhTKtxpV9wAgNA9N8flhUCH0NqQv1/05y4 2022-01-20T12:34:56\n",
2461        )
2462        .unwrap();
2463        let sr2 = SharedRandStatus::from_item(&sr2).unwrap();
2464        assert_eq!(sr2.n_reveals, sr.n_reveals);
2465        assert_eq!(sr2.value.0, sr.value.0);
2466        assert_eq!(
2467            sr2.timestamp.unwrap().0,
2468            humantime::parse_rfc3339("2022-01-20T12:34:56Z").unwrap()
2469        );
2470
2471        let sr = gettok("foo bar\n").unwrap();
2472        let sr = SharedRandStatus::from_item(&sr);
2473        assert!(sr.is_err());
2474    }
2475
2476    #[test]
2477    fn test_protostatus() {
2478        let my_protocols: Protocols = "Link=7 Cons=1-5 Desc=3-10".parse().unwrap();
2479
2480        let outcome = ProtoStatus {
2481            recommended: "Link=7".parse().unwrap(),
2482            required: "Desc=5".parse().unwrap(),
2483        }
2484        .check_protocols(&my_protocols);
2485        assert!(outcome.is_ok());
2486
2487        let outcome = ProtoStatus {
2488            recommended: "Microdesc=4 Link=7".parse().unwrap(),
2489            required: "Desc=5".parse().unwrap(),
2490        }
2491        .check_protocols(&my_protocols);
2492        assert_eq!(
2493            outcome,
2494            Err(ProtocolSupportError::MissingRecommended(
2495                "Microdesc=4".parse().unwrap()
2496            ))
2497        );
2498
2499        let outcome = ProtoStatus {
2500            recommended: "Microdesc=4 Link=7".parse().unwrap(),
2501            required: "Desc=5 Cons=5-12 Wombat=15".parse().unwrap(),
2502        }
2503        .check_protocols(&my_protocols);
2504        assert_eq!(
2505            outcome,
2506            Err(ProtocolSupportError::MissingRequired(
2507                "Cons=6-12 Wombat=15".parse().unwrap()
2508            ))
2509        );
2510    }
2511
2512    #[test]
2513    fn serialize_protostatus() {
2514        let ps = ProtoStatuses {
2515            client: ProtoStatus {
2516                recommended: "Link=1-5 LinkAuth=2-5".parse().unwrap(),
2517                required: "Link=5 LinkAuth=3".parse().unwrap(),
2518            },
2519            relay: ProtoStatus {
2520                recommended: "Wombat=20-30 Knish=20-30".parse().unwrap(),
2521                required: "Wombat=20-22 Knish=25-27".parse().unwrap(),
2522            },
2523        };
2524        let json = serde_json::to_string(&ps).unwrap();
2525        let ps2 = serde_json::from_str(json.as_str()).unwrap();
2526        assert_eq!(ps, ps2);
2527
2528        let ps3: ProtoStatuses = serde_json::from_str(
2529            r#"{
2530            "client":{
2531                "required":"Link=5 LinkAuth=3",
2532                "recommended":"Link=1-5 LinkAuth=2-5"
2533            },
2534            "relay":{
2535                "required":"Wombat=20-22 Knish=25-27",
2536                "recommended":"Wombat=20-30 Knish=20-30"
2537            }
2538        }"#,
2539        )
2540        .unwrap();
2541        assert_eq!(ps, ps3);
2542    }
2543}