unity_version/version/
mod.rs

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}