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}