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