netdoc and version docs

This commit is contained in:
Nick Mathewson 2020-05-08 19:47:25 -04:00
parent c65c40170d
commit c4d0f59e7d
8 changed files with 364 additions and 43 deletions

View File

@ -1,16 +1,24 @@
//! Parse and represent directory objects used in Tor.
//! Parse and represent directory objects used in Tor.
//!
//! Tor has several "directory objects" that it uses to convey
//! information about relays on the network. They are documented in
//! dir-spec.txt.
//!
//! TODO: Currently, this crate can handle the metaformat, along with
//! certain parts of the router descriptor type. We will eventually
//! need to handle more types.
//! This crate has common code to parse and validate these documents.
//! Currently, it can handle the metaformat, along with certain parts
//! of the router descriptor type. We will eventually need to handle
//! more types.
//!
//! # Caveat haxxor: limitations and infelicities
//!
//! TODO: This crate requires that all of its inputs be valid UTF-8.
//!
//! TODO: This crate has several pieces that should probably be split out
//! into other smaller cases, including handling for version numbers
//! and exit policies.
#![allow(dead_code)]
//#![warn(missing_docs)]
mod argtype;
mod err;
@ -20,9 +28,10 @@ mod tokenize;
mod util;
#[macro_use]
mod macros; // xxxx
mod policy;
mod routerdesc;
mod version;
pub mod policy;
pub mod routerdesc;
pub mod version;
pub use err::{Error, Position};
/// Alias for the Result type returned by most objects in this module.
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -1,8 +1,29 @@
/// Macro for declaring a keyword enumeration to help parse a document.
///
/// A keyword enumber implements the Keyword trait.
///
/// These enums are a bit different from those made by `caret`, in a
/// few ways. Notably, they are optimized for parsing, they are
/// required to be compact, and they allow multiple strings to be mapped to
/// a single index.
///
/// ```ignore
/// decl_keyword! {
/// Location {
// "start" => START,
/// "middle" | "center" => MID,
/// "end" => END
/// }
/// }
///
/// assert_eq!(Location::from_str("start"), Location::START);
/// assert_eq!(Location::from_str("stfff"), Location::UNRECOGNIZED);
/// ```
macro_rules! decl_keyword {
{ $name:ident { $( $($s:literal)|+ => $i:ident),* $(,)? } } => {
#[derive(Copy,Clone,Eq,PartialEq,Debug,std::hash::Hash)]
#[allow(non_camel_case_types)]
pub enum $name {
enum $name {
$( $i , )*
UNRECOGNIZED
}
@ -10,12 +31,18 @@ macro_rules! decl_keyword {
fn idx(self) -> usize { self as usize }
fn n_vals() -> usize { ($name::UNRECOGNIZED as usize) + 1 }
fn from_str(s : &str) -> Self {
// Note usage of phf crate to create a perfect hash over
// the possible keywords. It will be even better if someday
// the phf crate can find hash functions that are better
// than siphash.
const KEYWORD: phf::Map<&'static str, $name> = phf::phf_map! {
$( $( $s => $name::$i , )+ )*
};
* KEYWORD.get(s).unwrap_or(& $name::UNRECOGNIZED)
}
fn from_idx(i : usize) -> Option<Self> {
// Note looking up the value in a vec. This may or may
// not be faster than a case statement would be.
lazy_static::lazy_static! {
static ref VALS: Vec<$name> =
vec![ $($name::$i , )* $name::UNRECOGNIZED ];

View File

@ -1,57 +1,130 @@
mod full;
mod short;
//! Exit policies: match patterns of addresses and/or ports.
//!
//! Every Tor relays has a set of address:port combinations that it
//! actually allows connections to. The set, abstractly, is the
//! relay's "exit policy".
//!
//! Address policies can be transmitted in two forms. One is a "full
//! policy", that includes a list of rules that are applied in order
//! to represent addresses and ports. We represent this with the
//! AddrPolicy type.
//!
//! In microdescriptors, and for IPv6 policies, policies are just
//! given a list of ports for which _most_ addresses are permitted.
//! We represent this kind of policy with the PortPolicy type.
//!
//! TODO: This module probably belongs in a crate of its own, with
//! possibly only the parsing code in this crate.
mod addrpolicy;
mod portpolicy;
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error;
pub use full::{AddrPolicy, AddrPortPattern};
pub use short::PortPolicy;
pub use addrpolicy::{AddrPolicy, AddrPortPattern};
pub use portpolicy::PortPolicy;
/// Error from an unpareasble or invalid policy.
#[derive(Debug, Error, Clone)]
#[non_exhaustive]
pub enum PolicyError {
/// A port was not a number in the range 1..65535
#[error("Invalid port")]
InvalidPort,
/// A port range had its starting-point higher than its ending point.
#[error("Invalid port range")]
InvalidRange,
#[error("Invalid policy")]
InvalidPolicy,
/// An address could not be interpreted.
#[error("Invalid address")]
InvalidAddress,
/// Tried to use a bitmask with the address "*".
#[error("mask with star")]
MaskWithStar,
/// A bit mask was out of range.
#[error("invalid mask")]
InvalidMask,
/// A policy could not be parsed for some other reason.
#[error("Invalid policy")]
InvalidPolicy,
}
/// A PortRange is a set of consecutively numbered TCP or UDP ports.
///
/// # Example
/// ```
/// use tor_netdoc::policy::PortRange;
///
/// let r: PortRange = "22-8000".parse().unwrap();
/// assert!(r.contains(128));
/// assert!(r.contains(22));
/// assert!(r.contains(8000));
///
/// assert!(! r.contains(21));
/// assert!(! r.contains(8001));
/// ```
#[derive(Debug, Clone)]
pub struct PortRange {
lo: u16,
hi: u16,
/// The first port in this range.
pub lo: u16,
/// The last port in this range.
pub hi: u16,
}
impl PortRange {
/// Create a new port range spanning from lo to hi, asserting that
/// the correct invariants hold.
fn new_unchecked(lo: u16, hi: u16) -> Self {
assert!(lo != 0);
assert!(lo <= hi);
PortRange { lo, hi }
}
fn new_all() -> Self {
/// Create a port range containing all ports.
pub fn new_all() -> Self {
PortRange::new_unchecked(1, 65535)
}
fn new(lo: u16, hi: u16) -> Option<Self> {
/// Create a new PortRange.
///
/// The Portrange contains all ports between `lo` and `hi` inclusive.
///
/// Returns None if lo is greater than hi, or if either is zero.
pub fn new(lo: u16, hi: u16) -> Option<Self> {
if lo != 0 && lo <= hi {
Some(PortRange { lo, hi })
} else {
None
}
}
fn contains(&self, port: u16) -> bool {
/// Return true if a port is in this range.
pub fn contains(&self, port: u16) -> bool {
self.lo <= port && port <= self.hi
}
/// Return true if this range contains all ports.
pub fn is_all(&self) -> bool {
self.lo == 1 && self.hi == 65535
}
/// Helper for binary search: compare this range to a port.
///
/// This range is "equal" to all ports that it contains. It is
/// "greater" than all ports that precede its starting point, and
/// "less" than all ports that follow its ending point.
fn compare_to_port(&self, port: u16) -> std::cmp::Ordering {
use std::cmp::Ordering::*;
if port < self.lo {
Greater
} else if port <= self.hi {
Equal
} else {
Less
}
}
}
/// A PortRange is displayed as a number if it contains a single port,
/// and as a start point and end point separated by a dash if it contains
/// more than one port.
impl Display for PortRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.lo == self.hi {
@ -66,7 +139,9 @@ impl FromStr for PortRange {
type Err = PolicyError;
fn from_str(s: &str) -> Result<Self, PolicyError> {
let idx = s.find('-');
// Find "lo" and "hi".
let (lo, hi) = if let Some(pos) = idx {
// This is a range; parse each part.
(
s[..pos]
.parse::<u16>()
@ -76,6 +151,7 @@ impl FromStr for PortRange {
.map_err(|_| PolicyError::InvalidPort)?,
)
} else {
// There was no hyphen, so try to parse this range as a singleton.
let v = s.parse::<u16>().map_err(|_| PolicyError::InvalidPort)?;
(v, v)
};

View File

@ -1,36 +1,66 @@
/// Implements address policies, based on a series of accept/reject
/// rules.
use std::fmt::Display;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use super::{PolicyError, PortRange};
/// A sequence of rules that are applied to an address:port until one
/// matches.
pub struct AddrPolicy {
rules: Vec<AddrPolicyRule>,
}
impl AddrPolicy {
pub fn allows(&self, addr: &IpAddr, port: u16, default_allow: bool) -> bool {
/// Apply this policy to an address:port combination
///
/// We do this by applying each rule in sequence, until one
/// matches. If that rule is accept, we return Some(true). If
/// that rule is reject, we return Some(false).
///
/// Returns None if no rule matches.
pub fn allows(&self, addr: &IpAddr, port: u16) -> Option<bool> {
match self
.rules
.iter()
.find(|rule| rule.pattern.matches(addr, port))
{
Some(AddrPolicyRule { accept, .. }) => *accept,
None => default_allow,
Some(AddrPolicyRule { accept, .. }) => Some(*accept),
None => None,
}
}
/// Create a new AddrPolicy that matches nothing.
pub fn new() -> Self {
AddrPolicy { rules: Vec::new() }
}
/// Add a new rule to this policy.
///
/// The newly added rule is applied _after_ all previous rules.
/// It matches all addresses and ports coverd by AddrPortPattern.
///
/// If accept is true, the rule is to accept addresses that match;
/// if accept is false, the rule rejects such addresses.
pub fn push(&mut self, accept: bool, pattern: AddrPortPattern) {
self.rules.push(AddrPolicyRule { accept, pattern })
}
}
impl Default for AddrPolicy {
fn default() -> Self {
AddrPolicy::new()
}
}
/// A single rule in an address policy.
///
/// Contains a pattern and what to do with things that match it.
struct AddrPolicyRule {
/// What do we do with items that match the pattern?
accept: bool,
/// What pattern are we trying to match?
pattern: AddrPortPattern,
}
@ -41,13 +71,31 @@ impl Display for AddrPolicyRule {
}
}
/// A pattern that may or may not match an address and port.
///
/// Each AddrPortPattern has an IP pattern, which matches a set of
/// addresses by prefix, and a port pattern, which matches a range of
/// ports.
///
/// # Example
///
/// ```
/// use tor_netdoc::policy::AddrPortPattern;
/// use std::net::{IpAddr,Ipv4Addr};
/// let localhost = IpAddr::V4(Ipv4Addr::new(127,3,4,5));
/// let not_localhost = IpAddr::V4(Ipv4Addr::new(192,0,2,16));
/// let pat: AddrPortPattern = "127.0.0.0/8:*".parse().unwrap();
///
/// assert!(pat.matches(&localhost, 22));
/// assert!(! pat.matches(&not_localhost, 22));
/// ```
pub struct AddrPortPattern {
pattern: IpPattern,
ports: PortRange,
}
impl AddrPortPattern {
fn matches(&self, addr: &IpAddr, port: u16) -> bool {
pub fn matches(&self, addr: &IpAddr, port: u16) -> bool {
self.pattern.matches(addr) && self.ports.contains(port)
}
}
@ -78,15 +126,22 @@ impl FromStr for AddrPortPattern {
}
}
/// A pattern that matches one or more IP addresses.
enum IpPattern {
/// Match all addresses.
Star,
/// Match all IPv4 addresses.
V4Star,
/// Match all IPv6 addresses.
V6Star,
/// Match all IPv4 addresses beginning with a given prefix.
V4(Ipv4Addr, u8),
/// Match all IPv6 addresses beginning with a given prefix.
V6(Ipv6Addr, u8),
}
impl IpPattern {
/// Construct an IpPattern that matches the first `mask` bits of `addr`.
fn from_addr_and_mask(addr: IpAddr, mask: u8) -> Result<Self, PolicyError> {
match (addr, mask) {
(IpAddr::V4(_), 0) => Ok(IpPattern::V4Star),
@ -96,6 +151,7 @@ impl IpPattern {
(_, _) => Err(PolicyError::InvalidMask),
}
}
/// Return true iff `addr` is matched by this pattern.
fn matches(&self, addr: &IpAddr) -> bool {
match (self, addr) {
(IpPattern::Star, _) => true,

View File

@ -1,10 +1,32 @@
//! Implement port-based policies
//!
//! These are also known as "short policies" or "policy summaries".
use std::fmt::Display;
use std::str::FromStr;
use super::{PolicyError, PortRange};
/// A policy to match zero or more TCP/UDP ports.
///
/// These are used in Tor to summarize all policies in
/// microdescriptors, and Ipv6 policies in router descriptors.
///
/// # Examples
/// ```
/// use tor_netdoc::policy::PortPolicy;
/// let policy: PortPolicy = "accept 1-1023,8000-8999,60000-65535".parse().unwrap();
///
/// assert!(policy.allows_port(22));
/// assert!(policy.allows_port(8000));
/// assert!(! policy.allows_port(1024));
/// assert!(! policy.allows_port(9000));
/// ```
#[derive(Clone)]
pub struct PortPolicy {
/// A list of port ranges that this policy allows.
///
/// These ranges are sorted and disjoint.
allowed: Vec<PortRange>,
}
@ -25,6 +47,7 @@ impl Display for PortPolicy {
}
impl PortPolicy {
/// Helper: replace this policy with its inverse.
fn invert(&mut self) {
let mut prev_hi = 0;
let mut new_allowed = Vec::new();
@ -41,11 +64,14 @@ impl PortPolicy {
}
self.allowed = new_allowed;
}
/// Return true iff `port` is allowed by this policy.
pub fn allows_port(&self, port: u16) -> bool {
// TODO: A binary search would be more efficient.
self.allowed.iter().any(|range| range.contains(port))
self.allowed
.binary_search_by(|range| range.compare_to_port(port))
.is_ok()
}
}
impl FromStr for PortPolicy {
type Err = PolicyError;
fn from_str(mut s: &str) -> Result<Self, PolicyError> {

View File

@ -1,35 +1,60 @@
//! Break a string into a set of directory-object Items.
use crate::{Error, Position, Result};
use std::cell::{Ref, RefCell};
use std::str::FromStr;
/// A tagged object that is part of a directory Item.
///
/// This represents a single blob within a pair of "-----BEGIN
/// FOO-----" and "-----END FOO-----". The data is not guaranteed to
/// be actual base64 when this object is created.
#[derive(Clone, Copy, Debug)]
pub struct Object<'a> {
tag: &'a str,
data: &'a str, // not yet guaranteed to be base64.
}
/// A single part of a directory object.
///
/// Each Item -- called an "entry" in dir-spec.txt -- has a keyword, a
/// (possibly empty) set of arguments, and an optional object.
///
/// This is a zero-copy implementation that points to slices within a
/// containing string.
#[derive(Clone, Debug)]
pub struct Item<'a> {
pub off: usize, // don't make this pub.XXXX
kwd: &'a str,
args: &'a str,
/// The arguments, split by whitespace. This vector is contructed
/// as needed, using interior mutability.
split_args: RefCell<Option<Vec<&'a str>>>,
object: Option<Object<'a>>,
}
/// A cursor into a string that returns Items one by one.
#[derive(Clone, Debug)]
pub struct NetDocReader<'a> {
/// The string we're parsing.
s: &'a str,
/// Our position within the string.
off: usize,
}
impl<'a> NetDocReader<'a> {
/// Create a new NetDocReader to split a string into tokens.
pub fn new(s: &'a str) -> Self {
NetDocReader { s, off: 0 }
}
/// Return the current Position within the string.
fn get_pos(&self, pos: usize) -> Position {
Position::from_offset(self.s, pos)
}
/// Skip forward by n bytes.
///
/// (Note that standard caveats with byte-oriented processing of
/// UTF-8 strings apply.)
fn advance(&mut self, n: usize) -> Result<()> {
if n > self.remaining() {
return Err(Error::Internal(Position::from_offset(self.s, self.off)));
@ -37,13 +62,16 @@ impl<'a> NetDocReader<'a> {
self.off += n;
Ok(())
}
/// Return the remaining number of bytes in this reader.
fn remaining(&self) -> usize {
self.s.len() - self.off
}
/// Return true if the next characters in this reader are `s`
fn starts_with(&self, s: &str) -> bool {
self.s[self.off..].starts_with(s)
}
/// Try to extract a NL-terminated line from this reader.
fn get_line(&mut self) -> Result<&'a str> {
let remainder = &self.s[self.off..];
let nl_pos = remainder
@ -59,6 +87,9 @@ impl<'a> NetDocReader<'a> {
Ok(line)
}
/// Try to extract a line that begins with a keyword from this reader.
///
/// Returns a (kwd, args) tuple on success.
fn get_kwdline(&mut self) -> Result<(&'a str, &'a str)> {
let pos = self.off;
let line = self.get_line()?;
@ -82,6 +113,12 @@ impl<'a> NetDocReader<'a> {
};
Ok((kwd, args))
}
/// Try to extract an Object beginning wrapped within BEGIN/END tags.
///
/// Returns Ok(Some(Object(...))) on success if an object is
/// found, Ok(None) if no object is found, and Err only if a
/// corrupt object is found.
fn get_object(&mut self) -> Result<Option<Object<'a>>> {
const BEGIN_STR: &str = "-----BEGIN ";
const END_STR: &str = "-----END ";
@ -117,6 +154,10 @@ impl<'a> NetDocReader<'a> {
Ok(Some(Object { tag, data }))
}
/// Read the next Item from this NetDocReader.
///
/// If successful, returns Ok(Some(Item)), or Ok(None) if exhausted.
/// Returns Err on failure.
pub fn get_item(&mut self) -> Result<Option<Item<'a>>> {
if self.remaining() == 0 {
return Ok(None);
@ -135,6 +176,7 @@ impl<'a> NetDocReader<'a> {
}
}
/// Return true iff 's' is a valid keyword.
fn keyword_ok(s: &str) -> bool {
fn kwd_char_ok(c: char) -> bool {
match c {
@ -153,6 +195,7 @@ fn keyword_ok(s: &str) -> bool {
s.chars().all(kwd_char_ok)
}
/// Return true iff 's' is a valid keyword for a BEGIN/END tag.
fn tag_keyword_ok(s: &str) -> bool {
fn kwd_char_ok(c: char) -> bool {
match c {
@ -172,6 +215,7 @@ fn tag_keyword_ok(s: &str) -> bool {
s.chars().all(kwd_char_ok)
}
/// When used as an Iterator, returns a sequence of Result<Item>.
impl<'a> Iterator for NetDocReader<'a> {
type Item = Result<Item<'a>>;
fn next(&mut self) -> Option<Self::Item> {
@ -179,6 +223,8 @@ impl<'a> Iterator for NetDocReader<'a> {
}
}
/// Helper: as base64::decode(), but allows newlines in the middle of the
/// encoded object.
fn base64_decode_multiline(s: &str) -> std::result::Result<Vec<u8>, base64::DecodeError> {
// base64 module hates whitespace.
let mut v = Vec::new();
@ -189,16 +235,18 @@ fn base64_decode_multiline(s: &str) -> std::result::Result<Vec<u8>, base64::Deco
}
impl<'a> Item<'a> {
pub fn get_pos_in(&self, s: &'a str) -> Position {
Position::from_offset(s, self.off)
}
/// Return the keyword part of this item.
pub fn get_kwd(&self) -> &'a str {
self.kwd
}
/// Return the arguments of this item, as a single string.
pub fn args_as_str(&self) -> &'a str {
self.args
}
/// Return the arguments of this item as a vector.
pub fn args_as_vec(&self) -> Ref<Vec<&'a str>> {
// We're using an interior mutability pattern here to lazily
// construct the vector.
if self.split_args.borrow().is_none() {
self.split_args.replace(Some(self.args().collect()));
}
@ -207,15 +255,21 @@ impl<'a> Item<'a> {
None => panic!(),
})
}
/// Return an iterator over the arguments of this item.
pub fn args(&self) -> impl Iterator<Item = &'a str> {
fn is_sp(c: char) -> bool {
c == ' ' || c == '\t'
}
self.args.split(is_sp).filter(|s| !s.is_empty())
}
/// Return the nth argument of this item, if there is one.
pub fn get_arg(&self, idx: usize) -> Option<&'a str> {
self.args_as_vec().get(idx).copied()
}
/// Try to parse the nth argument (if it exists) into some type
/// that supports FromStr.
///
/// Returns Ok(None) if the argument doesn't exist.
pub fn parse_optional_arg<V: FromStr>(&self, idx: usize) -> Result<Option<V>>
where
<V as FromStr>::Err: std::error::Error,
@ -228,6 +282,10 @@ impl<'a> Item<'a> {
},
}
}
/// Try to parse the nth argument (if it exists) into some type
/// that supports FromStr.
///
/// Return an error if the argument doesn't exist.
pub fn parse_arg<V: FromStr>(&self, idx: usize) -> Result<V>
where
<V as FromStr>::Err: std::error::Error,
@ -238,12 +296,15 @@ impl<'a> Item<'a> {
Err(e) => Err(e),
}
}
/// Return the number of arguments for this Item
pub fn n_args(&self) -> usize {
self.args().count()
}
/// Return true iff this Item has an associated object.
pub fn has_obj(&self) -> bool {
self.object.is_some()
}
/// Try to decode the base64 contents of this Item's associated object.
pub fn get_obj(&self, want_tag: &str) -> Result<Vec<u8>> {
match self.object {
None => Err(Error::MissingObject("entry", self.pos())),
@ -257,10 +318,15 @@ impl<'a> Item<'a> {
}
}
}
/// Return the position of this item without reference to its containing
/// string.
pub fn pos(&self) -> Position {
Position::from_byte(self.off)
}
/// Return the Position of this item within s.
pub fn pos_in(&self, s: &str) -> Position {
// There are crates that claim they can do this for us and let us
// throw out 'off' entirely, but I don't trust them.
Position::from_offset(s, self.off)
}
}

View File

@ -1,11 +1,15 @@
/// Helper functions and types for use in parsing
///
/// For now this module has a single type -- an iterator that pauses
/// when a certain predicate is true. We use it for chunking
/// documents into sections. If it turns out to be useful somewhere
/// else, we should move it.
use std::iter::Peekable;
///
/// Iterator adaptor that pauses when a given predicate is true.
/// An iterator adaptor that pauses when a given predicate is true.
///
/// Unlike std::iter::TakeWhile, it doesn't consume the first non-returned
/// element.
///
pub struct PauseAt<I: Iterator, F: FnMut(&I::Item) -> bool> {
peek: Peekable<I>,
pred: F,
@ -13,12 +17,14 @@ pub struct PauseAt<I: Iterator, F: FnMut(&I::Item) -> bool> {
/// Trait for iterators that support `pause_at()`.
pub trait Pausable: Iterator + Sized {
/// Construct a new iterator based on 'self' that will pause when
/// the function 'pred' would be true of the next item.
fn pause_at<F>(self, pred: F) -> PauseAt<Self, F>
where
F: FnMut(&Self::Item) -> bool;
}
// Make all iterators Trait for iterators support `pause_at()`
// Make all iterators support `pause_at()`.
impl<I> Pausable for I
where
I: Iterator,

View File

@ -1,20 +1,67 @@
//! Parsing and comparison for Tor versions
//!
//! Tor versions use a slightly unusual encoding described in Tor's
//! [version-spec.txt](https://spec.torproject.org/version-spec).
//! Briefly, version numbers are of the form
//!
//! `MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]*`
//!
//! Here we parse everything up to the first space, but ignore the
//! "EXTRA_INFO" component.
//!
//! # Examples
//!
//! ```
//! use tor_netdoc::version::TorVersion;
//! let older: TorVersion = "0.3.5.8".parse()?;
//! let latest: TorVersion = "0.4.3.4-rc".parse()?;
//! assert!(older < latest);
//!
//! # tor_netdoc::Result::Ok(())
//! ```
//!
//! # Limitations
//!
//! This module handles the version format which Tor has used ever
//! since 0.1.0.1-rc. Earlier versions used a different format, also
//! documented in
//! [version-spec.txt](https://spec.torproject.org/version-spec).
//! Fortunately, those versions are long obsolete, and there's not
//! much reason to parse them.
//!
//! TODO: Possibly, this module should be extracted into a crate of
//! its own. I'm not 100% sure though -- does anything need versions
//! but not router docs?
use std::cmp::Ordering;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use crate::{Error, Position};
/// Represents the status tag on a Tor version number
///
/// Status tags indicate that a release is alpha, beta (seldom used),
/// a release candidate (rc), or stable.
///
/// We accept unrecognized tags, and store them as "Other".
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
#[repr(u8)]
pub enum TorVerStatus {
enum TorVerStatus {
/// An unknown release status
Other,
/// An alpha release
Alpha,
/// A beta release
Beta,
/// A release candidate
Rc,
/// A stable release
Stable,
}
impl TorVerStatus {
/// Helper for encoding: return the suffix that represents a version.
fn suffix(self) -> &'static str {
use TorVerStatus::*;
match self {
@ -27,10 +74,11 @@ impl TorVerStatus {
}
}
/// A parsed Tor version number.
#[derive(Clone, Eq, PartialEq)]
pub struct TorVersion {
maj: u8,
min: u8,
major: u8,
minor: u8,
micro: u8,
patch: u8,
status: TorVerStatus,
@ -43,8 +91,8 @@ impl Display for TorVersion {
write!(
f,
"{}.{}.{}.{}{}{}",
self.maj,
self.min,
self.major,
self.minor,
self.micro,
self.patch,
self.status.suffix(),
@ -57,6 +105,9 @@ impl FromStr for TorVersion {
type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Self> {
// Split the string on "-" into "version", "status", and "dev."
// Note that "dev" may actually be in the "status" field if
// the version is stable; we'll handle that later.
let mut parts = s.split('-').fuse();
let ver_part = parts.next();
let status_part = parts.next();
@ -65,6 +116,7 @@ impl FromStr for TorVersion {
return Err(Error::BadVersion(Position::None));
}
// Split the version on "." into 3 or 4 numbers.
let vers: Result<Vec<_>, _> = ver_part
.ok_or(Error::BadVersion(Position::None))?
.splitn(4, '.')
@ -74,11 +126,12 @@ impl FromStr for TorVersion {
if vers.len() < 3 {
return Err(Error::BadVersion(Position::None));
}
let maj = vers[0];
let min = vers[1];
let major = vers[0];
let minor = vers[1];
let micro = vers[2];
let patch = if vers.len() == 4 { vers[3] } else { 0 };
// Compute real status and version.
let status = match status_part {
Some("alpha") => TorVerStatus::Alpha,
Some("beta") => TorVerStatus::Beta,
@ -96,8 +149,8 @@ impl FromStr for TorVersion {
};
Ok(TorVersion {
maj,
min,
major,
minor,
micro,
patch,
status,
@ -105,11 +158,13 @@ impl FromStr for TorVersion {
})
}
}
impl TorVersion {
/// Helper: used to implement Ord.
fn as_tuple(&self) -> (u8, u8, u8, u8, TorVerStatus, bool) {
(
self.maj,
self.min,
self.major,
self.minor,
self.micro,
self.patch,
self.status,