Skip to main content

running_process/broker/protocol/
validate.rs

1//! Shared v1 frame-envelope validation (#376).
2//!
3//! Client and broker validate the same four `Frame` envelope fields in
4//! the same order before touching a payload: `envelope_version`, `kind`,
5//! `payload_protocol`, `payload_encoding`. This module centralizes that
6//! check once; each call site keeps its own error type by mapping the
7//! neutral [`FrameValidationError`] onto its existing errors, so observable
8//! behavior (error variants, refusal codes, message strings) is unchanged.
9//!
10//! The expected frame kind and payload protocol differ per call site
11//! (client expects `RESPONSE` frames, the broker Hello path expects
12//! `REQUEST`, the handoff relay expects `EVENT`), so both are explicit
13//! parameters rather than baked-in defaults.
14
15use crate::broker::protocol::registry::PROTOCOL_VERSION;
16use crate::broker::protocol::{Frame, FrameKind, PayloadEncoding};
17
18/// Neutral envelope-validation failure.
19///
20/// Carries the offending wire values so callers can render their existing
21/// diagnostics; it deliberately does NOT pick an error mapping (client
22/// `BrokerClientError` vs broker `Refused` vs handoff static strings differ
23/// and must stay byte-for-byte identical to their pre-#376 behavior).
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum FrameValidationError {
26    /// `Frame.envelope_version` is not [`PROTOCOL_VERSION`].
27    EnvelopeVersion {
28        /// Version found on the frame.
29        actual: u32,
30    },
31    /// `Frame.kind` is not the expected kind for this exchange.
32    Kind {
33        /// Kind the call site requires.
34        expected: FrameKind,
35        /// Raw kind value found on the frame.
36        actual: i32,
37    },
38    /// `Frame.payload_protocol` is not the expected payload protocol.
39    PayloadProtocol {
40        /// Payload protocol the call site requires.
41        expected: u32,
42        /// Payload protocol found on the frame.
43        actual: u32,
44    },
45    /// `Frame.payload_encoding` is not `PAYLOAD_ENCODING_NONE`.
46    PayloadEncoding {
47        /// Raw encoding value found on the frame.
48        actual: i32,
49    },
50}
51
52/// Validate the four v1 envelope fields shared by every framed exchange.
53///
54/// Check order is fixed (version, kind, payload protocol, encoding) and
55/// matches every pre-existing call site, so the first failure reported is
56/// identical to the previous hand-rolled validators.
57pub fn validate_frame_envelope(
58    frame: &Frame,
59    expected_kind: FrameKind,
60    expected_payload_protocol: u32,
61) -> Result<(), FrameValidationError> {
62    if frame.envelope_version != PROTOCOL_VERSION {
63        return Err(FrameValidationError::EnvelopeVersion {
64            actual: frame.envelope_version,
65        });
66    }
67    if FrameKind::try_from(frame.kind) != Ok(expected_kind) {
68        return Err(FrameValidationError::Kind {
69            expected: expected_kind,
70            actual: frame.kind,
71        });
72    }
73    if frame.payload_protocol != expected_payload_protocol {
74        return Err(FrameValidationError::PayloadProtocol {
75            expected: expected_payload_protocol,
76            actual: frame.payload_protocol,
77        });
78    }
79    if PayloadEncoding::try_from(frame.payload_encoding) != Ok(PayloadEncoding::None) {
80        return Err(FrameValidationError::PayloadEncoding {
81            actual: frame.payload_encoding,
82        });
83    }
84    Ok(())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::{validate_frame_envelope, FrameValidationError};
90    use crate::broker::protocol::registry::{
91        CONTROL_PAYLOAD_PROTOCOL, HANDOFF_PAYLOAD_PROTOCOL, PROTOCOL_VERSION,
92    };
93    use crate::broker::protocol::{Frame, FrameKind, PayloadEncoding};
94
95    fn valid_frame(kind: FrameKind, payload_protocol: u32) -> Frame {
96        Frame {
97            envelope_version: PROTOCOL_VERSION,
98            kind: kind as i32,
99            payload_protocol,
100            payload: Vec::new(),
101            request_id: 7,
102            payload_encoding: PayloadEncoding::None as i32,
103            deadline_unix_ms: 0,
104            traceparent: String::new(),
105            tracestate: String::new(),
106        }
107    }
108
109    #[test]
110    fn accepts_well_formed_envelope() {
111        let frame = valid_frame(FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL);
112        assert_eq!(
113            validate_frame_envelope(&frame, FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL),
114            Ok(())
115        );
116    }
117
118    #[test]
119    fn rejects_wrong_envelope_version_first() {
120        let mut frame = valid_frame(FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL);
121        frame.envelope_version = 2;
122        // Also break later fields to prove version is checked first.
123        frame.payload_protocol = HANDOFF_PAYLOAD_PROTOCOL;
124        assert_eq!(
125            validate_frame_envelope(&frame, FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL),
126            Err(FrameValidationError::EnvelopeVersion { actual: 2 })
127        );
128    }
129
130    #[test]
131    fn rejects_unexpected_kind() {
132        let frame = valid_frame(FrameKind::Event, CONTROL_PAYLOAD_PROTOCOL);
133        assert_eq!(
134            validate_frame_envelope(&frame, FrameKind::Response, CONTROL_PAYLOAD_PROTOCOL),
135            Err(FrameValidationError::Kind {
136                expected: FrameKind::Response,
137                actual: FrameKind::Event as i32,
138            })
139        );
140    }
141
142    #[test]
143    fn rejects_unknown_kind_value() {
144        let mut frame = valid_frame(FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL);
145        frame.kind = 99;
146        assert_eq!(
147            validate_frame_envelope(&frame, FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL),
148            Err(FrameValidationError::Kind {
149                expected: FrameKind::Request,
150                actual: 99,
151            })
152        );
153    }
154
155    #[test]
156    fn rejects_unexpected_payload_protocol() {
157        let frame = valid_frame(FrameKind::Request, HANDOFF_PAYLOAD_PROTOCOL);
158        assert_eq!(
159            validate_frame_envelope(&frame, FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL),
160            Err(FrameValidationError::PayloadProtocol {
161                expected: CONTROL_PAYLOAD_PROTOCOL,
162                actual: HANDOFF_PAYLOAD_PROTOCOL,
163            })
164        );
165    }
166
167    #[test]
168    fn rejects_compressed_payload() {
169        let mut frame = valid_frame(FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL);
170        frame.payload_encoding = PayloadEncoding::Zstd as i32;
171        assert_eq!(
172            validate_frame_envelope(&frame, FrameKind::Request, CONTROL_PAYLOAD_PROTOCOL),
173            Err(FrameValidationError::PayloadEncoding {
174                actual: PayloadEncoding::Zstd as i32,
175            })
176        );
177    }
178}