Skip to main content

sccp_protocol/message/
wire.rs

1//! SCCP frame encoding and incremental stream decoding.
2//!
3//! Construct an outbound [`Frame`] and call [`Frame::encode`]. For inbound TCP
4//! data, retain one [`FrameDecoder`] per connection and feed each received
5//! chunk to [`FrameDecoder::push`]; it handles both fragmented and coalesced
6//! frames while enforcing [`MAX_FRAME_SIZE`]. Typed message decoding happens
7//! after framing through the message enums in the parent module.
8
9use std::fmt;
10use std::io::Cursor;
11
12use binrw::{BinRead, BinWrite};
13use thiserror::Error;
14
15use super::catalog::MessageId;
16use super::catalog::MessageRoute;
17
18/// Number of bytes in an SCCP frame header, including the message identifier.
19pub const HEADER_SIZE: usize = 12;
20/// Largest complete frame accepted or emitted by the framing layer.
21pub const MAX_FRAME_SIZE: usize = 8192;
22
23/// The fixed SCCP framing header. Keeping this separate from [`Frame`] lets
24/// the streaming decoder inspect a header before the complete payload has
25/// arrived, while still giving encoding and decoding one declarative layout.
26#[derive(BinRead, BinWrite, Clone, Copy, Debug, Eq, PartialEq)]
27#[brw(little)]
28struct WireHeader {
29    wire_len: u32,
30    protocol_version: u32,
31    message_id: u32,
32}
33
34#[derive(Clone, Eq, PartialEq)]
35/// One complete SCCP frame with an uninterpreted payload.
36///
37/// [`FrameDecoder`] produces frames from a byte stream. Call the appropriate
38/// typed message decoder afterward to validate the payload contract.
39pub struct Frame {
40    pub protocol_version: u32,
41    pub message_id: u32,
42    pub payload: Vec<u8>,
43}
44
45impl fmt::Debug for Frame {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("Frame")
48            .field("protocol_version", &self.protocol_version)
49            .field("message_id", &format_args!("0x{:04x}", self.message_id))
50            .field("payload_len", &self.payload.len())
51            .finish()
52    }
53}
54
55impl Frame {
56    /// Creates a frame without validating its payload or total encoded size.
57    ///
58    /// Size validation occurs in [`Frame::encode`] or in typed message codecs.
59    pub fn new(protocol_version: u32, message_id: u32, payload: Vec<u8>) -> Self {
60        Self {
61            protocol_version,
62            message_id,
63            payload,
64        }
65    }
66
67    /// SCCP length includes the four-byte message ID, but excludes the first
68    /// two header words.
69    pub fn encode(&self) -> Result<Vec<u8>, CodecError> {
70        let wire_len = self
71            .payload
72            .len()
73            .checked_add(4)
74            .ok_or(CodecError::FrameTooLarge(usize::MAX))?;
75        let total = wire_len + 8;
76        if total > MAX_FRAME_SIZE {
77            return Err(CodecError::FrameTooLarge(total));
78        }
79        let header = WireHeader {
80            wire_len: u32::try_from(wire_len).map_err(|_| CodecError::FrameTooLarge(wire_len))?,
81            protocol_version: self.protocol_version,
82            message_id: self.message_id,
83        };
84        let mut output = Cursor::new(Vec::with_capacity(total));
85        header
86            .write(&mut output)
87            .map_err(|error| CodecError::wire("encode", self.message_id, &error))?;
88        output.get_mut().extend_from_slice(&self.payload);
89        Ok(output.into_inner())
90    }
91
92    /// Resolves the numeric identifier to its typed catalog value.
93    ///
94    /// Unrecognized identifiers become [`MessageId::Unknown`].
95    pub fn message_type(&self) -> MessageId {
96        MessageId::from(self.message_id)
97    }
98}
99
100#[derive(Debug, Error, Clone, Eq, PartialEq)]
101/// Validation and serialization failures produced by framing and message codecs.
102pub enum CodecError {
103    /// The header length cannot describe a valid frame.
104    #[error("SCCP frame length {0} is invalid")]
105    InvalidLength(u32),
106    /// The complete frame would exceed [`MAX_FRAME_SIZE`].
107    #[error("SCCP frame size {0} exceeds the configured maximum")]
108    FrameTooLarge(usize),
109    /// A fixed or mandatory payload prefix is incomplete.
110    #[error("message 0x{message_id:04x} is truncated: need {needed} bytes, got {actual}")]
111    Truncated {
112        message_id: u32,
113        needed: usize,
114        actual: usize,
115    },
116    /// A station identifier failed its textual or structural validation.
117    #[error("invalid SCCP device ID: {0}")]
118    InvalidDeviceId(String),
119    /// A runtime device definition violates a message-level constraint.
120    #[error("invalid SCCP device definition: {0}")]
121    InvalidDefinition(String),
122    /// A wire text field is not valid under the selected text policy.
123    #[error("invalid UTF-8-compatible SCCP text field")]
124    InvalidText,
125    /// The requested protocol version is outside the supported range.
126    #[error("unsupported SCCP protocol version {0}")]
127    UnsupportedProtocol(u32),
128    /// A known message was decoded through the wrong protocol role.
129    #[error("message 0x{message_id:04x} has route {actual:?}, expected {expected}")]
130    UnexpectedRoute {
131        message_id: u32,
132        actual: MessageRoute,
133        expected: &'static str,
134    },
135    /// A scalar field contains a value forbidden by its wire contract.
136    #[error("message 0x{message_id:04x} contains invalid {field}: {value}")]
137    InvalidValue {
138        message_id: u32,
139        field: &'static str,
140        value: u64,
141    },
142    /// A declared or decoded item count exceeds its bounded capacity.
143    #[error("message 0x{message_id:04x} {field} count {count} exceeds maximum {maximum}")]
144    CountTooLarge {
145        message_id: u32,
146        field: &'static str,
147        count: usize,
148        maximum: usize,
149    },
150    /// Reserved bytes in a fixed field are not zero-filled.
151    #[error("message 0x{message_id:04x} contains non-zero {field} padding")]
152    NonZeroPadding {
153        message_id: u32,
154        field: &'static str,
155    },
156    /// Bytes remain after decoding a layout that requires exact consumption.
157    #[error("message 0x{message_id:04x} has {count} unexpected trailing bytes")]
158    TrailingBytes { message_id: u32, count: usize },
159    /// A payload that requires 32-bit alignment has an invalid length.
160    #[error(
161        "message 0x{message_id:04x} payload length {actual} is not aligned to a four-byte boundary"
162    )]
163    InvalidAlignment { message_id: u32, actual: usize },
164    /// A text value exceeds the capacity of its wire field.
165    #[error(
166        "message 0x{message_id:04x} field {field} is too long: {actual} bytes, maximum {maximum}"
167    )]
168    TextTooLong {
169        message_id: u32,
170        field: &'static str,
171        actual: usize,
172        maximum: usize,
173    },
174    /// Secret key material exceeds its fixed wire capacity.
175    #[error("secret field {field} is too long: {actual} bytes, maximum {maximum}")]
176    SecretTooLong {
177        field: &'static str,
178        actual: usize,
179        maximum: usize,
180    },
181    /// The binary reader or writer failed at a known wire offset.
182    #[error("could not {operation} SCCP message 0x{message_id:04x} at byte {offset}: {detail}")]
183    Wire {
184        operation: &'static str,
185        message_id: u32,
186        offset: u64,
187        detail: String,
188    },
189}
190
191impl CodecError {
192    pub(crate) fn wire(operation: &'static str, message_id: u32, error: &binrw::Error) -> Self {
193        let offset = match error {
194            binrw::Error::BadMagic { pos, .. }
195            | binrw::Error::AssertFail { pos, .. }
196            | binrw::Error::Custom { pos, .. }
197            | binrw::Error::NoVariantMatch { pos }
198            | binrw::Error::EnumErrors { pos, .. } => *pos,
199            binrw::Error::Io(_) | binrw::Error::Backtrace(_) => 0,
200            _ => 0,
201        };
202        Self::Wire {
203            operation,
204            message_id,
205            offset,
206            detail: error.to_string(),
207        }
208    }
209}
210
211#[derive(Debug, Default)]
212/// Incremental decoder for an SCCP byte stream.
213///
214/// The decoder retains an incomplete trailing frame between calls. Complete
215/// frames are returned in stream order, including when a chunk contains more
216/// than one frame.
217pub struct FrameDecoder {
218    buffer: Vec<u8>,
219}
220
221impl FrameDecoder {
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// Appends a stream chunk and returns every newly completed frame.
227    ///
228    /// An empty vector means the buffered bytes do not yet complete a frame.
229    /// Framing errors are terminal for the current buffered stream; callers
230    /// should discard the decoder with the connection.
231    pub fn push(&mut self, bytes: &[u8]) -> Result<Vec<Frame>, CodecError> {
232        self.buffer.extend_from_slice(bytes);
233        let mut frames = Vec::new();
234        loop {
235            if self.buffer.len() < HEADER_SIZE {
236                break;
237            }
238            let header = WireHeader::read(&mut Cursor::new(&self.buffer[..HEADER_SIZE]))
239                .map_err(|error| CodecError::wire("decode header for", 0, &error))?;
240            let length = header.wire_len;
241            if length < 4 {
242                return Err(CodecError::InvalidLength(length));
243            }
244            let total =
245                usize::try_from(length).map_err(|_| CodecError::FrameTooLarge(usize::MAX))? + 8;
246            if total > MAX_FRAME_SIZE {
247                return Err(CodecError::FrameTooLarge(total));
248            }
249            if self.buffer.len() < total {
250                break;
251            }
252            let payload = self.buffer[HEADER_SIZE..total].to_vec();
253            self.buffer.drain(..total);
254            frames.push(Frame {
255                protocol_version: header.protocol_version,
256                message_id: header.message_id,
257                payload,
258            });
259        }
260        debug_assert!(self.buffer.len() < MAX_FRAME_SIZE);
261        Ok(frames)
262    }
263
264    pub fn buffered_len(&self) -> usize {
265        self.buffer.len()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn one_large_chunk_of_small_frames_is_drained_incrementally() {
275        let encoded = Frame::new(22, 0x0100, Vec::new()).encode().unwrap();
276        let frame_count = MAX_FRAME_SIZE * 2 / encoded.len() + 1;
277        let stream = encoded.repeat(frame_count);
278        assert!(stream.len() > MAX_FRAME_SIZE * 2);
279
280        let frames = FrameDecoder::new().push(&stream).unwrap();
281
282        assert_eq!(frames.len(), frame_count);
283        assert!(frames.iter().all(|frame| {
284            frame.protocol_version == 22 && frame.message_id == 0x0100 && frame.payload.is_empty()
285        }));
286    }
287
288    #[test]
289    fn frame_length_matches_skinny_header_definition() {
290        let frame = Frame::new(22, 0x0006, vec![1, 2, 3, 4]);
291        let encoded = frame.encode().unwrap();
292        assert_eq!(&encoded[..4], &8_u32.to_le_bytes());
293        assert_eq!(encoded.len(), 16);
294    }
295
296    #[test]
297    fn decoder_handles_fragmented_and_coalesced_frames() {
298        let first = Frame::new(0, 0, Vec::new()).encode().unwrap();
299        let second = Frame::new(22, 6, vec![1; 8]).encode().unwrap();
300        let mut decoder = FrameDecoder::new();
301        assert!(decoder.push(&first[..5]).unwrap().is_empty());
302        let mut rest = first[5..].to_vec();
303        rest.extend_from_slice(&second);
304        rest.extend_from_slice(&first);
305        rest.extend_from_slice(&second);
306        let frames = decoder.push(&rest).unwrap();
307        assert_eq!(
308            frames
309                .iter()
310                .map(|frame| frame.message_id)
311                .collect::<Vec<_>>(),
312            [0, 6, 0, 6]
313        );
314        assert_eq!(frames[1], frames[3], "duplicate frames changed in transit");
315        assert_eq!(frames[0], frames[2], "reordered frames changed in transit");
316    }
317
318    #[test]
319    fn decoder_accepts_every_possible_single_fragment_boundary() {
320        let bytes = Frame::new(22, 0x22, (0_u8..64).collect()).encode().unwrap();
321        for split in 0..bytes.len() {
322            let mut decoder = FrameDecoder::new();
323            assert!(decoder.push(&bytes[..split]).unwrap().is_empty());
324            let frames = decoder.push(&bytes[split..]).unwrap();
325            assert_eq!(frames.len(), 1, "split at byte {split}");
326            assert_eq!(frames[0].payload, (0_u8..64).collect::<Vec<_>>());
327            assert_eq!(decoder.buffered_len(), 0);
328        }
329    }
330
331    #[test]
332    fn invalid_short_length_is_rejected() {
333        let mut decoder = FrameDecoder::new();
334        let mut bytes = vec![0; 12];
335        bytes[..4].copy_from_slice(&3_u32.to_le_bytes());
336        assert_eq!(decoder.push(&bytes), Err(CodecError::InvalidLength(3)));
337    }
338}