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)]
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 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 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("" )).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 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}