Skip to main content

composer_semver/
bound.rs

1//! Constraint bounds — port of `Composer\Semver\Constraint\Bound`.
2//!
3//! A [`Bound`] is one end of a version interval: a normalized version
4//! string plus an inclusivity flag. Composer derives the lower/upper
5//! bound of a constraint (`Constraint::getLowerBound` /
6//! `MultiConstraint::extractBounds`) and the autoloader's
7//! `platform_check.php` generator uses the *lowest* PHP bound across
8//! every `php` / `php-64bit` requirement to emit the
9//! `PHP_VERSION_ID >= N` guard.
10//!
11//! The two sentinels match Composer's `Bound::zero()` /
12//! `Bound::positiveInfinity()` byte-for-byte so [`Bound::is_zero`] /
13//! [`Bound::is_positive_infinity`] reproduce the upstream short-circuits.
14
15use crate::version::Version;
16use std::cmp::Ordering;
17
18/// PHP's `PHP_INT_MAX` on a 64-bit build — the version body Composer
19/// uses for the positive-infinity sentinel.
20const PHP_INT_MAX: &str = "9223372036854775807";
21
22/// One end of a version interval. Mirrors `Composer\Semver\Constraint\Bound`:
23/// a normalized version string and whether the endpoint itself is
24/// included in the interval.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Bound {
27    /// Normalized version string (what `Version::normalized` produces),
28    /// or one of the two sentinel bodies (`0.0.0.0-dev` /
29    /// `<PHP_INT_MAX>.0.0.0`).
30    version: String,
31    is_inclusive: bool,
32}
33
34impl Bound {
35    /// Construct a bound from a normalized version string.
36    #[must_use]
37    pub fn new(version: String, is_inclusive: bool) -> Self {
38        Self {
39            version,
40            is_inclusive,
41        }
42    }
43
44    /// Composer's `Bound::zero()` — `0.0.0.0-dev` inclusive. The lower
45    /// bound of any constraint with no real floor (`<X`, `!=X`, `*`).
46    #[must_use]
47    pub fn zero() -> Self {
48        Self {
49            version: "0.0.0.0-dev".to_owned(),
50            is_inclusive: true,
51        }
52    }
53
54    /// Composer's `Bound::positiveInfinity()` — `<PHP_INT_MAX>.0.0.0`
55    /// exclusive. The upper bound of any constraint with no real ceiling
56    /// (`>X`, `>=X`, `!=X`, `*`).
57    #[must_use]
58    pub fn positive_infinity() -> Self {
59        Self {
60            version: format!("{PHP_INT_MAX}.0.0.0"),
61            is_inclusive: false,
62        }
63    }
64
65    /// The normalized version body.
66    #[must_use]
67    pub fn version(&self) -> &str {
68        &self.version
69    }
70
71    /// Whether the endpoint is part of the interval.
72    #[must_use]
73    pub fn is_inclusive(&self) -> bool {
74        self.is_inclusive
75    }
76
77    /// Matches `Bound::isZero()`.
78    #[must_use]
79    pub fn is_zero(&self) -> bool {
80        self.version == "0.0.0.0-dev" && self.is_inclusive
81    }
82
83    /// Matches `Bound::isPositiveInfinity()`.
84    #[must_use]
85    pub fn is_positive_infinity(&self) -> bool {
86        self.version == format!("{PHP_INT_MAX}.0.0.0") && !self.is_inclusive
87    }
88
89    /// Port of `Bound::compareTo($other, $operator)` with `$operator`
90    /// expressed as `gt` (`true` → `'>'`, `false` → `'<'`). Returns
91    /// whether `self` lies strictly beyond `other` in the requested
92    /// direction; two structurally-equal bounds compare `false` (as in
93    /// Composer's `if ($this == $other) return false`).
94    #[must_use]
95    pub fn compare_to(&self, other: &Bound, gt: bool) -> bool {
96        if self == other {
97            return false;
98        }
99        match self.version_cmp(other) {
100            Ordering::Greater => gt,
101            Ordering::Less => !gt,
102            // Equal versions, differing inclusivity: Composer returns
103            // `$other->isInclusive()` for `'>'`, `!$other->isInclusive()`
104            // for `'<'`.
105            Ordering::Equal => {
106                if gt {
107                    other.is_inclusive
108                } else {
109                    !other.is_inclusive
110                }
111            }
112        }
113    }
114
115    /// Order the two endpoints purely by version body (ignoring
116    /// inclusivity), the way PHP's `version_compare` would for these
117    /// normalized strings. The sentinels parse like any other numeric
118    /// version (`0.0.0.0-dev` sorts below every real version; the
119    /// `PHP_INT_MAX` body sorts above), so a plain [`Version`]
120    /// comparison reproduces the upstream ordering.
121    #[must_use]
122    pub fn version_cmp(&self, other: &Bound) -> Ordering {
123        match (
124            Version::parse(&self.version),
125            Version::parse(&other.version),
126        ) {
127            (Ok(a), Ok(b)) => a.cmp(&b),
128            // Both sentinel bodies and every normalized version parse,
129            // so this fallback is unreachable in practice; lexical
130            // ordering keeps the function total.
131            _ => self.version.cmp(&other.version),
132        }
133    }
134}