blob: 9c19eb301301a9c6b593f555f968663e4353cfb1 [file] [edit]
use super::semver_eval_ext;
use semver::{Comparator, Op, Version, VersionReq};
use std::fmt::{self, Display};
pub trait VersionExt {
fn is_prerelease(&self) -> bool;
fn to_req(&self, op: Op) -> VersionReq;
fn to_exact_req(&self) -> VersionReq {
self.to_req(Op::Exact)
}
fn to_caret_req(&self) -> VersionReq {
self.to_req(Op::Caret)
}
}
impl VersionExt for Version {
fn is_prerelease(&self) -> bool {
!self.pre.is_empty()
}
fn to_req(&self, op: Op) -> VersionReq {
VersionReq {
comparators: vec![Comparator {
op,
major: self.major,
minor: Some(self.minor),
patch: Some(self.patch),
pre: self.pre.clone(),
}],
}
}
}
pub trait VersionReqExt {
fn matches_prerelease(&self, version: &Version) -> bool;
}
impl VersionReqExt for VersionReq {
fn matches_prerelease(&self, version: &Version) -> bool {
semver_eval_ext::matches_prerelease(self, version)
}
}
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub enum OptVersionReq {
Any,
Req(VersionReq),
/// The exact locked version and the original version requirement.
Locked(Version, VersionReq),
/// The exact requested version and the original version requirement.
///
/// This looks identical to [`OptVersionReq::Locked`] but has a different
/// meaning, and is used for the `--precise` field of `cargo update`.
/// See comments in [`OptVersionReq::matches`] for more.
Precise(Version, VersionReq),
}
impl OptVersionReq {
pub fn exact(version: &Version) -> Self {
OptVersionReq::Req(version.to_exact_req())
}
// Since some registries have allowed crate versions to differ only by build metadata,
// A query using OptVersionReq::exact return nondeterministic results.
// So we `lock_to` the exact version were interested in.
pub fn lock_to_exact(version: &Version) -> Self {
OptVersionReq::Locked(version.clone(), version.to_exact_req())
}
pub fn is_exact(&self) -> bool {
match self {
OptVersionReq::Any => false,
OptVersionReq::Req(req) | OptVersionReq::Precise(_, req) => {
req.comparators.len() == 1 && {
let cmp = &req.comparators[0];
cmp.op == Op::Exact && cmp.minor.is_some() && cmp.patch.is_some()
}
}
OptVersionReq::Locked(..) => true,
}
}
pub fn lock_to(&mut self, version: &Version) {
assert!(self.matches(version), "cannot lock {} to {}", self, version);
use OptVersionReq::*;
let version = version.clone();
*self = match self {
Any => Locked(version, VersionReq::STAR),
Req(req) | Locked(_, req) | Precise(_, req) => Locked(version, req.clone()),
};
}
/// Makes the requirement precise to the requested version.
///
/// This is used for the `--precise` field of `cargo update`.
pub fn precise_to(&mut self, version: &Version) {
use OptVersionReq::*;
let version = version.clone();
*self = match self {
Any => Precise(version, VersionReq::STAR),
Req(req) | Locked(_, req) | Precise(_, req) => Precise(version, req.clone()),
};
}
pub fn is_precise(&self) -> bool {
matches!(self, OptVersionReq::Precise(..))
}
/// Gets the version to which this req is precise to, if any.
pub fn precise_version(&self) -> Option<&Version> {
match self {
OptVersionReq::Precise(version, _) => Some(version),
_ => None,
}
}
pub fn is_locked(&self) -> bool {
matches!(self, OptVersionReq::Locked(..))
}
/// Gets the version to which this req is locked, if any.
pub fn locked_version(&self) -> Option<&Version> {
match self {
OptVersionReq::Locked(version, _) => Some(version),
_ => None,
}
}
/// Allows to match pre-release in SemVer-Compatible way.
/// See [`semver_eval_ext`] for `matches_prerelease` semantics.
pub fn matches_prerelease(&self, version: &Version) -> bool {
if let OptVersionReq::Req(req) = self {
return req.matches_prerelease(version);
} else {
return self.matches(version);
}
}
pub fn matches(&self, version: &Version) -> bool {
match self {
OptVersionReq::Any => true,
OptVersionReq::Req(req) => req.matches(version),
OptVersionReq::Locked(v, _) => {
// Generally, cargo is of the opinion that semver metadata should be ignored.
// If your registry has two versions that only differing metadata you get the bugs you deserve.
// We also believe that lock files should ensure reproducibility
// and protect against mutations from the registry.
// In this circumstance these two goals are in conflict, and we pick reproducibility.
// If the lock file tells us that there is a version called `1.0.0+bar` then
// we should not silently use `1.0.0+foo` even though they have the same version.
v == version
}
OptVersionReq::Precise(v, _) => {
// This is used for the `--precise` field of cargo update.
//
// Unfortunately crates.io allowed versions to differ only
// by build metadata. This shouldn't be allowed, but since
// it is, this will honor it if requested.
//
// In that context we treat a requirement that does not have
// build metadata as allowing any metadata. But, if a requirement
// has build metadata, then we only allow it to match the exact
// metadata.
v.major == version.major
&& v.minor == version.minor
&& v.patch == version.patch
&& v.pre == version.pre
&& (v.build == version.build || v.build.is_empty())
}
}
}
}
impl Display for OptVersionReq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OptVersionReq::Any => f.write_str("*"),
OptVersionReq::Req(req)
| OptVersionReq::Locked(_, req)
| OptVersionReq::Precise(_, req) => Display::fmt(req, f),
}
}
}
impl From<VersionReq> for OptVersionReq {
fn from(req: VersionReq) -> Self {
OptVersionReq::Req(req)
}
}
#[cfg(test)]
mod matches_prerelease {
use semver::VersionReq;
use super::OptVersionReq;
use super::Version;
#[test]
fn prerelease() {
// As of the writing, this test is not the final semantic of pre-release
// semver matching. Part of the behavior is buggy. This test just tracks
// the current behavior of the unstable `--precise <prerelease>`.
//
// The below transformation proposed in the RFC is hard to implement
// outside the semver crate.
//
// ```
// >=1.2.3, <2.0.0 -> >=1.2.3, <2.0.0-0
// ```
//
// The upper bound semantic is also not resolved. So, at least two
// outstanding issues are required to be fixed before the stabilization:
//
// * Bug 1: `x.y.z-pre.0` shouldn't match `x.y.z`.
// * Upper bound: Whether `>=x.y.z-0, <x.y.z` should match `x.y.z-0`.
//
// See the RFC 3493 for the unresolved upper bound issue:
// https://rust-lang.github.io/rfcs/3493-precise-pre-release-cargo-update.html#version-ranges-with-pre-release-upper-bounds
let cases = [
//
("1.2.3", "1.2.3-0", false),
("1.2.3", "1.2.3-1", false),
("1.2.3", "1.2.4-0", true),
//
(">=1.2.3", "1.2.3-0", false),
(">=1.2.3", "1.2.3-1", false),
(">=1.2.3", "1.2.4-0", true),
//
(">1.2.3", "1.2.3-0", false),
(">1.2.3", "1.2.3-1", false),
(">1.2.3", "1.2.4-0", true),
//
(">1.2.3, <1.2.4", "1.2.3-0", false),
(">1.2.3, <1.2.4", "1.2.3-1", false),
(">1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic
//
(">=1.2.3, <1.2.4", "1.2.3-0", false),
(">=1.2.3, <1.2.4", "1.2.3-1", false),
(">=1.2.3, <1.2.4", "1.2.4-0", false), // upper bound semantic
//
(">1.2.3, <=1.2.4", "1.2.3-0", false),
(">1.2.3, <=1.2.4", "1.2.3-1", false),
(">1.2.3, <=1.2.4", "1.2.4-0", true),
//
(">=1.2.3-0, <1.2.3", "1.2.3-0", true), // upper bound semantic
(">=1.2.3-0, <1.2.3", "1.2.3-1", true), // upper bound semantic
(">=1.2.3-0, <1.2.3", "1.2.4-0", false),
//
("1.2.3", "2.0.0-0", false), // upper bound semantics
("=1.2.3-0", "1.2.3", false),
("=1.2.3-0", "1.2.3-0", true),
("=1.2.3-0", "1.2.4", false),
(">=1.2.3-2, <1.2.3-4", "1.2.3-0", false),
(">=1.2.3-2, <1.2.3-4", "1.2.3-3", true),
(">=1.2.3-2, <1.2.3-4", "1.2.3-5", false), // upper bound semantics
];
for (req, ver, expected) in cases {
let version_req = req.parse().unwrap();
let version = ver.parse().unwrap();
let matched = OptVersionReq::Req(version_req).matches_prerelease(&version);
assert_eq!(expected, matched, "req: {req}; ver: {ver}");
}
}
#[test]
fn opt_version_req_matches_prerelease() {
let req_ver: VersionReq = "^1.2.3-rc.0".parse().unwrap();
let to_ver: Version = "1.2.3-rc.0".parse().unwrap();
let req = OptVersionReq::Req(req_ver.clone());
assert!(req.matches_prerelease(&to_ver));
let req = OptVersionReq::Locked(to_ver.clone(), req_ver.clone());
assert!(req.matches_prerelease(&to_ver));
let req = OptVersionReq::Precise(to_ver.clone(), req_ver.clone());
assert!(req.matches_prerelease(&to_ver));
}
}