1use crate::sys::version as version_impl;
2use derive_more::Display;
3use nom::{
4 branch::alt,
5 character::complete::{char, digit1},
6 combinator::map_res,
7 error::context,
8 IResult, Parser,
9};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use std::path::{Path, PathBuf};
12use std::{cmp::Ordering, str::FromStr};
13use regex::Regex;
14
15mod release_type;
16mod revision_hash;
17use crate::error::VersionError;
18pub use release_type::ReleaseType;
19pub use revision_hash::RevisionHash;
20
21#[derive(Eq, Debug, Clone, Hash, PartialOrd, Display)]
22#[display("{}{}{}", base, release_type, revision)]
23pub struct Version {
24 base: semver::Version,
25 release_type: ReleaseType,
26 revision: u64,
27}
28
29impl Serialize for Version {
30 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
31 where
32 S: Serializer,
33 {
34 let s = self.to_string();
35 serializer.serialize_str(&s)
36 }
37}
38
39impl<'de> Deserialize<'de> for Version {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let s = String::deserialize(deserializer)?;
45 Version::from_str(&s).map_err(serde::de::Error::custom)
46 }
47}
48
49impl Ord for Version {
50 fn cmp(&self, other: &Version) -> Ordering {
51 self.base
52 .cmp(&other.base)
53 .then(self.release_type.cmp(&other.release_type))
54 .then(self.revision.cmp(&other.revision))
55 }
56}
57
58impl PartialEq for Version {
59 fn eq(&self, other: &Self) -> bool {
60 self.base == other.base
61 && self.release_type == other.release_type
62 && self.revision == other.revision
63 }
64}
65
66impl AsRef<Version> for Version {
67 fn as_ref(&self) -> &Self {
68 self
69 }
70}
71
72impl AsMut<Version> for Version {
73 fn as_mut(&mut self) -> &mut Self {
74 self
75 }
76}
77
78impl FromStr for Version {
79 type Err = VersionError;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 match parse_version(s) {
83 Ok((_, version)) => Ok(version),
84 Err(err) => {
85 let error_msg = match err {
86 nom::Err::Error(e) | nom::Err::Failure(e) => {
87 format!("Parse error at: {}", e.input)
88 },
89 _ => "Unknown parsing error".to_string(),
90 };
91 Err(VersionError::ParsingFailed(error_msg))
92 }
93 }
94 }
95}
96
97impl TryFrom<&str> for Version {
98 type Error = <Version as FromStr>::Err;
99
100 fn try_from(value: &str) -> Result<Self, Self::Error> {
101 Version::from_str(value)
102 }
103}
104
105impl TryFrom<String> for Version {
106 type Error = <Version as FromStr>::Err;
107
108 fn try_from(value: String) -> Result<Self, Self::Error> {
109 Version::from_str(&value)
110 }
111}
112
113impl TryFrom<PathBuf> for Version {
114 type Error = VersionError;
115
116 fn try_from(path: PathBuf) -> Result<Self, VersionError> {
117 Version::from_path(path)
118 }
119}
120
121impl TryFrom<&Path> for Version {
122 type Error = VersionError;
123
124 fn try_from(path: &Path) -> Result<Self, VersionError> {
125 Version::from_path(path)
126 }
127}
128
129impl Version {
130 pub fn new(
131 major: u64,
132 minor: u64,
133 patch: u64,
134 release_type: ReleaseType,
135 revision: u64,
136 ) -> Version {
137 let base = semver::Version::new(major, minor, patch);
138 Version {
139 base,
140 release_type,
141 revision,
142 }
143 }
144
145 pub fn release_type(&self) -> ReleaseType {
146 self.release_type
147 }
148
149 pub fn major(&self) -> u64 {
150 self.base.major
151 }
152
153 pub fn minor(&self) -> u64 {
154 self.base.minor
155 }
156
157 pub fn patch(&self) -> u64 {
158 self.base.patch
159 }
160
161 pub fn revision(&self) -> u64 {
162 self.revision
163 }
164
165 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, VersionError> {
166 version_impl::read_version_from_path(path)
167 }
168
169 pub fn from_string_containing<S: AsRef<str>>(s: S) -> Result<Self, VersionError> {
170 let s = s.as_ref();
171
172 let re = Regex::new(r"\b\d+\.\d+\.\d+[fabp]\d+\b").unwrap();
173 for mat in re.find_iter(s){
174 if let Ok(version) = Version::from_str(mat.as_str()) {
175 return Ok(version);
176 }
177 }
178 Err(VersionError::ParsingFailed(format!("Could not find a valid Unity version in string: {}", s)))
179 }
180
181 pub fn base(&self) -> &semver::Version {
182 &self.base
183 }
184}
185
186#[derive(Eq, Debug, Clone, Hash, Display)]
187#[display("{} ({})", version, revision)]
188#[allow(dead_code)]
189pub struct CompleteVersion {
190 version: Version,
191 revision: RevisionHash,
192}
193
194impl PartialEq for CompleteVersion {
195 fn eq(&self, other: &Self) -> bool {
196 self.version == other.version
197 }
198}
199
200impl PartialOrd for CompleteVersion {
201 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
202 self.version.partial_cmp(&other.version)
203 }
204}
205
206fn parse_release_type(input: &str) -> IResult<&str, ReleaseType> {
207 context(
208 "release type",
209 map_res(alt((char('f'), char('b'), char('a'), char('p'))), |c| {
210 ReleaseType::try_from(c)
211 }),
212 ).parse(input)
213}
214
215fn parse_version(input: &str) -> IResult<&str, Version> {
216 context(
217 "version",
218 (
219 context("major version", map_res(digit1, |s: &str| s.parse::<u64>())),
220 char('.'),
221 context("minor version", map_res(digit1, |s: &str| s.parse::<u64>())),
222 char('.'),
223 context("patch version", map_res(digit1, |s: &str| s.parse::<u64>())),
224 parse_release_type,
225 context("revision", map_res(digit1, |s: &str| s.parse::<u64>())),
226 )
227 )
228 .map(|(major, _, minor, _, patch, release_type, revision)| {
229 let base = semver::Version::new(major, minor, patch);
230 Version {
231 base,
232 release_type,
233 revision,
234 }
235 })
236 .parse(input)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use proptest::prelude::*;
243
244 #[test]
245 fn parse_version_string_with_valid_input() {
246 let version_string = "1.2.3f4";
247 let version = Version::from_str(version_string);
248 assert!(version.is_ok(), "valid input returns a version")
249 }
250
251 #[test]
252 fn splits_version_string_into_components() {
253 let version_string = "11.2.3f4";
254 let version = Version::from_str(version_string).unwrap();
255
256 assert_eq!(version.base.major, 11, "parse correct major component");
257 assert_eq!(version.base.minor, 2, "parse correct minor component");
258 assert_eq!(version.base.patch, 3, "parse correct patch component");
259
260 assert_eq!(version.release_type, ReleaseType::Final);
261 assert_eq!(version.revision, 4, "parse correct revision component");
262 }
263
264 #[test]
265 fn extracts_version_from_text() {
266 let text = "Some text before 2023.1.4f5 and some after";
267 let result = Version::from_string_containing(text);
268 assert!(result.is_ok(), "Should successfully extract the version");
269
270 let version = result.unwrap();
271 assert_eq!(version.base.major, 2023);
272 assert_eq!(version.base.minor, 1);
273 assert_eq!(version.base.patch, 4);
274 assert_eq!(version.release_type, ReleaseType::Final);
275 assert_eq!(version.revision, 5);
276 }
277
278
279 #[test]
280 fn extracts_version_from_text_and_returns_first_complete_version() {
281 let text = "Some text 23 before 2023.1.4f5 and some after";
282 let result = Version::from_string_containing(text);
283 assert!(result.is_ok(), "Should successfully extract the version");
284
285 let version = result.unwrap();
286 assert_eq!(version.base.major, 2023);
287 assert_eq!(version.base.minor, 1);
288 assert_eq!(version.base.patch, 4);
289 assert_eq!(version.release_type, ReleaseType::Final);
290 assert_eq!(version.revision, 5);
291 }
292
293 proptest! {
294 #[test]
295 fn from_str_does_not_crash(s in "\\PC*") {
296 let _v = Version::from_str(&s);
297 }
298
299 #[test]
300 fn from_str_supports_all_valid_cases(
301 major in 0u64..=u64::MAX,
302 minor in 0u64..=u64::MAX,
303 patch in 0u64..=u64::MAX,
304 release_type in prop_oneof!["f", "p", "b", "a"],
305 revision in 0u64..=u64::MAX,
306 ) {
307 let version_string = format!("{}.{}.{}{}{}", major, minor, patch, release_type, revision);
308 let version = Version::from_str(&version_string).unwrap();
309
310 assert!(version.base.major == major, "parse correct major component");
311 assert!(version.base.minor == minor, "parse correct minor component");
312 assert!(version.base.patch == patch, "parse correct patch component");
313
314 assert_eq!(version.release_type, ReleaseType::from_str(&release_type).unwrap());
315 assert!(version.revision == revision, "parse correct revision component");
316 }
317 }
318}