1use crate::sys::version as version_impl;
2use derive_more::Display;
3use nom::{
4 branch::alt,
5 character::complete::{char, digit1, hex_digit1, space1},
6 combinator::{map_res, verify},
7 error::context,
8 sequence::delimited,
9 IResult, Parser,
10};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use std::path::{Path, PathBuf};
13use std::{cmp::Ordering, str::FromStr};
14use regex::Regex;
15
16mod release_type;
17mod revision_hash;
18use crate::error::VersionError;
19pub use release_type::ReleaseType;
20pub use revision_hash::RevisionHash;
21
22#[derive(Eq, Debug, Clone, Hash, PartialOrd, Display)]
23#[display("{}{}{}", base, release_type, revision)]
24pub struct Version {
25 base: semver::Version,
26 release_type: ReleaseType,
27 revision: u64,
28}
29
30impl Serialize for Version {
31 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
32 where
33 S: Serializer,
34 {
35 let s = self.to_string();
36 serializer.serialize_str(&s)
37 }
38}
39
40impl<'de> Deserialize<'de> for Version {
41 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42 where
43 D: Deserializer<'de>,
44 {
45 let s = String::deserialize(deserializer)?;
46 Version::from_str(&s).map_err(serde::de::Error::custom)
47 }
48}
49
50impl Ord for Version {
51 fn cmp(&self, other: &Version) -> Ordering {
52 self.base
53 .cmp(&other.base)
54 .then(self.release_type.cmp(&other.release_type))
55 .then(self.revision.cmp(&other.revision))
56 }
57}
58
59impl PartialEq for Version {
60 fn eq(&self, other: &Self) -> bool {
61 self.base == other.base
62 && self.release_type == other.release_type
63 && self.revision == other.revision
64 }
65}
66
67impl AsRef<Version> for Version {
68 fn as_ref(&self) -> &Self {
69 self
70 }
71}
72
73impl AsMut<Version> for Version {
74 fn as_mut(&mut self) -> &mut Self {
75 self
76 }
77}
78
79impl FromStr for Version {
80 type Err = VersionError;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 match parse_version(s) {
84 Ok((_, version)) => Ok(version),
85 Err(err) => {
86 let error_msg = match err {
87 nom::Err::Error(e) | nom::Err::Failure(e) => {
88 format!("Parse error at: {}", e.input)
89 },
90 _ => "Unknown parsing error".to_string(),
91 };
92 Err(VersionError::ParsingFailed(error_msg))
93 }
94 }
95 }
96}
97
98impl TryFrom<&str> for Version {
99 type Error = <Version as FromStr>::Err;
100
101 fn try_from(value: &str) -> Result<Self, Self::Error> {
102 Version::from_str(value)
103 }
104}
105
106impl TryFrom<String> for Version {
107 type Error = <Version as FromStr>::Err;
108
109 fn try_from(value: String) -> Result<Self, Self::Error> {
110 Version::from_str(&value)
111 }
112}
113
114impl TryFrom<PathBuf> for Version {
115 type Error = VersionError;
116
117 fn try_from(path: PathBuf) -> Result<Self, VersionError> {
118 Version::from_path(path)
119 }
120}
121
122impl TryFrom<&Path> for Version {
123 type Error = VersionError;
124
125 fn try_from(path: &Path) -> Result<Self, VersionError> {
126 Version::from_path(path)
127 }
128}
129
130impl Version {
131 pub fn new(
132 major: u64,
133 minor: u64,
134 patch: u64,
135 release_type: ReleaseType,
136 revision: u64,
137 ) -> Version {
138 let base = semver::Version::new(major, minor, patch);
139 Version {
140 base,
141 release_type,
142 revision,
143 }
144 }
145
146 pub fn release_type(&self) -> ReleaseType {
147 self.release_type
148 }
149
150 pub fn major(&self) -> u64 {
151 self.base.major
152 }
153
154 pub fn minor(&self) -> u64 {
155 self.base.minor
156 }
157
158 pub fn patch(&self) -> u64 {
159 self.base.patch
160 }
161
162 pub fn revision(&self) -> u64 {
163 self.revision
164 }
165
166 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, VersionError> {
167 version_impl::read_version_from_path(path)
168 }
169
170 pub fn from_string_containing<S: AsRef<str>>(s: S) -> Result<Self, VersionError> {
171 let s = s.as_ref();
172 Self::extract_version_from_text(s)
173 .ok_or_else(|| VersionError::ParsingFailed(format!("Could not find a valid Unity version in string: {}", s)))
174 }
175
176 fn extract_version_from_text(text: &str) -> Option<Version> {
179 use std::sync::OnceLock;
180
181 static VERSION_REGEX: OnceLock<Regex> = OnceLock::new();
183 let regex = VERSION_REGEX.get_or_init(|| {
184 Regex::new(r"([0-9]{1,4})\.([0-9]{1,4})\.([0-9]{1,4})(f|p|b|a)([0-9]{1,4})(_([a-z0-9]{12})| \(([a-z0-9]{12})\)|/([a-z0-9]{12}))?").unwrap()
185 });
186
187 for captures in regex.captures_iter(text) {
189 if captures.get(8).is_some() {
190 let version_string = format!(
192 "{}.{}.{}{}{}",
193 &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
194 );
195
196 if let Ok(version) = Version::from_str(&version_string) {
197 return Some(version);
198 }
199 }
200 }
201
202 for captures in regex.captures_iter(text) {
204 if captures.get(7).is_some() {
205 let version_string = format!(
207 "{}.{}.{}{}{}",
208 &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
209 );
210
211 if let Ok(version) = Version::from_str(&version_string) {
212 return Some(version);
213 }
214 }
215 }
216
217 for captures in regex.captures_iter(text) {
219 if captures.get(9).is_some() {
220 let version_string = format!(
222 "{}.{}.{}{}{}",
223 &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
224 );
225
226 if let Ok(version) = Version::from_str(&version_string) {
227 return Some(version);
228 }
229 }
230 }
231
232 for captures in regex.captures_iter(text) {
234 let version_string = format!(
235 "{}.{}.{}{}{}",
236 &captures[1], &captures[2], &captures[3], &captures[4], &captures[5]
237 );
238
239 if let Ok(version) = Version::from_str(&version_string) {
240 return Some(version);
241 }
242 }
243
244 None
245 }
246
247 pub fn base(&self) -> &semver::Version {
248 &self.base
249 }
250
251 #[cfg(unix)]
254 pub fn find_version_in_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, VersionError> {
255 use std::process::{Command, Stdio};
256 use log::debug;
257
258 let path = path.as_ref();
259 debug!("find api version in Unity executable {}", path.display());
260
261 let child = Command::new("strings")
262 .arg("--")
263 .arg(path)
264 .stdout(Stdio::piped())
265 .stderr(Stdio::piped())
266 .spawn()
267 .map_err(|e| VersionError::Other {
268 source: e.into(),
269 msg: "failed to spawn strings".to_string(),
270 })?;
271
272 let output = child.wait_with_output().map_err(|e| VersionError::Other {
273 source: e.into(),
274 msg: "failed to spawn strings".to_string(),
275 })?;
276
277 if !output.status.success() {
278 return Err(VersionError::ExecutableContainsNoVersion(
279 path.to_path_buf(),
280 ));
281 }
282
283 let strings_output = String::from_utf8_lossy(&output.stdout);
284
285 Self::extract_version_from_text(&strings_output)
287 .map(|version| {
288 debug!("found version {} in executable", &version);
289 version
290 })
291 .ok_or_else(|| VersionError::ExecutableContainsNoVersion(path.to_path_buf()))
292 }
293}
294
295#[derive(Eq, Debug, Clone, Hash, Display)]
296#[display("{} ({})", version, revision)]
297#[allow(dead_code)]
298pub struct CompleteVersion {
299 version: Version,
300 revision: RevisionHash,
301}
302
303impl CompleteVersion {
304 pub fn new(version: Version, revision: RevisionHash) -> Self {
306 Self { version, revision }
307 }
308
309 pub fn version(&self) -> &Version {
311 &self.version
312 }
313
314 pub fn revision(&self) -> &RevisionHash {
316 &self.revision
317 }
318}
319
320impl FromStr for CompleteVersion {
321 type Err = VersionError;
322
323 fn from_str(s: &str) -> Result<Self, Self::Err> {
337 match parse_complete_version(s.trim()) {
338 Ok((remaining, complete_version)) => {
339 if remaining.is_empty() {
340 Ok(complete_version)
341 } else {
342 Err(VersionError::ParsingFailed(
343 format!("Unexpected remaining input: '{}'", remaining)
344 ))
345 }
346 }
347 Err(err) => {
348 let error_msg = match err {
349 nom::Err::Error(e) | nom::Err::Failure(e) => {
350 format!("Parse error at: '{}'", e.input)
351 }
352 nom::Err::Incomplete(_) => {
353 "Incomplete input".to_string()
354 }
355 };
356 Err(VersionError::ParsingFailed(error_msg))
357 }
358 }
359 }
360}
361
362impl PartialEq for CompleteVersion {
363 fn eq(&self, other: &Self) -> bool {
364 self.version == other.version
365 }
366}
367
368impl PartialOrd for CompleteVersion {
369 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
370 self.version.partial_cmp(&other.version)
371 }
372}
373
374fn parse_release_type(input: &str) -> IResult<&str, ReleaseType> {
375 context(
376 "release type",
377 map_res(alt((char('f'), char('b'), char('a'), char('p'))), |c| {
378 ReleaseType::try_from(c)
379 }),
380 ).parse(input)
381}
382
383fn parse_version(input: &str) -> IResult<&str, Version> {
384 context(
385 "version",
386 (
387 context("major version", map_res(digit1, |s: &str| s.parse::<u64>())),
388 char('.'),
389 context("minor version", map_res(digit1, |s: &str| s.parse::<u64>())),
390 char('.'),
391 context("patch version", map_res(digit1, |s: &str| s.parse::<u64>())),
392 parse_release_type,
393 context("revision", map_res(digit1, |s: &str| s.parse::<u64>())),
394 )
395 )
396 .map(|(major, _, minor, _, patch, release_type, revision)| {
397 let base = semver::Version::new(major, minor, patch);
398 Version {
399 base,
400 release_type,
401 revision,
402 }
403 })
404 .parse(input)
405}
406
407fn parse_revision_hash(input: &str) -> IResult<&str, RevisionHash> {
408 context(
409 "revision hash",
410 map_res(
411 verify(hex_digit1, |s: &str| s.len() == 12),
412 |hex_str: &str| RevisionHash::new(hex_str)
413 )
414 ).parse(input)
415}
416
417fn parse_complete_version(input: &str) -> IResult<&str, CompleteVersion> {
418 context(
419 "complete version",
420 (
421 parse_version,
422 space1,
423 delimited(char('('), parse_revision_hash, char(')')),
424 )
425 )
426 .map(|(version, _, revision)| CompleteVersion::new(version, revision))
427 .parse(input)
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use proptest::prelude::*;
434
435 #[test]
436 fn parse_version_string_with_valid_input() {
437 let version_string = "1.2.3f4";
438 let version = Version::from_str(version_string);
439 assert!(version.is_ok(), "valid input returns a version")
440 }
441
442 #[test]
443 fn splits_version_string_into_components() {
444 let version_string = "11.2.3f4";
445 let version = Version::from_str(version_string).unwrap();
446
447 assert_eq!(version.base.major, 11, "parse correct major component");
448 assert_eq!(version.base.minor, 2, "parse correct minor component");
449 assert_eq!(version.base.patch, 3, "parse correct patch component");
450
451 assert_eq!(version.release_type, ReleaseType::Final);
452 assert_eq!(version.revision, 4, "parse correct revision component");
453 }
454
455 #[test]
456 fn test_complete_version_from_str() {
457 let complete_version = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360)").unwrap();
459 assert_eq!(complete_version.version().to_string(), "2021.3.55f1");
460 assert_eq!(complete_version.revision().as_str(), "f87d5274e360");
461 assert_eq!(complete_version.to_string(), "2021.3.55f1 (f87d5274e360)");
462
463 let alpha_version = CompleteVersion::from_str("2023.1.0a1 (123456789abc)").unwrap();
465 assert_eq!(alpha_version.version().to_string(), "2023.1.0a1");
466 assert_eq!(alpha_version.revision().as_str(), "123456789abc");
467
468 let no_hash_result = CompleteVersion::from_str("2021.3.55f1");
472 assert!(no_hash_result.is_err());
473 let error_msg = no_hash_result.unwrap_err().to_string();
474 assert!(error_msg.contains("Parse error"), "Expected parsing error for missing hash, got: {}", error_msg);
475
476 let invalid_version_result = CompleteVersion::from_str("invalid (f87d5274e360)");
478 assert!(invalid_version_result.is_err());
479 let error_msg = invalid_version_result.unwrap_err().to_string();
480 assert!(error_msg.contains("Parse error"), "Expected parsing error for invalid version, got: {}", error_msg);
481
482 let invalid_hash_result = CompleteVersion::from_str("2021.3.55f1 (invalid)");
484 assert!(invalid_hash_result.is_err());
485 let error_msg = invalid_hash_result.unwrap_err().to_string();
486 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
487 "Expected revision hash error, got: {}", error_msg);
488
489 let short_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d527)");
491 assert!(short_hash_result.is_err());
492 let error_msg = short_hash_result.unwrap_err().to_string();
493 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
494 "Expected revision hash error for short hash, got: {}", error_msg);
495
496 let long_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360ab)");
498 assert!(long_hash_result.is_err());
499 let error_msg = long_hash_result.unwrap_err().to_string();
500 assert!(error_msg.contains("Parse error"), "Expected parsing error for long hash, got: {}", error_msg);
501
502 let non_hex_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e36z)");
504 assert!(non_hex_result.is_err());
505 let error_msg = non_hex_result.unwrap_err().to_string();
506 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
507 "Expected revision hash error for non-hex chars, got: {}", error_msg);
508 }
509
510 #[test]
511 fn extracts_version_from_text() {
512 let text = "Some text before 2023.1.4f5 and some after";
513 let result = Version::from_string_containing(text);
514 assert!(result.is_ok(), "Should successfully extract the version");
515
516 let version = result.unwrap();
517 assert_eq!(version.base.major, 2023);
518 assert_eq!(version.base.minor, 1);
519 assert_eq!(version.base.patch, 4);
520 assert_eq!(version.release_type, ReleaseType::Final);
521 assert_eq!(version.revision, 5);
522 }
523
524
525 #[test]
526 fn extracts_version_from_text_and_returns_first_complete_version() {
527 let text = "Some text 23 before 2023.1.4f5 and some after";
528 let result = Version::from_string_containing(text);
529 assert!(result.is_ok(), "Should successfully extract the version");
530
531 let version = result.unwrap();
532 assert_eq!(version.base.major, 2023);
533 assert_eq!(version.base.minor, 1);
534 assert_eq!(version.base.patch, 4);
535 assert_eq!(version.release_type, ReleaseType::Final);
536 assert_eq!(version.revision, 5);
537 }
538 fn generate_test_data_with_versions(
540 parentheses_version: Option<&str>,
541 underscore_version: Option<&str>,
542 slash_version: Option<&str>,
543 standalone_versions: &[&str],
544 bogus_data_size: usize
545 ) -> String {
546 let mut content = String::new();
547
548 for i in 0..bogus_data_size / 4 {
550 content.push_str(&format!("__libc_start_main_{}\n", i));
551 content.push_str("malloc\nfree\nstrlen\n");
552 content.push_str("/lib64/ld-linux-x86-64.so.2\n");
553 content.push_str("Some random binary string data\n");
554 }
555
556 for (idx, version) in standalone_versions.iter().enumerate() {
558 if idx % 2 == 0 {
559 content.push_str(&format!("Random text {}\n", idx));
560 }
561 content.push_str(&format!("{}\n", version));
562 content.push_str("More random data\n");
563 }
564
565 for i in 0..bogus_data_size / 4 {
567 content.push_str(&format!("function_name_{}\n", i));
568 content.push_str("symbol_table_entry\n");
569 content.push_str("debug_info_string\n");
570 }
571
572 if let Some(version) = slash_version {
574 content.push_str("path/to/unity/\n");
575 content.push_str(&format!("{}\n", version));
576 content.push_str("more/path/data\n");
577 }
578
579 for i in 0..bogus_data_size / 4 {
581 content.push_str(&format!("error_message_{}\n", i));
582 content.push_str("log_entry_data\n");
583 }
584
585 if let Some(version) = underscore_version {
587 content.push_str("version_info_block\n");
588 content.push_str(&format!("{}\n", version));
589 content.push_str("build_metadata\n");
590 }
591
592 for i in 0..bogus_data_size / 4 {
594 content.push_str(&format!("final_symbol_{}\n", i));
595 content.push_str("cleanup_data\n");
596 }
597
598 if let Some(version) = parentheses_version {
600 content.push_str("unity_build_info\n");
601 content.push_str(&format!("{}\n", version));
602 content.push_str("end_of_data\n");
603 }
604
605 content
606 }
607
608 #[test]
609 fn prioritizes_parentheses_hash_over_other_formats_in_large_dataset() {
610 let test_data = generate_test_data_with_versions(
611 Some("2023.1.5f1 (abc123def456)"),
612 Some("2022.3.2f1_xyz789uvw012"),
613 Some("2021.2.1f1/def456ghi789"),
614 &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2018.3.5f1"],
615 1000 );
617
618 let result = Version::from_string_containing(&test_data);
619 assert!(result.is_ok(), "Should extract version from large dataset");
620
621 let version = result.unwrap();
622 assert_eq!(version.base.major, 2023);
624 assert_eq!(version.base.minor, 1);
625 assert_eq!(version.base.patch, 5);
626 assert_eq!(version.release_type, ReleaseType::Final);
627 assert_eq!(version.revision, 1);
628 }
629
630 #[test]
631 fn prioritizes_underscore_hash_when_no_parentheses_version() {
632 let test_data = generate_test_data_with_versions(
633 None, Some("2022.3.2f1_xyz789uvw012"),
635 Some("2021.2.1f1/def456ghi789"),
636 &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1"],
637 800
638 );
639
640 let result = Version::from_string_containing(&test_data);
641 assert!(result.is_ok(), "Should extract underscore hash version");
642
643 let version = result.unwrap();
644 assert_eq!(version.base.major, 2022);
646 assert_eq!(version.base.minor, 3);
647 assert_eq!(version.base.patch, 2);
648 assert_eq!(version.release_type, ReleaseType::Final);
649 assert_eq!(version.revision, 1);
650 }
651
652 #[test]
653 fn prioritizes_slash_hash_when_no_other_hash_formats() {
654 let test_data = generate_test_data_with_versions(
655 None, None, Some("2021.2.1f1/def456ghi789"),
658 &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2025.1.0b1"],
659 600
660 );
661
662 let result = Version::from_string_containing(&test_data);
663 assert!(result.is_ok(), "Should extract slash hash version");
664
665 let version = result.unwrap();
666 assert_eq!(version.base.major, 2021);
668 assert_eq!(version.base.minor, 2);
669 assert_eq!(version.base.patch, 1);
670 assert_eq!(version.release_type, ReleaseType::Final);
671 assert_eq!(version.revision, 1);
672 }
673
674 #[test]
675 fn falls_back_to_first_standalone_version_when_no_hash_versions() {
676 let test_data = generate_test_data_with_versions(
677 None, None, None, &["2020.1.0f1", "2019.4.2f1", "2024.1.0a1", "2025.1.0b1"],
681 400
682 );
683
684 let result = Version::from_string_containing(&test_data);
685 assert!(result.is_ok(), "Should extract first standalone version");
686
687 let version = result.unwrap();
688 assert_eq!(version.base.major, 2020);
690 assert_eq!(version.base.minor, 1);
691 assert_eq!(version.base.patch, 0);
692 assert_eq!(version.release_type, ReleaseType::Final);
693 assert_eq!(version.revision, 1);
694 }
695
696 #[test]
697 fn handles_multiple_versions_with_same_priority_returns_first_found() {
698 let test_data = generate_test_data_with_versions(
699 Some("2023.1.5f1 (abc123def456)"),
700 None,
701 None,
702 &["2020.1.0f1"],
703 200
704 );
705
706 let mut modified_data = String::new();
708 modified_data.push_str("Early data\n");
709 modified_data.push_str("2024.2.1f1 (first123hash)\n");
710 modified_data.push_str("More early data\n");
711 modified_data.push_str(&test_data);
712
713 let result = Version::from_string_containing(&modified_data);
714 assert!(result.is_ok(), "Should extract first parentheses version");
715
716 let version = result.unwrap();
717 assert_eq!(version.base.major, 2024);
719 assert_eq!(version.base.minor, 2);
720 assert_eq!(version.base.patch, 1);
721 assert_eq!(version.release_type, ReleaseType::Final);
722 assert_eq!(version.revision, 1);
723 }
724
725
726 #[test]
727 fn handles_extremely_large_dataset_with_performance() {
728 let test_data = generate_test_data_with_versions(
730 Some("2023.3.10f1 (abc123def456)"),
731 Some("2022.1.1f1_def456ghi789"),
732 None,
733 &[
734 "2021.1.0f1", "2020.3.15f1", "2019.4.28f1", "2018.4.36f1",
735 "2017.4.40f1", "2016.4.39f1", "2015.4.39f1", "5.6.7f1",
736 "5.5.6f1", "5.4.6f1", "5.3.8f2", "5.2.5f1"
737 ],
738 5000 );
740
741 let start = std::time::Instant::now();
742 let result = Version::from_string_containing(&test_data);
743 let duration = start.elapsed();
744
745 assert!(result.is_ok(), "Should handle large dataset");
746 assert!(duration.as_millis() < 100, "Should parse large dataset quickly (took {:?})", duration);
747
748 let version = result.unwrap();
749 assert_eq!(version.base.major, 2023);
751 assert_eq!(version.base.minor, 3);
752 assert_eq!(version.base.patch, 10);
753 assert_eq!(version.release_type, ReleaseType::Final);
754 assert_eq!(version.revision, 1);
755 }
756
757 #[test]
758 fn prioritizes_versions_with_hashes() {
759 let test_content = r#"
761/lib64/ld-linux-x86-64.so.2
762__libc_start_main
7632020.2.0b2
7642018.1.0b7
7656000.2.0f1 (eed1c594c913)
7666000.2.0f1_eed1c594c913
7672022.2.0a1
7682018.3.0a1
7696000.2/respin/6000.2.0f1-517f89d850d1
7705.0.0a1
7716000.2.0f1.2588.6057
7722017.2.0b1
7736000.2.0f1
774"#;
775
776 let result = Version::from_string_containing(test_content);
777 assert!(result.is_ok(), "Should successfully extract a version");
778
779 let version = result.unwrap();
780 assert_eq!(version.base.major, 6000);
782 assert_eq!(version.base.minor, 2);
783 assert_eq!(version.base.patch, 0);
784 assert_eq!(version.release_type, ReleaseType::Final);
785 assert_eq!(version.revision, 1);
786 }
787
788 #[test]
789 fn handles_fallback_to_versions_without_hashes() {
790 let test_content = r#"
792Some random text
7932020.2.0b2
794More text
7952018.1.0b7
796Even more text
797"#;
798
799 let result = Version::from_string_containing(test_content);
800 assert!(result.is_ok(), "Should successfully extract a version");
801
802 let version = result.unwrap();
803 assert_eq!(version.base.major, 2020);
805 assert_eq!(version.base.minor, 2);
806 assert_eq!(version.base.patch, 0);
807 assert_eq!(version.release_type, ReleaseType::Beta);
808 assert_eq!(version.revision, 2);
809 }
810
811 proptest! {
812 #[test]
813 fn from_str_does_not_crash(s in "\\PC*") {
814 let _v = Version::from_str(&s);
815 }
816
817 #[test]
818 fn from_str_supports_all_valid_cases(
819 major in 0u64..=u64::MAX,
820 minor in 0u64..=u64::MAX,
821 patch in 0u64..=u64::MAX,
822 release_type in prop_oneof!["f", "p", "b", "a"],
823 revision in 0u64..=u64::MAX,
824 ) {
825 let version_string = format!("{}.{}.{}{}{}", major, minor, patch, release_type, revision);
826 let version = Version::from_str(&version_string).unwrap();
827
828 assert!(version.base.major == major, "parse correct major component");
829 assert!(version.base.minor == minor, "parse correct minor component");
830 assert!(version.base.patch == patch, "parse correct patch component");
831
832 assert_eq!(version.release_type, ReleaseType::from_str(&release_type).unwrap());
833 assert!(version.revision == revision, "parse correct revision component");
834 }
835 }
836}