1#![cfg_attr(not(feature = "std"), no_std)]
2
3extern crate alloc;
6
7use core::fmt;
8
9use umsh_core::options::OptionEncoder;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Error {
14 Core(umsh_core::ParseError),
15 InvalidUtf8,
16 InvalidChatAction(u8),
17 InvalidOptionValue,
18 InvalidLength { expected: usize, actual: usize },
19 BufferTooSmall,
20 InvalidField,
21}
22
23impl From<umsh_core::EncodeError> for Error {
24 fn from(_: umsh_core::EncodeError) -> Self {
25 Self::BufferTooSmall
26 }
27}
28
29impl fmt::Display for Error {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "{self:?}")
32 }
33}
34
35#[cfg(feature = "std")]
36impl std::error::Error for Error {}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum ChatAction<'a> {
41 GetRoomInfo,
42 RoomInfo(RoomInfo<'a>),
43 Login(LoginParams<'a>),
44 Logout,
45 FetchMessages { timestamp: u32, max_count: u8 },
46 FetchUsers,
47 AdminCommand(&'a [u8]),
48 RoomUpdate(&'a [u8]),
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct RoomInfo<'a> {
53 pub options: &'a [u8],
54 pub description: Option<&'a str>,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct LoginParams<'a> {
59 pub handle: Option<&'a str>,
60 pub last_message_timestamp: Option<u32>,
61 pub session_timeout_minutes: Option<u8>,
62 pub password: Option<&'a [u8]>,
63}
64
65pub fn parse(payload: &[u8]) -> Result<ChatAction<'_>, Error> {
66 let (&action, body) = payload
67 .split_first()
68 .ok_or(Error::Core(umsh_core::ParseError::Truncated))?;
69
70 match action {
71 0 => {
72 if body.is_empty() {
73 Ok(ChatAction::GetRoomInfo)
74 } else {
75 Err(Error::InvalidOptionValue)
76 }
77 }
78 1 => {
79 let (options, description) =
80 if let Some(index) = body.iter().position(|byte| *byte == 0xFF) {
81 (&body[..=index], Some(parse_utf8(&body[index + 1..])?))
82 } else {
83 (body, None)
84 };
85 Ok(ChatAction::RoomInfo(RoomInfo {
86 options,
87 description,
88 }))
89 }
90 2 => Ok(ChatAction::Login(parse_login(body)?)),
91 3 => {
92 if body.is_empty() {
93 Ok(ChatAction::Logout)
94 } else {
95 Err(Error::InvalidOptionValue)
96 }
97 }
98 5 => match body {
99 [a, b, c, d, max_count] => Ok(ChatAction::FetchMessages {
100 timestamp: u32::from_be_bytes([*a, *b, *c, *d]),
101 max_count: *max_count,
102 }),
103 _ => Err(Error::InvalidLength {
104 expected: 5,
105 actual: body.len(),
106 }),
107 },
108 6 => {
109 if body.is_empty() {
110 Ok(ChatAction::FetchUsers)
111 } else {
112 Err(Error::InvalidOptionValue)
113 }
114 }
115 7 => Ok(ChatAction::AdminCommand(body)),
116 8 => Ok(ChatAction::RoomUpdate(body)),
117 other => Err(Error::InvalidChatAction(other)),
118 }
119}
120
121fn parse_login(payload: &[u8]) -> Result<LoginParams<'_>, Error> {
122 let mut handle = None;
123 let mut last_message_timestamp = None;
124 let mut session_timeout_minutes = None;
125 let mut password = None;
126 let remainder = decode_options_allow_eof(payload, |number, value| {
127 match number {
128 0 => handle = Some(parse_utf8(value)?),
129 1 => {
130 if value.len() != 4 {
131 return Err(Error::InvalidLength {
132 expected: 4,
133 actual: value.len(),
134 });
135 }
136 last_message_timestamp = Some(u32::from_be_bytes(value.try_into().unwrap()));
137 }
138 2 => {
139 if value.len() != 1 {
140 return Err(Error::InvalidLength {
141 expected: 1,
142 actual: value.len(),
143 });
144 }
145 session_timeout_minutes = Some(value[0]);
146 }
147 3 => password = Some(value),
148 _ => {}
149 }
150 Ok(())
151 })?;
152
153 if !remainder.is_empty() {
154 return Err(Error::InvalidOptionValue);
155 }
156
157 Ok(LoginParams {
158 handle,
159 last_message_timestamp,
160 session_timeout_minutes,
161 password,
162 })
163}
164
165pub fn encode(action: &ChatAction<'_>, buf: &mut [u8]) -> Result<usize, Error> {
166 match action {
167 ChatAction::GetRoomInfo => {
168 if buf.is_empty() {
169 return Err(Error::BufferTooSmall);
170 }
171 buf[0] = 0;
172 Ok(1)
173 }
174 ChatAction::RoomInfo(room_info) => {
175 let mut pos = 0usize;
176 push_byte(buf, &mut pos, 1)?;
177 copy_into(buf, &mut pos, room_info.options)?;
178 if let Some(description) = room_info.description {
179 if room_info.options.last().copied() != Some(0xFF) {
180 push_byte(buf, &mut pos, 0xFF)?;
181 }
182 copy_into(buf, &mut pos, description.as_bytes())?;
183 }
184 Ok(pos)
185 }
186 ChatAction::Login(login) => {
187 if buf.is_empty() {
188 return Err(Error::BufferTooSmall);
189 }
190 buf[0] = 2;
191 let mut encoder = OptionEncoder::new(&mut buf[1..]);
192 if let Some(handle) = login.handle {
193 encoder.put(0, handle.as_bytes())?;
194 }
195 if let Some(timestamp) = login.last_message_timestamp {
196 encoder.put(1, ×tamp.to_be_bytes())?;
197 }
198 if let Some(timeout) = login.session_timeout_minutes {
199 encoder.put(2, &[timeout])?;
200 }
201 if let Some(password) = login.password {
202 encoder.put(3, password)?;
203 }
204 Ok(1 + encoder.finish())
205 }
206 ChatAction::Logout => {
207 if buf.is_empty() {
208 return Err(Error::BufferTooSmall);
209 }
210 buf[0] = 3;
211 Ok(1)
212 }
213 ChatAction::FetchMessages {
214 timestamp,
215 max_count,
216 } => {
217 let mut pos = 0usize;
218 push_byte(buf, &mut pos, 5)?;
219 copy_into(buf, &mut pos, ×tamp.to_be_bytes())?;
220 push_byte(buf, &mut pos, *max_count)?;
221 Ok(pos)
222 }
223 ChatAction::FetchUsers => {
224 if buf.is_empty() {
225 return Err(Error::BufferTooSmall);
226 }
227 buf[0] = 6;
228 Ok(1)
229 }
230 ChatAction::AdminCommand(payload) => {
231 let mut pos = 0usize;
232 push_byte(buf, &mut pos, 7)?;
233 copy_into(buf, &mut pos, payload)?;
234 Ok(pos)
235 }
236 ChatAction::RoomUpdate(payload) => {
237 let mut pos = 0usize;
238 push_byte(buf, &mut pos, 8)?;
239 copy_into(buf, &mut pos, payload)?;
240 Ok(pos)
241 }
242 }
243}
244
245fn parse_utf8(input: &[u8]) -> Result<&str, Error> {
246 core::str::from_utf8(input).map_err(|_| Error::InvalidUtf8)
247}
248
249fn copy_into(dst: &mut [u8], pos: &mut usize, src: &[u8]) -> Result<(), Error> {
250 if dst.len().saturating_sub(*pos) < src.len() {
251 return Err(Error::BufferTooSmall);
252 }
253 dst[*pos..*pos + src.len()].copy_from_slice(src);
254 *pos += src.len();
255 Ok(())
256}
257
258fn push_byte(dst: &mut [u8], pos: &mut usize, byte: u8) -> Result<(), Error> {
259 copy_into(dst, pos, &[byte])
260}
261
262fn decode_options_allow_eof<'a>(
263 data: &'a [u8],
264 mut on_option: impl FnMut(u16, &'a [u8]) -> Result<(), Error>,
265) -> Result<&'a [u8], Error> {
266 let mut pos = 0usize;
267 let mut last_number = 0u16;
268
269 while pos < data.len() {
270 let first = data[pos];
271 if first == 0xFF {
272 return Ok(&data[pos + 1..]);
273 }
274 pos += 1;
275
276 let (delta, delta_len) = read_extended(&data[pos..], first >> 4)?;
277 pos += delta_len;
278 let (len, len_len) = read_extended(&data[pos..], first & 0x0F)?;
279 pos += len_len;
280
281 let end = pos
282 .checked_add(len as usize)
283 .ok_or(Error::InvalidOptionValue)?;
284 if end > data.len() {
285 return Err(Error::Core(umsh_core::ParseError::Truncated));
286 }
287 let number = last_number
288 .checked_add(delta)
289 .ok_or(Error::InvalidOptionValue)?;
290 on_option(number, &data[pos..end])?;
291 pos = end;
292 last_number = number;
293 }
294
295 Ok(&data[pos..])
296}
297
298fn read_extended(data: &[u8], nibble: u8) -> Result<(u16, usize), Error> {
299 match nibble {
300 0..=12 => Ok((nibble as u16, 0)),
301 13 => {
302 if data.is_empty() {
303 Err(Error::Core(umsh_core::ParseError::Truncated))
304 } else {
305 Ok((data[0] as u16 + 13, 1))
306 }
307 }
308 14 => {
309 if data.len() < 2 {
310 Err(Error::Core(umsh_core::ParseError::Truncated))
311 } else {
312 Ok((u16::from_be_bytes([data[0], data[1]]) + 269, 2))
313 }
314 }
315 _ => Err(Error::Core(umsh_core::ParseError::InvalidOptionNibble)),
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn chat_room_login_and_room_info_round_trip() {
325 let login = ChatAction::Login(LoginParams {
326 handle: Some("guest"),
327 last_message_timestamp: Some(0x01020304),
328 session_timeout_minutes: Some(9),
329 password: Some(b"secret"),
330 });
331 let mut buf = [0u8; 128];
332 let len = encode(&login, &mut buf).unwrap();
333 let parsed = parse(&buf[..len]).unwrap();
334 assert_eq!(parsed, login);
335
336 let room_info = ChatAction::RoomInfo(RoomInfo {
337 options: &[0x11, 0x22, 0xFF],
338 description: Some("mesh room"),
339 });
340 let len = encode(&room_info, &mut buf).unwrap();
341 let parsed = parse(&buf[..len]).unwrap();
342 assert_eq!(parsed, room_info);
343 }
344}