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)]
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 ToString for LegacyTorVersion {
168    fn to_string(&self) -> String {
169        match &self.status_tag {
170            Some(status_tag) => format!(
171                "{}.{}.{}.{}-{}",
172                self.major, self.minor, self.micro, self.patch_level, status_tag
173            ),
174            None => format!(
175                "{}.{}.{}.{}",
176                self.major, self.minor, self.micro, self.patch_level
177            ),
178        }
179    }
180}
181
182impl PartialEq for LegacyTorVersion {
183    fn eq(&self, other: &Self) -> bool {
184        self.major == other.major
185            && self.minor == other.minor
186            && self.micro == other.micro
187            && self.patch_level == other.patch_level
188            && self.status_tag == other.status_tag
189    }
190}
191
192impl PartialOrd for LegacyTorVersion {
193    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
194        if let Some(order) = self.major.partial_cmp(&other.major) {
195            if order != Ordering::Equal {
196                return Some(order);
197            }
198        }
199
200        if let Some(order) = self.minor.partial_cmp(&other.minor) {
201            if order != Ordering::Equal {
202                return Some(order);
203            }
204        }
205
206        if let Some(order) = self.micro.partial_cmp(&other.micro) {
207            if order != Ordering::Equal {
208                return Some(order);
209            }
210        }
211
212        if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) {
213            if order != Ordering::Equal {
214                return Some(order);
215            }
216        }
217
218        // version-spect.txt *does* say that we should compare tags lexicgraphically
219        // if all of the version numbers are the same when comparing, but we are
220        // going to diverge here and say we can only compare tags for equality.
221        //
222        // In practice we will be comparing tor daemon tags against tagless (stable)
223        // versions so this shouldn't be an issue
224
225        if self.status_tag == other.status_tag {
226            return Some(Ordering::Equal);
227        }
228
229        None
230    }
231}
232
233#[test]
234fn test_version() -> anyhow::Result<()> {
235    assert!(LegacyTorVersion::from_str("1.2.3")? == LegacyTorVersion::new(1, 2, 3, None, None)?);
236    assert!(
237        LegacyTorVersion::from_str("1.2.3.4")? == LegacyTorVersion::new(1, 2, 3, Some(4), None)?
238    );
239    assert!(
240        LegacyTorVersion::from_str("1.2.3-test")?
241            == LegacyTorVersion::new(1, 2, 3, None, Some("test"))?
242    );
243    assert!(
244        LegacyTorVersion::from_str("1.2.3.4-test")?
245            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("test"))?
246    );
247    assert!(
248        LegacyTorVersion::from_str("1.2.3 (extra_info)")?
249            == LegacyTorVersion::new(1, 2, 3, None, None)?
250    );
251    assert!(
252        LegacyTorVersion::from_str("1.2.3.4 (extra_info)")?
253            == LegacyTorVersion::new(1, 2, 3, Some(4), None)?
254    );
255    assert!(
256        LegacyTorVersion::from_str("1.2.3.4-tag (extra_info)")?
257            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))?
258    );
259
260    assert!(
261        LegacyTorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")?
262            == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))?
263    );
264
265    assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err());
266    assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err());
267    assert!(LegacyTorVersion::from_str("").is_err());
268    assert!(LegacyTorVersion::from_str("1.2").is_err());
269    assert!(LegacyTorVersion::from_str("1.2-foo").is_err());
270    assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar").is_err());
271    assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err());
272    assert!(LegacyTorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err());
273    assert!(
274        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
275            < LegacyTorVersion::new(1, 0, 0, Some(0), None)?
276    );
277    assert!(
278        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
279            < LegacyTorVersion::new(0, 1, 0, Some(0), None)?
280    );
281    assert!(
282        LegacyTorVersion::new(0, 0, 0, Some(0), None)?
283            < LegacyTorVersion::new(0, 0, 1, Some(0), None)?
284    );
285
286    // ensure status tags make comparison between equal versions (apart from
287    // tags) unknowable
288    let zero_version = LegacyTorVersion::new(0, 0, 0, Some(0), None)?;
289    let zero_version_tag = LegacyTorVersion::new(0, 0, 0, Some(0), Some("tag"))?;
290
291    assert!(!(zero_version < zero_version_tag));
292    assert!(!(zero_version <= zero_version_tag));
293    assert!(!(zero_version > zero_version_tag));
294    assert!(!(zero_version >= zero_version_tag));
295
296    Ok(())
297}