Skip to main content

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