1use crate::ParseError;
8use crate::codec::ParseInfo;
9use crate::model::{
10 ConversationKey, ExtensionOptions, MessageSequence, MessageType, Regarding, SenderScope,
11 TextMessage, option,
12};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum DeliveryPath {
17 Unicast,
19 BlindUnicast,
21 Multicast,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Envelope {
28 pub path: DeliveryPath,
29 pub conversation: ConversationKey,
30 pub sender: SenderScope,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum ValidateError {
36 Parse(ParseError),
39 ResendRequestPath,
41 ResendRequestMissingSequence,
43 ChannelGroupResendPath,
46 UnavailableMissingSequence,
49 RegardingWidth,
51 UnrecognizedMessageType(u8),
53 DuplicateExtensionOption(u16),
56 InvalidExtensionOption(u16),
58}
59
60impl From<ParseError> for ValidateError {
61 fn from(value: ParseError) -> Self {
62 Self::Parse(value)
63 }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum DuplicateTreatment {
69 Fatal,
71 FirstWins,
73 Idempotent,
75}
76
77pub trait TextProfile {
83 fn recognizes_content_type(&self, message_type: MessageType) -> bool;
86
87 fn extension_treatment(&self, number: u16) -> Option<DuplicateTreatment>;
90}
91
92#[derive(Clone, Copy, Debug, Default)]
95pub struct DirectChannelProfile;
96
97impl TextProfile for DirectChannelProfile {
98 fn recognizes_content_type(&self, message_type: MessageType) -> bool {
99 matches!(message_type, MessageType::Basic | MessageType::Status)
100 }
101
102 fn extension_treatment(&self, _number: u16) -> Option<DuplicateTreatment> {
103 None
104 }
105}
106
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub struct ValidationNotes {
110 pub repeated_presentation_mask: u16,
112 pub ignored_continuation_metadata: bool,
115 pub ignored_channel_group_resend: bool,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub struct ContentMessage<'a> {
123 pub message_type: MessageType,
124 pub sender_handle: Option<&'a str>,
125 pub sequence: Option<MessageSequence>,
126 pub sequence_reset: bool,
127 pub regarding: Option<Regarding>,
128 pub editing: Option<u8>,
129 pub bg_color: Option<[u8; 3]>,
130 pub text_color: Option<[u8; 3]>,
131 pub extensions: ExtensionOptions<'a>,
132 pub body: &'a [u8],
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub enum Validated<'a> {
139 Content(ContentMessage<'a>),
140 ResendRequest {
142 sequence: MessageSequence,
143 channel_group: bool,
144 },
145 Unavailable {
147 sequence: MessageSequence,
148 },
149}
150
151pub fn validate<'a>(
153 profile: &dyn TextProfile,
154 envelope: &Envelope,
155 message: &TextMessage<'a>,
156 info: &ParseInfo,
157) -> Result<(Validated<'a>, ValidationNotes), ValidateError> {
158 let mut notes = ValidationNotes {
159 repeated_presentation_mask: info.repeated_presentation_mask,
160 ..ValidationNotes::default()
161 };
162
163 match message.message_type {
164 MessageType::ResendRequest => {
165 if !matches!(
166 envelope.path,
167 DeliveryPath::Unicast | DeliveryPath::BlindUnicast
168 ) {
169 return Err(ValidateError::ResendRequestPath);
170 }
171 let sequence = message
172 .sequence
173 .ok_or(ValidateError::ResendRequestMissingSequence)?;
174 if message.channel_group_resend && envelope.path != DeliveryPath::BlindUnicast {
175 return Err(ValidateError::ChannelGroupResendPath);
176 }
177 return Ok((
180 Validated::ResendRequest {
181 sequence,
182 channel_group: message.channel_group_resend,
183 },
184 notes,
185 ));
186 }
187 MessageType::MessageUnavailable => {
188 let sequence = message
189 .sequence
190 .ok_or(ValidateError::UnavailableMissingSequence)?;
191 if message.channel_group_resend {
192 notes.ignored_channel_group_resend = true;
193 }
194 return Ok((Validated::Unavailable { sequence }, notes));
195 }
196 other => {
197 if !profile.recognizes_content_type(other) {
198 return Err(ValidateError::UnrecognizedMessageType(other.to_byte()));
199 }
200 }
201 }
202
203 if let Some(regarding) = message.regarding {
204 let multicast_form = matches!(regarding, Regarding::Multicast { .. });
205 if multicast_form != envelope.conversation.uses_multicast_references() {
206 return Err(ValidateError::RegardingWidth);
207 }
208 }
209
210 validate_extensions(profile, &message.extensions, &mut notes)?;
211
212 let mut content = ContentMessage {
213 message_type: message.message_type,
214 sender_handle: message.sender_handle,
215 sequence: message.sequence,
216 sequence_reset: message.sequence_reset,
217 regarding: message.regarding,
218 editing: message.editing,
219 bg_color: message.bg_color,
220 text_color: message.text_color,
221 extensions: message.extensions,
222 body: message.body,
223 };
224
225 if message.channel_group_resend {
226 notes.ignored_channel_group_resend = true;
227 }
228
229 let fragmented = message.sequence.and_then(|sequence| sequence.fragment);
230 let is_continuation = fragmented.is_some_and(|fragment| fragment.index > 0);
231 if is_continuation {
232 let metadata_mask = (1 << option::MESSAGE_TYPE)
235 | (1 << option::SENDER_HANDLE)
236 | (1 << option::SEQUENCE_RESET)
237 | (1 << option::REGARDING)
238 | (1 << option::EDITING)
239 | (1 << option::BACKGROUND_COLOR)
240 | (1 << option::TEXT_COLOR);
241 if info.seen_mask & metadata_mask != 0 || !message.extensions.is_empty() {
242 notes.ignored_continuation_metadata = true;
243 }
244 content.message_type = MessageType::Basic;
245 content.sender_handle = None;
246 content.sequence_reset = false;
247 content.regarding = None;
248 content.editing = None;
249 content.bg_color = None;
250 content.text_color = None;
251 content.extensions = ExtensionOptions::empty();
252 } else if fragmented.is_none() {
253 core::str::from_utf8(message.body).map_err(|_| ParseError::InvalidUtf8)?;
256 }
257
258 Ok((Validated::Content(content), notes))
259}
260
261fn validate_extensions(
262 profile: &dyn TextProfile,
263 extensions: &ExtensionOptions<'_>,
264 notes: &mut ValidationNotes,
265) -> Result<(), ValidateError> {
266 let mut seen: u64 = 0;
269 for item in extensions.iter() {
270 let (number, _value) = item.map_err(ValidateError::Parse)?;
271 let Some(treatment) = profile.extension_treatment(number) else {
272 continue;
273 };
274 let bit_index = number - option::EXTENSION_BASE;
275 if bit_index >= 64 {
276 continue;
277 }
278 let bit = 1u64 << bit_index;
279 if seen & bit != 0 {
280 match treatment {
281 DuplicateTreatment::Fatal => {
282 return Err(ValidateError::DuplicateExtensionOption(number));
283 }
284 DuplicateTreatment::FirstWins => {
285 notes.repeated_presentation_mask |= 1 << (number.min(15));
286 }
287 DuplicateTreatment::Idempotent => {}
288 }
289 }
290 seen |= bit;
291 }
292 Ok(())
293}