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, Serialize, Deserialize, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct BindIdentity {
103 pub project_root: PathBuf,
104 pub harness: String,
105 pub session: String,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub project_id: Option<String>,
125}
126
127impl BindIdentity {
128 pub fn new(
133 project_root: impl Into<PathBuf>,
134 harness: impl Into<String>,
135 session: impl Into<String>,
136 ) -> Self {
137 Self {
138 project_root: project_root.into(),
139 harness: harness.into(),
140 session: session.into(),
141 project_id: None,
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(tag = "kind", rename_all = "snake_case")]
149pub enum Principal {
150 Reserved { module_id: String },
152 Direct,
154 Unverified,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171#[serde(tag = "kind", rename_all = "snake_case")]
172pub enum RouteTarget {
173 ToolProvider {
174 module_id: String,
175 },
176 ManagementSurface {
177 module_id: String,
178 },
179 InternalService {
180 module_id: String,
181 service_id: String,
182 },
183}
184
185pub const PROTOCOL_VERSION: u8 = 2;
187
188pub const SUBC_PROTOCOL_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
197
198pub const MIN_SUPPORTED_VERSION: u8 = 2;
200
201pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
204
205pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
210
211pub const HEADER_LEN: usize = 21;
213
214pub const FROZEN_PREFIX_LEN: usize = 5;
218
219pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
235pub struct ErrorBody {
236 pub code: String,
237 pub message: String,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub detail: Option<serde_json::Value>,
240}
241
242impl ErrorBody {
243 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
245 Self {
246 code: code.into(),
247 message: message.into(),
248 detail: None,
249 }
250 }
251
252 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
254 self.detail = Some(detail);
255 self
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
261pub struct ModuleHelloBody {
262 pub manifest: manifest::ModuleManifest,
263 pub protocol_ver: u8,
264 #[serde(default)]
265 pub control_ops: Option<Vec<String>>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub launch_nonce: Option<String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279pub struct ModuleHelloAckBody {
280 pub negotiated_ver: u8,
281 pub subc_ops: Vec<String>,
282 pub subc_capabilities: Vec<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub storage: Option<serde_json::Value>,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298#[repr(u8)]
299pub enum FrameType {
300 Request = 0,
301 Response = 1,
302 Push = 2,
303 StreamData = 3,
304 StreamEnd = 4,
305 Error = 5,
306 Cancel = 6,
307 Ping = 7,
308 Pong = 8,
309 Hello = 9,
310 HelloAck = 10,
311 Goodbye = 11,
312}
313
314impl FrameType {
315 pub fn from_u8(b: u8) -> Option<Self> {
317 Some(match b {
318 0 => Self::Request,
319 1 => Self::Response,
320 2 => Self::Push,
321 3 => Self::StreamData,
322 4 => Self::StreamEnd,
323 5 => Self::Error,
324 6 => Self::Cancel,
325 7 => Self::Ping,
326 8 => Self::Pong,
327 9 => Self::Hello,
328 10 => Self::HelloAck,
329 11 => Self::Goodbye,
330 _ => return None,
331 })
332 }
333
334 pub fn is_pure_header(self) -> bool {
335 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342#[repr(u8)]
343pub enum Priority {
344 Passive = 0,
345 Interactive = 1,
346 Background = 2,
347}
348
349impl Priority {
350 fn from_bits(bits: u8) -> Option<Self> {
351 Some(match bits {
352 0 => Self::Passive,
353 1 => Self::Interactive,
354 2 => Self::Background,
355 _ => return None,
356 })
357 }
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362#[repr(u8)]
363pub enum AdmissionClass {
364 Normal = 0,
365 Expedite = 1,
366 Sheddable = 2,
367}
368
369impl AdmissionClass {
370 fn from_bits(bits: u8) -> Option<Self> {
371 Some(match bits {
372 0 => Self::Normal,
373 1 => Self::Expedite,
374 2 => Self::Sheddable,
375 _ => return None,
376 })
377 }
378}
379
380const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
383const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
386pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
387const FLAG_RESERVED_MASK: u8 = 0b1000_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub struct Flags(pub u8);
392
393impl Flags {
394 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
396 let mut b = 0u8;
397 if binary {
398 b |= FLAG_BINARY;
399 }
400 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
401 if last {
402 b |= FLAG_LAST;
403 }
404 Flags(b)
405 }
406
407 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
409 self.0 =
410 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
411 self
412 }
413
414 pub fn is_binary(self) -> bool {
416 self.0 & FLAG_BINARY != 0
417 }
418
419 pub fn is_last(self) -> bool {
421 self.0 & FLAG_LAST != 0
422 }
423
424 pub fn priority(self) -> Option<Priority> {
426 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
427 }
428
429 pub fn admission_class(self) -> Option<AdmissionClass> {
431 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
432 }
433
434 pub fn has_reserved_bits(self) -> bool {
436 self.0 & FLAG_RESERVED_MASK != 0
437 }
438
439 pub fn is_daemon_origin(self) -> bool {
441 self.0 & FLAG_DAEMON_ORIGIN != 0
442 }
443
444 pub fn with_daemon_origin(mut self) -> Self {
446 self.0 |= FLAG_DAEMON_ORIGIN;
447 self
448 }
449
450 pub fn without_daemon_origin(self) -> Self {
452 Self(self.0 & !FLAG_DAEMON_ORIGIN)
453 }
454}
455
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub struct EnvelopeHeader {
459 pub len: u32,
461 pub ver: u8,
463 pub ty: FrameType,
465 pub flags: Flags,
467 pub channel: u16,
469 pub epoch: u32,
471 pub corr: u64,
473}
474
475impl EnvelopeHeader {
476 pub fn encode(&self) -> [u8; HEADER_LEN] {
478 let mut buf = [0u8; HEADER_LEN];
479 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
480 buf[4] = self.ver;
481 buf[5] = self.ty as u8;
482 buf[6] = self.flags.0;
483 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
484 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
485 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
486 buf
487 }
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum DecodeError {
493 TooShortForPrefix { have: usize },
495 UnsupportedVersion { ver: u8 },
497 TooShortForHeader { have: usize, need: usize },
499 UnknownFrameType { byte: u8 },
501 ReservedFlagBits { flags: u8 },
503 ReservedPriorityBits { flags: u8 },
505 ReservedAdmissionClass { flags: u8 },
507 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
509 NonzeroEpochOnControlChannel { epoch: u32 },
511 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
513}
514
515impl fmt::Display for DecodeError {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 match self {
518 Self::TooShortForPrefix { have } => {
519 write!(f, "header shorter than frozen prefix: have {have} bytes")
520 }
521 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
522 Self::TooShortForHeader { have, need } => {
523 write!(
524 f,
525 "header too short for version: have {have} bytes, need {need}"
526 )
527 }
528 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
529 Self::ReservedFlagBits { flags } => {
530 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
531 }
532 Self::ReservedPriorityBits { flags } => {
533 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
534 }
535 Self::ReservedAdmissionClass { flags } => {
536 write!(f, "reserved admission class set in flags 0b{flags:08b}")
537 }
538 Self::SheddableIllegalFrameType { ty, flags } => write!(
539 f,
540 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
541 ),
542 Self::NonzeroEpochOnControlChannel { epoch } => {
543 write!(f, "control channel carried nonzero epoch {epoch}")
544 }
545 Self::PureHeaderFrameWithBody { ty, len } => {
546 write!(
547 f,
548 "pure-header frame {ty:?} declared non-zero body length {len}"
549 )
550 }
551 }
552 }
553}
554
555impl Error for DecodeError {}
556
557fn header_len_for_version(ver: u8) -> Option<usize> {
560 match ver {
561 PROTOCOL_VERSION => Some(HEADER_LEN),
562 _ => None,
563 }
564}
565
566pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
574 if bytes.len() < FROZEN_PREFIX_LEN {
575 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
576 }
577 let ver = bytes[4];
578 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
579 if bytes.len() < need {
580 return Err(DecodeError::TooShortForHeader {
581 have: bytes.len(),
582 need,
583 });
584 }
585
586 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
587 let ty =
588 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
589 let flags = Flags(bytes[6]);
590 if flags.has_reserved_bits() {
591 return Err(DecodeError::ReservedFlagBits { flags: bytes[6] });
592 }
593 if flags.priority().is_none() {
594 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
595 }
596 let admission_class = flags
597 .admission_class()
598 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
599 if admission_class == AdmissionClass::Sheddable
600 && !matches!(ty, FrameType::Push | FrameType::StreamData)
601 {
602 return Err(DecodeError::SheddableIllegalFrameType {
603 ty,
604 flags: bytes[6],
605 });
606 }
607 if ty.is_pure_header() && len != 0 {
608 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
609 }
610 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
611 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
612 if channel == 0 && epoch != 0 {
613 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
614 }
615 let corr = u64::from_le_bytes([
616 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
617 ]);
618
619 Ok(EnvelopeHeader {
620 len,
621 ver,
622 ty,
623 flags,
624 channel,
625 epoch,
626 corr,
627 })
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
635 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
636 }
637
638 fn hdr_with_epoch(
639 len: u32,
640 ty: FrameType,
641 flags: Flags,
642 channel: u16,
643 epoch: u32,
644 corr: u64,
645 ) -> EnvelopeHeader {
646 EnvelopeHeader {
647 len,
648 ver: PROTOCOL_VERSION,
649 ty,
650 flags,
651 channel,
652 epoch,
653 corr,
654 }
655 }
656
657 #[test]
658 fn bind_identity_with_project_id_round_trips_json() {
659 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
660 identity.project_id = Some("pj-a1b2c3d4".to_string());
661
662 let encoded = serde_json::to_vec(&identity).unwrap();
663 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
664
665 assert_eq!(decoded, identity);
666 }
667
668 #[test]
669 fn bind_identity_without_project_id_round_trips_json() {
670 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
671
672 let encoded = serde_json::to_vec(&identity).unwrap();
673 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
674
675 assert_eq!(decoded, identity);
676 }
677
678 #[test]
679 fn legacy_bind_identity_without_project_id_decodes() {
680 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
681 "project_root": "/tmp/project",
682 "harness": "opencode",
683 "session": "session-1"
684 }))
685 .unwrap();
686
687 assert_eq!(decoded.project_id, None);
688 }
689
690 #[test]
691 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
692 let encoded =
693 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
694 .unwrap();
695
696 assert!(encoded.get("project_id").is_none());
697 }
698
699 #[test]
700 fn wire_crate_version_is_a_numeric_three_component_version() {
701 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
702
703 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
704 assert_eq!(components.len(), 3);
705 assert!(components
706 .iter()
707 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
708 }
709
710 #[test]
711 fn route_target_variants_round_trip_json() {
712 let targets = [
713 RouteTarget::ToolProvider {
714 module_id: "aft".to_string(),
715 },
716 RouteTarget::ManagementSurface {
717 module_id: "memory".to_string(),
718 },
719 RouteTarget::InternalService {
720 module_id: "bus".to_string(),
721 service_id: "dm".to_string(),
722 },
723 ];
724
725 for target in targets {
726 let encoded = serde_json::to_vec(&target).unwrap();
727 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
728 assert_eq!(decoded, target);
729 }
730 }
731
732 #[test]
733 fn error_body_round_trips_json() {
734 let body = ErrorBody {
735 code: "config_divergence".to_string(),
736 message: "active config differs".to_string(),
737 detail: None,
738 };
739
740 let encoded = serde_json::to_vec(&body).unwrap();
741 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
742
743 assert_eq!(decoded, body);
744 }
745
746 #[test]
747 fn round_trip_request() {
748 let h = hdr(
749 1234,
750 FrameType::Request,
751 Flags::new(false, Priority::Interactive, false),
752 42,
753 0xDEAD_BEEF_0000_0001,
754 );
755 let decoded = decode_header(&h.encode()).unwrap();
756 assert_eq!(h, decoded);
757 }
758
759 #[test]
760 fn round_trip_all_frame_types() {
761 for b in 0u8..=11 {
762 let ty = FrameType::from_u8(b).unwrap();
763 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
764 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
765 }
766 }
767
768 #[test]
769 fn pure_header_frame_has_zero_len() {
770 let h = hdr(
772 0,
773 FrameType::Cancel,
774 Flags::new(false, Priority::Passive, false),
775 7,
776 99,
777 );
778 let d = decode_header(&h.encode()).unwrap();
779 assert_eq!(d.len, 0);
780 assert_eq!(d.corr, 99);
781 }
782
783 #[test]
784 fn flags_round_trip() {
785 let f = Flags::new(true, Priority::Background, true)
786 .with_admission_class(AdmissionClass::Expedite);
787 assert!(f.is_binary());
788 assert!(f.is_last());
789 assert_eq!(f.priority(), Some(Priority::Background));
790 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
791 let h = hdr(8, FrameType::StreamData, f, 1, 1);
792 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
793 }
794
795 #[test]
796 fn daemon_origin_flags_decode_and_round_trip() {
797 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
798 let old_decoded = decode_header(&old.encode()).unwrap();
799 assert!(!old_decoded.flags.is_daemon_origin());
800
801 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
802 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
803 assert!(daemon_decoded.flags.is_daemon_origin());
804 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
805 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
806 }
807
808 #[test]
809 fn little_endian_and_frozen_prefix_layout() {
810 let h = hdr_with_epoch(
811 0x0403_0201,
812 FrameType::Request,
813 Flags(0),
814 0x0605,
815 0x0a09_0807,
816 0x1211_100f_0e0d_0c0b,
817 );
818 let buf = h.encode();
819 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
820 assert_eq!(buf[4], PROTOCOL_VERSION);
821 assert_eq!(&buf[7..9], &[5, 6]);
822 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
823 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
824 assert_eq!(buf.len(), HEADER_LEN);
825 }
826
827 #[test]
828 fn reject_too_short_for_prefix() {
829 assert_eq!(
830 decode_header(&[0, 0, 0, 0]),
831 Err(DecodeError::TooShortForPrefix { have: 4 })
832 );
833 }
834
835 #[test]
836 fn reject_too_short_for_header() {
837 let mut b = [0u8; 10];
839 b[4] = PROTOCOL_VERSION;
840 assert_eq!(
841 decode_header(&b),
842 Err(DecodeError::TooShortForHeader {
843 have: 10,
844 need: HEADER_LEN
845 })
846 );
847 }
848
849 #[test]
850 fn reject_unsupported_version() {
851 let mut b = [0u8; HEADER_LEN];
852 b[4] = 1;
853 assert_eq!(
854 decode_header(&b),
855 Err(DecodeError::UnsupportedVersion { ver: 1 })
856 );
857 }
858
859 #[test]
860 fn reject_unknown_frame_type() {
861 let mut b = [0u8; HEADER_LEN];
862 b[4] = PROTOCOL_VERSION;
863 b[5] = 99;
864 assert_eq!(
865 decode_header(&b),
866 Err(DecodeError::UnknownFrameType { byte: 99 })
867 );
868 }
869
870 #[test]
871 fn reject_reserved_flag_bits() {
872 let mut b = [0u8; HEADER_LEN];
873 b[4] = PROTOCOL_VERSION;
874 b[5] = FrameType::Request as u8;
875 b[6] = 0b1000_0000; assert_eq!(
877 decode_header(&b),
878 Err(DecodeError::ReservedFlagBits { flags: 0b1000_0000 })
879 );
880 }
881
882 #[test]
883 fn reject_reserved_priority_bits() {
884 let mut b = [0u8; HEADER_LEN];
885 b[4] = PROTOCOL_VERSION;
886 b[5] = FrameType::Request as u8;
887 b[6] = 0b0000_0110; assert_eq!(
889 decode_header(&b),
890 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
891 );
892 }
893
894 #[test]
895 fn reject_pure_header_frame_with_body_len() {
896 let h = hdr(
897 1,
898 FrameType::Ping,
899 Flags::new(false, Priority::Passive, false),
900 0,
901 1,
902 );
903 assert_eq!(
904 decode_header(&h.encode()),
905 Err(DecodeError::PureHeaderFrameWithBody {
906 ty: FrameType::Ping,
907 len: 1
908 })
909 );
910 }
911
912 #[test]
913 fn epoch_boundaries_round_trip() {
914 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
915 let h = hdr_with_epoch(
916 0,
917 FrameType::Request,
918 Flags::new(false, Priority::Passive, false),
919 channel,
920 epoch,
921 9,
922 );
923 assert_eq!(decode_header(&h.encode()).unwrap(), h);
924 }
925 }
926
927 #[test]
928 fn admission_classes_accept_three_values_and_reject_reserved_value() {
929 for (ty, admission_class) in [
930 (FrameType::Request, AdmissionClass::Normal),
931 (FrameType::Request, AdmissionClass::Expedite),
932 (FrameType::Push, AdmissionClass::Sheddable),
933 (FrameType::StreamData, AdmissionClass::Sheddable),
934 ] {
935 let flags = Flags::new(false, Priority::Interactive, false)
936 .with_admission_class(admission_class);
937 let h = hdr(0, ty, flags, 1, 2);
938 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
939 }
940
941 let mut h = hdr(
942 0,
943 FrameType::Push,
944 Flags::new(false, Priority::Passive, false),
945 1,
946 2,
947 )
948 .encode();
949 h[6] |= 0b0011_0000;
950 assert_eq!(
951 decode_header(&h),
952 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
953 );
954 }
955
956 #[test]
957 fn sheddable_rejected_on_every_illegal_frame_type() {
958 let flags = Flags::new(false, Priority::Passive, false)
959 .with_admission_class(AdmissionClass::Sheddable);
960 for ty in [
961 FrameType::Request,
962 FrameType::Response,
963 FrameType::StreamEnd,
964 FrameType::Error,
965 FrameType::Cancel,
966 FrameType::Ping,
967 FrameType::Pong,
968 FrameType::Hello,
969 FrameType::HelloAck,
970 FrameType::Goodbye,
971 ] {
972 let h = hdr(0, ty, flags, 1, 2);
973 assert_eq!(
974 decode_header(&h.encode()),
975 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
976 );
977 }
978 }
979
980 #[test]
981 fn nonzero_epoch_on_control_channel_is_rejected() {
982 let h = hdr_with_epoch(
983 0,
984 FrameType::Request,
985 Flags::new(false, Priority::Passive, false),
986 0,
987 u32::MAX,
988 2,
989 );
990 assert_eq!(
991 decode_header(&h.encode()),
992 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
993 );
994 }
995}