quic_reverse_control/
messages.rs

1// Copyright 2024-2026 Farlight Networks, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Protocol message definitions.
16//!
17//! This module defines all control plane messages exchanged between peers
18//! during a quic-reverse session.
19
20use bitflags::bitflags;
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23
24/// Current protocol version.
25pub const PROTOCOL_VERSION: u16 = 1;
26
27/// Identifies a logical service for multiplexing.
28///
29/// Services are identified by string names such as "ssh", "http", or "tcp".
30/// The service ID is used to route incoming stream requests to the appropriate
31/// handler.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct ServiceId(pub String);
34
35impl ServiceId {
36    /// Creates a new service identifier.
37    #[must_use]
38    pub fn new(name: impl Into<String>) -> Self {
39        Self(name.into())
40    }
41
42    /// Returns the service name as a string slice.
43    #[must_use]
44    pub fn as_str(&self) -> &str {
45        &self.0
46    }
47}
48
49impl std::fmt::Display for ServiceId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.0)
52    }
53}
54
55impl From<&str> for ServiceId {
56    fn from(s: &str) -> Self {
57        Self(s.to_owned())
58    }
59}
60
61impl From<String> for ServiceId {
62    fn from(s: String) -> Self {
63        Self(s)
64    }
65}
66
67/// Metadata attached to stream open requests.
68///
69/// Metadata can be empty, raw bytes, or a structured key-value map.
70/// The format is negotiated during the `Hello`/`HelloAck` exchange.
71#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
72pub enum Metadata {
73    /// No metadata.
74    #[default]
75    Empty,
76    /// Raw byte payload.
77    Bytes(Vec<u8>),
78    /// Structured key-value pairs.
79    Structured(HashMap<String, MetadataValue>),
80}
81
82/// A value within structured metadata.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub enum MetadataValue {
85    /// String value.
86    String(String),
87    /// Integer value.
88    Integer(i64),
89    /// Boolean value.
90    Boolean(bool),
91    /// Binary data.
92    Bytes(Vec<u8>),
93}
94
95impl Eq for MetadataValue {}
96
97bitflags! {
98    /// Feature flags negotiated during `Hello`/`HelloAck` exchange.
99    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
100    pub struct Features: u32 {
101        /// Support for structured metadata in `OpenRequest`.
102        const STRUCTURED_METADATA = 0b0000_0001;
103        /// Support for Ping/Pong keep-alive messages.
104        const PING_PONG = 0b0000_0010;
105        /// Support for stream priority hints.
106        const STREAM_PRIORITY = 0b0000_0100;
107    }
108}
109
110impl Default for Features {
111    fn default() -> Self {
112        Self::empty()
113    }
114}
115
116bitflags! {
117    /// Flags for `OpenRequest` messages.
118    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119    pub struct OpenFlags: u8 {
120        /// Request a unidirectional stream (send only).
121        const UNIDIRECTIONAL = 0b0000_0001;
122        /// High priority stream hint.
123        const HIGH_PRIORITY = 0b0000_0010;
124    }
125}
126
127impl Default for OpenFlags {
128    fn default() -> Self {
129        Self::empty()
130    }
131}
132
133/// All protocol messages that can be exchanged on the control stream.
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub enum ProtocolMessage {
136    /// Initial handshake message.
137    Hello(Hello),
138    /// Handshake acknowledgment.
139    HelloAck(HelloAck),
140    /// Request to open a reverse stream.
141    OpenRequest(OpenRequest),
142    /// Response to an open request.
143    OpenResponse(OpenResponse),
144    /// Notification that a stream has closed.
145    StreamClose(StreamClose),
146    /// Keep-alive ping.
147    Ping(Ping),
148    /// Keep-alive pong.
149    Pong(Pong),
150}
151
152/// Initial handshake message sent by both peers.
153///
154/// Each peer sends a Hello message after the QUIC connection is established.
155/// The messages are used to negotiate protocol version and features.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct Hello {
158    /// Protocol version supported by this peer.
159    pub protocol_version: u16,
160    /// Feature flags supported by this peer.
161    pub features: Features,
162    /// Optional agent identifier (e.g., "quic-reverse/0.1.0").
163    pub agent: Option<String>,
164}
165
166impl Hello {
167    /// Creates a new Hello message with the current protocol version.
168    #[must_use]
169    pub const fn new(features: Features) -> Self {
170        Self {
171            protocol_version: PROTOCOL_VERSION,
172            features,
173            agent: None,
174        }
175    }
176
177    /// Sets the agent identifier.
178    #[must_use]
179    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
180        self.agent = Some(agent.into());
181        self
182    }
183}
184
185/// Handshake acknowledgment confirming negotiated parameters.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct HelloAck {
188    /// Selected protocol version (highest mutually supported).
189    pub selected_version: u16,
190    /// Selected feature set (intersection of both peers' features).
191    pub selected_features: Features,
192}
193
194/// Request to open a reverse stream.
195///
196/// Sent by the peer that wants to initiate a reverse stream. The receiving
197/// peer will respond with an `OpenResponse` and, if accepted, open a new
198/// QUIC stream back to the initiator.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct OpenRequest {
201    /// Unique identifier for this request, used to correlate responses.
202    pub request_id: u64,
203    /// Target service identifier.
204    pub service: ServiceId,
205    /// Optional metadata for the stream.
206    pub metadata: Metadata,
207    /// Request flags.
208    pub flags: OpenFlags,
209}
210
211impl OpenRequest {
212    /// Creates a new open request for the specified service.
213    #[must_use]
214    pub fn new(request_id: u64, service: impl Into<ServiceId>) -> Self {
215        Self {
216            request_id,
217            service: service.into(),
218            metadata: Metadata::Empty,
219            flags: OpenFlags::empty(),
220        }
221    }
222
223    /// Sets the metadata for this request.
224    #[must_use]
225    pub fn with_metadata(mut self, metadata: Metadata) -> Self {
226        self.metadata = metadata;
227        self
228    }
229
230    /// Sets the flags for this request.
231    #[must_use]
232    pub const fn with_flags(mut self, flags: OpenFlags) -> Self {
233        self.flags = flags;
234        self
235    }
236}
237
238/// Response to an `OpenRequest`.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct OpenResponse {
241    /// Request ID from the corresponding `OpenRequest`.
242    pub request_id: u64,
243    /// Result of the open request.
244    pub status: OpenStatus,
245    /// Optional reason message (typically for rejections).
246    pub reason: Option<String>,
247    /// Logical stream ID assigned to this stream (if accepted).
248    pub logical_stream_id: Option<u64>,
249}
250
251impl OpenResponse {
252    /// Creates an accepted response with the given logical stream ID.
253    #[must_use]
254    pub const fn accepted(request_id: u64, logical_stream_id: u64) -> Self {
255        Self {
256            request_id,
257            status: OpenStatus::Accepted,
258            reason: None,
259            logical_stream_id: Some(logical_stream_id),
260        }
261    }
262
263    /// Creates a rejected response with the given code and optional reason.
264    #[must_use]
265    pub const fn rejected(request_id: u64, code: RejectCode, reason: Option<String>) -> Self {
266        Self {
267            request_id,
268            status: OpenStatus::Rejected(code),
269            reason,
270            logical_stream_id: None,
271        }
272    }
273}
274
275/// Status of an `OpenRequest`.
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub enum OpenStatus {
278    /// Request accepted; stream will be opened.
279    Accepted,
280    /// Request rejected with the given code.
281    Rejected(RejectCode),
282}
283
284/// Reason codes for rejecting an `OpenRequest`.
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
286pub enum RejectCode {
287    /// The requested service is not available.
288    ServiceUnavailable,
289    /// The requested service is not supported.
290    UnsupportedService,
291    /// Resource limits have been exceeded.
292    LimitExceeded,
293    /// The request is not authorized.
294    Unauthorized,
295    /// An internal error occurred.
296    InternalError,
297}
298
299impl std::fmt::Display for RejectCode {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            Self::ServiceUnavailable => write!(f, "service unavailable"),
303            Self::UnsupportedService => write!(f, "unsupported service"),
304            Self::LimitExceeded => write!(f, "limit exceeded"),
305            Self::Unauthorized => write!(f, "unauthorized"),
306            Self::InternalError => write!(f, "internal error"),
307        }
308    }
309}
310
311/// Notification that a stream has closed.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct StreamClose {
314    /// Logical stream ID of the closed stream.
315    pub logical_stream_id: u64,
316    /// Close code indicating the reason.
317    pub code: CloseCode,
318    /// Optional human-readable reason.
319    pub reason: Option<String>,
320}
321
322impl StreamClose {
323    /// Creates a normal close notification.
324    #[must_use]
325    pub const fn normal(logical_stream_id: u64) -> Self {
326        Self {
327            logical_stream_id,
328            code: CloseCode::Normal,
329            reason: None,
330        }
331    }
332
333    /// Creates an error close notification.
334    #[must_use]
335    pub fn error(logical_stream_id: u64, reason: impl Into<String>) -> Self {
336        Self {
337            logical_stream_id,
338            code: CloseCode::Error,
339            reason: Some(reason.into()),
340        }
341    }
342}
343
344/// Close codes for `StreamClose` messages.
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
346pub enum CloseCode {
347    /// Normal closure.
348    Normal,
349    /// Error condition.
350    Error,
351    /// Timeout expired.
352    Timeout,
353    /// Stream was reset.
354    Reset,
355}
356
357impl CloseCode {
358    /// Returns the numeric code for wire transmission.
359    #[must_use]
360    pub const fn as_u8(self) -> u8 {
361        match self {
362            Self::Normal => 0,
363            Self::Error => 1,
364            Self::Timeout => 2,
365            Self::Reset => 3,
366        }
367    }
368}
369
370/// Keep-alive ping message.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct Ping {
373    /// Sequence number for matching with Pong responses.
374    pub sequence: u64,
375}
376
377/// Keep-alive pong response.
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct Pong {
380    /// Sequence number from the corresponding Ping.
381    pub sequence: u64,
382}
383
384/// Stream binding frame sent on data streams.
385///
386/// When a data stream is opened, the first frame sent must be a `StreamBind`
387/// to identify which logical stream this QUIC stream belongs to. This allows
388/// the receiving peer to match the data stream with the corresponding
389/// `OpenRequest`/`OpenResponse` exchange.
390///
391/// # Wire Format
392///
393/// The stream bind frame is encoded as:
394/// - 4 bytes: magic number (`0x51524256`, "QRBV" for "Quic Reverse Bind Version")
395/// - 1 byte: version (currently 1)
396/// - 8 bytes: `logical_stream_id` (big-endian u64)
397///
398/// Total: 13 bytes
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400pub struct StreamBind {
401    /// Logical stream ID assigned during `OpenResponse`.
402    pub logical_stream_id: u64,
403}
404
405impl StreamBind {
406    /// Magic number identifying the stream bind frame.
407    pub const MAGIC: [u8; 4] = [0x51, 0x52, 0x42, 0x56]; // "QRBV"
408
409    /// Current stream bind version.
410    pub const VERSION: u8 = 1;
411
412    /// Size of the encoded stream bind frame.
413    pub const ENCODED_SIZE: usize = 13; // 4 + 1 + 8
414
415    /// Creates a new stream bind frame.
416    #[must_use]
417    pub const fn new(logical_stream_id: u64) -> Self {
418        Self { logical_stream_id }
419    }
420
421    /// Encodes the stream bind to bytes.
422    #[must_use]
423    pub fn encode(&self) -> [u8; Self::ENCODED_SIZE] {
424        let mut buf = [0u8; Self::ENCODED_SIZE];
425        buf[0..4].copy_from_slice(&Self::MAGIC);
426        buf[4] = Self::VERSION;
427        buf[5..13].copy_from_slice(&self.logical_stream_id.to_be_bytes());
428        buf
429    }
430
431    /// Decodes a stream bind from bytes.
432    ///
433    /// Returns `None` if the magic number is invalid or the version is unsupported.
434    #[must_use]
435    pub fn decode(buf: &[u8; Self::ENCODED_SIZE]) -> Option<Self> {
436        if buf[0..4] != Self::MAGIC {
437            return None;
438        }
439        if buf[4] != Self::VERSION {
440            return None;
441        }
442        let logical_stream_id = u64::from_be_bytes([
443            buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12],
444        ]);
445        Some(Self { logical_stream_id })
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    // Property-based testing with proptest
454    mod proptest_tests {
455        use super::*;
456        use crate::{BincodeCodec, Codec};
457        use proptest::prelude::*;
458
459        // Strategy for generating arbitrary Features
460        fn arb_features() -> impl Strategy<Value = Features> {
461            (0u32..8).prop_map(Features::from_bits_truncate)
462        }
463
464        // Strategy for generating arbitrary OpenFlags
465        fn arb_open_flags() -> impl Strategy<Value = OpenFlags> {
466            (0u8..4).prop_map(OpenFlags::from_bits_truncate)
467        }
468
469        // Strategy for generating arbitrary ServiceId
470        fn arb_service_id() -> impl Strategy<Value = ServiceId> {
471            "[a-z][a-z0-9_-]{0,31}".prop_map(ServiceId::new)
472        }
473
474        // Strategy for generating arbitrary MetadataValue
475        fn arb_metadata_value() -> impl Strategy<Value = MetadataValue> {
476            prop_oneof![
477                ".*".prop_map(MetadataValue::String),
478                any::<i64>().prop_map(MetadataValue::Integer),
479                any::<bool>().prop_map(MetadataValue::Boolean),
480                prop::collection::vec(any::<u8>(), 0..64).prop_map(MetadataValue::Bytes),
481            ]
482        }
483
484        // Strategy for generating arbitrary Metadata
485        fn arb_metadata() -> impl Strategy<Value = Metadata> {
486            prop_oneof![
487                Just(Metadata::Empty),
488                prop::collection::vec(any::<u8>(), 0..128).prop_map(Metadata::Bytes),
489                prop::collection::hash_map("[a-z]{1,16}", arb_metadata_value(), 0..8)
490                    .prop_map(Metadata::Structured),
491            ]
492        }
493
494        // Strategy for generating arbitrary RejectCode
495        fn arb_reject_code() -> impl Strategy<Value = RejectCode> {
496            prop_oneof![
497                Just(RejectCode::ServiceUnavailable),
498                Just(RejectCode::UnsupportedService),
499                Just(RejectCode::LimitExceeded),
500                Just(RejectCode::Unauthorized),
501                Just(RejectCode::InternalError),
502            ]
503        }
504
505        // Strategy for generating arbitrary CloseCode
506        fn arb_close_code() -> impl Strategy<Value = CloseCode> {
507            prop_oneof![
508                Just(CloseCode::Normal),
509                Just(CloseCode::Reset),
510                Just(CloseCode::Timeout),
511                Just(CloseCode::Error),
512            ]
513        }
514
515        // Strategy for generating arbitrary Hello
516        fn arb_hello() -> impl Strategy<Value = Hello> {
517            (arb_features(), proptest::option::of(".*")).prop_map(|(features, agent)| {
518                let mut hello = Hello::new(features);
519                hello.agent = agent;
520                hello
521            })
522        }
523
524        // Strategy for generating arbitrary HelloAck
525        fn arb_hello_ack() -> impl Strategy<Value = HelloAck> {
526            (any::<u16>(), arb_features()).prop_map(|(version, features)| HelloAck {
527                selected_version: version,
528                selected_features: features,
529            })
530        }
531
532        // Strategy for generating arbitrary OpenRequest
533        fn arb_open_request() -> impl Strategy<Value = OpenRequest> {
534            (
535                any::<u64>(),
536                arb_service_id(),
537                arb_metadata(),
538                arb_open_flags(),
539            )
540                .prop_map(|(request_id, service, metadata, flags)| OpenRequest {
541                    request_id,
542                    service,
543                    metadata,
544                    flags,
545                })
546        }
547
548        // Strategy for generating arbitrary OpenResponse
549        fn arb_open_response() -> impl Strategy<Value = OpenResponse> {
550            (
551                any::<u64>(),
552                prop_oneof![
553                    Just(OpenStatus::Accepted),
554                    arb_reject_code().prop_map(OpenStatus::Rejected),
555                ],
556                proptest::option::of(".*"),
557                proptest::option::of(any::<u64>()),
558            )
559                .prop_map(|(request_id, status, reason, logical_stream_id)| {
560                    OpenResponse {
561                        request_id,
562                        status,
563                        reason,
564                        logical_stream_id,
565                    }
566                })
567        }
568
569        // Strategy for generating arbitrary StreamClose
570        fn arb_stream_close() -> impl Strategy<Value = StreamClose> {
571            (any::<u64>(), arb_close_code(), proptest::option::of(".*")).prop_map(
572                |(logical_stream_id, code, reason)| StreamClose {
573                    logical_stream_id,
574                    code,
575                    reason,
576                },
577            )
578        }
579
580        // Strategy for generating arbitrary Ping
581        fn arb_ping() -> impl Strategy<Value = Ping> {
582            any::<u64>().prop_map(|sequence| Ping { sequence })
583        }
584
585        // Strategy for generating arbitrary Pong
586        fn arb_pong() -> impl Strategy<Value = Pong> {
587            any::<u64>().prop_map(|sequence| Pong { sequence })
588        }
589
590        // Strategy for generating arbitrary ProtocolMessage
591        fn arb_protocol_message() -> impl Strategy<Value = ProtocolMessage> {
592            prop_oneof![
593                arb_hello().prop_map(ProtocolMessage::Hello),
594                arb_hello_ack().prop_map(ProtocolMessage::HelloAck),
595                arb_open_request().prop_map(ProtocolMessage::OpenRequest),
596                arb_open_response().prop_map(ProtocolMessage::OpenResponse),
597                arb_stream_close().prop_map(ProtocolMessage::StreamClose),
598                arb_ping().prop_map(ProtocolMessage::Ping),
599                arb_pong().prop_map(ProtocolMessage::Pong),
600            ]
601        }
602
603        proptest! {
604            #![proptest_config(ProptestConfig::with_cases(1000))]
605
606            #[test]
607            fn protocol_message_round_trip(msg in arb_protocol_message()) {
608                let codec = BincodeCodec::new();
609                let encoded = codec.encode(&msg).expect("encoding should succeed");
610                let decoded: ProtocolMessage = codec.decode(&encoded).expect("decoding should succeed");
611                prop_assert_eq!(msg, decoded);
612            }
613
614            #[test]
615            fn hello_round_trip(msg in arb_hello()) {
616                let codec = BincodeCodec::new();
617                let wrapped = ProtocolMessage::Hello(msg.clone());
618                let encoded = codec.encode(&wrapped).expect("encoding should succeed");
619                let decoded: ProtocolMessage = codec.decode(&encoded).expect("decoding should succeed");
620                prop_assert_eq!(ProtocolMessage::Hello(msg), decoded);
621            }
622
623            #[test]
624            fn open_request_round_trip(msg in arb_open_request()) {
625                let codec = BincodeCodec::new();
626                let wrapped = ProtocolMessage::OpenRequest(msg.clone());
627                let encoded = codec.encode(&wrapped).expect("encoding should succeed");
628                let decoded: ProtocolMessage = codec.decode(&encoded).expect("decoding should succeed");
629                prop_assert_eq!(ProtocolMessage::OpenRequest(msg), decoded);
630            }
631
632            #[test]
633            fn stream_bind_round_trip(id in any::<u64>()) {
634                let bind = StreamBind::new(id);
635                let encoded = bind.encode();
636                let decoded = StreamBind::decode(&encoded).expect("decode should succeed");
637                prop_assert_eq!(bind.logical_stream_id, decoded.logical_stream_id);
638            }
639
640            #[test]
641            fn service_id_preserves_content(s in "[a-z][a-z0-9_-]{0,63}") {
642                let id = ServiceId::new(&s);
643                prop_assert_eq!(id.as_str(), s.as_str());
644                prop_assert_eq!(format!("{id}"), s);
645            }
646        }
647    }
648
649    #[test]
650    fn service_id_from_str() {
651        let id: ServiceId = "ssh".into();
652        assert_eq!(id.as_str(), "ssh");
653    }
654
655    #[test]
656    fn service_id_display() {
657        let id = ServiceId::new("http");
658        assert_eq!(format!("{id}"), "http");
659    }
660
661    #[test]
662    fn hello_with_agent() {
663        let hello = Hello::new(Features::PING_PONG).with_agent("test/1.0");
664        assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
665        assert_eq!(hello.features, Features::PING_PONG);
666        assert_eq!(hello.agent.as_deref(), Some("test/1.0"));
667    }
668
669    #[test]
670    fn open_request_builder() {
671        let req = OpenRequest::new(42, "tcp")
672            .with_metadata(Metadata::Bytes(vec![1, 2, 3]))
673            .with_flags(OpenFlags::HIGH_PRIORITY);
674
675        assert_eq!(req.request_id, 42);
676        assert_eq!(req.service.as_str(), "tcp");
677        assert_eq!(req.metadata, Metadata::Bytes(vec![1, 2, 3]));
678        assert!(req.flags.contains(OpenFlags::HIGH_PRIORITY));
679    }
680
681    #[test]
682    fn open_response_accepted() {
683        let resp = OpenResponse::accepted(42, 100);
684        assert_eq!(resp.request_id, 42);
685        assert_eq!(resp.status, OpenStatus::Accepted);
686        assert_eq!(resp.logical_stream_id, Some(100));
687    }
688
689    #[test]
690    fn open_response_rejected() {
691        let resp = OpenResponse::rejected(42, RejectCode::Unauthorized, Some("denied".into()));
692        assert_eq!(resp.request_id, 42);
693        assert_eq!(resp.status, OpenStatus::Rejected(RejectCode::Unauthorized));
694        assert_eq!(resp.reason.as_deref(), Some("denied"));
695        assert_eq!(resp.logical_stream_id, None);
696    }
697
698    #[test]
699    fn stream_close_normal() {
700        let close = StreamClose::normal(99);
701        assert_eq!(close.logical_stream_id, 99);
702        assert_eq!(close.code, CloseCode::Normal);
703        assert!(close.reason.is_none());
704    }
705
706    #[test]
707    fn features_intersection() {
708        let a = Features::PING_PONG | Features::STRUCTURED_METADATA;
709        let b = Features::PING_PONG | Features::STREAM_PRIORITY;
710        let intersection = a & b;
711        assert_eq!(intersection, Features::PING_PONG);
712    }
713
714    #[test]
715    fn stream_bind_encode_decode() {
716        let bind = StreamBind::new(0x0102_0304_0506_0708);
717        let encoded = bind.encode();
718
719        // Check magic number
720        assert_eq!(&encoded[0..4], &StreamBind::MAGIC);
721        // Check version
722        assert_eq!(encoded[4], StreamBind::VERSION);
723        // Check logical_stream_id (big-endian)
724        assert_eq!(
725            &encoded[5..13],
726            &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
727        );
728
729        // Decode and verify
730        let decoded = StreamBind::decode(&encoded).expect("decode should succeed");
731        assert_eq!(decoded.logical_stream_id, 0x0102_0304_0506_0708);
732    }
733
734    #[test]
735    fn stream_bind_invalid_magic() {
736        let mut buf = [0u8; StreamBind::ENCODED_SIZE];
737        buf[0..4].copy_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Wrong magic
738        buf[4] = StreamBind::VERSION;
739        assert!(StreamBind::decode(&buf).is_none());
740    }
741
742    #[test]
743    fn stream_bind_invalid_version() {
744        let mut buf = [0u8; StreamBind::ENCODED_SIZE];
745        buf[0..4].copy_from_slice(&StreamBind::MAGIC);
746        buf[4] = 0xFF; // Wrong version
747        assert!(StreamBind::decode(&buf).is_none());
748    }
749}