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;
5
6/// BCX protocol version negotiated by a transport binding.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct ProtocolVersion {
9    /// Major version.
10    pub major: u16,
11    /// Minor version.
12    pub minor: u16,
13}
14
15impl ProtocolVersion {
16    /// Current implemented protocol version.
17    pub const CURRENT: Self = Self::new(1, 0);
18
19    /// Creates a protocol version.
20    #[must_use]
21    pub const fn new(major: u16, minor: u16) -> Self {
22        Self { major, minor }
23    }
24}
25
26/// Conservative limits applied before expensive parsing or verification.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct WireLimits {
29    /// Maximum single message length in bytes.
30    maximum_message_len: usize,
31    /// Maximum parent events in a compact cause capsule.
32    maximum_parent_events: usize,
33    /// Maximum WHY graph traversal depth.
34    maximum_why_depth: usize,
35    /// Maximum events returned by one explanation query.
36    maximum_explanation_events: usize,
37}
38
39impl WireLimits {
40    /// Hard upper bound for one canonical message.
41    pub const MAXIMUM_MESSAGE_LEN: usize = 16 * 1024 * 1024;
42    /// Hard upper bound for compact parent references.
43    pub const MAXIMUM_PARENT_EVENTS: usize = 1_024;
44    /// Hard upper bound for WHY traversal depth.
45    pub const MAXIMUM_WHY_DEPTH: usize = 32;
46    /// Hard upper bound for events returned in one explanation.
47    pub const MAXIMUM_EXPLANATION_EVENTS: usize = 10_000;
48
49    /// Default limits for the first development profile.
50    pub const DEVELOPMENT: Self = Self {
51        maximum_message_len: 1_048_576,
52        maximum_parent_events: 16,
53        maximum_why_depth: 5,
54        maximum_explanation_events: 100,
55    };
56
57    /// Creates validated wire limits.
58    pub const fn new(
59        maximum_message_len: usize,
60        maximum_parent_events: usize,
61        maximum_why_depth: usize,
62        maximum_explanation_events: usize,
63    ) -> Result<Self, ValidationError> {
64        if maximum_message_len == 0
65            || maximum_parent_events == 0
66            || maximum_why_depth == 0
67            || maximum_explanation_events == 0
68        {
69            return Err(ValidationError::Empty);
70        }
71        if maximum_message_len > Self::MAXIMUM_MESSAGE_LEN
72            || maximum_parent_events > Self::MAXIMUM_PARENT_EVENTS
73            || maximum_why_depth > Self::MAXIMUM_WHY_DEPTH
74            || maximum_explanation_events > Self::MAXIMUM_EXPLANATION_EVENTS
75        {
76            return Err(ValidationError::TooLarge);
77        }
78        Ok(Self {
79            maximum_message_len,
80            maximum_parent_events,
81            maximum_why_depth,
82            maximum_explanation_events,
83        })
84    }
85
86    /// Returns the maximum single message length in bytes.
87    #[must_use]
88    pub const fn maximum_message_len(self) -> usize {
89        self.maximum_message_len
90    }
91
92    /// Returns the maximum parent events in a compact cause capsule.
93    #[must_use]
94    pub const fn maximum_parent_events(self) -> usize {
95        self.maximum_parent_events
96    }
97
98    /// Returns the maximum WHY graph traversal depth.
99    #[must_use]
100    pub const fn maximum_why_depth(self) -> usize {
101        self.maximum_why_depth
102    }
103
104    /// Returns the maximum events returned by one explanation query.
105    #[must_use]
106    pub const fn maximum_explanation_events(self) -> usize {
107        self.maximum_explanation_events
108    }
109}
110
111/// Fixed header metadata common to native and HTTP-carried BCX messages.
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub struct WireHeader {
114    /// Protocol version.
115    pub version: ProtocolVersion,
116    /// Canonical payload length in bytes.
117    pub payload_len: usize,
118}
119
120impl WireHeader {
121    /// Validates protocol version and payload length.
122    pub const fn validate(&self, limits: WireLimits) -> Result<(), ValidationError> {
123        if self.version.major != ProtocolVersion::CURRENT.major {
124            return Err(ValidationError::NotPermitted);
125        }
126        if self.version.minor != ProtocolVersion::CURRENT.minor {
127            return Err(ValidationError::NotPermitted);
128        }
129        if self.payload_len == 0 {
130            return Err(ValidationError::Empty);
131        }
132        if self.payload_len > limits.maximum_message_len() {
133            return Err(ValidationError::TooLarge);
134        }
135        Ok(())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn header_rejects_empty_payload() {
145        let header = WireHeader {
146            version: ProtocolVersion::CURRENT,
147            payload_len: 0,
148        };
149
150        assert_eq!(
151            header.validate(WireLimits::DEVELOPMENT),
152            Err(ValidationError::Empty)
153        );
154    }
155
156    #[test]
157    fn header_rejects_future_minor_version() {
158        let header = WireHeader {
159            version: ProtocolVersion::new(1, 1),
160            payload_len: 1,
161        };
162
163        assert_eq!(
164            header.validate(WireLimits::DEVELOPMENT),
165            Err(ValidationError::NotPermitted)
166        );
167    }
168
169    #[test]
170    fn limits_reject_unbounded_values() {
171        assert_eq!(
172            WireLimits::new(usize::MAX, 1, 1, 1),
173            Err(ValidationError::TooLarge)
174        );
175        assert_eq!(WireLimits::new(1, 0, 1, 1), Err(ValidationError::Empty));
176    }
177}