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, convert_error, VerboseError},
8 sequence::tuple,
9 IResult,
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(fmt = "{}{}{}", 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 verbose_error = match err {
87 nom::Err::Error(e) | nom::Err::Failure(e) => e,
88 _ => VerboseError {
89 errors: vec![(s, nom::error::VerboseErrorKind::Context("unknown error"))],
90 },
91 };
92 Err(VersionError::ParsingFailed(convert_error(s, verbose_error)))
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
173 let re = Regex::new(r"\b\d+\.\d+\.\d+[fabp]\d+\b").unwrap();
174 for mat in re.find_iter(s){
175 if let Ok(version) = Version::from_str(mat.as_str()) {
176 return Ok(version);
177 }
178 }
179 Err(VersionError::ParsingFailed(format!("Could not find a valid Unity version in string: {}", s)))
180 }
181}
182
183#[derive(Eq, Debug, Clone, Hash, Display)]
184#[display(fmt = "{} ({})", version, revision)]
185pub struct CompleteVersion {
186 version: Version,
187 revision: RevisionHash,
188}
189
190impl PartialEq for CompleteVersion {
191 fn eq(&self, other: &Self) -> bool {
192 self.version == other.version
193 }
194}
195
196impl PartialOrd for CompleteVersion {
197 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
198 self.version.partial_cmp(&other.version)
199 }
200}
201
202fn parse_release_type(input: &str) -> IResult<&str, ReleaseType, VerboseError<&str>> {
203 context(
204 "release type",
205 map_res(alt((char('f'), char('b'), char('a'), char('p'))), |c| {
206 ReleaseType::try_from(c)
207 }),
208 )(input)
209}
210
211fn parse_version(input: &str) -> IResult<&str, Version, VerboseError<&str>> {
212 context(
213 "version",
214 tuple((
215 context("major version", map_res(digit1, |s: &str| s.parse::<u64>())),
216 char('.'),
217 context("minor version", map_res(digit1, |s: &str| s.parse::<u64>())),
218 char('.'),
219 context("patch version", map_res(digit1, |s: &str| s.parse::<u64>())),
220 context("release type", parse_release_type),
221 context("revision", map_res(digit1, |s: &str| s.parse::<u64>())),
222 )),
223 )(input)
224 .map(
225 |(next_input, (major, _, minor, _, patch, release_type, revision))| {
226 let base = semver::Version::new(major, minor, patch);
227 (
228 next_input,
229 Version {
230 base,
231 release_type,
232 revision,
233 },
234 )
235 },
236 )
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}