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,
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}