umsh_text/
text.rs

1use alloc::string::String;
2
3use umsh_core::{NodeHint, options::OptionDecoder, options::OptionEncoder};
4
5use crate::{EncodeError, ParseError};
6
7fn parse_utf8(input: &[u8]) -> Result<&str, ParseError> {
8    core::str::from_utf8(input).map_err(|_| ParseError::InvalidUtf8)
9}
10
11/// Text-message rendering type.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum MessageType {
14    Basic = 0,
15    Status = 1,
16    ResendRequest = 2,
17}
18
19impl MessageType {
20    fn from_byte(value: u8) -> Result<Self, ParseError> {
21        match value {
22            0 => Ok(Self::Basic),
23            1 => Ok(Self::Status),
24            2 => Ok(Self::ResendRequest),
25            _ => Err(ParseError::InvalidMessageType(value)),
26        }
27    }
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct Fragment {
32    pub index: u8,
33    pub count: u8,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct MessageSequence {
38    pub message_id: u8,
39    pub fragment: Option<Fragment>,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Regarding {
44    Unicast {
45        message_id: u8,
46    },
47    Multicast {
48        message_id: u8,
49        source_prefix: NodeHint,
50    },
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct TextMessage<'a> {
55    pub message_type: MessageType,
56    pub sender_handle: Option<&'a str>,
57    pub sequence: Option<MessageSequence>,
58    pub sequence_reset: bool,
59    pub regarding: Option<Regarding>,
60    pub editing: Option<u8>,
61    pub bg_color: Option<[u8; 3]>,
62    pub text_color: Option<[u8; 3]>,
63    pub body: &'a str,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct OwnedTextMessage {
68    pub message_type: MessageType,
69    pub sender_handle: Option<String>,
70    pub sequence: Option<MessageSequence>,
71    pub sequence_reset: bool,
72    pub regarding: Option<Regarding>,
73    pub editing: Option<u8>,
74    pub bg_color: Option<[u8; 3]>,
75    pub text_color: Option<[u8; 3]>,
76    pub body: String,
77}
78
79impl OwnedTextMessage {
80    pub fn as_borrowed(&self) -> TextMessage<'_> {
81        TextMessage {
82            message_type: self.message_type,
83            sender_handle: self.sender_handle.as_deref(),
84            sequence: self.sequence,
85            sequence_reset: self.sequence_reset,
86            regarding: self.regarding,
87            editing: self.editing,
88            bg_color: self.bg_color,
89            text_color: self.text_color,
90            body: &self.body,
91        }
92    }
93}
94
95impl From<TextMessage<'_>> for OwnedTextMessage {
96    fn from(value: TextMessage<'_>) -> Self {
97        Self {
98            message_type: value.message_type,
99            sender_handle: value.sender_handle.map(String::from),
100            sequence: value.sequence,
101            sequence_reset: value.sequence_reset,
102            regarding: value.regarding,
103            editing: value.editing,
104            bg_color: value.bg_color,
105            text_color: value.text_color,
106            body: String::from(value.body),
107        }
108    }
109}
110
111pub fn parse(payload: &[u8]) -> Result<TextMessage<'_>, ParseError> {
112    let mut decoder = OptionDecoder::new(payload);
113    let mut message_type = MessageType::Basic;
114    let mut sender_handle = None;
115    let mut sequence = None;
116    let mut sequence_reset = false;
117    let mut regarding = None;
118    let mut editing = None;
119    let mut bg_color = None;
120    let mut text_color = None;
121
122    while let Some(item) = decoder.next() {
123        let (number, value) = item?;
124        match number {
125            0 => {
126                message_type = if value.is_empty() {
127                    MessageType::Basic
128                } else if value.len() == 1 {
129                    MessageType::from_byte(value[0])?
130                } else {
131                    return Err(ParseError::InvalidOptionValue);
132                };
133            }
134            1 => sender_handle = Some(parse_utf8(value)?),
135            2 => {
136                sequence = Some(match value {
137                    [message_id] => MessageSequence {
138                        message_id: *message_id,
139                        fragment: None,
140                    },
141                    [message_id, index, count] if *count >= 2 => MessageSequence {
142                        message_id: *message_id,
143                        fragment: Some(Fragment {
144                            index: *index,
145                            count: *count,
146                        }),
147                    },
148                    _ => return Err(ParseError::InvalidOptionValue),
149                });
150            }
151            3 => {
152                if !value.is_empty() {
153                    return Err(ParseError::InvalidOptionValue);
154                }
155                sequence_reset = true;
156            }
157            4 => {
158                regarding = Some(match value {
159                    [message_id] => Regarding::Unicast {
160                        message_id: *message_id,
161                    },
162                    [message_id, a, b, c] => Regarding::Multicast {
163                        message_id: *message_id,
164                        source_prefix: NodeHint([*a, *b, *c]),
165                    },
166                    _ => return Err(ParseError::InvalidOptionValue),
167                });
168            }
169            5 => {
170                editing = match value {
171                    [message_id] => Some(*message_id),
172                    _ => return Err(ParseError::InvalidOptionValue),
173                };
174            }
175            6 => {
176                bg_color = match value {
177                    [r, g, b] => Some([*r, *g, *b]),
178                    _ => return Err(ParseError::InvalidOptionValue),
179                };
180            }
181            7 => {
182                text_color = match value {
183                    [r, g, b] => Some([*r, *g, *b]),
184                    _ => return Err(ParseError::InvalidOptionValue),
185                };
186            }
187            _ => {}
188        }
189    }
190
191    let body = parse_utf8(decoder.remainder())?;
192    Ok(TextMessage {
193        message_type,
194        sender_handle,
195        sequence,
196        sequence_reset,
197        regarding,
198        editing,
199        bg_color,
200        text_color,
201        body,
202    })
203}
204
205pub fn encode(msg: &TextMessage<'_>, buf: &mut [u8]) -> Result<usize, EncodeError> {
206    let mut encoder = OptionEncoder::new(buf);
207
208    if msg.message_type != MessageType::Basic {
209        encoder.put(0, &[msg.message_type as u8])?;
210    }
211    if let Some(handle) = msg.sender_handle {
212        encoder.put(1, handle.as_bytes())?;
213    }
214    if let Some(sequence) = msg.sequence {
215        let mut seq_buf = [0u8; 3];
216        let seq_len = if let Some(fragment) = sequence.fragment {
217            if fragment.count < 2 {
218                return Err(EncodeError::InvalidField);
219            }
220            seq_buf = [sequence.message_id, fragment.index, fragment.count];
221            3
222        } else {
223            seq_buf[0] = sequence.message_id;
224            1
225        };
226        encoder.put(2, &seq_buf[..seq_len])?;
227    }
228    if msg.sequence_reset {
229        encoder.put(3, &[])?;
230    }
231    if let Some(regarding) = msg.regarding {
232        let mut regarding_buf = [0u8; 4];
233        let regarding_len = match regarding {
234            Regarding::Unicast { message_id } => {
235                regarding_buf[0] = message_id;
236                1
237            }
238            Regarding::Multicast {
239                message_id,
240                source_prefix,
241            } => {
242                regarding_buf = [
243                    message_id,
244                    source_prefix.0[0],
245                    source_prefix.0[1],
246                    source_prefix.0[2],
247                ];
248                4
249            }
250        };
251        encoder.put(4, &regarding_buf[..regarding_len])?;
252    }
253    if let Some(editing) = msg.editing {
254        encoder.put(5, &[editing])?;
255    }
256    if let Some(color) = msg.bg_color {
257        encoder.put(6, &color)?;
258    }
259    if let Some(color) = msg.text_color {
260        encoder.put(7, &color)?;
261    }
262
263    encoder.end_marker()?;
264    let prefix_len = encoder.finish();
265    if buf.len().saturating_sub(prefix_len) < msg.body.len() {
266        return Err(EncodeError::BufferTooSmall);
267    }
268    buf[prefix_len..prefix_len + msg.body.len()].copy_from_slice(msg.body.as_bytes());
269    Ok(prefix_len + msg.body.len())
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn basic_text_message_round_trips() {
278        let message = TextMessage {
279            message_type: MessageType::Basic,
280            sender_handle: None,
281            sequence: None,
282            sequence_reset: false,
283            regarding: None,
284            editing: None,
285            bg_color: None,
286            text_color: None,
287            body: "hello",
288        };
289
290        let mut buf = [0u8; 64];
291        let len = encode(&message, &mut buf).expect("encode should succeed");
292        let parsed = parse(&buf[..len]).expect("parse should succeed");
293
294        assert_eq!(parsed, message);
295    }
296}