use crate::WireError;
use crate::codec::{WireDecode, WireEncode, read_u32_le};
use crate::primitives::identifier::WireName;
use bytes::{BufMut, BytesMut};
const COMPONENT_BITS: u32 = 10;
const COMPONENT_MAX: u32 = (1 << COMPONENT_BITS) - 1;
const PATCH_MASK: u32 = COMPONENT_MAX;
pub const IGGY_PROTOCOL_VERSION: u32 = pack_protocol_version(0, 12, 0);
pub const IGGY_PROTOCOL_VERSION_MIN: u32 = pack_protocol_version(0, 12, 0);
const _: () =
assert!(IGGY_PROTOCOL_VERSION_MIN > 0 && IGGY_PROTOCOL_VERSION_MIN <= IGGY_PROTOCOL_VERSION);
#[must_use]
pub const fn is_protocol_compatible(client: u32) -> bool {
client >= IGGY_PROTOCOL_VERSION_MIN && client <= IGGY_PROTOCOL_VERSION
}
#[must_use]
pub const fn pack_protocol_version(major: u32, minor: u32, patch: u32) -> u32 {
assert!(
major <= COMPONENT_MAX && minor <= COMPONENT_MAX && patch <= COMPONENT_MAX,
"semver component exceeds 10-bit packing range"
);
(major << (2 * COMPONENT_BITS)) | (minor << COMPONENT_BITS) | patch
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProtocolVersion(pub u32);
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}.{}.{}",
self.0 >> (2 * COMPONENT_BITS),
(self.0 >> COMPONENT_BITS) & COMPONENT_MAX,
self.0 & PATCH_MASK
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientVersionInfo {
pub protocol_version: u32,
pub sdk_name: WireName,
pub sdk_version: WireName,
}
impl WireEncode for ClientVersionInfo {
fn encoded_size(&self) -> usize {
4 + self.sdk_name.encoded_size() + self.sdk_version.encoded_size()
}
fn encode(&self, buf: &mut BytesMut) {
buf.put_u32_le(self.protocol_version);
self.sdk_name.encode(buf);
self.sdk_version.encode(buf);
}
}
impl WireDecode for ClientVersionInfo {
fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
let protocol_version = read_u32_le(buf, 0)?;
let mut pos = 4;
let (sdk_name, sdk_name_len) = WireName::decode(&buf[pos..])?;
pos += sdk_name_len;
let (sdk_version, sdk_version_len) = WireName::decode(&buf[pos..])?;
pos += sdk_version_len;
Ok((
Self {
protocol_version,
sdk_name,
sdk_version,
},
pos,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> ClientVersionInfo {
ClientVersionInfo {
protocol_version: IGGY_PROTOCOL_VERSION,
sdk_name: WireName::new("rust-sdk").unwrap(),
sdk_version: WireName::new("0.10.1").unwrap(),
}
}
#[test]
fn roundtrip() {
let info = sample();
let bytes = info.to_bytes();
let (decoded, consumed) = ClientVersionInfo::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, info);
}
#[test]
fn encoded_size_matches_output() {
let info = sample();
assert_eq!(info.encoded_size(), info.to_bytes().len());
}
#[test]
fn truncated_returns_error() {
let bytes = sample().to_bytes();
for i in 0..bytes.len() {
assert!(
ClientVersionInfo::decode(&bytes[..i]).is_err(),
"expected error for truncation at byte {i}"
);
}
}
#[test]
fn wire_layout_protocol_version_first() {
let bytes = sample().to_bytes();
assert_eq!(
u32::from_le_bytes(bytes[..4].try_into().unwrap()),
IGGY_PROTOCOL_VERSION
);
assert_eq!(bytes[4], 8); assert_eq!(&bytes[5..13], b"rust-sdk");
}
#[test]
fn login_from_a_per_resource_update_protocol_is_outside_the_compatible_window() {
assert!(!is_protocol_compatible(pack_protocol_version(0, 11, 0)));
assert!(!is_protocol_compatible(pack_protocol_version(0, 11, 1)));
assert!(is_protocol_compatible(pack_protocol_version(0, 12, 0)));
assert!(!is_protocol_compatible(pack_protocol_version(0, 12, 1)));
}
#[test]
fn packing_preserves_semver_order() {
assert!(pack_protocol_version(0, 9, 999) < pack_protocol_version(0, 10, 0));
assert!(pack_protocol_version(0, 10, 1) < pack_protocol_version(1, 0, 0));
}
#[test]
fn min_requires_the_generic_update_wire_layout() {
assert_eq!(IGGY_PROTOCOL_VERSION_MIN, pack_protocol_version(0, 12, 0));
}
#[test]
fn compatibility_range_boundaries() {
assert!(is_protocol_compatible(IGGY_PROTOCOL_VERSION_MIN));
assert!(is_protocol_compatible(IGGY_PROTOCOL_VERSION));
assert!(
!is_protocol_compatible(IGGY_PROTOCOL_VERSION + 1),
"a newer patch must not pass the current release's protocol gate"
);
assert!(!is_protocol_compatible(IGGY_PROTOCOL_VERSION_MIN - 1));
assert!(!is_protocol_compatible(u32::MAX));
}
#[test]
fn protocol_version_display() {
assert_eq!(
ProtocolVersion(pack_protocol_version(0, 10, 1)).to_string(),
"0.10.1"
);
}
}