Skip to main content

rvoip_core_traits/
data.rs

1//! Transport-neutral reliable/unreliable data messages.
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use thiserror::Error;
7
8use crate::ids::MessageId;
9
10/// Delivery contract requested by an application data message.
11#[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/// A data-plane message that can be mapped to a WebRTC DataChannel, UCTP
44/// `message.send`, SIP MESSAGE, or an application-owned metadata transport.
45#[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}