#
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}