umsh_chat_room/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! Chat-room management payload codecs for UMSH.
4
5extern crate alloc;
6
7use core::fmt;
8
9use umsh_core::options::OptionEncoder;
10
11/// Error returned when parsing or encoding chat-room management payloads.
12#[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/// Parsed chat-room management action.
39#[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, &timestamp.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, &timestamp.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}