1#![forbid(unsafe_code)]
32
33use std::{error::Error, fmt, path::PathBuf};
34
35use serde::{Deserialize, Serialize};
36
37pub mod frame;
38pub mod manifest;
39pub mod session;
40pub mod tool_call;
41
42pub mod error_codes {
47 pub const UNKNOWN_MODULE: &str = "unknown_module";
48 pub const MODULE_REMOVED: &str = "module_removed";
49 pub const MODULE_RELOADING: &str = "module_reloading";
50 pub const MODULE_WARMING: &str = "module_warming";
51 pub const TARGET_UNAVAILABLE: &str = "target_unavailable";
52 pub const MODULE_TIMEOUT: &str = "module_timeout";
53 pub const MODULE_NO_PROTOCOL: &str = "module_no_protocol";
65
66 pub fn is_retryable_route_open(code: &str) -> bool {
86 matches!(
87 code,
88 MODULE_RELOADING | MODULE_WARMING | TARGET_UNAVAILABLE | MODULE_TIMEOUT
89 )
90 }
91}
92
93pub use frame::{Frame, FrameBuildError};
94
95#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
97#[serde(rename_all = "snake_case")]
98pub enum RouteCloseReason {
99 Reload,
100 Restart,
101 Disable,
102 Crash,
103 CapabilityDenied,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129#[non_exhaustive]
130pub struct BindIdentity {
131 pub project_root: PathBuf,
132 pub harness: String,
133 pub session: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub project_id: Option<String>,
153}
154
155impl BindIdentity {
156 pub fn new(
161 project_root: impl Into<PathBuf>,
162 harness: impl Into<String>,
163 session: impl Into<String>,
164 ) -> Self {
165 Self {
166 project_root: project_root.into(),
167 harness: harness.into(),
168 session: session.into(),
169 project_id: None,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(tag = "kind", rename_all = "snake_case")]
177pub enum Principal {
178 Reserved { module_id: String },
180 Direct,
182 Unverified,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
199#[serde(tag = "kind", rename_all = "snake_case")]
200pub enum RouteTarget {
201 ToolProvider {
202 module_id: String,
203 },
204 ManagementSurface {
205 module_id: String,
206 },
207 InternalService {
208 module_id: String,
209 service_id: String,
210 },
211}
212
213pub const PROTOCOL_VERSION: u8 = 2;
215
216pub const SUBC_PROTOCOL_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
225
226pub const MIN_SUPPORTED_VERSION: u8 = 2;
228
229pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
232
233pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
238
239pub const HEADER_LEN: usize = 21;
241
242pub const FROZEN_PREFIX_LEN: usize = 5;
246
247pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
263pub struct ErrorBody {
264 pub code: String,
265 pub message: String,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub detail: Option<serde_json::Value>,
268}
269
270impl ErrorBody {
271 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
273 Self {
274 code: code.into(),
275 message: message.into(),
276 detail: None,
277 }
278 }
279
280 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
282 self.detail = Some(detail);
283 self
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
289pub struct ModuleHelloBody {
290 pub manifest: manifest::ModuleManifest,
291 pub protocol_ver: u8,
292 #[serde(default)]
293 pub control_ops: Option<Vec<String>>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub launch_nonce: Option<String>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
307pub struct ModuleHelloAckBody {
308 pub negotiated_ver: u8,
309 pub subc_ops: Vec<String>,
310 pub subc_capabilities: Vec<String>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub storage: Option<serde_json::Value>,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326#[repr(u8)]
327pub enum FrameType {
328 Request = 0,
329 Response = 1,
330 Push = 2,
331 StreamData = 3,
332 StreamEnd = 4,
333 Error = 5,
334 Cancel = 6,
335 Ping = 7,
336 Pong = 8,
337 Hello = 9,
338 HelloAck = 10,
339 Goodbye = 11,
340}
341
342impl FrameType {
343 pub fn from_u8(b: u8) -> Option<Self> {
345 Some(match b {
346 0 => Self::Request,
347 1 => Self::Response,
348 2 => Self::Push,
349 3 => Self::StreamData,
350 4 => Self::StreamEnd,
351 5 => Self::Error,
352 6 => Self::Cancel,
353 7 => Self::Ping,
354 8 => Self::Pong,
355 9 => Self::Hello,
356 10 => Self::HelloAck,
357 11 => Self::Goodbye,
358 _ => return None,
359 })
360 }
361
362 pub fn is_pure_header(self) -> bool {
363 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
364 }
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370#[repr(u8)]
371pub enum Priority {
372 Passive = 0,
373 Interactive = 1,
374 Background = 2,
375}
376
377impl Priority {
378 fn from_bits(bits: u8) -> Option<Self> {
379 Some(match bits {
380 0 => Self::Passive,
381 1 => Self::Interactive,
382 2 => Self::Background,
383 _ => return None,
384 })
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390#[repr(u8)]
391pub enum AdmissionClass {
392 Normal = 0,
393 Expedite = 1,
394 Sheddable = 2,
395}
396
397impl AdmissionClass {
398 fn from_bits(bits: u8) -> Option<Self> {
399 Some(match bits {
400 0 => Self::Normal,
401 1 => Self::Expedite,
402 2 => Self::Sheddable,
403 _ => return None,
404 })
405 }
406}
407
408const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
411const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
414pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
415pub const FLAG_SUBSCRIPTION: u8 = 0b1000_0000;
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct Flags(pub u8);
421
422impl Flags {
423 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
425 let mut b = 0u8;
426 if binary {
427 b |= FLAG_BINARY;
428 }
429 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
430 if last {
431 b |= FLAG_LAST;
432 }
433 Flags(b)
434 }
435
436 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
438 self.0 =
439 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
440 self
441 }
442
443 pub fn is_binary(self) -> bool {
445 self.0 & FLAG_BINARY != 0
446 }
447
448 pub fn is_last(self) -> bool {
450 self.0 & FLAG_LAST != 0
451 }
452
453 pub fn priority(self) -> Option<Priority> {
455 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
456 }
457
458 pub fn admission_class(self) -> Option<AdmissionClass> {
460 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
461 }
462
463 pub fn is_subscription(self) -> bool {
465 self.0 & FLAG_SUBSCRIPTION != 0
466 }
467
468 pub fn is_daemon_origin(self) -> bool {
470 self.0 & FLAG_DAEMON_ORIGIN != 0
471 }
472
473 pub fn with_daemon_origin(mut self) -> Self {
475 self.0 |= FLAG_DAEMON_ORIGIN;
476 self
477 }
478
479 pub fn without_daemon_origin(self) -> Self {
481 Self(self.0 & !FLAG_DAEMON_ORIGIN)
482 }
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub struct EnvelopeHeader {
488 pub len: u32,
490 pub ver: u8,
492 pub ty: FrameType,
494 pub flags: Flags,
496 pub channel: u16,
498 pub epoch: u32,
500 pub corr: u64,
502}
503
504impl EnvelopeHeader {
505 pub fn encode(&self) -> [u8; HEADER_LEN] {
507 let mut buf = [0u8; HEADER_LEN];
508 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
509 buf[4] = self.ver;
510 buf[5] = self.ty as u8;
511 buf[6] = self.flags.0;
512 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
513 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
514 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
515 buf
516 }
517}
518
519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
521pub enum DecodeError {
522 TooShortForPrefix { have: usize },
524 UnsupportedVersion { ver: u8 },
526 TooShortForHeader { have: usize, need: usize },
528 UnknownFrameType { byte: u8 },
530 ReservedFlagBits { flags: u8 },
532 ReservedPriorityBits { flags: u8 },
534 ReservedAdmissionClass { flags: u8 },
536 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
538 NonzeroEpochOnControlChannel { epoch: u32 },
540 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
542}
543
544impl fmt::Display for DecodeError {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 match self {
547 Self::TooShortForPrefix { have } => {
548 write!(f, "header shorter than frozen prefix: have {have} bytes")
549 }
550 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
551 Self::TooShortForHeader { have, need } => {
552 write!(
553 f,
554 "header too short for version: have {have} bytes, need {need}"
555 )
556 }
557 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
558 Self::ReservedFlagBits { flags } => {
559 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
560 }
561 Self::ReservedPriorityBits { flags } => {
562 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
563 }
564 Self::ReservedAdmissionClass { flags } => {
565 write!(f, "reserved admission class set in flags 0b{flags:08b}")
566 }
567 Self::SheddableIllegalFrameType { ty, flags } => write!(
568 f,
569 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
570 ),
571 Self::NonzeroEpochOnControlChannel { epoch } => {
572 write!(f, "control channel carried nonzero epoch {epoch}")
573 }
574 Self::PureHeaderFrameWithBody { ty, len } => {
575 write!(
576 f,
577 "pure-header frame {ty:?} declared non-zero body length {len}"
578 )
579 }
580 }
581 }
582}
583
584impl Error for DecodeError {}
585
586fn header_len_for_version(ver: u8) -> Option<usize> {
589 match ver {
590 PROTOCOL_VERSION => Some(HEADER_LEN),
591 _ => None,
592 }
593}
594
595pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
603 if bytes.len() < FROZEN_PREFIX_LEN {
604 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
605 }
606 let ver = bytes[4];
607 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
608 if bytes.len() < need {
609 return Err(DecodeError::TooShortForHeader {
610 have: bytes.len(),
611 need,
612 });
613 }
614
615 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
616 let ty =
617 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
618 let flags = Flags(bytes[6]);
619 if flags.priority().is_none() {
620 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
621 }
622 let admission_class = flags
623 .admission_class()
624 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
625 if admission_class == AdmissionClass::Sheddable
626 && !matches!(ty, FrameType::Push | FrameType::StreamData)
627 {
628 return Err(DecodeError::SheddableIllegalFrameType {
629 ty,
630 flags: bytes[6],
631 });
632 }
633 if ty.is_pure_header() && len != 0 {
634 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
635 }
636 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
637 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
638 if channel == 0 && epoch != 0 {
639 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
640 }
641 let corr = u64::from_le_bytes([
642 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
643 ]);
644
645 Ok(EnvelopeHeader {
646 len,
647 ver,
648 ty,
649 flags,
650 channel,
651 epoch,
652 corr,
653 })
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
661 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
662 }
663
664 fn hdr_with_epoch(
665 len: u32,
666 ty: FrameType,
667 flags: Flags,
668 channel: u16,
669 epoch: u32,
670 corr: u64,
671 ) -> EnvelopeHeader {
672 EnvelopeHeader {
673 len,
674 ver: PROTOCOL_VERSION,
675 ty,
676 flags,
677 channel,
678 epoch,
679 corr,
680 }
681 }
682
683 #[test]
684 fn bind_identity_with_project_id_round_trips_json() {
685 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
686 identity.project_id = Some("pj-a1b2c3d4".to_string());
687
688 let encoded = serde_json::to_vec(&identity).unwrap();
689 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
690
691 assert_eq!(decoded, identity);
692 }
693
694 #[test]
695 fn bind_identity_without_project_id_round_trips_json() {
696 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
697
698 let encoded = serde_json::to_vec(&identity).unwrap();
699 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
700
701 assert_eq!(decoded, identity);
702 }
703
704 #[test]
705 fn legacy_bind_identity_without_project_id_decodes() {
706 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
707 "project_root": "/tmp/project",
708 "harness": "opencode",
709 "session": "session-1"
710 }))
711 .unwrap();
712
713 assert_eq!(decoded.project_id, None);
714 }
715
716 #[test]
717 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
718 let encoded =
719 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
720 .unwrap();
721
722 assert!(encoded.get("project_id").is_none());
723 }
724
725 #[test]
726 fn wire_crate_version_is_a_numeric_three_component_version() {
727 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
728
729 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
730 assert_eq!(components.len(), 3);
731 assert!(components
732 .iter()
733 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
734 }
735
736 #[test]
737 fn route_target_variants_round_trip_json() {
738 let targets = [
739 RouteTarget::ToolProvider {
740 module_id: "aft".to_string(),
741 },
742 RouteTarget::ManagementSurface {
743 module_id: "memory".to_string(),
744 },
745 RouteTarget::InternalService {
746 module_id: "bus".to_string(),
747 service_id: "dm".to_string(),
748 },
749 ];
750
751 for target in targets {
752 let encoded = serde_json::to_vec(&target).unwrap();
753 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
754 assert_eq!(decoded, target);
755 }
756 }
757
758 #[test]
759 fn error_body_round_trips_json() {
760 let body = ErrorBody {
761 code: "config_divergence".to_string(),
762 message: "active config differs".to_string(),
763 detail: None,
764 };
765
766 let encoded = serde_json::to_vec(&body).unwrap();
767 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
768
769 assert_eq!(decoded, body);
770 }
771
772 #[test]
773 fn round_trip_request() {
774 let h = hdr(
775 1234,
776 FrameType::Request,
777 Flags::new(false, Priority::Interactive, false),
778 42,
779 0xDEAD_BEEF_0000_0001,
780 );
781 let decoded = decode_header(&h.encode()).unwrap();
782 assert_eq!(h, decoded);
783 }
784
785 #[test]
786 fn round_trip_all_frame_types() {
787 for b in 0u8..=11 {
788 let ty = FrameType::from_u8(b).unwrap();
789 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
790 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
791 }
792 }
793
794 #[test]
795 fn pure_header_frame_has_zero_len() {
796 let h = hdr(
798 0,
799 FrameType::Cancel,
800 Flags::new(false, Priority::Passive, false),
801 7,
802 99,
803 );
804 let d = decode_header(&h.encode()).unwrap();
805 assert_eq!(d.len, 0);
806 assert_eq!(d.corr, 99);
807 }
808
809 #[test]
810 fn flags_round_trip() {
811 let f = Flags::new(true, Priority::Background, true)
812 .with_admission_class(AdmissionClass::Expedite);
813 assert!(f.is_binary());
814 assert!(f.is_last());
815 assert_eq!(f.priority(), Some(Priority::Background));
816 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
817 let h = hdr(8, FrameType::StreamData, f, 1, 1);
818 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
819 }
820
821 #[test]
822 fn daemon_origin_flags_decode_and_round_trip() {
823 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
824 let old_decoded = decode_header(&old.encode()).unwrap();
825 assert!(!old_decoded.flags.is_daemon_origin());
826
827 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
828 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
829 assert!(daemon_decoded.flags.is_daemon_origin());
830 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
831 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
832 }
833
834 #[test]
835 fn little_endian_and_frozen_prefix_layout() {
836 let h = hdr_with_epoch(
837 0x0403_0201,
838 FrameType::Request,
839 Flags(0),
840 0x0605,
841 0x0a09_0807,
842 0x1211_100f_0e0d_0c0b,
843 );
844 let buf = h.encode();
845 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
846 assert_eq!(buf[4], PROTOCOL_VERSION);
847 assert_eq!(&buf[7..9], &[5, 6]);
848 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
849 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
850 assert_eq!(buf.len(), HEADER_LEN);
851 }
852
853 #[test]
854 fn reject_too_short_for_prefix() {
855 assert_eq!(
856 decode_header(&[0, 0, 0, 0]),
857 Err(DecodeError::TooShortForPrefix { have: 4 })
858 );
859 }
860
861 #[test]
862 fn reject_too_short_for_header() {
863 let mut b = [0u8; 10];
865 b[4] = PROTOCOL_VERSION;
866 assert_eq!(
867 decode_header(&b),
868 Err(DecodeError::TooShortForHeader {
869 have: 10,
870 need: HEADER_LEN
871 })
872 );
873 }
874
875 #[test]
876 fn reject_unsupported_version() {
877 let mut b = [0u8; HEADER_LEN];
878 b[4] = 1;
879 assert_eq!(
880 decode_header(&b),
881 Err(DecodeError::UnsupportedVersion { ver: 1 })
882 );
883 }
884
885 #[test]
886 fn reject_unknown_frame_type() {
887 let mut b = [0u8; HEADER_LEN];
888 b[4] = PROTOCOL_VERSION;
889 b[5] = 99;
890 assert_eq!(
891 decode_header(&b),
892 Err(DecodeError::UnknownFrameType { byte: 99 })
893 );
894 }
895
896 #[test]
897 fn subscription_flag_decodes_and_tags_the_request() {
898 let mut b = [0u8; HEADER_LEN];
899 b[4] = PROTOCOL_VERSION;
900 b[5] = FrameType::Request as u8;
901 b[6] = FLAG_SUBSCRIPTION;
902 let decoded = decode_header(&b).unwrap();
903 assert!(decoded.flags.is_subscription());
904 }
905
906 #[test]
907 fn reject_reserved_priority_bits() {
908 let mut b = [0u8; HEADER_LEN];
909 b[4] = PROTOCOL_VERSION;
910 b[5] = FrameType::Request as u8;
911 b[6] = 0b0000_0110; assert_eq!(
913 decode_header(&b),
914 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
915 );
916 }
917
918 #[test]
919 fn reject_pure_header_frame_with_body_len() {
920 let h = hdr(
921 1,
922 FrameType::Ping,
923 Flags::new(false, Priority::Passive, false),
924 0,
925 1,
926 );
927 assert_eq!(
928 decode_header(&h.encode()),
929 Err(DecodeError::PureHeaderFrameWithBody {
930 ty: FrameType::Ping,
931 len: 1
932 })
933 );
934 }
935
936 #[test]
937 fn epoch_boundaries_round_trip() {
938 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
939 let h = hdr_with_epoch(
940 0,
941 FrameType::Request,
942 Flags::new(false, Priority::Passive, false),
943 channel,
944 epoch,
945 9,
946 );
947 assert_eq!(decode_header(&h.encode()).unwrap(), h);
948 }
949 }
950
951 #[test]
952 fn admission_classes_accept_three_values_and_reject_reserved_value() {
953 for (ty, admission_class) in [
954 (FrameType::Request, AdmissionClass::Normal),
955 (FrameType::Request, AdmissionClass::Expedite),
956 (FrameType::Push, AdmissionClass::Sheddable),
957 (FrameType::StreamData, AdmissionClass::Sheddable),
958 ] {
959 let flags = Flags::new(false, Priority::Interactive, false)
960 .with_admission_class(admission_class);
961 let h = hdr(0, ty, flags, 1, 2);
962 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
963 }
964
965 let mut h = hdr(
966 0,
967 FrameType::Push,
968 Flags::new(false, Priority::Passive, false),
969 1,
970 2,
971 )
972 .encode();
973 h[6] |= 0b0011_0000;
974 assert_eq!(
975 decode_header(&h),
976 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
977 );
978 }
979
980 #[test]
981 fn sheddable_rejected_on_every_illegal_frame_type() {
982 let flags = Flags::new(false, Priority::Passive, false)
983 .with_admission_class(AdmissionClass::Sheddable);
984 for ty in [
985 FrameType::Request,
986 FrameType::Response,
987 FrameType::StreamEnd,
988 FrameType::Error,
989 FrameType::Cancel,
990 FrameType::Ping,
991 FrameType::Pong,
992 FrameType::Hello,
993 FrameType::HelloAck,
994 FrameType::Goodbye,
995 ] {
996 let h = hdr(0, ty, flags, 1, 2);
997 assert_eq!(
998 decode_header(&h.encode()),
999 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
1000 );
1001 }
1002 }
1003
1004 #[test]
1005 fn nonzero_epoch_on_control_channel_is_rejected() {
1006 let h = hdr_with_epoch(
1007 0,
1008 FrameType::Request,
1009 Flags::new(false, Priority::Passive, false),
1010 0,
1011 u32::MAX,
1012 2,
1013 );
1014 assert_eq!(
1015 decode_header(&h.encode()),
1016 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
1017 );
1018 }
1019}