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, sync::OnceLock};
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 static VERSION_REGEX: OnceLock<Regex> = OnceLock::new();
172 let regex = VERSION_REGEX.get_or_init(|| {
173 Regex::new(r"\b\d+\.\d+\.\d+[fabp]\d+\b").unwrap()
174 });
175
176 let s = s.as_ref();
177
178 for mat in regex.find_iter(s) {
179 if let Ok(version) = Version::from_str(mat.as_str()) {
180 return Ok(version);
181 }
182 }
183 Err(VersionError::ParsingFailed(format!("Could not find a valid Unity version in string: {}", s)))
184 }
185
186 pub fn base(&self) -> &semver::Version {
187 &self.base
188 }
189}
190
191#[derive(Eq, Debug, Clone, Hash, Display)]
192#[display("{} ({})", version, revision)]
193#[allow(dead_code)]
194pub struct CompleteVersion {
195 version: Version,
196 revision: RevisionHash,
197}
198
199impl CompleteVersion {
200 pub fn new(version: Version, revision: RevisionHash) -> Self {
202 Self { version, revision }
203 }
204
205 pub fn version(&self) -> &Version {
207 &self.version
208 }
209
210 pub fn revision(&self) -> &RevisionHash {
212 &self.revision
213 }
214}
215
216impl FromStr for CompleteVersion {
217 type Err = VersionError;
218
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
233 match parse_complete_version(s.trim()) {
234 Ok((remaining, complete_version)) => {
235 if remaining.is_empty() {
236 Ok(complete_version)
237 } else {
238 Err(VersionError::ParsingFailed(
239 format!("Unexpected remaining input: '{}'", remaining)
240 ))
241 }
242 }
243 Err(err) => {
244 let error_msg = match err {
245 nom::Err::Error(e) | nom::Err::Failure(e) => {
246 format!("Parse error at: '{}'", e.input)
247 }
248 nom::Err::Incomplete(_) => {
249 "Incomplete input".to_string()
250 }
251 };
252 Err(VersionError::ParsingFailed(error_msg))
253 }
254 }
255 }
256}
257
258impl PartialEq for CompleteVersion {
259 fn eq(&self, other: &Self) -> bool {
260 self.version == other.version
261 }
262}
263
264impl PartialOrd for CompleteVersion {
265 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
266 self.version.partial_cmp(&other.version)
267 }
268}
269
270fn parse_release_type(input: &str) -> IResult<&str, ReleaseType> {
271 context(
272 "release type",
273 map_res(alt((char('f'), char('b'), char('a'), char('p'))), |c| {
274 ReleaseType::try_from(c)
275 }),
276 ).parse(input)
277}
278
279fn parse_version(input: &str) -> IResult<&str, Version> {
280 context(
281 "version",
282 (
283 context("major version", map_res(digit1, |s: &str| s.parse::<u64>())),
284 char('.'),
285 context("minor version", map_res(digit1, |s: &str| s.parse::<u64>())),
286 char('.'),
287 context("patch version", map_res(digit1, |s: &str| s.parse::<u64>())),
288 parse_release_type,
289 context("revision", map_res(digit1, |s: &str| s.parse::<u64>())),
290 )
291 )
292 .map(|(major, _, minor, _, patch, release_type, revision)| {
293 let base = semver::Version::new(major, minor, patch);
294 Version {
295 base,
296 release_type,
297 revision,
298 }
299 })
300 .parse(input)
301}
302
303fn parse_revision_hash(input: &str) -> IResult<&str, RevisionHash> {
304 context(
305 "revision hash",
306 map_res(
307 verify(hex_digit1, |s: &str| s.len() == 12),
308 |hex_str: &str| RevisionHash::new(hex_str)
309 )
310 ).parse(input)
311}
312
313fn parse_complete_version(input: &str) -> IResult<&str, CompleteVersion> {
314 context(
315 "complete version",
316 (
317 parse_version,
318 space1,
319 delimited(char('('), parse_revision_hash, char(')')),
320 )
321 )
322 .map(|(version, _, revision)| CompleteVersion::new(version, revision))
323 .parse(input)
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use proptest::prelude::*;
330
331 #[test]
332 fn parse_version_string_with_valid_input() {
333 let version_string = "1.2.3f4";
334 let version = Version::from_str(version_string);
335 assert!(version.is_ok(), "valid input returns a version")
336 }
337
338 #[test]
339 fn splits_version_string_into_components() {
340 let version_string = "11.2.3f4";
341 let version = Version::from_str(version_string).unwrap();
342
343 assert_eq!(version.base.major, 11, "parse correct major component");
344 assert_eq!(version.base.minor, 2, "parse correct minor component");
345 assert_eq!(version.base.patch, 3, "parse correct patch component");
346
347 assert_eq!(version.release_type, ReleaseType::Final);
348 assert_eq!(version.revision, 4, "parse correct revision component");
349 }
350
351 #[test]
352 fn test_complete_version_from_str() {
353 let complete_version = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360)").unwrap();
355 assert_eq!(complete_version.version().to_string(), "2021.3.55f1");
356 assert_eq!(complete_version.revision().as_str(), "f87d5274e360");
357 assert_eq!(complete_version.to_string(), "2021.3.55f1 (f87d5274e360)");
358
359 let alpha_version = CompleteVersion::from_str("2023.1.0a1 (123456789abc)").unwrap();
361 assert_eq!(alpha_version.version().to_string(), "2023.1.0a1");
362 assert_eq!(alpha_version.revision().as_str(), "123456789abc");
363
364 let no_hash_result = CompleteVersion::from_str("2021.3.55f1");
368 assert!(no_hash_result.is_err());
369 let error_msg = no_hash_result.unwrap_err().to_string();
370 assert!(error_msg.contains("Parse error"), "Expected parsing error for missing hash, got: {}", error_msg);
371
372 let invalid_version_result = CompleteVersion::from_str("invalid (f87d5274e360)");
374 assert!(invalid_version_result.is_err());
375 let error_msg = invalid_version_result.unwrap_err().to_string();
376 assert!(error_msg.contains("Parse error"), "Expected parsing error for invalid version, got: {}", error_msg);
377
378 let invalid_hash_result = CompleteVersion::from_str("2021.3.55f1 (invalid)");
380 assert!(invalid_hash_result.is_err());
381 let error_msg = invalid_hash_result.unwrap_err().to_string();
382 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
383 "Expected revision hash error, got: {}", error_msg);
384
385 let short_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d527)");
387 assert!(short_hash_result.is_err());
388 let error_msg = short_hash_result.unwrap_err().to_string();
389 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
390 "Expected revision hash error for short hash, got: {}", error_msg);
391
392 let long_hash_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e360ab)");
394 assert!(long_hash_result.is_err());
395 let error_msg = long_hash_result.unwrap_err().to_string();
396 assert!(error_msg.contains("Parse error"), "Expected parsing error for long hash, got: {}", error_msg);
397
398 let non_hex_result = CompleteVersion::from_str("2021.3.55f1 (f87d5274e36z)");
400 assert!(non_hex_result.is_err());
401 let error_msg = non_hex_result.unwrap_err().to_string();
402 assert!(error_msg.contains("Invalid revision hash") || error_msg.contains("Parse error"),
403 "Expected revision hash error for non-hex chars, got: {}", error_msg);
404 }
405
406 #[test]
407 fn extracts_version_from_text() {
408 let text = "Some text before 2023.1.4f5 and some after";
409 let result = Version::from_string_containing(text);
410 assert!(result.is_ok(), "Should successfully extract the version");
411
412 let version = result.unwrap();
413 assert_eq!(version.base.major, 2023);
414 assert_eq!(version.base.minor, 1);
415 assert_eq!(version.base.patch, 4);
416 assert_eq!(version.release_type, ReleaseType::Final);
417 assert_eq!(version.revision, 5);
418 }
419
420
421 #[test]
422 fn extracts_version_from_text_and_returns_first_complete_version() {
423 let text = "Some text 23 before 2023.1.4f5 and some after";
424 let result = Version::from_string_containing(text);
425 assert!(result.is_ok(), "Should successfully extract the version");
426
427 let version = result.unwrap();
428 assert_eq!(version.base.major, 2023);
429 assert_eq!(version.base.minor, 1);
430 assert_eq!(version.base.patch, 4);
431 assert_eq!(version.release_type, ReleaseType::Final);
432 assert_eq!(version.revision, 5);
433 }
434
435 proptest! {
436 #[test]
437 fn from_str_does_not_crash(s in "\\PC*") {
438 let _v = Version::from_str(&s);
439 }
440
441 #[test]
442 fn from_str_supports_all_valid_cases(
443 major in 0u64..=u64::MAX,
444 minor in 0u64..=u64::MAX,
445 patch in 0u64..=u64::MAX,
446 release_type in prop_oneof!["f", "p", "b", "a"],
447 revision in 0u64..=u64::MAX,
448 ) {
449 let version_string = format!("{}.{}.{}{}{}", major, minor, patch, release_type, revision);
450 let version = Version::from_str(&version_string).unwrap();
451
452 assert!(version.base.major == major, "parse correct major component");
453 assert!(version.base.minor == minor, "parse correct minor component");
454 assert!(version.base.patch == patch, "parse correct patch component");
455
456 assert_eq!(version.release_type, ReleaseType::from_str(&release_type).unwrap());
457 assert!(version.revision == revision, "parse correct revision component");
458 }
459 }
460}