Skip to main content

khive_wire_protocol/
version.rs

1//! Protocol version and the compatibility policy from ADR-137's
2//! "Compatibility policy" section.
3
4use serde::{Deserialize, Serialize};
5
6/// A wire protocol version number.
7///
8/// Version numbers are monotonic. A breaking wire change — a new frame kind,
9/// a new wire error code, or a change to an existing frame's field shape that
10/// isn't backward-compatible — is never introduced within a version number;
11/// it requires incrementing this value.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ProtocolVersion(pub u32);
15
16impl ProtocolVersion {
17    pub const fn new(version: u32) -> Self {
18        Self(version)
19    }
20
21    pub const fn get(self) -> u32 {
22        self.0
23    }
24}
25
26impl From<u32> for ProtocolVersion {
27    fn from(value: u32) -> Self {
28        Self(value)
29    }
30}
31
32impl std::fmt::Display for ProtocolVersion {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38/// The current protocol version implemented by this crate.
39pub const CURRENT_VERSION: ProtocolVersion = ProtocolVersion(1);
40
41// Version 0 does not exist in this protocol: the range floor is 1
42// ([`SupportedVersions::new`] rejects a zero `min`). Pin the invariant at
43// compile time so a future edit setting `CURRENT_VERSION` to 0 fails the
44// build instead of silently producing an all-zero supported range.
45const _: () = assert!(CURRENT_VERSION.0 >= 1, "CURRENT_VERSION must be >= 1");
46
47/// The set of protocol versions a server built against this crate accepts.
48///
49/// ADR-137's compatibility policy requires a server to support the current
50/// version and at least the immediately prior version. This crate is
51/// currently at version 1, so there is no prior version yet; the lower bound
52/// saturates at 1 rather than underflowing to 0 (there is no version 0).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct SupportedVersions {
55    min: ProtocolVersion,
56    max: ProtocolVersion,
57}
58
59impl SupportedVersions {
60    /// The default policy: the current version and the immediately prior one.
61    pub const fn current() -> Self {
62        let max = CURRENT_VERSION.0;
63        let min = if max > 1 { max - 1 } else { 1 };
64        Self {
65            min: ProtocolVersion(min),
66            max: ProtocolVersion(max),
67        }
68    }
69
70    /// Construct an explicit `[min, max]` inclusive supported range within
71    /// the protocol grammar implemented by this crate.
72    ///
73    /// Fails with [`SupportedVersionsError`] rather than panicking on an
74    /// unusable range:
75    ///
76    /// - [`SupportedVersionsError::MinVersionZero`] if `min` is version 0 —
77    ///   version 0 does not exist in this protocol, and a range admitting
78    ///   it would accept handshakes no conforming client would send.
79    /// - [`SupportedVersionsError::InvertedRange`] if `min > max` — an
80    ///   inverted range would silently reject every handshake (no version
81    ///   satisfies it), which is always a configuration bug.
82    /// - [`SupportedVersionsError::MaxAboveCurrent`] if `max` exceeds
83    ///   [`CURRENT_VERSION`] — this crate cannot admit a version whose wire
84    ///   grammar it does not implement.
85    pub const fn new(
86        min: ProtocolVersion,
87        max: ProtocolVersion,
88    ) -> Result<Self, SupportedVersionsError> {
89        if min.0 < 1 {
90            return Err(SupportedVersionsError::MinVersionZero);
91        }
92        if min.0 > max.0 {
93            return Err(SupportedVersionsError::InvertedRange);
94        }
95        if max.0 > CURRENT_VERSION.0 {
96            return Err(SupportedVersionsError::MaxAboveCurrent);
97        }
98        Ok(Self { min, max })
99    }
100
101    pub const fn min(self) -> ProtocolVersion {
102        self.min
103    }
104
105    pub const fn max(self) -> ProtocolVersion {
106        self.max
107    }
108
109    pub const fn contains(self, version: ProtocolVersion) -> bool {
110        version.0 >= self.min.0 && version.0 <= self.max.0
111    }
112}
113
114impl Default for SupportedVersions {
115    fn default() -> Self {
116        Self::current()
117    }
118}
119
120/// Why an explicit [`SupportedVersions::new`] range was rejected.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
122pub enum SupportedVersionsError {
123    /// The range's lower bound was version 0; this protocol has no
124    /// version 0, and the floor is 1.
125    #[error("supported-version range floor must be >= 1 (version 0 does not exist)")]
126    MinVersionZero,
127    /// The range's lower bound exceeded its upper bound; no version
128    /// satisfies it, so it would reject every handshake.
129    #[error("supported-version range is inverted: min must not exceed max")]
130    InvertedRange,
131    /// The range's upper bound is newer than the grammar implemented by this
132    /// crate.
133    #[error("supported-version range max must not exceed the current protocol version")]
134    MaxAboveCurrent,
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn new_accepts_the_current_range() {
143        let supported =
144            SupportedVersions::new(ProtocolVersion::new(1), ProtocolVersion::new(1)).unwrap();
145        assert_eq!(supported.min(), ProtocolVersion::new(1));
146        assert_eq!(supported.max(), ProtocolVersion::new(1));
147    }
148
149    #[test]
150    fn new_rejects_a_range_above_current_version() {
151        for (min, max) in [(1, 2), (2, 2)] {
152            let err = SupportedVersions::new(ProtocolVersion::new(min), ProtocolVersion::new(max))
153                .unwrap_err();
154            assert_eq!(err, SupportedVersionsError::MaxAboveCurrent);
155        }
156    }
157
158    #[test]
159    fn new_rejects_an_inverted_range() {
160        let err =
161            SupportedVersions::new(ProtocolVersion::new(5), ProtocolVersion::new(2)).unwrap_err();
162        assert_eq!(err, SupportedVersionsError::InvertedRange);
163    }
164
165    #[test]
166    fn new_rejects_a_zero_min_version() {
167        // Version 0 does not exist in this protocol; the floor is 1.
168        // Checked before the ordering check, so a doubly-broken range
169        // reports the zero floor first.
170        let err =
171            SupportedVersions::new(ProtocolVersion::new(0), ProtocolVersion::new(1)).unwrap_err();
172        assert_eq!(err, SupportedVersionsError::MinVersionZero);
173        let err =
174            SupportedVersions::new(ProtocolVersion::new(0), ProtocolVersion::new(0)).unwrap_err();
175        assert_eq!(err, SupportedVersionsError::MinVersionZero);
176    }
177
178    #[test]
179    fn current_version_is_at_least_one() {
180        // The test mirror of the compile-time `const _: () = assert!` in
181        // this module: the invariant gets an explicit, greppable failure
182        // either way.
183        assert!(CURRENT_VERSION.get() >= 1);
184    }
185
186    #[test]
187    fn contains_is_inclusive_at_both_bounds() {
188        // Boundary inclusion: min and max themselves are supported; one
189        // below min and one above max are not.
190        let supported =
191            SupportedVersions::new(ProtocolVersion::new(1), ProtocolVersion::new(1)).unwrap();
192        assert!(!supported.contains(ProtocolVersion::new(0)));
193        assert!(supported.contains(ProtocolVersion::new(1)));
194        assert!(!supported.contains(ProtocolVersion::new(2)));
195    }
196
197    #[test]
198    fn current_saturates_at_version_one() {
199        // The crate is at version 1, so there is no prior version and the
200        // default range is [1, 1].
201        let supported = SupportedVersions::current();
202        assert_eq!(supported.min(), ProtocolVersion::new(1));
203        assert_eq!(supported.max(), ProtocolVersion::new(1));
204    }
205}