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    /// Default limits for the first development profile.
61    pub const DEVELOPMENT: Self = Self {
62        maximum_message_len: 1_048_576,
63        maximum_parent_events: 16,
64        maximum_why_depth: 5,
65        maximum_explanation_events: 100,
66    };
67
68    /// Creates validated wire limits.
69    pub const fn new(
70        maximum_message_len: usize,
71        maximum_parent_events: usize,
72        maximum_why_depth: usize,
73        maximum_explanation_events: usize,
74    ) -> Result<Self, ValidationError> {
75        if maximum_message_len == 0
76            || maximum_parent_events == 0
77            || maximum_why_depth == 0
78            || maximum_explanation_events == 0
79        {
80            return Err(ValidationError::Empty);
81        }
82        if maximum_message_len > Self::MAXIMUM_MESSAGE_LEN
83            || maximum_parent_events > Self::MAXIMUM_PARENT_EVENTS
84            || maximum_why_depth > Self::MAXIMUM_WHY_DEPTH
85            || maximum_explanation_events > Self::MAXIMUM_EXPLANATION_EVENTS
86        {
87            return Err(ValidationError::TooLarge);
88        }
89        Ok(Self {
90            maximum_message_len,
91            maximum_parent_events,
92            maximum_why_depth,
93            maximum_explanation_events,
94        })
95    }
96
97    /// Returns the maximum single message length in bytes.
98    #[must_use]
99    pub const fn maximum_message_len(self) -> usize {
100        self.maximum_message_len
101    }
102
103    /// Returns the maximum parent events in a compact cause capsule.
104    #[must_use]
105    pub const fn maximum_parent_events(self) -> usize {
106        self.maximum_parent_events
107    }
108
109    /// Returns the maximum WHY graph traversal depth.
110    #[must_use]
111    pub const fn maximum_why_depth(self) -> usize {
112        self.maximum_why_depth
113    }
114
115    /// Returns the maximum events returned by one explanation query.
116    #[must_use]
117    pub const fn maximum_explanation_events(self) -> usize {
118        self.maximum_explanation_events
119    }
120}
121
122/// Fixed header metadata common to profile-carried BCX messages.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub struct WireHeader {
125    version: ProtocolVersion,
126    payload_len: u32,
127}
128
129impl WireHeader {
130    /// Creates a validated wire header.
131    pub fn new(
132        version: ProtocolVersion,
133        payload_len: u32,
134        limits: WireLimits,
135    ) -> Result<Self, ValidationError> {
136        let header = Self {
137            version,
138            payload_len,
139        };
140        match header.validate(limits) {
141            Ok(()) => Ok(header),
142            Err(error) => Err(error),
143        }
144    }
145
146    /// Validates protocol version and payload length.
147    pub fn validate(&self, limits: WireLimits) -> Result<(), ValidationError> {
148        if self.version.major() != ProtocolVersion::CURRENT.major() {
149            return Err(ValidationError::NotPermitted);
150        }
151        if self.version.minor() != ProtocolVersion::CURRENT.minor() {
152            return Err(ValidationError::NotPermitted);
153        }
154        if self.payload_len == 0 {
155            return Err(ValidationError::Empty);
156        }
157        let payload_len =
158            usize::try_from(self.payload_len).map_err(|_| ValidationError::TooLarge)?;
159        if payload_len > limits.maximum_message_len() {
160            return Err(ValidationError::TooLarge);
161        }
162        Ok(())
163    }
164
165    /// Returns the protocol version.
166    #[must_use]
167    pub const fn version(self) -> ProtocolVersion {
168        self.version
169    }
170
171    /// Returns the canonical payload length in bytes.
172    #[must_use]
173    pub const fn payload_len(self) -> u32 {
174        self.payload_len
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn header_rejects_empty_payload() {
184        assert_eq!(
185            WireHeader::new(ProtocolVersion::CURRENT, 0, WireLimits::DEVELOPMENT),
186            Err(ValidationError::Empty)
187        );
188    }
189
190    #[test]
191    fn header_rejects_future_minor_version() {
192        assert_eq!(
193            WireHeader::new(ProtocolVersion::new(1, 1), 1, WireLimits::DEVELOPMENT),
194            Err(ValidationError::NotPermitted)
195        );
196    }
197
198    #[test]
199    fn limits_reject_unbounded_values() {
200        assert_eq!(
201            WireLimits::new(usize::MAX, 1, 1, 1),
202            Err(ValidationError::TooLarge)
203        );
204        assert_eq!(WireLimits::new(1, 0, 1, 1), Err(ValidationError::Empty));
205    }
206}