loro_protocol/
protocol.rs

1//! Protocol types and constants mirrored from `protocol.md` and the TS package.
2//!
3//! These are the source-of-truth structures for the Rust encoder/decoder. The
4//! `ProtocolMessage` enum aggregates all message variants.
5/// Reserved library magic string, aligned with the TypeScript library.
6/// Not part of the message envelope; included for parity.
7pub const MAGIC: &str = "LRSP";
8pub const MAX_MESSAGE_SIZE: usize = 256 * 1024; // 256KB
9
10/// CRDT types supported by the wire format.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CrdtType {
13    /// "%LOR"
14    Loro,
15    /// "%EPH"
16    LoroEphemeralStore,
17    /// "%EPS"
18    LoroEphemeralStorePersisted,
19    /// "%YJS"
20    Yjs,
21    /// "%YAW"
22    YjsAwareness,
23    /// "%ELO" (End-to-End Encrypted Loro)
24    Elo,
25}
26
27impl CrdtType {
28    pub fn magic_bytes(self) -> [u8; 4] {
29        match self {
30            CrdtType::Loro => *b"%LOR",
31            CrdtType::LoroEphemeralStore => *b"%EPH",
32            CrdtType::LoroEphemeralStorePersisted => *b"%EPS",
33            CrdtType::Yjs => *b"%YJS",
34            CrdtType::YjsAwareness => *b"%YAW",
35            CrdtType::Elo => *b"%ELO",
36        }
37    }
38
39    pub fn from_magic_bytes(bytes: [u8; 4]) -> Option<Self> {
40        match &bytes {
41            b"%LOR" => Some(CrdtType::Loro),
42            b"%EPH" => Some(CrdtType::LoroEphemeralStore),
43            b"%EPS" => Some(CrdtType::LoroEphemeralStorePersisted),
44            b"%YJS" => Some(CrdtType::Yjs),
45            b"%YAW" => Some(CrdtType::YjsAwareness),
46            b"%ELO" => Some(CrdtType::Elo),
47            _ => None,
48        }
49    }
50}
51
52/// Permission returned by a successful JoinResponse.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Permission {
55    Read,
56    Write,
57}
58
59/// Message type tags as defined in protocol.md
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[repr(u8)]
62pub enum MessageType {
63    JoinRequest = 0x00,
64    JoinResponseOk = 0x01,
65    JoinError = 0x02,
66    DocUpdate = 0x03,
67    DocUpdateFragmentHeader = 0x04,
68    DocUpdateFragment = 0x05,
69    UpdateError = 0x06,
70    Leave = 0x07,
71}
72
73impl MessageType {
74    pub fn from_u8(v: u8) -> Option<Self> {
75        Some(match v {
76            0x00 => MessageType::JoinRequest,
77            0x01 => MessageType::JoinResponseOk,
78            0x02 => MessageType::JoinError,
79            0x03 => MessageType::DocUpdate,
80            0x04 => MessageType::DocUpdateFragmentHeader,
81            0x05 => MessageType::DocUpdateFragment,
82            0x06 => MessageType::UpdateError,
83            0x07 => MessageType::Leave,
84            _ => return None,
85        })
86    }
87}
88
89/// Error codes for JoinError (0x02).
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[repr(u8)]
92pub enum JoinErrorCode {
93    Unknown = 0x00,
94    VersionUnknown = 0x01,
95    AuthFailed = 0x02,
96    AppError = 0x7f,
97}
98
99impl JoinErrorCode {
100    pub fn from_u8(v: u8) -> Option<Self> {
101        Some(match v {
102            0x00 => JoinErrorCode::Unknown,
103            0x01 => JoinErrorCode::VersionUnknown,
104            0x02 => JoinErrorCode::AuthFailed,
105            0x7f => JoinErrorCode::AppError,
106            _ => return None,
107        })
108    }
109}
110
111/// Error codes for UpdateError (0x06).
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113#[repr(u8)]
114pub enum UpdateErrorCode {
115    Unknown = 0x00,
116    PermissionDenied = 0x03,
117    InvalidUpdate = 0x04,
118    PayloadTooLarge = 0x05,
119    RateLimited = 0x06,
120    FragmentTimeout = 0x07,
121    AppError = 0x7f,
122}
123
124impl UpdateErrorCode {
125    pub fn from_u8(v: u8) -> Option<Self> {
126        Some(match v {
127            0x00 => UpdateErrorCode::Unknown,
128            0x03 => UpdateErrorCode::PermissionDenied,
129            0x04 => UpdateErrorCode::InvalidUpdate,
130            0x05 => UpdateErrorCode::PayloadTooLarge,
131            0x06 => UpdateErrorCode::RateLimited,
132            0x07 => UpdateErrorCode::FragmentTimeout,
133            0x7f => UpdateErrorCode::AppError,
134            _ => return None,
135        })
136    }
137}
138
139/// 8-byte batch ID for fragmenting. On the wire this is exactly 8 raw bytes.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub struct BatchId(pub [u8; 8]);
142
143impl BatchId {
144    pub fn from_hex(s: &str) -> Result<Self, String> {
145        let s = s.strip_prefix("0x").unwrap_or(s);
146        if s.len() != 16 {
147            return Err("batch id hex must be 16 chars".into());
148        }
149        let mut out = [0u8; 8];
150        for (i, slot) in out.iter_mut().enumerate() {
151            let idx = i * 2;
152            let byte =
153                u8::from_str_radix(&s[idx..idx + 2], 16).map_err(|_| "invalid hex".to_string())?;
154            *slot = byte;
155        }
156        Ok(BatchId(out))
157    }
158
159    pub fn to_hex(self) -> String {
160        let mut s = String::from("0x");
161        for b in self.0.iter() {
162            use std::fmt::Write as _;
163            let _ = write!(s, "{:02x}", b);
164        }
165        s
166    }
167}
168
169/// All protocol messages as a single enum. Each variant includes the common
170/// `crdt` magic and `room_id` as part of the struct fields.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum ProtocolMessage {
173    JoinRequest {
174        crdt: CrdtType,
175        room_id: String,
176        auth: Vec<u8>,
177        version: Vec<u8>,
178    },
179    JoinResponseOk {
180        crdt: CrdtType,
181        room_id: String,
182        permission: Permission,
183        version: Vec<u8>,
184        extra: Option<Vec<u8>>,
185    },
186    JoinError {
187        crdt: CrdtType,
188        room_id: String,
189        code: JoinErrorCode,
190        message: String,
191        receiver_version: Option<Vec<u8>>,
192        app_code: Option<String>,
193    },
194    DocUpdate {
195        crdt: CrdtType,
196        room_id: String,
197        updates: Vec<Vec<u8>>,
198    },
199    DocUpdateFragmentHeader {
200        crdt: CrdtType,
201        room_id: String,
202        batch_id: BatchId,
203        fragment_count: u64,
204        total_size_bytes: u64,
205    },
206    DocUpdateFragment {
207        crdt: CrdtType,
208        room_id: String,
209        batch_id: BatchId,
210        index: u64,
211        fragment: Vec<u8>,
212    },
213    UpdateError {
214        crdt: CrdtType,
215        room_id: String,
216        code: UpdateErrorCode,
217        message: String,
218        batch_id: Option<BatchId>,
219        app_code: Option<String>,
220    },
221    Leave {
222        crdt: CrdtType,
223        room_id: String,
224    },
225}