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
54 pub fn is_retryable_route_open(code: &str) -> bool {
67 matches!(
68 code,
69 UNKNOWN_MODULE
70 | MODULE_RELOADING
71 | MODULE_WARMING
72 | TARGET_UNAVAILABLE
73 | MODULE_TIMEOUT
74 )
75 }
76}
77
78pub use frame::{Frame, FrameBuildError};
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "snake_case")]
83pub enum RouteCloseReason {
84 Reload,
85 Restart,
86 Disable,
87 Crash,
88 CapabilityDenied,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[non_exhaustive]
115pub struct BindIdentity {
116 pub project_root: PathBuf,
117 pub harness: String,
118 pub session: String,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub project_id: Option<String>,
138}
139
140impl BindIdentity {
141 pub fn new(
146 project_root: impl Into<PathBuf>,
147 harness: impl Into<String>,
148 session: impl Into<String>,
149 ) -> Self {
150 Self {
151 project_root: project_root.into(),
152 harness: harness.into(),
153 session: session.into(),
154 project_id: None,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
161#[serde(tag = "kind", rename_all = "snake_case")]
162pub enum Principal {
163 Reserved { module_id: String },
165 Direct,
167 Unverified,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184#[serde(tag = "kind", rename_all = "snake_case")]
185pub enum RouteTarget {
186 ToolProvider {
187 module_id: String,
188 },
189 ManagementSurface {
190 module_id: String,
191 },
192 InternalService {
193 module_id: String,
194 service_id: String,
195 },
196}
197
198pub const PROTOCOL_VERSION: u8 = 2;
200
201pub const SUBC_PROTOCOL_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
210
211pub const MIN_SUPPORTED_VERSION: u8 = 2;
213
214pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
217
218pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
223
224pub const HEADER_LEN: usize = 21;
226
227pub const FROZEN_PREFIX_LEN: usize = 5;
231
232pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
238
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct ErrorBody {
249 pub code: String,
250 pub message: String,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub detail: Option<serde_json::Value>,
253}
254
255impl ErrorBody {
256 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
258 Self {
259 code: code.into(),
260 message: message.into(),
261 detail: None,
262 }
263 }
264
265 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
267 self.detail = Some(detail);
268 self
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
274pub struct ModuleHelloBody {
275 pub manifest: manifest::ModuleManifest,
276 pub protocol_ver: u8,
277 #[serde(default)]
278 pub control_ops: Option<Vec<String>>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub launch_nonce: Option<String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct ModuleHelloAckBody {
293 pub negotiated_ver: u8,
294 pub subc_ops: Vec<String>,
295 pub subc_capabilities: Vec<String>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub storage: Option<serde_json::Value>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311#[repr(u8)]
312pub enum FrameType {
313 Request = 0,
314 Response = 1,
315 Push = 2,
316 StreamData = 3,
317 StreamEnd = 4,
318 Error = 5,
319 Cancel = 6,
320 Ping = 7,
321 Pong = 8,
322 Hello = 9,
323 HelloAck = 10,
324 Goodbye = 11,
325}
326
327impl FrameType {
328 pub fn from_u8(b: u8) -> Option<Self> {
330 Some(match b {
331 0 => Self::Request,
332 1 => Self::Response,
333 2 => Self::Push,
334 3 => Self::StreamData,
335 4 => Self::StreamEnd,
336 5 => Self::Error,
337 6 => Self::Cancel,
338 7 => Self::Ping,
339 8 => Self::Pong,
340 9 => Self::Hello,
341 10 => Self::HelloAck,
342 11 => Self::Goodbye,
343 _ => return None,
344 })
345 }
346
347 pub fn is_pure_header(self) -> bool {
348 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355#[repr(u8)]
356pub enum Priority {
357 Passive = 0,
358 Interactive = 1,
359 Background = 2,
360}
361
362impl Priority {
363 fn from_bits(bits: u8) -> Option<Self> {
364 Some(match bits {
365 0 => Self::Passive,
366 1 => Self::Interactive,
367 2 => Self::Background,
368 _ => return None,
369 })
370 }
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375#[repr(u8)]
376pub enum AdmissionClass {
377 Normal = 0,
378 Expedite = 1,
379 Sheddable = 2,
380}
381
382impl AdmissionClass {
383 fn from_bits(bits: u8) -> Option<Self> {
384 Some(match bits {
385 0 => Self::Normal,
386 1 => Self::Expedite,
387 2 => Self::Sheddable,
388 _ => return None,
389 })
390 }
391}
392
393const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
396const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
399pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
400pub const FLAG_SUBSCRIPTION: u8 = 0b1000_0000;
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub struct Flags(pub u8);
406
407impl Flags {
408 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
410 let mut b = 0u8;
411 if binary {
412 b |= FLAG_BINARY;
413 }
414 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
415 if last {
416 b |= FLAG_LAST;
417 }
418 Flags(b)
419 }
420
421 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
423 self.0 =
424 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
425 self
426 }
427
428 pub fn is_binary(self) -> bool {
430 self.0 & FLAG_BINARY != 0
431 }
432
433 pub fn is_last(self) -> bool {
435 self.0 & FLAG_LAST != 0
436 }
437
438 pub fn priority(self) -> Option<Priority> {
440 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
441 }
442
443 pub fn admission_class(self) -> Option<AdmissionClass> {
445 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
446 }
447
448 pub fn is_subscription(self) -> bool {
450 self.0 & FLAG_SUBSCRIPTION != 0
451 }
452
453 pub fn is_daemon_origin(self) -> bool {
455 self.0 & FLAG_DAEMON_ORIGIN != 0
456 }
457
458 pub fn with_daemon_origin(mut self) -> Self {
460 self.0 |= FLAG_DAEMON_ORIGIN;
461 self
462 }
463
464 pub fn without_daemon_origin(self) -> Self {
466 Self(self.0 & !FLAG_DAEMON_ORIGIN)
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
472pub struct EnvelopeHeader {
473 pub len: u32,
475 pub ver: u8,
477 pub ty: FrameType,
479 pub flags: Flags,
481 pub channel: u16,
483 pub epoch: u32,
485 pub corr: u64,
487}
488
489impl EnvelopeHeader {
490 pub fn encode(&self) -> [u8; HEADER_LEN] {
492 let mut buf = [0u8; HEADER_LEN];
493 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
494 buf[4] = self.ver;
495 buf[5] = self.ty as u8;
496 buf[6] = self.flags.0;
497 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
498 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
499 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
500 buf
501 }
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506pub enum DecodeError {
507 TooShortForPrefix { have: usize },
509 UnsupportedVersion { ver: u8 },
511 TooShortForHeader { have: usize, need: usize },
513 UnknownFrameType { byte: u8 },
515 ReservedFlagBits { flags: u8 },
517 ReservedPriorityBits { flags: u8 },
519 ReservedAdmissionClass { flags: u8 },
521 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
523 NonzeroEpochOnControlChannel { epoch: u32 },
525 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
527}
528
529impl fmt::Display for DecodeError {
530 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531 match self {
532 Self::TooShortForPrefix { have } => {
533 write!(f, "header shorter than frozen prefix: have {have} bytes")
534 }
535 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
536 Self::TooShortForHeader { have, need } => {
537 write!(
538 f,
539 "header too short for version: have {have} bytes, need {need}"
540 )
541 }
542 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
543 Self::ReservedFlagBits { flags } => {
544 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
545 }
546 Self::ReservedPriorityBits { flags } => {
547 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
548 }
549 Self::ReservedAdmissionClass { flags } => {
550 write!(f, "reserved admission class set in flags 0b{flags:08b}")
551 }
552 Self::SheddableIllegalFrameType { ty, flags } => write!(
553 f,
554 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
555 ),
556 Self::NonzeroEpochOnControlChannel { epoch } => {
557 write!(f, "control channel carried nonzero epoch {epoch}")
558 }
559 Self::PureHeaderFrameWithBody { ty, len } => {
560 write!(
561 f,
562 "pure-header frame {ty:?} declared non-zero body length {len}"
563 )
564 }
565 }
566 }
567}
568
569impl Error for DecodeError {}
570
571fn header_len_for_version(ver: u8) -> Option<usize> {
574 match ver {
575 PROTOCOL_VERSION => Some(HEADER_LEN),
576 _ => None,
577 }
578}
579
580pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
588 if bytes.len() < FROZEN_PREFIX_LEN {
589 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
590 }
591 let ver = bytes[4];
592 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
593 if bytes.len() < need {
594 return Err(DecodeError::TooShortForHeader {
595 have: bytes.len(),
596 need,
597 });
598 }
599
600 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
601 let ty =
602 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
603 let flags = Flags(bytes[6]);
604 if flags.priority().is_none() {
605 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
606 }
607 let admission_class = flags
608 .admission_class()
609 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
610 if admission_class == AdmissionClass::Sheddable
611 && !matches!(ty, FrameType::Push | FrameType::StreamData)
612 {
613 return Err(DecodeError::SheddableIllegalFrameType {
614 ty,
615 flags: bytes[6],
616 });
617 }
618 if ty.is_pure_header() && len != 0 {
619 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
620 }
621 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
622 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
623 if channel == 0 && epoch != 0 {
624 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
625 }
626 let corr = u64::from_le_bytes([
627 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
628 ]);
629
630 Ok(EnvelopeHeader {
631 len,
632 ver,
633 ty,
634 flags,
635 channel,
636 epoch,
637 corr,
638 })
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
646 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
647 }
648
649 fn hdr_with_epoch(
650 len: u32,
651 ty: FrameType,
652 flags: Flags,
653 channel: u16,
654 epoch: u32,
655 corr: u64,
656 ) -> EnvelopeHeader {
657 EnvelopeHeader {
658 len,
659 ver: PROTOCOL_VERSION,
660 ty,
661 flags,
662 channel,
663 epoch,
664 corr,
665 }
666 }
667
668 #[test]
669 fn bind_identity_with_project_id_round_trips_json() {
670 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
671 identity.project_id = Some("pj-a1b2c3d4".to_string());
672
673 let encoded = serde_json::to_vec(&identity).unwrap();
674 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
675
676 assert_eq!(decoded, identity);
677 }
678
679 #[test]
680 fn bind_identity_without_project_id_round_trips_json() {
681 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
682
683 let encoded = serde_json::to_vec(&identity).unwrap();
684 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
685
686 assert_eq!(decoded, identity);
687 }
688
689 #[test]
690 fn legacy_bind_identity_without_project_id_decodes() {
691 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
692 "project_root": "/tmp/project",
693 "harness": "opencode",
694 "session": "session-1"
695 }))
696 .unwrap();
697
698 assert_eq!(decoded.project_id, None);
699 }
700
701 #[test]
702 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
703 let encoded =
704 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
705 .unwrap();
706
707 assert!(encoded.get("project_id").is_none());
708 }
709
710 #[test]
711 fn wire_crate_version_is_a_numeric_three_component_version() {
712 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
713
714 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
715 assert_eq!(components.len(), 3);
716 assert!(components
717 .iter()
718 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
719 }
720
721 #[test]
722 fn route_target_variants_round_trip_json() {
723 let targets = [
724 RouteTarget::ToolProvider {
725 module_id: "aft".to_string(),
726 },
727 RouteTarget::ManagementSurface {
728 module_id: "memory".to_string(),
729 },
730 RouteTarget::InternalService {
731 module_id: "bus".to_string(),
732 service_id: "dm".to_string(),
733 },
734 ];
735
736 for target in targets {
737 let encoded = serde_json::to_vec(&target).unwrap();
738 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
739 assert_eq!(decoded, target);
740 }
741 }
742
743 #[test]
744 fn error_body_round_trips_json() {
745 let body = ErrorBody {
746 code: "config_divergence".to_string(),
747 message: "active config differs".to_string(),
748 detail: None,
749 };
750
751 let encoded = serde_json::to_vec(&body).unwrap();
752 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
753
754 assert_eq!(decoded, body);
755 }
756
757 #[test]
758 fn round_trip_request() {
759 let h = hdr(
760 1234,
761 FrameType::Request,
762 Flags::new(false, Priority::Interactive, false),
763 42,
764 0xDEAD_BEEF_0000_0001,
765 );
766 let decoded = decode_header(&h.encode()).unwrap();
767 assert_eq!(h, decoded);
768 }
769
770 #[test]
771 fn round_trip_all_frame_types() {
772 for b in 0u8..=11 {
773 let ty = FrameType::from_u8(b).unwrap();
774 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
775 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
776 }
777 }
778
779 #[test]
780 fn pure_header_frame_has_zero_len() {
781 let h = hdr(
783 0,
784 FrameType::Cancel,
785 Flags::new(false, Priority::Passive, false),
786 7,
787 99,
788 );
789 let d = decode_header(&h.encode()).unwrap();
790 assert_eq!(d.len, 0);
791 assert_eq!(d.corr, 99);
792 }
793
794 #[test]
795 fn flags_round_trip() {
796 let f = Flags::new(true, Priority::Background, true)
797 .with_admission_class(AdmissionClass::Expedite);
798 assert!(f.is_binary());
799 assert!(f.is_last());
800 assert_eq!(f.priority(), Some(Priority::Background));
801 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
802 let h = hdr(8, FrameType::StreamData, f, 1, 1);
803 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
804 }
805
806 #[test]
807 fn daemon_origin_flags_decode_and_round_trip() {
808 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
809 let old_decoded = decode_header(&old.encode()).unwrap();
810 assert!(!old_decoded.flags.is_daemon_origin());
811
812 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
813 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
814 assert!(daemon_decoded.flags.is_daemon_origin());
815 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
816 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
817 }
818
819 #[test]
820 fn little_endian_and_frozen_prefix_layout() {
821 let h = hdr_with_epoch(
822 0x0403_0201,
823 FrameType::Request,
824 Flags(0),
825 0x0605,
826 0x0a09_0807,
827 0x1211_100f_0e0d_0c0b,
828 );
829 let buf = h.encode();
830 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
831 assert_eq!(buf[4], PROTOCOL_VERSION);
832 assert_eq!(&buf[7..9], &[5, 6]);
833 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
834 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
835 assert_eq!(buf.len(), HEADER_LEN);
836 }
837
838 #[test]
839 fn reject_too_short_for_prefix() {
840 assert_eq!(
841 decode_header(&[0, 0, 0, 0]),
842 Err(DecodeError::TooShortForPrefix { have: 4 })
843 );
844 }
845
846 #[test]
847 fn reject_too_short_for_header() {
848 let mut b = [0u8; 10];
850 b[4] = PROTOCOL_VERSION;
851 assert_eq!(
852 decode_header(&b),
853 Err(DecodeError::TooShortForHeader {
854 have: 10,
855 need: HEADER_LEN
856 })
857 );
858 }
859
860 #[test]
861 fn reject_unsupported_version() {
862 let mut b = [0u8; HEADER_LEN];
863 b[4] = 1;
864 assert_eq!(
865 decode_header(&b),
866 Err(DecodeError::UnsupportedVersion { ver: 1 })
867 );
868 }
869
870 #[test]
871 fn reject_unknown_frame_type() {
872 let mut b = [0u8; HEADER_LEN];
873 b[4] = PROTOCOL_VERSION;
874 b[5] = 99;
875 assert_eq!(
876 decode_header(&b),
877 Err(DecodeError::UnknownFrameType { byte: 99 })
878 );
879 }
880
881 #[test]
882 fn subscription_flag_decodes_and_tags_the_request() {
883 let mut b = [0u8; HEADER_LEN];
884 b[4] = PROTOCOL_VERSION;
885 b[5] = FrameType::Request as u8;
886 b[6] = FLAG_SUBSCRIPTION;
887 let decoded = decode_header(&b).unwrap();
888 assert!(decoded.flags.is_subscription());
889 }
890
891 #[test]
892 fn reject_reserved_priority_bits() {
893 let mut b = [0u8; HEADER_LEN];
894 b[4] = PROTOCOL_VERSION;
895 b[5] = FrameType::Request as u8;
896 b[6] = 0b0000_0110; assert_eq!(
898 decode_header(&b),
899 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
900 );
901 }
902
903 #[test]
904 fn reject_pure_header_frame_with_body_len() {
905 let h = hdr(
906 1,
907 FrameType::Ping,
908 Flags::new(false, Priority::Passive, false),
909 0,
910 1,
911 );
912 assert_eq!(
913 decode_header(&h.encode()),
914 Err(DecodeError::PureHeaderFrameWithBody {
915 ty: FrameType::Ping,
916 len: 1
917 })
918 );
919 }
920
921 #[test]
922 fn epoch_boundaries_round_trip() {
923 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
924 let h = hdr_with_epoch(
925 0,
926 FrameType::Request,
927 Flags::new(false, Priority::Passive, false),
928 channel,
929 epoch,
930 9,
931 );
932 assert_eq!(decode_header(&h.encode()).unwrap(), h);
933 }
934 }
935
936 #[test]
937 fn admission_classes_accept_three_values_and_reject_reserved_value() {
938 for (ty, admission_class) in [
939 (FrameType::Request, AdmissionClass::Normal),
940 (FrameType::Request, AdmissionClass::Expedite),
941 (FrameType::Push, AdmissionClass::Sheddable),
942 (FrameType::StreamData, AdmissionClass::Sheddable),
943 ] {
944 let flags = Flags::new(false, Priority::Interactive, false)
945 .with_admission_class(admission_class);
946 let h = hdr(0, ty, flags, 1, 2);
947 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
948 }
949
950 let mut h = hdr(
951 0,
952 FrameType::Push,
953 Flags::new(false, Priority::Passive, false),
954 1,
955 2,
956 )
957 .encode();
958 h[6] |= 0b0011_0000;
959 assert_eq!(
960 decode_header(&h),
961 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
962 );
963 }
964
965 #[test]
966 fn sheddable_rejected_on_every_illegal_frame_type() {
967 let flags = Flags::new(false, Priority::Passive, false)
968 .with_admission_class(AdmissionClass::Sheddable);
969 for ty in [
970 FrameType::Request,
971 FrameType::Response,
972 FrameType::StreamEnd,
973 FrameType::Error,
974 FrameType::Cancel,
975 FrameType::Ping,
976 FrameType::Pong,
977 FrameType::Hello,
978 FrameType::HelloAck,
979 FrameType::Goodbye,
980 ] {
981 let h = hdr(0, ty, flags, 1, 2);
982 assert_eq!(
983 decode_header(&h.encode()),
984 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
985 );
986 }
987 }
988
989 #[test]
990 fn nonzero_epoch_on_control_channel_is_rejected() {
991 let h = hdr_with_epoch(
992 0,
993 FrameType::Request,
994 Flags::new(false, Priority::Passive, false),
995 0,
996 u32::MAX,
997 2,
998 );
999 assert_eq!(
1000 decode_header(&h.encode()),
1001 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
1002 );
1003 }
1004}