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

            
42
use std::fmt::{self, Display, Formatter};
43
use std::str::FromStr;
44

            
45
use 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)]
55
enum 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

            
68
impl TorVerStatus {
69
    /// Helper for encoding: return the suffix that represents a version.
70
16
    fn suffix(self) -> &'static str {
71
        use TorVerStatus::*;
72
16
        match self {
73
6
            Stable => "",
74
2
            Rc => "-rc",
75
2
            Beta => "-beta",
76
4
            Alpha => "-alpha",
77
2
            Other => "-???",
78
        }
79
16
    }
80
}
81

            
82
/// A parsed Tor version number.
83
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
84
pub 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

            
103
impl Display for TorVersion {
104
16
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
105
16
        let devsuffix = if self.dev { "-dev" } else { "" };
106
16
        write!(
107
16
            f,
108
16
            "{}.{}.{}.{}{}{}",
109
            self.major,
110
            self.minor,
111
            self.micro,
112
            self.patch,
113
16
            self.status.suffix(),
114
            devsuffix
115
        )
116
16
    }
117
}
118

            
119
impl FromStr for TorVersion {
120
    type Err = crate::Error;
121

            
122
4066
    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
4066
        let mut parts = s.split('-').fuse();
127
4066
        let ver_part = parts.next();
128
4066
        let status_part = parts.next();
129
4066
        let dev_part = parts.next();
130
4066
        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
2
            return Err(EK::BadTorVersion.at_pos(Pos::at_end_of(dev_part.unwrap())));
135
4064
        }
136

            
137
        // Split the version on "." into 3 or 4 numbers.
138
4064
        let vers: Result<Vec<_>, _> = ver_part
139
4064
            .ok_or_else(|| EK::BadTorVersion.at_pos(Pos::at(s)))?
140
4064
            .splitn(4, '.')
141
16384
            .map(|v| v.parse::<u8>())
142
4064
            .collect();
143
4067
        let vers = vers.map_err(|_| EK::BadTorVersion.at_pos(Pos::at(s)))?;
144
4058
        if vers.len() < 3 {
145
4
            return Err(EK::BadTorVersion.at_pos(Pos::at(s)));
146
4054
        }
147
4054
        let major = vers[0];
148
4054
        let minor = vers[1];
149
4054
        let micro = vers[2];
150
4054
        let patch = if vers.len() == 4 { vers[3] } else { 0 };
151

            
152
        // Compute real status and version.
153
4054
        let status = match status_part {
154
1942
            Some("alpha") => TorVerStatus::Alpha,
155
1526
            Some("beta") => TorVerStatus::Beta,
156
1524
            Some("rc") => TorVerStatus::Rc,
157
3632
            None | Some("dev") => TorVerStatus::Stable,
158
2
            _ => TorVerStatus::Other,
159
        };
160
4054
        let dev = match (status_part, dev_part) {
161
12
            (_, Some("dev")) => true,
162
2
            (_, Some(s)) => {
163
2
                return Err(EK::BadTorVersion.at_pos(Pos::at(s)));
164
            }
165
1930
            (Some("dev"), None) => true,
166
2522
            (_, _) => false,
167
        };
168

            
169
4052
        Ok(TorVersion {
170
4052
            major,
171
4052
            minor,
172
4052
            micro,
173
4052
            patch,
174
4052
            status,
175
4052
            dev,
176
4052
        })
177
4066
    }
178
}
179

            
180
#[cfg(test)]
181
mod 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_time_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
}