umsh_text/
codec.rs

1//! Wire codec for text messages: exact bytes only.
2//!
3//! The codec decodes base options into typed fields, retains extension
4//! options verbatim, and records the occurrence information that semantic
5//! validation needs. It performs no conversation-context checks.
6
7use umsh_core::options::{OptionDecoder, OptionEncoder};
8
9use crate::model::{
10    ExtensionOptions, FRAGMENT_COUNT_MAX, Fragment, MessageSequence, MessageType, Regarding,
11    TextMessage, option,
12};
13use crate::{EncodeError, ParseError};
14
15/// Occurrence information recorded while decoding, for semantic validation
16/// and diagnostics.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct ParseInfo {
19    /// Bitmask of base option numbers (0–8) present at least once.
20    pub seen_mask: u16,
21    /// Bitmask of presentation options (Sender Handle, colors) that appeared
22    /// more than once; the first occurrence was kept.
23    pub repeated_presentation_mask: u16,
24}
25
26impl ParseInfo {
27    pub fn saw(&self, number: u16) -> bool {
28        number < 16 && self.seen_mask & (1 << number) != 0
29    }
30}
31
32fn parse_utf8(input: &[u8]) -> Result<&str, ParseError> {
33    core::str::from_utf8(input).map_err(|_| ParseError::InvalidUtf8)
34}
35
36/// Decode a text-message payload (after the payload-type byte).
37pub fn parse(payload: &[u8]) -> Result<TextMessage<'_>, ParseError> {
38    parse_with_info(payload).map(|(message, _)| message)
39}
40
41/// Decode a text-message payload, also returning option occurrence
42/// information for semantic validation.
43pub fn parse_with_info(payload: &[u8]) -> Result<(TextMessage<'_>, ParseInfo), ParseError> {
44    let mut decoder = OptionDecoder::new(payload);
45    let mut message = TextMessage::basic("");
46    let mut info = ParseInfo::default();
47    let mut ext_range: Option<(usize, u16, usize)> = None; // start, base number, end
48
49    loop {
50        let snapshot_pos = decoder.position();
51        let snapshot_number = decoder.last_number();
52        let Some(item) = decoder.next() else {
53            if let Some((_, _, end)) = ext_range.as_mut() {
54                *end = snapshot_pos;
55            }
56            break;
57        };
58        let (number, value) = item?;
59
60        if number >= option::EXTENSION_BASE {
61            if ext_range.is_none() {
62                ext_range = Some((snapshot_pos, snapshot_number, snapshot_pos));
63            }
64            continue;
65        }
66
67        let bit = 1u16 << number;
68        if info.seen_mask & bit != 0 {
69            match number {
70                // Identity, sequencing, and reference options: duplication is
71                // unresolvable ambiguity, even with identical values.
72                option::MESSAGE_TYPE
73                | option::MESSAGE_SEQUENCE
74                | option::REGARDING
75                | option::EDITING => return Err(ParseError::DuplicateOption(number)),
76                // Zero-length flags are idempotent, but each occurrence must
77                // still be syntactically valid.
78                option::SEQUENCE_RESET | option::CHANNEL_GROUP_RESEND => {
79                    if !value.is_empty() {
80                        return Err(ParseError::InvalidOptionValue);
81                    }
82                    continue;
83                }
84                // Presentation options: first occurrence wins.
85                _ => {
86                    info.repeated_presentation_mask |= bit;
87                    continue;
88                }
89            }
90        }
91        info.seen_mask |= bit;
92
93        match number {
94            option::MESSAGE_TYPE => {
95                message.message_type = if value.is_empty() {
96                    MessageType::Basic
97                } else if value.len() == 1 {
98                    MessageType::from_byte(value[0])
99                } else {
100                    return Err(ParseError::InvalidOptionValue);
101                };
102            }
103            option::SENDER_HANDLE => message.sender_handle = Some(parse_utf8(value)?),
104            option::MESSAGE_SEQUENCE => {
105                message.sequence = Some(match value {
106                    [message_id] => MessageSequence {
107                        message_id: *message_id,
108                        fragment: None,
109                    },
110                    [message_id, index, count] if *count >= 2 && *index < *count => {
111                        MessageSequence {
112                            message_id: *message_id,
113                            fragment: Some(Fragment {
114                                index: *index,
115                                count: *count,
116                            }),
117                        }
118                    }
119                    _ => return Err(ParseError::InvalidOptionValue),
120                });
121            }
122            option::SEQUENCE_RESET => {
123                if !value.is_empty() {
124                    return Err(ParseError::InvalidOptionValue);
125                }
126                message.sequence_reset = true;
127            }
128            option::REGARDING => {
129                message.regarding = Some(match value {
130                    [message_id] => Regarding::Unicast {
131                        message_id: *message_id,
132                    },
133                    [message_id, a, b, c] => Regarding::Multicast {
134                        message_id: *message_id,
135                        source_prefix: umsh_core::NodeHint([*a, *b, *c]),
136                    },
137                    _ => return Err(ParseError::InvalidOptionValue),
138                });
139            }
140            option::EDITING => {
141                message.editing = match value {
142                    [message_id] => Some(*message_id),
143                    _ => return Err(ParseError::InvalidOptionValue),
144                };
145            }
146            option::BACKGROUND_COLOR => {
147                message.bg_color = match value {
148                    [r, g, b] => Some([*r, *g, *b]),
149                    _ => return Err(ParseError::InvalidOptionValue),
150                };
151            }
152            option::TEXT_COLOR => {
153                message.text_color = match value {
154                    [r, g, b] => Some([*r, *g, *b]),
155                    _ => return Err(ParseError::InvalidOptionValue),
156                };
157            }
158            option::CHANNEL_GROUP_RESEND => {
159                if !value.is_empty() {
160                    return Err(ParseError::InvalidOptionValue);
161                }
162                message.channel_group_resend = true;
163            }
164            _ => unreachable!(),
165        }
166    }
167
168    if let Some((start, base_number, end)) = ext_range {
169        message.extensions = ExtensionOptions {
170            base_number,
171            data: &payload[start..end],
172        };
173    }
174    message.body = decoder.remainder();
175    Ok((message, info))
176}
177
178/// Encode a text message into a caller-provided buffer, returning the number
179/// of bytes written.
180pub fn encode(msg: &TextMessage<'_>, buf: &mut [u8]) -> Result<usize, EncodeError> {
181    let mut encoder = OptionEncoder::new(buf);
182
183    if msg.message_type != MessageType::Basic {
184        encoder.put(option::MESSAGE_TYPE, &[msg.message_type.to_byte()])?;
185    }
186    if let Some(handle) = msg.sender_handle {
187        encoder.put(option::SENDER_HANDLE, handle.as_bytes())?;
188    }
189    if let Some(sequence) = msg.sequence {
190        let mut seq_buf = [0u8; 3];
191        let seq_len = if let Some(fragment) = sequence.fragment {
192            if fragment.count < 2
193                || fragment.index >= fragment.count
194                || fragment.count > FRAGMENT_COUNT_MAX
195            {
196                return Err(EncodeError::InvalidField);
197            }
198            seq_buf = [sequence.message_id, fragment.index, fragment.count];
199            3
200        } else {
201            seq_buf[0] = sequence.message_id;
202            1
203        };
204        encoder.put(option::MESSAGE_SEQUENCE, &seq_buf[..seq_len])?;
205    }
206    if msg.sequence_reset {
207        encoder.put(option::SEQUENCE_RESET, &[])?;
208    }
209    if let Some(regarding) = msg.regarding {
210        let mut regarding_buf = [0u8; 4];
211        let regarding_len = match regarding {
212            Regarding::Unicast { message_id } => {
213                regarding_buf[0] = message_id;
214                1
215            }
216            Regarding::Multicast {
217                message_id,
218                source_prefix,
219            } => {
220                regarding_buf = [
221                    message_id,
222                    source_prefix.0[0],
223                    source_prefix.0[1],
224                    source_prefix.0[2],
225                ];
226                4
227            }
228        };
229        encoder.put(option::REGARDING, &regarding_buf[..regarding_len])?;
230    }
231    if let Some(editing) = msg.editing {
232        encoder.put(option::EDITING, &[editing])?;
233    }
234    if let Some(color) = msg.bg_color {
235        encoder.put(option::BACKGROUND_COLOR, &color)?;
236    }
237    if let Some(color) = msg.text_color {
238        encoder.put(option::TEXT_COLOR, &color)?;
239    }
240    if msg.channel_group_resend {
241        encoder.put(option::CHANNEL_GROUP_RESEND, &[])?;
242    }
243    let mut last_ext = option::CHANNEL_GROUP_RESEND;
244    for item in msg.extensions.iter() {
245        let (number, value) = item.map_err(|_| EncodeError::InvalidField)?;
246        if number < option::EXTENSION_BASE || number < last_ext {
247            return Err(EncodeError::InvalidField);
248        }
249        last_ext = number;
250        encoder.put(number, value)?;
251    }
252
253    if !msg.body.is_empty() {
254        encoder.end_marker()?;
255    }
256    let prefix_len = encoder.finish();
257    if buf.len().saturating_sub(prefix_len) < msg.body.len() {
258        return Err(EncodeError::BufferTooSmall);
259    }
260    buf[prefix_len..prefix_len + msg.body.len()].copy_from_slice(msg.body);
261    Ok(prefix_len + msg.body.len())
262}