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";
63 pub const MODULE_WARMING: &str = "module_warming";
64 pub const TARGET_UNAVAILABLE: &str = "target_unavailable";
65 pub const MODULE_TIMEOUT: &str = "module_timeout";
66 pub const MODULE_NO_PROTOCOL: &str = "module_no_protocol";
78
79 pub fn is_retryable_route_open(code: &str) -> bool {
99 matches!(
100 code,
101 MODULE_RELOADING | MODULE_WARMING | TARGET_UNAVAILABLE | MODULE_TIMEOUT
102 )
103 }
104}
105
106pub use frame::{Frame, FrameBuildError};
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
110#[serde(rename_all = "snake_case")]
111pub enum RouteCloseReason {
112 Reload,
113 Restart,
114 Disable,
115 Crash,
116 CapabilityDenied,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
142#[non_exhaustive]
143pub struct BindIdentity {
144 pub project_root: PathBuf,
145 pub harness: String,
146 pub session: String,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub project_id: Option<String>,
166}
167
168impl BindIdentity {
169 pub fn new(
174 project_root: impl Into<PathBuf>,
175 harness: impl Into<String>,
176 session: impl Into<String>,
177 ) -> Self {
178 Self {
179 project_root: project_root.into(),
180 harness: harness.into(),
181 session: session.into(),
182 project_id: None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
189#[serde(tag = "kind", rename_all = "snake_case")]
190pub enum Principal {
191 Reserved { module_id: String },
193 Direct,
195 Unverified,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(tag = "kind", rename_all = "snake_case")]
213pub enum RouteTarget {
214 ToolProvider {
215 module_id: String,
216 },
217 ManagementSurface {
218 module_id: String,
219 },
220 InternalService {
221 module_id: String,
222 service_id: String,
223 },
224}
225
226pub const PROTOCOL_VERSION: u8 = 2;
228
229pub const SUBC_PROTOCOL_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
238
239pub const MIN_SUPPORTED_VERSION: u8 = 2;
241
242pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
245
246pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
251
252pub const HEADER_LEN: usize = 21;
254
255pub const FROZEN_PREFIX_LEN: usize = 5;
259
260pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276pub struct ErrorBody {
277 pub code: String,
278 pub message: String,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub detail: Option<serde_json::Value>,
281}
282
283impl ErrorBody {
284 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
286 Self {
287 code: code.into(),
288 message: message.into(),
289 detail: None,
290 }
291 }
292
293 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
295 self.detail = Some(detail);
296 self
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
302pub struct ModuleHelloBody {
303 pub manifest: manifest::ModuleManifest,
304 pub protocol_ver: u8,
305 #[serde(default)]
306 pub control_ops: Option<Vec<String>>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub launch_nonce: Option<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct ModuleHelloAckBody {
321 pub negotiated_ver: u8,
322 pub subc_ops: Vec<String>,
323 pub subc_capabilities: Vec<String>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub storage: Option<serde_json::Value>,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339#[repr(u8)]
340pub enum FrameType {
341 Request = 0,
342 Response = 1,
343 Push = 2,
344 StreamData = 3,
345 StreamEnd = 4,
346 Error = 5,
347 Cancel = 6,
348 Ping = 7,
349 Pong = 8,
350 Hello = 9,
351 HelloAck = 10,
352 Goodbye = 11,
353}
354
355impl FrameType {
356 pub fn from_u8(b: u8) -> Option<Self> {
358 Some(match b {
359 0 => Self::Request,
360 1 => Self::Response,
361 2 => Self::Push,
362 3 => Self::StreamData,
363 4 => Self::StreamEnd,
364 5 => Self::Error,
365 6 => Self::Cancel,
366 7 => Self::Ping,
367 8 => Self::Pong,
368 9 => Self::Hello,
369 10 => Self::HelloAck,
370 11 => Self::Goodbye,
371 _ => return None,
372 })
373 }
374
375 pub fn is_pure_header(self) -> bool {
376 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
377 }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383#[repr(u8)]
384pub enum Priority {
385 Passive = 0,
386 Interactive = 1,
387 Background = 2,
388}
389
390impl Priority {
391 fn from_bits(bits: u8) -> Option<Self> {
392 Some(match bits {
393 0 => Self::Passive,
394 1 => Self::Interactive,
395 2 => Self::Background,
396 _ => return None,
397 })
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403#[repr(u8)]
404pub enum AdmissionClass {
405 Normal = 0,
406 Expedite = 1,
407 Sheddable = 2,
408}
409
410impl AdmissionClass {
411 fn from_bits(bits: u8) -> Option<Self> {
412 Some(match bits {
413 0 => Self::Normal,
414 1 => Self::Expedite,
415 2 => Self::Sheddable,
416 _ => return None,
417 })
418 }
419}
420
421const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
424const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
427pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
428pub const FLAG_SUBSCRIPTION: u8 = 0b1000_0000;
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub struct Flags(pub u8);
434
435impl Flags {
436 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
438 let mut b = 0u8;
439 if binary {
440 b |= FLAG_BINARY;
441 }
442 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
443 if last {
444 b |= FLAG_LAST;
445 }
446 Flags(b)
447 }
448
449 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
451 self.0 =
452 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
453 self
454 }
455
456 pub fn is_binary(self) -> bool {
458 self.0 & FLAG_BINARY != 0
459 }
460
461 pub fn is_last(self) -> bool {
463 self.0 & FLAG_LAST != 0
464 }
465
466 pub fn priority(self) -> Option<Priority> {
468 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
469 }
470
471 pub fn admission_class(self) -> Option<AdmissionClass> {
473 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
474 }
475
476 pub fn is_subscription(self) -> bool {
478 self.0 & FLAG_SUBSCRIPTION != 0
479 }
480
481 pub fn is_daemon_origin(self) -> bool {
483 self.0 & FLAG_DAEMON_ORIGIN != 0
484 }
485
486 pub fn with_daemon_origin(mut self) -> Self {
488 self.0 |= FLAG_DAEMON_ORIGIN;
489 self
490 }
491
492 pub fn without_daemon_origin(self) -> Self {
494 Self(self.0 & !FLAG_DAEMON_ORIGIN)
495 }
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub struct EnvelopeHeader {
501 pub len: u32,
503 pub ver: u8,
505 pub ty: FrameType,
507 pub flags: Flags,
509 pub channel: u16,
511 pub epoch: u32,
513 pub corr: u64,
515}
516
517impl EnvelopeHeader {
518 pub fn encode(&self) -> [u8; HEADER_LEN] {
520 let mut buf = [0u8; HEADER_LEN];
521 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
522 buf[4] = self.ver;
523 buf[5] = self.ty as u8;
524 buf[6] = self.flags.0;
525 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
526 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
527 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
528 buf
529 }
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534pub enum DecodeError {
535 TooShortForPrefix { have: usize },
537 UnsupportedVersion { ver: u8 },
539 TooShortForHeader { have: usize, need: usize },
541 UnknownFrameType { byte: u8 },
543 ReservedFlagBits { flags: u8 },
545 ReservedPriorityBits { flags: u8 },
547 ReservedAdmissionClass { flags: u8 },
549 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
551 NonzeroEpochOnControlChannel { epoch: u32 },
553 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
555}
556
557impl fmt::Display for DecodeError {
558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 match self {
560 Self::TooShortForPrefix { have } => {
561 write!(f, "header shorter than frozen prefix: have {have} bytes")
562 }
563 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
564 Self::TooShortForHeader { have, need } => {
565 write!(
566 f,
567 "header too short for version: have {have} bytes, need {need}"
568 )
569 }
570 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
571 Self::ReservedFlagBits { flags } => {
572 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
573 }
574 Self::ReservedPriorityBits { flags } => {
575 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
576 }
577 Self::ReservedAdmissionClass { flags } => {
578 write!(f, "reserved admission class set in flags 0b{flags:08b}")
579 }
580 Self::SheddableIllegalFrameType { ty, flags } => write!(
581 f,
582 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
583 ),
584 Self::NonzeroEpochOnControlChannel { epoch } => {
585 write!(f, "control channel carried nonzero epoch {epoch}")
586 }
587 Self::PureHeaderFrameWithBody { ty, len } => {
588 write!(
589 f,
590 "pure-header frame {ty:?} declared non-zero body length {len}"
591 )
592 }
593 }
594 }
595}
596
597impl Error for DecodeError {}
598
599fn header_len_for_version(ver: u8) -> Option<usize> {
602 match ver {
603 PROTOCOL_VERSION => Some(HEADER_LEN),
604 _ => None,
605 }
606}
607
608pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
616 if bytes.len() < FROZEN_PREFIX_LEN {
617 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
618 }
619 let ver = bytes[4];
620 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
621 if bytes.len() < need {
622 return Err(DecodeError::TooShortForHeader {
623 have: bytes.len(),
624 need,
625 });
626 }
627
628 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
629 let ty =
630 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
631 let flags = Flags(bytes[6]);
632 if flags.priority().is_none() {
633 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
634 }
635 let admission_class = flags
636 .admission_class()
637 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
638 if admission_class == AdmissionClass::Sheddable
639 && !matches!(ty, FrameType::Push | FrameType::StreamData)
640 {
641 return Err(DecodeError::SheddableIllegalFrameType {
642 ty,
643 flags: bytes[6],
644 });
645 }
646 if ty.is_pure_header() && len != 0 {
647 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
648 }
649 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
650 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
651 if channel == 0 && epoch != 0 {
652 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
653 }
654 let corr = u64::from_le_bytes([
655 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
656 ]);
657
658 Ok(EnvelopeHeader {
659 len,
660 ver,
661 ty,
662 flags,
663 channel,
664 epoch,
665 corr,
666 })
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
674 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
675 }
676
677 fn hdr_with_epoch(
678 len: u32,
679 ty: FrameType,
680 flags: Flags,
681 channel: u16,
682 epoch: u32,
683 corr: u64,
684 ) -> EnvelopeHeader {
685 EnvelopeHeader {
686 len,
687 ver: PROTOCOL_VERSION,
688 ty,
689 flags,
690 channel,
691 epoch,
692 corr,
693 }
694 }
695
696 #[test]
697 fn bind_identity_with_project_id_round_trips_json() {
698 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
699 identity.project_id = Some("pj-a1b2c3d4".to_string());
700
701 let encoded = serde_json::to_vec(&identity).unwrap();
702 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
703
704 assert_eq!(decoded, identity);
705 }
706
707 #[test]
708 fn bind_identity_without_project_id_round_trips_json() {
709 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
710
711 let encoded = serde_json::to_vec(&identity).unwrap();
712 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
713
714 assert_eq!(decoded, identity);
715 }
716
717 #[test]
718 fn legacy_bind_identity_without_project_id_decodes() {
719 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
720 "project_root": "/tmp/project",
721 "harness": "opencode",
722 "session": "session-1"
723 }))
724 .unwrap();
725
726 assert_eq!(decoded.project_id, None);
727 }
728
729 #[test]
730 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
731 let encoded =
732 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
733 .unwrap();
734
735 assert!(encoded.get("project_id").is_none());
736 }
737
738 #[test]
739 fn wire_crate_version_is_a_numeric_three_component_version() {
740 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
741
742 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
743 assert_eq!(components.len(), 3);
744 assert!(components
745 .iter()
746 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
747 }
748
749 #[test]
750 fn route_target_variants_round_trip_json() {
751 let targets = [
752 RouteTarget::ToolProvider {
753 module_id: "aft".to_string(),
754 },
755 RouteTarget::ManagementSurface {
756 module_id: "memory".to_string(),
757 },
758 RouteTarget::InternalService {
759 module_id: "bus".to_string(),
760 service_id: "dm".to_string(),
761 },
762 ];
763
764 for target in targets {
765 let encoded = serde_json::to_vec(&target).unwrap();
766 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
767 assert_eq!(decoded, target);
768 }
769 }
770
771 #[test]
772 fn error_body_round_trips_json() {
773 let body = ErrorBody {
774 code: "config_divergence".to_string(),
775 message: "active config differs".to_string(),
776 detail: None,
777 };
778
779 let encoded = serde_json::to_vec(&body).unwrap();
780 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
781
782 assert_eq!(decoded, body);
783 }
784
785 #[test]
786 fn round_trip_request() {
787 let h = hdr(
788 1234,
789 FrameType::Request,
790 Flags::new(false, Priority::Interactive, false),
791 42,
792 0xDEAD_BEEF_0000_0001,
793 );
794 let decoded = decode_header(&h.encode()).unwrap();
795 assert_eq!(h, decoded);
796 }
797
798 #[test]
799 fn round_trip_all_frame_types() {
800 for b in 0u8..=11 {
801 let ty = FrameType::from_u8(b).unwrap();
802 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
803 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
804 }
805 }
806
807 #[test]
808 fn pure_header_frame_has_zero_len() {
809 let h = hdr(
811 0,
812 FrameType::Cancel,
813 Flags::new(false, Priority::Passive, false),
814 7,
815 99,
816 );
817 let d = decode_header(&h.encode()).unwrap();
818 assert_eq!(d.len, 0);
819 assert_eq!(d.corr, 99);
820 }
821
822 #[test]
823 fn flags_round_trip() {
824 let f = Flags::new(true, Priority::Background, true)
825 .with_admission_class(AdmissionClass::Expedite);
826 assert!(f.is_binary());
827 assert!(f.is_last());
828 assert_eq!(f.priority(), Some(Priority::Background));
829 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
830 let h = hdr(8, FrameType::StreamData, f, 1, 1);
831 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
832 }
833
834 #[test]
835 fn daemon_origin_flags_decode_and_round_trip() {
836 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
837 let old_decoded = decode_header(&old.encode()).unwrap();
838 assert!(!old_decoded.flags.is_daemon_origin());
839
840 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
841 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
842 assert!(daemon_decoded.flags.is_daemon_origin());
843 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
844 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
845 }
846
847 #[test]
848 fn little_endian_and_frozen_prefix_layout() {
849 let h = hdr_with_epoch(
850 0x0403_0201,
851 FrameType::Request,
852 Flags(0),
853 0x0605,
854 0x0a09_0807,
855 0x1211_100f_0e0d_0c0b,
856 );
857 let buf = h.encode();
858 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
859 assert_eq!(buf[4], PROTOCOL_VERSION);
860 assert_eq!(&buf[7..9], &[5, 6]);
861 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
862 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
863 assert_eq!(buf.len(), HEADER_LEN);
864 }
865
866 #[test]
867 fn reject_too_short_for_prefix() {
868 assert_eq!(
869 decode_header(&[0, 0, 0, 0]),
870 Err(DecodeError::TooShortForPrefix { have: 4 })
871 );
872 }
873
874 #[test]
875 fn reject_too_short_for_header() {
876 let mut b = [0u8; 10];
878 b[4] = PROTOCOL_VERSION;
879 assert_eq!(
880 decode_header(&b),
881 Err(DecodeError::TooShortForHeader {
882 have: 10,
883 need: HEADER_LEN
884 })
885 );
886 }
887
888 #[test]
889 fn reject_unsupported_version() {
890 let mut b = [0u8; HEADER_LEN];
891 b[4] = 1;
892 assert_eq!(
893 decode_header(&b),
894 Err(DecodeError::UnsupportedVersion { ver: 1 })
895 );
896 }
897
898 #[test]
899 fn reject_unknown_frame_type() {
900 let mut b = [0u8; HEADER_LEN];
901 b[4] = PROTOCOL_VERSION;
902 b[5] = 99;
903 assert_eq!(
904 decode_header(&b),
905 Err(DecodeError::UnknownFrameType { byte: 99 })
906 );
907 }
908
909 #[test]
910 fn subscription_flag_decodes_and_tags_the_request() {
911 let mut b = [0u8; HEADER_LEN];
912 b[4] = PROTOCOL_VERSION;
913 b[5] = FrameType::Request as u8;
914 b[6] = FLAG_SUBSCRIPTION;
915 let decoded = decode_header(&b).unwrap();
916 assert!(decoded.flags.is_subscription());
917 }
918
919 #[test]
920 fn reject_reserved_priority_bits() {
921 let mut b = [0u8; HEADER_LEN];
922 b[4] = PROTOCOL_VERSION;
923 b[5] = FrameType::Request as u8;
924 b[6] = 0b0000_0110; assert_eq!(
926 decode_header(&b),
927 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
928 );
929 }
930
931 #[test]
932 fn reject_pure_header_frame_with_body_len() {
933 let h = hdr(
934 1,
935 FrameType::Ping,
936 Flags::new(false, Priority::Passive, false),
937 0,
938 1,
939 );
940 assert_eq!(
941 decode_header(&h.encode()),
942 Err(DecodeError::PureHeaderFrameWithBody {
943 ty: FrameType::Ping,
944 len: 1
945 })
946 );
947 }
948
949 #[test]
950 fn epoch_boundaries_round_trip() {
951 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
952 let h = hdr_with_epoch(
953 0,
954 FrameType::Request,
955 Flags::new(false, Priority::Passive, false),
956 channel,
957 epoch,
958 9,
959 );
960 assert_eq!(decode_header(&h.encode()).unwrap(), h);
961 }
962 }
963
964 #[test]
965 fn admission_classes_accept_three_values_and_reject_reserved_value() {
966 for (ty, admission_class) in [
967 (FrameType::Request, AdmissionClass::Normal),
968 (FrameType::Request, AdmissionClass::Expedite),
969 (FrameType::Push, AdmissionClass::Sheddable),
970 (FrameType::StreamData, AdmissionClass::Sheddable),
971 ] {
972 let flags = Flags::new(false, Priority::Interactive, false)
973 .with_admission_class(admission_class);
974 let h = hdr(0, ty, flags, 1, 2);
975 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
976 }
977
978 let mut h = hdr(
979 0,
980 FrameType::Push,
981 Flags::new(false, Priority::Passive, false),
982 1,
983 2,
984 )
985 .encode();
986 h[6] |= 0b0011_0000;
987 assert_eq!(
988 decode_header(&h),
989 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
990 );
991 }
992
993 #[test]
994 fn sheddable_rejected_on_every_illegal_frame_type() {
995 let flags = Flags::new(false, Priority::Passive, false)
996 .with_admission_class(AdmissionClass::Sheddable);
997 for ty in [
998 FrameType::Request,
999 FrameType::Response,
1000 FrameType::StreamEnd,
1001 FrameType::Error,
1002 FrameType::Cancel,
1003 FrameType::Ping,
1004 FrameType::Pong,
1005 FrameType::Hello,
1006 FrameType::HelloAck,
1007 FrameType::Goodbye,
1008 ] {
1009 let h = hdr(0, ty, flags, 1, 2);
1010 assert_eq!(
1011 decode_header(&h.encode()),
1012 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
1013 );
1014 }
1015 }
1016
1017 #[test]
1018 fn nonzero_epoch_on_control_channel_is_rejected() {
1019 let h = hdr_with_epoch(
1020 0,
1021 FrameType::Request,
1022 Flags::new(false, Priority::Passive, false),
1023 0,
1024 u32::MAX,
1025 2,
1026 );
1027 assert_eq!(
1028 decode_header(&h.encode()),
1029 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
1030 );
1031 }
1032}