Skip to main content

bcx_wire/
lib.rs

1#![no_std]
2#![doc = "Wire versioning and bounded-message primitives for BCX."]
3
4use bcx_core::ValidationError;
5use core::convert::TryFrom;
6
7/// BCX protocol version negotiated by a transport binding.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct ProtocolVersion {
10    major: u16,
11    minor: u16,
12}
13
14impl ProtocolVersion {
15    /// Current implemented protocol version.
16    pub const CURRENT: Self = Self::new(1, 0);
17
18    /// Creates a protocol version.
19    #[must_use]
20    pub const fn new(major: u16, minor: u16) -> Self {
21        Self { major, minor }
22    }
23
24    /// Returns the major protocol version.
25    #[must_use]
26    pub const fn major(self) -> u16 {
27        self.major
28    }
29
30    /// Returns the minor protocol version.
31    #[must_use]
32    pub const fn minor(self) -> u16 {
33        self.minor
34    }
35}
36
37/// Conservative limits applied before expensive parsing or verification.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct WireLimits {
40    /// Maximum single message length in bytes.
41    maximum_message_len: usize,
42    /// Maximum parent events in a compact cause capsule.
43    maximum_parent_events: usize,
44    /// Maximum WHY graph traversal depth.
45    maximum_why_depth: usize,
46    /// Maximum events returned by one explanation query.
47    maximum_explanation_events: usize,
48}
49
50impl WireLimits {
51    /// Hard upper bound for one canonical message.
52    pub const MAXIMUM_MESSAGE_LEN: usize = 16 * 1024 * 1024;
53    /// Hard upper bound for compact parent references.
54    pub const MAXIMUM_PARENT_EVENTS: usize = 1_024;
55    /// Hard upper bound for WHY traversal depth.
56    pub const MAXIMUM_WHY_DEPTH: usize = 32;
57    /// Hard upper bound for events returned in one explanation.
58    pub const MAXIMUM_EXPLANATION_EVENTS: usize = 10_000;
59
60    /// Unsafe development/test limits.
61    ///
62    /// Production profiles must construct explicit limits with `WireLimits::new`
63    /// rather than reusing this convenience constant.
64    pub const UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION: Self = Self {
65        maximum_message_len: 1_048_576,
66        maximum_parent_events: 16,
67        maximum_why_depth: 5,
68        maximum_explanation_events: 100,
69    };
70
71    /// Creates validated wire limits.
72    pub const fn new(
73        maximum_message_len: usize,
74        maximum_parent_events: usize,
75        maximum_why_depth: usize,
76        maximum_explanation_events: usize,
77    ) -> Result<Self, ValidationError> {
78        if maximum_message_len == 0
79            || maximum_parent_events == 0
80            || maximum_why_depth == 0
81            || maximum_explanation_events == 0
82        {
83            return Err(ValidationError::Empty);
84        }
85        if maximum_message_len > Self::MAXIMUM_MESSAGE_LEN
86            || maximum_parent_events > Self::MAXIMUM_PARENT_EVENTS
87            || maximum_why_depth > Self::MAXIMUM_WHY_DEPTH
88            || maximum_explanation_events > Self::MAXIMUM_EXPLANATION_EVENTS
89        {
90            return Err(ValidationError::TooLarge);
91        }
92        Ok(Self {
93            maximum_message_len,
94            maximum_parent_events,
95            maximum_why_depth,
96            maximum_explanation_events,
97        })
98    }
99
100    /// Returns the maximum single message length in bytes.
101    #[must_use]
102    pub const fn maximum_message_len(self) -> usize {
103        self.maximum_message_len
104    }
105
106    /// Returns the maximum parent events in a compact cause capsule.
107    #[must_use]
108    pub const fn maximum_parent_events(self) -> usize {
109        self.maximum_parent_events
110    }
111
112    /// Returns the maximum WHY graph traversal depth.
113    #[must_use]
114    pub const fn maximum_why_depth(self) -> usize {
115        self.maximum_why_depth
116    }
117
118    /// Returns the maximum events returned by one explanation query.
119    #[must_use]
120    pub const fn maximum_explanation_events(self) -> usize {
121        self.maximum_explanation_events
122    }
123}
124
125/// Fixed header metadata common to profile-carried BCX messages.
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct WireHeader {
128    version: ProtocolVersion,
129    payload_len: u32,
130}
131
132impl WireHeader {
133    /// Creates a validated wire header.
134    pub fn new(
135        version: ProtocolVersion,
136        payload_len: u32,
137        limits: WireLimits,
138    ) -> Result<Self, ValidationError> {
139        let header = Self {
140            version,
141            payload_len,
142        };
143        match header.validate(limits) {
144            Ok(()) => Ok(header),
145            Err(error) => Err(error),
146        }
147    }
148
149    /// Validates protocol version and payload length.
150    pub(crate) fn validate(&self, limits: WireLimits) -> Result<(), ValidationError> {
151        if self.version.major() != ProtocolVersion::CURRENT.major() {
152            return Err(ValidationError::NotPermitted);
153        }
154        if self.version.minor() != ProtocolVersion::CURRENT.minor() {
155            return Err(ValidationError::NotPermitted);
156        }
157        if self.payload_len == 0 {
158            return Err(ValidationError::Empty);
159        }
160        let payload_len =
161            usize::try_from(self.payload_len).map_err(|_| ValidationError::TooLarge)?;
162        if payload_len > limits.maximum_message_len() {
163            return Err(ValidationError::TooLarge);
164        }
165        Ok(())
166    }
167
168    /// Returns the protocol version.
169    #[must_use]
170    pub const fn version(self) -> ProtocolVersion {
171        self.version
172    }
173
174    /// Returns the canonical payload length in bytes.
175    #[must_use]
176    pub const fn payload_len(self) -> u32 {
177        self.payload_len
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn header_rejects_empty_payload() {
187        assert_eq!(
188            WireHeader::new(
189                ProtocolVersion::CURRENT,
190                0,
191                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
192            ),
193            Err(ValidationError::Empty)
194        );
195    }
196
197    #[test]
198    fn header_rejects_future_minor_version() {
199        assert_eq!(
200            WireHeader::new(
201                ProtocolVersion::new(1, 1),
202                1,
203                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
204            ),
205            Err(ValidationError::NotPermitted)
206        );
207    }
208
209    #[test]
210    fn limits_reject_unbounded_values() {
211        assert_eq!(
212            WireLimits::new(usize::MAX, 1, 1, 1),
213            Err(ValidationError::TooLarge)
214        );
215        assert_eq!(WireLimits::new(1, 0, 1, 1), Err(ValidationError::Empty));
216    }
217}