versions/
version.rs

1//! Types and logic for handling general [`Version`]s.
2
3use crate::{Chunk, Chunks, Error, MChunk, Mess, Release, Sep};
4use nom::character::complete::char;
5use nom::combinator::opt;
6use nom::{IResult, Parser};
7use std::cmp::Ordering;
8use std::cmp::Ordering::{Equal, Greater, Less};
9use std::hash::Hash;
10use std::str::FromStr;
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15/// A version number with decent structure and comparison logic.
16///
17/// This is a *descriptive* scheme, meaning that it encapsulates the most
18/// common, unconscious patterns that developers use when assigning version
19/// numbers to their software. If not [`crate::SemVer`], most version numbers
20/// found in the wild will parse as a `Version`. These generally conform to the
21/// `x.x.x-x` pattern, and may optionally have an *epoch*.
22///
23/// # Epochs
24///
25/// Epochs are prefixes marked by a colon, like in `1:2.3.4`. When comparing two
26/// `Version` values, epochs take precedent. So `2:1.0.0 > 1:9.9.9`. If one of
27/// the given `Version`s has no epoch, its epoch is assumed to be `0`.
28///
29/// # Examples
30///
31/// ```
32/// use versions::{SemVer, Version};
33///
34/// // None of these are SemVer, but can still be parsed and compared.
35/// let vers = vec!["0.25-2", "8.u51-1", "20150826-1", "1:2.3.4"];
36///
37/// for v in vers {
38///     assert!(SemVer::new(v).is_none());
39///     assert!(Version::new(v).is_some());
40/// }
41/// ```
42
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
45pub struct Version {
46    /// An optional prefix that marks that some paradigm shift in versioning has
47    /// occurred between releases of some software.
48    pub epoch: Option<u32>,
49    /// The main sections of the `Version`. Unlike [`crate::SemVer`], these
50    /// sections are allowed to contain letters.
51    pub chunks: Chunks,
52    /// This either indicates a prerelease like [`crate::SemVer`], or a
53    /// "release" revision for software packages. In the latter case, a version
54    /// like `1.2.3-2` implies that the software itself hasn't changed, but that
55    /// this is the second bundling/release (etc.) of that particular package.
56    pub release: Option<Release>,
57    /// Some extra metadata that doesn't factor into comparison.
58    pub meta: Option<String>,
59}
60
61impl Version {
62    /// Parse a `Version` from some input.
63    pub fn new<S>(s: S) -> Option<Version>
64    where
65        S: AsRef<str>,
66    {
67        match Version::parse(s.as_ref()) {
68            Ok(("", v)) => Some(v),
69            _ => None,
70        }
71    }
72
73    /// Try to extract a position from the `Version` as a nice integer, as if it
74    /// were a [`crate::SemVer`].
75    ///
76    /// ```
77    /// use versions::Version;
78    ///
79    /// let mess = Version::new("1:2.a.4.5.6.7-r1").unwrap();
80    /// assert_eq!(Some(2), mess.nth(0));
81    /// assert_eq!(None, mess.nth(1));
82    /// assert_eq!(Some(4), mess.nth(2));
83    /// ```
84    pub fn nth(&self, n: usize) -> Option<u32> {
85        self.chunks.0.get(n).and_then(Chunk::single_digit)
86    }
87
88    /// Like `nth`, but pulls a number even if it was followed by letters.
89    pub fn nth_lenient(&self, n: usize) -> Option<u32> {
90        self.chunks.0.get(n).and_then(Chunk::single_digit_lenient)
91    }
92
93    /// A lossless conversion from `Version` to [`Mess`].
94    ///
95    /// ```
96    /// use versions::Version;
97    ///
98    /// let orig = "1:1.2.3-r1";
99    /// let mess = Version::new(orig).unwrap().to_mess();
100    ///
101    /// assert_eq!(orig, format!("{}", mess));
102    /// ```
103    pub fn to_mess(&self) -> Mess {
104        match self.epoch {
105            None => self.to_mess_continued(),
106            Some(e) => {
107                let chunks = vec![MChunk::Digits(e, e.to_string())];
108                let next = Some((Sep::Colon, Box::new(self.to_mess_continued())));
109                Mess { chunks, next }
110            }
111        }
112    }
113
114    /// Convert to a `Mess` without considering the epoch.
115    fn to_mess_continued(&self) -> Mess {
116        let chunks = self.chunks.0.iter().map(|c| c.mchunk()).collect();
117        let next = self.release.as_ref().map(|cs| {
118            let chunks = cs.0.iter().map(|c| c.mchunk()).collect();
119            (Sep::Hyphen, Box::new(Mess { chunks, next: None }))
120        });
121        Mess { chunks, next }
122    }
123
124    /// If we're lucky, we can pull specific numbers out of both inputs and
125    /// accomplish the comparison without extra allocations.
126    pub(crate) fn cmp_mess(&self, other: &Mess) -> Ordering {
127        match self.epoch {
128            Some(e) if e > 0 && other.chunks.len() == 1 => match &other.next {
129                // A near-nonsense case where a `Mess` is comprised of a single
130                // digit and nothing else. In this case its epoch would be
131                // considered 0.
132                None => Greater,
133                Some((Sep::Colon, m)) => match other.nth(0) {
134                    // The Mess's epoch is a letter, etc.
135                    None => Greater,
136                    Some(me) => match e.cmp(&me) {
137                        Equal => Version::cmp_mess_continued(self, m),
138                        ord => ord,
139                    },
140                },
141                // Similar nonsense, where the Mess had a single *something*
142                // before some non-colon separator. We then consider the epoch
143                // to be 0.
144                Some(_) => Greater,
145            },
146            // The `Version` has an epoch but the `Mess` doesn't. Or if it does,
147            // it's malformed.
148            Some(e) if e > 0 => Greater,
149            _ => Version::cmp_mess_continued(self, other),
150        }
151    }
152
153    /// It's assumed the epoch check has already been done, and we're comparing
154    /// the main parts of each version now.
155    fn cmp_mess_continued(&self, other: &Mess) -> Ordering {
156        (0..)
157            .find_map(
158                |n| match self.nth(n).and_then(|x| other.nth(n).map(|y| x.cmp(&y))) {
159                    // Sane values can't be extracted from one or both of the
160                    // arguments.
161                    None => Some(self.to_mess().cmp(other)),
162                    Some(Greater) => Some(Greater),
163                    Some(Less) => Some(Less),
164                    // Continue to the next position.
165                    Some(Equal) => None,
166                },
167            )
168            .unwrap_or_else(|| self.to_mess().cmp(other))
169    }
170
171    /// The raw `nom` parser for [`Version`]. Feel free to use this in
172    /// combination with other general `nom` parsers.
173    pub fn parse(i: &str) -> IResult<&str, Version> {
174        let (i, epoch) = opt(Version::epoch).parse(i)?;
175        let (i, chunks) = Chunks::parse(i)?;
176        let (i, release) = opt(Release::parse).parse(i)?;
177        let (i, meta) = opt(crate::parsers::meta).parse(i)?;
178
179        let v = Version {
180            epoch,
181            chunks,
182            meta,
183            release,
184        };
185
186        Ok((i, v))
187    }
188
189    fn epoch(i: &str) -> IResult<&str, u32> {
190        let (i, epoch) = crate::parsers::unsigned(i)?;
191        let (i, _) = char(':')(i)?;
192
193        Ok((i, epoch))
194    }
195
196    pub(crate) fn matches_tilde(&self, other: &Version) -> bool {
197        if self.chunks.0.len() != other.chunks.0.len() {
198            false
199        } else {
200            // Compare all but the final chunk.
201            let inits_equal = self
202                .chunks
203                .0
204                .iter()
205                .rev()
206                .skip(1)
207                .rev()
208                .zip(other.chunks.0.iter().rev().skip(1).rev())
209                .all(|(a, b)| a == b);
210
211            let last_good = match (self.chunks.0.last(), other.chunks.0.last()) {
212                // TODO: Do our best with strings. Right now, the alpha patch version can be "less" than the
213                // first one and this will still be true
214                (Some(Chunk::Alphanum(_)), Some(Chunk::Alphanum(_))) => true,
215                (Some(Chunk::Numeric(n1)), Some(Chunk::Numeric(n2))) => n2 >= n1,
216                _ => false,
217            };
218
219            inits_equal && last_good
220        }
221    }
222
223    // TODO 2024-01-11 Refactor this to be more functional-style.
224    pub(crate) fn matches_caret(&self, other: &Version) -> bool {
225        let mut got_first_nonzero = false;
226
227        for (v1_chunk, v2_chunk) in self.chunks.0.iter().zip(other.chunks.0.iter()) {
228            if !got_first_nonzero {
229                if !v1_chunk.single_digit().is_some_and(|n| n == 0) {
230                    got_first_nonzero = true;
231
232                    if v1_chunk != v2_chunk {
233                        return false;
234                    }
235                }
236            } else if v2_chunk.cmp_lenient(v1_chunk).is_lt() {
237                return false;
238            }
239        }
240
241        true
242    }
243}
244
245impl PartialOrd for Version {
246    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
247        Some(self.cmp(other))
248    }
249}
250
251impl Ord for Version {
252    /// If two epochs are equal, we need to compare their actual version
253    /// numbers. Otherwise, the comparison of the epochs is the only thing that
254    /// matters.
255    fn cmp(&self, other: &Self) -> Ordering {
256        let ae = self.epoch.unwrap_or(0);
257        let be = other.epoch.unwrap_or(0);
258        match ae.cmp(&be) {
259            Equal => match self.chunks.cmp(&other.chunks) {
260                Equal => self.release.cmp(&other.release),
261                ord => ord,
262            },
263            ord => ord,
264        }
265    }
266}
267
268impl std::fmt::Display for Version {
269    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
270        if let Some(e) = self.epoch {
271            write!(f, "{}:", e)?;
272        }
273
274        write!(f, "{}", self.chunks)?;
275
276        if let Some(r) = &self.release {
277            write!(f, "-{}", r)?;
278        }
279
280        if let Some(m) = &self.meta {
281            write!(f, "+{}", m)?;
282        }
283
284        Ok(())
285    }
286}
287
288impl FromStr for Version {
289    type Err = Error;
290
291    fn from_str(s: &str) -> Result<Self, Self::Err> {
292        Version::new(s).ok_or_else(|| Error::IllegalVersion(s.to_string()))
293    }
294}
295
296impl TryFrom<&str> for Version {
297    type Error = Error;
298
299    /// ```
300    /// use versions::Version;
301    ///
302    /// let orig = "1.2.3.4";
303    /// let prsd: Version = orig.try_into().unwrap();
304    /// assert_eq!(orig, prsd.to_string());
305    /// ```
306    fn try_from(value: &str) -> Result<Self, Self::Error> {
307        Version::from_str(value)
308    }
309}