cargo/util/
semver_ext.rs

1use super::semver_eval_ext;
2use semver::{Comparator, Op, Version, VersionReq};
3use std::fmt::{self, Display};
4
5pub trait VersionExt {
6    fn is_prerelease(&self) -> bool;
7
8    fn to_req(&self, op: Op) -> VersionReq;
9
10    fn to_exact_req(&self) -> VersionReq {
11        self.to_req(Op::Exact)
12    }
13
14    fn to_caret_req(&self) -> VersionReq {
15        self.to_req(Op::Caret)
16    }
17}
18
19impl VersionExt for Version {
20    fn is_prerelease(&self) -> bool {
21        !self.pre.is_empty()
22    }
23
24    fn to_req(&self, op: Op) -> VersionReq {
25        VersionReq {
26            comparators: vec![Comparator {
27                op,
28                major: self.major,
29                minor: Some(self.minor),
30                patch: Some(self.patch),
31                pre: self.pre.clone(),
32            }],
33        }
34    }
35}
36
37pub trait VersionReqExt {
38    fn matches_prerelease(&self, version: &Version) -> bool;
39}
40
41impl VersionReqExt for VersionReq {
42    fn matches_prerelease(&self, version: &Version) -> bool {
43        semver_eval_ext::matches_prerelease(self, version)
44    }
45}
46
47#[derive(PartialEq, Eq, Hash, Clone, Debug)]
48pub enum OptVersionReq {
49    Any,
50    Req(VersionReq),
51    /// The exact locked version and the original version requirement.
52    Locked(Version, VersionReq),
53    /// The exact requested version and the original version requirement.
54    ///
55    /// This looks identical to [`OptVersionReq::Locked`] but has a different
56    /// meaning, and is used for the `--precise` field of `cargo update`.
57    /// See comments in [`OptVersionReq::matches`] for more.
58    Precise(Version, VersionReq),
59}
60
61impl OptVersionReq {
62    pub fn exact(version: &Version) -> Self {
63        OptVersionReq::Req(version.to_exact_req())
64    }
65
66    // Since some registries have allowed crate versions to differ only by build metadata,
67    // A query using OptVersionReq::exact return nondeterministic results.
68    // So we `lock_to` the exact version were interested in.
69    pub fn lock_to_exact(version: &Version) -> Self {
70        OptVersionReq::Locked(version.clone(), version.to_exact_req())
71    }
72
73    pub fn is_exact(&self) -> bool {
74        match self {
75            OptVersionReq::Any => false,
76            OptVersionReq::Req(req) | OptVersionReq::Precise(_, req) => {
77                req.comparators.len() == 1 && {
78                    let cmp = &req.comparators[0];
79                    cmp.op == Op::Exact && cmp.minor.is_some() && cmp.patch.is_some()
80                }
81            }
82            OptVersionReq::Locked(..) => true,
83        }
84    }
85
86    pub fn lock_to(&mut self, version: &Version) {
87        assert!(self.matches(version), "cannot lock {} to {}", self, version);
88        use OptVersionReq::*;
89        let version = version.clone();
90        *self = match self {
91            Any => Locked(version, VersionReq::STAR),
92            Req(req) | Locked(_, req) | Precise(_, req) => Locked(version, req.clone()),
93        };
94    }
95
96    /// Makes the requirement precise to the requested version.
97    ///
98    /// This is used for the `--precise` field of `cargo update`.
99    pub fn precise_to(&mut self, version: &Version) {
100        use OptVersionReq::*;
101        let version = version.clone();
102        *self = match self {
103            Any => Precise(version, VersionReq::STAR),
104            Req(req) | Locked(_, req) | Precise(_, req) => Precise(version, req.clone()),
105        };
106    }
107
108    pub fn is_precise(&self) -> bool {
109        matches!(self, OptVersionReq::Precise(..))
110    }
111
112    /// Gets the version to which this req is precise to, if any.
113    pub fn precise_version(&self) -> Option<&Version> {
114        match self {
115            OptVersionReq::Precise(version, _) => Some(version),
116            _ => None,
117        }
118    }
119
120    pub fn is_locked(&self) -> bool {
121        matches!(self, OptVersionReq::Locked(..))
122    }
123
124    /// Gets the version to which this req is locked, if any.
125    pub fn locked_version(&self) -> Option<&Version> {
126        match self {
127            OptVersionReq::Locked(version, _) => Some(version),
128            _ => None,
129        }
130    }
131
132    /// Allows to match pre-release in SemVer-Compatible way.
133    /// See [`semver_eval_ext`] for `matches_prerelease` semantics.
134    pub fn matches_prerelease(&self, version: &Version) -> bool {
135        if let OptVersionReq::Req(req) = self {
136            return req.matches_prerelease(version);
137        } else {
138            return self.matches(version);
139        }
140    }
141
142    pub fn matches(&self, version: &Version) -> bool {
143        match self {
144            OptVersionReq::Any => true,
145            OptVersionReq::Req(req) => req.matches(version),
146            OptVersionReq::Locked(v, _) => {
147                // Generally, cargo is of the opinion that semver metadata should be ignored.
148                // If your registry has two versions that only differing metadata you get the bugs you deserve.
149                // We also believe that lock files should ensure reproducibility
150                // and protect against mutations from the registry.
151                // In this circumstance these two goals are in conflict, and we pick reproducibility.
152                // If the lock file tells us that there is a version called `1.0.0+bar` then
153                // we should not silently use `1.0.0+foo` even though they have the same version.
154                v == version
155            }
156            OptVersionReq::Precise(v, _) => {
157                // This is used for the `--precise` field of cargo update.
158                //
159                // Unfortunately crates.io allowed versions to differ only
160                // by build metadata. This shouldn't be allowed, but since
161                // it is, this will honor it if requested.
162                //
163                // In that context we treat a requirement that does not have
164                // build metadata as allowing any metadata. But, if a requirement
165                // has build metadata, then we only allow it to match the exact
166                // metadata.
167                v.major == version.major
168                    && v.minor == version.minor
169                    && v.patch == version.patch
170                    && v.pre == version.pre
171                    && (v.build == version.build || v.build.is_empty())
172            }
173        }
174    }
175}
176
177impl Display for OptVersionReq {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            OptVersionReq::Any => f.write_str("*"),
181            OptVersionReq::Req(req)
182            | OptVersionReq::Locked(_, req)
183            | OptVersionReq::Precise(_, req) => Display::fmt(req, f),
184        }
185    }
186}
187
188impl From<VersionReq> for OptVersionReq {
189    fn from(req: VersionReq) -> Self {
190        OptVersionReq::Req(req)
191    }
192}
193
194#[cfg(test)]
195mod matches_prerelease {
196    use semver::VersionReq;
197
198    use super::OptVersionReq;
199    use super::Version;
200
201    #[test]
202    fn prerelease() {
203        // As of the writing, this test is not the final semantic of pre-release
204        // semver matching. Part of the behavior is buggy. This test just tracks
205        // the current behavior of the unstable `--precise <prerelease>`.
206        //
207        // The below transformation proposed in the RFC is hard to implement
208        // outside the semver crate.
209        //
210        // ```
211        // >=1.2.3, <2.0.0 -> >=1.2.3, <2.0.0-0
212        // ```
213        //
214        // The upper bound semantic is also not resolved. So, at least two
215        // outstanding issues are required to be fixed before the stabilization:
216        //
217        // * Bug 1: `x.y.z-pre.0` shouldn't match `x.y.z`.
218        // * Upper bound: Whether `>=x.y.z-0, <x.y.z` should match `x.y.z-0`.
219        //
220        // See the RFC 3493 for the unresolved upper bound issue:
221        // https://rust-lang.github.io/rfcs/3493-precise-pre-release-cargo-update.html#version-ranges-with-pre-release-upper-bounds
222        let cases = [
223            //
224            ("1.2.3", "1.2.3-0", false),
225            ("1.2.3", "1.2.3-1", false),
226            ("1.2.3", "1.2.4-0", true),
227            //
228            (">=1.2.3", "1.2.3-0", false),
229            (">=1.2.3", "1.2.3-1", false),
230            (">=1.2.3", "1.2.4-0", true),
231            //
232            (">1.2.3", "1.2.3-0", false),
233            (">1.2.3", "1.2.3-1", false),
234            (">1.2.3", "1.2.4-0", true),
235            //
236            (">1.2.3, <1.2.4", "1.2.3-0", false),
237            (">1.2.3, <1.2.4", "1.2.3-1", false),
238            (">1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic
239            //
240            (">=1.2.3, <1.2.4", "1.2.3-0", false),
241            (">=1.2.3, <1.2.4", "1.2.3-1", false),
242            (">=1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic
243            //
244            (">1.2.3, <=1.2.4", "1.2.3-0", false),
245            (">1.2.3, <=1.2.4", "1.2.3-1", false),
246            (">1.2.3, <=1.2.4", "1.2.4-0", true),
247            //
248            (">=1.2.3-0, <1.2.3", "1.2.3-0", true), // upper bound semantic
249            (">=1.2.3-0, <1.2.3", "1.2.3-1", true), // upper bound semantic
250            (">=1.2.3-0, <1.2.3", "1.2.4-0", false),
251            //
252            ("1.2.3", "2.0.0-0", false), // upper bound semantics
253            ("=1.2.3-0", "1.2.3", false),
254            ("=1.2.3-0", "1.2.3-0", true),
255            ("=1.2.3-0", "1.2.4", false),
256            (">=1.2.3-2, <1.2.3-4", "1.2.3-0", false),
257            (">=1.2.3-2, <1.2.3-4", "1.2.3-3", true),
258            (">=1.2.3-2, <1.2.3-4", "1.2.3-5", false), // upper bound semantics
259        ];
260        for (req, ver, expected) in cases {
261            let version_req = req.parse().unwrap();
262            let version = ver.parse().unwrap();
263            let matched = OptVersionReq::Req(version_req).matches_prerelease(&version);
264            assert_eq!(expected, matched, "req: {req}; ver: {ver}");
265        }
266    }
267
268    #[test]
269    fn opt_version_req_matches_prerelease() {
270        let req_ver: VersionReq = "^1.2.3-rc.0".parse().unwrap();
271        let to_ver: Version = "1.2.3-rc.0".parse().unwrap();
272
273        let req = OptVersionReq::Req(req_ver.clone());
274        assert!(req.matches_prerelease(&to_ver));
275
276        let req = OptVersionReq::Locked(to_ver.clone(), req_ver.clone());
277        assert!(req.matches_prerelease(&to_ver));
278
279        let req = OptVersionReq::Precise(to_ver.clone(), req_ver.clone());
280        assert!(req.matches_prerelease(&to_ver));
281    }
282}