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}