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(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
321impl fmt::Debug for ModuleHelloBody {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 f.debug_struct("ModuleHelloBody")
328 .field("manifest", &self.manifest)
329 .field("protocol_ver", &self.protocol_ver)
330 .field("control_ops", &self.control_ops)
331 .field(
332 "launch_nonce",
333 &self
334 .launch_nonce
335 .as_ref()
336 .map(|nonce| format!("<{} bytes redacted>", nonce.len())),
337 )
338 .finish()
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
344pub struct ModuleHelloAckBody {
345 pub negotiated_ver: u8,
346 pub subc_ops: Vec<String>,
347 pub subc_capabilities: Vec<String>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub storage: Option<serde_json::Value>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub machine_id: Option<String>,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374#[repr(u8)]
375pub enum FrameType {
376 Request = 0,
377 Response = 1,
378 Push = 2,
379 StreamData = 3,
380 StreamEnd = 4,
381 Error = 5,
382 Cancel = 6,
383 Ping = 7,
384 Pong = 8,
385 Hello = 9,
386 HelloAck = 10,
387 Goodbye = 11,
388}
389
390impl FrameType {
391 pub fn from_u8(b: u8) -> Option<Self> {
393 Some(match b {
394 0 => Self::Request,
395 1 => Self::Response,
396 2 => Self::Push,
397 3 => Self::StreamData,
398 4 => Self::StreamEnd,
399 5 => Self::Error,
400 6 => Self::Cancel,
401 7 => Self::Ping,
402 8 => Self::Pong,
403 9 => Self::Hello,
404 10 => Self::HelloAck,
405 11 => Self::Goodbye,
406 _ => return None,
407 })
408 }
409
410 pub fn is_pure_header(self) -> bool {
411 matches!(self, Self::Cancel | Self::Ping | Self::Pong | Self::Goodbye)
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418#[repr(u8)]
419pub enum Priority {
420 Passive = 0,
421 Interactive = 1,
422 Background = 2,
423}
424
425impl Priority {
426 fn from_bits(bits: u8) -> Option<Self> {
427 Some(match bits {
428 0 => Self::Passive,
429 1 => Self::Interactive,
430 2 => Self::Background,
431 _ => return None,
432 })
433 }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
438#[repr(u8)]
439pub enum AdmissionClass {
440 Normal = 0,
441 Expedite = 1,
442 Sheddable = 2,
443}
444
445impl AdmissionClass {
446 fn from_bits(bits: u8) -> Option<Self> {
447 Some(match bits {
448 0 => Self::Normal,
449 1 => Self::Expedite,
450 2 => Self::Sheddable,
451 _ => return None,
452 })
453 }
454}
455
456const FLAG_BINARY: u8 = 0b0000_0001; const FLAG_PRIORITY_MASK: u8 = 0b0000_0110; const FLAG_PRIORITY_SHIFT: u8 = 1;
459const FLAG_LAST: u8 = 0b0000_1000; const FLAG_ADMISSION_MASK: u8 = 0b0011_0000; const FLAG_ADMISSION_SHIFT: u8 = 4;
462pub const FLAG_DAEMON_ORIGIN: u8 = 0b0100_0000;
463pub const FLAG_SUBSCRIPTION: u8 = 0b1000_0000;
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub struct Flags(pub u8);
469
470impl Flags {
471 pub fn new(binary: bool, priority: Priority, last: bool) -> Self {
473 let mut b = 0u8;
474 if binary {
475 b |= FLAG_BINARY;
476 }
477 b |= (priority as u8) << FLAG_PRIORITY_SHIFT;
478 if last {
479 b |= FLAG_LAST;
480 }
481 Flags(b)
482 }
483
484 pub fn with_admission_class(mut self, admission_class: AdmissionClass) -> Self {
486 self.0 =
487 (self.0 & !FLAG_ADMISSION_MASK) | ((admission_class as u8) << FLAG_ADMISSION_SHIFT);
488 self
489 }
490
491 pub fn is_binary(self) -> bool {
493 self.0 & FLAG_BINARY != 0
494 }
495
496 pub fn is_last(self) -> bool {
498 self.0 & FLAG_LAST != 0
499 }
500
501 pub fn priority(self) -> Option<Priority> {
503 Priority::from_bits((self.0 & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT)
504 }
505
506 pub fn admission_class(self) -> Option<AdmissionClass> {
508 AdmissionClass::from_bits((self.0 & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT)
509 }
510
511 pub fn is_subscription(self) -> bool {
513 self.0 & FLAG_SUBSCRIPTION != 0
514 }
515
516 pub fn is_daemon_origin(self) -> bool {
518 self.0 & FLAG_DAEMON_ORIGIN != 0
519 }
520
521 pub fn with_daemon_origin(mut self) -> Self {
523 self.0 |= FLAG_DAEMON_ORIGIN;
524 self
525 }
526
527 pub fn without_daemon_origin(self) -> Self {
529 Self(self.0 & !FLAG_DAEMON_ORIGIN)
530 }
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535pub struct EnvelopeHeader {
536 pub len: u32,
538 pub ver: u8,
540 pub ty: FrameType,
542 pub flags: Flags,
544 pub channel: u16,
546 pub epoch: u32,
548 pub corr: u64,
550}
551
552impl EnvelopeHeader {
553 pub fn encode(&self) -> [u8; HEADER_LEN] {
555 let mut buf = [0u8; HEADER_LEN];
556 buf[0..4].copy_from_slice(&self.len.to_le_bytes());
557 buf[4] = self.ver;
558 buf[5] = self.ty as u8;
559 buf[6] = self.flags.0;
560 buf[7..9].copy_from_slice(&self.channel.to_le_bytes());
561 buf[9..13].copy_from_slice(&self.epoch.to_le_bytes());
562 buf[13..21].copy_from_slice(&self.corr.to_le_bytes());
563 buf
564 }
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569pub enum DecodeError {
570 TooShortForPrefix { have: usize },
572 UnsupportedVersion { ver: u8 },
574 TooShortForHeader { have: usize, need: usize },
576 UnknownFrameType { byte: u8 },
578 ReservedFlagBits { flags: u8 },
580 ReservedPriorityBits { flags: u8 },
582 ReservedAdmissionClass { flags: u8 },
584 SheddableIllegalFrameType { ty: FrameType, flags: u8 },
586 NonzeroEpochOnControlChannel { epoch: u32 },
588 PureHeaderFrameWithBody { ty: FrameType, len: u32 },
590}
591
592impl fmt::Display for DecodeError {
593 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594 match self {
595 Self::TooShortForPrefix { have } => {
596 write!(f, "header shorter than frozen prefix: have {have} bytes")
597 }
598 Self::UnsupportedVersion { ver } => write!(f, "unsupported envelope version {ver}"),
599 Self::TooShortForHeader { have, need } => {
600 write!(
601 f,
602 "header too short for version: have {have} bytes, need {need}"
603 )
604 }
605 Self::UnknownFrameType { byte } => write!(f, "unknown frame type byte {byte}"),
606 Self::ReservedFlagBits { flags } => {
607 write!(f, "reserved flag bits set in flags 0b{flags:08b}")
608 }
609 Self::ReservedPriorityBits { flags } => {
610 write!(f, "reserved priority bits set in flags 0b{flags:08b}")
611 }
612 Self::ReservedAdmissionClass { flags } => {
613 write!(f, "reserved admission class set in flags 0b{flags:08b}")
614 }
615 Self::SheddableIllegalFrameType { ty, flags } => write!(
616 f,
617 "SHEDDABLE admission class is illegal on {ty:?} in flags 0b{flags:08b}"
618 ),
619 Self::NonzeroEpochOnControlChannel { epoch } => {
620 write!(f, "control channel carried nonzero epoch {epoch}")
621 }
622 Self::PureHeaderFrameWithBody { ty, len } => {
623 write!(
624 f,
625 "pure-header frame {ty:?} declared non-zero body length {len}"
626 )
627 }
628 }
629 }
630}
631
632impl Error for DecodeError {}
633
634fn header_len_for_version(ver: u8) -> Option<usize> {
637 match ver {
638 PROTOCOL_VERSION => Some(HEADER_LEN),
639 _ => None,
640 }
641}
642
643pub fn decode_header(bytes: &[u8]) -> Result<EnvelopeHeader, DecodeError> {
651 if bytes.len() < FROZEN_PREFIX_LEN {
652 return Err(DecodeError::TooShortForPrefix { have: bytes.len() });
653 }
654 let ver = bytes[4];
655 let need = header_len_for_version(ver).ok_or(DecodeError::UnsupportedVersion { ver })?;
656 if bytes.len() < need {
657 return Err(DecodeError::TooShortForHeader {
658 have: bytes.len(),
659 need,
660 });
661 }
662
663 let len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
664 let ty =
665 FrameType::from_u8(bytes[5]).ok_or(DecodeError::UnknownFrameType { byte: bytes[5] })?;
666 let flags = Flags(bytes[6]);
667 if flags.priority().is_none() {
668 return Err(DecodeError::ReservedPriorityBits { flags: bytes[6] });
669 }
670 let admission_class = flags
671 .admission_class()
672 .ok_or(DecodeError::ReservedAdmissionClass { flags: bytes[6] })?;
673 if admission_class == AdmissionClass::Sheddable
674 && !matches!(ty, FrameType::Push | FrameType::StreamData)
675 {
676 return Err(DecodeError::SheddableIllegalFrameType {
677 ty,
678 flags: bytes[6],
679 });
680 }
681 if ty.is_pure_header() && len != 0 {
682 return Err(DecodeError::PureHeaderFrameWithBody { ty, len });
683 }
684 let channel = u16::from_le_bytes([bytes[7], bytes[8]]);
685 let epoch = u32::from_le_bytes([bytes[9], bytes[10], bytes[11], bytes[12]]);
686 if channel == 0 && epoch != 0 {
687 return Err(DecodeError::NonzeroEpochOnControlChannel { epoch });
688 }
689 let corr = u64::from_le_bytes([
690 bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19], bytes[20],
691 ]);
692
693 Ok(EnvelopeHeader {
694 len,
695 ver,
696 ty,
697 flags,
698 channel,
699 epoch,
700 corr,
701 })
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 fn hdr(len: u32, ty: FrameType, flags: Flags, channel: u16, corr: u64) -> EnvelopeHeader {
709 hdr_with_epoch(len, ty, flags, channel, u32::from(channel != 0), corr)
710 }
711
712 fn hdr_with_epoch(
713 len: u32,
714 ty: FrameType,
715 flags: Flags,
716 channel: u16,
717 epoch: u32,
718 corr: u64,
719 ) -> EnvelopeHeader {
720 EnvelopeHeader {
721 len,
722 ver: PROTOCOL_VERSION,
723 ty,
724 flags,
725 channel,
726 epoch,
727 corr,
728 }
729 }
730
731 #[test]
732 fn bind_identity_with_project_id_round_trips_json() {
733 let mut identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
734 identity.project_id = Some("pj-a1b2c3d4".to_string());
735
736 let encoded = serde_json::to_vec(&identity).unwrap();
737 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
738
739 assert_eq!(decoded, identity);
740 }
741
742 #[test]
743 fn bind_identity_without_project_id_round_trips_json() {
744 let identity = BindIdentity::new("/tmp/project", "opencode", "session-1");
745
746 let encoded = serde_json::to_vec(&identity).unwrap();
747 let decoded: BindIdentity = serde_json::from_slice(&encoded).unwrap();
748
749 assert_eq!(decoded, identity);
750 }
751
752 #[test]
753 fn legacy_bind_identity_without_project_id_decodes() {
754 let decoded: BindIdentity = serde_json::from_value(serde_json::json!({
755 "project_root": "/tmp/project",
756 "harness": "opencode",
757 "session": "session-1"
758 }))
759 .unwrap();
760
761 assert_eq!(decoded.project_id, None);
762 }
763
764 #[test]
765 fn bind_identity_none_omits_project_id_instead_of_serializing_null() {
766 let encoded =
767 serde_json::to_value(BindIdentity::new("/tmp/project", "opencode", "session-1"))
768 .unwrap();
769
770 assert!(encoded.get("project_id").is_none());
771 }
772
773 #[test]
774 fn wire_crate_version_is_a_numeric_three_component_version() {
775 let components = SUBC_PROTOCOL_CRATE_VERSION.split('.').collect::<Vec<_>>();
776
777 assert!(!SUBC_PROTOCOL_CRATE_VERSION.is_empty());
778 assert_eq!(components.len(), 3);
779 assert!(components
780 .iter()
781 .all(|component| !component.is_empty() && component.parse::<u64>().is_ok()));
782 }
783
784 #[test]
785 fn route_target_variants_round_trip_json() {
786 let targets = [
787 RouteTarget::ToolProvider {
788 module_id: "aft".to_string(),
789 },
790 RouteTarget::ManagementSurface {
791 module_id: "memory".to_string(),
792 },
793 RouteTarget::InternalService {
794 module_id: "bus".to_string(),
795 service_id: "dm".to_string(),
796 },
797 ];
798
799 for target in targets {
800 let encoded = serde_json::to_vec(&target).unwrap();
801 let decoded: RouteTarget = serde_json::from_slice(&encoded).unwrap();
802 assert_eq!(decoded, target);
803 }
804 }
805
806 #[test]
807 fn error_body_round_trips_json() {
808 let body = ErrorBody {
809 code: "config_divergence".to_string(),
810 message: "active config differs".to_string(),
811 detail: None,
812 };
813
814 let encoded = serde_json::to_vec(&body).unwrap();
815 let decoded: ErrorBody = serde_json::from_slice(&encoded).unwrap();
816
817 assert_eq!(decoded, body);
818 }
819
820 #[test]
821 fn round_trip_request() {
822 let h = hdr(
823 1234,
824 FrameType::Request,
825 Flags::new(false, Priority::Interactive, false),
826 42,
827 0xDEAD_BEEF_0000_0001,
828 );
829 let decoded = decode_header(&h.encode()).unwrap();
830 assert_eq!(h, decoded);
831 }
832
833 #[test]
834 fn round_trip_all_frame_types() {
835 for b in 0u8..=11 {
836 let ty = FrameType::from_u8(b).unwrap();
837 let h = hdr(0, ty, Flags::new(false, Priority::Passive, false), 0, 0);
838 assert_eq!(decode_header(&h.encode()).unwrap().ty, ty);
839 }
840 }
841
842 #[test]
843 fn pure_header_frame_has_zero_len() {
844 let h = hdr(
846 0,
847 FrameType::Cancel,
848 Flags::new(false, Priority::Passive, false),
849 7,
850 99,
851 );
852 let d = decode_header(&h.encode()).unwrap();
853 assert_eq!(d.len, 0);
854 assert_eq!(d.corr, 99);
855 }
856
857 #[test]
858 fn flags_round_trip() {
859 let f = Flags::new(true, Priority::Background, true)
860 .with_admission_class(AdmissionClass::Expedite);
861 assert!(f.is_binary());
862 assert!(f.is_last());
863 assert_eq!(f.priority(), Some(Priority::Background));
864 assert_eq!(f.admission_class(), Some(AdmissionClass::Expedite));
865 let h = hdr(8, FrameType::StreamData, f, 1, 1);
866 assert_eq!(decode_header(&h.encode()).unwrap().flags, f);
867 }
868
869 #[test]
870 fn daemon_origin_flags_decode_and_round_trip() {
871 let old = hdr(0, FrameType::Error, Flags(0), 7, 1);
872 let old_decoded = decode_header(&old.encode()).unwrap();
873 assert!(!old_decoded.flags.is_daemon_origin());
874
875 let daemon = hdr(0, FrameType::Error, Flags(0).with_daemon_origin(), 7, 1);
876 let daemon_decoded = decode_header(&daemon.encode()).unwrap();
877 assert!(daemon_decoded.flags.is_daemon_origin());
878 assert_eq!(daemon_decoded.flags.without_daemon_origin(), Flags(0));
879 assert!(Flags(0).with_daemon_origin().is_daemon_origin());
880 }
881
882 #[test]
883 fn little_endian_and_frozen_prefix_layout() {
884 let h = hdr_with_epoch(
885 0x0403_0201,
886 FrameType::Request,
887 Flags(0),
888 0x0605,
889 0x0a09_0807,
890 0x1211_100f_0e0d_0c0b,
891 );
892 let buf = h.encode();
893 assert_eq!(&buf[0..4], &[1, 2, 3, 4]);
894 assert_eq!(buf[4], PROTOCOL_VERSION);
895 assert_eq!(&buf[7..9], &[5, 6]);
896 assert_eq!(&buf[9..13], &[7, 8, 9, 10]);
897 assert_eq!(&buf[13..21], &[11, 12, 13, 14, 15, 16, 17, 18]);
898 assert_eq!(buf.len(), HEADER_LEN);
899 }
900
901 #[test]
902 fn reject_too_short_for_prefix() {
903 assert_eq!(
904 decode_header(&[0, 0, 0, 0]),
905 Err(DecodeError::TooShortForPrefix { have: 4 })
906 );
907 }
908
909 #[test]
910 fn reject_too_short_for_header() {
911 let mut b = [0u8; 10];
913 b[4] = PROTOCOL_VERSION;
914 assert_eq!(
915 decode_header(&b),
916 Err(DecodeError::TooShortForHeader {
917 have: 10,
918 need: HEADER_LEN
919 })
920 );
921 }
922
923 #[test]
924 fn reject_unsupported_version() {
925 let mut b = [0u8; HEADER_LEN];
926 b[4] = 1;
927 assert_eq!(
928 decode_header(&b),
929 Err(DecodeError::UnsupportedVersion { ver: 1 })
930 );
931 }
932
933 #[test]
934 fn reject_unknown_frame_type() {
935 let mut b = [0u8; HEADER_LEN];
936 b[4] = PROTOCOL_VERSION;
937 b[5] = 99;
938 assert_eq!(
939 decode_header(&b),
940 Err(DecodeError::UnknownFrameType { byte: 99 })
941 );
942 }
943
944 #[test]
945 fn subscription_flag_decodes_and_tags_the_request() {
946 let mut b = [0u8; HEADER_LEN];
947 b[4] = PROTOCOL_VERSION;
948 b[5] = FrameType::Request as u8;
949 b[6] = FLAG_SUBSCRIPTION;
950 let decoded = decode_header(&b).unwrap();
951 assert!(decoded.flags.is_subscription());
952 }
953
954 #[test]
955 fn reject_reserved_priority_bits() {
956 let mut b = [0u8; HEADER_LEN];
957 b[4] = PROTOCOL_VERSION;
958 b[5] = FrameType::Request as u8;
959 b[6] = 0b0000_0110; assert_eq!(
961 decode_header(&b),
962 Err(DecodeError::ReservedPriorityBits { flags: 0b0000_0110 })
963 );
964 }
965
966 #[test]
967 fn reject_pure_header_frame_with_body_len() {
968 let h = hdr(
969 1,
970 FrameType::Ping,
971 Flags::new(false, Priority::Passive, false),
972 0,
973 1,
974 );
975 assert_eq!(
976 decode_header(&h.encode()),
977 Err(DecodeError::PureHeaderFrameWithBody {
978 ty: FrameType::Ping,
979 len: 1
980 })
981 );
982 }
983
984 #[test]
985 fn epoch_boundaries_round_trip() {
986 for (channel, epoch) in [(0, 0), (1, 1), (u16::MAX, u32::MAX)] {
987 let h = hdr_with_epoch(
988 0,
989 FrameType::Request,
990 Flags::new(false, Priority::Passive, false),
991 channel,
992 epoch,
993 9,
994 );
995 assert_eq!(decode_header(&h.encode()).unwrap(), h);
996 }
997 }
998
999 #[test]
1000 fn admission_classes_accept_three_values_and_reject_reserved_value() {
1001 for (ty, admission_class) in [
1002 (FrameType::Request, AdmissionClass::Normal),
1003 (FrameType::Request, AdmissionClass::Expedite),
1004 (FrameType::Push, AdmissionClass::Sheddable),
1005 (FrameType::StreamData, AdmissionClass::Sheddable),
1006 ] {
1007 let flags = Flags::new(false, Priority::Interactive, false)
1008 .with_admission_class(admission_class);
1009 let h = hdr(0, ty, flags, 1, 2);
1010 assert_eq!(decode_header(&h.encode()).unwrap().flags, flags);
1011 }
1012
1013 let mut h = hdr(
1014 0,
1015 FrameType::Push,
1016 Flags::new(false, Priority::Passive, false),
1017 1,
1018 2,
1019 )
1020 .encode();
1021 h[6] |= 0b0011_0000;
1022 assert_eq!(
1023 decode_header(&h),
1024 Err(DecodeError::ReservedAdmissionClass { flags: h[6] })
1025 );
1026 }
1027
1028 #[test]
1029 fn sheddable_rejected_on_every_illegal_frame_type() {
1030 let flags = Flags::new(false, Priority::Passive, false)
1031 .with_admission_class(AdmissionClass::Sheddable);
1032 for ty in [
1033 FrameType::Request,
1034 FrameType::Response,
1035 FrameType::StreamEnd,
1036 FrameType::Error,
1037 FrameType::Cancel,
1038 FrameType::Ping,
1039 FrameType::Pong,
1040 FrameType::Hello,
1041 FrameType::HelloAck,
1042 FrameType::Goodbye,
1043 ] {
1044 let h = hdr(0, ty, flags, 1, 2);
1045 assert_eq!(
1046 decode_header(&h.encode()),
1047 Err(DecodeError::SheddableIllegalFrameType { ty, flags: flags.0 })
1048 );
1049 }
1050 }
1051
1052 #[test]
1053 fn nonzero_epoch_on_control_channel_is_rejected() {
1054 let h = hdr_with_epoch(
1055 0,
1056 FrameType::Request,
1057 Flags::new(false, Priority::Passive, false),
1058 0,
1059 u32::MAX,
1060 2,
1061 );
1062 assert_eq!(
1063 decode_header(&h.encode()),
1064 Err(DecodeError::NonzeroEpochOnControlChannel { epoch: u32::MAX })
1065 );
1066 }
1067}
1068
1069#[cfg(test)]
1070mod launch_nonce_redaction_tests {
1071 use super::*;
1072
1073 #[test]
1074 fn hello_body_debug_never_prints_the_nonce() {
1075 let body = ModuleHelloBody {
1076 manifest: manifest::ModuleManifest::builder("broca", "0.1.0").build(),
1077 protocol_ver: 2,
1078 control_ops: None,
1079 launch_nonce: Some("nonce-f00dfeed1234abcd".to_string()),
1080 };
1081 let printed = format!("{body:?}");
1082 assert!(printed.contains("broca"), "{printed}");
1083 assert!(
1084 !printed.contains("nonce-f00dfeed1234abcd"),
1085 "launch nonce printed: {printed}"
1086 );
1087 }
1088}