Skip to main content

sccp_protocol/message/
bounded.rs

1//! Size-bounded storage for opaque or partially understood wire fields.
2//!
3//! [`BoundedBytes`] makes allocation limits part of a message type. Use
4//! [`BoundedBytes::new`] or a `TryFrom` implementation at trust boundaries,
5//! then borrow the retained value with [`BoundedBytes::as_bytes`].
6
7use std::error::Error;
8use std::fmt;
9use std::ops::Deref;
10
11/// Owned wire bytes whose allocation is capped by the message contract.
12///
13/// The bytes are otherwise uninterpreted. Message-specific decoders can read
14/// every complete field they understand while retaining the rest verbatim.
15#[derive(Clone, Default, Eq, Hash, PartialEq)]
16pub struct BoundedBytes<const MAX: usize>(Box<[u8]>);
17
18impl<const MAX: usize> BoundedBytes<MAX> {
19    /// Retains `bytes` when its length does not exceed `MAX`.
20    ///
21    /// The error reports both the configured maximum and the supplied length;
22    /// the rejected allocation is not retained.
23    pub fn new(bytes: impl Into<Box<[u8]>>) -> Result<Self, BoundedBytesError> {
24        let bytes = bytes.into();
25        if bytes.len() > MAX {
26            return Err(BoundedBytesError {
27                maximum: MAX,
28                actual: bytes.len(),
29            });
30        }
31        Ok(Self(bytes))
32    }
33
34    pub const fn maximum_len() -> usize {
35        MAX
36    }
37
38    pub fn as_bytes(&self) -> &[u8] {
39        &self.0
40    }
41
42    pub fn len(&self) -> usize {
43        self.0.len()
44    }
45
46    pub fn is_empty(&self) -> bool {
47        self.0.is_empty()
48    }
49
50    pub fn into_boxed_slice(self) -> Box<[u8]> {
51        self.0
52    }
53}
54
55impl<const MAX: usize> AsRef<[u8]> for BoundedBytes<MAX> {
56    fn as_ref(&self) -> &[u8] {
57        self.as_bytes()
58    }
59}
60
61impl<const MAX: usize> Deref for BoundedBytes<MAX> {
62    type Target = [u8];
63
64    fn deref(&self) -> &Self::Target {
65        self.as_bytes()
66    }
67}
68
69impl<const MAX: usize> TryFrom<Vec<u8>> for BoundedBytes<MAX> {
70    type Error = BoundedBytesError;
71
72    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
73        Self::new(bytes)
74    }
75}
76
77impl<const MAX: usize> TryFrom<Box<[u8]>> for BoundedBytes<MAX> {
78    type Error = BoundedBytesError;
79
80    fn try_from(bytes: Box<[u8]>) -> Result<Self, Self::Error> {
81        Self::new(bytes)
82    }
83}
84
85impl<const MAX: usize> TryFrom<&[u8]> for BoundedBytes<MAX> {
86    type Error = BoundedBytesError;
87
88    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
89        Self::new(bytes)
90    }
91}
92
93impl<const MAX: usize> From<BoundedBytes<MAX>> for Box<[u8]> {
94    fn from(bytes: BoundedBytes<MAX>) -> Self {
95        bytes.into_boxed_slice()
96    }
97}
98
99impl<const MAX: usize> From<BoundedBytes<MAX>> for Vec<u8> {
100    fn from(bytes: BoundedBytes<MAX>) -> Self {
101        bytes.into_boxed_slice().into_vec()
102    }
103}
104
105impl<const MAX: usize> fmt::Debug for BoundedBytes<MAX> {
106    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107        formatter
108            .debug_struct("BoundedBytes")
109            .field("len", &self.len())
110            .field("maximum", &MAX)
111            .finish()
112    }
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116/// Failure returned when a value exceeds a [`BoundedBytes`] allocation limit.
117pub struct BoundedBytesError {
118    pub maximum: usize,
119    pub actual: usize,
120}
121
122impl fmt::Display for BoundedBytesError {
123    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(
125            formatter,
126            "payload contains {} bytes, exceeding the {}-byte bound",
127            self.actual, self.maximum
128        )
129    }
130}
131
132impl Error for BoundedBytesError {}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn accepts_every_length_through_the_bound() {
140        for length in 0..=8 {
141            let bytes = BoundedBytes::<8>::try_from(vec![0xa5; length]).unwrap();
142            assert_eq!(bytes.len(), length);
143            assert!(bytes.iter().all(|byte| *byte == 0xa5));
144        }
145    }
146
147    #[test]
148    fn rejects_the_first_oversized_value_without_retaining_it() {
149        assert_eq!(
150            BoundedBytes::<8>::try_from(vec![0xa5; 9]).unwrap_err(),
151            BoundedBytesError {
152                maximum: 8,
153                actual: 9,
154            }
155        );
156    }
157
158    #[test]
159    fn debug_reports_shape_without_contents() {
160        let bytes = BoundedBytes::<8>::try_from(b"secret".as_slice()).unwrap();
161        let rendered = format!("{bytes:?}");
162        assert!(rendered.contains("len: 6"));
163        assert!(!rendered.contains("secret"));
164    }
165}