├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── tests └── semver.rs ├── Cargo.toml ├── LICENSE ├── src ├── parsers.rs ├── versioning.rs ├── require.rs ├── mess.rs ├── version.rs ├── semver.rs └── lib.rs ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | deps.png 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | 19 | - name: Cache Dependencies 20 | uses: Swatinem/rust-cache@v2 21 | 22 | - name: Run tests 23 | run: cargo test 24 | -------------------------------------------------------------------------------- /tests/semver.rs: -------------------------------------------------------------------------------- 1 | //! Compare parse results with the `semver` crate. 2 | 3 | #[test] 4 | fn zeroes() { 5 | let vs = ["1.2.2-0a", "1.2.2-00a"]; 6 | for v in vs { 7 | semver_parse_both(v); 8 | semver_parser_parse_both(v); 9 | } 10 | } 11 | 12 | fn semver_parse_both(v: &str) { 13 | let sv = semver::Version::parse(v).unwrap(); 14 | assert_eq!(v, sv.to_string()); 15 | } 16 | 17 | fn semver_parser_parse_both(v: &str) { 18 | let sv = semver_parser::version::parse(v).unwrap(); 19 | assert_eq!(v, sv.to_string()); 20 | } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "versions" 3 | version = "7.0.0" 4 | authors = ["Colin Woodbury "] 5 | edition = "2024" 6 | description = "A library for parsing and comparing software version numbers." 7 | homepage = "https://github.com/fosskers/rs-versions" 8 | repository = "https://github.com/fosskers/rs-versions" 9 | readme = "README.md" 10 | license = "MIT" 11 | keywords = ["version", "compare", "semantic"] 12 | categories = ["parser-implementations"] 13 | rust-version = "1.85.0" 14 | 15 | [dependencies] 16 | itertools = "0.14" 17 | nom = "8.0" 18 | serde = { version = "1.0", features = ["derive"], optional = true } 19 | 20 | [dev-dependencies] 21 | semver = "1.0" 22 | semver-parser = "0.10" 23 | serde_json = "1.0" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Colin Woodbury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/parsers.rs: -------------------------------------------------------------------------------- 1 | //! Reusable parsers for the `versions` library. 2 | 3 | use nom::branch::alt; 4 | use nom::bytes::complete::{tag, take_while1}; 5 | use nom::character::complete::{char, digit1}; 6 | use nom::combinator::{map, map_res}; 7 | use nom::{IResult, Parser}; 8 | 9 | /// Parse an unsigned integer. 10 | /// 11 | /// Should yield either a zero on its own, or some other multi-digit number. 12 | pub(crate) fn unsigned(i: &str) -> IResult<&str, u32> { 13 | map_res(alt((tag("0"), digit1)), |s: &str| s.parse::()).parse(i) 14 | } 15 | 16 | #[test] 17 | fn unsigned_test() { 18 | assert!(unsigned("0").is_ok()); 19 | assert!(unsigned("123").is_ok()); 20 | 21 | match unsigned("06") { 22 | Ok(("6", 0)) => {} 23 | Ok(_) => panic!("Parsed 06, but gave wrong output"), 24 | Err(_) => panic!("Couldn't parse 06"), 25 | } 26 | } 27 | 28 | /// Some alphanumeric combination, possibly punctuated by `-` characters. 29 | pub(crate) fn hyphenated_alphanums(i: &str) -> IResult<&str, &str> { 30 | take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-')(i) 31 | } 32 | 33 | /// Some alphanumeric combination. 34 | pub(crate) fn alphanums(i: &str) -> IResult<&str, &str> { 35 | take_while1(|c: char| c.is_ascii_alphanumeric())(i) 36 | } 37 | 38 | /// Parse metadata. As of SemVer 2.0, this can contain alphanumeric characters 39 | /// as well as hyphens. 40 | pub fn meta(i: &str) -> IResult<&str, String> { 41 | let (i, _) = char('+')(i)?; 42 | map( 43 | take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '.'), 44 | |s: &str| s.to_owned(), 45 | ) 46 | .parse(i) 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Versions 2 | 3 | [![Tests](https://github.com/fosskers/rs-versions/workflows/Tests/badge.svg)](https://github.com/fosskers/rs-versions/actions) 4 | [![](https://img.shields.io/crates/v/versions.svg)](https://crates.io/crates/versions) 5 | 6 | 7 | 8 | A library for parsing and comparing software version numbers. 9 | 10 | We like to give version numbers to our software in a myriad of different 11 | ways. Some ways follow strict guidelines for incrementing and comparison. 12 | Some follow conventional wisdom and are generally self-consistent. Some are 13 | just plain asinine. This library provides a means of parsing and comparing 14 | *any* style of versioning, be it a nice Semantic Version like this: 15 | 16 | > 1.2.3-r1 17 | 18 | ...or a monstrosity like this: 19 | 20 | > 2:10.2+0.0093r3+1-1 21 | 22 | ## Usage 23 | 24 | If you're parsing several version numbers that don't follow a single scheme 25 | (say, as in system packages), then use the [`Versioning`] type and its 26 | parser [`Versioning::new`]. Otherwise, each main type - [`SemVer`], 27 | [`Version`], or [`Mess`] - can be parsed on their own via the `new` method 28 | (e.g. [`SemVer::new`]). 29 | 30 | ## Examples 31 | 32 | ```rust 33 | use versions::Versioning; 34 | 35 | let good = Versioning::new("1.6.0").unwrap(); 36 | let evil = Versioning::new("1.6.0a+2014+m872b87e73dfb-1").unwrap(); 37 | 38 | assert!(good.is_ideal()); // It parsed as a `SemVer`. 39 | assert!(evil.is_complex()); // It parsed as a `Mess`. 40 | assert!(good > evil); // We can compare them anyway! 41 | ``` 42 | 43 | ## Version Constraints 44 | 45 | Tools like `cargo` also allow version constraints to be prepended to a 46 | version number, like in `^1.2.3`. 47 | 48 | ```rust 49 | use versions::{Requirement, Versioning}; 50 | 51 | let req = Requirement::new("^1.2.3").unwrap(); 52 | let ver = Versioning::new("1.2.4").unwrap(); 53 | assert!(req.matches(&ver)); 54 | ``` 55 | 56 | In this case, the incoming version `1.2.4` satisfies the "caret" constraint, 57 | which demands anything greater than or equal to `1.2.3`. 58 | 59 | See the [`Requirement`] type for more details. 60 | 61 | ## Usage with `nom` 62 | 63 | In constructing your own [`nom`](https://lib.rs/nom) parsers, you can 64 | integrate the parsers used for the types in this crate via 65 | [`Versioning::parse`], [`SemVer::parse`], [`Version::parse`], and 66 | [`Mess::parse`]. 67 | 68 | ## Features 69 | 70 | You can enable [`Serde`](https://serde.rs/) support for serialization and 71 | deserialization with the `serde` feature. 72 | 73 | By default the version structs are serialized/deserialized as-is. If instead 74 | you'd like to deserialize directly from a raw version string like `1.2.3`, 75 | see [`Versioning::deserialize_pretty`]. 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `versions` Changelog 2 | 3 | ## 7.0.0 (2025-02-24) 4 | 5 | #### Changed 6 | 7 | - Support for Rust 2024 and other dependency bumps, `nom` in particular. 8 | 9 | ## 6.3.2 (2024-08-09) 10 | 11 | #### Fixed 12 | 13 | - An edge-case in Version/Mess comparisons. [#25][i25] 14 | 15 | [i25]: https://github.com/fosskers/rs-versions/issues/25 16 | 17 | ## 6.3.1 (2024-08-05) 18 | 19 | #### Fixed 20 | 21 | - Parsing of `Mess` on a single digit will correctly yield `MChunk::Digits`. 22 | - Improved comparison of `Version`s whose main chunks lead with a letter, e.g. `r23`. 23 | 24 | ## 6.3.0 (2024-06-15) 25 | 26 | #### Changed 27 | 28 | - Relaxed argument to `new` functions to accept `S: AsRef`. 29 | 30 | ## 6.2.0 (2024-03-18) 31 | 32 | #### Added 33 | 34 | - `Requirement::serialize` for pretty-encoding of version "requirements strings". 35 | 36 | ## 6.1.0 (2024-01-12) 37 | 38 | #### Added 39 | 40 | - The `Requirement` type for testing version constraints. 41 | - `Versioning::parse` for usage with `nom`. 42 | - `Versioning::deserialize_pretty` for deserializing directly from raw version strings. 43 | 44 | ## 6.0.0 (2023-12-06) 45 | 46 | While technically a breaking change, most users should not notice. 47 | 48 | #### Changed 49 | 50 | - The `Err` / `Error` associated types for automatic conversion of strings into 51 | proper version types have been changed from `()` to a proper `Error` enum. 52 | 53 | ## 5.0.1 (2023-08-13) 54 | 55 | #### Changed 56 | 57 | - Bumped `itertools` dependency. 58 | 59 | ## 5.0.0 (2023-05-09) 60 | 61 | This introduces a very small, technically breaking change to the API involving a 62 | single type. If you're just doing basic parsing and comparisons and not actually 63 | inspecting the types themselves, you shouldn't notice a difference. 64 | 65 | #### Changed 66 | 67 | - Versions with `~` in their metadata will now parse as a `Mess`. Example: `12.0.0-3ubuntu1~20.04.5` 68 | 69 | ## 4.1.0 (2022-04-21) 70 | 71 | #### Added 72 | 73 | - `FromStr` and `TryFrom` instances for each type. 74 | 75 | ## 4.0.0 (2022-01-07) 76 | 77 | #### Added 78 | 79 | - The `Release` type. 80 | 81 | #### Changed 82 | 83 | - `SemVer` and `Version` have had their prerel field changed to `Release`. 84 | - The `Chunk` type has changed from a struct to an enum. 85 | 86 | #### Removed 87 | 88 | - The `Unit` type. 89 | 90 | #### Fixed 91 | 92 | - A bug involving zeroes in `SemVer` prereleases. 93 | - A bug involving the `Display` instance for `Version`. 94 | 95 | ## 3.0.3 (2021-08-23) 96 | 97 | #### Changed 98 | 99 | - Upgraded to `nom-7.0`. 100 | 101 | ## 3.0.2 (2021-05-27) 102 | 103 | #### Added 104 | 105 | - A proper LICENSE file. 106 | 107 | #### Changed 108 | 109 | - The `Hash` instance of `SemVer` is now hand-written to uphold the Law that: 110 | 111 | ``` 112 | k1 == k2 -> hash(k1) == hash(k2) 113 | ``` 114 | 115 | ## 3.0.1 (2021-05-09) 116 | 117 | #### Changed 118 | 119 | - Certain parsers are now faster and use less memory. 120 | 121 | ## 3.0.0 (2021-04-16) 122 | 123 | This release brings `versions` in line with version `2.0.0` of the SemVer spec. 124 | The main addition to the spec is the allowance of hyphens in both the prerelease 125 | and metadata sections. As such, **certain versions like 1.2.3+1-1 which 126 | previously would not parse as SemVer now do.** 127 | 128 | To accomodate this and other small spec updates, the `SemVer` and `Version` 129 | types have received breaking changes here. 130 | 131 | #### Added 132 | 133 | - [`Serde`](https://serde.rs/) support through the optional `serde` feature. 134 | - `Versioning::nth` to pick out certain fields of a generically parsed version. 135 | - `Default` is now derived on `Versioning`, `SemVer`, `Version`, `Mess` and 136 | `Chunks` so that it's possible to initialize as a struct's field. 137 | 138 | #### Changed 139 | 140 | - **Breaking:** `SemVer::meta` and `Version::meta` no longer parse as `Chunks` 141 | but as vanilla `String`s. 142 | - **Breaking:** As a semantic change, `Version`s now expect metadata to come 143 | **after** any prerelease, just as with `SemVer`. `Version` is now thus fairly 144 | similar to `SemVer`, except that is allows letters in more permissive 145 | positions. 146 | 147 | #### Fixed 148 | 149 | - Two small bugs involving `SemVer`/`Version` comparisons. 150 | 151 | ## 2.1.0 (2021-03-22) 152 | 153 | #### Added 154 | 155 | - `SemVer::parse`, `Version::parse`, and `Mess::parse` have been made `pub` so 156 | that these parsers can be integrated into other general `nom` parsers. 157 | 158 | ## 2.0.2 (2021-01-23) 159 | 160 | #### Changed 161 | 162 | - Updated to `itertools-0.10`. 163 | 164 | ## 2.0.1 (2020-11-19) 165 | 166 | #### Changed 167 | 168 | - Updated to `nom-6.0`. 169 | - Utilize new type linking in docstrings, thanks to the latest stable Rust. 170 | 171 | ## 2.0.0 (2020-10-21) 172 | 173 | #### Changed 174 | 175 | - **Breaking:** `Mess::chunk` renamed to `Mess::chunks` to match `Version`. 176 | - **Breaking:** `Mess` now stores smarter `Vec` instead of `String`s. 177 | - `SemVer::to_version` is no longer a lossy conversion, due to the new field in `Version` (see below). 178 | - Most `Display` instances are more efficient. 179 | 180 | #### Added 181 | 182 | - **Breaking:** The `meta: Option` field for `Version`. 183 | - The `MChunk` type which allows `Mess` to do smarter comparisons. 184 | 185 | #### Fixed 186 | 187 | - A number of comparison edge cases. 188 | 189 | ## 1.0.1 (2020-06-15) 190 | 191 | #### Changed 192 | 193 | - Performance and readability improvements. 194 | 195 | ## 1.0.0 (2020-06-03) 196 | 197 | This is the initial release of the library. 198 | 199 | #### Added 200 | 201 | - All types, parsers, and tests. 202 | -------------------------------------------------------------------------------- /src/versioning.rs: -------------------------------------------------------------------------------- 1 | //! Types and logic for handling combinde [`Versioning`]s. 2 | 3 | use std::cmp::Ordering; 4 | 5 | use crate::{Error, Mess, SemVer, Version}; 6 | use nom::IResult; 7 | use nom::combinator::map; 8 | use nom::{Parser, branch::alt}; 9 | use std::str::FromStr; 10 | 11 | #[cfg(feature = "serde")] 12 | use serde::{Deserialize, Deserializer, Serialize, de::Error as _}; 13 | 14 | /// A top-level Versioning type which acts as a wrapper for the more specific 15 | /// types. 16 | /// 17 | /// # Examples 18 | /// 19 | /// ``` 20 | /// use versions::Versioning; 21 | /// 22 | /// let a = Versioning::new("1.2.3-1").unwrap(); // SemVer. 23 | /// let b = Versioning::new("1.2.3r1").unwrap(); // Not SemVer but good enough. 24 | /// let c = Versioning::new("000.007-1").unwrap(); // Garbage. 25 | /// 26 | /// assert!(a.is_ideal()); 27 | /// assert!(b.is_general()); 28 | /// assert!(c.is_complex()); 29 | /// ``` 30 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 31 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 32 | pub enum Versioning { 33 | /// Follows good parsing and comparison rules. 34 | Ideal(SemVer), 35 | /// A little more permissive than [`SemVer`]. 36 | General(Version), 37 | /// Hope that you need not venture here. 38 | Complex(Mess), 39 | } 40 | 41 | impl Versioning { 42 | /// Create a `Versioning` by attempting to parse the input first as 43 | /// [`SemVer`], then as a [`Version`], and finally as a [`Mess`]. 44 | pub fn new(s: S) -> Option 45 | where 46 | S: AsRef, 47 | { 48 | let str = s.as_ref(); 49 | 50 | SemVer::new(str) 51 | .map(Versioning::Ideal) 52 | .or_else(|| Version::new(str).map(Versioning::General)) 53 | .or_else(|| Mess::new(str).map(Versioning::Complex)) 54 | } 55 | 56 | /// The raw `nom` parser for [`Versioning`]. Feel free to use this in 57 | /// combination with other general `nom` parsers. 58 | pub fn parse(i: &str) -> IResult<&str, Versioning> { 59 | alt(( 60 | map(SemVer::parse, Versioning::Ideal), 61 | map(Version::parse, Versioning::General), 62 | map(Mess::parse, Versioning::Complex), 63 | )) 64 | .parse(i) 65 | } 66 | 67 | /// A short-hand for detecting an inner [`SemVer`]. 68 | pub fn is_ideal(&self) -> bool { 69 | matches!(self, Versioning::Ideal(_)) 70 | } 71 | 72 | /// A short-hand for detecting an inner [`Version`]. 73 | pub fn is_general(&self) -> bool { 74 | matches!(self, Versioning::General(_)) 75 | } 76 | 77 | /// A short-hand for detecting an inner [`Mess`]. 78 | pub fn is_complex(&self) -> bool { 79 | matches!(self, Versioning::Complex(_)) 80 | } 81 | 82 | /// Try to extract a position from the `Versioning` as a nice integer, as if it 83 | /// were a [`SemVer`]. 84 | /// 85 | /// ``` 86 | /// use versions::Versioning; 87 | /// 88 | /// let semver = Versioning::new("1.2.3-r1+git123").unwrap(); 89 | /// assert!(semver.is_ideal()); 90 | /// assert_eq!(Some(1), semver.nth(0)); 91 | /// assert_eq!(Some(2), semver.nth(1)); 92 | /// assert_eq!(Some(3), semver.nth(2)); 93 | /// 94 | /// let version = Versioning::new("1:2.a.4.5.6.7-r1").unwrap(); 95 | /// assert!(version.is_general()); 96 | /// assert_eq!(Some(2), version.nth(0)); 97 | /// assert_eq!(None, version.nth(1)); 98 | /// assert_eq!(Some(4), version.nth(2)); 99 | /// 100 | /// let mess = Versioning::new("1.6a.0+2014+m872b87e73dfb-1").unwrap(); 101 | /// assert!(mess.is_complex()); 102 | /// assert_eq!(Some(1), mess.nth(0)); 103 | /// assert_eq!(None, mess.nth(1)); 104 | /// assert_eq!(Some(0), mess.nth(2)); 105 | /// ``` 106 | pub fn nth(&self, n: usize) -> Option { 107 | match self { 108 | Versioning::Ideal(s) if n == 0 => Some(s.major), 109 | Versioning::Ideal(s) if n == 1 => Some(s.minor), 110 | Versioning::Ideal(s) if n == 2 => Some(s.patch), 111 | Versioning::Ideal(_) => None, 112 | Versioning::General(v) => v.nth(n), 113 | Versioning::Complex(m) => m.nth(n), 114 | } 115 | } 116 | 117 | pub(crate) fn matches_tilde(&self, other: &Versioning) -> bool { 118 | match (self, other) { 119 | (Versioning::Ideal(a), Versioning::Ideal(b)) => a.matches_tilde(b), 120 | (Versioning::General(a), Versioning::General(b)) => a.matches_tilde(b), 121 | // Complex can't be tilde-equal because they're not semantic. 122 | (Versioning::Complex(_), Versioning::Complex(_)) => false, 123 | // Any other combination cannot be compared. 124 | (_, _) => false, 125 | } 126 | } 127 | 128 | pub(crate) fn matches_caret(&self, other: &Versioning) -> bool { 129 | match (self, other) { 130 | (Versioning::Ideal(v1), Versioning::Ideal(v2)) => v1.matches_caret(v2), 131 | (Versioning::General(v1), Versioning::General(v2)) => v1.matches_caret(v2), 132 | // Complex can't be caret-equal because they're not semantic 133 | (Versioning::Complex(_), Versioning::Complex(_)) => false, 134 | // Any other combination cannot be compared. 135 | (_, _) => false, 136 | } 137 | } 138 | 139 | #[cfg(feature = "serde")] 140 | /// Function suitable for use as a custom serde deserializer for 141 | /// `Versioning` where `Versioning` is the type of a field in a struct. 142 | /// 143 | /// ```rust 144 | /// use versions::Versioning; 145 | /// use serde::Deserialize; 146 | /// 147 | /// #[derive(Deserialize)] 148 | /// struct Foo { 149 | /// #[serde(deserialize_with = "Versioning::deserialize_pretty")] 150 | /// version: Versioning, 151 | /// // ... 152 | /// } 153 | /// 154 | /// let foo: Foo = serde_json::from_str(r#"{"version": "1.0.0"}"#).unwrap(); 155 | /// ``` 156 | pub fn deserialize_pretty<'de, D>(deserializer: D) -> Result 157 | where 158 | D: Deserializer<'de>, 159 | { 160 | let s: String = Deserialize::deserialize(deserializer)?; 161 | 162 | Versioning::new(&s) 163 | .ok_or_else(|| Error::IllegalVersioning(s)) 164 | .map_err(D::Error::custom) 165 | } 166 | } 167 | 168 | impl PartialOrd for Versioning { 169 | fn partial_cmp(&self, other: &Self) -> Option { 170 | Some(self.cmp(other)) 171 | } 172 | } 173 | 174 | impl Ord for Versioning { 175 | fn cmp(&self, other: &Self) -> Ordering { 176 | match (self, other) { 177 | // Obvious comparisons when the types are the same. 178 | (Versioning::Ideal(a), Versioning::Ideal(b)) => a.cmp(b), 179 | (Versioning::General(a), Versioning::General(b)) => a.cmp(b), 180 | (Versioning::Complex(a), Versioning::Complex(b)) => a.cmp(b), 181 | // SemVer and Version can compare nicely. 182 | (Versioning::Ideal(a), Versioning::General(b)) => a.cmp_version(b), 183 | (Versioning::General(a), Versioning::Ideal(b)) => b.cmp_version(a).reverse(), 184 | // If we're lucky, the `Mess` is well-formed enough to pull 185 | // SemVer-like values out of its initial positions. Otherwise we 186 | // need to downcast the `SemVer` into a `Mess` and hope for the 187 | // best. 188 | (Versioning::Ideal(a), Versioning::Complex(b)) => a.cmp_mess(b), 189 | (Versioning::Complex(a), Versioning::Ideal(b)) => b.cmp_mess(a).reverse(), 190 | // Same as above - we might get lucky, we might not. 191 | // The lucky fate means no extra allocations. 192 | (Versioning::General(a), Versioning::Complex(b)) => a.cmp_mess(b), 193 | (Versioning::Complex(a), Versioning::General(b)) => b.cmp_mess(a).reverse(), 194 | } 195 | } 196 | } 197 | 198 | impl std::fmt::Display for Versioning { 199 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 200 | match self { 201 | Versioning::Ideal(s) => write!(f, "{}", s), 202 | Versioning::General(v) => write!(f, "{}", v), 203 | Versioning::Complex(m) => write!(f, "{}", m), 204 | } 205 | } 206 | } 207 | 208 | impl FromStr for Versioning { 209 | type Err = Error; 210 | 211 | fn from_str(s: &str) -> Result { 212 | Versioning::new(s).ok_or_else(|| Error::IllegalVersioning(s.to_string())) 213 | } 214 | } 215 | 216 | impl TryFrom<&str> for Versioning { 217 | type Error = Error; 218 | 219 | /// ``` 220 | /// use versions::Versioning; 221 | /// 222 | /// let orig = "1.2.3"; 223 | /// let prsd: Versioning = orig.try_into().unwrap(); 224 | /// assert_eq!(orig, prsd.to_string()); 225 | /// ``` 226 | fn try_from(value: &str) -> Result { 227 | Versioning::from_str(value) 228 | } 229 | } 230 | 231 | impl Default for Versioning { 232 | fn default() -> Self { 233 | Self::Ideal(SemVer::default()) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/require.rs: -------------------------------------------------------------------------------- 1 | //! Constraints on version numbers. 2 | 3 | use crate::{Error, Versioning}; 4 | use nom::IResult; 5 | use nom::bytes::complete::tag; 6 | use nom::combinator::map; 7 | use nom::{Parser, branch::alt}; 8 | use std::str::FromStr; 9 | 10 | #[cfg(feature = "serde")] 11 | use serde::{Deserialize, Deserializer, Serializer, de::Error as _}; 12 | 13 | /// [`Versioning`] comparison operators used in a [`Requirement`]: `=`, `>`, 14 | /// `>=`, `<`, `<=`, `~`, `^`, `*`. 15 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] 16 | pub enum Op { 17 | /// A matching `Versioning` exactly equals the requirement. 18 | Exact, 19 | /// A matching `Versioning` must be strictly greater than the requirement. 20 | Greater, 21 | /// A matching `Versioning` must be greater than or equal to the requirement. 22 | GreaterEq, 23 | /// A matching `Versioning` must be strictly less than the requirement. 24 | Less, 25 | /// A matching `Versioning` must be less than or equal to the requirement. 26 | LessEq, 27 | /// A matching `Versioning` may have a patch (or last component of the) version 28 | /// greater than or equal to the requirement. 29 | Tilde, 30 | /// A matching `Versioning` has its first non-zero component equal to the 31 | /// requirement, and all other components greater than or equal to the 32 | /// requirement. 33 | Caret, 34 | /// Any `Versioning` matches the requirement. 35 | Wildcard, 36 | } 37 | 38 | impl std::fmt::Display for Op { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | match self { 41 | Op::Exact => write!(f, "="), 42 | Op::Greater => write!(f, ">"), 43 | Op::GreaterEq => write!(f, ">="), 44 | Op::Less => write!(f, "<"), 45 | Op::LessEq => write!(f, "<="), 46 | Op::Tilde => write!(f, "~"), 47 | Op::Caret => write!(f, "^"), 48 | Op::Wildcard => write!(f, "*"), 49 | } 50 | } 51 | } 52 | 53 | impl Op { 54 | fn parse(i: &str) -> IResult<&str, Op> { 55 | // FIXME Use `value` instead of `map`. 56 | alt(( 57 | map(tag("="), |_| Op::Exact), 58 | map(tag(">="), |_| Op::GreaterEq), 59 | map(tag(">"), |_| Op::Greater), 60 | map(tag("<="), |_| Op::LessEq), 61 | map(tag("<"), |_| Op::Less), 62 | map(tag("~"), |_| Op::Tilde), 63 | map(tag("^"), |_| Op::Caret), 64 | map(tag("*"), |_| Op::Wildcard), 65 | )) 66 | .parse(i) 67 | } 68 | } 69 | 70 | /// A version requirement expression, like `^1.4.163`. 71 | /// 72 | /// See also [`Op`] for all possibilities. 73 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 74 | pub struct Requirement { 75 | /// The version requirement operation. 76 | pub op: Op, 77 | /// The version itself. `None` when `op` is `*`. 78 | pub version: Option, 79 | } 80 | 81 | impl Requirement { 82 | /// Parse a new `Requirement` from a string. 83 | pub fn new(s: &str) -> Option { 84 | match Requirement::parse(s) { 85 | Ok(("", r)) => Some(r), 86 | _ => None, 87 | } 88 | } 89 | 90 | /// Does this [`Requirement`] succeed on a tilde-match with another version? 91 | /// 92 | /// A tilde match is defined as a match where the major and minor versions 93 | /// are equal and the patch version is greater than or equal. For non-semver 94 | /// conformant `Versioning`s, this match extends the rule such that the last 95 | /// part of the version is greater than or equal. 96 | fn matches_tilde(&self, other: &Versioning) -> bool { 97 | self.version 98 | .as_ref() 99 | .is_some_and(|v| v.matches_tilde(other)) 100 | } 101 | 102 | /// Does this [`Requirement`] succeed on a caret-matche with another version? 103 | /// 104 | /// A caret match is defined as a match where the first non-zero part of the 105 | /// version is equal and the remaining parts are greater than or equal. 106 | fn matches_caret(&self, other: &Versioning) -> bool { 107 | self.version 108 | .as_ref() 109 | .is_some_and(|v| v.matches_caret(other)) 110 | } 111 | 112 | /// Check if a version matches a version constraint. 113 | /// 114 | /// ```rust 115 | /// use versions::{Requirement, Versioning}; 116 | /// use std::str::FromStr; 117 | /// 118 | /// let gt = Requirement::from_str(">=1.0.0").unwrap(); 119 | /// assert!(gt.matches(&Versioning::new("1.0.0").unwrap())); 120 | /// assert!(gt.matches(&Versioning::new("1.1.0").unwrap())); 121 | /// assert!(!gt.matches(&Versioning::new("0.9.0").unwrap())); 122 | /// 123 | /// let wild = Requirement::from_str("*").unwrap(); 124 | /// assert!(wild.matches(&Versioning::new("1.0.0").unwrap())); 125 | /// assert!(wild.matches(&Versioning::new("1.1.0").unwrap())); 126 | /// assert!(wild.matches(&Versioning::new("0.9.0").unwrap())); 127 | /// 128 | /// let constraint_eq = Requirement::from_str("=1.0.0").unwrap(); 129 | /// assert!(constraint_eq.matches(&Versioning::new("1.0.0").unwrap())); 130 | /// assert!(!constraint_eq.matches(&Versioning::new("1.1.0").unwrap())); 131 | /// assert!(!constraint_eq.matches(&Versioning::new("0.9.0").unwrap())); 132 | /// ``` 133 | pub fn matches(&self, other: &Versioning) -> bool { 134 | if let Some(version) = &self.version { 135 | match self.op { 136 | Op::Exact => other == version, 137 | Op::Greater => other > version, 138 | Op::GreaterEq => other >= version, 139 | Op::Less => other < version, 140 | Op::LessEq => other <= version, 141 | Op::Tilde => self.matches_tilde(other), 142 | Op::Caret => self.matches_caret(other), 143 | Op::Wildcard => true, 144 | } 145 | } else { 146 | matches!(self.op, Op::Wildcard) 147 | } 148 | } 149 | 150 | /// The raw `nom` parser for [`Requirement`]. Feel free to use this in 151 | /// combination with other general `nom` parsers. 152 | pub fn parse(i: &str) -> IResult<&str, Requirement> { 153 | let (i, op) = Op::parse(i)?; 154 | 155 | let (i, req) = match op { 156 | Op::Wildcard => { 157 | let req = Requirement { op, version: None }; 158 | (i, req) 159 | } 160 | _ => { 161 | let (i, vr) = Versioning::parse(i)?; 162 | 163 | let req = Requirement { 164 | op, 165 | version: Some(vr), 166 | }; 167 | 168 | (i, req) 169 | } 170 | }; 171 | 172 | Ok((i, req)) 173 | } 174 | 175 | #[cfg(feature = "serde")] 176 | /// Function suitable for use as a serde deserializer for `Requirement` where 177 | /// `Requirement` is the type of a field in a struct. 178 | /// 179 | /// ```rust 180 | /// use versions::Requirement; 181 | /// use serde::Deserialize; 182 | /// use serde_json::from_str; 183 | /// 184 | /// #[derive(Deserialize)] 185 | /// struct Foo { 186 | /// #[serde(deserialize_with = "Requirement::deserialize")] 187 | /// requirement: Requirement, 188 | /// // ... 189 | /// } 190 | /// 191 | /// let foo: Foo = from_str(r#"{"requirement": ">=1.0.0"}"#).unwrap(); 192 | /// ``` 193 | pub fn deserialize<'de, D>(deserializer: D) -> Result 194 | where 195 | D: Deserializer<'de>, 196 | { 197 | let s: String = Deserialize::deserialize(deserializer)?; 198 | 199 | s.parse().map_err(D::Error::custom) 200 | } 201 | 202 | #[cfg(feature = "serde")] 203 | /// Function suitable for use as a custom serde serializer for 204 | /// the `Requirment` type. 205 | /// 206 | /// ```rust 207 | /// use versions::Requirement; 208 | /// use serde::Serialize; 209 | /// use serde_json::to_string; 210 | /// 211 | /// #[derive(Serialize)] 212 | /// struct Foo { 213 | /// #[serde(serialize_with = "Requirement::serialize")] 214 | /// requirement: Requirement, 215 | /// // ... 216 | /// } 217 | /// 218 | /// ``` 219 | pub fn serialize(&self, serializer: S) -> Result 220 | where 221 | S: Serializer, 222 | { 223 | let s: String = self.to_string(); 224 | serializer.serialize_str(&s) 225 | } 226 | } 227 | 228 | impl FromStr for Requirement { 229 | type Err = Error; 230 | 231 | fn from_str(s: &str) -> Result { 232 | Requirement::new(s).ok_or_else(|| Error::IllegalOp(s.to_string())) 233 | } 234 | } 235 | 236 | impl Default for Requirement { 237 | fn default() -> Self { 238 | Requirement { 239 | op: Op::Wildcard, 240 | version: None, 241 | } 242 | } 243 | } 244 | 245 | impl std::fmt::Display for Requirement { 246 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 247 | let version = self 248 | .version 249 | .as_ref() 250 | .map(|v| v.to_string()) 251 | .unwrap_or_default(); 252 | write!(f, "{}{}", self.op, version,) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/mess.rs: -------------------------------------------------------------------------------- 1 | //! Types and logic for handling complex [`Mess`] versions. 2 | 3 | use crate::{Chunk, Error}; 4 | use itertools::Itertools; 5 | use nom::branch::alt; 6 | use nom::bytes::complete::tag; 7 | use nom::character::complete::{alphanumeric1, char, digit1}; 8 | use nom::combinator::eof; 9 | use nom::combinator::{map_res, opt, peek, recognize, value}; 10 | use nom::multi::separated_list1; 11 | use nom::{IResult, Parser}; 12 | use std::cmp::Ordering; 13 | use std::cmp::Ordering::{Equal, Greater, Less}; 14 | use std::hash::Hash; 15 | use std::str::FromStr; 16 | 17 | #[cfg(feature = "serde")] 18 | use serde::{Deserialize, Serialize}; 19 | 20 | /// A complex version number with no specific structure. 21 | /// 22 | /// Like [`crate::Version`] this is a *descriptive* scheme, but it is based on 23 | /// examples of stupidly crafted, near-lawless version numbers used in the wild. 24 | /// Versions like this are a considerable burden to package management software. 25 | /// 26 | /// With `Mess`, groups of letters/numbers are separated by a period, but can be 27 | /// further separated by the symbols `_-+:`. 28 | /// 29 | /// Unfortunately, [`Chunk`] cannot be used here, as some developers have 30 | /// numbers like `1.003.04` which make parsers quite sad. 31 | /// 32 | /// Some `Mess` values have a shape that is tantalizingly close to a 33 | /// [`crate::SemVer`]. Example: `1.6.0a+2014+m872b87e73dfb-1`. For values like 34 | /// these, we can extract the SemVer-compatible values out with [`Mess::nth`]. 35 | /// 36 | /// In general this is not guaranteed to have well-defined ordering behaviour, 37 | /// but existing tests show sufficient consistency. [`Mess::nth`] is used 38 | /// internally where appropriate to enhance accuracy. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ``` 43 | /// use versions::{Mess, SemVer, Version}; 44 | /// 45 | /// let mess = "20.0026.1_0-2+0.93"; 46 | /// 47 | /// let s = SemVer::new(mess); 48 | /// let v = Version::new(mess); 49 | /// let m = Mess::new(mess); 50 | /// 51 | /// assert!(s.is_none()); 52 | /// assert!(v.is_none()); 53 | /// assert_eq!(Some(mess.to_string()), m.map(|v| format!("{}", v))); 54 | /// ``` 55 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 56 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Default)] 57 | pub struct Mess { 58 | /// The first section of a `Mess`. 59 | pub chunks: Vec, 60 | /// The rest of the `Mess`. 61 | pub next: Option<(Sep, Box)>, 62 | } 63 | 64 | impl Mess { 65 | /// Parse a `Mess` from some input. 66 | pub fn new(s: S) -> Option 67 | where 68 | S: AsRef, 69 | { 70 | match Mess::parse(s.as_ref()) { 71 | Ok(("", m)) => Some(m), 72 | _ => None, 73 | } 74 | } 75 | 76 | /// Try to extract a position from the `Mess` as a nice integer, as if it 77 | /// were a [`crate::SemVer`]. 78 | /// 79 | /// ``` 80 | /// use versions::Mess; 81 | /// 82 | /// let mess = Mess::new("1.6a.0+2014+m872b87e73dfb-1").unwrap(); 83 | /// assert_eq!(Some(1), mess.nth(0)); 84 | /// assert_eq!(None, mess.nth(1)); 85 | /// assert_eq!(Some(0), mess.nth(2)); 86 | /// 87 | /// let mess = Mess::new("0:1.6a.0+2014+m872b87e73dfb-1").unwrap(); 88 | /// assert_eq!(Some(1), mess.nth(0)); 89 | /// ``` 90 | pub fn nth(&self, x: usize) -> Option { 91 | if let Some((Sep::Colon, next)) = self.next.as_ref() { 92 | next.nth(x) 93 | } else { 94 | self.chunks.get(x).and_then(|chunk| match chunk { 95 | MChunk::Digits(i, _) => Some(*i), 96 | _ => None, 97 | }) 98 | } 99 | } 100 | 101 | /// Like [`Mess::nth`], but tries to parse out a full [`Chunk`] instead. 102 | pub(crate) fn nth_chunk(&self, x: usize) -> Option { 103 | let chunk = self.chunks.get(x)?.text(); 104 | let (i, c) = Chunk::parse_without_hyphens(chunk).ok()?; 105 | match i { 106 | "" => Some(c), 107 | _ => None, 108 | } 109 | } 110 | 111 | /// The raw `nom` parser for [`Mess`]. Feel free to use this in combination 112 | /// with other general `nom` parsers. 113 | pub fn parse(i: &str) -> IResult<&str, Mess> { 114 | let (i, chunks) = separated_list1(char('.'), MChunk::parse).parse(i)?; 115 | let (i, next) = opt(Mess::next).parse(i)?; 116 | 117 | let m = Mess { 118 | chunks, 119 | next: next.map(|(s, m)| (s, Box::new(m))), 120 | }; 121 | 122 | Ok((i, m)) 123 | } 124 | 125 | fn next(i: &str) -> IResult<&str, (Sep, Mess)> { 126 | let (i, sep) = Mess::sep(i)?; 127 | let (i, mess) = Mess::parse(i)?; 128 | 129 | Ok((i, (sep, mess))) 130 | } 131 | 132 | fn sep(i: &str) -> IResult<&str, Sep> { 133 | alt(( 134 | value(Sep::Colon, char(':')), 135 | value(Sep::Hyphen, char('-')), 136 | value(Sep::Plus, char('+')), 137 | value(Sep::Underscore, char('_')), 138 | value(Sep::Tilde, char('~')), 139 | )) 140 | .parse(i) 141 | } 142 | } 143 | 144 | impl PartialOrd for Mess { 145 | fn partial_cmp(&self, other: &Self) -> Option { 146 | Some(self.cmp(other)) 147 | } 148 | } 149 | 150 | /// Build metadata does not affect version precendence, and pre-release versions 151 | /// have *lower* precedence than normal versions. 152 | impl Ord for Mess { 153 | fn cmp(&self, other: &Self) -> Ordering { 154 | match self.chunks.cmp(&other.chunks) { 155 | Equal => { 156 | let an = self.next.as_ref().map(|(_, m)| m); 157 | let bn = other.next.as_ref().map(|(_, m)| m); 158 | an.cmp(&bn) 159 | } 160 | ord => ord, 161 | } 162 | } 163 | } 164 | 165 | impl std::fmt::Display for Mess { 166 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 167 | write!(f, "{}", self.chunks.iter().join("."))?; 168 | 169 | if let Some((sep, m)) = &self.next { 170 | write!(f, "{}{}", sep, m)?; 171 | } 172 | 173 | Ok(()) 174 | } 175 | } 176 | 177 | impl FromStr for Mess { 178 | type Err = Error; 179 | 180 | fn from_str(s: &str) -> Result { 181 | Mess::new(s).ok_or_else(|| Error::IllegalMess(s.to_string())) 182 | } 183 | } 184 | 185 | impl TryFrom<&str> for Mess { 186 | type Error = Error; 187 | 188 | /// ``` 189 | /// use versions::Mess; 190 | /// 191 | /// let orig = "1.2.3.4_123_abc+101a"; 192 | /// let prsd: Mess = orig.try_into().unwrap(); 193 | /// assert_eq!(orig, prsd.to_string()); 194 | /// ``` 195 | fn try_from(value: &str) -> Result { 196 | Mess::from_str(value) 197 | } 198 | } 199 | 200 | /// Possible values of a section of a [`Mess`]. 201 | /// 202 | /// A numeric value is extracted if it could be, alongside the original text it 203 | /// came from. This preserves both `Ord` and `Display` behaviour for versions 204 | /// like `1.003.0`. 205 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 206 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 207 | pub enum MChunk { 208 | /// A nice numeric value. 209 | Digits(u32, String), 210 | /// A numeric value preceeded by an `r`, indicating a revision. 211 | Rev(u32, String), 212 | /// Anything else. 213 | Plain(String), 214 | } 215 | 216 | impl MChunk { 217 | /// Extract the original `String`, no matter which variant it parsed into. 218 | pub fn text(&self) -> &str { 219 | match self { 220 | MChunk::Digits(_, s) => s, 221 | MChunk::Rev(_, s) => s, 222 | MChunk::Plain(s) => s, 223 | } 224 | } 225 | 226 | pub(crate) fn parse(i: &str) -> IResult<&str, MChunk> { 227 | alt((MChunk::digits, MChunk::rev, MChunk::plain)).parse(i) 228 | } 229 | 230 | fn digits(i: &str) -> IResult<&str, MChunk> { 231 | let (i, (u, s)) = map_res(recognize(digit1), |s: &str| { 232 | s.parse::().map(|u| (u, s)) 233 | }) 234 | .parse(i)?; 235 | let (i, _) = alt((peek(recognize(char('.'))), peek(recognize(Mess::sep)), eof)).parse(i)?; 236 | let chunk = MChunk::Digits(u, s.to_string()); 237 | Ok((i, chunk)) 238 | } 239 | 240 | fn rev(i: &str) -> IResult<&str, MChunk> { 241 | let (i, _) = tag("r")(i)?; 242 | let (i, (u, s)) = map_res(recognize(digit1), |s: &str| { 243 | s.parse::().map(|u| (u, s)) 244 | }) 245 | .parse(i)?; 246 | let (i, _) = alt((peek(recognize(char('.'))), peek(recognize(Mess::sep)), eof)).parse(i)?; 247 | let chunk = MChunk::Rev(u, format!("r{}", s)); 248 | Ok((i, chunk)) 249 | } 250 | 251 | fn plain(i: &str) -> IResult<&str, MChunk> { 252 | let (i, s) = alphanumeric1(i)?; 253 | let chunk = MChunk::Plain(s.to_string()); 254 | Ok((i, chunk)) 255 | } 256 | } 257 | 258 | impl PartialOrd for MChunk { 259 | fn partial_cmp(&self, other: &Self) -> Option { 260 | Some(self.cmp(other)) 261 | } 262 | } 263 | 264 | impl Ord for MChunk { 265 | fn cmp(&self, other: &Self) -> Ordering { 266 | match (self, other) { 267 | // Normal cases. 268 | (MChunk::Digits(a, _), MChunk::Digits(b, _)) => a.cmp(b), 269 | (MChunk::Rev(a, _), MChunk::Rev(b, _)) => a.cmp(b), 270 | // If I'm a concrete number and you're just a revision, then I'm greater no matter what. 271 | (MChunk::Digits(_, _), MChunk::Rev(_, _)) => Greater, 272 | (MChunk::Rev(_, _), MChunk::Digits(_, _)) => Less, 273 | // There's no sensible pairing, so we fall back to String-based comparison. 274 | (a, b) => a.text().cmp(b.text()), 275 | } 276 | } 277 | } 278 | 279 | impl std::fmt::Display for MChunk { 280 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 281 | match self { 282 | MChunk::Digits(_, s) => write!(f, "{}", s), 283 | MChunk::Rev(_, s) => write!(f, "{}", s), 284 | MChunk::Plain(s) => write!(f, "{}", s), 285 | } 286 | } 287 | } 288 | 289 | /// Symbols that separate groups of digits/letters in a version number. Used in 290 | /// the [`Mess`]. 291 | /// 292 | /// These are: 293 | /// 294 | /// - A colon (:). Often denotes an "epoch". 295 | /// - A hyphen (-). 296 | /// - A tilde (~). Example: `12.0.0-3ubuntu1~20.04.5` 297 | /// - A plus (+). Stop using this outside of metadata if you are. Example: `10.2+0.93+1-1` 298 | /// - An underscore (_). Stop using this if you are. 299 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 300 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 301 | pub enum Sep { 302 | /// `:` 303 | Colon, 304 | /// `-` 305 | Hyphen, 306 | /// `+` 307 | Plus, 308 | /// `_` 309 | Underscore, 310 | /// `~` 311 | Tilde, 312 | } 313 | 314 | impl std::fmt::Display for Sep { 315 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 316 | let c = match self { 317 | Sep::Colon => ':', 318 | Sep::Hyphen => '-', 319 | Sep::Plus => '+', 320 | Sep::Underscore => '_', 321 | Sep::Tilde => '~', 322 | }; 323 | write!(f, "{}", c) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | //! Types and logic for handling general [`Version`]s. 2 | 3 | use crate::{Chunk, Chunks, Error, MChunk, Mess, Release, Sep}; 4 | use nom::character::complete::char; 5 | use nom::combinator::opt; 6 | use nom::{IResult, Parser}; 7 | use std::cmp::Ordering; 8 | use std::cmp::Ordering::{Equal, Greater, Less}; 9 | use std::hash::Hash; 10 | use std::str::FromStr; 11 | 12 | #[cfg(feature = "serde")] 13 | use 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)] 45 | pub 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, 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, 57 | /// Some extra metadata that doesn't factor into comparison. 58 | pub meta: Option, 59 | } 60 | 61 | impl Version { 62 | /// Parse a `Version` from some input. 63 | pub fn new(s: S) -> Option 64 | where 65 | S: AsRef, 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 { 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 { 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 | 245 | impl PartialOrd for Version { 246 | fn partial_cmp(&self, other: &Self) -> Option { 247 | Some(self.cmp(other)) 248 | } 249 | } 250 | 251 | impl 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 | 268 | impl 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 | 288 | impl FromStr for Version { 289 | type Err = Error; 290 | 291 | fn from_str(s: &str) -> Result { 292 | Version::new(s).ok_or_else(|| Error::IllegalVersion(s.to_string())) 293 | } 294 | } 295 | 296 | impl 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 { 307 | Version::from_str(value) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/semver.rs: -------------------------------------------------------------------------------- 1 | //! Types and logic for handling ideal [`SemVer`]s. 2 | 3 | use crate::{Chunk, Chunks, Error, MChunk, Mess, Release, Sep, Version}; 4 | use nom::character::complete::char; 5 | use nom::combinator::opt; 6 | use nom::{IResult, Parser}; 7 | use std::cmp::Ordering; 8 | use std::cmp::Ordering::{Equal, Greater, Less}; 9 | use std::hash::{Hash, Hasher}; 10 | use std::str::FromStr; 11 | 12 | #[cfg(feature = "serde")] 13 | use serde::{Deserialize, Serialize}; 14 | 15 | /// An ideal version number that conforms to Semantic Versioning. 16 | /// 17 | /// This is a *prescriptive* scheme, meaning that it follows the [SemVer 18 | /// standard][semver]. 19 | /// 20 | /// Legal semvers are of the form: MAJOR.MINOR.PATCH-PREREL+META 21 | /// 22 | /// - Simple Sample: `1.2.3` 23 | /// - Full Sample: `1.2.3-alpha.2+a1b2c3.1` 24 | /// 25 | /// # Extra Rules 26 | /// 27 | /// 1. Pre-release versions have *lower* precedence than normal versions. 28 | /// 2. Build metadata does not affect version precedence. 29 | /// 3. PREREL and META strings may only contain ASCII alphanumerics. 30 | /// 31 | /// # Examples 32 | /// 33 | /// ``` 34 | /// use versions::SemVer; 35 | /// 36 | /// let orig = "1.2.3-r1+git"; 37 | /// let attempt = SemVer::new(orig).unwrap(); 38 | /// 39 | /// assert_eq!(orig, format!("{}", attempt)); 40 | /// ``` 41 | /// 42 | /// [semver]: http://semver.org 43 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 44 | #[derive(Debug, Eq, Clone, Default)] 45 | pub struct SemVer { 46 | /// The major version. 47 | pub major: u32, 48 | /// The minor version. 49 | pub minor: u32, 50 | /// The patch version. 51 | pub patch: u32, 52 | /// `Some` implies that the inner `Vec` of the `Chunks` is not empty. 53 | pub pre_rel: Option, 54 | /// `Some` implies that the inner `String` is not empty. 55 | pub meta: Option, 56 | } 57 | 58 | impl SemVer { 59 | /// Parse a `SemVer` from some input. 60 | pub fn new(s: S) -> Option 61 | where 62 | S: AsRef, 63 | { 64 | match SemVer::parse(s.as_ref()) { 65 | Ok(("", sv)) => Some(sv), 66 | _ => None, 67 | } 68 | } 69 | 70 | /// A lossless conversion from `SemVer` to [`Version`]. 71 | /// 72 | /// ``` 73 | /// use versions::SemVer; 74 | /// 75 | /// let orig = "1.2.3-r1+git123"; 76 | /// let ver = SemVer::new(orig).unwrap().to_version(); 77 | /// 78 | /// assert_eq!("1.2.3-r1+git123", format!("{}", ver)); 79 | /// ``` 80 | pub fn to_version(&self) -> Version { 81 | let chunks = Chunks(vec![ 82 | Chunk::Numeric(self.major), 83 | Chunk::Numeric(self.minor), 84 | Chunk::Numeric(self.patch), 85 | ]); 86 | 87 | Version { 88 | epoch: None, 89 | chunks, 90 | meta: self.meta.clone(), 91 | release: self.pre_rel.clone(), 92 | } 93 | } 94 | 95 | /// A lossless conversion from `SemVer` to [`Mess`]. 96 | /// 97 | /// ``` 98 | /// use versions::SemVer; 99 | /// 100 | /// let orig = "1.2.3-r1+git123"; 101 | /// let mess = SemVer::new(orig).unwrap().to_mess(); 102 | /// 103 | /// assert_eq!(orig, format!("{}", mess)); 104 | /// ``` 105 | pub fn to_mess(&self) -> Mess { 106 | let chunks = vec![ 107 | MChunk::Digits(self.major, self.major.to_string()), 108 | MChunk::Digits(self.minor, self.minor.to_string()), 109 | MChunk::Digits(self.patch, self.patch.to_string()), 110 | ]; 111 | let next = self.pre_rel.as_ref().map(|pr| { 112 | let chunks = pr.0.iter().map(|c| c.mchunk()).collect(); 113 | let next = self.meta.as_ref().map(|meta| { 114 | let chunks = vec![MChunk::Plain(meta.clone())]; 115 | (Sep::Plus, Box::new(Mess { chunks, next: None })) 116 | }); 117 | 118 | (Sep::Hyphen, Box::new(Mess { chunks, next })) 119 | }); 120 | 121 | Mess { chunks, next } 122 | } 123 | 124 | /// Analyse the `Version` as if it's a `SemVer`. 125 | /// 126 | /// `nth_lenient` pulls a leading digit from the `Version`'s chunk if it 127 | /// could. If it couldn't, that chunk is some string (perhaps a git hash) 128 | /// and is considered as marking a beta/prerelease version. It is thus 129 | /// considered less than the `SemVer`. 130 | pub(crate) fn cmp_version(&self, other: &Version) -> Ordering { 131 | // A `Version` with a non-zero epoch value is automatically greater than 132 | // any `SemVer`. 133 | match other.epoch { 134 | Some(n) if n > 0 => Less, 135 | _ => match other.nth_lenient(0).map(|x| self.major.cmp(&x)) { 136 | None => Greater, 137 | Some(Greater) => Greater, 138 | Some(Less) => Less, 139 | Some(Equal) => match other.nth_lenient(1).map(|x| self.minor.cmp(&x)) { 140 | None => Greater, 141 | Some(Greater) => Greater, 142 | Some(Less) => Less, 143 | Some(Equal) => match other.nth_lenient(2).map(|x| self.patch.cmp(&x)) { 144 | None => Greater, 145 | Some(Greater) => Greater, 146 | Some(Less) => Less, 147 | // By this point, the major/minor/patch positions have 148 | // all been equal. If there is a fourth position, its 149 | // type, not its value, will determine which overall 150 | // version is greater. 151 | Some(Equal) => match other.chunks.0.get(3) { 152 | // 1.2.3 > 1.2.3.git 153 | Some(Chunk::Alphanum(_)) => Greater, 154 | // 1.2.3 < 1.2.3.0 155 | Some(Chunk::Numeric(_)) => Less, 156 | None => self.pre_rel.cmp(&other.release), 157 | }, 158 | }, 159 | }, 160 | }, 161 | } 162 | } 163 | 164 | /// Do our best to compare a `SemVer` and a [`Mess`]. 165 | /// 166 | /// If we're lucky, the `Mess` will be well-formed enough to pull out 167 | /// SemVer-like values at each position, yielding sane comparisons. 168 | /// Otherwise we're forced to downcast the `SemVer` into a `Mess` and let 169 | /// the String-based `Ord` instance of `Mess` handle things. 170 | pub(crate) fn cmp_mess(&self, other: &Mess) -> Ordering { 171 | match other.nth(0).map(|x| self.major.cmp(&x)) { 172 | None => self.to_mess().cmp(other), 173 | Some(Greater) => Greater, 174 | Some(Less) => Less, 175 | Some(Equal) => match other.nth(1).map(|x| self.minor.cmp(&x)) { 176 | None => self.to_mess().cmp(other), 177 | Some(Greater) => Greater, 178 | Some(Less) => Less, 179 | Some(Equal) => match other.nth(2).map(|x| self.patch.cmp(&x)) { 180 | Some(Greater) => Greater, 181 | Some(Less) => Less, 182 | // If they've been equal up to this point, the `Mess` will 183 | // by definition have more to it, meaning that it's more 184 | // likely to be newer, despite its poor shape. 185 | Some(Equal) => self.to_mess().cmp(other), 186 | // Even if we weren't able to extract a standalone patch 187 | // number, we might still be able to find a number at the 188 | // head of the `Chunk` in that position. 189 | None => match other.nth_chunk(2).and_then(|c| c.single_digit_lenient()) { 190 | // We were very close, but in the end the `Mess` had a 191 | // nonsensical value in its patch position. 192 | None => self.to_mess().cmp(other), 193 | Some(p) => match self.patch.cmp(&p) { 194 | Greater => Greater, 195 | Less => Less, 196 | // This follows SemVer's rule that pre-releases have 197 | // lower precedence. 198 | Equal => Greater, 199 | }, 200 | }, 201 | }, 202 | }, 203 | } 204 | } 205 | 206 | /// The raw `nom` parser for [`SemVer`]. Feel free to use this in 207 | /// combination with other general `nom` parsers. 208 | pub fn parse(i: &str) -> IResult<&str, SemVer> { 209 | let (i, major) = crate::parsers::unsigned(i)?; 210 | let (i, _) = char('.')(i)?; 211 | let (i, minor) = crate::parsers::unsigned(i)?; 212 | let (i, _) = char('.')(i)?; 213 | let (i, patch) = crate::parsers::unsigned(i)?; 214 | let (i, pre_rel) = opt(Release::parse).parse(i)?; 215 | let (i, meta) = opt(crate::parsers::meta).parse(i)?; 216 | 217 | let sv = SemVer { 218 | major, 219 | minor, 220 | patch, 221 | pre_rel, 222 | meta, 223 | }; 224 | 225 | Ok((i, sv)) 226 | } 227 | 228 | pub(crate) fn matches_tilde(&self, other: &SemVer) -> bool { 229 | self.major == other.major && self.minor == other.minor && other.patch >= self.patch 230 | } 231 | 232 | pub(crate) fn matches_caret(&self, other: &SemVer) -> bool { 233 | // Two ideal versions are caret-compatible if the first nonzero part of v1 and 234 | // v2 are equal and v2's parts right of the first nonzero part are greater than 235 | // or equal to v1's. 236 | if self.major == 0 && other.major == 0 { 237 | // If both major versions are zero, then the first nonzero part is the minor 238 | // version. 239 | if self.minor == 0 && other.minor == 0 { 240 | // If both minor versions are zero, then the first nonzero part is the 241 | // patch version. 242 | other.patch == self.patch 243 | } else { 244 | other.minor == self.minor && other.patch >= self.patch 245 | } 246 | } else { 247 | other.major == self.major 248 | && (other.minor > self.minor 249 | || (other.minor >= self.minor && other.patch >= self.patch)) 250 | } 251 | } 252 | } 253 | 254 | /// For Rust, it is a Law that the following must hold: 255 | /// 256 | /// > k1 == k2 -> hash(k1) == hash(k2) 257 | /// 258 | /// And so this is hand-implemented, since `PartialEq` also is. 259 | impl Hash for SemVer { 260 | fn hash(&self, state: &mut H) { 261 | self.major.hash(state); 262 | self.minor.hash(state); 263 | self.patch.hash(state); 264 | self.pre_rel.hash(state); 265 | } 266 | } 267 | 268 | /// Two SemVers are equal if all fields except metadata are equal. 269 | impl PartialEq for SemVer { 270 | fn eq(&self, other: &Self) -> bool { 271 | self.major == other.major 272 | && self.minor == other.minor 273 | && self.patch == other.patch 274 | && self.pre_rel == other.pre_rel 275 | } 276 | } 277 | 278 | impl PartialOrd for SemVer { 279 | fn partial_cmp(&self, other: &Self) -> Option { 280 | Some(self.cmp(other)) 281 | } 282 | } 283 | 284 | /// Build metadata does not affect version precendence, and pre-release versions 285 | /// have *lower* precedence than normal versions. 286 | impl Ord for SemVer { 287 | fn cmp(&self, other: &Self) -> Ordering { 288 | let a = (self.major, self.minor, self.patch); 289 | let b = (other.major, other.minor, other.patch); 290 | match a.cmp(&b) { 291 | Less => Less, 292 | Greater => Greater, 293 | Equal => match (&self.pre_rel, &other.pre_rel) { 294 | (None, None) => Equal, 295 | (None, _) => Greater, 296 | (_, None) => Less, 297 | (Some(ap), Some(bp)) => ap.cmp(bp), 298 | }, 299 | } 300 | } 301 | } 302 | 303 | impl std::fmt::Display for SemVer { 304 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 305 | write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; 306 | 307 | if let Some(p) = &self.pre_rel { 308 | write!(f, "-{}", p)?; 309 | } 310 | 311 | if let Some(m) = &self.meta { 312 | write!(f, "+{}", m)?; 313 | } 314 | 315 | Ok(()) 316 | } 317 | } 318 | 319 | impl FromStr for SemVer { 320 | type Err = Error; 321 | 322 | fn from_str(s: &str) -> Result { 323 | SemVer::new(s).ok_or_else(|| Error::IllegalSemver(s.to_string())) 324 | } 325 | } 326 | 327 | impl TryFrom<&str> for SemVer { 328 | type Error = Error; 329 | 330 | /// ``` 331 | /// use versions::SemVer; 332 | /// 333 | /// let orig = "1.2.3"; 334 | /// let prsd: SemVer = orig.try_into().unwrap(); 335 | /// assert_eq!(orig, prsd.to_string()); 336 | /// ``` 337 | fn try_from(value: &str) -> Result { 338 | SemVer::from_str(value) 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for parsing and comparing software version numbers. 2 | //! 3 | //! We like to give version numbers to our software in a myriad of different 4 | //! ways. Some ways follow strict guidelines for incrementing and comparison. 5 | //! Some follow conventional wisdom and are generally self-consistent. Some are 6 | //! just plain asinine. This library provides a means of parsing and comparing 7 | //! *any* style of versioning, be it a nice Semantic Version like this: 8 | //! 9 | //! > 1.2.3-r1 10 | //! 11 | //! ...or a monstrosity like this: 12 | //! 13 | //! > 2:10.2+0.0093r3+1-1 14 | //! 15 | //! # Usage 16 | //! 17 | //! If you're parsing several version numbers that don't follow a single scheme 18 | //! (say, as in system packages), then use the [`Versioning`] type and its 19 | //! parser [`Versioning::new`]. Otherwise, each main type - [`SemVer`], 20 | //! [`Version`], or [`Mess`] - can be parsed on their own via the `new` method 21 | //! (e.g. [`SemVer::new`]). 22 | //! 23 | //! # Examples 24 | //! 25 | //! ``` 26 | //! use versions::Versioning; 27 | //! 28 | //! let good = Versioning::new("1.6.0").unwrap(); 29 | //! let evil = Versioning::new("1.6.0a+2014+m872b87e73dfb-1").unwrap(); 30 | //! 31 | //! assert!(good.is_ideal()); // It parsed as a `SemVer`. 32 | //! assert!(evil.is_complex()); // It parsed as a `Mess`. 33 | //! assert!(good > evil); // We can compare them anyway! 34 | //! ``` 35 | //! 36 | //! # Version Constraints 37 | //! 38 | //! Tools like `cargo` also allow version constraints to be prepended to a 39 | //! version number, like in `^1.2.3`. 40 | //! 41 | //! ``` 42 | //! use versions::{Requirement, Versioning}; 43 | //! 44 | //! let req = Requirement::new("^1.2.3").unwrap(); 45 | //! let ver = Versioning::new("1.2.4").unwrap(); 46 | //! assert!(req.matches(&ver)); 47 | //! ``` 48 | //! 49 | //! In this case, the incoming version `1.2.4` satisfies the "caret" constraint, 50 | //! which demands anything greater than or equal to `1.2.3`. 51 | //! 52 | //! See the [`Requirement`] type for more details. 53 | //! 54 | //! # Usage with `nom` 55 | //! 56 | //! In constructing your own [`nom`](https://lib.rs/nom) parsers, you can 57 | //! integrate the parsers used for the types in this crate via 58 | //! [`Versioning::parse`], [`SemVer::parse`], [`Version::parse`], and 59 | //! [`Mess::parse`]. 60 | //! 61 | //! # Features 62 | //! 63 | //! You can enable [`Serde`](https://serde.rs/) support for serialization and 64 | //! deserialization with the `serde` feature. 65 | //! 66 | //! By default the version structs are serialized/deserialized as-is. If instead 67 | //! you'd like to deserialize directly from a raw version string like `1.2.3`, 68 | //! see [`Versioning::deserialize_pretty`]. 69 | 70 | #![allow(clippy::many_single_char_names)] 71 | #![warn(missing_docs)] 72 | 73 | mod mess; 74 | mod parsers; 75 | mod require; 76 | mod semver; 77 | mod version; 78 | mod versioning; 79 | 80 | pub use mess::{MChunk, Mess, Sep}; 81 | pub use require::{Op, Requirement}; 82 | pub use semver::SemVer; 83 | pub use version::Version; 84 | pub use versioning::Versioning; 85 | 86 | use itertools::EitherOrBoth::{Both, Left, Right}; 87 | use itertools::Itertools; 88 | use nom::branch::alt; 89 | use nom::character::complete::char; 90 | use nom::combinator::{fail, map}; 91 | use nom::multi::separated_list1; 92 | use nom::{IResult, Parser}; 93 | use parsers::{alphanums, hyphenated_alphanums, unsigned}; 94 | use std::cmp::Ordering; 95 | use std::cmp::Ordering::{Equal, Greater, Less}; 96 | use std::hash::Hash; 97 | 98 | #[cfg(feature = "serde")] 99 | use serde::{Deserialize, Serialize}; 100 | 101 | /// Errors unique to the parsing of version numbers. 102 | #[derive(Debug, Clone)] 103 | pub enum Error { 104 | /// Some string failed to parse into a [`SemVer`] via functions like 105 | /// [`std::str::FromStr::from_str`] or [`TryFrom::try_from`]. 106 | IllegalSemver(String), 107 | /// Some string failed to parse into a [`Version`]. 108 | IllegalVersion(String), 109 | /// Some string failed to parse into a [`Mess`]. 110 | IllegalMess(String), 111 | /// Some string failed to parse into a [`Versioning`]. 112 | IllegalVersioning(String), 113 | /// An operator failed to parse into a [`Op`]. 114 | IllegalOp(String), 115 | } 116 | 117 | impl std::error::Error for Error {} 118 | 119 | impl std::fmt::Display for Error { 120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 121 | match self { 122 | Error::IllegalSemver(s) => write!(f, "Illegal SemVer: {s}"), 123 | Error::IllegalVersion(s) => write!(f, "Illegal Version: {s}"), 124 | Error::IllegalMess(s) => write!(f, "Illegal Mess: {s}"), 125 | Error::IllegalVersioning(s) => write!(f, "Illegal Versioning: {s}"), 126 | Error::IllegalOp(s) => write!(f, "Illegal Op: {s}"), 127 | } 128 | } 129 | } 130 | 131 | /// [`Chunk`]s that have comparison behaviour according to SemVer's rules for 132 | /// prereleases. 133 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 134 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 135 | pub struct Release(pub Vec); 136 | 137 | impl Release { 138 | fn parse(i: &str) -> IResult<&str, Release> { 139 | let (i, _) = char('-')(i)?; 140 | map(separated_list1(char('.'), Chunk::parse), Release).parse(i) 141 | } 142 | } 143 | 144 | impl PartialOrd for Release { 145 | fn partial_cmp(&self, other: &Self) -> Option { 146 | Some(self.cmp(other)) 147 | } 148 | } 149 | 150 | impl Ord for Release { 151 | fn cmp(&self, other: &Self) -> Ordering { 152 | self.0 153 | .iter() 154 | .zip_longest(&other.0) 155 | .find_map(|eob| match eob { 156 | Both(a, b) => match a.cmp_semver(b) { 157 | Less => Some(Less), 158 | Greater => Some(Greater), 159 | Equal => None, 160 | }, 161 | // From the Semver spec: A larger set of pre-release fields has 162 | // a higher precedence than a smaller set, if all the preceding 163 | // identifiers are equal. 164 | Left(_) => Some(Greater), 165 | Right(_) => Some(Less), 166 | }) 167 | .unwrap_or(Equal) 168 | } 169 | } 170 | 171 | impl std::fmt::Display for Release { 172 | // FIXME Fri Jan 7 11:44:50 2022 173 | // 174 | // Use `itersperse` here once it stabilises. 175 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 176 | match self.0.as_slice() { 177 | [] => Ok(()), 178 | [c] => write!(f, "{}", c), 179 | [c, rest @ ..] => { 180 | write!(f, "{}", c)?; 181 | 182 | for r in rest { 183 | write!(f, ".{}", r)?; 184 | } 185 | 186 | Ok(()) 187 | } 188 | } 189 | } 190 | } 191 | 192 | /// [`Chunk`]s that have a comparison behaviour specific to [`Version`]. 193 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 194 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Default)] 195 | pub struct Chunks(pub Vec); 196 | 197 | impl Chunks { 198 | // Intended for parsing a `Version`. 199 | fn parse(i: &str) -> IResult<&str, Chunks> { 200 | map( 201 | separated_list1(char('.'), Chunk::parse_without_hyphens), 202 | Chunks, 203 | ) 204 | .parse(i) 205 | } 206 | } 207 | 208 | impl PartialOrd for Chunks { 209 | fn partial_cmp(&self, other: &Self) -> Option { 210 | Some(self.cmp(other)) 211 | } 212 | } 213 | 214 | impl Ord for Chunks { 215 | fn cmp(&self, other: &Self) -> Ordering { 216 | self.0 217 | .iter() 218 | .zip_longest(&other.0) 219 | .find_map(|eob| match eob { 220 | Both(a, b) => match a.cmp_lenient(b) { 221 | Less => Some(Less), 222 | Greater => Some(Greater), 223 | Equal => None, 224 | }, 225 | // From the Semver spec: A larger set of pre-release fields has 226 | // a higher precedence than a smaller set, if all the preceding 227 | // identifiers are equal. 228 | Left(_) => Some(Greater), 229 | Right(_) => Some(Less), 230 | }) 231 | .unwrap_or(Equal) 232 | } 233 | } 234 | 235 | impl std::fmt::Display for Chunks { 236 | // FIXME Fri Jan 7 11:44:50 2022 237 | // 238 | // Use `itersperse` here once it stabilises. 239 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 240 | match self.0.as_slice() { 241 | [] => Ok(()), 242 | [c] => write!(f, "{}", c), 243 | [c, rest @ ..] => { 244 | write!(f, "{}", c)?; 245 | 246 | for r in rest { 247 | write!(f, ".{}", r)?; 248 | } 249 | 250 | Ok(()) 251 | } 252 | } 253 | } 254 | } 255 | 256 | /// A logical unit of a version number. 257 | /// 258 | /// Either entirely numerical (with no leading zeroes) or entirely 259 | /// alphanumerical (with a free mixture of numbers, letters, and hyphens). 260 | /// 261 | /// Groups of these (like [`Release`]) are separated by periods to form a full 262 | /// section of a version number. 263 | /// 264 | /// # Examples 265 | /// 266 | /// - `1` 267 | /// - `20150826` 268 | /// - `r3` 269 | /// - `0rc1-abc3` 270 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 271 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 272 | pub enum Chunk { 273 | /// A nice, pure number. 274 | Numeric(u32), 275 | /// A mixture of letters, numbers, and hyphens. 276 | Alphanum(String), 277 | } 278 | 279 | impl Chunk { 280 | /// If this `Chunk` is made up of a single digit, then pull out the inner 281 | /// value. 282 | /// 283 | /// ``` 284 | /// use versions::Chunk; 285 | /// 286 | /// let v = Chunk::Numeric(1); 287 | /// assert_eq!(Some(1), v.single_digit()); 288 | /// 289 | /// let v = Chunk::Alphanum("abc".to_string()); 290 | /// assert_eq!(None, v.single_digit()); 291 | /// 292 | /// let v = Chunk::Alphanum("1abc".to_string()); 293 | /// assert_eq!(None, v.single_digit()); 294 | /// ``` 295 | pub fn single_digit(&self) -> Option { 296 | match self { 297 | Chunk::Numeric(n) => Some(*n), 298 | Chunk::Alphanum(_) => None, 299 | } 300 | } 301 | 302 | /// Like [`Chunk::single_digit`], but will grab a leading `u32` even if 303 | /// followed by letters. 304 | /// 305 | /// ``` 306 | /// use versions::Chunk; 307 | /// 308 | /// let v = Chunk::Numeric(1); 309 | /// assert_eq!(Some(1), v.single_digit_lenient()); 310 | /// 311 | /// let v = Chunk::Alphanum("abc".to_string()); 312 | /// assert_eq!(None, v.single_digit_lenient()); 313 | /// 314 | /// let v = Chunk::Alphanum("1abc".to_string()); 315 | /// assert_eq!(Some(1), v.single_digit_lenient()); 316 | /// ``` 317 | pub fn single_digit_lenient(&self) -> Option { 318 | match self { 319 | Chunk::Numeric(n) => Some(*n), 320 | Chunk::Alphanum(s) => unsigned(s).ok().map(|(_, n)| n), 321 | } 322 | } 323 | 324 | /// Like [`Chunk::single_digit`], but will grab a trailing `u32`. 325 | /// 326 | /// ``` 327 | /// use versions::Chunk; 328 | /// 329 | /// let v = Chunk::Alphanum("r23".to_string()); 330 | /// assert_eq!(Some(23), v.single_digit_lenient_post()); 331 | /// ``` 332 | pub fn single_digit_lenient_post(&self) -> Option { 333 | match self { 334 | Chunk::Numeric(n) => Some(*n), 335 | Chunk::Alphanum(s) => { 336 | // FIXME 2024-08-05 `strip_prefix` may be too aggressive. Should 337 | // we only strip one char instead? 338 | s.strip_prefix(|c: char| c.is_ascii_alphabetic()) 339 | .and_then(|stripped| unsigned(stripped).ok()) 340 | .map(|(_, n)| n) 341 | } 342 | } 343 | } 344 | 345 | fn parse(i: &str) -> IResult<&str, Chunk> { 346 | alt((Chunk::alphanum, Chunk::numeric)).parse(i) 347 | } 348 | 349 | fn parse_without_hyphens(i: &str) -> IResult<&str, Chunk> { 350 | alt((Chunk::alphanum_without_hyphens, Chunk::numeric)).parse(i) 351 | } 352 | 353 | // A clever interpretation of the grammar of "alphanumeric identifier". 354 | // Instead of having a big, composed parser that structurally accounts for 355 | // the presence of a "non-digit", we just check for one after the fact. 356 | fn alphanum(i: &str) -> IResult<&str, Chunk> { 357 | let (i2, ids) = hyphenated_alphanums(i)?; 358 | 359 | if ids.contains(|c: char| c.is_ascii_alphabetic() || c == '-') { 360 | Ok((i2, Chunk::Alphanum(ids.to_string()))) 361 | } else { 362 | fail().parse(i) 363 | } 364 | } 365 | 366 | fn alphanum_without_hyphens(i: &str) -> IResult<&str, Chunk> { 367 | let (i2, ids) = alphanums(i)?; 368 | 369 | if ids.contains(|c: char| c.is_ascii_alphabetic()) { 370 | Ok((i2, Chunk::Alphanum(ids.to_string()))) 371 | } else { 372 | fail().parse(i) 373 | } 374 | } 375 | 376 | fn numeric(i: &str) -> IResult<&str, Chunk> { 377 | map(unsigned, Chunk::Numeric).parse(i) 378 | } 379 | 380 | fn mchunk(&self) -> MChunk { 381 | // FIXME Fri Jan 7 12:34:24 2022 382 | // 383 | // Is there going to be an issue here, having not accounted for an `r`? 384 | // 385 | // 2023-08-05 386 | // This actually just came up in Aura, but for Versions. 387 | match self { 388 | Chunk::Numeric(n) => MChunk::Digits(*n, n.to_string()), 389 | Chunk::Alphanum(s) => MChunk::Plain(s.clone()), 390 | } 391 | // match self.0.as_slice() { 392 | // [] => None, 393 | // [Unit::Digits(u)] => Some(MChunk::Digits(*u, u.to_string())), 394 | // [Unit::Letters(s), Unit::Digits(u)] if s == "r" => { 395 | // Some(MChunk::Rev(*u, format!("r{}", u))) 396 | // } 397 | // [Unit::Letters(s)] => Some(MChunk::Plain(s.clone())), 398 | // _ => Some(MChunk::Plain(format!("{}", self))), 399 | // } 400 | } 401 | 402 | fn cmp_semver(&self, other: &Self) -> Ordering { 403 | match (self, other) { 404 | (Chunk::Numeric(a), Chunk::Numeric(b)) => a.cmp(b), 405 | (Chunk::Numeric(_), Chunk::Alphanum(_)) => Less, 406 | (Chunk::Alphanum(_), Chunk::Numeric(_)) => Greater, 407 | (Chunk::Alphanum(a), Chunk::Alphanum(b)) => a.cmp(b), 408 | } 409 | } 410 | 411 | fn cmp_lenient(&self, other: &Self) -> Ordering { 412 | match (self, other) { 413 | (Chunk::Numeric(a), Chunk::Numeric(b)) => a.cmp(b), 414 | (a @ Chunk::Alphanum(x), b @ Chunk::Alphanum(y)) => { 415 | match (x.chars().next(), y.chars().next()) { 416 | (Some(xc), Some(yc)) if xc.is_ascii_alphabetic() && xc == yc => { 417 | match (a.single_digit_lenient_post(), b.single_digit_lenient_post()) { 418 | // r8 < r23 419 | (Some(m), Some(n)) => m.cmp(&n), 420 | _ => x.cmp(y), 421 | } 422 | } 423 | (Some(xc), Some(yc)) if xc.is_ascii_digit() && yc.is_ascii_digit() => { 424 | match (a.single_digit_lenient(), b.single_digit_lenient()) { 425 | // 0rc1 < 1rc1 426 | (Some(i), Some(j)) => i.cmp(&j), 427 | _ => x.cmp(y), 428 | } 429 | } 430 | _ => x.cmp(y), 431 | } 432 | } 433 | (Chunk::Numeric(n), b @ Chunk::Alphanum(_)) => match b.single_digit_lenient() { 434 | None => Greater, 435 | Some(m) => match n.cmp(&m) { 436 | // 1.2.0 > 1.2.0rc1 437 | Equal => Greater, 438 | c => c, 439 | }, 440 | }, 441 | (a @ Chunk::Alphanum(_), Chunk::Numeric(n)) => match a.single_digit_lenient() { 442 | None => Less, 443 | Some(m) => match m.cmp(n) { 444 | // 1.2.0rc1 < 1.2.0 445 | Equal => Less, 446 | c => c, 447 | }, 448 | }, 449 | } 450 | } 451 | } 452 | 453 | impl std::fmt::Display for Chunk { 454 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 455 | match self { 456 | Chunk::Numeric(n) => write!(f, "{}", n), 457 | Chunk::Alphanum(a) => write!(f, "{}", a), 458 | } 459 | } 460 | } 461 | 462 | #[cfg(test)] 463 | mod tests { 464 | use std::str::FromStr; 465 | 466 | use super::*; 467 | 468 | #[test] 469 | fn chanks() { 470 | assert_eq!(Ok(("", Chunk::Numeric(123))), Chunk::parse("123")); 471 | assert_eq!( 472 | Ok(("", Chunk::Alphanum("123a".to_string()))), 473 | Chunk::parse("123a") 474 | ); 475 | assert_eq!( 476 | Ok(("", Chunk::Alphanum("123-456".to_string()))), 477 | Chunk::parse("123-456") 478 | ); 479 | assert_eq!( 480 | Ok(("", Chunk::Alphanum("00a".to_string()))), 481 | Chunk::parse("00a") 482 | ); 483 | } 484 | 485 | #[test] 486 | fn mess_chunks() { 487 | assert_eq!( 488 | Ok(("", MChunk::Digits(1, "1".to_owned()))), 489 | MChunk::parse("1") 490 | ); 491 | 492 | assert_eq!( 493 | Ok(("", MChunk::Digits(1, "01".to_owned()))), 494 | MChunk::parse("01") 495 | ); 496 | 497 | assert_eq!( 498 | Ok(("", MChunk::Digits(987, "987".to_owned()))), 499 | MChunk::parse("987") 500 | ); 501 | 502 | assert_eq!( 503 | Ok(("", MChunk::Rev(1, "r1".to_owned()))), 504 | MChunk::parse("r1") 505 | ); 506 | 507 | assert_eq!( 508 | Ok(("", MChunk::Rev(1, "r01".to_owned()))), 509 | MChunk::parse("r01") 510 | ); 511 | 512 | assert_eq!( 513 | Ok(("", MChunk::Rev(987, "r987".to_owned()))), 514 | MChunk::parse("r987") 515 | ); 516 | 517 | assert_eq!( 518 | Ok(("", MChunk::Plain("abcd".to_owned()))), 519 | MChunk::parse("abcd") 520 | ); 521 | 522 | assert_eq!( 523 | Ok(("", MChunk::Plain("1r3".to_owned()))), 524 | MChunk::parse("1r3") 525 | ); 526 | 527 | assert_eq!( 528 | Ok(("", MChunk::Plain("alpha0".to_owned()))), 529 | MChunk::parse("alpha0") 530 | ); 531 | } 532 | 533 | #[test] 534 | fn parse_mess() { 535 | assert_eq!( 536 | Ok(( 537 | "", 538 | Mess { 539 | chunks: vec![MChunk::Digits(1, "1".to_owned()),], 540 | next: None 541 | } 542 | )), 543 | Mess::parse("1") 544 | ); 545 | } 546 | 547 | #[test] 548 | fn official_semvers() { 549 | let goods = vec![ 550 | "0.1.0", 551 | "1.2.3", 552 | "1.2.3-1", 553 | "1.2.3-alpha", 554 | "1.2.3-alpha.2", 555 | "1.2.3+a1b2c3.1", 556 | "1.2.3-alpha.2+a1b2c3.1", 557 | "1.0.0-x-y-z.-", 558 | "1.0.0-alpha+001", 559 | "1.0.0+21AF26D3---117B344092BD", 560 | ]; 561 | 562 | for s in goods { 563 | assert_eq!( 564 | Some(s.to_string()), 565 | SemVer::new(s).map(|sv| format!("{}", sv)) 566 | ) 567 | } 568 | } 569 | 570 | #[test] 571 | fn good_semvers() { 572 | let goods = vec![ 573 | "0.4.8-1", 574 | "7.42.13-4", 575 | "2.1.16102-2", 576 | "2.2.1-b05", 577 | "1.11.0+20200830-1", 578 | ]; 579 | 580 | for s in goods { 581 | assert_eq!( 582 | Some(s.to_string()), 583 | SemVer::new(s).map(|sv| format!("{}", sv)) 584 | ) 585 | } 586 | } 587 | 588 | #[test] 589 | fn tricky_semvers() { 590 | let v = "1.2.2-00a"; 591 | 592 | assert_eq!("", SemVer::parse(v).unwrap().0); 593 | } 594 | 595 | #[test] 596 | fn bad_semvers() { 597 | let bads = vec![ 598 | "1", 599 | "1.2", 600 | "a.b.c", 601 | "1.01.1", 602 | "1.2.3+a1b!2c3.1", 603 | "", 604 | "1.2.3 ", 605 | ]; 606 | 607 | bads.iter().for_each(|s| assert_eq!(None, SemVer::new(s))); 608 | } 609 | 610 | #[test] 611 | /// The exact example from http://semver.org 612 | fn semver_ord() { 613 | let svs = vec![ 614 | "1.0.0-alpha", 615 | "1.0.0-alpha.1", 616 | "1.0.0-alpha.beta", 617 | "1.0.0-beta", 618 | "1.0.0-beta.2", 619 | "1.0.0-beta.11", 620 | "1.0.0-rc.1", 621 | "1.0.0", 622 | ]; 623 | 624 | for (a, b) in svs.iter().zip(&svs[1..]) { 625 | let x = SemVer::new(a).unwrap(); 626 | let y = SemVer::new(b).unwrap(); 627 | 628 | assert!(x < y, "{} < {}", x, y); 629 | } 630 | } 631 | 632 | #[test] 633 | fn good_versions() { 634 | let goods = vec![ 635 | "1", 636 | "1.2", 637 | "1.0rc0", 638 | "1.0rc1", 639 | "1.1rc1", 640 | "44.0.2403.157-1", 641 | "0.25-2", 642 | "8.u51-1", 643 | "21-2", 644 | "7.1p1-1", 645 | "20150826-1", 646 | "1:0.10.16-3", 647 | "8.64.0.81-1", 648 | "1:3.20-1", 649 | ]; 650 | 651 | for s in goods { 652 | assert!(SemVer::new(s).is_none(), "Shouldn't be SemVer: {}", s); 653 | assert_eq!( 654 | Some(s.to_string()), 655 | Version::new(s).map(|v| format!("{}", v)) 656 | ) 657 | } 658 | } 659 | 660 | #[test] 661 | fn bad_versions() { 662 | let bads = vec!["", "1.2 "]; 663 | 664 | bads.iter().for_each(|b| assert_eq!(None, Version::new(b))); 665 | } 666 | 667 | #[test] 668 | fn version_ord() { 669 | let vs = vec!["0.9.9.9", "1.0.0.0", "1.0.0.1", "2"]; 670 | 671 | for (a, b) in vs.iter().zip(&vs[1..]) { 672 | cmp_versions(a, b); 673 | } 674 | 675 | cmp_versions("1.2-5", "1.2.3-1"); 676 | cmp_versions("1.0rc1", "1.0"); 677 | cmp_versions("1.0", "1:1.0"); 678 | cmp_versions("1.1", "1:1.0"); 679 | cmp_versions("1.1", "1:1.1"); 680 | } 681 | 682 | // https://github.com/fosskers/rs-versions/issues/25 683 | #[test] 684 | fn versions_25() { 685 | let bad = Versioning::new("0:8.7p1-38.el9").unwrap(); 686 | let good = Versioning::new("0:8.7p1-38.el9_4.1").unwrap(); 687 | 688 | assert!(bad.is_general()); 689 | assert!(good.is_complex()); 690 | assert!(good > bad); 691 | } 692 | 693 | // https://github.com/fosskers/rs-versions/issues/29 694 | // 695 | // If a `Chunk` goes longer than `u32`, we can't parse it as a `SemVer`. I 696 | // decided to mark this "wontfix", as accounting for it increases complexity 697 | // and might screw up comparison precedence if something that is actually 698 | // true a number is treated instead as a string. 699 | #[test] 700 | fn versions_29() { 701 | let bad = Versioning::new("0.0.0-0.1730239248325").unwrap(); 702 | assert!(bad.is_complex()); 703 | } 704 | 705 | // https://github.com/fosskers/aura/issues/876 706 | #[test] 707 | fn aura_876() { 708 | let x = Versioning::new("2.14.r8.g28399bf-1").unwrap(); 709 | let y = Versioning::new("2.14.r23.ga07d3df-1").unwrap(); 710 | assert!(x.is_general()); 711 | assert!(y.is_general()); 712 | cmp_versions("2.14.r8.g28399bf-1", "2.14.r23.ga07d3df-1"); 713 | cmp_versions("2.14.r23.ga07d3df-1", "2.14.8"); 714 | } 715 | 716 | fn cmp_versions(a: &str, b: &str) { 717 | let x = Version::new(a).unwrap(); 718 | let y = Version::new(b).unwrap(); 719 | 720 | assert!(x < y, "{} < {}", x, y); 721 | assert!(y > x, "{} > {}", y, x); 722 | } 723 | 724 | #[test] 725 | fn good_messes() { 726 | let messes = vec![ 727 | "10.2+0.93+1-1", 728 | "003.03-3", 729 | "002.000-7", 730 | "20.26.1_0-2", 731 | "1.6.0a+2014+m872b87e73dfb-1", 732 | "0.17.0+r8+gc41db5f1-1", 733 | "0.17.0+r157+g584760cf-1", 734 | "1.002.3+r003", 735 | "1.3.00.16851-1", 736 | "5.2.458699.0906-1", 737 | "12.0.0-3ubuntu1~20.04.5", 738 | ]; 739 | 740 | for s in messes { 741 | let sv = SemVer::new(s); 742 | let vr = Version::new(s); 743 | assert!(sv.is_none(), "Shouldn't be SemVer: {} -> {:#?}", s, sv); 744 | assert!(vr.is_none(), "Shouldn't be Version: {} -> {:#?}", s, vr); 745 | assert_eq!(Some(s.to_string()), Mess::new(s).map(|v| format!("{}", v))); 746 | } 747 | } 748 | 749 | #[test] 750 | fn bad_messes() { 751 | let bads = vec!["", "003.03-3 "]; 752 | 753 | bads.iter().for_each(|b| assert_eq!(None, Mess::new(b))); 754 | } 755 | 756 | #[test] 757 | fn mess_ord() { 758 | let messes = vec![ 759 | "10.2+0.93+1-1", 760 | "10.2+0.93+1-2", 761 | "10.2+0.93+2-1", 762 | "10.2+0.94+1-1", 763 | "10.3+0.93+1-1", 764 | "11.2+0.93+1-1", 765 | "12", 766 | ]; 767 | 768 | for (a, b) in messes.iter().zip(&messes[1..]) { 769 | cmp_messes(a, b); 770 | } 771 | } 772 | 773 | #[test] 774 | fn mess_7zip() { 775 | cmp_messes("22.01-ZS-v1.5.5-R2", "22.01-ZS-v1.5.6-R2"); 776 | cmp_messes("22.01-ZS-v1.5.5-R2", "24.02-ZS-v1.6.0"); 777 | } 778 | 779 | fn cmp_messes(a: &str, b: &str) { 780 | let x = Mess::new(a).unwrap(); 781 | let y = Mess::new(b).unwrap(); 782 | 783 | assert!(x < y, "{} < {}", x, y); 784 | assert!(y > x, "{} > {}", y, x); 785 | } 786 | 787 | #[test] 788 | fn equality() { 789 | let vers = vec![ 790 | "1:3.20.1-1", 791 | "1.3.00.25560-1", 792 | "150_28-3", 793 | "1.0.r15.g3fc772c-5", 794 | "0.88-2", 795 | ]; 796 | 797 | for v in vers { 798 | let x = Versioning::new(v).unwrap(); 799 | 800 | assert_eq!(Equal, x.cmp(&x)); 801 | } 802 | } 803 | 804 | #[test] 805 | fn mixed_comparisons() { 806 | cmp_versioning("1.2.3", "1.2.3.0"); 807 | cmp_versioning("1.2.3.git", "1.2.3"); 808 | cmp_versioning("1.2.2r1-1", "1.2.3-1"); 809 | cmp_versioning("1.2.3-1", "1.2.4r1-1"); 810 | cmp_versioning("1.2.3-1", "2+0007-1"); 811 | cmp_versioning("1.2.3r1-1", "2+0007-1"); 812 | cmp_versioning("1.2-5", "1.2.3-1"); 813 | cmp_versioning("1.6.0a+2014+m872b87e73dfb-1", "1.6.0-1"); 814 | cmp_versioning("1.11.0.git.20200404-1", "1.11.0+20200830-1"); 815 | cmp_versioning("0.17.0+r8+gc41db5f1-1", "0.17.0+r157+g584760cf-1"); 816 | cmp_versioning("2.2.3", "10e"); 817 | cmp_versioning("e.2.3", "1.2.3"); 818 | cmp_versioning("0.4.8-1", "0.4.9-1"); 819 | cmp_versioning("2.1.16102-2", "2.1.17627-1"); 820 | cmp_versioning("8.64.0.81-1", "8.65.0.78-1"); 821 | cmp_versioning("1.3.00.16851-1", "1.3.00.25560-1"); 822 | cmp_versioning("1:3.20-1", "1:3.20.1-1"); 823 | cmp_versioning("5.2.458699.0906-1", "5.3.472687.1012-1"); 824 | cmp_versioning("1.2.3", "1:1.2.0"); 825 | } 826 | 827 | fn cmp_versioning(a: &str, b: &str) { 828 | let x = Versioning::new(a).unwrap(); 829 | let y = Versioning::new(b).unwrap(); 830 | 831 | assert!( 832 | x < y, 833 | "\nAttempted: {} < {}\nLesser: {:?}\nGreater: {:?}\nThinks: {:?}", 834 | x, 835 | y, 836 | x, 837 | y, 838 | x.cmp(&y) 839 | ); 840 | } 841 | 842 | #[test] 843 | fn parsing_sanity() { 844 | assert_eq!(Ok(34), "0034".parse::()) 845 | } 846 | 847 | #[test] 848 | fn test_eq() { 849 | assert!( 850 | Requirement::from_str("=1.0.0") 851 | .unwrap() 852 | .matches(&Versioning::new("1.0.0").unwrap()) 853 | ); 854 | assert!( 855 | Requirement::from_str("=1.1.0") 856 | .unwrap() 857 | .matches(&Versioning::new("1.1.0").unwrap()) 858 | ); 859 | assert!( 860 | Requirement::from_str("=0.9.0") 861 | .unwrap() 862 | .matches(&Versioning::new("0.9.0").unwrap()) 863 | ); 864 | assert!( 865 | Requirement::from_str("=6.0.pre134") 866 | .unwrap() 867 | .matches(&Versioning::new("6.0.pre134").unwrap()) 868 | ); 869 | assert!( 870 | Requirement::from_str("=6.0.166") 871 | .unwrap() 872 | .matches(&Versioning::new("6.0.166").unwrap()) 873 | ); 874 | } 875 | 876 | #[test] 877 | fn test_wild() { 878 | let wild = Requirement::from_str("*").unwrap(); 879 | assert!(wild.matches(&Versioning::new("1.0.0").unwrap())); 880 | assert!(wild.matches(&Versioning::new("1.1.0").unwrap())); 881 | assert!(wild.matches(&Versioning::new("0.9.0").unwrap())); 882 | assert!(wild.matches(&Versioning::new("6.0.pre134").unwrap())); 883 | assert!(wild.matches(&Versioning::new("6.0.166").unwrap())); 884 | } 885 | 886 | #[test] 887 | fn test_gt() { 888 | let gt = Requirement::from_str(">1.1.1").unwrap(); 889 | 890 | assert!(!gt.matches(&Versioning::new("1.1.1").unwrap())); 891 | assert!(gt.matches(&Versioning::new("2.2.2").unwrap())); 892 | assert!(gt.matches(&Versioning::new("2.0.0").unwrap())); 893 | assert!(gt.matches(&Versioning::new("1.2.0").unwrap())); 894 | assert!(gt.matches(&Versioning::new("1.1.2").unwrap())); 895 | assert!(!gt.matches(&Versioning::new("0.9.9").unwrap())); 896 | assert!(!gt.matches(&Versioning::new("0.1.1").unwrap())); 897 | assert!(!gt.matches(&Versioning::new("1.0.0").unwrap())); 898 | assert!(!gt.matches(&Versioning::new("1.1.0").unwrap())); 899 | } 900 | 901 | #[test] 902 | fn test_lt() { 903 | let lt = Requirement::from_str("<1.1.1").unwrap(); 904 | assert!(!lt.matches(&Versioning::new("1.1.1").unwrap())); 905 | assert!(!lt.matches(&Versioning::new("2.2.2").unwrap())); 906 | assert!(!lt.matches(&Versioning::new("2.0.0").unwrap())); 907 | assert!(!lt.matches(&Versioning::new("1.2.0").unwrap())); 908 | assert!(!lt.matches(&Versioning::new("1.1.2").unwrap())); 909 | assert!(lt.matches(&Versioning::new("0.9.9").unwrap())); 910 | assert!(lt.matches(&Versioning::new("0.1.1").unwrap())); 911 | assert!(lt.matches(&Versioning::new("1.0.0").unwrap())); 912 | assert!(lt.matches(&Versioning::new("1.1.0").unwrap())); 913 | } 914 | 915 | #[test] 916 | fn test_gte() { 917 | let gte = Requirement::from_str(">=1.1.1").unwrap(); 918 | assert!(gte.matches(&Versioning::new("1.1.1").unwrap())); 919 | assert!(gte.matches(&Versioning::new("2.2.2").unwrap())); 920 | assert!(gte.matches(&Versioning::new("2.0.0").unwrap())); 921 | assert!(gte.matches(&Versioning::new("1.2.0").unwrap())); 922 | assert!(gte.matches(&Versioning::new("1.1.2").unwrap())); 923 | assert!(!gte.matches(&Versioning::new("0.9.9").unwrap())); 924 | assert!(!gte.matches(&Versioning::new("0.1.1").unwrap())); 925 | assert!(!gte.matches(&Versioning::new("1.0.0").unwrap())); 926 | assert!(!gte.matches(&Versioning::new("1.1.0").unwrap())); 927 | } 928 | 929 | #[test] 930 | fn test_lte() { 931 | let lte = Requirement::from_str("<=1.1.1").unwrap(); 932 | assert!(lte.matches(&Versioning::new("1.1.1").unwrap())); 933 | assert!(!lte.matches(&Versioning::new("2.2.2").unwrap())); 934 | assert!(!lte.matches(&Versioning::new("2.0.0").unwrap())); 935 | assert!(!lte.matches(&Versioning::new("1.2.0").unwrap())); 936 | assert!(!lte.matches(&Versioning::new("1.1.2").unwrap())); 937 | assert!(lte.matches(&Versioning::new("0.9.9").unwrap())); 938 | assert!(lte.matches(&Versioning::new("0.1.1").unwrap())); 939 | assert!(lte.matches(&Versioning::new("1.0.0").unwrap())); 940 | assert!(lte.matches(&Versioning::new("1.1.0").unwrap())); 941 | } 942 | 943 | #[test] 944 | fn test_tilde() { 945 | let tilde = Requirement::from_str("~1.1.1").unwrap(); 946 | assert!(tilde.matches(&Versioning::new("1.1.1").unwrap())); 947 | assert!(tilde.matches(&Versioning::new("1.1.2").unwrap())); 948 | assert!(tilde.matches(&Versioning::new("1.1.3").unwrap())); 949 | assert!(!tilde.matches(&Versioning::new("1.2.0").unwrap())); 950 | assert!(!tilde.matches(&Versioning::new("2.0.0").unwrap())); 951 | assert!(!tilde.matches(&Versioning::new("2.2.2").unwrap())); 952 | assert!(!tilde.matches(&Versioning::new("0.9.9").unwrap())); 953 | assert!(!tilde.matches(&Versioning::new("0.1.1").unwrap())); 954 | assert!(!tilde.matches(&Versioning::new("1.0.0").unwrap())); 955 | } 956 | 957 | #[test] 958 | fn test_caret() { 959 | let caret = Requirement::from_str("^1.1.1").unwrap(); 960 | assert!(caret.matches(&Versioning::new("1.1.1").unwrap())); 961 | assert!(caret.matches(&Versioning::new("1.1.2").unwrap())); 962 | assert!(caret.matches(&Versioning::new("1.1.3").unwrap())); 963 | assert!(caret.matches(&Versioning::new("1.2.0").unwrap())); 964 | assert!(!caret.matches(&Versioning::new("2.0.0").unwrap())); 965 | assert!(!caret.matches(&Versioning::new("2.2.2").unwrap())); 966 | assert!(!caret.matches(&Versioning::new("0.9.9").unwrap())); 967 | assert!(!caret.matches(&Versioning::new("0.1.1").unwrap())); 968 | assert!(!caret.matches(&Versioning::new("1.0.0").unwrap())); 969 | } 970 | 971 | #[cfg(feature = "serde")] 972 | #[test] 973 | fn test_deserialize() { 974 | use serde::Deserialize; 975 | 976 | #[derive(Deserialize)] 977 | struct DeserializableVersioning { 978 | #[serde(deserialize_with = "Versioning::deserialize_pretty")] 979 | version: Versioning, 980 | } 981 | 982 | let deserializable: DeserializableVersioning = 983 | serde_json::from_str(r#"{"version": "1.2.3"}"#).unwrap(); 984 | 985 | assert_eq!(deserializable.version, Versioning::new("1.2.3").unwrap()); 986 | 987 | #[derive(Deserialize)] 988 | struct DeserializableRequirement { 989 | #[serde(deserialize_with = "Requirement::deserialize")] 990 | version: Requirement, 991 | } 992 | 993 | let deserializable: DeserializableRequirement = 994 | serde_json::from_str(r#"{"version": ">=1.2.3"}"#).unwrap(); 995 | 996 | assert_eq!( 997 | deserializable.version, 998 | Requirement { 999 | op: Op::GreaterEq, 1000 | version: Some(Versioning::new("1.2.3").unwrap()) 1001 | } 1002 | ); 1003 | } 1004 | 1005 | #[cfg(feature = "serde")] 1006 | #[test] 1007 | fn test_serialize() { 1008 | use serde::Serialize; 1009 | 1010 | #[derive(Serialize)] 1011 | struct SerializableRequirement { 1012 | #[serde(serialize_with = "Requirement::serialize")] 1013 | req: Requirement, 1014 | } 1015 | 1016 | let test_object = SerializableRequirement { 1017 | req: Requirement::from_str(">=1.2.3").unwrap(), 1018 | }; 1019 | 1020 | let string = serde_json::to_string(&test_object).unwrap(); 1021 | 1022 | assert_eq!(string, "{\"req\":\">=1.2.3\"}"); 1023 | } 1024 | } 1025 | --------------------------------------------------------------------------------