tor_interface/
legacy_tor_version.rs

1// standard
2use std::cmp::Ordering;
3use std::option::Option;
4use std::str::FromStr;
5use std::string::ToString;
6
7/// `LegacyTorVersion`-specific error type
8#[derive(thiserror::Error, Debug)]
9pub enum Error {
10    #[error("{}", .0)]
11    ParseError(String),
12}
13
14/// Type representing a legacy c-tor daemon's version number. This version conforms c-tor's [version-spec](https://spec.torproject.org/version-spec.htm).
15#[derive(Clone, Default)]
16pub struct LegacyTorVersion {
17    pub(crate) major: u32,
18    pub(crate) minor: u32,
19    pub(crate) micro: u32,
20    pub(crate) patch_level: u32,
21    pub(crate) status_tag: Option<String>,
22}
23
24impl LegacyTorVersion {
25    fn status_tag_pattern_is_match(status_tag: &str) -> bool {
26        if status_tag.is_empty() {
27            return false;
28        }
29
30        for c in status_tag.chars() {
31            if c.is_whitespace() {
32                return false;
33            }
34        }
35        true
36    }
37
38    /// Construct a new `LegacyTorVersion` object.
39    pub fn new(
40        major: u32,
41        minor: u32,
42        micro: u32,
43        patch_level: Option<u32>,
44        status_tag: Option<&str>,
45    ) -> Result<LegacyTorVersion, Error> {
46        let status_tag = if let Some(status_tag) = status_tag {
47            if Self::status_tag_pattern_is_match(status_tag) {
48                Some(status_tag.to_string())
49            } else {
50                return Err(Error::ParseError(
51                    "tor version status tag may not be empty or contain white-space".to_string(),
52                ));
53            }
54        } else {
55            None
56        };
57
58        Ok(LegacyTorVersion {
59            major,
60            minor,
61            micro,
62            patch_level: patch_level.unwrap_or(0u32),
63            status_tag,
64        })
65    }
66}
67
68impl FromStr for LegacyTorVersion {
69    type Err = Error;
70
71    fn from_str(s: &str) -> Result<LegacyTorVersion, Self::Err> {
72        // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]*
73        let mut tokens = s.split(' ');
74        let (major, minor, micro, patch_level, status_tag) =
75            if let Some(version_status_tag) = tokens.next() {
76                let mut tokens = version_status_tag.split('-');
77                let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() {
78                    let mut tokens = version.split('.');
79                    let major: u32 = if let Some(major) = tokens.next() {
80                        match major.parse() {
81                            Ok(major) => major,
82                            Err(_) => {
83                                return Err(Error::ParseError(format!(
84                                    "failed to parse '{}' as MAJOR portion of tor version",
85                                    major
86                                )))
87                            }
88                        }
89                    } else {
90                        return Err(Error::ParseError(
91                            "failed to find MAJOR portion of tor version".to_string(),
92                        ));
93                    };
94                    let minor: u32 = if let Some(minor) = tokens.next() {
95                        match minor.parse() {
96                            Ok(minor) => minor,
97                            Err(_) => {
98                                return Err(Error::ParseError(format!(
99                                    "failed to parse '{}' as MINOR portion of tor version",
100                                    minor
101                                )))
102                            }
103                        }
104                    } else {
105                        return Err(Error::ParseError(
106                            "failed to find MINOR portion of tor version".to_string(),
107                        ));
108                    };
109                    let micro: u32 = if let Some(micro) = tokens.next() {
110                        match micro.parse() {
111                            Ok(micro) => micro,
112                            Err(_) => {
113                                return Err(Error::ParseError(format!(
114                                    "failed to parse '{}' as MICRO portion of tor version",
115                                    micro
116                                )))
117                            }
118                        }
119                    } else {
120                        return Err(Error::ParseError(
121                            "failed to find MICRO portion of tor version".to_string(),
122                        ));
123                    };
124                    let patch_level: u32 = if let Some(patch_level) = tokens.next() {
125                        match patch_level.parse() {
126                            Ok(patch_level) => patch_level,
127                            Err(_) => {
128                                return Err(Error::ParseError(format!(
129                                    "failed to parse '{}' as PATCHLEVEL portion of tor version",
130                                    patch_level
131                                )))
132                            }
133                        }
134                    } else {
135                        0u32
136                    };
137                    (major, minor, micro, patch_level)
138                } else {
139                    // if there were '-' the previous next() would have returned the enire string
140                    unreachable!();
141                };
142                let status_tag = tokens.next().map(|status_tag| status_tag.to_string());
143
144                (major, minor, micro, patch_level, status_tag)
145            } else {
146                // if there were no ' ' character the previou snext() would have returned the enire string
147                unreachable!();
148            };
149        for extra_info in tokens {
150            if !extra_info.starts_with('(') || !extra_info.ends_with(')') {
151                return Err(Error::ParseError(format!(
152                    "failed to parse '{}' as [ (EXTRA_INFO)]",
153                    extra_info
154                )));
155            }
156        }
157        LegacyTorVersion::new(
158            major,
159            minor,
160            micro,
161            Some(patch_level),
162            status_tag.as_deref(),
163        )
164    }
165}
166
167impl std::fmt::Display for LegacyTorVersion {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        match &self.status_tag {
170            Some(status_tag) => write!(
171                f,
172                "{}.{}.{}.{}-{}",
173                self.major, self.minor, self.micro, self.patch_level, status_tag
174            ),
175            None => write!(
176                f,
177                "{}.{}.{}.{}",
178                self.major, self.minor, self.micro, self.patch_level
179            ),
180        }
181    }
182}
183
184impl PartialEq for LegacyTorVersion {
185    fn eq(&self, other: &Self) -> bool {
186        self.major == other.major
187            && self.minor == other.minor
188            && self.micro == other.micro
189            && self.patch_level == other.patch_level
190            && self.status_tag == other.status_tag
191    }
192}
193
194impl PartialOrd for LegacyTorVersion {
195    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
196        if let Some(order) = self.major.partial_cmp(&other.major) {
197            if order != Ordering::Equal {
198                return Some(order);
199            }
200        }
201
202        if let Some(order) = self.minor.partial_cmp(&other.minor) {
203            if order != Ordering::Equal {
204                return Some(order);
205            }
206        }
207
208        if let Some(order) = self.micro.partial_cmp(&other.micro) {
209            if order != Ordering::Equal {
210                return Some(order);
211            }
212        }
213
214        if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) {
215            if order != Ordering::Equal {
216                return Some(order);
217            }
218        }
219
220        // version-spect.txt *does* say that we should compare tags lexicgraphically
221        // if all of the version numbers are the same when comparing, but we are
222        // going to diverge here and say we can only compare tags for equality.
223        //
224        // In practice we will be comparing tor daemon tags against tagless (stable)
225        // versions so this shouldn't be an issue
226
227        if self.status_tag == other.status_tag {
228            return Some(Ordering::Equal);
229        }
230
231        None
232    }
233}
234
235#[test]
236fn test_version() -> anyhow::Result<()> {
237    assert!(LegacyTorVersion::from_str("1.2.3")? == LegacyTorVersion::new(1, 2, 3, None, None)?);
238    assert!(
239        LegacyTorVersion::from_str("1.2.3.4")? == LegacyTorVersion::new(1, 2, 3, Some(4), None)?
240    );
241    assert!(
242        LegacyTorVersion::from_str("1.2.3-test")?
243            == LegacyTorVersion::new(1, 2, 3, None, Some("test"))?
244    );
245    assert!(
246        LegacyTorVersion::from_str("1.2.3.4-test")?
247            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("test"))?
248    );
249    assert!(
250        LegacyTorVersion::from_str("1.2.3 (extra_info)")?
251            == LegacyTorVersion::new(1, 2, 3, None, None)?
252    );
253    assert!(
254        LegacyTorVersion::from_str("1.2.3.4 (extra_info)")?
255            == LegacyTorVersion::new(1, 2, 3, Some(4), None)?
256    );
257    assert!(
258        LegacyTorVersion::from_str("1.2.3.4-tag (extra_info)")?
259            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))?
260    );
261
262    assert!(
263        LegacyTorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")?
264            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))?
265    );
266
267    assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err());
268    assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err());
269    assert!(LegacyTorVersion::from_str("").is_err());
270    assert!(LegacyTorVersion::from_str("1.2").is_err());
271    assert!(LegacyTorVersion::from_str("1.2-foo").is_err());
272    assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar").is_err());
273    assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err());
274    assert!(LegacyTorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err());
275    assert!(
276        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
277            < LegacyTorVersion::new(1, 0, 0, Some(0), None)?
278    );
279    assert!(
280        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
281            < LegacyTorVersion::new(0, 1, 0, Some(0), None)?
282    );
283    assert!(
284        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
285            < LegacyTorVersion::new(0, 0, 1, Some(0), None)?
286    );
287
288    // ensure status tags make comparison between equal versions (apart from
289    // tags) unknowable
290    let zero_version = LegacyTorVersion::new(0, 0, 0, Some(0), None)?;
291    let zero_version_tag = LegacyTorVersion::new(0, 0, 0, Some(0), Some("tag"))?;
292
293    assert!(!(zero_version < zero_version_tag));
294    assert!(!(zero_version <= zero_version_tag));
295    assert!(!(zero_version > zero_version_tag));
296    assert!(!(zero_version >= zero_version_tag));
297
298    Ok(())
299}