Skip to main content

sccp_protocol/message/
catalog.rs

1//! SCCP/SPCP message identifiers and direction metadata for the message domain.
2//!
3//! The numeric values are protocol facts.  The catalog intentionally includes
4//! messages which this crate can only preserve opaquely today: knowing the ID
5//! is still useful for bounded forwarding and future typed implementations.
6//!
7//! Start with [`MessageId`] when inspecting an unknown frame. Its
8//! [`MessageId::contract`] links the numeric identifier to routing, payload
9//! bounds, codec coverage, response selection, and field fidelity. Use
10//! [`implemented_message_contracts`] to enumerate the typed subset.
11
12use std::fmt;
13
14use super::wire::{HEADER_SIZE, MAX_FRAME_SIZE};
15
16/// The protocol roles between which a message is normally sent.
17///
18/// SCCP is not solely a station/client protocol. Conference resources, media
19/// resource services, and call-control peers share the same numeric message
20/// space. Keeping those routes explicit prevents a decoder or runtime from
21/// treating a service-node frame as handset input merely because both travel
22/// toward call control.
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24pub enum MessageRoute {
25    StationToControl,
26    ControlToStation,
27    ControlToServiceNode,
28    ServiceNodeToControl,
29    IntraControl,
30}
31
32/// Legacy station-oriented view of the two handset message directions.
33///
34/// New code should use [`MessageRoute`]. A service-node or intra-control
35/// message deliberately has no `MessageDirection`.
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37pub enum MessageDirection {
38    DeviceToServer,
39    ServerToDevice,
40}
41
42/// How completely the public message model implements a catalog entry.
43#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44pub enum CodecSupport {
45    /// The message has a typed public representation and a checked codec.
46    Typed,
47    /// Only the identifier, direction, and opaque bytes are preserved.
48    OpaqueOnly,
49}
50
51/// The rule used to choose and bound a message payload layout.
52#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
53pub enum PayloadLayout {
54    /// No semantic payload bytes are carried.
55    Empty,
56    /// One fixed layout is used for all supported protocol versions.
57    Fixed,
58    /// The negotiated protocol selects between fixed layouts.
59    VersionSelected,
60    /// The negotiated protocol and exact body length jointly select a layout.
61    VersionAndLengthSelected,
62    /// A typed fixed prefix is decoded while a bounded extension is preserved.
63    MinimumLengthPreserved,
64    /// A bounded length/count field controls a variable tail.
65    LengthPrefixed,
66    /// A bounded payload is retained exactly while consumers may inspect it.
67    BoundedPreserved,
68    /// A bounded extension is retained byte-for-byte because its internal
69    /// schema is not modeled.
70    BoundedOpaque,
71    /// NUL-terminated station strings are followed by zero bytes to a
72    /// four-byte boundary.
73    DynamicWordPadded,
74    /// The crate deliberately does not interpret the payload.
75    Opaque,
76}
77
78/// Whether application code can construct a message without supplying raw
79/// wire bytes.
80#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
81pub enum EmissionSupport {
82    /// A typed encoder is available.
83    Typed,
84    /// Bytes can be forwarded explicitly through `KnownOpaque`, but there is
85    /// no typed constructor and runtime code must not synthesize the message.
86    PreserveOnly,
87}
88
89/// Present production/runtime role of a known message.
90#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
91pub enum RuntimeUse {
92    /// A typed phone-originated input accepted by the session runtime.
93    DeviceInput,
94    /// A server response required by a currently handled phone request.
95    RequiredResponse,
96    /// A server output emitted only for the corresponding configured feature
97    /// or call state.
98    ConditionalServerOutput,
99    /// A typed service-node input accepted by its independent runtime.
100    ServiceNodeInput,
101    /// A service-node output emitted only for an owned reservation transition.
102    ConditionalServiceNodeOutput,
103    /// The codec is typed for conformance/testing, but ordinary runtime flows
104    /// intentionally do not emit it.
105    TypedButNotEmitted,
106    /// Only catalog metadata and explicit opaque preservation are supported.
107    CatalogOnly,
108}
109
110/// Whether all semantic wire fields survive typed decoding and re-encoding.
111#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
112pub enum FieldFidelity {
113    /// Every accepted semantic field is represented; reserved/padding bytes
114    /// are validated rather than exposed.
115    Lossless,
116    /// A server-only producer omits or fills the named fields. Decoding may
117    /// project other values, so this is not an exact decode/re-encode guarantee.
118    CanonicalServerOutput(&'static str),
119    /// Typed decoding is intentionally projected onto the named runtime data.
120    SemanticProjection(&'static str),
121    /// The uninterpreted bounded body is retained exactly.
122    OpaquePreserved,
123}
124
125/// SCCP-level response expected for a request or media transaction.
126#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
127pub enum ResponseExpectation {
128    None,
129    Message(MessageId),
130    /// The negotiated protocol selects the response identifier.
131    VersionSelected {
132        /// Response used before `minimum_protocol`.
133        before: MessageId,
134        /// Response used at and after `minimum_protocol`.
135        from: MessageId,
136        /// First protocol version that selects `from`.
137        minimum_protocol: u8,
138    },
139    /// Negotiated session inputs select the response identifier.
140    SessionSelected {
141        /// Response used when `selector` does not select the dynamic form.
142        before: MessageId,
143        /// Dynamic response selected by `selector`.
144        from: MessageId,
145        /// Session rule that chooses between the response identifiers.
146        selector: SessionResponseSelector,
147    },
148    /// The response may be any member of this family.
149    OneOf(&'static [MessageId]),
150}
151
152/// Session rule used to select a dynamic response identifier.
153#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
154pub enum SessionResponseSelector {
155    /// Select the dynamic form when the feature is present or the negotiated
156    /// protocol meets the stated minimum.
157    DynamicMessagesOrProtocol { minimum_protocol: u8 },
158    /// Select the dynamic form only when the feature is present.
159    DynamicMessages,
160}
161
162/// Verification depth for a wire contract.
163#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
164pub enum ContractVerification {
165    Structural,
166    StructuralAndValidated,
167}
168
169/// Whether an identifier belongs to the base station-control inventory or an
170/// independently supported extension family.
171#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
172pub enum ContractScope {
173    Base,
174    Supplemental,
175}
176
177/// Inclusive payload-size bounds, excluding the 12-byte frame header.
178#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
179pub struct PayloadSizeBounds {
180    /// Smallest accepted payload in bytes.
181    pub minimum: usize,
182    /// Largest accepted payload in bytes.
183    pub maximum: usize,
184}
185
186/// Machine-readable support record for one known message identifier.
187///
188/// This is an implementation inventory, not a claim that every cataloged
189/// message is safe to send. `OpaqueOnly` entries exist for bounded forwarding
190/// and remain non-emittable through the typed API. `response`
191/// describes SCCP transaction acknowledgement; TCP acknowledgement is
192/// intentionally not treated as application-level acceptance.
193#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194pub struct MessageContract {
195    pub id: MessageId,
196    pub scope: ContractScope,
197    pub route: MessageRoute,
198    pub codec: CodecSupport,
199    pub payload_layout: PayloadLayout,
200    /// Canonical typed-encoder payload size when there is one stable,
201    /// independently useful value. This excludes the 12-byte frame header;
202    /// nominally empty decoders may still accept bounded extension bytes.
203    pub fixed_payload_bytes: Option<usize>,
204    /// Accepted payload-size range when both bounds are known.
205    pub payload_size_bounds: Option<PayloadSizeBounds>,
206    /// Typed construction versus explicit opaque preservation.
207    pub emission: EmissionSupport,
208    /// Production/runtime use, distinct from mere encoder availability.
209    pub runtime_use: RuntimeUse,
210    /// Whether the typed model retains every accepted semantic wire field.
211    pub field_fidelity: FieldFidelity,
212    /// SCCP response/acknowledgement family, when one exists.
213    pub response: ResponseExpectation,
214    /// Depth of contract validation performed by the codec.
215    pub verification: ContractVerification,
216}
217
218macro_rules! message_catalog {
219    ($(($variant:ident, $value:expr, $route:ident)),+ $(,)?) => {
220        /// A Skinny message identifier.
221        ///
222        /// Unknown values are retained to keep decoding forward-compatible.
223        #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
224        pub enum MessageId {
225            $($variant,)+
226            Unknown(u32),
227        }
228
229        impl MessageId {
230            pub const ALL_KNOWN: &'static [Self] = &[$(Self::$variant,)+];
231
232            pub const fn wire_value(self) -> u32 {
233                match self {
234                    $(Self::$variant => $value,)+
235                    Self::Unknown(value) => value,
236                }
237            }
238
239            /// Returns the protocol route for a known identifier.
240            ///
241            /// Unknown identifiers return `None` because direction cannot be
242            /// inferred from their numeric value alone.
243            pub const fn route(self) -> Option<MessageRoute> {
244                match self {
245                    $(Self::$variant => Some(MessageRoute::$route),)+
246                    Self::Unknown(_) => None,
247                }
248            }
249
250            /// Return the legacy two-ended station direction, if applicable.
251            pub const fn direction(self) -> Option<MessageDirection> {
252                match self.route() {
253                    Some(MessageRoute::StationToControl) => {
254                        Some(MessageDirection::DeviceToServer)
255                    }
256                    Some(MessageRoute::ControlToStation) => {
257                        Some(MessageDirection::ServerToDevice)
258                    }
259                    Some(MessageRoute::ControlToServiceNode)
260                    | Some(MessageRoute::ServiceNodeToControl)
261                    | Some(MessageRoute::IntraControl)
262                    | None => None,
263                }
264            }
265
266            pub const fn name(self) -> &'static str {
267                match self {
268                    $(Self::$variant => stringify!($variant),)+
269                    Self::Unknown(_) => "Unknown",
270                }
271            }
272
273            pub const fn is_known(self) -> bool {
274                !matches!(self, Self::Unknown(_))
275            }
276
277            /// Return the codec and wire contract for this identifier.
278            pub const fn contract(self) -> Option<MessageContract> {
279                let route = match self.route() {
280                    Some(route) => route,
281                    None => return None,
282                };
283                let codec = codec_support(self);
284                Some(MessageContract {
285                    id: self,
286                    scope: contract_scope(self),
287                    route,
288                    codec,
289                    payload_layout: payload_layout(self, codec),
290                    fixed_payload_bytes: fixed_payload_bytes(self),
291                    payload_size_bounds: payload_size_bounds(self),
292                    emission: match codec {
293                        CodecSupport::Typed => EmissionSupport::Typed,
294                        CodecSupport::OpaqueOnly => EmissionSupport::PreserveOnly,
295                    },
296                    runtime_use: runtime_use(self, route, codec),
297                    field_fidelity: field_fidelity(self),
298                    response: primary_response(self),
299                    verification: verification(self),
300                })
301            }
302        }
303
304        impl From<u32> for MessageId {
305            fn from(value: u32) -> Self {
306                match value {
307                    $($value => Self::$variant,)+
308                    value => Self::Unknown(value),
309                }
310            }
311        }
312    };
313}
314
315const fn contract_scope(id: MessageId) -> ContractScope {
316    use MessageId::*;
317    match id {
318        IpPort
319        | MediaPortList
320        | SetHookFlashDetect
321        | StartMediaReception
322        | StopMediaReception
323        | EnunciatorCommand
324        | ExtensionDeviceCapabilities
325        | SpcpRegisterTokenRequest
326        | SpcpRegisterTokenAck
327        | SpcpRegisterTokenReject => ContractScope::Supplemental,
328        _ => ContractScope::Base,
329    }
330}
331
332/// Exhaustive by design: adding an identifier must make the compiler require
333/// an explicit fidelity decision. In particular, there is no catch-all that
334/// silently upgrades a new typed codec to `Lossless`.
335const fn field_fidelity(id: MessageId) -> FieldFidelity {
336    use MessageId::*;
337    match id {
338        MediaPortList
339        | SetHookFlashDetect
340        | StartMediaReception
341        | StopMediaReception
342        | EnunciatorCommand
343        | SpcpRegisterTokenRequest
344        | SpcpRegisterTokenAck
345        | SpcpRegisterTokenReject
346        | Unknown(_) => FieldFidelity::OpaquePreserved,
347
348        KeepAlive
349        | ConfigStatusRequest
350        | TimeDateRequest
351        | VersionRequest
352        | ServerRequest
353        | SoftKeySetRequest
354        | SoftKeyTemplateRequest => FieldFidelity::SemanticProjection(
355            "nominally empty request; bounded extension bytes are accepted but not modeled",
356        ),
357        ButtonTemplateRequest => FieldFidelity::SemanticProjection(
358            "the optional total-button-count request word is accepted but not modeled",
359        ),
360        Register => FieldFidelity::Lossless,
361        CapabilitiesResponse => FieldFidelity::SemanticProjection(
362            "advertised capability count; inactive fixed-array entries are not modeled",
363        ),
364        RegisterTokenRequest => FieldFidelity::SemanticProjection(
365            "simultaneously populated IPv4 and IPv6 station addresses collapse to one address",
366        ),
367        Unregister => FieldFidelity::SemanticProjection(
368            "an empty reason-zero body is accepted and normalized to the typed reason",
369        ),
370        MediaTransmissionFailure => FieldFidelity::SemanticProjection(
371            "the public status is synthesized because the failure wire layouts carry no status",
372        ),
373        HeadsetStatus => FieldFidelity::SemanticProjection(
374            "non-canonical raw states are projected onto a boolean",
375        ),
376        RegisterAvailableLines => FieldFidelity::SemanticProjection(
377            "an absent or short legacy body is projected onto zero available lines",
378        ),
379        PortResponse => FieldFidelity::SemanticProjection(
380            "pre-v20 bodies omit media type, which is synthesized on decode",
381        ),
382        Alarm => FieldFidelity::Lossless,
383
384        RegisterAck | ConfigStatus | ConfigStatusDynamic => FieldFidelity::Lossless,
385        LineStatus | LineStatusDynamic => FieldFidelity::CanonicalServerOutput(
386            "display label/fully-qualified display name and display-options word",
387        ),
388        ServerResponse => {
389            FieldFidelity::SemanticProjection("empty server-list slot positions are not retained")
390        }
391        DefineTimeDate => FieldFidelity::Lossless,
392        CallState => FieldFidelity::CanonicalServerOutput("visibility, precedence and domain"),
393        CallInfo | CallInfoDynamic => FieldFidelity::CanonicalServerOutput(
394            "mailboxes, call instance/security and version-selected party metadata",
395        ),
396        ForwardStatus => FieldFidelity::CanonicalServerOutput(
397            "aggregate active flag and inactive forwarding-number slots",
398        ),
399        StopTone => FieldFidelity::CanonicalServerOutput("post-v11 tone word"),
400        ButtonTemplate | SoftKeyTemplateResponse | SoftKeySetResponse => {
401            FieldFidelity::CanonicalServerOutput(
402                "template offsets/count metadata and unused fixed-array entries",
403            )
404        }
405        ConnectionStatisticsRequest => {
406            FieldFidelity::CanonicalServerOutput("post-v18 directory-number alignment bytes")
407        }
408        ClearDisplay => FieldFidelity::CanonicalServerOutput("display-control word"),
409        KeepAliveAck | CapabilitiesRequest | ClearNotify | DeactivateCallPlane
410        | RegisterTokenAck | CallCountResponse => FieldFidelity::CanonicalServerOutput(
411            "nominally empty response; accepted extension bytes are not modeled",
412        ),
413        UnregisterAck => FieldFidelity::CanonicalServerOutput("acknowledgement body word"),
414        StartAnnouncement => FieldFidelity::CanonicalServerOutput(
415            "unused announcement and conference-party array entries",
416        ),
417
418        IpPort
419        | KeypadButton
420        | EnblocCall
421        | Stimulus
422        | OffHook
423        | OnHook
424        | HookFlash
425        | ForwardStatusRequest
426        | SpeedDialStatusRequest
427        | LineStatusRequest
428        | MulticastMediaReceptionAck
429        | OpenReceiveChannelAck
430        | ConnectionStatisticsResponse
431        | OffHookWithCallingParty
432        | SoftKeyEvent
433        | MediaResourceNotification
434        | DeviceToUserData
435        | DeviceToUserDataResponse
436        | UpdateCapabilities
437        | ClearConference
438        | ServiceUrlStatusRequest
439        | FeatureStatusRequest
440        | CreateConferenceResponse
441        | DeleteConferenceResponse
442        | ModifyConferenceResponse
443        | AddParticipantResponse
444        | AuditConferenceResponse
445        | AuditParticipantResponse
446        | DeviceToUserDataV1
447        | DeviceToUserDataResponseV1
448        | UpdateCapabilitiesV2
449        | UpdateCapabilitiesV3
450        | QosReservationNotify
451        | QosErrorNotify
452        | SubscriptionStatusRequest
453        | MediaPathEvent
454        | StartTone
455        | SetRinger
456        | SetLamp
457        | SetSpeakerMode
458        | SetMicrophoneMode
459        | StartMediaTransmission
460        | CloseReceiveChannel
461        | StopMediaTransmission
462        | SpeedDialStatus
463        | Version
464        | DisplayText
465        | RegisterReject
466        | Reset
467        | StartMulticastMediaReception
468        | StartMulticastMediaTransmission
469        | StopMulticastMediaReception
470        | StopMulticastMediaTransmission
471        | OpenReceiveChannel
472        | SelectSoftKeys
473        | DisplayPromptStatus
474        | ClearPromptStatus
475        | DisplayNotify
476        | ActivateCallPlane
477        | BackspaceResponse
478        | RegisterTokenReject
479        | DialedNumber
480        | UserToDeviceData
481        | FeatureStatus
482        | DisplayPriorityNotify
483        | ClearPriorityNotify
484        | StopAnnouncement
485        | AnnouncementFinish
486        | SubscribeDtmfPayloadRequest
487        | SubscribeDtmfPayloadResponse
488        | SubscribeDtmfPayloadError
489        | UnsubscribeDtmfPayloadRequest
490        | UnsubscribeDtmfPayloadResponse
491        | UnsubscribeDtmfPayloadError
492        | ServiceUrlStatus
493        | CallSelectStatus
494        | CreateConferenceRequest
495        | DeleteConferenceRequest
496        | ModifyConferenceRequest
497        | AddParticipantRequest
498        | DropParticipantRequest
499        | AuditConferenceRequest
500        | AuditParticipantRequest
501        | ChangeParticipantRequest
502        | UserToDeviceDataV1
503        | DisplayDynamicNotify
504        | DisplayDynamicPriorityNotify
505        | DisplayDynamicPromptStatus
506        | FeatureStatusDynamic
507        | ServiceUrlStatusDynamic
508        | SpeedDialStatusDynamic
509        | PortRequest
510        | PortClose
511        | QosListen
512        | QosPath
513        | QosTeardown
514        | UpdateDscp
515        | QosModify
516        | SubscriptionStatus
517        | Notification
518        | StartMediaTransmissionAck
519        | CallHistoryDisposition
520        | LocationInfo
521        | XmlAlarm
522        | CallCountRequest
523        | RecordingStatus
524        | MediaPathCapability => FieldFidelity::Lossless,
525        StartSessionTransmission
526        | StopSessionTransmission
527        | OpenMultimediaChannel
528        | StartMultimediaTransmission
529        | MiscellaneousCommand => FieldFidelity::Lossless,
530        StartMediaFailureDetection => FieldFidelity::Lossless,
531        MwiNotification
532        | MwiResponse
533        | OpenMultimediaReceiveChannelAck
534        | StartMultimediaTransmissionAck
535        | ExtensionDeviceCapabilities
536        | NotifyDtmfTone
537        | SendDtmfTone
538        | StopMultimediaTransmission
539        | FlowControlCommand
540        | CloseMultimediaReceiveChannel
541        | VideoDisplayCommand
542        | FlowControlNotify => FieldFidelity::Lossless,
543    }
544}
545
546const fn runtime_use(id: MessageId, route: MessageRoute, support: CodecSupport) -> RuntimeUse {
547    use MessageId::*;
548    if matches!(support, CodecSupport::OpaqueOnly) {
549        return RuntimeUse::CatalogOnly;
550    }
551    if matches!(route, MessageRoute::StationToControl) {
552        return RuntimeUse::DeviceInput;
553    }
554    if matches!(route, MessageRoute::ServiceNodeToControl) {
555        return match id {
556            QosReservationNotify | QosErrorNotify => RuntimeUse::ServiceNodeInput,
557            _ => RuntimeUse::TypedButNotEmitted,
558        };
559    }
560    if matches!(route, MessageRoute::ControlToServiceNode) {
561        return match id {
562            QosListen | QosPath | QosTeardown | UpdateDscp | QosModify => {
563                RuntimeUse::ConditionalServiceNodeOutput
564            }
565            _ => RuntimeUse::TypedButNotEmitted,
566        };
567    }
568    if !matches!(route, MessageRoute::ControlToStation) {
569        return RuntimeUse::TypedButNotEmitted;
570    }
571    match id {
572        RegisterAck
573        | RegisterReject
574        | KeepAliveAck
575        | UnregisterAck
576        | CapabilitiesRequest
577        | ConfigStatus
578        | LineStatus
579        | LineStatusDynamic
580        | ButtonTemplate
581        | Version
582        | ServerResponse
583        | DefineTimeDate
584        | SoftKeyTemplateResponse
585        | SoftKeySetResponse
586        | RegisterTokenAck
587        | RegisterTokenReject
588        | FeatureStatus
589        | FeatureStatusDynamic
590        | ServiceUrlStatus
591        | ServiceUrlStatusDynamic
592        | CallCountResponse => RuntimeUse::RequiredResponse,
593
594        StartMulticastMediaReception
595        | StartMulticastMediaTransmission
596        | StopMulticastMediaReception
597        | StopMulticastMediaTransmission
598        | StartSessionTransmission
599        | StopSessionTransmission
600        | ClearConference
601        | DisplayNotify
602        | DisplayDynamicNotify
603        | ClearNotify
604        | DeactivateCallPlane
605        | UserToDeviceData
606        | SubscribeDtmfPayloadRequest
607        | SubscribeDtmfPayloadError
608        | UnsubscribeDtmfPayloadRequest
609        | UnsubscribeDtmfPayloadError
610        | CreateConferenceRequest
611        | DeleteConferenceRequest
612        | ModifyConferenceRequest
613        | AddParticipantRequest
614        | DropParticipantRequest
615        | AuditConferenceRequest
616        | AuditParticipantRequest
617        | ChangeParticipantRequest => RuntimeUse::TypedButNotEmitted,
618
619        _ => RuntimeUse::ConditionalServerOutput,
620    }
621}
622
623const fn codec_support(id: MessageId) -> CodecSupport {
624    use MessageId::*;
625    match id {
626        MediaPortList
627        | SetHookFlashDetect
628        | StartMediaReception
629        | StopMediaReception
630        | EnunciatorCommand
631        | SpcpRegisterTokenRequest
632        | SpcpRegisterTokenAck
633        | SpcpRegisterTokenReject => CodecSupport::OpaqueOnly,
634        KeepAlive
635        | Register
636        | IpPort
637        | KeypadButton
638        | EnblocCall
639        | Stimulus
640        | OffHook
641        | OnHook
642        | HookFlash
643        | ForwardStatusRequest
644        | SpeedDialStatusRequest
645        | LineStatusRequest
646        | ConfigStatusRequest
647        | TimeDateRequest
648        | ButtonTemplateRequest
649        | VersionRequest
650        | CapabilitiesResponse
651        | ServerRequest
652        | Alarm
653        | MulticastMediaReceptionAck
654        | OpenReceiveChannelAck
655        | ConnectionStatisticsResponse
656        | OffHookWithCallingParty
657        | SoftKeySetRequest
658        | SoftKeyEvent
659        | Unregister
660        | SoftKeyTemplateRequest
661        | RegisterTokenRequest
662        | MediaTransmissionFailure
663        | HeadsetStatus
664        | MediaResourceNotification
665        | RegisterAvailableLines
666        | DeviceToUserData
667        | DeviceToUserDataResponse
668        | UpdateCapabilities
669        | ClearConference
670        | ServiceUrlStatusRequest
671        | FeatureStatusRequest
672        | CreateConferenceResponse
673        | DeleteConferenceResponse
674        | ModifyConferenceResponse
675        | AddParticipantResponse
676        | AuditConferenceResponse
677        | AuditParticipantResponse
678        | DeviceToUserDataV1
679        | DeviceToUserDataResponseV1
680        | UpdateCapabilitiesV2
681        | UpdateCapabilitiesV3
682        | PortResponse
683        | QosReservationNotify
684        | QosErrorNotify
685        | SubscriptionStatusRequest
686        | MediaPathEvent
687        | StartMediaFailureDetection
688        | RegisterAck
689        | StartTone
690        | StopTone
691        | SetRinger
692        | SetLamp
693        | SetSpeakerMode
694        | SetMicrophoneMode
695        | StartMediaTransmission
696        | StopMediaTransmission
697        | CallInfo
698        | ForwardStatus
699        | SpeedDialStatus
700        | LineStatus
701        | ConfigStatus
702        | DefineTimeDate
703        | ButtonTemplate
704        | Version
705        | DisplayText
706        | ClearDisplay
707        | CapabilitiesRequest
708        | RegisterReject
709        | ServerResponse
710        | Reset
711        | KeepAliveAck
712        | StartMulticastMediaReception
713        | StartMulticastMediaTransmission
714        | StopMulticastMediaReception
715        | StopMulticastMediaTransmission
716        | OpenReceiveChannel
717        | CloseReceiveChannel
718        | ConnectionStatisticsRequest
719        | SoftKeyTemplateResponse
720        | SoftKeySetResponse
721        | SelectSoftKeys
722        | CallState
723        | DisplayPromptStatus
724        | ClearPromptStatus
725        | DisplayNotify
726        | ClearNotify
727        | ActivateCallPlane
728        | DeactivateCallPlane
729        | UnregisterAck
730        | BackspaceResponse
731        | RegisterTokenAck
732        | RegisterTokenReject
733        | DialedNumber
734        | UserToDeviceData
735        | FeatureStatus
736        | DisplayPriorityNotify
737        | ClearPriorityNotify
738        | StartAnnouncement
739        | StopAnnouncement
740        | AnnouncementFinish
741        | SubscribeDtmfPayloadRequest
742        | SubscribeDtmfPayloadResponse
743        | SubscribeDtmfPayloadError
744        | UnsubscribeDtmfPayloadRequest
745        | UnsubscribeDtmfPayloadResponse
746        | UnsubscribeDtmfPayloadError
747        | ServiceUrlStatus
748        | CallSelectStatus
749        | CreateConferenceRequest
750        | DeleteConferenceRequest
751        | ModifyConferenceRequest
752        | AddParticipantRequest
753        | DropParticipantRequest
754        | AuditConferenceRequest
755        | AuditParticipantRequest
756        | ChangeParticipantRequest
757        | UserToDeviceDataV1
758        | DisplayDynamicNotify
759        | DisplayDynamicPriorityNotify
760        | DisplayDynamicPromptStatus
761        | FeatureStatusDynamic
762        | LineStatusDynamic
763        | ServiceUrlStatusDynamic
764        | SpeedDialStatusDynamic
765        | CallInfoDynamic
766        | PortRequest
767        | PortClose
768        | QosListen
769        | QosPath
770        | QosTeardown
771        | UpdateDscp
772        | QosModify
773        | SubscriptionStatus
774        | Notification
775        | StartMediaTransmissionAck
776        | CallHistoryDisposition
777        | LocationInfo
778        | XmlAlarm
779        | CallCountRequest
780        | CallCountResponse
781        | RecordingStatus
782        | MediaPathCapability => CodecSupport::Typed,
783        StartSessionTransmission
784        | StopSessionTransmission
785        | OpenMultimediaChannel
786        | StartMultimediaTransmission
787        | MiscellaneousCommand => CodecSupport::Typed,
788        MwiNotification
789        | MwiResponse
790        | OpenMultimediaReceiveChannelAck
791        | StartMultimediaTransmissionAck
792        | ExtensionDeviceCapabilities
793        | NotifyDtmfTone
794        | SendDtmfTone
795        | StopMultimediaTransmission
796        | FlowControlCommand
797        | CloseMultimediaReceiveChannel
798        | VideoDisplayCommand
799        | FlowControlNotify => CodecSupport::Typed,
800        ConfigStatusDynamic => CodecSupport::Typed,
801        Unknown(_) => CodecSupport::OpaqueOnly,
802    }
803}
804
805const fn payload_layout(id: MessageId, support: CodecSupport) -> PayloadLayout {
806    use MessageId::*;
807    if matches!(support, CodecSupport::OpaqueOnly) {
808        return PayloadLayout::Opaque;
809    }
810    match id {
811        KeepAlive
812        | ConfigStatusRequest
813        | TimeDateRequest
814        | ButtonTemplateRequest
815        | VersionRequest
816        | ServerRequest
817        | SoftKeySetRequest
818        | SoftKeyTemplateRequest
819        | KeepAliveAck
820        | CapabilitiesRequest
821        | ClearDisplay
822        | ClearNotify
823        | DeactivateCallPlane
824        | RegisterTokenAck
825        | CallCountResponse => PayloadLayout::Empty,
826
827        UpdateCapabilities | KeypadButton | EnblocCall => PayloadLayout::VersionAndLengthSelected,
828
829        Register | UpdateCapabilitiesV3 | AddParticipantResponse => {
830            PayloadLayout::MinimumLengthPreserved
831        }
832
833        CapabilitiesResponse => PayloadLayout::LengthPrefixed,
834
835        UpdateCapabilitiesV2 => PayloadLayout::Fixed,
836
837        XmlAlarm => PayloadLayout::BoundedPreserved,
838
839        OpenReceiveChannelAck
840        | ConnectionStatisticsResponse
841        | MediaTransmissionFailure
842        | PortResponse
843        | ServerResponse
844        | ForwardStatus
845        | DialedNumber
846        | OpenReceiveChannel
847        | ConnectionStatisticsRequest
848        | StartMediaTransmission
849        | StartMulticastMediaReception
850        | StartMulticastMediaTransmission
851        | OpenMultimediaReceiveChannelAck
852        | StartMultimediaTransmissionAck
853        | StartSessionTransmission
854        | StopSessionTransmission
855        | OpenMultimediaChannel
856        | StartMultimediaTransmission
857        | PortRequest
858        | PortClose => PayloadLayout::VersionSelected,
859
860        StartMediaTransmissionAck => PayloadLayout::VersionAndLengthSelected,
861
862        DeviceToUserData
863        | DeviceToUserDataResponse
864        | DeviceToUserDataV1
865        | DeviceToUserDataResponseV1
866        | CreateConferenceResponse
867        | ModifyConferenceResponse
868        | AuditConferenceResponse
869        | UserToDeviceData
870        | UserToDeviceDataV1
871        | CreateConferenceRequest
872        | ModifyConferenceRequest => PayloadLayout::LengthPrefixed,
873
874        AuditParticipantResponse => PayloadLayout::BoundedOpaque,
875
876        DisplayDynamicNotify
877        | DisplayDynamicPriorityNotify
878        | DisplayDynamicPromptStatus
879        | ConfigStatusDynamic
880        | LineStatusDynamic
881        | ServiceUrlStatusDynamic
882        | CallInfoDynamic => PayloadLayout::DynamicWordPadded,
883
884        _ => PayloadLayout::Fixed,
885    }
886}
887
888const fn fixed_payload_bytes(id: MessageId) -> Option<usize> {
889    use MessageId::*;
890    match id {
891        KeepAlive
892        | ConfigStatusRequest
893        | TimeDateRequest
894        | ButtonTemplateRequest
895        | VersionRequest
896        | ServerRequest
897        | SoftKeySetRequest
898        | SoftKeyTemplateRequest
899        | KeepAliveAck
900        | CapabilitiesRequest
901        | ClearDisplay
902        | ClearNotify
903        | DeactivateCallPlane
904        | RegisterTokenAck
905        | CallCountResponse => Some(0),
906        RegisterAck => Some(20),
907        UpdateCapabilitiesV2 => Some(2_000),
908        MwiNotification => Some(88),
909        MwiResponse => Some(32),
910        LineStatus => Some(112),
911        DefineTimeDate => Some(36),
912        AddParticipantResponse => Some(272),
913        AuditConferenceRequest => Some(0),
914        SubscribeDtmfPayloadRequest | UnsubscribeDtmfPayloadRequest => Some(16),
915        SubscribeDtmfPayloadResponse
916        | SubscribeDtmfPayloadError
917        | UnsubscribeDtmfPayloadResponse
918        | UnsubscribeDtmfPayloadError => Some(12),
919        ButtonTemplate => Some(96),
920        LocationInfo => Some(2_404),
921        SoftKeyTemplateResponse => Some(652),
922        SoftKeySetResponse => Some(780),
923        MulticastMediaReceptionAck => Some(12),
924        NotifyDtmfTone | SendDtmfTone | VideoDisplayCommand => Some(12),
925        StopMediaTransmission
926        | CloseReceiveChannel
927        | StopMultimediaTransmission
928        | FlowControlCommand
929        | CloseMultimediaReceiveChannel
930        | FlowControlNotify => Some(16),
931        MiscellaneousCommand => Some(52),
932        ExtensionDeviceCapabilities => Some(164),
933        StartMediaFailureDetection => Some(28),
934        QosReservationNotify | QosTeardown | UpdateDscp => Some(24),
935        QosErrorNotify => Some(44),
936        QosListen => Some(172),
937        QosPath => Some(168),
938        QosModify => Some(152),
939        _ => None,
940    }
941}
942
943const fn payload_size_bounds(id: MessageId) -> Option<PayloadSizeBounds> {
944    use MessageId::*;
945    const MAX_PAYLOAD_BYTES: usize = MAX_FRAME_SIZE - HEADER_SIZE;
946    match id {
947        KeepAlive
948        | ConfigStatusRequest
949        | TimeDateRequest
950        | ButtonTemplateRequest
951        | VersionRequest
952        | ServerRequest
953        | SoftKeySetRequest
954        | SoftKeyTemplateRequest
955        | KeepAliveAck
956        | CapabilitiesRequest
957        | ClearDisplay
958        | ClearNotify
959        | DeactivateCallPlane
960        | RegisterTokenAck
961        | CallCountResponse => Some(PayloadSizeBounds {
962            minimum: 0,
963            maximum: MAX_PAYLOAD_BYTES,
964        }),
965        Register => Some(PayloadSizeBounds {
966            minimum: 124,
967            maximum: 172,
968        }),
969        XmlAlarm => Some(PayloadSizeBounds {
970            minimum: 0,
971            maximum: 2_048,
972        }),
973        AddParticipantResponse => Some(PayloadSizeBounds {
974            minimum: 12,
975            maximum: 272,
976        }),
977        CapabilitiesResponse => Some(PayloadSizeBounds {
978            minimum: 4,
979            maximum: 292,
980        }),
981        UpdateCapabilitiesV3 => Some(PayloadSizeBounds {
982            minimum: 20,
983            maximum: 2_380,
984        }),
985        _ => match fixed_payload_bytes(id) {
986            Some(size) => Some(PayloadSizeBounds {
987                minimum: size,
988                maximum: size,
989            }),
990            None => None,
991        },
992    }
993}
994
995const CAPABILITY_RESPONSES: &[MessageId] = &[
996    MessageId::CapabilitiesResponse,
997    MessageId::UpdateCapabilities,
998    MessageId::UpdateCapabilitiesV2,
999    MessageId::UpdateCapabilitiesV3,
1000];
1001
1002const REGISTER_TOKEN_RESPONSES: &[MessageId] =
1003    &[MessageId::RegisterTokenAck, MessageId::RegisterTokenReject];
1004
1005const fn primary_response(id: MessageId) -> ResponseExpectation {
1006    use MessageId::*;
1007    match id {
1008        Register => ResponseExpectation::Message(RegisterAck),
1009        KeepAlive => ResponseExpectation::Message(KeepAliveAck),
1010        Unregister => ResponseExpectation::Message(UnregisterAck),
1011        ConfigStatusRequest => ResponseExpectation::SessionSelected {
1012            before: ConfigStatus,
1013            from: ConfigStatusDynamic,
1014            selector: SessionResponseSelector::DynamicMessagesOrProtocol {
1015                minimum_protocol: 9,
1016            },
1017        },
1018        TimeDateRequest => ResponseExpectation::Message(DefineTimeDate),
1019        ButtonTemplateRequest => ResponseExpectation::Message(ButtonTemplate),
1020        VersionRequest => ResponseExpectation::Message(Version),
1021        CapabilitiesRequest => ResponseExpectation::OneOf(CAPABILITY_RESPONSES),
1022        ServerRequest => ResponseExpectation::Message(ServerResponse),
1023        OpenReceiveChannel => ResponseExpectation::Message(OpenReceiveChannelAck),
1024        ConnectionStatisticsRequest => ResponseExpectation::Message(ConnectionStatisticsResponse),
1025        SoftKeySetRequest => ResponseExpectation::Message(SoftKeySetResponse),
1026        SoftKeyTemplateRequest => ResponseExpectation::Message(SoftKeyTemplateResponse),
1027        RegisterTokenRequest => ResponseExpectation::OneOf(REGISTER_TOKEN_RESPONSES),
1028        LineStatusRequest => ResponseExpectation::SessionSelected {
1029            before: LineStatus,
1030            from: LineStatusDynamic,
1031            selector: SessionResponseSelector::DynamicMessagesOrProtocol {
1032                minimum_protocol: 9,
1033            },
1034        },
1035        SpeedDialStatusRequest => ResponseExpectation::VersionSelected {
1036            before: SpeedDialStatus,
1037            from: SpeedDialStatusDynamic,
1038            minimum_protocol: 15,
1039        },
1040        ServiceUrlStatusRequest => ResponseExpectation::SessionSelected {
1041            before: ServiceUrlStatus,
1042            from: ServiceUrlStatusDynamic,
1043            selector: SessionResponseSelector::DynamicMessagesOrProtocol {
1044                minimum_protocol: 9,
1045            },
1046        },
1047        FeatureStatusRequest => ResponseExpectation::SessionSelected {
1048            before: FeatureStatus,
1049            from: FeatureStatusDynamic,
1050            selector: SessionResponseSelector::DynamicMessages,
1051        },
1052        CreateConferenceRequest => ResponseExpectation::Message(CreateConferenceResponse),
1053        DeleteConferenceRequest => ResponseExpectation::Message(DeleteConferenceResponse),
1054        ModifyConferenceRequest => ResponseExpectation::Message(ModifyConferenceResponse),
1055        AddParticipantRequest => ResponseExpectation::Message(AddParticipantResponse),
1056        AuditConferenceRequest => ResponseExpectation::Message(AuditConferenceResponse),
1057        AuditParticipantRequest => ResponseExpectation::Message(AuditParticipantResponse),
1058        PortRequest => ResponseExpectation::Message(PortResponse),
1059        SubscribeDtmfPayloadRequest => ResponseExpectation::Message(SubscribeDtmfPayloadResponse),
1060        UnsubscribeDtmfPayloadRequest => {
1061            ResponseExpectation::Message(UnsubscribeDtmfPayloadResponse)
1062        }
1063        StartMediaTransmission => ResponseExpectation::Message(StartMediaTransmissionAck),
1064        OpenMultimediaChannel => ResponseExpectation::Message(OpenMultimediaReceiveChannelAck),
1065        StartMultimediaTransmission => ResponseExpectation::Message(StartMultimediaTransmissionAck),
1066        CallCountRequest => ResponseExpectation::Message(CallCountResponse),
1067        _ => ResponseExpectation::None,
1068    }
1069}
1070
1071const fn verification(id: MessageId) -> ContractVerification {
1072    use MessageId::*;
1073    match id {
1074        KeepAlive
1075        | Register
1076        | OffHook
1077        | OnHook
1078        | SoftKeyEvent
1079        | UpdateCapabilities
1080        | MediaPathEvent
1081        | OpenReceiveChannelAck
1082        | RegisterAck
1083        | SetRinger
1084        | SetLamp
1085        | StartMediaTransmission
1086        | ButtonTemplate
1087        | DefineTimeDate
1088        | SoftKeyTemplateResponse
1089        | SoftKeySetResponse
1090        | SelectSoftKeys
1091        | CallState
1092        | ActivateCallPlane
1093        | ClearPromptStatus
1094        | DisplayDynamicPromptStatus
1095        | LineStatusDynamic
1096        | CallInfoDynamic
1097        | OpenReceiveChannel
1098        | CloseReceiveChannel
1099        | StartMediaTransmissionAck => ContractVerification::StructuralAndValidated,
1100        _ => ContractVerification::Structural,
1101    }
1102}
1103
1104message_catalog! {
1105    (KeepAlive, 0x0000, StationToControl),
1106    (Register, 0x0001, StationToControl),
1107    (IpPort, 0x0002, StationToControl),
1108    (KeypadButton, 0x0003, StationToControl),
1109    (EnblocCall, 0x0004, StationToControl),
1110    (Stimulus, 0x0005, StationToControl),
1111    (OffHook, 0x0006, StationToControl),
1112    (OnHook, 0x0007, StationToControl),
1113    (HookFlash, 0x0008, StationToControl),
1114    (ForwardStatusRequest, 0x0009, StationToControl),
1115    (SpeedDialStatusRequest, 0x000a, StationToControl),
1116    (LineStatusRequest, 0x000b, StationToControl),
1117    (ConfigStatusRequest, 0x000c, StationToControl),
1118    (TimeDateRequest, 0x000d, StationToControl),
1119    (ButtonTemplateRequest, 0x000e, StationToControl),
1120    (VersionRequest, 0x000f, StationToControl),
1121    (CapabilitiesResponse, 0x0010, StationToControl),
1122    (MediaPortList, 0x0011, StationToControl),
1123    (ServerRequest, 0x0012, StationToControl),
1124    (Alarm, 0x0020, StationToControl),
1125    (MulticastMediaReceptionAck, 0x0021, StationToControl),
1126    (OpenReceiveChannelAck, 0x0022, StationToControl),
1127    (ConnectionStatisticsResponse, 0x0023, StationToControl),
1128    (OffHookWithCallingParty, 0x0024, StationToControl),
1129    (SoftKeySetRequest, 0x0025, StationToControl),
1130    (SoftKeyEvent, 0x0026, StationToControl),
1131    (Unregister, 0x0027, StationToControl),
1132    (SoftKeyTemplateRequest, 0x0028, StationToControl),
1133    (RegisterTokenRequest, 0x0029, StationToControl),
1134    (MediaTransmissionFailure, 0x002a, StationToControl),
1135    (HeadsetStatus, 0x002b, StationToControl),
1136    (MediaResourceNotification, 0x002c, ServiceNodeToControl),
1137    (RegisterAvailableLines, 0x002d, StationToControl),
1138    (DeviceToUserData, 0x002e, StationToControl),
1139    (DeviceToUserDataResponse, 0x002f, StationToControl),
1140    (UpdateCapabilities, 0x0030, StationToControl),
1141    (OpenMultimediaReceiveChannelAck, 0x0031, StationToControl),
1142    (ClearConference, 0x0032, ServiceNodeToControl),
1143    (ServiceUrlStatusRequest, 0x0033, StationToControl),
1144    (FeatureStatusRequest, 0x0034, StationToControl),
1145    (CreateConferenceResponse, 0x0035, ServiceNodeToControl),
1146    (DeleteConferenceResponse, 0x0036, ServiceNodeToControl),
1147    (ModifyConferenceResponse, 0x0037, ServiceNodeToControl),
1148    (AddParticipantResponse, 0x0038, ServiceNodeToControl),
1149    (AuditConferenceResponse, 0x0039, ServiceNodeToControl),
1150    (AuditParticipantResponse, 0x0040, ServiceNodeToControl),
1151    (DeviceToUserDataV1, 0x0041, StationToControl),
1152    (DeviceToUserDataResponseV1, 0x0042, StationToControl),
1153    (UpdateCapabilitiesV2, 0x0043, StationToControl),
1154    (UpdateCapabilitiesV3, 0x0044, StationToControl),
1155    (PortResponse, 0x0045, ServiceNodeToControl),
1156    (QosReservationNotify, 0x0046, ServiceNodeToControl),
1157    (QosErrorNotify, 0x0047, ServiceNodeToControl),
1158    (SubscriptionStatusRequest, 0x0048, StationToControl),
1159    (MediaPathEvent, 0x0049, StationToControl),
1160    (MediaPathCapability, 0x004a, StationToControl),
1161    (MwiNotification, 0x004c, ServiceNodeToControl),
1162
1163    (RegisterAck, 0x0081, ControlToStation),
1164    (StartTone, 0x0082, ControlToStation),
1165    (StopTone, 0x0083, ControlToStation),
1166    (SetRinger, 0x0085, ControlToStation),
1167    (SetLamp, 0x0086, ControlToStation),
1168    (SetHookFlashDetect, 0x0087, ControlToStation),
1169    (SetSpeakerMode, 0x0088, ControlToStation),
1170    (SetMicrophoneMode, 0x0089, ControlToStation),
1171    (StartMediaTransmission, 0x008a, ControlToStation),
1172    (StopMediaTransmission, 0x008b, ControlToStation),
1173    (StartMediaReception, 0x008c, ControlToStation),
1174    (StopMediaReception, 0x008d, ControlToStation),
1175    (CallInfo, 0x008f, ControlToStation),
1176    (ForwardStatus, 0x0090, ControlToStation),
1177    (SpeedDialStatus, 0x0091, ControlToStation),
1178    (LineStatus, 0x0092, ControlToStation),
1179    (ConfigStatus, 0x0093, ControlToStation),
1180    (DefineTimeDate, 0x0094, ControlToStation),
1181    (StartSessionTransmission, 0x0095, ControlToServiceNode),
1182    (StopSessionTransmission, 0x0096, ControlToServiceNode),
1183    (ButtonTemplate, 0x0097, ControlToStation),
1184    (Version, 0x0098, ControlToStation),
1185    (DisplayText, 0x0099, ControlToStation),
1186    (ClearDisplay, 0x009a, ControlToStation),
1187    (CapabilitiesRequest, 0x009b, ControlToStation),
1188    (EnunciatorCommand, 0x009c, ControlToStation),
1189    (RegisterReject, 0x009d, ControlToStation),
1190    (ServerResponse, 0x009e, ControlToStation),
1191    (Reset, 0x009f, ControlToStation),
1192    (KeepAliveAck, 0x0100, ControlToStation),
1193    (StartMulticastMediaReception, 0x0101, ControlToStation),
1194    (StartMulticastMediaTransmission, 0x0102, ControlToStation),
1195    (StopMulticastMediaReception, 0x0103, ControlToStation),
1196    (StopMulticastMediaTransmission, 0x0104, ControlToStation),
1197    (OpenReceiveChannel, 0x0105, ControlToStation),
1198    (CloseReceiveChannel, 0x0106, ControlToStation),
1199    (ConnectionStatisticsRequest, 0x0107, ControlToStation),
1200    (SoftKeyTemplateResponse, 0x0108, ControlToStation),
1201    (SoftKeySetResponse, 0x0109, ControlToStation),
1202    (SelectSoftKeys, 0x0110, ControlToStation),
1203    (CallState, 0x0111, ControlToStation),
1204    (DisplayPromptStatus, 0x0112, ControlToStation),
1205    (ClearPromptStatus, 0x0113, ControlToStation),
1206    (DisplayNotify, 0x0114, ControlToStation),
1207    (ClearNotify, 0x0115, ControlToStation),
1208    (ActivateCallPlane, 0x0116, ControlToStation),
1209    (DeactivateCallPlane, 0x0117, ControlToStation),
1210    (UnregisterAck, 0x0118, ControlToStation),
1211    (BackspaceResponse, 0x0119, ControlToStation),
1212    (RegisterTokenAck, 0x011a, ControlToStation),
1213    (RegisterTokenReject, 0x011b, ControlToStation),
1214    (StartMediaFailureDetection, 0x011c, ControlToStation),
1215    (DialedNumber, 0x011d, ControlToStation),
1216    (UserToDeviceData, 0x011e, ControlToStation),
1217    (FeatureStatus, 0x011f, ControlToStation),
1218    (DisplayPriorityNotify, 0x0120, ControlToStation),
1219    (ClearPriorityNotify, 0x0121, ControlToStation),
1220    (StartAnnouncement, 0x0122, IntraControl),
1221    (StopAnnouncement, 0x0123, IntraControl),
1222    (AnnouncementFinish, 0x0124, IntraControl),
1223    (NotifyDtmfTone, 0x0127, ControlToStation),
1224    (SendDtmfTone, 0x0128, ControlToStation),
1225    (SubscribeDtmfPayloadRequest, 0x0129, ControlToStation),
1226    (SubscribeDtmfPayloadResponse, 0x012a, StationToControl),
1227    (SubscribeDtmfPayloadError, 0x012b, ControlToStation),
1228    (UnsubscribeDtmfPayloadRequest, 0x012c, ControlToStation),
1229    (UnsubscribeDtmfPayloadResponse, 0x012d, StationToControl),
1230    (UnsubscribeDtmfPayloadError, 0x012e, ControlToStation),
1231    (ServiceUrlStatus, 0x012f, ControlToStation),
1232    (CallSelectStatus, 0x0130, ControlToStation),
1233    (OpenMultimediaChannel, 0x0131, ControlToStation),
1234    (StartMultimediaTransmission, 0x0132, ControlToStation),
1235    (StopMultimediaTransmission, 0x0133, ControlToStation),
1236    (MiscellaneousCommand, 0x0134, ControlToStation),
1237    (FlowControlCommand, 0x0135, ControlToStation),
1238    (CloseMultimediaReceiveChannel, 0x0136, ControlToStation),
1239    (CreateConferenceRequest, 0x0137, ControlToServiceNode),
1240    (DeleteConferenceRequest, 0x0138, ControlToServiceNode),
1241    (ModifyConferenceRequest, 0x0139, ControlToServiceNode),
1242    (AddParticipantRequest, 0x013a, ControlToServiceNode),
1243    (DropParticipantRequest, 0x013b, ControlToServiceNode),
1244    (AuditConferenceRequest, 0x013c, ControlToServiceNode),
1245    (AuditParticipantRequest, 0x013d, ControlToServiceNode),
1246    (ChangeParticipantRequest, 0x013e, ControlToServiceNode),
1247    (UserToDeviceDataV1, 0x013f, ControlToStation),
1248    (VideoDisplayCommand, 0x0140, ControlToStation),
1249    (FlowControlNotify, 0x0141, ControlToStation),
1250    (ConfigStatusDynamic, 0x0142, ControlToStation),
1251    (DisplayDynamicNotify, 0x0143, ControlToStation),
1252    (DisplayDynamicPriorityNotify, 0x0144, ControlToStation),
1253    (DisplayDynamicPromptStatus, 0x0145, ControlToStation),
1254    (FeatureStatusDynamic, 0x0146, ControlToStation),
1255    (LineStatusDynamic, 0x0147, ControlToStation),
1256    (ServiceUrlStatusDynamic, 0x0148, ControlToStation),
1257    (SpeedDialStatusDynamic, 0x0149, ControlToStation),
1258    (CallInfoDynamic, 0x014a, ControlToStation),
1259    (PortRequest, 0x014b, ControlToStation),
1260    (PortClose, 0x014c, ControlToStation),
1261    (QosListen, 0x014d, ControlToServiceNode),
1262    (QosPath, 0x014e, ControlToServiceNode),
1263    (QosTeardown, 0x014f, ControlToServiceNode),
1264    (UpdateDscp, 0x0150, ControlToServiceNode),
1265    (QosModify, 0x0151, ControlToServiceNode),
1266
1267    (SubscriptionStatus, 0x0152, ControlToStation),
1268    (Notification, 0x0153, ControlToStation),
1269    (StartMediaTransmissionAck, 0x0154, StationToControl),
1270    (StartMultimediaTransmissionAck, 0x0155, StationToControl),
1271    (CallHistoryDisposition, 0x0156, ControlToStation),
1272    (LocationInfo, 0x0157, StationToControl),
1273    (MwiResponse, 0x0158, ControlToServiceNode),
1274    (ExtensionDeviceCapabilities, 0x0159, StationToControl),
1275    (XmlAlarm, 0x015a, StationToControl),
1276    (CallCountRequest, 0x015e, StationToControl),
1277    (CallCountResponse, 0x015f, ControlToStation),
1278    (RecordingStatus, 0x0160, ControlToStation),
1279
1280    (SpcpRegisterTokenRequest, 0x8000, StationToControl),
1281    (SpcpRegisterTokenAck, 0x8100, ControlToStation),
1282    (SpcpRegisterTokenReject, 0x8101, ControlToStation),
1283}
1284
1285impl fmt::Display for MessageId {
1286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1287        match self {
1288            Self::Unknown(value) => write!(f, "Unknown(0x{value:04x})"),
1289            known => f.write_str(known.name()),
1290        }
1291    }
1292}
1293
1294/// Iterates the complete typed implementation inventory in wire-ID order.
1295///
1296/// Opaque-only contracts are intentionally excluded. To inspect every known
1297/// identifier, iterate [`MessageId::ALL_KNOWN`] and call
1298/// [`MessageId::contract`] instead.
1299pub fn implemented_message_contracts() -> impl Iterator<Item = MessageContract> {
1300    MessageId::ALL_KNOWN
1301        .iter()
1302        .filter_map(|id| id.contract())
1303        .filter(|contract| contract.codec == CodecSupport::Typed)
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309    use std::collections::HashSet;
1310
1311    #[test]
1312    fn known_catalog_values_are_unique_and_round_trip() {
1313        let mut values = HashSet::new();
1314        for id in MessageId::ALL_KNOWN {
1315            assert!(values.insert(id.wire_value()), "duplicate {id}");
1316            assert_eq!(MessageId::from(id.wire_value()), *id);
1317            assert!(id.route().is_some());
1318            assert!(id.is_known());
1319        }
1320        assert!(MessageId::ALL_KNOWN.len() > 140);
1321    }
1322
1323    #[test]
1324    fn supplemental_contract_scope_is_explicit_and_closed() {
1325        let supplemental = MessageId::ALL_KNOWN
1326            .iter()
1327            .copied()
1328            .filter(|id| id.contract().unwrap().scope == ContractScope::Supplemental)
1329            .collect::<Vec<_>>();
1330
1331        assert_eq!(
1332            supplemental,
1333            [
1334                MessageId::IpPort,
1335                MessageId::MediaPortList,
1336                MessageId::SetHookFlashDetect,
1337                MessageId::StartMediaReception,
1338                MessageId::StopMediaReception,
1339                MessageId::EnunciatorCommand,
1340                MessageId::ExtensionDeviceCapabilities,
1341                MessageId::SpcpRegisterTokenRequest,
1342                MessageId::SpcpRegisterTokenAck,
1343                MessageId::SpcpRegisterTokenReject,
1344            ]
1345        );
1346    }
1347
1348    #[test]
1349    fn unknown_identifiers_remain_lossless() {
1350        let id = MessageId::from(0xdead_beef);
1351        assert_eq!(id, MessageId::Unknown(0xdead_beef));
1352        assert_eq!(id.wire_value(), 0xdead_beef);
1353        assert_eq!(id.direction(), None);
1354    }
1355
1356    #[test]
1357    fn dtmf_subscription_responses_have_the_device_to_server_direction() {
1358        assert_eq!(
1359            MessageId::SubscribeDtmfPayloadRequest.direction(),
1360            Some(MessageDirection::ServerToDevice)
1361        );
1362        assert_eq!(
1363            MessageId::SubscribeDtmfPayloadResponse.direction(),
1364            Some(MessageDirection::DeviceToServer)
1365        );
1366        assert_eq!(
1367            MessageId::UnsubscribeDtmfPayloadRequest.direction(),
1368            Some(MessageDirection::ServerToDevice)
1369        );
1370        assert_eq!(
1371            MessageId::UnsubscribeDtmfPayloadResponse.direction(),
1372            Some(MessageDirection::DeviceToServer)
1373        );
1374    }
1375
1376    #[test]
1377    fn every_known_id_has_an_explicit_support_and_runtime_contract() {
1378        for id in MessageId::ALL_KNOWN {
1379            let contract = id.contract().expect("known ID has a contract");
1380            assert_eq!(contract.id, *id);
1381            assert_eq!(contract.route, id.route().unwrap());
1382            match contract.codec {
1383                CodecSupport::Typed => {
1384                    assert_eq!(contract.emission, EmissionSupport::Typed);
1385                    assert_ne!(contract.runtime_use, RuntimeUse::CatalogOnly);
1386                }
1387                CodecSupport::OpaqueOnly => {
1388                    assert_eq!(contract.emission, EmissionSupport::PreserveOnly);
1389                    assert_eq!(contract.runtime_use, RuntimeUse::CatalogOnly);
1390                    assert_eq!(contract.payload_layout, PayloadLayout::Opaque);
1391                }
1392            }
1393            match contract.field_fidelity {
1394                FieldFidelity::CanonicalServerOutput(detail) => {
1395                    assert!(matches!(
1396                        contract.route,
1397                        MessageRoute::ControlToStation
1398                            | MessageRoute::ControlToServiceNode
1399                            | MessageRoute::IntraControl
1400                    ));
1401                    assert!(!detail.is_empty());
1402                }
1403                FieldFidelity::SemanticProjection(detail) => {
1404                    assert_eq!(contract.codec, CodecSupport::Typed);
1405                    assert!(!detail.is_empty());
1406                }
1407                FieldFidelity::OpaquePreserved => {
1408                    assert_eq!(contract.codec, CodecSupport::OpaqueOnly);
1409                }
1410                FieldFidelity::Lossless => {}
1411            }
1412        }
1413        assert!(implemented_message_contracts().count() > 100);
1414
1415        for id in [
1416            MessageId::OpenReceiveChannel,
1417            MessageId::StartMediaTransmission,
1418            MessageId::StartMediaTransmissionAck,
1419            MessageId::KeypadButton,
1420            MessageId::EnblocCall,
1421            MessageId::Register,
1422            MessageId::Alarm,
1423            MessageId::DefineTimeDate,
1424        ] {
1425            assert_eq!(
1426                id.contract().unwrap().field_fidelity,
1427                FieldFidelity::Lossless
1428            );
1429        }
1430    }
1431
1432    #[test]
1433    fn semantic_field_fidelity_overclaims_are_explicitly_excluded() {
1434        for (id, omitted) in [
1435            (MessageId::LineStatus, "display label"),
1436            (MessageId::CallInfo, "mailboxes"),
1437            (MessageId::CallState, "visibility"),
1438        ] {
1439            let FieldFidelity::CanonicalServerOutput(detail) =
1440                id.contract().unwrap().field_fidelity
1441            else {
1442                panic!("{id} must not claim lossless field fidelity");
1443            };
1444            assert!(detail.contains(omitted), "{id}: {detail}");
1445        }
1446
1447        for id in [MessageId::ConfigStatus, MessageId::ConfigStatusDynamic] {
1448            assert_eq!(
1449                id.contract().unwrap().field_fidelity,
1450                FieldFidelity::Lossless
1451            );
1452        }
1453
1454        for id in [
1455            MessageId::CapabilitiesResponse,
1456            MessageId::MediaTransmissionFailure,
1457            MessageId::PortResponse,
1458        ] {
1459            assert!(matches!(
1460                id.contract().unwrap().field_fidelity,
1461                FieldFidelity::SemanticProjection(_)
1462            ));
1463        }
1464    }
1465
1466    #[test]
1467    fn variable_layout_messages_never_claim_one_fixed_payload_size() {
1468        for contract in MessageId::ALL_KNOWN.iter().filter_map(|id| id.contract()) {
1469            if matches!(
1470                contract.payload_layout,
1471                PayloadLayout::VersionSelected
1472                    | PayloadLayout::VersionAndLengthSelected
1473                    | PayloadLayout::BoundedPreserved
1474            ) {
1475                assert_eq!(
1476                    contract.fixed_payload_bytes, None,
1477                    "{} has a variable payload layout",
1478                    contract.id
1479                );
1480            }
1481        }
1482    }
1483
1484    #[test]
1485    fn bounded_and_counted_payload_contracts_report_their_wire_limits() {
1486        for id in [
1487            MessageId::KeepAlive,
1488            MessageId::ConfigStatusRequest,
1489            MessageId::ButtonTemplateRequest,
1490            MessageId::KeepAliveAck,
1491        ] {
1492            assert_eq!(
1493                id.contract().unwrap().payload_size_bounds,
1494                Some(PayloadSizeBounds {
1495                    minimum: 0,
1496                    maximum: MAX_FRAME_SIZE - HEADER_SIZE,
1497                }),
1498                "{id}"
1499            );
1500        }
1501
1502        let capabilities = MessageId::CapabilitiesResponse.contract().unwrap();
1503        assert_eq!(capabilities.payload_layout, PayloadLayout::LengthPrefixed);
1504        assert_eq!(
1505            capabilities.payload_size_bounds,
1506            Some(PayloadSizeBounds {
1507                minimum: 4,
1508                maximum: 292,
1509            })
1510        );
1511
1512        let version_two = MessageId::UpdateCapabilitiesV2.contract().unwrap();
1513        assert_eq!(version_two.payload_layout, PayloadLayout::Fixed);
1514        assert_eq!(version_two.fixed_payload_bytes, Some(2_000));
1515
1516        let version_three = MessageId::UpdateCapabilitiesV3.contract().unwrap();
1517        assert_eq!(
1518            version_three.payload_layout,
1519            PayloadLayout::MinimumLengthPreserved
1520        );
1521        assert_eq!(
1522            version_three.payload_size_bounds,
1523            Some(PayloadSizeBounds {
1524                minimum: 20,
1525                maximum: 2_380,
1526            })
1527        );
1528    }
1529
1530    #[test]
1531    fn service_message_payload_bounds_are_explicit() {
1532        assert_eq!(
1533            MessageId::XmlAlarm.contract().unwrap().payload_size_bounds,
1534            Some(PayloadSizeBounds {
1535                minimum: 0,
1536                maximum: 2_048,
1537            })
1538        );
1539        assert_eq!(
1540            MessageId::AddParticipantResponse
1541                .contract()
1542                .unwrap()
1543                .payload_size_bounds,
1544            Some(PayloadSizeBounds {
1545                minimum: 12,
1546                maximum: 272,
1547            })
1548        );
1549
1550        for (id, size) in [
1551            (MessageId::AuditConferenceRequest, 0),
1552            (MessageId::SubscribeDtmfPayloadRequest, 16),
1553            (MessageId::SubscribeDtmfPayloadResponse, 12),
1554            (MessageId::SubscribeDtmfPayloadError, 12),
1555            (MessageId::UnsubscribeDtmfPayloadRequest, 16),
1556            (MessageId::UnsubscribeDtmfPayloadResponse, 12),
1557            (MessageId::UnsubscribeDtmfPayloadError, 12),
1558        ] {
1559            assert_eq!(
1560                id.contract().unwrap().payload_size_bounds,
1561                Some(PayloadSizeBounds {
1562                    minimum: size,
1563                    maximum: size,
1564                }),
1565                "{id}"
1566            );
1567        }
1568    }
1569
1570    #[test]
1571    fn media_contracts_record_fixed_and_version_selected_sizes() {
1572        for id in [
1573            MessageId::OpenMultimediaReceiveChannelAck,
1574            MessageId::StartMultimediaTransmissionAck,
1575            MessageId::StartSessionTransmission,
1576            MessageId::StopSessionTransmission,
1577            MessageId::OpenMultimediaChannel,
1578            MessageId::StartMultimediaTransmission,
1579            MessageId::PortRequest,
1580            MessageId::PortClose,
1581        ] {
1582            let contract = id.contract().unwrap();
1583            assert_eq!(contract.payload_layout, PayloadLayout::VersionSelected);
1584            assert_eq!(contract.fixed_payload_bytes, None);
1585        }
1586
1587        for (id, size) in [
1588            (MessageId::MulticastMediaReceptionAck, 12),
1589            (MessageId::CloseReceiveChannel, 16),
1590            (MessageId::StopMediaTransmission, 16),
1591            (MessageId::MiscellaneousCommand, 52),
1592            (MessageId::QosReservationNotify, 24),
1593            (MessageId::QosErrorNotify, 44),
1594            (MessageId::QosListen, 172),
1595            (MessageId::QosPath, 168),
1596            (MessageId::QosTeardown, 24),
1597            (MessageId::UpdateDscp, 24),
1598            (MessageId::QosModify, 152),
1599        ] {
1600            assert_eq!(id.contract().unwrap().fixed_payload_bytes, Some(size));
1601        }
1602
1603        assert_eq!(
1604            MessageId::StartMediaTransmissionAck
1605                .contract()
1606                .unwrap()
1607                .payload_layout,
1608            PayloadLayout::VersionAndLengthSelected
1609        );
1610        assert_eq!(
1611            MessageId::LocationInfo
1612                .contract()
1613                .unwrap()
1614                .payload_size_bounds,
1615            Some(PayloadSizeBounds {
1616                minimum: 2_404,
1617                maximum: 2_404,
1618            })
1619        );
1620        for id in [
1621            MessageId::CloseReceiveChannel,
1622            MessageId::StopMediaTransmission,
1623        ] {
1624            assert_eq!(
1625                id.contract().unwrap().field_fidelity,
1626                FieldFidelity::Lossless
1627            );
1628        }
1629    }
1630
1631    #[test]
1632    fn session_transmission_contracts_use_the_service_node_codec() {
1633        for id in [
1634            MessageId::StartSessionTransmission,
1635            MessageId::StopSessionTransmission,
1636        ] {
1637            let contract = id.contract().unwrap();
1638            assert_eq!(contract.route, MessageRoute::ControlToServiceNode);
1639            assert_eq!(contract.codec, CodecSupport::Typed);
1640            assert_eq!(contract.emission, EmissionSupport::Typed);
1641            assert_eq!(contract.runtime_use, RuntimeUse::TypedButNotEmitted);
1642            assert_eq!(contract.field_fidelity, FieldFidelity::Lossless);
1643            assert_eq!(contract.payload_layout, PayloadLayout::VersionSelected);
1644        }
1645    }
1646
1647    #[test]
1648    fn supplemental_token_messages_remain_preserve_only() {
1649        for id in [
1650            MessageId::SpcpRegisterTokenRequest,
1651            MessageId::SpcpRegisterTokenAck,
1652            MessageId::SpcpRegisterTokenReject,
1653        ] {
1654            let contract = id.contract().unwrap();
1655            assert_eq!(contract.codec, CodecSupport::OpaqueOnly);
1656            assert_eq!(contract.emission, EmissionSupport::PreserveOnly);
1657            assert_eq!(contract.runtime_use, RuntimeUse::CatalogOnly);
1658            assert_eq!(contract.payload_layout, PayloadLayout::Opaque);
1659        }
1660    }
1661
1662    #[test]
1663    fn runtime_emission_is_distinct_from_typed_encodability() {
1664        let dtmf = MessageId::SubscribeDtmfPayloadRequest.contract().unwrap();
1665        assert_eq!(dtmf.codec, CodecSupport::Typed);
1666        assert_eq!(dtmf.emission, EmissionSupport::Typed);
1667        assert_eq!(dtmf.runtime_use, RuntimeUse::TypedButNotEmitted);
1668
1669        let open = MessageId::OpenReceiveChannel.contract().unwrap();
1670        assert_eq!(open.runtime_use, RuntimeUse::ConditionalServerOutput);
1671        assert_eq!(
1672            open.response,
1673            ResponseExpectation::Message(MessageId::OpenReceiveChannelAck)
1674        );
1675
1676        for id in [
1677            MessageId::MiscellaneousCommand,
1678            MessageId::FlowControlCommand,
1679            MessageId::FlowControlNotify,
1680        ] {
1681            assert_eq!(
1682                id.contract().unwrap().runtime_use,
1683                RuntimeUse::ConditionalServerOutput
1684            );
1685        }
1686
1687        for (id, response, runtime_use) in [
1688            (
1689                MessageId::OpenMultimediaChannel,
1690                MessageId::OpenMultimediaReceiveChannelAck,
1691                RuntimeUse::ConditionalServerOutput,
1692            ),
1693            (
1694                MessageId::StartMultimediaTransmission,
1695                MessageId::StartMultimediaTransmissionAck,
1696                RuntimeUse::ConditionalServerOutput,
1697            ),
1698        ] {
1699            let contract = id.contract().unwrap();
1700            assert_eq!(contract.route, MessageRoute::ControlToStation);
1701            assert_eq!(contract.codec, CodecSupport::Typed);
1702            assert_eq!(contract.emission, EmissionSupport::Typed);
1703            assert_eq!(contract.runtime_use, runtime_use);
1704            assert_eq!(contract.field_fidelity, FieldFidelity::Lossless);
1705            assert_eq!(contract.payload_layout, PayloadLayout::VersionSelected);
1706            assert_eq!(contract.response, ResponseExpectation::Message(response));
1707        }
1708    }
1709
1710    #[test]
1711    fn dynamic_response_contracts_include_every_session_selector() {
1712        for (request, before, from) in [
1713            (
1714                MessageId::ConfigStatusRequest,
1715                MessageId::ConfigStatus,
1716                MessageId::ConfigStatusDynamic,
1717            ),
1718            (
1719                MessageId::LineStatusRequest,
1720                MessageId::LineStatus,
1721                MessageId::LineStatusDynamic,
1722            ),
1723            (
1724                MessageId::ServiceUrlStatusRequest,
1725                MessageId::ServiceUrlStatus,
1726                MessageId::ServiceUrlStatusDynamic,
1727            ),
1728        ] {
1729            assert_eq!(
1730                request.contract().unwrap().response,
1731                ResponseExpectation::SessionSelected {
1732                    before,
1733                    from,
1734                    selector: SessionResponseSelector::DynamicMessagesOrProtocol {
1735                        minimum_protocol: 9,
1736                    },
1737                }
1738            );
1739        }
1740
1741        assert_eq!(
1742            MessageId::FeatureStatusRequest.contract().unwrap().response,
1743            ResponseExpectation::SessionSelected {
1744                before: MessageId::FeatureStatus,
1745                from: MessageId::FeatureStatusDynamic,
1746                selector: SessionResponseSelector::DynamicMessages,
1747            }
1748        );
1749        assert_eq!(
1750            MessageId::SpeedDialStatusRequest
1751                .contract()
1752                .unwrap()
1753                .response,
1754            ResponseExpectation::VersionSelected {
1755                before: MessageId::SpeedDialStatus,
1756                from: MessageId::SpeedDialStatusDynamic,
1757                minimum_protocol: 15,
1758            }
1759        );
1760    }
1761}