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