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 use crate::Flags;
51
52 pub const UNKNOWN_CHANNEL: &str = "unknown_channel";
53 pub const STALE_ROUTE_EPOCH: &str = "stale_route_epoch";
54 pub const UNKNOWN_MODULE: &str = "unknown_module";
55 pub const MODULE_REMOVED: &str = "module_removed";
56 pub const MODULE_RELOADING: &str = "module_reloading";
70 pub const MODULE_WARMING: &str = "module_warming";
71 pub const TARGET_UNAVAILABLE: &str = "target_unavailable";
72 pub const MODULE_TIMEOUT: &str = "module_timeout";
73 pub const MODULE_NO_PROTOCOL: &str = "module_no_protocol";
85
86 pub fn is_retryable_route_open(code: &str) -> bool {
106 matches!(
107 code,
108 MODULE_RELOADING | MODULE_WARMING | TARGET_UNAVAILABLE | MODULE_TIMEOUT
109 )
110 }
111
112 pub fn is_established_route_dead(_flags: Flags, code: &str) -> bool {
128 matches!(code, UNKNOWN_CHANNEL | STALE_ROUTE_EPOCH)
129 }
130}
131
132pub use frame::{Frame, FrameBuildError};
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
136#[serde(rename_all = "snake_case")]
137pub enum RouteCloseReason {
138 Reload,
139 Restart,
140 Disable,
141 Crash,
142 CapabilityDenied,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct BindIdentity {
170 pub project_root: PathBuf,
171 pub harness: String,
172 pub session: String,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub project_id: Option<String>,
192}
193
194impl BindIdentity {
195 pub fn new(
200 project_root: impl Into<PathBuf>,
201 harness: impl Into<String>,
202 session: impl Into<String>,
203 ) -> Self {
204 Self {
205 project_root: project_root.into(),
206 harness: harness.into(),
207 session: session.into(),
208 project_id: None,
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215#[serde(tag = "kind", rename_all = "snake_case")]
216pub enum Principal {
217 Reserved { module_id: String },
219 Direct,
221 Unverified,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238#[serde(tag = "kind", rename_all = "snake_case")]
239pub enum RouteTarget {
240 ToolProvider {
241 module_id: String,
242 },
243 ManagementSurface {
244 module_id: String,
245 },
246 InternalService {
247 module_id: String,
248 service_id: String,
249 },
250}
251
252pub const PROTOCOL_VERSION: u8 = 2;
254
255pub const SUBC_PROTOCOL_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
264
265pub const MIN_SUPPORTED_VERSION: u8 = 2;
267
268pub const SUBC_MODULE_ID_ENV: &str = "SUBC_MODULE_ID";
271
272pub const SUBC_LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
277
278pub const HEADER_LEN: usize = 21;
280
281pub const FROZEN_PREFIX_LEN: usize = 5;
285
286pub const MAX_FRAME_BODY_LEN: u32 = 64 * 1024 * 1024;
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
302pub struct ErrorBody {
303 pub code: String,
304 pub message: String,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub detail: Option<serde_json::Value>,
307}
308
309impl ErrorBody {
310 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
312 Self {
313 code: code.into(),
314 message: message.into(),
315 detail: None,
316 }
317 }
318
319 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
321 self.detail = Some(detail);
322 self
323 }
324}
325
326#[derive(Clone, Serialize, Deserialize, PartialEq)]
328pub struct ModuleHelloBody {
329 pub manifest: manifest::ModuleManifest,
330 pub protocol_ver: u8,
331 #[serde(default)]
332 pub control_ops: Option<Vec<String>>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub launch_nonce: Option<String>,
342}
343
344impl fmt::Debug for ModuleHelloBody {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 f.debug_struct("ModuleHelloBody")
351 .field("manifest", &self.manifest)
352 .field("protocol_ver", &self.protocol_ver)
353 .field("control_ops", &self.control_ops)
354 .field(
355 "launch_nonce",
356 &self
357 .launch_nonce
358 .as_ref()
359 .map(|nonce| format!("<{} bytes redacted>", nonce.len())),
360 )
361 .finish()
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
367pub struct ModuleHelloAckBody {
368 pub negotiated_ver: u8,
369 pub subc_ops: Vec<String>,
370 pub subc_capabilities: Vec<String>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub storage: Option<serde_json::Value>,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub machine_id: Option<String>,
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397#[repr(u8)]
398pub enum FrameType {
399 Request = 0,
400 Response = 1,
401 Push = 2,
402 StreamData = 3,
403 StreamEnd = 4,
404 Error = 5,
405 Cancel = 6,
406 Ping = 7,
407 Pong = 8,
408 Hello = 9,
409 HelloAck = 10,
410 Goodbye = 11,
411}
412
413impl FrameType {
414 pub fn from_u8(b: u8) -> Option<Self> {
416 Some(match b {
417 0 => Self::Request,
418 1 => Self::Response,
419 2 => Self::Push,
420 3 => Self::StreamData,
421 4 => Self::StreamEnd,
422 5 => Self::Error,
423 6 => Self::Cancel,
424 7 => Self::Ping,
425 8 => Self::Pong,
426 9 => Self::Hello,
427 10 => Self::HelloAck,
428 11 => Self::Goodbye,
429 _ => return None,
430 })
431 }
432
433 pub fn is_pure_header(self) -> bool {
434 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
435 }
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441#[repr(u8)]
442pub enum Priority {
443 Passive = 0,
444 Interactive = 1,
445 Background = 2,
446}
447
448impl Priority {
449 fn from_bits(bits: u8) -> Option<Self> {
450 Some(match bits {
451 0 => Self::Passive,
452 1 => Self::Interactive,
453 2 => Self::Background,
454 _ => return None,
455 })
456 }
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
461#[repr(u8)]
462pub enum AdmissionClass {
463 Normal = 0,
464 Expedite = 1,
465 Sheddable = 2,
466}
467
468impl AdmissionClass {
469 fn from_bits(bits: u8) -> Option<Self> {
470 Some(match bits {
471 0 => Self::Normal,
472 1 => Self::Expedite,
473 2 => Self::Sheddable,
474 _ => return None,
475 })
476 }
477}
478
479const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
482const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
485pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
486pub const FLAG_SUBSCRIPTION: u8 = 0b1000_0000;
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub struct Flags(pub u8);
492
493impl Flags {
494 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
496 let mut b = 0u8;
497 if binary {
498 b |= FLAG_BINARY;
499 }
500 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
501 if last {
502 b |= FLAG_LAST;
503 }
504 Flags(b)
505 }
506
507 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
509 self.0 =
510 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
511 self
512 }
513
514 pub fn is_binary(self) -> bool {
516 self.0 & FLAG_BINARY != 0
517 }
518
519 pub fn is_last(self) -> bool {
521 self.0 & FLAG_LAST != 0
522 }
523
524 pub fn priority(self) -> Option<Priority> {
526 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
527 }
528
529 pub fn admission_class(self) -> Option<AdmissionClass> {
531 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
532 }
533
534 pub fn is_subscription(self) -> bool {
536 self.0 & FLAG_SUBSCRIPTION != 0
537 }
538
539 pub fn is_daemon_origin(self) -> bool {
541 self.0 & FLAG_DAEMON_ORIGIN != 0
542 }
543
544 pub fn with_daemon_origin(mut self) -> Self {
546 self.0 |= FLAG_DAEMON_ORIGIN;
547 self
548 }
549
550 pub fn without_daemon_origin(self) -> Self {
552 Self(self.0 & !FLAG_DAEMON_ORIGIN)
553 }
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub struct EnvelopeHeader {
559 pub len: u32,
561 pub ver: u8,
563 pub ty: FrameType,
565 pub flags: Flags,
567 pub channel: u16,
569 pub epoch: u32,
571 pub corr: u64,
573}
574
575impl EnvelopeHeader {
576 pub fn encode(&self) -> [u8; HEADER_LEN] {
578 let mut buf = [0u8; HEADER_LEN];
579 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
580 buf[4] = self.ver;
581 buf[5] = self.ty as u8;
582 buf[6] = self.flags.0;
583 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
584 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
585 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
586 buf
587 }
588}
589
590#[derive(Debug, Clone, Copy, PartialEq, Eq)]
592pub enum DecodeError {
593 TooShortForPrefix { have: usize },
595 UnsupportedVersion { ver: u8 },
597 TooShortForHeader { have: usize, need: usize },
599 UnknownFrameType { byte: u8 },
601 ReservedFlagBits { flags: u8 },
603 ReservedPriorityBits { flags: u8 },
605 ReservedAdmissionClass { flags: u8 },
607 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
609 NonzeroEpochOnControlChannel { epoch: u32 },
611 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
613}
614
615impl fmt::Display for DecodeError {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 match self {
618 Self::TooShortForPrefix { have } => {
619 write!(f, "header shorter than frozen prefix: have {have} bytes")
620 }
621 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
622 Self::TooShortForHeader { have, need } => {
623 write!(
624 f,
625 "header too short for version: have {have} bytes, need {need}"
626 )
627 }
628 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
629 Self::ReservedFlagBits { flags } => {
630 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
631 }
632 Self::ReservedPriorityBits { flags } => {
633 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
634 }
635 Self::ReservedAdmissionClass { flags } => {
636 write!(f, "reserved admission class set in flags 0b{flags:08b}")
637 }
638 Self::SheddableIllegalFrameType { ty, flags } => write!(
639 f,
640 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
641 ),
642 Self::NonzeroEpochOnControlChannel { epoch } => {
643 write!(f, "control channel carried nonzero epoch {epoch}")
644 }
645 Self::PureHeaderFrameWithBody { ty, len } => {
646 write!(
647 f,
648 "pure-header frame {ty:?} declared non-zero body length {len}"
649 )
650 }
651 }
652 }
653}
654
655impl Error for DecodeError {}
656
657fn header_len_for_version(ver: u8) -> Option<usize> {
660 match ver {
661 PROTOCOL_VERSION => Some(HEADER_LEN),
662 _ => None,
663 }
664}
665
666pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
674 if bytes.len() < FROZEN_PREFIX_LEN {
675 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
676 }
677 let ver = bytes[4];
678 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
679 if bytes.len() < need {
680 return Err(DecodeError::TooShortForHeader {
681 have: bytes.len(),
682 need,
683 });
684 }
685
686 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
687 let ty =
688 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
689 let flags = Flags(bytes[6]);
690 if flags.priority().is_none() {
691 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
692 }
693 let admission_class = flags
694 .admission_class()
695 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
696 if admission_class == AdmissionClass::Sheddable
697 && !matches!(ty, FrameType::Push | FrameType::StreamData)
698 {
699 return Err(DecodeError::SheddableIllegalFrameType {
700 ty,
701 flags: bytes[6],
702 });
703 }
704 if ty.is_pure_header() && len != 0 {
705 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
706 }
707 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
708 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
709 if channel == 0 && epoch != 0 {
710 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
711 }
712 let corr = u64::from_le_bytes([
713 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
714 ]);
715
716 Ok(EnvelopeHeader {
717 len,
718 ver,
719 ty,
720 flags,
721 channel,
722 epoch,
723 corr,
724 })
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
732 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
733 }
734
735 fn hdr_with_epoch(
736 len: u32,
737 ty: FrameType,
738 flags: Flags,
739 channel: u16,
740 epoch: u32,
741 corr: u64,
742 ) -> EnvelopeHeader {
743 EnvelopeHeader {
744 len,
745 ver: PROTOCOL_VERSION,
746 ty,
747 flags,
748 channel,
749 epoch,
750 corr,
751 }
752 }
753
754 #[test]
755 fn bind_identity_with_project_id_round_trips_json() {
756 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
757 identity.project_id = Some("pj-a1b2c3d4".to_string());
758
759 let encoded = serde_json::to_vec(&identity).unwrap();
760 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
761
762 assert_eq!(decoded, identity);
763 }
764
765 #[test]
766 fn bind_identity_without_project_id_round_trips_json() {
767 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
768
769 let encoded = serde_json::to_vec(&identity).unwrap();
770 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
771
772 assert_eq!(decoded, identity);
773 }
774
775 #[test]
776 fn legacy_bind_identity_without_project_id_decodes() {
777 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
778 "project_root": "/tmp/project",
779 "harness": "opencode",
780 "session": "session-1"
781 }))
782 .unwrap();
783
784 assert_eq!(decoded.project_id, None);
785 }
786
787 #[test]
788 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
789 let encoded =
790 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
791 .unwrap();
792
793 assert!(encoded.get("project_id").is_none());
794 }
795
796 #[test]
797 fn wire_crate_version_is_a_numeric_three_component_version() {
798 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
799
800 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
801 assert_eq!(components.len(), 3);
802 assert!(components
803 .iter()
804 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
805 }
806
807 #[test]
808 fn route_target_variants_round_trip_json() {
809 let targets = [
810 RouteTarget::ToolProvider {
811 module_id: "aft".to_string(),
812 },
813 RouteTarget::ManagementSurface {
814 module_id: "memory".to_string(),
815 },
816 RouteTarget::InternalService {
817 module_id: "bus".to_string(),
818 service_id: "dm".to_string(),
819 },
820 ];
821
822 for target in targets {
823 let encoded = serde_json::to_vec(&target).unwrap();
824 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
825 assert_eq!(decoded, target);
826 }
827 }
828
829 #[test]
830 fn error_body_round_trips_json() {
831 let body = ErrorBody {
832 code: "config_divergence".to_string(),
833 message: "active config differs".to_string(),
834 detail: None,
835 };
836
837 let encoded = serde_json::to_vec(&body).unwrap();
838 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
839
840 assert_eq!(decoded, body);
841 }
842
843 #[test]
844 fn round_trip_request() {
845 let h = hdr(
846 1234,
847 FrameType::Request,
848 Flags::new(false, Priority::Interactive, false),
849 42,
850 0xDEAD_BEEF_0000_0001,
851 );
852 let decoded = decode_header(&h.encode()).unwrap();
853 assert_eq!(h, decoded);
854 }
855
856 #[test]
857 fn round_trip_all_frame_types() {
858 for b in 0u8..=11 {
859 let ty = FrameType::from_u8(b).unwrap();
860 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
861 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
862 }
863 }
864
865 #[test]
866 fn pure_header_frame_has_zero_len() {
867 let h = hdr(
869 0,
870 FrameType::Cancel,
871 Flags::new(false, Priority::Passive, false),
872 7,
873 99,
874 );
875 let d = decode_header(&h.encode()).unwrap();
876 assert_eq!(d.len, 0);
877 assert_eq!(d.corr, 99);
878 }
879
880 #[test]
881 fn flags_round_trip() {
882 let f = Flags::new(true, Priority::Background, true)
883 .with_admission_class(AdmissionClass::Expedite);
884 assert!(f.is_binary());
885 assert!(f.is_last());
886 assert_eq!(f.priority(), Some(Priority::Background));
887 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
888 let h = hdr(8, FrameType::StreamData, f, 1, 1);
889 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
890 }
891
892 #[test]
893 fn daemon_origin_flags_decode_and_round_trip() {
894 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
895 let old_decoded = decode_header(&old.encode()).unwrap();
896 assert!(!old_decoded.flags.is_daemon_origin());
897
898 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
899 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
900 assert!(daemon_decoded.flags.is_daemon_origin());
901 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
902 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
903 }
904
905 #[test]
906 fn little_endian_and_frozen_prefix_layout() {
907 let h = hdr_with_epoch(
908 0x0403_0201,
909 FrameType::Request,
910 Flags(0),
911 0x0605,
912 0x0a09_0807,
913 0x1211_100f_0e0d_0c0b,
914 );
915 let buf = h.encode();
916 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
917 assert_eq!(buf[4], PROTOCOL_VERSION);
918 assert_eq!(&buf[7..9], &[5, 6]);
919 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
920 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
921 assert_eq!(buf.len(), HEADER_LEN);
922 }
923
924 #[test]
925 fn reject_too_short_for_prefix() {
926 assert_eq!(
927 decode_header(&[0, 0, 0, 0]),
928 Err(DecodeError::TooShortForPrefix { have: 4 })
929 );
930 }
931
932 #[test]
933 fn reject_too_short_for_header() {
934 let mut b = [0u8; 10];
936 b[4] = PROTOCOL_VERSION;
937 assert_eq!(
938 decode_header(&b),
939 Err(DecodeError::TooShortForHeader {
940 have: 10,
941 need: HEADER_LEN
942 })
943 );
944 }
945
946 #[test]
947 fn reject_unsupported_version() {
948 let mut b = [0u8; HEADER_LEN];
949 b[4] = 1;
950 assert_eq!(
951 decode_header(&b),
952 Err(DecodeError::UnsupportedVersion { ver: 1 })
953 );
954 }
955
956 #[test]
957 fn reject_unknown_frame_type() {
958 let mut b = [0u8; HEADER_LEN];
959 b[4] = PROTOCOL_VERSION;
960 b[5] = 99;
961 assert_eq!(
962 decode_header(&b),
963 Err(DecodeError::UnknownFrameType { byte: 99 })
964 );
965 }
966
967 #[test]
968 fn subscription_flag_decodes_and_tags_the_request() {
969 let mut b = [0u8; HEADER_LEN];
970 b[4] = PROTOCOL_VERSION;
971 b[5] = FrameType::Request as u8;
972 b[6] = FLAG_SUBSCRIPTION;
973 let decoded = decode_header(&b).unwrap();
974 assert!(decoded.flags.is_subscription());
975 }
976
977 #[test]
978 fn reject_reserved_priority_bits() {
979 let mut b = [0u8; HEADER_LEN];
980 b[4] = PROTOCOL_VERSION;
981 b[5] = FrameType::Request as u8;
982 b[6] = 0b0000_0110; assert_eq!(
984 decode_header(&b),
985 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
986 );
987 }
988
989 #[test]
990 fn reject_pure_header_frame_with_body_len() {
991 let h = hdr(
992 1,
993 FrameType::Ping,
994 Flags::new(false, Priority::Passive, false),
995 0,
996 1,
997 );
998 assert_eq!(
999 decode_header(&h.encode()),
1000 Err(DecodeError::PureHeaderFrameWithBody {
1001 ty: FrameType::Ping,
1002 len: 1
1003 })
1004 );
1005 }
1006
1007 #[test]
1008 fn epoch_boundaries_round_trip() {
1009 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
1010 let h = hdr_with_epoch(
1011 0,
1012 FrameType::Request,
1013 Flags::new(false, Priority::Passive, false),
1014 channel,
1015 epoch,
1016 9,
1017 );
1018 assert_eq!(decode_header(&h.encode()).unwrap(), h);
1019 }
1020 }
1021
1022 #[test]
1023 fn admission_classes_accept_three_values_and_reject_reserved_value() {
1024 for (ty, admission_class) in [
1025 (FrameType::Request, AdmissionClass::Normal),
1026 (FrameType::Request, AdmissionClass::Expedite),
1027 (FrameType::Push, AdmissionClass::Sheddable),
1028 (FrameType::StreamData, AdmissionClass::Sheddable),
1029 ] {
1030 let flags = Flags::new(false, Priority::Interactive, false)
1031 .with_admission_class(admission_class);
1032 let h = hdr(0, ty, flags, 1, 2);
1033 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
1034 }
1035
1036 let mut h = hdr(
1037 0,
1038 FrameType::Push,
1039 Flags::new(false, Priority::Passive, false),
1040 1,
1041 2,
1042 )
1043 .encode();
1044 h[6] |= 0b0011_0000;
1045 assert_eq!(
1046 decode_header(&h),
1047 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
1048 );
1049 }
1050
1051 #[test]
1052 fn sheddable_rejected_on_every_illegal_frame_type() {
1053 let flags = Flags::new(false, Priority::Passive, false)
1054 .with_admission_class(AdmissionClass::Sheddable);
1055 for ty in [
1056 FrameType::Request,
1057 FrameType::Response,
1058 FrameType::StreamEnd,
1059 FrameType::Error,
1060 FrameType::Cancel,
1061 FrameType::Ping,
1062 FrameType::Pong,
1063 FrameType::Hello,
1064 FrameType::HelloAck,
1065 FrameType::Goodbye,
1066 ] {
1067 let h = hdr(0, ty, flags, 1, 2);
1068 assert_eq!(
1069 decode_header(&h.encode()),
1070 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
1071 );
1072 }
1073 }
1074
1075 #[test]
1076 fn nonzero_epoch_on_control_channel_is_rejected() {
1077 let h = hdr_with_epoch(
1078 0,
1079 FrameType::Request,
1080 Flags::new(false, Priority::Passive, false),
1081 0,
1082 u32::MAX,
1083 2,
1084 );
1085 assert_eq!(
1086 decode_header(&h.encode()),
1087 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
1088 );
1089 }
1090}
1091
1092#[cfg(test)]
1093mod launch_nonce_redaction_tests {
1094 use super::*;
1095
1096 #[test]
1097 fn hello_body_debug_never_prints_the_nonce() {
1098 let body = ModuleHelloBody {
1099 manifest: manifest::ModuleManifest::builder("broca", "0.1.0").build(),
1100 protocol_ver: 2,
1101 control_ops: None,
1102 launch_nonce: Some("nonce-f00dfeed1234abcd".to_string()),
1103 };
1104 let printed = format!("{body:?}");
1105 assert!(printed.contains("broca"), "{printed}");
1106 assert!(
1107 !printed.contains("nonce-f00dfeed1234abcd"),
1108 "launch nonce printed: {printed}"
1109 );
1110 }
1111}