semver2/
version.rs

1use std::fmt;
2use std::io::{self, BufRead, Cursor};
3use std::str::FromStr;
4
5use thiserror::Error;
6
7use crate::util::*;
8
9/// A single semver compliant version.
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct Version {
12    /// The major version.
13    pub major: u64,
14    /// The minor version.
15    pub minor: u64,
16    /// The patch version.
17    pub patch: u64,
18    /// The prerelease version.
19    pub prerelease: Vec<Identifier>,
20    /// The build version.
21    pub build: Vec<Identifier>,
22}
23
24impl fmt::Display for Version {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
27        if !self.prerelease.is_empty() {
28            write!(f, "-")?;
29            for (i, id) in self.prerelease.iter().enumerate() {
30                if i < self.prerelease.len() - 1 {
31                    write!(f, "{}.", id)?;
32                } else {
33                    write!(f, "{}", id)?;
34                }
35            }
36        }
37
38        if !self.build.is_empty() {
39            write!(f, "+")?;
40            for (i, id) in self.build.iter().enumerate() {
41                if i < self.build.len() - 1 {
42                    write!(f, "{}.", id)?;
43                } else {
44                    write!(f, "{}", id)?;
45                }
46            }
47        }
48        Ok(())
49    }
50}
51
52/// This the invidual parts in the string `beta.9`, separated by `.`.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Identifier {
55    Number(u64),
56    /// Only ASCII symbols.
57    String(String),
58}
59
60impl fmt::Display for Identifier {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            Identifier::Number(num) => num.fmt(f),
64            Identifier::String(st) => st.fmt(f),
65        }
66    }
67}
68macro_rules! id_from_number {
69    ($num:ty) => {
70        impl From<$num> for Identifier {
71            fn from(s: $num) -> Self {
72                Identifier::Number(s as u64)
73            }
74        }
75    };
76}
77
78id_from_number!(u8);
79id_from_number!(i8);
80id_from_number!(u16);
81id_from_number!(i16);
82id_from_number!(u32);
83id_from_number!(i32);
84id_from_number!(u64);
85id_from_number!(i64);
86
87impl Version {
88    /// Create a new version.
89    ///
90    /// ```rust
91    /// use semver2::Version;
92    ///
93    /// assert_eq!(&Version::new(2, 3, 0).to_string(), "2.3.0");
94    /// ```
95    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
96        Version {
97            major,
98            minor,
99            patch,
100            prerelease: Vec::new(),
101            build: Vec::new(),
102        }
103    }
104
105    /// Create a new version.
106    ///
107    /// ```rust
108    /// use semver2::Version;
109    ///
110    /// assert_eq!(
111    ///     &Version::new_prerelease(2, 3, 0, vec!["alpha".parse().unwrap()]).to_string(),
112    ///     "2.3.0-alpha"
113    /// );
114    /// ```
115    pub fn new_prerelease(major: u64, minor: u64, patch: u64, prerelease: Vec<Identifier>) -> Self {
116        Version {
117            major,
118            minor,
119            patch,
120            prerelease,
121            build: Vec::new(),
122        }
123    }
124
125    /// Create a new version.
126    ///
127    /// ```rust
128    /// use semver2::Version;
129    ///
130    /// assert_eq!(
131    ///     &Version::new_build(2, 3, 0, vec!["githash".parse().unwrap()]).to_string(),
132    ///     "2.3.0+githash"
133    /// );
134    /// ```
135    pub fn new_build(major: u64, minor: u64, patch: u64, build: Vec<Identifier>) -> Self {
136        Version {
137            major,
138            minor,
139            patch,
140            prerelease: Vec::new(),
141            build,
142        }
143    }
144}
145
146// range-set  ::= range ( logical-or range ) *
147// logical-or ::= ( ' ' ) * '||' ( ' ' ) *
148// range      ::= hyphen | simple ( ' ' simple ) * | ''
149// hyphen     ::= partial ' - ' partial
150// simple     ::= primitive | partial | tilde | caret
151// primitive  ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
152// partial    ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
153// xr         ::= 'x' | 'X' | '*' | nr
154// nr         ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
155// tilde      ::= '~' partial
156// caret      ::= '^' partial
157// qualifier  ::= ( '-' pre )? ( '+' build )?
158// pre        ::= parts
159// build      ::= parts
160// parts      ::= part ( '.' part ) *
161// part       ::= nr | [-0-9A-Za-z]+
162
163#[derive(Debug, Error)]
164pub enum ParseError {
165    #[error("Invalid input: {:?}", found)]
166    Invalid { found: Option<char> },
167    #[error("Invalid numeric range")]
168    InvalidNumericRange,
169    #[error("Unexpected end of input")]
170    UnexpectedEof,
171    #[error("IO")]
172    Io(#[from] io::Error),
173}
174
175/// Parses any sequence of digits.
176fn parse_numeric_range_loose<R: BufRead>(s: R) -> Result<u64, ParseError> {
177    let raw = take_string_while(s, |b| b.is_ascii_digit())?;
178    raw.parse().map_err(|_| ParseError::InvalidNumericRange)
179}
180
181fn parse_part<R: BufRead>(mut s: R) -> Result<Identifier, ParseError> {
182    let part = take_string_while(&mut s, |b| b.is_ascii_alphanumeric())?;
183    match part.parse::<u64>() {
184        Ok(number) => Ok(number.into()),
185        _ => Ok(Identifier::String(part)),
186    }
187}
188
189fn parse_parts<R: BufRead>(mut s: R) -> Result<Vec<Identifier>, ParseError> {
190    let mut res = Vec::new();
191    loop {
192        if is_eof(&mut s) {
193            break;
194        }
195
196        res.push(parse_part(&mut s)?);
197
198        let next = peek1(&mut s);
199        if next == Some(b'.') {
200            s.consume(1)
201        } else {
202            break;
203        }
204    }
205
206    if res.is_empty() {
207        // return Err(ParseError::UnexpectedEof);
208    }
209
210    Ok(res)
211}
212
213impl FromStr for Identifier {
214    type Err = ParseError;
215
216    fn from_str(s: &str) -> Result<Self, Self::Err> {
217        let mut s = Cursor::new(s);
218
219        parse_part(&mut s)
220    }
221}
222
223impl FromStr for Version {
224    type Err = ParseError;
225
226    fn from_str(s: &str) -> Result<Self, Self::Err> {
227        let mut s = Cursor::new(s);
228
229        let mut version = Version::default();
230
231        // Major
232        version.major = parse_numeric_range_loose(&mut s)?;
233        if is_eof(&mut s) {
234            return Ok(version);
235        }
236
237        // .
238        let next = take1(&mut s).map(|s| s as char);
239        if next != Some('.') {
240            return Err(ParseError::Invalid { found: next });
241        }
242
243        // Minor (optional)
244        version.minor = parse_numeric_range_loose(&mut s)?;
245        if is_eof(&mut s) {
246            return Ok(version);
247        }
248
249        // .
250        let next = take1(&mut s).map(|s| s as char);
251        if next != Some('.') {
252            return Err(ParseError::Invalid { found: next });
253        }
254
255        // Patch (optional)
256        version.patch = parse_numeric_range_loose(&mut s)?;
257        if is_eof(&mut s) {
258            return Ok(version);
259        }
260
261        let mut next = peek1(&mut s).map(|s| s as char);
262        if next == Some('.') {
263            s.consume(1);
264            return Err(ParseError::Invalid { found: next });
265        }
266
267        if next == Some('+') || next == Some('-') {
268            s.consume(1);
269        }
270
271        // prerelease (optional)
272        // interpret 1.2.3foo as 1.2.3-foo
273        if next.is_some() && next != Some('+') {
274            version.prerelease = parse_parts(&mut s)?;
275            if is_eof(&mut s) {
276                return Ok(version);
277            }
278
279            // read the next part, as we consumed our next.
280            next = take1(&mut s).map(|s| s as char);
281        }
282
283        // build (optional)
284        if next == Some('+') {
285            version.build = parse_parts(&mut s)?;
286            if is_eof(&mut s) {
287                return Ok(version);
288            }
289        }
290        Err(ParseError::Invalid { found: next })
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn simple_parsing() {
300        assert_eq!("1.2.3".parse::<Version>().unwrap(), Version::new(1, 2, 3));
301        assert_eq!(
302            "1.2.3-alpha.3".parse::<Version>().unwrap(),
303            Version::new_prerelease(1, 2, 3, vec!["alpha".parse().unwrap(), 3.into()])
304        );
305        assert_eq!(
306            "1.2.3+alpha.3".parse::<Version>().unwrap(),
307            Version::new_build(1, 2, 3, vec!["alpha".parse().unwrap(), 3.into()])
308        );
309
310        assert_eq!(
311            "1.2.3-beta.9+acd.v3.2".parse::<Version>().unwrap(),
312            Version {
313                major: 1,
314                minor: 2,
315                patch: 3,
316                prerelease: vec!["beta".parse().unwrap(), 9.into()],
317                build: vec!["acd".parse().unwrap(), "v3".parse().unwrap(), 2.into()],
318            }
319        );
320    }
321
322    #[test]
323    fn display() {
324        assert_eq!(&Version::new(1, 2, 3).to_string(), "1.2.3");
325        assert_eq!(
326            &Version::new_prerelease(1, 2, 3, vec![0.into(), "alpha".parse().unwrap()]).to_string(),
327            "1.2.3-0.alpha"
328        );
329        assert_eq!(
330            &Version::new_build(1, 2, 3, vec![0.into(), "alpha".parse().unwrap()]).to_string(),
331            "1.2.3+0.alpha"
332        );
333        assert_eq!(
334            &Version {
335                major: 1,
336                minor: 2,
337                patch: 3,
338                prerelease: vec![0.into(), "alpha".parse().unwrap()],
339                build: vec!["bla".parse().unwrap(), 9.into()],
340            }
341            .to_string(),
342            "1.2.3-0.alpha+bla.9"
343        );
344    }
345
346    #[test]
347    fn loose_versions() {
348        assert_eq!(
349            "001.20.0301".parse::<Version>().unwrap(),
350            Version::new(1, 20, 301)
351        );
352
353        assert_eq!(
354            "1.2.3-beta.01".parse::<Version>().unwrap(),
355            Version::new_prerelease(1, 2, 3, vec!["beta".parse().unwrap(), 1.into()])
356        );
357
358        assert_eq!(
359            "1.2.3foo".parse::<Version>().unwrap(),
360            Version::new_prerelease(1, 2, 3, vec!["foo".parse().unwrap()])
361        );
362
363        assert_eq!(
364            "1.2.3foo.8".parse::<Version>().unwrap(),
365            Version::new_prerelease(1, 2, 3, vec!["foo".parse().unwrap(), 8.into()])
366        );
367    }
368
369    #[test]
370    fn invalid_versions() {
371        assert!("1.2.3.8".parse::<Version>().is_err());
372        assert!("HELLO WORLD".parse::<Version>().is_err());
373        assert!("foo.bar.baz".parse::<Version>().is_err());
374    }
375}