use bytes::{Buf, BufMut};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubprotocolVersion {
pub major: u8,
pub minor: u8,
}
impl SubprotocolVersion {
pub const fn new(major: u8, minor: u8) -> Self {
Self { major, minor }
}
#[inline]
pub fn satisfies(self, min_required: Self) -> bool {
self >= min_required
}
#[inline]
pub fn to_bytes(self) -> [u8; 2] {
[self.major, self.minor]
}
#[inline]
pub fn from_bytes(data: &[u8; 2]) -> Self {
Self {
major: data[0],
minor: data[1],
}
}
}
impl std::fmt::Display for SubprotocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Debug, Clone)]
pub struct SubprotocolDescriptor {
pub id: u16,
pub name: String,
pub version: SubprotocolVersion,
pub min_compatible: SubprotocolVersion,
pub handler_present: bool,
}
impl SubprotocolDescriptor {
pub fn new(id: u16, name: impl Into<String>, version: SubprotocolVersion) -> Self {
Self {
id,
name: name.into(),
version,
min_compatible: version,
handler_present: true,
}
}
pub fn with_min_compatible(mut self, min: SubprotocolVersion) -> Self {
self.min_compatible = if min > self.version {
self.version
} else {
min
};
self
}
pub fn forwarding_only(mut self) -> Self {
self.handler_present = false;
self
}
pub fn is_compatible_with(&self, other: &Self) -> bool {
self.id == other.id
&& self.version.satisfies(other.min_compatible)
&& other.version.satisfies(self.min_compatible)
}
pub fn capability_tag(&self) -> String {
format!("subprotocol:{:#06x}", self.id)
}
}
impl std::fmt::Display for SubprotocolDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({:#06x}) v{}", self.name, self.id, self.version)
}
}
pub const MANIFEST_ENTRY_SIZE: usize = 6;
pub fn write_manifest_entry(desc: &SubprotocolDescriptor, buf: &mut impl BufMut) {
buf.put_u16_le(desc.id);
buf.put_slice(&desc.version.to_bytes());
buf.put_slice(&desc.min_compatible.to_bytes());
}
pub fn read_manifest_entry(
buf: &mut impl Buf,
) -> Option<(u16, SubprotocolVersion, SubprotocolVersion)> {
if buf.remaining() < MANIFEST_ENTRY_SIZE {
return None;
}
let id = buf.get_u16_le();
let version = SubprotocolVersion::new(buf.get_u8(), buf.get_u8());
let min_compat = SubprotocolVersion::new(buf.get_u8(), buf.get_u8());
if min_compat > version {
return None;
}
Some((id, version, min_compat))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_ordering() {
let v1_0 = SubprotocolVersion::new(1, 0);
let v1_1 = SubprotocolVersion::new(1, 1);
let v2_0 = SubprotocolVersion::new(2, 0);
assert!(v1_0 < v1_1);
assert!(v1_1 < v2_0);
assert!(v1_0 < v2_0);
}
#[test]
fn test_version_satisfies() {
let v1_0 = SubprotocolVersion::new(1, 0);
let v1_1 = SubprotocolVersion::new(1, 1);
let v2_0 = SubprotocolVersion::new(2, 0);
assert!(v1_1.satisfies(v1_0)); assert!(v2_0.satisfies(v1_0)); assert!(!v1_0.satisfies(v1_1)); }
#[test]
fn test_descriptor_compatibility() {
let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 1))
.with_min_compatible(SubprotocolVersion::new(1, 0));
let b = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
assert!(a.is_compatible_with(&b)); }
#[test]
fn test_descriptor_incompatible() {
let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(2, 0))
.with_min_compatible(SubprotocolVersion::new(2, 0));
let b = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
assert!(!a.is_compatible_with(&b)); }
#[test]
fn test_descriptor_different_id() {
let a = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
let b = SubprotocolDescriptor::new(0x0500, "migration", SubprotocolVersion::new(1, 0));
assert!(!a.is_compatible_with(&b));
}
#[test]
fn test_capability_tag() {
let d = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
assert_eq!(d.capability_tag(), "subprotocol:0x0400");
}
#[test]
fn test_manifest_entry_roundtrip() {
let desc = SubprotocolDescriptor::new(0x1234, "test", SubprotocolVersion::new(3, 7))
.with_min_compatible(SubprotocolVersion::new(2, 0));
let mut buf = Vec::new();
write_manifest_entry(&desc, &mut buf);
assert_eq!(buf.len(), MANIFEST_ENTRY_SIZE);
let mut cursor = &buf[..];
let (id, version, min_compat) = read_manifest_entry(&mut cursor).unwrap();
assert_eq!(id, 0x1234);
assert_eq!(version, SubprotocolVersion::new(3, 7));
assert_eq!(min_compat, SubprotocolVersion::new(2, 0));
}
#[test]
fn test_version_display() {
assert_eq!(format!("{}", SubprotocolVersion::new(1, 2)), "1.2");
}
#[test]
fn test_descriptor_display() {
let d = SubprotocolDescriptor::new(0x0400, "causal", SubprotocolVersion::new(1, 0));
assert_eq!(format!("{}", d), "causal(0x0400) v1.0");
}
#[test]
fn read_manifest_entry_rejects_min_compatible_above_version() {
let mut buf = Vec::new();
buf.extend_from_slice(&0x1234u16.to_le_bytes()); buf.extend_from_slice(&[1, 0]); buf.extend_from_slice(&[255, 255]);
let mut cursor = &buf[..];
let parsed = read_manifest_entry(&mut cursor);
assert!(
parsed.is_none(),
"read_manifest_entry must reject min_compat > version",
);
}
#[test]
fn read_manifest_entry_accepts_min_compatible_equal_to_version() {
let mut buf = Vec::new();
buf.extend_from_slice(&0x4242u16.to_le_bytes());
buf.extend_from_slice(&[3, 7]);
buf.extend_from_slice(&[3, 7]);
let mut cursor = &buf[..];
let (id, version, min_compat) =
read_manifest_entry(&mut cursor).expect("equal min_compat must be accepted");
assert_eq!(id, 0x4242);
assert_eq!(version, SubprotocolVersion::new(3, 7));
assert_eq!(min_compat, SubprotocolVersion::new(3, 7));
}
#[test]
fn with_min_compatible_clamps_to_version() {
let desc = SubprotocolDescriptor::new(0x1000, "x", SubprotocolVersion::new(1, 0))
.with_min_compatible(SubprotocolVersion::new(2, 5));
assert_eq!(
desc.min_compatible,
SubprotocolVersion::new(1, 0),
"with_min_compatible must clamp to self.version",
);
}
#[test]
fn with_min_compatible_preserves_lower_floor() {
let desc = SubprotocolDescriptor::new(0x1000, "x", SubprotocolVersion::new(2, 5))
.with_min_compatible(SubprotocolVersion::new(1, 0));
assert_eq!(desc.min_compatible, SubprotocolVersion::new(1, 0));
}
}