1use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use thiserror::Error;
7
8use crate::ids::MessageId;
9
10#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case", tag = "mode")]
13pub enum DataReliability {
14 ReliableOrdered,
15 ReliableUnordered,
16 MaxRetransmits { ordered: bool, count: u16 },
17 MaxLifetime { ordered: bool, milliseconds: u32 },
18}
19
20impl DataReliability {
21 pub fn validate(&self) -> Result<(), DataMessageValidationError> {
22 if let Self::MaxLifetime { milliseconds, .. } = self {
23 if *milliseconds == 0 {
24 return Err(DataMessageValidationError::ZeroLifetime);
25 }
26 if *milliseconds > u16::MAX as u32 {
27 return Err(DataMessageValidationError::LifetimeTooLarge {
28 milliseconds: *milliseconds,
29 maximum: u16::MAX as u32,
30 });
31 }
32 }
33 Ok(())
34 }
35}
36
37impl Default for DataReliability {
38 fn default() -> Self {
39 Self::ReliableOrdered
40 }
41}
42
43#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
46pub struct DataMessage {
47 pub label: String,
48 pub content_type: String,
49 pub bytes: Bytes,
50 #[serde(default)]
51 pub reliability: DataReliability,
52 pub message_id: MessageId,
53}
54
55impl fmt::Debug for DataMessage {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter
58 .debug_struct("DataMessage")
59 .field("label_present", &!self.label.is_empty())
60 .field("label_bytes", &self.label.len())
61 .field("content_type_present", &!self.content_type.is_empty())
62 .field("content_type_bytes", &self.content_type.len())
63 .field("body_bytes", &self.bytes.len())
64 .field("reliability", &self.reliability)
65 .field("message_id_present", &!self.message_id.as_str().is_empty())
66 .finish()
67 }
68}
69
70pub const MAX_DATA_LABEL_BYTES: usize = 128;
71pub const MAX_CONTENT_TYPE_BYTES: usize = 255;
72pub const MAX_DATA_MESSAGE_BYTES: usize = 64 * 1024;
73pub const MAX_DATA_MESSAGE_ID_BYTES: usize = 128;
74
75#[derive(Clone, Debug, Eq, Error, PartialEq)]
76pub enum DataMessageValidationError {
77 #[error("data message label must not be empty")]
78 EmptyLabel,
79 #[error("data message label is {actual} bytes; maximum is {maximum}")]
80 LabelTooLong { actual: usize, maximum: usize },
81 #[error("data message label contains a control character")]
82 LabelContainsControl,
83 #[error("data message content type must not be empty")]
84 EmptyContentType,
85 #[error("data message content type is {actual} bytes; maximum is {maximum}")]
86 ContentTypeTooLong { actual: usize, maximum: usize },
87 #[error("data message content type contains a control character")]
88 ContentTypeContainsControl,
89 #[error("data message content type is not a valid MIME media type")]
90 InvalidContentType,
91 #[error("data message body is {actual} bytes; maximum is {maximum}")]
92 BodyTooLarge { actual: usize, maximum: usize },
93 #[error("data message ID must not be empty")]
94 EmptyMessageId,
95 #[error("data message ID is {actual} bytes; maximum is {maximum}")]
96 MessageIdTooLong { actual: usize, maximum: usize },
97 #[error("data message ID contains a control character")]
98 MessageIdContainsControl,
99 #[error("max-lifetime reliability must be greater than zero milliseconds")]
100 ZeroLifetime,
101 #[error("max-lifetime reliability is {milliseconds}ms; maximum is {maximum}ms")]
102 LifetimeTooLarge { milliseconds: u32, maximum: u32 },
103}
104
105impl DataMessage {
106 pub fn reliable(
107 label: impl Into<String>,
108 content_type: impl Into<String>,
109 bytes: impl Into<Bytes>,
110 ) -> Self {
111 Self {
112 label: label.into(),
113 content_type: content_type.into(),
114 bytes: bytes.into(),
115 reliability: DataReliability::ReliableOrdered,
116 message_id: MessageId::new(),
117 }
118 }
119
120 pub fn try_new(
121 label: impl Into<String>,
122 content_type: impl Into<String>,
123 bytes: impl Into<Bytes>,
124 reliability: DataReliability,
125 message_id: MessageId,
126 ) -> Result<Self, DataMessageValidationError> {
127 let message = Self {
128 label: label.into(),
129 content_type: content_type.into(),
130 bytes: bytes.into(),
131 reliability,
132 message_id,
133 };
134 message.validate()?;
135 Ok(message)
136 }
137
138 pub fn validate(&self) -> Result<(), DataMessageValidationError> {
139 if self.label.is_empty() {
140 return Err(DataMessageValidationError::EmptyLabel);
141 }
142 if self.label.len() > MAX_DATA_LABEL_BYTES {
143 return Err(DataMessageValidationError::LabelTooLong {
144 actual: self.label.len(),
145 maximum: MAX_DATA_LABEL_BYTES,
146 });
147 }
148 if self.label.chars().any(char::is_control) {
149 return Err(DataMessageValidationError::LabelContainsControl);
150 }
151
152 if self.content_type.is_empty() {
153 return Err(DataMessageValidationError::EmptyContentType);
154 }
155 if self.content_type.len() > MAX_CONTENT_TYPE_BYTES {
156 return Err(DataMessageValidationError::ContentTypeTooLong {
157 actual: self.content_type.len(),
158 maximum: MAX_CONTENT_TYPE_BYTES,
159 });
160 }
161 if self.content_type.chars().any(char::is_control) {
162 return Err(DataMessageValidationError::ContentTypeContainsControl);
163 }
164 if !valid_content_type(&self.content_type) {
165 return Err(DataMessageValidationError::InvalidContentType);
166 }
167
168 if self.bytes.len() > MAX_DATA_MESSAGE_BYTES {
169 return Err(DataMessageValidationError::BodyTooLarge {
170 actual: self.bytes.len(),
171 maximum: MAX_DATA_MESSAGE_BYTES,
172 });
173 }
174 if self.message_id.as_str().is_empty() {
175 return Err(DataMessageValidationError::EmptyMessageId);
176 }
177 if self.message_id.as_str().len() > MAX_DATA_MESSAGE_ID_BYTES {
178 return Err(DataMessageValidationError::MessageIdTooLong {
179 actual: self.message_id.as_str().len(),
180 maximum: MAX_DATA_MESSAGE_ID_BYTES,
181 });
182 }
183 if self.message_id.as_str().chars().any(char::is_control) {
184 return Err(DataMessageValidationError::MessageIdContainsControl);
185 }
186 self.reliability.validate()
187 }
188}
189
190fn valid_content_type(value: &str) -> bool {
191 let mut sections = value.split(';');
192 let media_type = sections.next().unwrap_or_default().trim();
193 let Some((type_name, subtype)) = media_type.split_once('/') else {
194 return false;
195 };
196 if !mime_token(type_name) || !mime_token(subtype) {
197 return false;
198 }
199 sections.all(|parameter| {
200 let parameter = parameter.trim();
201 !parameter.is_empty()
202 && parameter
203 .chars()
204 .all(|character| character.is_ascii() && !character.is_ascii_control())
205 })
206}
207
208fn mime_token(value: &str) -> bool {
209 !value.is_empty()
210 && value.bytes().all(|byte| {
211 byte.is_ascii_alphanumeric()
212 || matches!(
213 byte,
214 b'!' | b'#'
215 | b'$'
216 | b'%'
217 | b'&'
218 | b'\''
219 | b'*'
220 | b'+'
221 | b'-'
222 | b'.'
223 | b'^'
224 | b'_'
225 | b'`'
226 | b'|'
227 | b'~'
228 )
229 })
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn message(label: &str, content_type: &str, body: Vec<u8>) -> DataMessage {
237 DataMessage {
238 label: label.into(),
239 content_type: content_type.into(),
240 bytes: Bytes::from(body),
241 reliability: DataReliability::ReliableOrdered,
242 message_id: MessageId::new(),
243 }
244 }
245
246 #[test]
247 fn accepts_boundary_sized_valid_message() {
248 let value = message(
249 &"l".repeat(MAX_DATA_LABEL_BYTES),
250 "application/vnd.bridgefu+json; charset=utf-8",
251 vec![0; MAX_DATA_MESSAGE_BYTES],
252 );
253 assert_eq!(value.validate(), Ok(()));
254 }
255
256 #[test]
257 fn rejects_invalid_labels() {
258 assert_eq!(
259 message("", "text/plain", vec![]).validate(),
260 Err(DataMessageValidationError::EmptyLabel)
261 );
262 assert!(matches!(
263 message(&"x".repeat(129), "text/plain", vec![]).validate(),
264 Err(DataMessageValidationError::LabelTooLong { .. })
265 ));
266 assert_eq!(
267 message("bad\nlabel", "text/plain", vec![]).validate(),
268 Err(DataMessageValidationError::LabelContainsControl)
269 );
270 }
271
272 #[test]
273 fn rejects_invalid_content_types() {
274 assert_eq!(
275 message("label", "", vec![]).validate(),
276 Err(DataMessageValidationError::EmptyContentType)
277 );
278 for invalid in [
279 "plain",
280 "text/",
281 "/plain",
282 "text /plain",
283 "text/plain\r\nx:y",
284 ] {
285 assert!(message("label", invalid, vec![]).validate().is_err());
286 }
287 assert!(matches!(
288 message("label", &"x".repeat(256), vec![]).validate(),
289 Err(DataMessageValidationError::ContentTypeTooLong { .. })
290 ));
291 }
292
293 #[test]
294 fn rejects_oversized_body_and_invalid_lifetime() {
295 assert!(matches!(
296 message("label", "application/octet-stream", vec![0; 65_537]).validate(),
297 Err(DataMessageValidationError::BodyTooLarge { .. })
298 ));
299 assert_eq!(
300 DataReliability::MaxLifetime {
301 ordered: true,
302 milliseconds: 0,
303 }
304 .validate(),
305 Err(DataMessageValidationError::ZeroLifetime)
306 );
307 assert!(matches!(
308 DataReliability::MaxLifetime {
309 ordered: false,
310 milliseconds: 65_536,
311 }
312 .validate(),
313 Err(DataMessageValidationError::LifetimeTooLarge { .. })
314 ));
315 assert_eq!(
316 DataReliability::MaxRetransmits {
317 ordered: false,
318 count: 0,
319 }
320 .validate(),
321 Ok(())
322 );
323 }
324
325 #[test]
326 fn rejects_invalid_message_ids() {
327 let mut value = message("label", "text/plain", vec![]);
328 value.message_id = MessageId::from_string("");
329 assert_eq!(
330 value.validate(),
331 Err(DataMessageValidationError::EmptyMessageId)
332 );
333
334 value.message_id = MessageId::from_string("x".repeat(MAX_DATA_MESSAGE_ID_BYTES + 1));
335 assert!(matches!(
336 value.validate(),
337 Err(DataMessageValidationError::MessageIdTooLong { .. })
338 ));
339
340 value.message_id = MessageId::from_string("msg_bad\nvalue");
341 assert_eq!(
342 value.validate(),
343 Err(DataMessageValidationError::MessageIdContainsControl)
344 );
345 }
346
347 #[test]
348 fn debug_keeps_message_content_and_identifiers_private() {
349 const CANARY: &str = "data-message-diagnostic-canary\r\nAuthorization: exposed";
350 let value = DataMessage {
351 label: CANARY.into(),
352 content_type: "application/octet-stream".into(),
353 bytes: Bytes::copy_from_slice(CANARY.as_bytes()),
354 reliability: DataReliability::ReliableOrdered,
355 message_id: MessageId::from_string(CANARY),
356 };
357 let rendered = format!("{value:?}");
358 assert!(!rendered.contains(CANARY), "message leaked: {rendered}");
359 assert_eq!(value.label, CANARY);
360 assert_eq!(value.bytes.as_ref(), CANARY.as_bytes());
361 assert_eq!(value.message_id.as_str(), CANARY);
362 }
363}