1use std::cmp::Ordering;
3use std::option::Option;
4use std::str::FromStr;
5use std::string::ToString;
6
7#[derive(thiserror::Error, Debug)]
9pub enum Error {
10 #[error("{}", .0)]
11 ParseError(String),
12}
13
14#[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 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 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 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 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 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("" )).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 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}