tor_netdoc/types/
version.rs

1//! Parsing and comparison for Tor versions
2//!
3//! Tor versions use a slightly unusual encoding described in Tor's
4//! [version-spec.txt](https://spec.torproject.org/version-spec).
5//! Briefly, version numbers are of the form
6//!
7//! `MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]*`
8//!
9//! Here we parse everything up to the first space, but ignore the
10//! "EXTRA_INFO" component.
11//!
12//! Why does Arti have to care about Tor versions?  Sometimes a given
13//! Tor version is broken for one purpose or another, and it's
14//! important to avoid using them for certain kinds of traffic.  (For
15//! planned incompatibilities, you should use protocol versions
16//! instead.)
17//!
18//! # Examples
19//!
20//! ```
21//! use tor_netdoc::types::version::TorVersion;
22//! let older: TorVersion = "0.3.5.8".parse()?;
23//! let latest: TorVersion = "0.4.3.4-rc".parse()?;
24//! assert!(older < latest);
25//!
26//! # tor_netdoc::Result::Ok(())
27//! ```
28//!
29//! # Limitations
30//!
31//! This module handles the version format which Tor has used ever
32//! since 0.1.0.1-rc.  Earlier versions used a different format, also
33//! documented in
34//! [version-spec.txt](https://spec.torproject.org/version-spec).
35//! Fortunately, those versions are long obsolete, and there's not
36//! much reason to parse them.
37//!
38//! TODO: Possibly, this module should be extracted into a crate of
39//! its own.  I'm not 100% sure though -- does anything need versions
40//! but not network docs?
41
42use std::fmt::{self, Display, Formatter};
43use std::str::FromStr;
44
45use crate::{NetdocErrorKind as EK, Pos};
46
47/// Represents the status tag on a Tor version number
48///
49/// Status tags indicate that a release is alpha, beta (seldom used),
50/// a release candidate (rc), or stable.
51///
52/// We accept unrecognized tags, and store them as "Other".
53#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
54#[repr(u8)]
55enum TorVerStatus {
56    /// An unknown release status
57    Other,
58    /// An alpha release
59    Alpha,
60    /// A beta release
61    Beta,
62    /// A release candidate
63    Rc,
64    /// A stable release
65    Stable,
66}
67
68impl TorVerStatus {
69    /// Helper for encoding: return the suffix that represents a version.
70    fn suffix(self) -> &'static str {
71        use TorVerStatus::*;
72        match self {
73            Stable => "",
74            Rc => "-rc",
75            Beta => "-beta",
76            Alpha => "-alpha",
77            Other => "-???",
78        }
79    }
80}
81
82/// A parsed Tor version number.
83#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
84pub struct TorVersion {
85    /// Major version number.  This has been zero since Tor was created.
86    major: u8,
87    /// Minor version number.
88    minor: u8,
89    /// Micro version number.  The major, minor, and micro version numbers
90    /// together constitute a "release series" that starts as an alpha
91    /// and eventually becomes stable.
92    micro: u8,
93    /// Patchlevel within a release series
94    patch: u8,
95    /// Status of a given release
96    status: TorVerStatus,
97    /// True if this version is given the "-dev" tag to indicate that it
98    /// isn't a real Tor release, but rather indicates the state of Tor
99    /// within some git repository.
100    dev: bool,
101}
102
103impl Display for TorVersion {
104    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
105        let devsuffix = if self.dev { "-dev" } else { "" };
106        write!(
107            f,
108            "{}.{}.{}.{}{}{}",
109            self.major,
110            self.minor,
111            self.micro,
112            self.patch,
113            self.status.suffix(),
114            devsuffix
115        )
116    }
117}
118
119impl FromStr for TorVersion {
120    type Err = crate::Error;
121
122    fn from_str(s: &str) -> crate::Result<Self> {
123        // Split the string on "-" into "version", "status", and "dev."
124        // Note that "dev" may actually be in the "status" field if
125        // the version is stable; we'll handle that later.
126        let mut parts = s.split('-').fuse();
127        let ver_part = parts.next();
128        let status_part = parts.next();
129        let dev_part = parts.next();
130        if parts.next().is_some() {
131            // NOTE: If `dev_part` cannot be unwrapped then there are bigger
132            // problems with `s` input
133            #[allow(clippy::unwrap_used)]
134            return Err(EK::BadTorVersion.at_pos(Pos::at_end_of(dev_part.unwrap())));
135        }
136
137        // Split the version on "." into 3 or 4 numbers.
138        let vers: Result<Vec<_>, _> = ver_part
139            .ok_or_else(|| EK::BadTorVersion.at_pos(Pos::at(s)))?
140            .splitn(4, '.')
141            .map(|v| v.parse::<u8>())
142            .collect();
143        let vers = vers.map_err(|_| EK::BadTorVersion.at_pos(Pos::at(s)))?;
144        if vers.len() < 3 {
145            return Err(EK::BadTorVersion.at_pos(Pos::at(s)));
146        }
147        let major = vers[0];
148        let minor = vers[1];
149        let micro = vers[2];
150        let patch = if vers.len() == 4 { vers[3] } else { 0 };
151
152        // Compute real status and version.
153        let status = match status_part {
154            Some("alpha") => TorVerStatus::Alpha,
155            Some("beta") => TorVerStatus::Beta,
156            Some("rc") => TorVerStatus::Rc,
157            None | Some("dev") => TorVerStatus::Stable,
158            _ => TorVerStatus::Other,
159        };
160        let dev = match (status_part, dev_part) {
161            (_, Some("dev")) => true,
162            (_, Some(s)) => {
163                return Err(EK::BadTorVersion.at_pos(Pos::at(s)));
164            }
165            (Some("dev"), None) => true,
166            (_, _) => false,
167        };
168
169        Ok(TorVersion {
170            major,
171            minor,
172            micro,
173            patch,
174            status,
175            dev,
176        })
177    }
178}
179
180#[cfg(test)]
181mod test {
182    // @@ begin test lint list maintained by maint/add_warning @@
183    #![allow(clippy::bool_assert_comparison)]
184    #![allow(clippy::clone_on_copy)]
185    #![allow(clippy::dbg_macro)]
186    #![allow(clippy::mixed_attributes_style)]
187    #![allow(clippy::print_stderr)]
188    #![allow(clippy::print_stdout)]
189    #![allow(clippy::single_char_pattern)]
190    #![allow(clippy::unwrap_used)]
191    #![allow(clippy::unchecked_duration_subtraction)]
192    #![allow(clippy::useless_vec)]
193    #![allow(clippy::needless_pass_by_value)]
194    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
195    use super::*;
196
197    #[test]
198    fn parse_good() {
199        let mut lastver = None;
200        for (s1, s2) in &[
201            ("0.1.2", "0.1.2.0"),
202            ("0.1.2.0-dev", "0.1.2.0-dev"),
203            ("0.4.3.1-bloop", "0.4.3.1-???"),
204            ("0.4.3.1-alpha", "0.4.3.1-alpha"),
205            ("0.4.3.1-alpha-dev", "0.4.3.1-alpha-dev"),
206            ("0.4.3.1-beta", "0.4.3.1-beta"),
207            ("0.4.3.1-rc", "0.4.3.1-rc"),
208            ("0.4.3.1", "0.4.3.1"),
209        ] {
210            let t: TorVersion = s1.parse().unwrap();
211            assert_eq!(&t.to_string(), s2);
212
213            if let Some(v) = lastver {
214                assert!(v < t);
215            }
216            lastver = Some(t);
217        }
218    }
219
220    #[test]
221    fn parse_bad() {
222        for s in &[
223            "fred.and.bob",
224            "11",
225            "11.22",
226            "0x2020",
227            "1.2.3.marzipan",
228            "0.1.2.5-alpha-deeev",
229            "0.1.2.5-alpha-dev-turducken",
230        ] {
231            assert!(s.parse::<TorVersion>().is_err());
232        }
233    }
234}