Skip to main content

crdt_kit/
version.rs

1//! Versioned serialization for CRDTs.
2//!
3//! Every CRDT type can be serialized with a version envelope that enables
4//! transparent migration when schemas evolve.
5
6use alloc::vec::Vec;
7use core::fmt;
8
9/// Magic byte identifying crdt-kit serialized data.
10pub const MAGIC_BYTE: u8 = 0xCF;
11
12/// Size of the version envelope header in bytes.
13pub const ENVELOPE_HEADER_SIZE: usize = 3;
14
15/// Identifies the type of CRDT for the version envelope.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[repr(u8)]
18pub enum CrdtType {
19    /// Grow-only counter.
20    GCounter = 1,
21    /// Positive-negative counter.
22    PNCounter = 2,
23    /// Grow-only set.
24    GSet = 3,
25    /// Two-phase set.
26    TwoPSet = 4,
27    /// Last-writer-wins register.
28    LWWRegister = 5,
29    /// Multi-value register.
30    MVRegister = 6,
31    /// Observed-remove set.
32    ORSet = 7,
33    /// Replicated Growable Array.
34    Rga = 8,
35    /// Collaborative text.
36    TextCrdt = 9,
37    /// Last-writer-wins map.
38    LWWMap = 10,
39    /// Add-wins map.
40    AWMap = 11,
41}
42
43impl CrdtType {
44    /// Convert from a raw byte.
45    pub fn from_byte(b: u8) -> Option<Self> {
46        match b {
47            1 => Some(Self::GCounter),
48            2 => Some(Self::PNCounter),
49            3 => Some(Self::GSet),
50            4 => Some(Self::TwoPSet),
51            5 => Some(Self::LWWRegister),
52            6 => Some(Self::MVRegister),
53            7 => Some(Self::ORSet),
54            8 => Some(Self::Rga),
55            9 => Some(Self::TextCrdt),
56            10 => Some(Self::LWWMap),
57            11 => Some(Self::AWMap),
58            _ => None,
59        }
60    }
61}
62
63/// Trait for CRDT types that support versioned serialization.
64///
65/// Types implementing this trait can be serialized with a 3-byte version
66/// envelope, enabling automatic migration when data schemas change.
67pub trait Versioned: Sized {
68    /// Current schema version for this CRDT type's serialization format.
69    const CURRENT_VERSION: u8;
70
71    /// The CRDT type identifier for the envelope.
72    const CRDT_TYPE: CrdtType;
73}
74
75/// Error during versioned serialization.
76#[derive(Debug, Clone)]
77pub enum VersionError {
78    /// Serialization failed.
79    Serialize(alloc::string::String),
80    /// Deserialization failed.
81    Deserialize(alloc::string::String),
82}
83
84impl fmt::Display for VersionError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::Serialize(msg) => write!(f, "serialization error: {msg}"),
88            Self::Deserialize(msg) => write!(f, "deserialization error: {msg}"),
89        }
90    }
91}
92
93#[cfg(feature = "std")]
94impl std::error::Error for VersionError {}
95
96// ── Versioned Envelope ─────────────────────────────────────────────
97
98/// Error parsing a version envelope.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum EnvelopeError {
101    /// Data is too short to contain a valid envelope.
102    TooShort,
103    /// Missing or incorrect magic byte.
104    InvalidMagic(u8),
105    /// Unknown CRDT type byte.
106    UnknownCrdtType(u8),
107}
108
109impl fmt::Display for EnvelopeError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::TooShort => write!(f, "data too short for version envelope"),
113            Self::InvalidMagic(b) => write!(f, "invalid magic byte: 0x{b:02X}, expected 0xCF"),
114            Self::UnknownCrdtType(b) => write!(f, "unknown CRDT type: {b}"),
115        }
116    }
117}
118
119#[cfg(feature = "std")]
120impl std::error::Error for EnvelopeError {}
121
122/// A version envelope wrapping serialized CRDT data.
123///
124/// Binary format (3 bytes overhead):
125/// ```text
126/// [MAGIC: 0xCF][VERSION: u8][CRDT_TYPE: u8][PAYLOAD: N bytes]
127/// ```
128///
129/// # Example
130///
131/// ```
132/// use crdt_kit::version::{VersionedEnvelope, CrdtType};
133///
134/// let data = b"some serialized crdt state";
135/// let envelope = VersionedEnvelope::new(1, CrdtType::GCounter, data.to_vec());
136///
137/// let bytes = envelope.to_bytes();
138/// let decoded = VersionedEnvelope::from_bytes(&bytes).unwrap();
139///
140/// assert_eq!(decoded.version, 1);
141/// assert_eq!(decoded.crdt_type, CrdtType::GCounter);
142/// assert_eq!(decoded.payload, data);
143/// ```
144#[derive(Debug, Clone, PartialEq)]
145pub struct VersionedEnvelope {
146    /// Schema version of the payload.
147    pub version: u8,
148    /// Type of CRDT contained.
149    pub crdt_type: CrdtType,
150    /// Serialized CRDT data.
151    pub payload: Vec<u8>,
152}
153
154impl VersionedEnvelope {
155    /// Create a new envelope.
156    pub fn new(version: u8, crdt_type: CrdtType, payload: Vec<u8>) -> Self {
157        Self {
158            version,
159            crdt_type,
160            payload,
161        }
162    }
163
164    /// Serialize the envelope to bytes.
165    #[must_use]
166    pub fn to_bytes(&self) -> Vec<u8> {
167        let mut bytes = Vec::with_capacity(ENVELOPE_HEADER_SIZE + self.payload.len());
168        bytes.push(MAGIC_BYTE);
169        bytes.push(self.version);
170        bytes.push(self.crdt_type as u8);
171        bytes.extend_from_slice(&self.payload);
172        bytes
173    }
174
175    /// Parse an envelope from bytes.
176    pub fn from_bytes(data: &[u8]) -> Result<Self, EnvelopeError> {
177        if data.len() < ENVELOPE_HEADER_SIZE {
178            return Err(EnvelopeError::TooShort);
179        }
180        if data[0] != MAGIC_BYTE {
181            return Err(EnvelopeError::InvalidMagic(data[0]));
182        }
183        let version = data[1];
184        let crdt_type =
185            CrdtType::from_byte(data[2]).ok_or(EnvelopeError::UnknownCrdtType(data[2]))?;
186        let payload = data[ENVELOPE_HEADER_SIZE..].to_vec();
187        Ok(Self {
188            version,
189            crdt_type,
190            payload,
191        })
192    }
193
194    /// Peek at the version without fully parsing the envelope.
195    pub fn peek_version(data: &[u8]) -> Result<u8, EnvelopeError> {
196        if data.len() < 2 {
197            return Err(EnvelopeError::TooShort);
198        }
199        if data[0] != MAGIC_BYTE {
200            return Err(EnvelopeError::InvalidMagic(data[0]));
201        }
202        Ok(data[1])
203    }
204
205    /// Check if bytes look like a versioned envelope (starts with magic byte).
206    #[must_use]
207    pub fn is_versioned(data: &[u8]) -> bool {
208        data.first() == Some(&MAGIC_BYTE)
209    }
210}
211
212// --- Versioned implementations for all 11 CRDT types ---
213
214impl Versioned for crate::GCounter {
215    const CURRENT_VERSION: u8 = 1;
216    const CRDT_TYPE: CrdtType = CrdtType::GCounter;
217}
218
219impl Versioned for crate::PNCounter {
220    const CURRENT_VERSION: u8 = 1;
221    const CRDT_TYPE: CrdtType = CrdtType::PNCounter;
222}
223
224impl<T: Ord + Clone> Versioned for crate::GSet<T> {
225    const CURRENT_VERSION: u8 = 1;
226    const CRDT_TYPE: CrdtType = CrdtType::GSet;
227}
228
229impl<T: Ord + Clone> Versioned for crate::TwoPSet<T> {
230    const CURRENT_VERSION: u8 = 1;
231    const CRDT_TYPE: CrdtType = CrdtType::TwoPSet;
232}
233
234impl<T: Clone> Versioned for crate::LWWRegister<T> {
235    const CURRENT_VERSION: u8 = 1;
236    const CRDT_TYPE: CrdtType = CrdtType::LWWRegister;
237}
238
239impl<T: Clone + Ord> Versioned for crate::MVRegister<T> {
240    const CURRENT_VERSION: u8 = 1;
241    const CRDT_TYPE: CrdtType = CrdtType::MVRegister;
242}
243
244impl<T: Ord + Clone> Versioned for crate::ORSet<T> {
245    const CURRENT_VERSION: u8 = 1;
246    const CRDT_TYPE: CrdtType = CrdtType::ORSet;
247}
248
249impl<T: Clone + Ord> Versioned for crate::Rga<T> {
250    const CURRENT_VERSION: u8 = 1;
251    const CRDT_TYPE: CrdtType = CrdtType::Rga;
252}
253
254impl Versioned for crate::TextCrdt {
255    const CURRENT_VERSION: u8 = 1;
256    const CRDT_TYPE: CrdtType = CrdtType::TextCrdt;
257}
258
259impl<K: Ord + Clone, V: Clone> Versioned for crate::LWWMap<K, V> {
260    const CURRENT_VERSION: u8 = 1;
261    const CRDT_TYPE: CrdtType = CrdtType::LWWMap;
262}
263
264impl<K: Ord + Clone, V: Clone + Eq> Versioned for crate::AWMap<K, V> {
265    const CURRENT_VERSION: u8 = 1;
266    const CRDT_TYPE: CrdtType = CrdtType::AWMap;
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn envelope_roundtrip() {
275        let original = VersionedEnvelope::new(3, CrdtType::ORSet, b"test-payload".to_vec());
276        let bytes = original.to_bytes();
277        let decoded = VersionedEnvelope::from_bytes(&bytes).unwrap();
278        assert_eq!(original, decoded);
279    }
280
281    #[test]
282    fn envelope_header_size() {
283        let envelope = VersionedEnvelope::new(1, CrdtType::GCounter, vec![]);
284        let bytes = envelope.to_bytes();
285        assert_eq!(bytes.len(), ENVELOPE_HEADER_SIZE);
286    }
287
288    #[test]
289    fn envelope_peek_version() {
290        let envelope = VersionedEnvelope::new(42, CrdtType::TextCrdt, b"data".to_vec());
291        let bytes = envelope.to_bytes();
292        assert_eq!(VersionedEnvelope::peek_version(&bytes).unwrap(), 42);
293    }
294
295    #[test]
296    fn envelope_is_versioned() {
297        assert!(VersionedEnvelope::is_versioned(&[MAGIC_BYTE, 1, 1]));
298        assert!(!VersionedEnvelope::is_versioned(&[0x00, 1, 1]));
299        assert!(!VersionedEnvelope::is_versioned(&[]));
300    }
301
302    #[test]
303    fn envelope_error_too_short() {
304        assert_eq!(
305            VersionedEnvelope::from_bytes(&[MAGIC_BYTE]),
306            Err(EnvelopeError::TooShort)
307        );
308    }
309
310    #[test]
311    fn envelope_error_invalid_magic() {
312        assert_eq!(
313            VersionedEnvelope::from_bytes(&[0xAB, 1, 1]),
314            Err(EnvelopeError::InvalidMagic(0xAB))
315        );
316    }
317
318    #[test]
319    fn envelope_error_unknown_crdt_type() {
320        assert_eq!(
321            VersionedEnvelope::from_bytes(&[MAGIC_BYTE, 1, 200]),
322            Err(EnvelopeError::UnknownCrdtType(200))
323        );
324    }
325
326    #[test]
327    fn all_crdt_types_roundtrip() {
328        let types = [
329            CrdtType::GCounter,
330            CrdtType::PNCounter,
331            CrdtType::GSet,
332            CrdtType::TwoPSet,
333            CrdtType::LWWRegister,
334            CrdtType::MVRegister,
335            CrdtType::ORSet,
336            CrdtType::Rga,
337            CrdtType::TextCrdt,
338            CrdtType::LWWMap,
339            CrdtType::AWMap,
340        ];
341        for ct in types {
342            let envelope = VersionedEnvelope::new(1, ct, b"x".to_vec());
343            let decoded = VersionedEnvelope::from_bytes(&envelope.to_bytes()).unwrap();
344            assert_eq!(decoded.crdt_type, ct);
345        }
346    }
347
348    #[test]
349    fn crdt_type_from_byte_unknown() {
350        assert_eq!(CrdtType::from_byte(0), None);
351        assert_eq!(CrdtType::from_byte(12), None);
352        assert_eq!(CrdtType::from_byte(255), None);
353    }
354}