Skip to main content

sccp_protocol/message/
mod.rs

1//! Typed SCCP messages used by the server.
2//!
3//! This module also exposes wire values, framing, and contract metadata.
4//!
5//! A typical inbound flow feeds TCP bytes to [`wire::FrameDecoder`], validates
6//! the negotiated [`values::ProtocolVersion`], then decodes the frame as
7//! [`ClientMessage`], [`ServerMessage`], or [`ControlMessage`] according to its
8//! [`catalog::MessageRoute`]. Outbound typed messages expose `encode` methods
9//! implemented by the private codec module. Unknown identifiers and partially
10//! modeled fields have explicit bounded-preservation types rather than being
11//! silently discarded.
12
13mod bounded;
14pub mod capabilities;
15pub mod catalog;
16mod codec;
17pub mod values;
18pub mod wire;
19
20use std::fmt;
21use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
22use std::num::{NonZeroU16, NonZeroU32};
23
24use crate::types::DateTemplate;
25use crate::types::{
26    ApplicationId, CallInfo, CallReference, ConferenceId, DeviceId, MediaEndpoint, SoftKeyProfile,
27    TransactionId,
28};
29use capabilities::CapabilityUpdate;
30use catalog::MessageId;
31pub(crate) use catalog::wire_id;
32use values::{
33    AddParticipantResult, AlarmSeverity, AnnouncementPlayMode, AnnouncementPlayStatus,
34    AuditParticipantResult, BusyLampFieldState, ButtonType, CallHistoryDisposition, CallState,
35    Codec, ConferenceResourceType, CreateConferenceResult, DeleteConferenceResult, DeviceType,
36    Digit, EchoCancellation, EncryptionMethod, EndOfAnnouncementAck, G723BitRate, IpAddressType,
37    KeyMode, LampMode, MediaPathCapability, MediaPathEvent, MediaPathId, MediaStatus,
38    MediaTransport, MediaType, MessageWaitingResult, MicrophoneMode, ModifyConferenceResult,
39    NotificationPriority, PartyInformationRestrictions, PhoneFeatures, ProtocolVersion,
40    QosDirection, QosErrorCode, QosReservationStyle, ResetType, RingDuration, RingerMode,
41    RsvpErrorCode, SilenceSuppression, SpeakerMode, StatisticsProcessing, Stimulus,
42    SubscriptionCause, Tone, ToneDirection, VideoFormat,
43};
44use wire::CodecError;
45
46pub use bounded::{BoundedBytes, BoundedBytesError};
47
48/// Largest opaque body retained from a valid frame.
49pub const MAX_OPAQUE_MESSAGE_BYTES: usize = wire::MAX_FRAME_SIZE - wire::HEADER_SIZE;
50
51/// Width of the codec-specific capability union in multimedia channel messages.
52pub const MULTIMEDIA_CAPABILITY_BYTES: usize = 76;
53/// Maximum picture-format entries in one multimedia video capability.
54pub const MAX_MULTIMEDIA_PICTURE_FORMATS: usize = 5;
55
56/// Number of definitions reserved by the fixed 96-byte ButtonTemplate body.
57pub(crate) const BUTTON_TEMPLATE_ENTRIES_PER_CHUNK: usize = 42;
58
59/// Non-zero token placed in the SCCP pass-through-party field to identify one
60/// media request generation, rather than the lifetime of a call.
61///
62/// Phones echo this field on conforming ORC/SMT acknowledgements. Changing it
63/// per request prevents a delayed ACK for a retired request from matching a
64/// later reopen and supplies explicit wire correlation to both ACK families.
65#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
66pub struct MediaRequestToken(NonZeroU32);
67
68impl MediaRequestToken {
69    /// Creates a token, returning `None` for the reserved value zero.
70    pub const fn new(value: u32) -> Option<Self> {
71        match NonZeroU32::new(value) {
72            Some(value) => Some(Self(value)),
73            None => None,
74        }
75    }
76
77    pub const fn get(self) -> u32 {
78        self.0.get()
79    }
80
81    /// Advance without wrapping or reusing token zero.
82    ///
83    /// Exhaustion is an explicit failure: silently wrapping would make an
84    /// ancient acknowledgement eligible to match a new request.
85    pub const fn checked_next(self) -> Option<Self> {
86        match self.get().checked_add(1) {
87            Some(value) => Self::new(value),
88            None => None,
89        }
90    }
91}
92
93/// Pending identity used to decide whether a handset media ACK belongs to the
94/// currently opening receive/transmit request.
95#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
96pub struct MediaRequestIdentity {
97    generation: u64,
98    token: MediaRequestToken,
99}
100
101impl MediaRequestIdentity {
102    /// Construct an identity. Generations are monotonic per call and start at
103    /// one; a deliberately coupled ORC/SMT pair shares one identity. Tokens
104    /// must be allocated uniquely among live and retired media sessions.
105    pub const fn new(generation: u64, token: MediaRequestToken) -> Option<Self> {
106        if generation == 0 {
107            None
108        } else {
109            Some(Self { generation, token })
110        }
111    }
112
113    pub const fn generation(self) -> u64 {
114        self.generation
115    }
116
117    pub const fn token(self) -> MediaRequestToken {
118        self.token
119    }
120
121    /// Advance both the logical generation and its wire token without wrap.
122    /// A caller must fail the media reopen when this returns `None`.
123    pub const fn checked_next(self) -> Option<Self> {
124        let generation = match self.generation.checked_add(1) {
125            Some(generation) => generation,
126            None => return None,
127        };
128        let token = match self.token.checked_next() {
129            Some(token) => token,
130            None => return None,
131        };
132        Some(Self { generation, token })
133    }
134
135    /// Match an ACK without permitting a prior generation to settle a reopen.
136    ///
137    /// An SMT acknowledgement may omit the party ID. That fallback is safe
138    /// only for generation one and only with the stable call reference;
139    /// after a reopen, a zero-party ACK is intrinsically ambiguous and fails
140    /// closed. A present party ID must match the fresh token, while a present
141    /// call reference must still identify the same call.
142    pub const fn accepts_ack(
143        self,
144        acknowledgement_party_id: u32,
145        acknowledgement_call_reference: u32,
146        stable_call_reference: u32,
147    ) -> bool {
148        let call_matches = acknowledgement_call_reference == 0
149            || acknowledgement_call_reference == stable_call_reference;
150        if acknowledgement_party_id == self.token.get() {
151            return call_matches;
152        }
153        self.generation == 1
154            && acknowledgement_party_id == 0
155            && acknowledgement_call_reference == stable_call_reference
156    }
157}
158
159#[derive(Clone, Debug, Eq, PartialEq)]
160/// An unrecognized frame retained without interpreting its identifier or payload.
161pub struct RawMessage {
162    pub message_id: u32,
163    pub protocol_version: u32,
164    pub payload: Vec<u8>,
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
168/// Station registration identity, addressing, capacity, and feature data.
169///
170/// The codec accepts both mandatory and extended registration bodies. Extended
171/// capacity fields are available through [`RegistrationMessage::wire`].
172pub struct RegistrationMessage {
173    pub device_id: DeviceId,
174    /// IPv4 address claimed by the station, independent of its TCP peer address.
175    pub reported_address: Option<Ipv4Addr>,
176    /// IPv6 address claimed by the station when the extended layout carries one.
177    pub reported_ipv6_address: Option<Ipv6Addr>,
178    pub device_type: DeviceType,
179    /// Raw protocol version advertised inside the registration body.
180    ///
181    /// Session code must validate/negotiate this through [`ProtocolVersion`].
182    pub advertised_protocol: u32,
183    /// Feature bits packed alongside the advertised body version.
184    pub features: PhoneFeatures,
185    pub firmware: String,
186    /// Bytes following the mandatory registration prefix.
187    pub configuration_version_stamp: BoundedBytes<48>,
188    /// Exact capacity and addressing metadata from the extended registration
189    /// layout. Runtime-created registrations may omit it and receive the
190    /// conservative wire defaults used by the encoder.
191    pub wire: Option<RegistrationWireDetails>,
192}
193
194/// Auxiliary fields carried by the extended station registration layout.
195///
196/// These fields are not registration policy, but retaining them prevents a
197/// decode/encode cycle from erasing capacity, scope, or station identity data.
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub struct RegistrationWireDetails {
200    pub station_user_id: u32,
201    pub station_instance: u32,
202    pub max_streams: u32,
203    pub active_streams: u32,
204    /// Six MAC bytes followed by the six documented reserved bytes.
205    pub mac_address_and_padding: [u8; 12],
206    pub max_conferences: u32,
207    pub active_conferences: u32,
208    /// Address-scope word associated with the reported IPv4 address.
209    pub ipv4_address_scope: u32,
210    pub max_lines: u32,
211    /// Address-scope word associated with the reported IPv6 address.
212    pub ipv6_address_scope: u32,
213}
214
215#[derive(Clone, Debug, Eq, PartialEq)]
216/// One audio codec capability advertised by a station.
217pub struct MediaCapability {
218    pub codec: Codec,
219    pub max_frames_per_packet: u32,
220    /// Fixed codec-specific parameter area retained byte-for-byte.
221    pub codec_parameters: [u8; 8],
222}
223
224pub const MEDIA_PORT_LIST_MAX_PORTS: usize = 16;
225
226#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct MediaPortList {
228    pub rtp_ports: Vec<u16>,
229}
230
231/// SRTP keying material. Debug output intentionally exposes metadata only.
232#[derive(Clone, Eq, PartialEq)]
233pub struct MediaEncryption {
234    pub algorithm: EncryptionMethod,
235    key: [u8; 16],
236    key_length: u8,
237    salt: [u8; 16],
238    salt_length: u8,
239    /// Non-zero when the media packet carries a master-key identifier.
240    pub mki_present: u32,
241    /// SRTP key-derivation rate word.
242    pub key_derivation_rate: u32,
243}
244
245impl MediaEncryption {
246    /// Copies validated SRTP keying material into redacted, zeroizing storage.
247    ///
248    /// Keys and salts are independently limited to 16 bytes.
249    pub fn new(
250        algorithm: EncryptionMethod,
251        key: &[u8],
252        salt: &[u8],
253        mki_present: u32,
254        key_derivation_rate: u32,
255    ) -> Result<Self, CodecError> {
256        if key.len() > 16 {
257            return Err(CodecError::SecretTooLong {
258                field: "media encryption key",
259                actual: key.len(),
260                maximum: 16,
261            });
262        }
263        if salt.len() > 16 {
264            return Err(CodecError::SecretTooLong {
265                field: "media encryption salt",
266                actual: salt.len(),
267                maximum: 16,
268            });
269        }
270        let mut wire_key = [0; 16];
271        wire_key[..key.len()].copy_from_slice(key);
272        let mut wire_salt = [0; 16];
273        wire_salt[..salt.len()].copy_from_slice(salt);
274        Ok(Self {
275            algorithm,
276            key: wire_key,
277            key_length: key.len() as u8,
278            salt: wire_salt,
279            salt_length: salt.len() as u8,
280            mki_present,
281            key_derivation_rate,
282        })
283    }
284
285    pub(crate) const fn from_wire(
286        algorithm: EncryptionMethod,
287        key: [u8; 16],
288        key_length: u8,
289        salt: [u8; 16],
290        salt_length: u8,
291        mki_present: u32,
292        key_derivation_rate: u32,
293    ) -> Self {
294        Self {
295            algorithm,
296            key,
297            key_length,
298            salt,
299            salt_length,
300            mki_present,
301            key_derivation_rate,
302        }
303    }
304
305    pub fn key(&self) -> &[u8] {
306        &self.key[..usize::from(self.key_length)]
307    }
308
309    pub fn salt(&self) -> &[u8] {
310        &self.salt[..usize::from(self.salt_length)]
311    }
312}
313
314impl fmt::Debug for MediaEncryption {
315    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316        formatter
317            .debug_struct("MediaEncryption")
318            .field("algorithm", &self.algorithm)
319            .field("key", &"<redacted>")
320            .field("key_len", &self.key_length)
321            .field("salt", &"<redacted>")
322            .field("salt_len", &self.salt_length)
323            .field("mki_present", &self.mki_present)
324            .field("key_derivation_rate", &self.key_derivation_rate)
325            .finish()
326    }
327}
328
329impl Drop for MediaEncryption {
330    fn drop(&mut self) {
331        self.key.fill(0);
332        self.salt.fill(0);
333    }
334}
335
336/// One locale-aware tone in a station announcement sequence.
337#[derive(Clone, Copy, Debug, Eq, PartialEq)]
338pub struct AnnouncementEntry {
339    pub locale: u32,
340    pub country: u32,
341    pub tone: Tone,
342}
343
344/// Parameters and application data for creating a station-managed conference.
345#[derive(Clone, Debug, Eq, PartialEq)]
346pub struct CreateConferenceRequest {
347    pub conference_id: ConferenceId,
348    pub reserved_participants: u32,
349    pub resource_type: ConferenceResourceType,
350    pub application_id: ApplicationId,
351    pub application_conference_id: String,
352    pub application_data: String,
353    pub passthrough_data: Vec<u8>,
354}
355
356#[derive(Clone, Debug, Eq, PartialEq)]
357/// Result and returned application bytes for conference creation.
358pub struct CreateConferenceResponse {
359    pub conference_id: ConferenceId,
360    pub result: CreateConferenceResult,
361    pub passthrough_data: Vec<u8>,
362}
363
364/// Parameters and application data for resizing or updating a conference.
365#[derive(Clone, Debug, Eq, PartialEq)]
366pub struct ModifyConferenceRequest {
367    pub conference_id: ConferenceId,
368    pub reserved_participants: u32,
369    pub application_id: ApplicationId,
370    pub application_conference_id: String,
371    pub application_data: String,
372    pub passthrough_data: Vec<u8>,
373}
374
375#[derive(Clone, Debug, Eq, PartialEq)]
376/// Result and returned application bytes for conference modification.
377pub struct ModifyConferenceResponse {
378    pub conference_id: ConferenceId,
379    pub result: ModifyConferenceResult,
380    pub passthrough_data: Vec<u8>,
381}
382
383#[derive(Clone, Debug, Eq, PartialEq)]
384/// One conference record returned by an audit operation.
385pub struct AuditConferenceEntry {
386    pub conference_id: ConferenceId,
387    pub resource_type: ConferenceResourceType,
388    pub reserved_participants: u32,
389    pub active_participants: u32,
390    pub application_id: ApplicationId,
391    pub application_conference_id: String,
392    pub application_data: String,
393}
394
395#[derive(Clone, Debug, Eq, PartialEq)]
396/// A page of conference audit records.
397pub struct AuditConferenceResponse {
398    /// Non-zero when this page is the final audit response.
399    pub last: u32,
400    pub entries: Vec<AuditConferenceEntry>,
401}
402
403#[derive(Clone, Debug, Eq, PartialEq)]
404/// Presentation identity and call reference for a conference participant.
405pub struct ConferenceParticipant {
406    pub call_reference: CallReference,
407    pub presentation_restrictions: PartyInformationRestrictions,
408    pub name: String,
409    pub number: String,
410    pub conference_name: String,
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
414/// Request to attach a call participant to a conference.
415pub struct AddParticipantRequest {
416    pub conference_id: ConferenceId,
417    pub participant: ConferenceParticipant,
418}
419
420/// Update the presentation identity of an existing conference participant.
421///
422/// This is the standalone intra-control `0x013e` request. It intentionally
423/// shares the participant layout with [`AddParticipantRequest`].
424#[derive(Clone, Debug, Eq, PartialEq)]
425pub struct ChangeParticipantRequest {
426    pub conference_id: ConferenceId,
427    pub participant: ConferenceParticipant,
428}
429
430#[derive(Clone, Debug, Eq, PartialEq)]
431/// Result of adding a participant, including the service-assigned identity.
432pub struct AddParticipantResponse {
433    pub conference_id: ConferenceId,
434    pub call_reference: CallReference,
435    pub result: AddParticipantResult,
436    /// Opaque service-assigned participant identity, bounded to its wire field.
437    pub bridge_participant_id: BoundedBytes<257>,
438}
439
440/// Participant audit entry bytes have an opaque schema. The typed envelope
441/// preserves them losslessly while enforcing the aggregate wire bound.
442#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct AuditParticipantResponse {
444    pub result: AuditParticipantResult,
445    pub last: u32,
446    pub conference_id: ConferenceId,
447    /// Declared entry count retained separately from the opaque entry bytes.
448    pub number_of_entries: u32,
449    /// Opaque participant records retained in their received order.
450    pub participant_entries: Vec<u8>,
451}
452
453/// Routing metadata for a participant change carried by the V1 application
454/// envelope rather than a standalone station message identifier.
455#[derive(Clone, Copy, Debug, Eq, PartialEq)]
456pub struct ParticipantChangeRouting {
457    pub application_id: ApplicationId,
458    pub line_instance: u32,
459    pub transaction_id: TransactionId,
460    pub sequence_flag: u32,
461    pub display_priority: u32,
462    pub application_instance_id: ApplicationId,
463    pub routing: u32,
464}
465
466#[derive(Clone, Debug, Eq, PartialEq)]
467/// A participant-identity change independent of application-envelope routing.
468pub struct ConferenceParticipantChange {
469    pub conference_id: ConferenceId,
470    pub participant: ConferenceParticipant,
471}
472
473#[derive(Clone, Debug, Eq, PartialEq)]
474/// Parameters for receiving an audio stream from a multicast endpoint.
475pub struct MulticastMediaReception {
476    pub conference_id: ConferenceId,
477    pub passthrough_party_id: crate::types::PassthroughPartyId,
478    pub call_reference: CallReference,
479    pub address: IpAddr,
480    pub port: u16,
481    pub packet_millis: u32,
482    pub codec: Codec,
483    pub echo_cancellation: EchoCancellation,
484    pub g723_bitrate: G723BitRate,
485}
486
487#[derive(Clone, Debug, Eq, PartialEq)]
488/// Parameters for transmitting an audio stream to a multicast endpoint.
489pub struct MulticastMediaTransmission {
490    pub conference_id: ConferenceId,
491    pub passthrough_party_id: crate::types::PassthroughPartyId,
492    pub call_reference: CallReference,
493    pub address: IpAddr,
494    pub port: u16,
495    pub packet_millis: u32,
496    pub codec: Codec,
497    pub precedence: u32,
498    pub silence_suppression: u32,
499    pub max_frames_per_packet: u32,
500    pub g723_bitrate: G723BitRate,
501}
502
503#[derive(Clone, Debug, Eq, PartialEq)]
504/// A cataloged but untyped message retained for explicit bounded forwarding.
505pub struct KnownOpaqueMessage {
506    pub id: MessageId,
507    pub protocol_version: u32,
508    pub payload: BoundedBytes<MAX_OPAQUE_MESSAGE_BYTES>,
509}
510
511#[derive(Clone, Debug, Eq, PartialEq)]
512/// Original application-data envelope with routing identifiers and opaque data.
513pub struct UserDataMessage {
514    pub application_id: u32,
515    pub line_instance: u32,
516    pub call_reference: u32,
517    pub transaction_id: u32,
518    pub data: Vec<u8>,
519}
520
521/// The extended XML/application-data envelope introduced after SCCP v3.
522#[derive(Clone, Debug, Eq, PartialEq)]
523pub struct UserDataV1Message {
524    pub application_id: u32,
525    pub line_instance: u32,
526    pub call_reference: u32,
527    pub transaction_id: u32,
528    pub sequence_flag: u32,
529    pub display_priority: u32,
530    pub conference_id: u32,
531    pub application_instance_id: u32,
532    pub routing: u32,
533    pub data: Vec<u8>,
534}
535
536#[derive(Clone, Debug, Eq, PartialEq)]
537/// Station token-registration identity and network endpoint.
538pub struct RegisterTokenMessage {
539    pub device_id: DeviceId,
540    pub device_instance: u32,
541    pub address: IpAddr,
542    pub device_type: DeviceType,
543    /// Firmware flags whose meaning is not fully documented.
544    pub flags: u32,
545}
546
547#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct SpcpRegisterTokenMessage {
549    pub device_id: DeviceId,
550    pub device_instance: u32,
551    pub address: Ipv4Addr,
552    pub device_type: DeviceType,
553    pub max_streams: u32,
554}
555
556/// Maximum endpoints carried by one station server-list response.
557pub const MAX_SIGNALING_SERVERS: usize = 5;
558
559/// One reachable control endpoint in a station server-list response.
560#[derive(Clone, Debug, Eq, PartialEq)]
561pub struct SignalingServerEndpoint {
562    pub name: String,
563    pub address: IpAddr,
564    pub port: NonZeroU16,
565}
566
567#[derive(Clone, Debug, Eq, PartialEq)]
568/// Media-resource service capacity notification.
569pub struct MediaResourceNotification {
570    pub device_type: DeviceType,
571    pub in_service_streams: u32,
572    pub max_streams_per_conference: u32,
573    pub out_of_service_streams: u32,
574}
575
576#[derive(Clone, Debug, Eq, PartialEq)]
577/// Request to create or renew a feature subscription.
578pub struct SubscriptionRequest {
579    pub transaction_id: u32,
580    pub feature_id: u32,
581    pub timer_seconds: u32,
582    pub subscription_id: String,
583}
584
585#[derive(Clone, Debug, Eq, PartialEq)]
586/// Allocated RTP/RTCP endpoint returned for a media flow.
587pub struct PortEndpoint {
588    pub conference_id: u32,
589    pub call_reference: u32,
590    pub passthrough_party_id: u32,
591    pub address: IpAddr,
592    pub rtp_port: u16,
593    pub rtcp_port: u16,
594    pub media_type: Option<MediaType>,
595}
596
597#[derive(Clone, Copy, Debug, Eq, PartialEq)]
598/// Request to allocate an endpoint for one media flow.
599pub struct PortRequest {
600    pub conference_id: ConferenceId,
601    pub call_reference: CallReference,
602    pub passthrough_party_id: crate::types::PassthroughPartyId,
603    pub transport: MediaTransport,
604    pub address_type: Option<IpAddressType>,
605    pub media_type: Option<MediaType>,
606}
607
608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
609/// Request to release a previously allocated media endpoint.
610pub struct PortClose {
611    pub conference_id: ConferenceId,
612    pub call_reference: CallReference,
613    pub passthrough_party_id: crate::types::PassthroughPartyId,
614    pub media_type: Option<MediaType>,
615}
616
617/// Addressed media flow used by the intra-control QoS message family.
618#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
619pub struct QosFlow {
620    pub conference_id: ConferenceId,
621    pub call_reference: CallReference,
622    pub passthrough_party_id: crate::types::PassthroughPartyId,
623    pub address: Ipv4Addr,
624    pub port: u16,
625}
626
627/// RSVP traffic parameters.
628///
629/// The codec identifier remains forward-compatible through [`Codec::Unknown`];
630/// the rate and burst values are protocol quantities rather than closed enums.
631#[derive(Clone, Copy, Debug, Eq, PartialEq)]
632pub struct QosTrafficSpecification {
633    pub codec: Codec,
634    pub average_bit_rate: u32,
635    pub burst_size: u32,
636    pub peak_rate: u32,
637}
638
639/// Fixed application identity carried by QoS listen/path/modify requests.
640#[derive(Clone, Debug, Eq, PartialEq)]
641pub struct QosApplicationIdentifier {
642    pub vendor_id: String,
643    pub version: String,
644    pub application_name: String,
645    pub sub_application_id: String,
646}
647
648#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
649/// New and previously heard message counts for one mailbox category.
650pub struct MessageWaitingCounts {
651    pub new: u32,
652    pub old: u32,
653}
654
655#[derive(Clone, Debug, Eq, PartialEq)]
656/// Message-waiting state and category counts for one target number.
657pub struct MessageWaitingNotification {
658    pub target_number: String,
659    pub control_number: String,
660    pub messages_waiting: bool,
661    pub total_voicemail: MessageWaitingCounts,
662    pub priority_voicemail: MessageWaitingCounts,
663    pub total_fax: MessageWaitingCounts,
664    pub priority_fax: MessageWaitingCounts,
665}
666
667#[derive(Clone, Copy, Debug, Eq, PartialEq)]
668/// Station acknowledgement for an opened multimedia receive channel.
669pub struct OpenMultimediaReceiveChannelAck {
670    pub status: MediaStatus,
671    pub endpoint: MediaEndpointAddress,
672    pub passthrough_party_id: crate::types::PassthroughPartyId,
673    pub call_reference: CallReference,
674}
675
676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
677/// Station acknowledgement for a multimedia transmit request.
678pub struct StartMultimediaTransmissionAck {
679    pub conference_id: ConferenceId,
680    pub passthrough_party_id: crate::types::PassthroughPartyId,
681    pub call_reference: CallReference,
682    pub endpoint: MediaEndpointAddress,
683    pub status: MediaStatus,
684}
685
686#[derive(Clone, Copy, Debug, Eq, PartialEq)]
687/// Network address and transport port for a media endpoint.
688pub struct MediaEndpointAddress {
689    pub address: IpAddr,
690    pub port: u16,
691}
692
693#[derive(Clone, Copy, Debug, Eq, PartialEq)]
694/// Identity fields shared by multimedia close and stop commands.
695pub struct MultimediaStreamControl {
696    pub conference_id: ConferenceId,
697    pub passthrough_party_id: crate::types::PassthroughPartyId,
698    pub call_reference: CallReference,
699    pub port_handling_flag: u32,
700}
701
702#[derive(Clone, Copy, Debug, Eq, PartialEq)]
703/// Identity fields shared by audio receive-close and transmit-stop commands.
704pub struct AudioStreamControl {
705    pub conference_id: ConferenceId,
706    pub passthrough_party_id: crate::types::PassthroughPartyId,
707    pub call_reference: CallReference,
708    pub port_handling_flag: u32,
709}
710
711#[derive(Clone, Copy, Debug, Eq, PartialEq)]
712/// Remote address and type for starting or stopping a control session.
713pub struct SessionTransmission {
714    pub remote_address: IpAddr,
715    pub session_type: u32,
716}
717
718/// Seven-bit RTP payload number used by a multimedia stream.
719#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
720pub struct RtpPayloadNumber(u8);
721
722impl RtpPayloadNumber {
723    pub const MAX: u32 = 127;
724
725    pub const fn new(value: u32) -> Result<Self, RtpPayloadNumberError> {
726        if value <= Self::MAX {
727            Ok(Self(value as u8))
728        } else {
729            Err(RtpPayloadNumberError { actual: value })
730        }
731    }
732
733    pub const fn get(self) -> u8 {
734        self.0
735    }
736}
737
738impl TryFrom<u32> for RtpPayloadNumber {
739    type Error = RtpPayloadNumberError;
740
741    fn try_from(value: u32) -> Result<Self, Self::Error> {
742        Self::new(value)
743    }
744}
745
746impl From<RtpPayloadNumber> for u32 {
747    fn from(value: RtpPayloadNumber) -> Self {
748        u32::from(value.get())
749    }
750}
751
752/// Failure returned when a value is outside the RTP payload-number range.
753#[derive(Clone, Copy, Debug, Eq, PartialEq)]
754pub struct RtpPayloadNumberError {
755    pub actual: u32,
756}
757
758impl fmt::Display for RtpPayloadNumberError {
759    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
760        write!(
761            formatter,
762            "RTP payload number {} exceeds {}",
763            self.actual,
764            RtpPayloadNumber::MAX
765        )
766    }
767}
768
769impl std::error::Error for RtpPayloadNumberError {}
770
771/// Two-word multimedia RTP descriptor.
772#[derive(Clone, Copy, Debug, Eq, PartialEq)]
773pub struct MultimediaPayloadDescriptor {
774    rfc_number: u32,
775    payload_number: RtpPayloadNumber,
776}
777
778impl MultimediaPayloadDescriptor {
779    /// Retains the packetization-format flags independently from the RTP payload number.
780    pub const fn new(rfc_number: u32, payload_number: RtpPayloadNumber) -> Self {
781        Self {
782            rfc_number,
783            payload_number,
784        }
785    }
786
787    /// Returns the preserved first descriptor word.
788    pub const fn rfc_number(self) -> u32 {
789        self.rfc_number
790    }
791
792    pub const fn payload_number(self) -> RtpPayloadNumber {
793        self.payload_number
794    }
795}
796
797/// One supported picture format and its minimum picture interval.
798#[derive(Clone, Copy, Debug, Eq, PartialEq)]
799pub struct MultimediaPictureFormat {
800    pub format: VideoFormat,
801    pub minimum_picture_interval: u32,
802}
803
804/// Codec-selected arm of a multimedia video capability.
805#[derive(Clone, Copy, Debug, Eq, PartialEq)]
806pub enum MultimediaVideoCapabilityArm {
807    H261 {
808        temporal_spatial_trade_off_capability: u32,
809        still_image_transmission: u32,
810    },
811    H263 {
812        capability_bitfield: u32,
813        annex_n_and_w_future_use: u32,
814    },
815    H263Plus {
816        model_number: u32,
817        bandwidth: u32,
818    },
819    H264 {
820        profile: u32,
821        level: u32,
822        custom_max_mbps: u32,
823        custom_max_fs: u32,
824        custom_max_dpb: u32,
825        custom_max_br_and_cpb: u32,
826    },
827}
828
829impl MultimediaVideoCapabilityArm {
830    pub const fn codec(self) -> Codec {
831        match self {
832            Self::H261 { .. } => Codec::H261,
833            Self::H263 { .. } => Codec::H263,
834            Self::H263Plus { .. } => Codec::H263Plus,
835            Self::H264 { .. } => Codec::H264,
836        }
837    }
838}
839
840/// Fully modeled video arm of a multimedia channel command.
841#[derive(Clone)]
842pub struct MultimediaVideoCapability {
843    bit_rate: u32,
844    picture_formats: Box<[MultimediaPictureFormat]>,
845    conference_service_number: u32,
846    arm: MultimediaVideoCapabilityArm,
847    preserved_wire: Option<[u8; MULTIMEDIA_CAPABILITY_BYTES]>,
848}
849
850impl MultimediaVideoCapability {
851    /// Builds a video capability when its picture-format list fits the wire table.
852    pub fn new(
853        bit_rate: u32,
854        picture_formats: impl IntoIterator<Item = MultimediaPictureFormat>,
855        conference_service_number: u32,
856        arm: MultimediaVideoCapabilityArm,
857    ) -> Result<Self, MultimediaCapabilityError> {
858        let picture_formats = picture_formats.into_iter().collect::<Box<[_]>>();
859        if picture_formats.len() > MAX_MULTIMEDIA_PICTURE_FORMATS {
860            return Err(MultimediaCapabilityError {
861                maximum: MAX_MULTIMEDIA_PICTURE_FORMATS,
862                actual: picture_formats.len(),
863            });
864        }
865        Ok(Self {
866            bit_rate,
867            picture_formats,
868            conference_service_number,
869            arm,
870            preserved_wire: None,
871        })
872    }
873
874    pub const fn bit_rate(&self) -> u32 {
875        self.bit_rate
876    }
877
878    pub fn picture_formats(&self) -> &[MultimediaPictureFormat] {
879        &self.picture_formats
880    }
881
882    pub const fn conference_service_number(&self) -> u32 {
883        self.conference_service_number
884    }
885
886    pub const fn arm(&self) -> MultimediaVideoCapabilityArm {
887        self.arm
888    }
889
890    pub const fn codec(&self) -> Codec {
891        self.arm.codec()
892    }
893}
894
895impl fmt::Debug for MultimediaVideoCapability {
896    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
897        formatter
898            .debug_struct("MultimediaVideoCapability")
899            .field("bit_rate", &self.bit_rate)
900            .field("picture_formats", &self.picture_formats)
901            .field("conference_service_number", &self.conference_service_number)
902            .field("arm", &self.arm)
903            .finish()
904    }
905}
906
907impl PartialEq for MultimediaVideoCapability {
908    fn eq(&self, other: &Self) -> bool {
909        self.bit_rate == other.bit_rate
910            && self.picture_formats == other.picture_formats
911            && self.conference_service_number == other.conference_service_number
912            && self.arm == other.arm
913            && self.preserved_wire == other.preserved_wire
914    }
915}
916
917impl Eq for MultimediaVideoCapability {}
918
919#[derive(Clone, Copy, Debug, Eq, PartialEq)]
920/// Failure returned when a video capability exceeds a fixed table bound.
921pub struct MultimediaCapabilityError {
922    pub maximum: usize,
923    pub actual: usize,
924}
925
926impl fmt::Display for MultimediaCapabilityError {
927    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
928        write!(
929            formatter,
930            "video capability contains {} picture formats, exceeding the maximum of {}",
931            self.actual, self.maximum
932        )
933    }
934}
935
936impl std::error::Error for MultimediaCapabilityError {}
937
938#[derive(Clone, Copy, Debug, Eq, PartialEq)]
939pub(crate) enum MultimediaPayloadDirection {
940    Receive,
941    Transmit,
942}
943
944#[derive(Clone, Eq, PartialEq)]
945enum MultimediaCapabilityState {
946    Video(MultimediaVideoCapability),
947    Preserved([u8; MULTIMEDIA_CAPABILITY_BYTES]),
948}
949
950#[derive(Clone, Copy, Debug, Eq, PartialEq)]
951enum MultimediaPayloadOrigin {
952    Constructed,
953    Decoded {
954        direction: MultimediaPayloadDirection,
955        protocol: ProtocolVersion,
956        compression_codec: Codec,
957    },
958}
959
960/// RTP descriptor and codec-selected capability for a multimedia stream.
961#[derive(Clone)]
962pub struct MultimediaPayload {
963    descriptor: MultimediaPayloadDescriptor,
964    capability: MultimediaCapabilityState,
965    origin: MultimediaPayloadOrigin,
966}
967
968impl MultimediaPayload {
969    /// Constructs an outbound payload using the capability arm as its codec selector.
970    pub fn new(payload_number: RtpPayloadNumber, capability: MultimediaVideoCapability) -> Self {
971        Self::with_descriptor(
972            MultimediaPayloadDescriptor::new(0, payload_number),
973            capability,
974        )
975    }
976
977    /// Constructs a payload with explicit packetization-format flags.
978    pub fn with_descriptor(
979        descriptor: MultimediaPayloadDescriptor,
980        capability: MultimediaVideoCapability,
981    ) -> Self {
982        Self {
983            descriptor,
984            capability: MultimediaCapabilityState::Video(capability),
985            origin: MultimediaPayloadOrigin::Constructed,
986        }
987    }
988
989    const fn from_decoded(
990        descriptor: MultimediaPayloadDescriptor,
991        capability: MultimediaCapabilityState,
992        direction: MultimediaPayloadDirection,
993        protocol: ProtocolVersion,
994        compression_codec: Codec,
995    ) -> Self {
996        Self {
997            descriptor,
998            capability,
999            origin: MultimediaPayloadOrigin::Decoded {
1000                direction,
1001                protocol,
1002                compression_codec,
1003            },
1004        }
1005    }
1006
1007    #[cfg(test)]
1008    pub(crate) fn from_wire(
1009        rfc_number: u32,
1010        payload_number: RtpPayloadNumber,
1011        capability: [u8; MULTIMEDIA_CAPABILITY_BYTES],
1012        codec: Codec,
1013        direction: MultimediaPayloadDirection,
1014        protocol: ProtocolVersion,
1015    ) -> Self {
1016        Self::from_decoded(
1017            MultimediaPayloadDescriptor::new(rfc_number, payload_number),
1018            MultimediaCapabilityState::Preserved(capability),
1019            direction,
1020            protocol,
1021            codec,
1022        )
1023    }
1024
1025    pub const fn descriptor(&self) -> MultimediaPayloadDescriptor {
1026        self.descriptor
1027    }
1028
1029    pub const fn codec(&self) -> Codec {
1030        self.compression_codec()
1031    }
1032
1033    pub const fn payload_number(&self) -> RtpPayloadNumber {
1034        self.descriptor.payload_number()
1035    }
1036
1037    /// Returns `None` for a decoded codec arm without a structured model.
1038    pub const fn video_capability(&self) -> Option<&MultimediaVideoCapability> {
1039        match &self.capability {
1040            MultimediaCapabilityState::Video(capability) => Some(capability),
1041            MultimediaCapabilityState::Preserved(_) => None,
1042        }
1043    }
1044
1045    pub(crate) fn is_valid_for(
1046        &self,
1047        direction: MultimediaPayloadDirection,
1048        protocol: ProtocolVersion,
1049    ) -> bool {
1050        match self.origin {
1051            MultimediaPayloadOrigin::Constructed => true,
1052            MultimediaPayloadOrigin::Decoded {
1053                direction: decoded_direction,
1054                protocol: decoded_protocol,
1055                ..
1056            } => decoded_direction == direction && decoded_protocol.wire() == protocol.wire(),
1057        }
1058    }
1059
1060    pub(crate) fn is_direction(&self, direction: MultimediaPayloadDirection) -> bool {
1061        match self.origin {
1062            MultimediaPayloadOrigin::Constructed => true,
1063            MultimediaPayloadOrigin::Decoded {
1064                direction: decoded_direction,
1065                ..
1066            } => decoded_direction == direction,
1067        }
1068    }
1069
1070    pub(crate) const fn compression_codec(&self) -> Codec {
1071        match self.origin {
1072            MultimediaPayloadOrigin::Constructed => match &self.capability {
1073                MultimediaCapabilityState::Video(capability) => capability.codec(),
1074                MultimediaCapabilityState::Preserved(_) => unreachable!(),
1075            },
1076            MultimediaPayloadOrigin::Decoded {
1077                compression_codec, ..
1078            } => compression_codec,
1079        }
1080    }
1081}
1082
1083impl PartialEq for MultimediaPayload {
1084    fn eq(&self, other: &Self) -> bool {
1085        self.descriptor == other.descriptor
1086            && self.capability == other.capability
1087            && self.origin == other.origin
1088    }
1089}
1090
1091impl Eq for MultimediaPayload {}
1092
1093impl fmt::Debug for MultimediaPayload {
1094    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1095        formatter
1096            .debug_struct("MultimediaPayload")
1097            .field("descriptor", &self.descriptor)
1098            .field("codec", &self.codec())
1099            .field("video_capability", &self.video_capability())
1100            .finish()
1101    }
1102}
1103
1104#[derive(Clone, Debug, Eq, PartialEq)]
1105/// Request to open a station multimedia receive channel.
1106pub struct OpenMultimediaChannel {
1107    pub conference_id: ConferenceId,
1108    pub passthrough_party_id: crate::types::PassthroughPartyId,
1109    pub line_instance: u32,
1110    pub call_reference: CallReference,
1111    pub payload: MultimediaPayload,
1112    pub conference_creator: bool,
1113    /// Optional SRTP parameters carried by extended layouts.
1114    pub encryption: Option<MediaEncryption>,
1115    /// Identity for this media stream within the conference.
1116    pub stream_passthrough_id: u32,
1117    /// Related stream identity, or zero when the stream is independent.
1118    pub associated_stream_id: u32,
1119    pub source: MediaEndpointAddress,
1120    pub requested_address_type: IpAddressType,
1121}
1122
1123#[derive(Clone, Debug, Eq, PartialEq)]
1124/// Request to transmit a multimedia stream to a remote endpoint.
1125pub struct StartMultimediaTransmission {
1126    pub conference_id: ConferenceId,
1127    pub passthrough_party_id: crate::types::PassthroughPartyId,
1128    pub endpoint: MediaEndpointAddress,
1129    pub call_reference: CallReference,
1130    pub payload: MultimediaPayload,
1131    pub traffic_class: crate::types::MediaTrafficClass,
1132    /// Optional SRTP parameters carried by extended layouts.
1133    pub encryption: Option<MediaEncryption>,
1134    /// Identity for this media stream within the conference.
1135    pub stream_passthrough_id: u32,
1136    /// Related stream identity, or zero when the stream is independent.
1137    pub associated_stream_id: u32,
1138}
1139
1140#[derive(Clone, Debug, Eq, PartialEq)]
1141/// Codec-specific multimedia command and its bounded parameter block.
1142pub struct MiscellaneousCommand {
1143    pub conference_id: ConferenceId,
1144    pub passthrough_party_id: crate::types::PassthroughPartyId,
1145    pub call_reference: CallReference,
1146    pub command: values::MiscCommandType,
1147    /// Command-specific bytes bounded by the fixed parameter area.
1148    pub data: BoundedBytes<36>,
1149}
1150
1151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1152/// Maximum-bit-rate update for one video stream.
1153pub struct VideoFlowControl {
1154    pub conference_id: ConferenceId,
1155    pub passthrough_party_id: crate::types::PassthroughPartyId,
1156    pub call_reference: CallReference,
1157    pub maximum_bit_rate: u32,
1158}
1159
1160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1161/// One signaling DTMF tone associated with a conference media party.
1162pub struct DtmfToneControl {
1163    pub tone: Tone,
1164    pub conference_id: ConferenceId,
1165    pub passthrough_party_id: u32,
1166}
1167
1168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1169/// Identity returned by a DTMF payload subscribe/unsubscribe operation.
1170pub struct DtmfPayloadIdentity {
1171    /// RTP payload-type word assigned to telephone-event packets.
1172    pub payload_type: u32,
1173    pub conference_id: u32,
1174    pub passthrough_party_id: u32,
1175}
1176
1177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1178/// Request to subscribe or unsubscribe a DTMF RTP payload mapping.
1179pub struct DtmfPayloadRequest {
1180    /// Requested RTP payload-type word for telephone-event packets.
1181    pub payload_type: u32,
1182    pub conference_id: u32,
1183    pub passthrough_party_id: u32,
1184    /// Numeric DTMF transport selector retained from the wire.
1185    pub dtmf_type: u32,
1186}
1187
1188/// Maximum inbound XML-alarm payload retained by the decoder.
1189pub const XML_ALARM_MAX_WIRE_BYTES: usize = 2_048;
1190/// Deterministic payload size emitted by [`XmlAlarmMessage::from_xml`].
1191pub const XML_ALARM_CANONICAL_WIRE_BYTES: usize = 2_004;
1192/// Maximum XML document size accepted by [`XmlAlarmMessage::from_xml`].
1193pub const XML_ALARM_CANONICAL_DOCUMENT_BYTES: usize = 2_000;
1194
1195#[derive(Clone, Debug, Eq, PartialEq)]
1196/// Bounded XML alarm with exact inbound wire-payload preservation.
1197///
1198/// [`Self::from_xml`] constructs the canonical zero-padded outbound form;
1199/// [`Self::from_wire_payload`] retains any accepted framed form byte-for-byte.
1200pub struct XmlAlarmMessage {
1201    wire_payload: BoundedBytes<XML_ALARM_MAX_WIRE_BYTES>,
1202}
1203
1204impl XmlAlarmMessage {
1205    /// Builds the canonical outbound alarm payload from a NUL-free XML document.
1206    pub fn from_xml(xml: impl AsRef<[u8]>) -> Result<Self, CodecError> {
1207        let xml = xml.as_ref();
1208        if xml.contains(&0) {
1209            return Err(CodecError::InvalidText);
1210        }
1211        if xml.len() > XML_ALARM_CANONICAL_DOCUMENT_BYTES {
1212            return Err(CodecError::TextTooLong {
1213                message_id: wire_id::XML_ALARM,
1214                field: "alarm XML",
1215                actual: xml.len(),
1216                maximum: XML_ALARM_CANONICAL_DOCUMENT_BYTES,
1217            });
1218        }
1219        let mut wire_payload = vec![0; XML_ALARM_CANONICAL_WIRE_BYTES];
1220        wire_payload[..xml.len()].copy_from_slice(xml);
1221        Self::from_wire_payload(wire_payload)
1222    }
1223
1224    /// Retains an inbound alarm payload without requiring a canonical length.
1225    pub fn from_wire_payload(payload: impl Into<Box<[u8]>>) -> Result<Self, CodecError> {
1226        let payload = payload.into();
1227        let wire_payload =
1228            BoundedBytes::new(payload).map_err(|error| CodecError::CountTooLarge {
1229                message_id: wire_id::XML_ALARM,
1230                field: "alarm payload",
1231                count: error.actual,
1232                maximum: error.maximum,
1233            })?;
1234        Ok(Self { wire_payload })
1235    }
1236
1237    /// Returns the XML bytes through the first NUL, or the full payload if none exists.
1238    pub fn xml_bytes(&self) -> &[u8] {
1239        let bytes = self.wire_payload.as_bytes();
1240        let end = bytes
1241            .iter()
1242            .position(|byte| *byte == 0)
1243            .unwrap_or(bytes.len());
1244        &bytes[..end]
1245    }
1246
1247    /// Returns the complete retained payload, including terminator and padding bytes.
1248    pub fn wire_payload(&self) -> &[u8] {
1249        self.wire_payload.as_bytes()
1250    }
1251}
1252
1253/// Audio media-failure detector configuration.
1254///
1255/// The final four qualifier bytes are either a G.723 rate word or four
1256/// codec-specific bytes, depending on protocol version and codec. Keeping
1257/// them raw makes that union lossless without inventing a universal meaning.
1258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1259pub struct MediaFailureDetection {
1260    pub conference_id: ConferenceId,
1261    pub passthrough_party_id: u32,
1262    pub packet_millis: u32,
1263    pub codec: Codec,
1264    pub echo_cancellation: EchoCancellation,
1265    pub codec_qualifier: [u8; 4],
1266    pub call_reference: CallReference,
1267}
1268
1269/// All three integers and the text buffer have unknown semantics, so the typed
1270/// model preserves each value without assigning invented meaning.
1271#[derive(Clone, Debug, Eq, PartialEq)]
1272pub struct ExtensionDeviceCapabilities {
1273    pub unknown_1: u32,
1274    pub unknown_2: u32,
1275    pub unknown_3: u32,
1276    pub description: String,
1277}
1278
1279#[derive(Clone, Debug, Eq, PartialEq)]
1280/// Static station and user information returned by a configuration request.
1281pub struct ConfigurationStatus {
1282    pub device_name: String,
1283    pub station_user_id: u32,
1284    pub station_instance: u32,
1285    pub line_count: u32,
1286    pub speed_dial_count: u32,
1287    pub user_name: String,
1288    pub server_name: String,
1289}
1290
1291/// Messages exchanged with conference/media-resource/call-control peers.
1292///
1293/// These IDs share the SCCP frame header with station traffic, but they are
1294/// not legal inputs to [`ClientMessage`] or outputs from [`ServerMessage`].
1295#[derive(Clone, Debug, Eq, PartialEq)]
1296pub enum ControlMessage {
1297    MediaResourceNotification(MediaResourceNotification),
1298    PortResponse(PortEndpoint),
1299    StartSessionTransmission(SessionTransmission),
1300    StopSessionTransmission(SessionTransmission),
1301    ClearConference {
1302        conference_id: ConferenceId,
1303        service_number: u32,
1304    },
1305    CreateConferenceRequest(CreateConferenceRequest),
1306    DeleteConferenceRequest {
1307        conference_id: ConferenceId,
1308    },
1309    ModifyConferenceRequest(ModifyConferenceRequest),
1310    AddParticipantRequest(AddParticipantRequest),
1311    DropParticipantRequest {
1312        conference_id: ConferenceId,
1313        call_reference: CallReference,
1314    },
1315    AuditConferenceRequest,
1316    AuditParticipantRequest {
1317        conference_id: ConferenceId,
1318    },
1319    ChangeParticipantRequest(ChangeParticipantRequest),
1320    CreateConferenceResponse(CreateConferenceResponse),
1321    DeleteConferenceResponse {
1322        conference_id: ConferenceId,
1323        result: DeleteConferenceResult,
1324    },
1325    ModifyConferenceResponse(ModifyConferenceResponse),
1326    AddParticipantResponse(AddParticipantResponse),
1327    AuditConferenceResponse(AuditConferenceResponse),
1328    AuditParticipantResponse(AuditParticipantResponse),
1329    /// Plays a bounded sequence of locale-aware tones for conference parties.
1330    StartAnnouncement {
1331        announcements: Vec<AnnouncementEntry>,
1332        /// Whether completion requires a protocol acknowledgement.
1333        end_of_ack: EndOfAnnouncementAck,
1334        conference_id: u32,
1335        /// Party identifiers participating in the announcement matrix.
1336        matrix_conference_party_ids: Vec<u32>,
1337        /// Bit mask selecting which matrix parties hear the announcement.
1338        hearing_conference_party_mask: u32,
1339        play_mode: AnnouncementPlayMode,
1340    },
1341    StopAnnouncement {
1342        conference_id: u32,
1343    },
1344    AnnouncementFinish {
1345        conference_id: u32,
1346        play_status: AnnouncementPlayStatus,
1347    },
1348    QosReservationNotify {
1349        flow: QosFlow,
1350        direction: QosDirection,
1351    },
1352    /// Reports admission or reservation failure details for a media flow.
1353    QosErrorNotify {
1354        flow: QosFlow,
1355        direction: QosDirection,
1356        error_code: QosErrorCode,
1357        /// Network node that originated the RSVP error.
1358        failure_node: Ipv4Addr,
1359        rsvp_error_code: RsvpErrorCode,
1360        rsvp_error_subcode: u32,
1361        rsvp_error_flags: u32,
1362    },
1363    /// Establishes an RSVP listener and its retry/admission policy.
1364    QosListen {
1365        flow: QosFlow,
1366        reservation_style: QosReservationStyle,
1367        maximum_retries: u32,
1368        retry_timer: u32,
1369        /// Whether the service node must confirm successful reservation.
1370        confirmation_required: bool,
1371        /// Priority used when competing reservations may be preempted.
1372        preemption_priority: u32,
1373        /// Priority used when defending this reservation from preemption.
1374        defending_priority: u32,
1375        traffic: QosTrafficSpecification,
1376        application: QosApplicationIdentifier,
1377    },
1378    /// Establishes the sending side of an RSVP path.
1379    QosPath {
1380        flow: QosFlow,
1381        reservation_style: QosReservationStyle,
1382        maximum_retries: u32,
1383        retry_timer: u32,
1384        preemption_priority: u32,
1385        defending_priority: u32,
1386        traffic: QosTrafficSpecification,
1387        application: QosApplicationIdentifier,
1388    },
1389    /// Tears down QoS state for one direction of a media flow.
1390    QosTeardown {
1391        flow: QosFlow,
1392        direction: QosDirection,
1393    },
1394    /// Updates the six-bit DSCP value for a media flow.
1395    UpdateDscp {
1396        flow: QosFlow,
1397        dscp: u8,
1398    },
1399    /// Changes traffic parameters on an existing QoS reservation.
1400    QosModify {
1401        flow: QosFlow,
1402        direction: QosDirection,
1403        traffic: QosTrafficSpecification,
1404        application: QosApplicationIdentifier,
1405    },
1406    MessageWaitingNotification(MessageWaitingNotification),
1407    MessageWaitingResponse {
1408        target_number: String,
1409        result: MessageWaitingResult,
1410    },
1411    /// A documented role whose payload layout is not independently stable.
1412    KnownOpaque(KnownOpaqueMessage),
1413}
1414
1415/// Maximum retained station quality-statistics payload.
1416pub const CONNECTION_QUALITY_MAX_BYTES: usize = 600;
1417
1418/// Bounded, owned station quality data retained for the typed MED-019 parser.
1419///
1420/// Firmware can place arbitrary text in this field, so diagnostics deliberately
1421/// expose only its length.
1422#[derive(Clone, Eq, PartialEq)]
1423pub struct ConnectionQualityStatistics(Vec<u8>);
1424
1425impl ConnectionQualityStatistics {
1426    /// Retains quality bytes when they fit the protocol allocation bound.
1427    pub fn new(bytes: impl Into<Vec<u8>>) -> Result<Self, CodecError> {
1428        let bytes = bytes.into();
1429        if bytes.len() > CONNECTION_QUALITY_MAX_BYTES {
1430            return Err(CodecError::CountTooLarge {
1431                message_id: wire_id::CONNECTION_STATISTICS_RES,
1432                field: "quality statistics",
1433                count: bytes.len(),
1434                maximum: CONNECTION_QUALITY_MAX_BYTES,
1435            });
1436        }
1437        Ok(Self(bytes))
1438    }
1439
1440    pub fn as_bytes(&self) -> &[u8] {
1441        &self.0
1442    }
1443}
1444
1445impl fmt::Debug for ConnectionQualityStatistics {
1446    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1447        formatter
1448            .debug_struct("ConnectionQualityStatistics")
1449            .field("byte_count", &self.0.len())
1450            .finish()
1451    }
1452}
1453
1454#[derive(Clone, Eq, PartialEq)]
1455/// Packet, octet, timing, and station-provided quality statistics for a call.
1456///
1457/// Debug output redacts the directory number and the nested quality payload.
1458pub struct ConnectionStatistics {
1459    pub directory_number: String,
1460    pub call_reference: u32,
1461    pub processing: StatisticsProcessing,
1462    pub packets_sent: u32,
1463    pub octets_sent: u32,
1464    pub packets_received: u32,
1465    pub octets_received: u32,
1466    pub packets_lost: u32,
1467    /// Inter-arrival jitter in milliseconds.
1468    pub jitter_millis: u32,
1469    /// Reported media latency in milliseconds.
1470    pub latency_millis: u32,
1471    pub quality: ConnectionQualityStatistics,
1472}
1473
1474impl fmt::Debug for ConnectionStatistics {
1475    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1476        formatter
1477            .debug_struct("ConnectionStatistics")
1478            .field("directory_number", &"<redacted>")
1479            .field("call_reference", &self.call_reference)
1480            .field("processing", &self.processing)
1481            .field("packets_sent", &self.packets_sent)
1482            .field("octets_sent", &self.octets_sent)
1483            .field("packets_received", &self.packets_received)
1484            .field("octets_received", &self.octets_received)
1485            .field("packets_lost", &self.packets_lost)
1486            .field("jitter_millis", &self.jitter_millis)
1487            .field("latency_millis", &self.latency_millis)
1488            .field("quality", &self.quality)
1489            .finish()
1490    }
1491}
1492
1493#[derive(Clone, Debug, Eq, PartialEq)]
1494/// Optional eight-byte extension retained from a media-transmission ACK.
1495pub struct MediaTransmissionAckWire {
1496    /// Extension present only in the longer selected ACK layout.
1497    pub extension: Option<[u8; 8]>,
1498}
1499
1500#[derive(Clone, Debug, Eq, PartialEq)]
1501/// Station acknowledgement for an audio media-transmission request.
1502pub struct MediaTransmissionAck {
1503    pub conference_id: u32,
1504    pub passthrough_party_id: u32,
1505    pub call_reference: u32,
1506    pub status: MediaStatus,
1507    pub address: IpAddr,
1508    pub port: u16,
1509    /// Optional layout-specific bytes needed for lossless re-encoding.
1510    pub wire: Option<MediaTransmissionAckWire>,
1511}
1512
1513/// Fields in OpenReceiveChannel which are not part of the runtime media
1514/// abstraction but are required for byte-exact capture round trips.
1515#[derive(Clone, Debug, Eq, PartialEq)]
1516pub struct OpenReceiveChannelWire {
1517    pub conference_id: u32,
1518    /// Codec qualifier word used as the G.723 bit-rate selector when applicable.
1519    pub g723_bitrate: u32,
1520    /// Identity for this media stream within the conference.
1521    pub stream_passthrough_id: u32,
1522    /// Related stream identity, or zero when the stream is independent.
1523    pub associated_stream_id: u32,
1524    /// Numeric DTMF transport selector retained from the wire.
1525    pub dtmf_type: u32,
1526    /// Conference mixer mode retained from the selected layout.
1527    pub mixing_mode: u32,
1528    /// Media-direction word retained from the selected layout.
1529    pub direction: u32,
1530    /// Requested address-family word retained from the selected layout.
1531    pub requested_address_type: u32,
1532    /// Station audio-level adjustment retained from the selected layout.
1533    pub audio_level_adjustment: u32,
1534    /// Fixed latent-capability area retained byte-for-byte.
1535    pub latent_capabilities: [u8; 36],
1536}
1537
1538/// Fields in StartMediaTransmission which are deliberately kept separate
1539/// from the runtime RTP endpoint but must not be discarded by the codec.
1540#[derive(Clone, Debug, Eq, PartialEq)]
1541pub struct StartMediaTransmissionWire {
1542    pub conference_id: u32,
1543    /// Codec qualifier word used as the G.723 bit-rate selector when applicable.
1544    pub g723_bitrate: u32,
1545    /// Identity for this media stream within the conference.
1546    pub stream_passthrough_id: u32,
1547    /// Related stream identity, or zero when the stream is independent.
1548    pub associated_stream_id: u32,
1549    /// Numeric DTMF transport selector retained from the wire.
1550    pub dtmf_type: u32,
1551    /// Conference mixer mode retained from the selected layout.
1552    pub mixing_mode: u32,
1553    /// Media-direction word retained from the selected layout.
1554    pub direction: u32,
1555    /// Fixed latent-capability area retained byte-for-byte.
1556    pub latent_capabilities: [u8; 36],
1557}
1558
1559/// Non-canonical phone-originated keypad bodies selected by their exact body
1560/// length. `None` on `ClientMessage::KeypadButton` emits the current extended
1561/// layout.
1562#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1563pub enum KeypadButtonWireLayout {
1564    /// Four-byte body carrying only the keypad value.
1565    LegacyButtonOnly,
1566    /// Twelve-byte body carrying keypad value, line, and call identity.
1567    WithCallIdentity,
1568}
1569
1570/// One physical position in a station button template.
1571#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1572pub struct ButtonTemplateEntry {
1573    pub instance: u32,
1574    pub button_type: ButtonType,
1575}
1576
1577impl Default for ButtonTemplateEntry {
1578    fn default() -> Self {
1579        Self {
1580            instance: 0,
1581            button_type: ButtonType::Unused,
1582        }
1583    }
1584}
1585
1586#[derive(Clone, Debug, Eq, PartialEq)]
1587/// Typed messages accepted from a station connection.
1588///
1589/// Variants correspond to station-to-control identifiers in
1590/// [`catalog::MessageId`]. [`KnownOpaque`](Self::KnownOpaque) retains a known
1591/// catalog entry without a typed payload, while [`Unknown`](Self::Unknown)
1592/// retains an unrecognized identifier. Decode with [`Self::decode`] during
1593/// registration and [`Self::decode_with_version`] after version negotiation.
1594pub enum ClientMessage {
1595    /// Reports that the station connection remains active.
1596    /// Refreshes the server-side keepalive deadline.
1597    KeepAlive,
1598    /// Introduces a station and its requested SCCP protocol characteristics.
1599    /// Starts registration and device authentication on the server.
1600    Register(RegistrationMessage),
1601    /// Reports the UDP port on which the station expects media.
1602    /// Supplies the station RTP port to server-side media setup.
1603    IpPort {
1604        rtp_port: u16,
1605    },
1606    /// Reports a digit pressed on the station keypad.
1607    /// Associates the input with its line and call when the wire layout permits.
1608    KeypadButton {
1609        button: Digit,
1610        line_instance: u32,
1611        call_reference: u32,
1612        wire_layout: Option<KeypadButtonWireLayout>,
1613    },
1614    /// Submits a complete called-party number in one message.
1615    /// Requests call setup without sending digits individually.
1616    EnblocCall {
1617        called_party: String,
1618        line_instance: u32,
1619    },
1620    /// Reports a physical or logical station button stimulus.
1621    /// Identifies the affected instance, call, and stimulus status.
1622    Stimulus {
1623        stimulus: Stimulus,
1624        instance: u32,
1625        call_reference: u32,
1626        status: u32,
1627    },
1628    /// Reports that the station handset or audio path went off hook.
1629    /// Starts or resumes call handling for the identified line and call.
1630    OffHook {
1631        line_instance: u32,
1632        call_reference: u32,
1633    },
1634    /// Reports that the station handset or audio path went on hook.
1635    /// Ends or releases call handling for the identified line and call.
1636    OnHook {
1637        line_instance: u32,
1638        call_reference: u32,
1639    },
1640    /// Reports an off-hook transition with originating-party details.
1641    /// Carries the calling number, mailbox, and selected line instance.
1642    OffHookWithCallingParty {
1643        calling_party_number: String,
1644        voice_mailbox: String,
1645        line_instance: u32,
1646    },
1647    /// Requests the configured status of one station line.
1648    /// Uses the line instance to select the directory number response.
1649    LineStatRequest {
1650        line_instance: u32,
1651    },
1652    /// Requests the station's current device configuration.
1653    /// Prompts the server to return its configuration status.
1654    ConfigStatRequest,
1655    /// Requests the server's current date and time.
1656    /// Prompts synchronization of the station clock.
1657    TimeDateRequest,
1658    /// Requests the station's provisioned button layout.
1659    /// Prompts one or more button-template chunks from the server.
1660    ButtonTemplateRequest,
1661    /// Requests the firmware or load version assigned to the station.
1662    /// Prompts the server's version response during provisioning.
1663    VersionRequest,
1664    /// Reports the station's supported media capabilities.
1665    /// Answers a server capability request with codec and media details.
1666    CapabilitiesResponse(Vec<MediaCapability>),
1667    /// Reports the RTP ports the station has allocated for media streams.
1668    /// Carries up to sixteen port numbers for use by call control.
1669    MediaPortList(MediaPortList),
1670    /// Reports a changed set of station media capabilities.
1671    /// Lets the server refresh capabilities after initial negotiation.
1672    CapabilitiesUpdate(CapabilityUpdate),
1673    /// Acknowledges opening a multimedia receive channel.
1674    /// Reports the resulting endpoint and status for the requested stream.
1675    OpenMultimediaReceiveChannelAck(OpenMultimediaReceiveChannelAck),
1676    /// Requests the station's configured signaling-server list.
1677    /// Prompts the server response used for failover discovery.
1678    ServerRequest,
1679    /// Reports a station alarm or diagnostic condition.
1680    /// Carries severity, text, and optional vendor parameter words.
1681    Alarm {
1682        severity: AlarmSeverity,
1683        text: String,
1684        /// Optional alarm parameter words. `None` preserves the shorter wire
1685        /// layout exactly.
1686        parameters: Option<[u32; 2]>,
1687    },
1688    /// Acknowledges starting multicast media reception.
1689    /// Reports status for the passthrough party and call.
1690    MulticastMediaReceptionAck {
1691        status: MediaStatus,
1692        passthrough_party_id: crate::types::PassthroughPartyId,
1693        call_reference: CallReference,
1694    },
1695    /// Acknowledges opening an audio receive channel.
1696    /// Reports status and the station's selected media endpoint.
1697    OpenReceiveChannelAck {
1698        status: MediaStatus,
1699        address: IpAddr,
1700        port: u16,
1701        passthrough_party_id: u32,
1702        call_reference: u32,
1703    },
1704    /// Requests the soft-key sets available to the station.
1705    /// Prompts the server to send the active soft-key profile.
1706    SoftKeySetRequest,
1707    /// Requests the station's soft-key action template.
1708    /// Prompts the server to enumerate supported soft-key actions.
1709    SoftKeyTemplateRequest,
1710    /// Reports activation of a station soft key.
1711    /// Associates the action with its line and call context.
1712    SoftKeyEvent {
1713        event: u32,
1714        line_instance: u32,
1715        call_reference: u32,
1716    },
1717    /// Requests removal of the station registration.
1718    /// Carries the station-provided reason for ending the session.
1719    Unregister {
1720        reason: u32,
1721    },
1722    /// Requests a registration token before full station registration.
1723    /// Supplies the station identity used for token admission control.
1724    RegisterToken(RegisterTokenMessage),
1725    /// Requests an SPCP registration token before full station registration.
1726    /// Supplies the station identity, address, device type, and stream capacity.
1727    SpcpRegisterToken(SpcpRegisterTokenMessage),
1728    /// Reports a hook-flash action on an analog-style call.
1729    /// Associates the flash with its line and call context.
1730    HookFlash {
1731        line_instance: u32,
1732        call_reference: u32,
1733    },
1734    /// Requests call-forwarding state for one line.
1735    /// Prompts the server to return configured forwarding destinations.
1736    ForwardStatusRequest {
1737        line_instance: u32,
1738    },
1739    /// Requests the contents of one speed-dial entry.
1740    /// Uses the speed-dial instance to select the response.
1741    SpeedDialStatusRequest {
1742        speed_dial_instance: u32,
1743    },
1744    /// Returns media connection statistics collected by the station.
1745    /// Carries packet, jitter, latency, and quality data for a call.
1746    ConnectionStatisticsResponse(ConnectionStatistics),
1747    /// Reports whether the station headset is enabled.
1748    /// Updates the server's view of the station audio accessory state.
1749    HeadsetStatus {
1750        enabled: bool,
1751    },
1752    /// Reports a station media-resource state change.
1753    /// Carries the resource type, direction, and availability details.
1754    MediaResourceNotification(MediaResourceNotification),
1755    /// Reports an event on a particular station media path.
1756    /// Identifies both the media path and the observed event.
1757    MediaPathEvent {
1758        path: MediaPathId,
1759        event: MediaPathEvent,
1760    },
1761    /// Reports a capability of a particular station media path.
1762    /// Identifies the path and its supported media behavior.
1763    MediaPathCapability {
1764        path: MediaPathId,
1765        capability: MediaPathCapability,
1766    },
1767    /// Reports failure of an active media transmission.
1768    /// Identifies the stream endpoint, call, and failure status.
1769    MediaTransmissionFailure {
1770        conference_id: u32,
1771        passthrough_party_id: u32,
1772        address: IpAddr,
1773        port: u16,
1774        call_reference: u32,
1775        status: MediaStatus,
1776    },
1777    /// Reports how many line appearances the station can register.
1778    /// Lets the server constrain provisioning to the station's capacity.
1779    RegisterAvailableLines {
1780        lines: u32,
1781    },
1782    /// Requests the configured service URL at an index.
1783    /// Prompts the server to return its URL, label, and extension text.
1784    ServiceUrlStatusRequest {
1785        index: u32,
1786    },
1787    /// Requests the state of a provisioned feature button.
1788    /// Carries the feature index and station capability bits.
1789    FeatureStatusRequest {
1790        index: u32,
1791        /// Station feature-capability bits included in the request layout.
1792        capabilities: u32,
1793    },
1794    /// Acknowledges a request to start audio transmission.
1795    /// Reports the station's result for the requested media stream.
1796    StartMediaTransmissionAck(MediaTransmissionAck),
1797    /// Acknowledges a request to start multimedia transmission.
1798    /// Reports the station's result for the requested video or data stream.
1799    StartMultimediaTransmissionAck(StartMultimediaTransmissionAck),
1800    /// Reports capabilities supplied by an attached extension device.
1801    /// Lets the server account for expansion-module and accessory features.
1802    ExtensionDeviceCapabilities(ExtensionDeviceCapabilities),
1803    /// Carries legacy application data from the station to the server.
1804    /// Uses the fixed-format device-to-user data layout.
1805    DeviceToUserData(UserDataMessage),
1806    /// Returns a legacy station response to server application data.
1807    /// Uses the fixed-format device-to-user response layout.
1808    DeviceToUserDataResponse(UserDataMessage),
1809    /// Carries version-one application data from the station.
1810    /// Supports the extended variable-length user-data layout.
1811    DeviceToUserDataV1(UserDataV1Message),
1812    /// Returns a version-one station response to application data.
1813    /// Supports the extended variable-length response layout.
1814    DeviceToUserDataResponseV1(UserDataV1Message),
1815    /// Returns endpoint information requested for a station port.
1816    /// Identifies the address and port selected by the station.
1817    PortResponse(PortEndpoint),
1818    /// Requests status for a station feature subscription.
1819    /// Carries the transaction and feature identifiers being queried.
1820    SubscriptionStatusRequest(SubscriptionRequest),
1821    /// Acknowledges subscription to an RTP DTMF payload.
1822    /// Identifies the negotiated payload associated with the subscription.
1823    SubscribeDtmfPayloadResponse(DtmfPayloadIdentity),
1824    /// Acknowledges removal of an RTP DTMF payload subscription.
1825    /// Identifies the payload whose subscription was removed.
1826    UnsubscribeDtmfPayloadResponse(DtmfPayloadIdentity),
1827    /// Reports the station's location information as XML.
1828    /// Provides location metadata for routing and emergency services.
1829    LocationInfo {
1830        /// Location XML limited to 2,400 bytes before its required terminator.
1831        xml: String,
1832    },
1833    /// Reports a structured station alarm encoded as XML.
1834    /// Carries the alarm payload and its associated station metadata.
1835    XmlAlarm(XmlAlarmMessage),
1836    /// Requests the server's current call-count information.
1837    /// Preserves the request word used by the station wire layout.
1838    CallCountRequest {
1839        /// Request word retained without assigning a narrower semantic meaning.
1840        value: u32,
1841    },
1842    /// Returns the result of creating a conference.
1843    /// Correlates the station outcome with the requested conference.
1844    CreateConferenceResponse(CreateConferenceResponse),
1845    /// Returns the result of deleting a conference.
1846    /// Identifies the conference and its deletion result.
1847    DeleteConferenceResponse {
1848        conference_id: ConferenceId,
1849        result: DeleteConferenceResult,
1850    },
1851    /// Returns the result of modifying a conference.
1852    /// Carries the station outcome for the requested conference changes.
1853    ModifyConferenceResponse(ModifyConferenceResponse),
1854    /// Returns station state for a conference audit.
1855    /// Reports the conference details requested by the server.
1856    AuditConferenceResponse(AuditConferenceResponse),
1857    /// Returns the result of adding a conference participant.
1858    /// Identifies the conference participant and operation outcome.
1859    AddParticipantResponse(AddParticipantResponse),
1860    /// Returns station state for a participant audit.
1861    /// Reports the participant details requested by the server.
1862    AuditParticipantResponse(AuditParticipantResponse),
1863    /// Preserves a recognized station-to-server message without typed decoding.
1864    /// Retains its catalog identifier and payload bytes for lossless handling.
1865    KnownOpaque(KnownOpaqueMessage),
1866    /// Preserves an unrecognized station-to-server message.
1867    /// Retains the unknown identifier and raw payload for diagnostics or forwarding.
1868    Unknown(RawMessage),
1869}
1870
1871#[derive(Clone, Debug, Eq, PartialEq)]
1872/// Typed messages emitted toward a station connection.
1873///
1874/// Use [`Self::encode_for_session`] after registration so both protocol version
1875/// and negotiated feature bits participate in layout selection. The simpler
1876/// [`Self::encode`] applies version-only selection. User-visible strings can be
1877/// encoded through the explicit legacy-code-page entry points when required.
1878pub enum ServerMessage {
1879    /// Accepts station registration and supplies session parameters.
1880    /// Carries keepalive intervals, protocol features, and the date template.
1881    RegisterAck {
1882        keepalive_seconds: u32,
1883        secondary_keepalive_seconds: u32,
1884        protocol: ProtocolVersion,
1885        features: PhoneFeatures,
1886        date_template: DateTemplate,
1887    },
1888    /// Rejects a station registration attempt.
1889    /// Returns a human-readable reason for refusing the registration.
1890    RegisterReject {
1891        reason: String,
1892    },
1893    /// Acknowledges a station keepalive message.
1894    /// Confirms that the signaling session remains active.
1895    KeepAliveAck,
1896    /// Acknowledges a station unregister request.
1897    /// Confirms that the server has released the registration.
1898    UnregisterAck,
1899    /// Requests the station's supported media capabilities.
1900    /// Prompts a capability response used for media negotiation.
1901    CapabilitiesRequest,
1902    /// Invokes the station's legacy announcement enunciator.
1903    /// Applies to the station rather than a particular line or call.
1904    EnunciatorCommand,
1905    /// Supplies the station's provisioned device configuration.
1906    /// Carries user, service, and device settings needed after registration.
1907    ConfigStatus(ConfigurationStatus),
1908    /// Supplies the configured identity of one station line.
1909    /// Maps a line instance to its directory number and display name.
1910    LineStatus {
1911        instance: u32,
1912        number: String,
1913        display_name: String,
1914    },
1915    /// Supplies one chunk of the station's logical button layout.
1916    /// Uses offset and total counts to span layouts across multiple frames.
1917    ButtonTemplate {
1918        offset: u32,
1919        total: u32,
1920        buttons: Vec<ButtonTemplateEntry>,
1921    },
1922    /// Supplies the firmware or load version assigned to the station.
1923    /// Answers the station's version request during provisioning.
1924    Version {
1925        firmware: String,
1926    },
1927    /// Supplies the station's signaling-server list.
1928    /// Provides primary and failover endpoints for server discovery.
1929    ServerResponse {
1930        servers: Vec<SignalingServerEndpoint>,
1931    },
1932    /// Supplies the server's current date and time.
1933    /// Synchronizes the station clock with calendar and Unix time fields.
1934    TimeDate {
1935        year: u32,
1936        month: u32,
1937        weekday: u32,
1938        day: u32,
1939        hour: u32,
1940        minute: u32,
1941        second: u32,
1942        milliseconds: u32,
1943        unix_seconds: u32,
1944    },
1945    /// Supplies the soft-key actions supported by the server.
1946    /// Defines the action identifiers referenced by soft-key sets.
1947    SoftKeyTemplate {
1948        actions: Vec<values::SoftKey>,
1949    },
1950    /// Supplies the station's soft-key set profile.
1951    /// Maps call modes to ordered soft-key action lists.
1952    SoftKeySet {
1953        profile: SoftKeyProfile,
1954    },
1955    /// Selects the soft-key set shown for a call.
1956    /// Uses a validity mask to enable positions in the selected set.
1957    SelectSoftKeys {
1958        line_instance: u32,
1959        call_reference: u32,
1960        set: KeyMode,
1961        /// Bit mask over positions in the selected soft-key set.
1962        valid_mask: u32,
1963    },
1964    /// Updates the station's state for a call appearance.
1965    /// Associates the new call state with a line and call reference.
1966    CallState {
1967        state: CallState,
1968        line_instance: u32,
1969        call_reference: u32,
1970    },
1971    /// Supplies calling and called party information for a call.
1972    /// Updates the station's call-information display and metadata.
1973    CallInfo {
1974        info: CallInfo,
1975        line_instance: u32,
1976        call_reference: u32,
1977    },
1978    /// Displays a call-specific prompt on the station.
1979    /// Sets its text, timeout, line, and call context.
1980    DisplayPrompt {
1981        timeout_seconds: u32,
1982        text: String,
1983        line_instance: u32,
1984        call_reference: u32,
1985    },
1986    /// Clears a call-specific prompt from the station.
1987    /// Targets the prompt associated with a line and call reference.
1988    ClearPrompt {
1989        line_instance: u32,
1990        call_reference: u32,
1991    },
1992    /// Displays a transient notification on the station.
1993    /// Supplies notification text and its timeout.
1994    DisplayNotify {
1995        timeout_seconds: u32,
1996        text: String,
1997    },
1998    /// Clears the station's transient notification.
1999    /// Removes the notification created by a display-notify message.
2000    ClearNotify,
2001    /// Displays a prioritized transient notification.
2002    /// Supplies its priority, text, and timeout.
2003    DisplayPriorityNotify {
2004        timeout_seconds: u32,
2005        priority: NotificationPriority,
2006        text: String,
2007    },
2008    /// Clears notifications at a specified priority.
2009    /// Leaves notifications at other priorities unaffected.
2010    ClearPriorityNotify {
2011        priority: NotificationPriority,
2012    },
2013    /// Notifies the station of a DTMF tone state.
2014    /// Carries digit and call context without requesting local tone generation.
2015    NotifyDtmfTone(DtmfToneControl),
2016    /// Instructs the station to send a DTMF tone.
2017    /// Carries the digit and call context for media signaling.
2018    SendDtmfTone(DtmfToneControl),
2019    /// Starts an announcement for a conference.
2020    /// Carries the announcement sequence, participants, mask, and play mode.
2021    StartAnnouncement {
2022        announcements: Vec<AnnouncementEntry>,
2023        end_of_ack: u32,
2024        conference_id: u32,
2025        matrix_conference_party_ids: Vec<u32>,
2026        hearing_conference_party_mask: u32,
2027        play_mode: u32,
2028    },
2029    /// Stops the active announcement for a conference.
2030    /// Targets the announcement by conference identifier.
2031    StopAnnouncement {
2032        conference_id: u32,
2033    },
2034    /// Reports or confirms announcement completion to the station.
2035    /// Carries the conference identifier and final play status.
2036    AnnouncementFinish {
2037        conference_id: u32,
2038        play_status: u32,
2039    },
2040    /// Clears conference state maintained by the station.
2041    /// Identifies the conference and associated service number.
2042    ClearConference {
2043        conference_id: ConferenceId,
2044        service_number: u32,
2045    },
2046    /// Requests creation of a station-managed conference.
2047    /// Carries the conference attributes and correlation identifiers.
2048    CreateConferenceRequest(CreateConferenceRequest),
2049    /// Requests deletion of a station-managed conference.
2050    /// Targets the conference by identifier.
2051    DeleteConferenceRequest {
2052        conference_id: ConferenceId,
2053    },
2054    /// Requests changes to a station-managed conference.
2055    /// Carries the updated conference attributes and identifiers.
2056    ModifyConferenceRequest(ModifyConferenceRequest),
2057    /// Requests the station's current conference state.
2058    /// Prompts an audit response for conference reconciliation.
2059    AuditConferenceRequest,
2060    /// Requests adding a participant to a conference.
2061    /// Carries the conference, call, and participant details.
2062    AddParticipantRequest(AddParticipantRequest),
2063    /// Requests removal of a participant from a conference.
2064    /// Targets the participant by conference and call reference.
2065    DropParticipantRequest {
2066        conference_id: ConferenceId,
2067        call_reference: CallReference,
2068    },
2069    /// Requests the station's state for conference participants.
2070    /// Targets the participant set associated with a conference.
2071    AuditParticipantRequest {
2072        conference_id: ConferenceId,
2073    },
2074    /// Requests changes to a conference participant.
2075    /// Carries updated participant attributes and identifiers.
2076    ChangeParticipantRequest(ChangeParticipantRequest),
2077    /// Stops a station multimedia transmit stream.
2078    /// Identifies the conference, party, and call owning the stream.
2079    StopMultimediaTransmission(MultimediaStreamControl),
2080    /// Commands a station video stream to change its flow.
2081    /// Carries the requested bit rate and stream identifiers.
2082    FlowControlCommand(VideoFlowControl),
2083    /// Closes a station multimedia receive channel.
2084    /// Identifies the conference, party, and call owning the channel.
2085    CloseMultimediaReceiveChannel(MultimediaStreamControl),
2086    /// Selects the station's video display layout for a call.
2087    /// Carries conference, call, and layout identifiers.
2088    VideoDisplayCommand {
2089        conference_id: ConferenceId,
2090        call_reference: CallReference,
2091        layout_id: u32,
2092    },
2093    /// Notifies the station of video flow-control state.
2094    /// Reports stream identifiers and the applicable bit rate.
2095    FlowControlNotify(VideoFlowControl),
2096    /// Activates the station call-control plane for a line.
2097    /// Makes the selected line instance the active call plane.
2098    ActivateCallPlane {
2099        line_instance: u32,
2100    },
2101    /// Deactivates the station call-control plane.
2102    /// Removes the currently active call-plane selection.
2103    DeactivateCallPlane,
2104    /// Confirms processing of a dial-string backspace.
2105    /// Associates the response with its line and call context.
2106    BackspaceResponse {
2107        line_instance: u32,
2108        call_reference: u32,
2109    },
2110    /// Accepts a station registration-token request.
2111    /// Allows the station to proceed with full registration.
2112    RegisterTokenAck,
2113    /// Rejects a station registration-token request.
2114    /// Supplies the delay before the station should retry.
2115    RegisterTokenReject {
2116        backoff_seconds: u32,
2117    },
2118    /// Accepts an SPCP registration-token request with a feature word.
2119    /// Allows the station to continue its SPCP registration sequence.
2120    SpcpRegisterTokenAck {
2121        features: u32,
2122    },
2123    /// Rejects an SPCP registration-token request temporarily.
2124    /// Supplies the delay before the station should request another token.
2125    SpcpRegisterTokenReject {
2126        backoff_seconds: u32,
2127    },
2128    /// Sets the station ringer behavior for a call.
2129    /// Carries mode, duration, line, and call context.
2130    SetRinger {
2131        mode: RingerMode,
2132        duration: RingDuration,
2133        line_instance: u32,
2134        call_reference: u32,
2135    },
2136    /// Sets the lamp state for a station button.
2137    /// Targets a button type and instance with the requested lamp mode.
2138    SetLamp {
2139        stimulus: ButtonType,
2140        instance: u32,
2141        mode: LampMode,
2142    },
2143    /// Enables hook-flash detection on stations that expose that capability.
2144    /// Causes subsequent hook-flash actions to be reported to call control.
2145    SetHookFlashDetect,
2146    /// Starts local tone generation on the station.
2147    /// Selects the tone, direction, line, and call context.
2148    StartTone {
2149        tone: Tone,
2150        direction: ToneDirection,
2151        line_instance: u32,
2152        call_reference: u32,
2153    },
2154    /// Stops local tone generation for a call.
2155    /// Targets the tone associated with a line and call reference.
2156    StopTone {
2157        line_instance: u32,
2158        call_reference: u32,
2159    },
2160    /// Starts station reception of a multicast media stream.
2161    /// Supplies multicast endpoint, codec, and stream identifiers.
2162    StartMulticastMediaReception(MulticastMediaReception),
2163    /// Starts station transmission to a multicast media stream.
2164    /// Supplies multicast endpoint, codec, and stream identifiers.
2165    StartMulticastMediaTransmission(MulticastMediaTransmission),
2166    /// Stops station reception of a multicast media stream.
2167    /// Targets the stream by conference, party, and call identifiers.
2168    StopMulticastMediaReception {
2169        conference_id: ConferenceId,
2170        passthrough_party_id: crate::types::PassthroughPartyId,
2171        call_reference: CallReference,
2172    },
2173    /// Stops station transmission to a multicast media stream.
2174    /// Targets the stream by conference, party, and call identifiers.
2175    StopMulticastMediaTransmission {
2176        conference_id: ConferenceId,
2177        passthrough_party_id: crate::types::PassthroughPartyId,
2178        call_reference: CallReference,
2179    },
2180    /// Opens a station audio receive channel.
2181    /// Supplies codec, packetization, source, encryption, and stream details.
2182    OpenReceiveChannel {
2183        call_reference: u32,
2184        passthrough_party_id: u32,
2185        packet_ms: u32,
2186        codec: Codec,
2187        echo_cancellation: EchoCancellation,
2188        /// Dynamic RTP payload type used for telephone-event DTMF, or zero for signaling DTMF.
2189        telephone_event_payload: u8,
2190        source_address: IpAddr,
2191        source_port: u16,
2192        encryption: Option<MediaEncryption>,
2193        /// Exact auxiliary wire fields, or encoder defaults when absent on a
2194        /// runtime-created message.
2195        wire: Option<OpenReceiveChannelWire>,
2196    },
2197    /// Closes a station audio receive channel.
2198    /// Identifies the conference, party, and call owning the stream.
2199    CloseReceiveChannel(AudioStreamControl),
2200    /// Requests media connection statistics from the station.
2201    /// Selects the call, directory number, and statistics processing mode.
2202    ConnectionStatisticsRequest {
2203        directory_number: String,
2204        call_reference: u32,
2205        processing: StatisticsProcessing,
2206    },
2207    /// Starts station audio transmission to a media endpoint.
2208    /// Supplies endpoint, traffic class, encryption, and stream details.
2209    StartMediaTransmission {
2210        call_reference: u32,
2211        passthrough_party_id: u32,
2212        endpoint: MediaEndpoint,
2213        silence_suppression: SilenceSuppression,
2214        /// Full traffic-class octet; configuration DSCP is shifted left by two.
2215        traffic_class: crate::types::MediaTrafficClass,
2216        encryption: Option<MediaEncryption>,
2217        /// Exact auxiliary wire fields, or encoder defaults when absent on a
2218        /// runtime-created message.
2219        wire: Option<StartMediaTransmissionWire>,
2220    },
2221    /// Stops a station audio transmit stream.
2222    /// Identifies the conference, party, and call owning the stream.
2223    StopMediaTransmission(AudioStreamControl),
2224    /// Starts the station's legacy receive-side media function.
2225    /// Carries no stream identity, endpoint, or codec parameters.
2226    StartMediaReception,
2227    /// Stops a legacy media-reception path for one conference party.
2228    /// Identifies the active reception by conference and passthrough party.
2229    StopMediaReception {
2230        conference_id: ConferenceId,
2231        passthrough_party_id: crate::types::PassthroughPartyId,
2232    },
2233    /// Requests subscription to an RTP DTMF payload.
2234    /// Carries the payload and transaction identity to subscribe.
2235    SubscribeDtmfPayloadRequest(DtmfPayloadRequest),
2236    /// Reports failure to establish a DTMF payload subscription.
2237    /// Identifies the payload and transaction that failed.
2238    SubscribeDtmfPayloadError(DtmfPayloadIdentity),
2239    /// Requests removal of an RTP DTMF payload subscription.
2240    /// Carries the payload and transaction identity to remove.
2241    UnsubscribeDtmfPayloadRequest(DtmfPayloadRequest),
2242    /// Reports failure to remove a DTMF payload subscription.
2243    /// Identifies the payload and transaction that failed.
2244    UnsubscribeDtmfPayloadError(DtmfPayloadIdentity),
2245    /// Sets the station speakerphone mode.
2246    /// Controls whether the station speaker audio path is active.
2247    SetSpeakerMode(SpeakerMode),
2248    /// Sets the station microphone mode.
2249    /// Controls whether the station microphone audio path is active.
2250    SetMicrophoneMode(MicrophoneMode),
2251    /// Requests a station reset or restart.
2252    /// Selects the reset behavior defined by the reset type.
2253    Reset(ResetType),
2254    /// Displays text in the station's general display area.
2255    /// Replaces the current non-call-specific display text.
2256    DisplayText {
2257        text: String,
2258    },
2259    /// Clears the station's general display area.
2260    /// Removes text previously sent with a display-text message.
2261    ClearDisplay,
2262    /// Supplies call-forwarding state for one line.
2263    /// Carries destinations for all, busy, and no-answer forwarding.
2264    ForwardStatus {
2265        line_instance: u32,
2266        forward_all: Option<String>,
2267        forward_busy: Option<String>,
2268        forward_no_answer: Option<String>,
2269    },
2270    /// Supplies the contents of one station speed-dial entry.
2271    /// Maps an entry instance to its number and display name.
2272    SpeedDialStatus {
2273        instance: u32,
2274        number: String,
2275        display_name: String,
2276    },
2277    /// Displays or records the dialed number for a call.
2278    /// Associates the number with its line and call reference.
2279    DialedNumber {
2280        number: String,
2281        line_instance: u32,
2282        call_reference: u32,
2283    },
2284    /// Starts station monitoring for media-path failure.
2285    /// Supplies thresholds and stream identifiers used for detection.
2286    StartMediaFailureDetection(MediaFailureDetection),
2287    /// Carries legacy application data from the server to the station.
2288    /// Uses the fixed-format user-to-device data layout.
2289    UserToDeviceData(UserDataMessage),
2290    /// Carries version-one application data from the server.
2291    /// Supports the extended variable-length user-data layout.
2292    UserToDeviceDataV1(UserDataV1Message),
2293    /// Supplies the state of a provisioned feature button.
2294    /// Carries its type, label, instance, and feature-specific state.
2295    FeatureStatus {
2296        instance: u32,
2297        button_type: ButtonType,
2298        label: String,
2299        /// Feature-specific state word interpreted according to `button_type`.
2300        state: u32,
2301    },
2302    /// Supplies the configured service URL at an index.
2303    /// Carries its URL, label, and optional extension text.
2304    ServiceUrlStatus {
2305        index: u32,
2306        url: String,
2307        label: String,
2308        /// Additional dynamic-layout text; empty in layouts that do not carry it.
2309        extension_text: String,
2310    },
2311    /// Updates whether a call is selected on the station.
2312    /// Associates the selection state with its line and call.
2313    CallSelectStatus {
2314        /// Selection-state word retained as an extensible numeric value.
2315        status: u32,
2316        call_reference: u32,
2317        line_instance: u32,
2318    },
2319    /// Requests endpoint information for a station port.
2320    /// Carries the port identity and addressing parameters to resolve.
2321    PortRequest(PortRequest),
2322    /// Requests closure of a station port endpoint.
2323    /// Identifies the endpoint and port resources to release.
2324    PortClose(PortClose),
2325    /// Opens a station multimedia receive channel.
2326    /// Supplies codec, endpoint, and stream negotiation details.
2327    OpenMultimediaChannel(OpenMultimediaChannel),
2328    /// Starts station transmission of multimedia.
2329    /// Supplies destination, codec, bandwidth, and stream identifiers.
2330    StartMultimediaTransmission(StartMultimediaTransmission),
2331    /// Sends a stream-specific multimedia control command.
2332    /// Carries command data for video, picture, or recovery behavior.
2333    MiscellaneousCommand(MiscellaneousCommand),
2334    /// Supplies the result or state of a feature subscription.
2335    /// Carries transaction, feature, timer, and cause values.
2336    SubscriptionStatus {
2337        transaction_id: u32,
2338        feature_id: u32,
2339        timer_seconds: u32,
2340        cause: SubscriptionCause,
2341    },
2342    /// Sends a feature-subscription notification to the station.
2343    /// Carries transaction state, feature state, and display text.
2344    Notification {
2345        transaction_id: u32,
2346        feature_id: u32,
2347        status: BusyLampFieldState,
2348        text: String,
2349    },
2350    /// Sets how a call is represented in station call history.
2351    /// Associates the disposition with its line and call reference.
2352    CallHistoryDisposition {
2353        disposition: CallHistoryDisposition,
2354        line_instance: u32,
2355        call_reference: u32,
2356    },
2357    /// Returns the server's current call-count response.
2358    /// Answers the corresponding station call-count request.
2359    CallCountResponse,
2360    /// Updates the station's recording indicator for a call.
2361    /// Carries the call reference and whether recording is active.
2362    RecordingStatus {
2363        call_reference: u32,
2364        active: bool,
2365    },
2366    /// Preserves a recognized server-to-station message without typed decoding.
2367    /// Retains its catalog identifier and payload bytes for lossless handling.
2368    KnownOpaque(KnownOpaqueMessage),
2369    /// Preserves an unrecognized server-to-station message.
2370    /// Retains the unknown identifier and raw payload for diagnostics or forwarding.
2371    Unknown(RawMessage),
2372}
2373
2374#[cfg(test)]
2375mod tests {
2376    use super::wire::{CodecError, Frame, FrameDecoder};
2377    use super::*;
2378
2379    #[test]
2380    fn protocol_fillers_have_semantic_defaults() {
2381        assert_eq!(
2382            ButtonTemplateEntry::default(),
2383            ButtonTemplateEntry {
2384                instance: 0,
2385                button_type: ButtonType::Unused,
2386            }
2387        );
2388        assert_eq!(
2389            MessageWaitingCounts::default(),
2390            MessageWaitingCounts { new: 0, old: 0 }
2391        );
2392    }
2393
2394    const fn test_rtp_payload_number(value: u32) -> RtpPayloadNumber {
2395        match RtpPayloadNumber::new(value) {
2396            Ok(value) => value,
2397            Err(_) => panic!("test RTP payload number is out of range"),
2398        }
2399    }
2400
2401    fn decode_frame(bytes: &[u8]) -> Frame {
2402        FrameDecoder::new().push(bytes).unwrap().remove(0)
2403    }
2404
2405    fn assert_contract_alignment(frame: &Frame) {
2406        use super::catalog::PayloadLayout;
2407
2408        let contract = frame.message_type().contract().unwrap();
2409        if !matches!(
2410            contract.payload_layout,
2411            PayloadLayout::Opaque
2412                | PayloadLayout::BoundedOpaque
2413                | PayloadLayout::BoundedPreserved
2414                | PayloadLayout::VersionAndLengthSelected
2415                | PayloadLayout::MinimumLengthPreserved
2416        ) {
2417            assert_eq!(frame.payload.len() % 4, 0, "{}", contract.id);
2418        }
2419    }
2420
2421    fn assert_client_round_trip(message: ClientMessage, protocol: ProtocolVersion) {
2422        let frame = decode_frame(&message.encode(protocol).unwrap());
2423        assert_contract_alignment(&frame);
2424        assert_eq!(
2425            ClientMessage::decode_with_version(frame, protocol).unwrap(),
2426            message
2427        );
2428    }
2429
2430    fn assert_server_round_trip(message: ServerMessage, protocol: ProtocolVersion) {
2431        let frame = decode_frame(&message.encode(protocol).unwrap());
2432        assert_contract_alignment(&frame);
2433        assert_eq!(ServerMessage::decode(frame, protocol).unwrap(), message);
2434    }
2435
2436    fn assert_control_round_trip(message: ControlMessage, protocol: ProtocolVersion) {
2437        let frame = decode_frame(&message.encode(protocol).unwrap());
2438        assert_contract_alignment(&frame);
2439        assert_eq!(ControlMessage::decode(frame, protocol).unwrap(), message);
2440    }
2441
2442    #[test]
2443    fn multimedia_payload_exposes_only_typed_construction() {
2444        let capability = MultimediaVideoCapability::new(
2445            1_024,
2446            [MultimediaPictureFormat {
2447                format: VideoFormat::Cif4,
2448                minimum_picture_interval: 2,
2449            }],
2450            7,
2451            MultimediaVideoCapabilityArm::H264 {
2452                profile: 100,
2453                level: 42,
2454                custom_max_mbps: 40_500,
2455                custom_max_fs: 1_620,
2456                custom_max_dpb: 8_100,
2457                custom_max_br_and_cpb: 10_000,
2458            },
2459        )
2460        .unwrap();
2461        let payload = MultimediaPayload::new(test_rtp_payload_number(97), capability.clone());
2462        assert_eq!(payload.payload_number().get(), 97);
2463        assert_eq!(payload.descriptor().rfc_number(), 0);
2464        assert_eq!(payload.codec(), Codec::H264);
2465        assert_eq!(payload.video_capability(), Some(&capability));
2466
2467        let packetized = MultimediaPayload::with_descriptor(
2468            MultimediaPayloadDescriptor::new(4, payload.payload_number()),
2469            capability.clone(),
2470        );
2471        assert_eq!(packetized.descriptor().rfc_number(), 4);
2472        assert_eq!(packetized.payload_number(), payload.payload_number());
2473
2474        let debug = format!("{capability:?}");
2475        assert!(debug.contains("bit_rate: 1024"));
2476        assert!(!debug.contains("preserved_wire"));
2477        assert_eq!(
2478            RtpPayloadNumber::new(128),
2479            Err(RtpPayloadNumberError { actual: 128 })
2480        );
2481    }
2482
2483    #[test]
2484    fn multimedia_picture_formats_are_bounded_before_payload_construction() {
2485        let formats = [MultimediaPictureFormat {
2486            format: VideoFormat::Cif,
2487            minimum_picture_interval: 1,
2488        }; MAX_MULTIMEDIA_PICTURE_FORMATS + 1];
2489        assert_eq!(
2490            MultimediaVideoCapability::new(
2491                1_024,
2492                formats,
2493                0,
2494                MultimediaVideoCapabilityArm::H261 {
2495                    temporal_spatial_trade_off_capability: 0,
2496                    still_image_transmission: 0,
2497                },
2498            )
2499            .unwrap_err(),
2500            MultimediaCapabilityError {
2501                maximum: MAX_MULTIMEDIA_PICTURE_FORMATS,
2502                actual: MAX_MULTIMEDIA_PICTURE_FORMATS + 1,
2503            }
2504        );
2505    }
2506
2507    #[test]
2508    fn media_request_identity_is_nonzero_and_exhaustion_never_wraps() {
2509        assert_eq!(MediaRequestToken::new(0), None);
2510        let token = MediaRequestToken::new(7).unwrap();
2511        assert_eq!(MediaRequestIdentity::new(0, token), None);
2512
2513        let first = MediaRequestIdentity::new(1, token).unwrap();
2514        let second = first.checked_next().unwrap();
2515        assert_eq!(second.generation(), 2);
2516        assert_eq!(second.token().get(), 8);
2517
2518        assert_eq!(
2519            MediaRequestToken::new(u32::MAX).unwrap().checked_next(),
2520            None
2521        );
2522        let exhausted_generation =
2523            MediaRequestIdentity::new(u64::MAX, MediaRequestToken::new(1).unwrap()).unwrap();
2524        assert_eq!(exhausted_generation.checked_next(), None);
2525    }
2526
2527    #[test]
2528    fn media_request_identity_matches_only_the_current_wire_token() {
2529        let identity =
2530            MediaRequestIdentity::new(2, MediaRequestToken::new(0x1020_3040).unwrap()).unwrap();
2531
2532        assert!(identity.accepts_ack(0x1020_3040, 0, 77));
2533        assert!(identity.accepts_ack(0x1020_3040, 77, 77));
2534        assert!(!identity.accepts_ack(0x1020_3040, 78, 77));
2535        assert!(!identity.accepts_ack(0x1020_303f, 77, 77));
2536    }
2537
2538    #[test]
2539    fn zero_party_fallback_cannot_settle_a_reopened_media_generation() {
2540        let first = MediaRequestIdentity::new(1, MediaRequestToken::new(700).unwrap()).unwrap();
2541        let reopened = first.checked_next().unwrap();
2542
2543        // A zero-party ACK must carry the stable call reference.
2544        assert!(first.accepts_ack(0, 42, 42));
2545        assert!(!first.accepts_ack(0, 0, 42));
2546
2547        // The same delayed ACK is ambiguous after a reopen and fails closed.
2548        assert!(!reopened.accepts_ack(0, 42, 42));
2549        assert!(!reopened.accepts_ack(first.token().get(), 42, 42));
2550        assert!(reopened.accepts_ack(reopened.token().get(), 42, 42));
2551    }
2552
2553    #[test]
2554    fn decodes_7962_off_hook_capture_shape() {
2555        let frame = Frame::new(22, wire_id::OFF_HOOK, vec![1, 0, 0, 0, 42, 0, 0, 0]);
2556        assert_eq!(
2557            ClientMessage::decode(frame).unwrap(),
2558            ClientMessage::OffHook {
2559                line_instance: 1,
2560                call_reference: 42
2561            }
2562        );
2563    }
2564
2565    #[test]
2566    fn decodes_7961_v22_three_word_keypad_capture_shape() {
2567        let payload: Vec<_> = [8_u32, 1, 1]
2568            .into_iter()
2569            .flat_map(u32::to_le_bytes)
2570            .collect();
2571        let frame = Frame::new(22, wire_id::KEYPAD_BUTTON, payload.clone());
2572        let decoded = ClientMessage::decode(frame).unwrap();
2573        assert_eq!(
2574            decoded,
2575            ClientMessage::KeypadButton {
2576                button: Digit::Number(8),
2577                line_instance: 1,
2578                call_reference: 1,
2579                wire_layout: Some(KeypadButtonWireLayout::WithCallIdentity),
2580            }
2581        );
2582        let encoded = FrameDecoder::new()
2583            .push(&decoded.encode(ProtocolVersion::V22).unwrap())
2584            .unwrap()
2585            .remove(0);
2586        assert_eq!(encoded.payload, payload);
2587    }
2588
2589    #[test]
2590    fn register_ack_is_protocol_zero_and_has_expected_fields() {
2591        let bytes = ServerMessage::RegisterAck {
2592            keepalive_seconds: 30,
2593            secondary_keepalive_seconds: 45,
2594            protocol: ProtocolVersion::V22,
2595            features: PhoneFeatures::UTF8 | PhoneFeatures::DYNAMIC_MESSAGES,
2596            date_template: DateTemplate::default(),
2597        }
2598        .encode(ProtocolVersion::V22)
2599        .unwrap();
2600        let frame = FrameDecoder::new().push(&bytes).unwrap().remove(0);
2601        assert_eq!(frame.protocol_version, 0);
2602        assert_eq!(frame.message_id, wire_id::REGISTER_ACK);
2603        assert_eq!(
2604            ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
2605            ServerMessage::RegisterAck {
2606                keepalive_seconds: 30,
2607                secondary_keepalive_seconds: 45,
2608                protocol: ProtocolVersion::V22,
2609                features: PhoneFeatures::UTF8 | PhoneFeatures::DYNAMIC_MESSAGES,
2610                date_template: DateTemplate::default(),
2611            }
2612        );
2613    }
2614
2615    #[test]
2616    fn media_layout_sizes_match_supported_wire_specs() {
2617        let endpoint = MediaEndpoint {
2618            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
2619            rtp_port: 4000,
2620            rtcp_port: 4001,
2621            codec: Codec::Pcmu,
2622            packet_ms: 20,
2623            max_frames_per_packet: 1,
2624            telephone_event_payload: 101,
2625        };
2626        let start = ServerMessage::StartMediaTransmission {
2627            call_reference: 7,
2628            passthrough_party_id: 9,
2629            endpoint,
2630            silence_suppression: SilenceSuppression::Off,
2631            traffic_class: crate::types::MediaTrafficClass::from_wire(184),
2632            encryption: None,
2633            wire: None,
2634        }
2635        .encode(ProtocolVersion::V17)
2636        .unwrap();
2637        assert_eq!(start.len(), 144); // 12-byte header + 132-byte payload
2638        assert_eq!(&start[52..56], &184_u32.to_le_bytes());
2639        assert_eq!(&start[140..144], &1_u32.to_le_bytes());
2640        let open = ServerMessage::OpenReceiveChannel {
2641            call_reference: 7,
2642            passthrough_party_id: 9,
2643            packet_ms: 20,
2644            codec: Codec::Pcmu,
2645            echo_cancellation: EchoCancellation::On,
2646            telephone_event_payload: 101,
2647            source_address: endpoint.address,
2648            source_port: endpoint.rtp_port,
2649            encryption: None,
2650            wire: None,
2651        }
2652        .encode(ProtocolVersion::V17)
2653        .unwrap();
2654        assert_eq!(open.len(), 140); // 12-byte header + 128-byte payload
2655        assert_eq!(&open[108..112], &1_u32.to_le_bytes());
2656
2657        let start_v3 = ServerMessage::StartMediaTransmission {
2658            call_reference: 7,
2659            passthrough_party_id: 9,
2660            endpoint,
2661            silence_suppression: SilenceSuppression::Off,
2662            traffic_class: crate::types::MediaTrafficClass::default(),
2663            encryption: None,
2664            wire: None,
2665        }
2666        .encode(ProtocolVersion::V3)
2667        .unwrap();
2668        assert_eq!(start_v3.len(), 120); // 12-byte header + 108-byte payload
2669        let open_v3 = ServerMessage::OpenReceiveChannel {
2670            call_reference: 7,
2671            passthrough_party_id: 9,
2672            packet_ms: 20,
2673            codec: Codec::Pcmu,
2674            echo_cancellation: EchoCancellation::On,
2675            telephone_event_payload: 101,
2676            source_address: endpoint.address,
2677            source_port: endpoint.rtp_port,
2678            encryption: None,
2679            wire: None,
2680        }
2681        .encode(ProtocolVersion::V3)
2682        .unwrap();
2683        assert_eq!(open_v3.len(), 104); // 12-byte header + 92-byte payload
2684
2685        let start_v22 = ServerMessage::StartMediaTransmission {
2686            call_reference: 7,
2687            passthrough_party_id: 9,
2688            endpoint,
2689            silence_suppression: SilenceSuppression::Off,
2690            traffic_class: crate::types::MediaTrafficClass::default(),
2691            encryption: None,
2692            wire: None,
2693        }
2694        .encode(ProtocolVersion::V22)
2695        .unwrap();
2696        assert_eq!(start_v22.len(), 180); // 12-byte header + 168-byte payload
2697        let open_v22 = ServerMessage::OpenReceiveChannel {
2698            call_reference: 7,
2699            passthrough_party_id: 9,
2700            packet_ms: 20,
2701            codec: Codec::Pcmu,
2702            echo_cancellation: EchoCancellation::On,
2703            telephone_event_payload: 101,
2704            source_address: endpoint.address,
2705            source_port: endpoint.rtp_port,
2706            encryption: None,
2707            wire: None,
2708        }
2709        .encode(ProtocolVersion::V22)
2710        .unwrap();
2711        assert_eq!(open_v22.len(), 180); // 12-byte header + 168-byte payload
2712    }
2713
2714    #[test]
2715    fn media_close_layouts_consume_the_reference_fields_exactly() {
2716        let close = ServerMessage::CloseReceiveChannel(AudioStreamControl {
2717            conference_id: 6.into(),
2718            passthrough_party_id: 9.into(),
2719            call_reference: 7.into(),
2720            port_handling_flag: 11,
2721        });
2722        let close_v3 = close.encode(ProtocolVersion::V3).unwrap();
2723        assert_eq!(close_v3.len(), 28);
2724        assert_eq!(
2725            ServerMessage::decode(decode_frame(&close_v3), ProtocolVersion::V3).unwrap(),
2726            close
2727        );
2728        let close_v5 = close.encode(ProtocolVersion::V5).unwrap();
2729        assert_eq!(close_v5.len(), 28);
2730        assert_eq!(
2731            ServerMessage::decode(decode_frame(&close_v5), ProtocolVersion::V5).unwrap(),
2732            close
2733        );
2734
2735        let stop = ServerMessage::StopMediaTransmission(AudioStreamControl {
2736            conference_id: 6.into(),
2737            passthrough_party_id: 9.into(),
2738            call_reference: 7.into(),
2739            port_handling_flag: 11,
2740        });
2741        let bytes = stop.encode(ProtocolVersion::V22).unwrap();
2742        assert_eq!(bytes.len(), 28);
2743        assert_eq!(
2744            ServerMessage::decode(decode_frame(&bytes), ProtocolVersion::V22).unwrap(),
2745            stop
2746        );
2747
2748        let mut trailing = decode_frame(&bytes);
2749        trailing.payload.extend_from_slice(&[0; 4]);
2750        assert!(matches!(
2751            ServerMessage::decode(trailing, ProtocolVersion::V22),
2752            Err(CodecError::TrailingBytes { count: 4, .. })
2753        ));
2754    }
2755
2756    #[test]
2757    fn audio_packetization_round_trips_without_default_substitution() {
2758        let endpoint = MediaEndpoint {
2759            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
2760            rtp_port: 4000,
2761            rtcp_port: 4001,
2762            codec: Codec::G72264k,
2763            packet_ms: 30,
2764            max_frames_per_packet: 2,
2765            telephone_event_payload: 101,
2766        };
2767        for protocol in [
2768            ProtocolVersion::V3,
2769            ProtocolVersion::V17,
2770            ProtocolVersion::V22,
2771        ] {
2772            let (source_address, source_port) = if protocol.wire() < 12 {
2773                (IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
2774            } else {
2775                (endpoint.address, endpoint.rtp_port)
2776            };
2777            assert_server_round_trip(
2778                ServerMessage::OpenReceiveChannel {
2779                    call_reference: 7,
2780                    passthrough_party_id: 9,
2781                    packet_ms: 30,
2782                    codec: Codec::G72264k,
2783                    echo_cancellation: EchoCancellation::On,
2784                    telephone_event_payload: 101,
2785                    source_address,
2786                    source_port,
2787                    encryption: None,
2788                    wire: None,
2789                },
2790                protocol,
2791            );
2792            assert_server_round_trip(
2793                ServerMessage::StartMediaTransmission {
2794                    call_reference: 7,
2795                    passthrough_party_id: 9,
2796                    endpoint,
2797                    silence_suppression: SilenceSuppression::On,
2798                    traffic_class: crate::types::MediaTrafficClass::default(),
2799                    encryption: None,
2800                    wire: None,
2801                },
2802                protocol,
2803            );
2804            assert_client_round_trip(
2805                ClientMessage::MediaTransmissionFailure {
2806                    conference_id: 7,
2807                    passthrough_party_id: 9,
2808                    address: endpoint.address,
2809                    port: endpoint.rtp_port,
2810                    call_reference: 7,
2811                    status: MediaStatus::UnspecifiedError,
2812                },
2813                protocol,
2814            );
2815        }
2816    }
2817
2818    #[test]
2819    fn ipv6_audio_endpoints_require_and_round_trip_extended_layouts() {
2820        let address: IpAddr = "2001:db8::42".parse().unwrap();
2821        let endpoint = MediaEndpoint {
2822            address,
2823            rtp_port: 40_000,
2824            rtcp_port: 40_001,
2825            codec: Codec::G72264k,
2826            packet_ms: 20,
2827            max_frames_per_packet: 1,
2828            telephone_event_payload: 101,
2829        };
2830        let start = ServerMessage::StartMediaTransmission {
2831            call_reference: 7,
2832            passthrough_party_id: 9,
2833            endpoint,
2834            silence_suppression: SilenceSuppression::Off,
2835            traffic_class: crate::types::MediaTrafficClass::default(),
2836            encryption: None,
2837            wire: None,
2838        };
2839        let receive_ack = ClientMessage::OpenReceiveChannelAck {
2840            status: MediaStatus::Ok,
2841            address,
2842            port: endpoint.rtp_port,
2843            passthrough_party_id: 9,
2844            call_reference: 7,
2845        };
2846        let transmit_ack = ClientMessage::StartMediaTransmissionAck(MediaTransmissionAck {
2847            conference_id: 6,
2848            passthrough_party_id: 9,
2849            call_reference: 7,
2850            status: MediaStatus::Ok,
2851            address,
2852            port: endpoint.rtp_port,
2853            wire: None,
2854        });
2855        let failure = ClientMessage::MediaTransmissionFailure {
2856            conference_id: 7,
2857            passthrough_party_id: 9,
2858            address,
2859            port: endpoint.rtp_port,
2860            call_reference: 7,
2861            status: MediaStatus::UnspecifiedError,
2862        };
2863
2864        for protocol in [ProtocolVersion::V17, ProtocolVersion::V22] {
2865            assert_server_round_trip(start.clone(), protocol);
2866            assert_client_round_trip(receive_ack.clone(), protocol);
2867            assert_client_round_trip(transmit_ack.clone(), protocol);
2868            assert_client_round_trip(failure.clone(), protocol);
2869        }
2870        for result in [
2871            start.encode(ProtocolVersion::V16),
2872            receive_ack.encode(ProtocolVersion::V16),
2873            transmit_ack.encode(ProtocolVersion::V16),
2874            failure.encode(ProtocolVersion::V16),
2875            failure.encode(ProtocolVersion::V3),
2876        ] {
2877            assert!(matches!(
2878                result,
2879                Err(CodecError::InvalidValue {
2880                    field: "IP address family for pre-v17 protocol"
2881                        | "IP address family for this protocol version",
2882                    ..
2883                })
2884            ));
2885        }
2886    }
2887
2888    #[test]
2889    fn skinny_dtmf_disables_the_telephone_event_payload_in_both_directions() {
2890        let endpoint = MediaEndpoint {
2891            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
2892            rtp_port: 4000,
2893            rtcp_port: 4001,
2894            codec: Codec::Pcmu,
2895            packet_ms: 20,
2896            max_frames_per_packet: 1,
2897            telephone_event_payload: 0,
2898        };
2899        for protocol in [
2900            ProtocolVersion::V3,
2901            ProtocolVersion::V17,
2902            ProtocolVersion::V22,
2903        ] {
2904            let (source_address, source_port) = if protocol.wire() < 12 {
2905                (IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
2906            } else {
2907                (endpoint.address, endpoint.rtp_port)
2908            };
2909            assert_server_round_trip(
2910                ServerMessage::OpenReceiveChannel {
2911                    call_reference: 7,
2912                    passthrough_party_id: 9,
2913                    packet_ms: 20,
2914                    codec: Codec::Pcmu,
2915                    echo_cancellation: EchoCancellation::On,
2916                    telephone_event_payload: 0,
2917                    source_address,
2918                    source_port,
2919                    encryption: None,
2920                    wire: None,
2921                },
2922                protocol,
2923            );
2924            assert_server_round_trip(
2925                ServerMessage::StartMediaTransmission {
2926                    call_reference: 7,
2927                    passthrough_party_id: 9,
2928                    endpoint,
2929                    silence_suppression: SilenceSuppression::Off,
2930                    traffic_class: crate::types::MediaTrafficClass::default(),
2931                    encryption: None,
2932                    wire: None,
2933                },
2934                protocol,
2935            );
2936        }
2937    }
2938
2939    #[test]
2940    fn open_receive_wildcard_source_round_trips_for_all_supported_layouts() {
2941        for protocol in [
2942            ProtocolVersion::V3,
2943            ProtocolVersion::V17,
2944            ProtocolVersion::V22,
2945        ] {
2946            assert_server_round_trip(
2947                ServerMessage::OpenReceiveChannel {
2948                    call_reference: 1,
2949                    passthrough_party_id: 1,
2950                    packet_ms: 20,
2951                    codec: Codec::Pcma,
2952                    echo_cancellation: EchoCancellation::Off,
2953                    telephone_event_payload: 101,
2954                    source_address: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
2955                    source_port: 0,
2956                    encryption: None,
2957                    wire: None,
2958                },
2959                protocol,
2960            );
2961        }
2962    }
2963
2964    #[test]
2965    fn media_encryption_round_trips_without_exposing_key_material() {
2966        let key = b"private-key-1234";
2967        let salt = b"private-salt-123";
2968        let encryption =
2969            MediaEncryption::new(EncryptionMethod::Aes128HmacSha1_80, key, salt, 1, 64).unwrap();
2970        assert_eq!(encryption.key(), key);
2971        assert_eq!(encryption.salt(), salt);
2972
2973        let debug = format!("{encryption:?}");
2974        assert!(debug.contains("<redacted>"));
2975        assert!(!debug.contains("112, 114, 105, 118, 97, 116, 101"));
2976        assert!(!debug.contains("private-key"));
2977        let endpoint = MediaEndpoint {
2978            address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)),
2979            rtp_port: 40_000,
2980            rtcp_port: 40_001,
2981            codec: Codec::Pcmu,
2982            packet_ms: 20,
2983            max_frames_per_packet: 1,
2984            telephone_event_payload: 101,
2985        };
2986
2987        for protocol in [
2988            ProtocolVersion::new(12).unwrap(),
2989            ProtocolVersion::V17,
2990            ProtocolVersion::V22,
2991        ] {
2992            let open = ServerMessage::OpenReceiveChannel {
2993                call_reference: 7,
2994                passthrough_party_id: 9,
2995                packet_ms: 20,
2996                codec: Codec::Pcmu,
2997                echo_cancellation: EchoCancellation::On,
2998                telephone_event_payload: 101,
2999                source_address: endpoint.address,
3000                source_port: endpoint.rtp_port,
3001                encryption: Some(encryption.clone()),
3002                wire: None,
3003            };
3004            let open_debug = format!("{open:?}");
3005            assert!(open_debug.contains("<redacted>"));
3006            assert!(!open_debug.contains("112, 114, 105, 118, 97, 116, 101"));
3007            assert_server_round_trip(open, protocol);
3008            assert_server_round_trip(
3009                ServerMessage::StartMediaTransmission {
3010                    call_reference: 7,
3011                    passthrough_party_id: 9,
3012                    endpoint,
3013                    silence_suppression: SilenceSuppression::Off,
3014                    traffic_class: crate::types::MediaTrafficClass::default(),
3015                    encryption: Some(encryption.clone()),
3016                    wire: None,
3017                },
3018                protocol,
3019            );
3020        }
3021    }
3022
3023    #[test]
3024    fn media_encryption_rejects_oversized_secrets_with_metadata_only_errors() {
3025        let oversized_key = [0xa5; 17];
3026        let error = MediaEncryption::new(
3027            EncryptionMethod::Aes128HmacSha1_32,
3028            &oversized_key,
3029            &[],
3030            0,
3031            0,
3032        )
3033        .unwrap_err();
3034        assert!(matches!(
3035            error,
3036            CodecError::SecretTooLong {
3037                field: "media encryption key",
3038                actual: 17,
3039                maximum: 16,
3040            }
3041        ));
3042        assert!(!error.to_string().contains("165"));
3043
3044        let oversized_salt = [0x5a; 17];
3045        let error = MediaEncryption::new(
3046            EncryptionMethod::Aes128HmacSha1_32,
3047            &[],
3048            &oversized_salt,
3049            0,
3050            0,
3051        )
3052        .unwrap_err();
3053        assert!(matches!(
3054            error,
3055            CodecError::SecretTooLong {
3056                field: "media encryption salt",
3057                actual: 17,
3058                maximum: 16,
3059            }
3060        ));
3061        assert!(!error.to_string().contains("90"));
3062    }
3063
3064    #[test]
3065    fn common_client_messages_round_trip_semantically() {
3066        assert_client_round_trip(
3067            ClientMessage::FeatureStatusRequest {
3068                index: 7,
3069                capabilities: 1,
3070            },
3071            ProtocolVersion::V22,
3072        );
3073        assert_client_round_trip(
3074            ClientMessage::OffHookWithCallingParty {
3075                calling_party_number: "1001".into(),
3076                voice_mailbox: "5001".into(),
3077                line_instance: 1,
3078            },
3079            ProtocolVersion::V3,
3080        );
3081        assert_client_round_trip(
3082            ClientMessage::RegisterToken(RegisterTokenMessage {
3083                device_id: DeviceId::new("SEP001122334455").unwrap(),
3084                device_instance: 2,
3085                address: "2001:db8::42".parse().unwrap(),
3086                device_type: DeviceType::Cisco7962,
3087                flags: 6,
3088            }),
3089            ProtocolVersion::V22,
3090        );
3091        assert_control_round_trip(
3092            ControlMessage::MediaResourceNotification(MediaResourceNotification {
3093                device_type: DeviceType::Unknown(0xfeed),
3094                in_service_streams: 2,
3095                max_streams_per_conference: 4,
3096                out_of_service_streams: 1,
3097            }),
3098            ProtocolVersion::V17,
3099        );
3100        assert_client_round_trip(
3101            ClientMessage::SubscriptionStatusRequest(SubscriptionRequest {
3102                transaction_id: 0x4b,
3103                feature_id: 1,
3104                timer_seconds: 30,
3105                subscription_id: "4000".into(),
3106            }),
3107            ProtocolVersion::V22,
3108        );
3109        for message in [
3110            ClientMessage::SubscribeDtmfPayloadResponse(DtmfPayloadIdentity {
3111                payload_type: 101,
3112                conference_id: 42,
3113                passthrough_party_id: 7,
3114            }),
3115            ClientMessage::UnsubscribeDtmfPayloadResponse(DtmfPayloadIdentity {
3116                payload_type: 102,
3117                conference_id: 43,
3118                passthrough_party_id: 8,
3119            }),
3120        ] {
3121            let encoded = message.encode(ProtocolVersion::V22).unwrap();
3122            let frame = decode_frame(&encoded);
3123            assert_eq!(frame.payload.len(), 12);
3124            assert_eq!(
3125                ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3126                message
3127            );
3128        }
3129        assert_client_round_trip(
3130            ClientMessage::DeviceToUserDataV1(UserDataV1Message {
3131                application_id: 7,
3132                line_instance: 1,
3133                call_reference: 42,
3134                transaction_id: 9,
3135                sequence_flag: 1,
3136                display_priority: 2,
3137                conference_id: 42,
3138                application_instance_id: 3,
3139                routing: 4,
3140                data: b"<CiscoIPPhoneText/>".to_vec(),
3141            }),
3142            ProtocolVersion::V17,
3143        );
3144        assert_client_round_trip(
3145            ClientMessage::DeviceToUserDataResponse(UserDataMessage {
3146                application_id: 8,
3147                line_instance: 2,
3148                call_reference: 43,
3149                transaction_id: 10,
3150                data: b"<CiscoIPPhoneResponse/>".to_vec(),
3151            }),
3152            ProtocolVersion::V17,
3153        );
3154        assert_client_round_trip(
3155            ClientMessage::DeviceToUserData(UserDataMessage {
3156                application_id: 9,
3157                line_instance: 2,
3158                call_reference: 44,
3159                transaction_id: 11,
3160                data: b"<CiscoIPPhoneInput/>".to_vec(),
3161            }),
3162            ProtocolVersion::V17,
3163        );
3164        assert_client_round_trip(
3165            ClientMessage::DeviceToUserDataResponseV1(UserDataV1Message {
3166                application_id: 9,
3167                line_instance: 2,
3168                call_reference: 44,
3169                transaction_id: 11,
3170                sequence_flag: 2,
3171                display_priority: 1,
3172                conference_id: 44,
3173                application_instance_id: 9,
3174                routing: 1,
3175                data: b"<CiscoIPPhoneResponse/>".to_vec(),
3176            }),
3177            ProtocolVersion::V17,
3178        );
3179        assert_client_round_trip(
3180            ClientMessage::LocationInfo {
3181                xml: "<location><building>west</building></location>".into(),
3182            },
3183            ProtocolVersion::V22,
3184        );
3185        assert_client_round_trip(
3186            ClientMessage::XmlAlarm(
3187                XmlAlarmMessage::from_xml(b"<alarm><severity>warning</severity></alarm>").unwrap(),
3188            ),
3189            ProtocolVersion::V22,
3190        );
3191        assert_client_round_trip(
3192            ClientMessage::CallCountRequest { value: 2 },
3193            ProtocolVersion::V22,
3194        );
3195        assert_control_round_trip(
3196            ControlMessage::PortResponse(PortEndpoint {
3197                conference_id: 42,
3198                call_reference: 42,
3199                passthrough_party_id: 8,
3200                address: "2001:db8::8".parse().unwrap(),
3201                rtp_port: 16_000,
3202                rtcp_port: 16_001,
3203                media_type: Some(MediaType::Audio),
3204            }),
3205            ProtocolVersion::V22,
3206        );
3207        assert_control_round_trip(
3208            ControlMessage::CreateConferenceResponse(CreateConferenceResponse {
3209                conference_id: ConferenceId::new(42),
3210                result: CreateConferenceResult::Ok,
3211                passthrough_data: vec![1, 2, 3],
3212            }),
3213            ProtocolVersion::V22,
3214        );
3215        assert_control_round_trip(
3216            ControlMessage::DeleteConferenceResponse {
3217                conference_id: ConferenceId::new(42),
3218                result: DeleteConferenceResult::ConferenceDoesNotExist,
3219            },
3220            ProtocolVersion::V22,
3221        );
3222        assert_control_round_trip(
3223            ControlMessage::ModifyConferenceResponse(ModifyConferenceResponse {
3224                conference_id: ConferenceId::new(42),
3225                result: ModifyConferenceResult::MoreActiveCallsThanReserved,
3226                passthrough_data: vec![4, 5],
3227            }),
3228            ProtocolVersion::V22,
3229        );
3230        assert_control_round_trip(
3231            ControlMessage::AuditConferenceResponse(AuditConferenceResponse {
3232                last: 1,
3233                entries: vec![AuditConferenceEntry {
3234                    conference_id: ConferenceId::new(42),
3235                    resource_type: ConferenceResourceType::Conference,
3236                    reserved_participants: 8,
3237                    active_participants: 3,
3238                    application_id: ApplicationId::new(7),
3239                    application_conference_id: "festival-42".into(),
3240                    application_data: "main-stage".into(),
3241                }],
3242            }),
3243            ProtocolVersion::V22,
3244        );
3245        assert_control_round_trip(
3246            ControlMessage::AddParticipantResponse(AddParticipantResponse {
3247                conference_id: ConferenceId::new(42),
3248                call_reference: CallReference::new(100),
3249                result: AddParticipantResult::Ok,
3250                bridge_participant_id: BoundedBytes::try_from(vec![3; 257]).unwrap(),
3251            }),
3252            ProtocolVersion::V22,
3253        );
3254        assert_control_round_trip(
3255            ControlMessage::AuditParticipantResponse(AuditParticipantResponse {
3256                result: AuditParticipantResult::Ok,
3257                last: 1,
3258                conference_id: ConferenceId::new(42),
3259                number_of_entries: 2,
3260                participant_entries: vec![1, 2, 3, 4],
3261            }),
3262            ProtocolVersion::V22,
3263        );
3264    }
3265
3266    #[test]
3267    fn common_server_messages_round_trip_semantically() {
3268        assert_server_round_trip(
3269            ServerMessage::SpeedDialStatus {
3270                instance: 7,
3271                number: "2001".into(),
3272                display_name: "Reception".into(),
3273            },
3274            ProtocolVersion::V3,
3275        );
3276        assert_server_round_trip(
3277            ServerMessage::ServiceUrlStatus {
3278                index: 4,
3279                url: "http://services.invalid/directory".into(),
3280                label: "Directory".into(),
3281                extension_text: String::new(),
3282            },
3283            ProtocolVersion::V3,
3284        );
3285        for protocol in [
3286            ProtocolVersion::V3,
3287            ProtocolVersion::V17,
3288            ProtocolVersion::V22,
3289        ] {
3290            assert_server_round_trip(
3291                ServerMessage::ConnectionStatisticsRequest {
3292                    directory_number: "1001".into(),
3293                    call_reference: 42,
3294                    processing: StatisticsProcessing::DoNotClear,
3295                },
3296                protocol,
3297            );
3298        }
3299        assert_server_round_trip(
3300            ServerMessage::DisplayPriorityNotify {
3301                timeout_seconds: 5,
3302                priority: NotificationPriority::Voicemail,
3303                text: "Incoming call".into(),
3304            },
3305            ProtocolVersion::V17,
3306        );
3307        assert_server_round_trip(
3308            ServerMessage::FeatureStatus {
3309                instance: 2,
3310                button_type: ButtonType::BlfSpeedDial,
3311                label: "Support".into(),
3312                state: 0x0002_0101,
3313            },
3314            ProtocolVersion::V22,
3315        );
3316        assert_server_round_trip(
3317            ServerMessage::PortRequest(PortRequest {
3318                conference_id: 42.into(),
3319                call_reference: 42.into(),
3320                passthrough_party_id: 9.into(),
3321                transport: MediaTransport::Rtp,
3322                address_type: Some(IpAddressType::Ipv4AndIpv6),
3323                media_type: Some(MediaType::Audio),
3324            }),
3325            ProtocolVersion::V22,
3326        );
3327        assert_server_round_trip(
3328            ServerMessage::Notification {
3329                transaction_id: 3,
3330                feature_id: 1,
3331                status: BusyLampFieldState::Unknown(77),
3332                text: "4000".into(),
3333            },
3334            ProtocolVersion::V22,
3335        );
3336        assert_server_round_trip(
3337            ServerMessage::SubscriptionStatus {
3338                transaction_id: 3,
3339                feature_id: 1,
3340                timer_seconds: 30,
3341                cause: SubscriptionCause::Ok,
3342            },
3343            ProtocolVersion::V22,
3344        );
3345        assert_server_round_trip(
3346            ServerMessage::UserToDeviceData(UserDataMessage {
3347                application_id: 7,
3348                line_instance: 1,
3349                call_reference: 42,
3350                transaction_id: 9,
3351                data: b"<CiscoIPPhoneText/>".to_vec(),
3352            }),
3353            ProtocolVersion::V17,
3354        );
3355        assert_server_round_trip(
3356            ServerMessage::UserToDeviceDataV1(UserDataV1Message {
3357                application_id: 7,
3358                line_instance: 1,
3359                call_reference: 42,
3360                transaction_id: 9,
3361                sequence_flag: 2,
3362                display_priority: 1,
3363                conference_id: 42,
3364                application_instance_id: 7,
3365                routing: 1,
3366                data: b"<CiscoIPPhoneMenu/>".to_vec(),
3367            }),
3368            ProtocolVersion::V17,
3369        );
3370        assert_server_round_trip(
3371            ServerMessage::CallHistoryDisposition {
3372                disposition: CallHistoryDisposition::Missed,
3373                line_instance: 1,
3374                call_reference: 42,
3375            },
3376            ProtocolVersion::V22,
3377        );
3378        assert_server_round_trip(ServerMessage::CallCountResponse, ProtocolVersion::V22);
3379        for message in [
3380            ServerMessage::SubscribeDtmfPayloadRequest(DtmfPayloadRequest {
3381                payload_type: 101,
3382                conference_id: 42,
3383                passthrough_party_id: 7,
3384                dtmf_type: 2,
3385            }),
3386            ServerMessage::SubscribeDtmfPayloadError(DtmfPayloadIdentity {
3387                payload_type: 102,
3388                conference_id: 43,
3389                passthrough_party_id: 8,
3390            }),
3391            ServerMessage::UnsubscribeDtmfPayloadRequest(DtmfPayloadRequest {
3392                payload_type: 103,
3393                conference_id: 44,
3394                passthrough_party_id: 9,
3395                dtmf_type: 3,
3396            }),
3397            ServerMessage::UnsubscribeDtmfPayloadError(DtmfPayloadIdentity {
3398                payload_type: 104,
3399                conference_id: 45,
3400                passthrough_party_id: 10,
3401            }),
3402        ] {
3403            let encoded = message.encode(ProtocolVersion::V22).unwrap();
3404            let frame = decode_frame(&encoded);
3405            assert!(matches!(frame.payload.len(), 12 | 16));
3406            assert_eq!(
3407                ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
3408                message
3409            );
3410        }
3411        assert_server_round_trip(
3412            ServerMessage::RecordingStatus {
3413                call_reference: 42,
3414                active: true,
3415            },
3416            ProtocolVersion::V22,
3417        );
3418        assert_control_round_trip(
3419            ControlMessage::StartAnnouncement {
3420                announcements: vec![
3421                    AnnouncementEntry {
3422                        locale: 1,
3423                        country: 46,
3424                        tone: Tone::Zip,
3425                    },
3426                    AnnouncementEntry {
3427                        locale: 0,
3428                        country: 0,
3429                        tone: Tone::Silence,
3430                    },
3431                    AnnouncementEntry {
3432                        locale: 2,
3433                        country: 1,
3434                        tone: Tone::RecorderWarning,
3435                    },
3436                ],
3437                end_of_ack: EndOfAnnouncementAck::Required,
3438                conference_id: 42,
3439                matrix_conference_party_ids: vec![7, 0, 9],
3440                hearing_conference_party_mask: 0b101,
3441                play_mode: AnnouncementPlayMode::Continuous,
3442            },
3443            ProtocolVersion::V22,
3444        );
3445        assert_control_round_trip(
3446            ControlMessage::StopAnnouncement { conference_id: 42 },
3447            ProtocolVersion::V22,
3448        );
3449        assert_control_round_trip(
3450            ControlMessage::AnnouncementFinish {
3451                conference_id: 42,
3452                play_status: AnnouncementPlayStatus::Unknown(3),
3453            },
3454            ProtocolVersion::V22,
3455        );
3456        assert_control_round_trip(
3457            ControlMessage::ClearConference {
3458                conference_id: ConferenceId::new(42),
3459                service_number: 3,
3460            },
3461            ProtocolVersion::V22,
3462        );
3463        assert_control_round_trip(
3464            ControlMessage::CreateConferenceRequest(CreateConferenceRequest {
3465                conference_id: ConferenceId::new(42),
3466                reserved_participants: 8,
3467                resource_type: ConferenceResourceType::Conference,
3468                application_id: ApplicationId::new(7),
3469                application_conference_id: "festival-42".into(),
3470                application_data: "main-stage".into(),
3471                passthrough_data: vec![1, 2, 3],
3472            }),
3473            ProtocolVersion::V22,
3474        );
3475        assert_control_round_trip(
3476            ControlMessage::DeleteConferenceRequest {
3477                conference_id: ConferenceId::new(42),
3478            },
3479            ProtocolVersion::V22,
3480        );
3481        assert_control_round_trip(
3482            ControlMessage::ModifyConferenceRequest(ModifyConferenceRequest {
3483                conference_id: ConferenceId::new(42),
3484                reserved_participants: 12,
3485                application_id: ApplicationId::new(7),
3486                application_conference_id: "festival-42".into(),
3487                application_data: "main-stage".into(),
3488                passthrough_data: vec![4, 5],
3489            }),
3490            ProtocolVersion::V22,
3491        );
3492        assert_control_round_trip(ControlMessage::AuditConferenceRequest, ProtocolVersion::V22);
3493        assert_control_round_trip(
3494            ControlMessage::AddParticipantRequest(AddParticipantRequest {
3495                conference_id: ConferenceId::new(42),
3496                participant: ConferenceParticipant {
3497                    call_reference: CallReference::new(100),
3498                    presentation_restrictions: PartyInformationRestrictions::CALLING_NUMBER,
3499                    name: "Festival Caller".into(),
3500                    number: "1001".into(),
3501                    conference_name: "Main Stage".into(),
3502                },
3503            }),
3504            ProtocolVersion::V22,
3505        );
3506        assert_control_round_trip(
3507            ControlMessage::DropParticipantRequest {
3508                conference_id: ConferenceId::new(42),
3509                call_reference: CallReference::new(100),
3510            },
3511            ProtocolVersion::V22,
3512        );
3513        assert_control_round_trip(
3514            ControlMessage::AuditParticipantRequest {
3515                conference_id: ConferenceId::new(42),
3516            },
3517            ProtocolVersion::V22,
3518        );
3519    }
3520
3521    #[test]
3522    fn connection_statistics_round_trip_all_layouts_and_redact_opaque_fields() {
3523        let statistics = ConnectionStatistics {
3524            directory_number: "2002".into(),
3525            call_reference: 42,
3526            processing: StatisticsProcessing::Clear,
3527            packets_sent: 100,
3528            octets_sent: 8_000,
3529            packets_received: 98,
3530            octets_received: 7_840,
3531            packets_lost: 2,
3532            jitter_millis: 7,
3533            latency_millis: 18,
3534            quality: ConnectionQualityStatistics::new(b"MLQK=4.5;Secret=opaque".to_vec()).unwrap(),
3535        };
3536        for protocol in [
3537            ProtocolVersion::V3,
3538            ProtocolVersion::V19,
3539            ProtocolVersion::V22,
3540        ] {
3541            assert_client_round_trip(
3542                ClientMessage::ConnectionStatisticsResponse(statistics.clone()),
3543                protocol,
3544            );
3545        }
3546        let debug = format!("{statistics:?}");
3547        assert!(!debug.contains("2002"));
3548        assert!(!debug.contains("Secret"));
3549        assert!(debug.contains("byte_count"));
3550        assert!(matches!(
3551            ConnectionQualityStatistics::new(vec![0; CONNECTION_QUALITY_MAX_BYTES + 1]),
3552            Err(CodecError::CountTooLarge {
3553                field: "quality statistics",
3554                maximum: CONNECTION_QUALITY_MAX_BYTES,
3555                ..
3556            })
3557        ));
3558    }
3559
3560    #[test]
3561    fn dtmf_subscription_messages_require_their_exact_word_layouts() {
3562        for message_id in [
3563            wire_id::SUBSCRIBE_DTMF_PAYLOAD_RES,
3564            wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_RES,
3565        ] {
3566            assert!(ClientMessage::decode(Frame::new(22, message_id, Vec::new())).is_err());
3567            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 11])).is_err());
3568            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 12])).is_ok());
3569            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 13])).is_err());
3570        }
3571        for (message_id, size) in [
3572            (wire_id::SUBSCRIBE_DTMF_PAYLOAD_REQ, 16),
3573            (wire_id::SUBSCRIBE_DTMF_PAYLOAD_ERR, 12),
3574            (wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_REQ, 16),
3575            (wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_ERR, 12),
3576        ] {
3577            assert!(
3578                ServerMessage::decode(
3579                    Frame::new(22, message_id, vec![0; size - 1]),
3580                    ProtocolVersion::V22,
3581                )
3582                .is_err()
3583            );
3584            assert!(
3585                ServerMessage::decode(
3586                    Frame::new(22, message_id, vec![0; size]),
3587                    ProtocolVersion::V22,
3588                )
3589                .is_ok()
3590            );
3591            assert!(
3592                ServerMessage::decode(
3593                    Frame::new(22, message_id, vec![0; size + 1]),
3594                    ProtocolVersion::V22,
3595                )
3596                .is_err()
3597            );
3598        }
3599    }
3600
3601    #[test]
3602    fn announcement_lists_enforce_station_bounds() {
3603        let error = ServerMessage::StartAnnouncement {
3604            announcements: vec![
3605                AnnouncementEntry {
3606                    locale: 1,
3607                    country: 1,
3608                    tone: Tone::Zip,
3609                };
3610                33
3611            ],
3612            end_of_ack: 0,
3613            conference_id: 1,
3614            matrix_conference_party_ids: Vec::new(),
3615            hearing_conference_party_mask: 0,
3616            play_mode: 0,
3617        }
3618        .encode(ProtocolVersion::V22)
3619        .unwrap_err();
3620        assert!(matches!(
3621            error,
3622            CodecError::CountTooLarge {
3623                field: "announcements",
3624                count: 33,
3625                maximum: 32,
3626                ..
3627            }
3628        ));
3629
3630        let error = ServerMessage::StartAnnouncement {
3631            announcements: Vec::new(),
3632            end_of_ack: 0,
3633            conference_id: 1,
3634            matrix_conference_party_ids: (1..=17).collect(),
3635            hearing_conference_party_mask: 0,
3636            play_mode: 0,
3637        }
3638        .encode(ProtocolVersion::V22)
3639        .unwrap_err();
3640        assert!(matches!(
3641            error,
3642            CodecError::CountTooLarge {
3643                field: "matrix conference party identifiers",
3644                count: 17,
3645                maximum: 16,
3646                ..
3647            }
3648        ));
3649    }
3650
3651    #[test]
3652    fn enbloc_uses_the_protocol_19_alignment_boundary() {
3653        for (protocol, payload_len, line_offset) in [
3654            (ProtocolVersion::V18, 28, 24),
3655            (ProtocolVersion::V19, 32, 28),
3656        ] {
3657            let message = ClientMessage::EnblocCall {
3658                called_party: "9801".into(),
3659                line_instance: 3,
3660            };
3661            let frame = FrameDecoder::new()
3662                .push(&message.encode(protocol).unwrap())
3663                .unwrap()
3664                .remove(0);
3665            assert_eq!(frame.payload.len(), payload_len);
3666            assert_eq!(
3667                &frame.payload[line_offset..line_offset + 4],
3668                &3_u32.to_le_bytes()
3669            );
3670            assert_eq!(
3671                ClientMessage::decode_with_version(frame, protocol).unwrap(),
3672                message
3673            );
3674        }
3675    }
3676
3677    #[test]
3678    fn supplemental_client_messages_have_typed_layouts() {
3679        let ports = ClientMessage::MediaPortList(MediaPortList {
3680            rtp_ports: vec![16_000, 16_002],
3681        });
3682        let frame = decode_frame(&ports.encode(ProtocolVersion::V22).unwrap());
3683        assert_eq!(frame.message_id, wire_id::MEDIA_PORT_LIST);
3684        assert_eq!(frame.payload.len(), 68);
3685        assert_eq!(
3686            &frame.payload[..12],
3687            &[2, 0, 0, 0, 0x80, 0x3e, 0, 0, 0x82, 0x3e, 0, 0]
3688        );
3689        assert_eq!(
3690            ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3691            ports
3692        );
3693
3694        let token = ClientMessage::SpcpRegisterToken(SpcpRegisterTokenMessage {
3695            device_id: DeviceId::new("SEP001122334455").unwrap(),
3696            device_instance: 2,
3697            address: Ipv4Addr::new(192, 0, 2, 10),
3698            device_type: DeviceType::Cisco7962,
3699            max_streams: 0x0102_0304,
3700        });
3701        let frame = decode_frame(&token.encode(ProtocolVersion::V22).unwrap());
3702        assert_eq!(frame.message_id, wire_id::SPCP_REGISTER_TOKEN_REQ);
3703        assert_eq!(frame.payload.len(), 36);
3704        assert_eq!(&frame.payload[16..20], &[0; 4]);
3705        assert_eq!(&frame.payload[24..28], &[10, 2, 0, 192]);
3706        assert_eq!(&frame.payload[32..36], &[4, 3, 2, 1]);
3707        assert_eq!(
3708            ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3709            token
3710        );
3711
3712        let oversized = ClientMessage::MediaPortList(MediaPortList {
3713            rtp_ports: vec![16_000; MEDIA_PORT_LIST_MAX_PORTS + 1],
3714        });
3715        assert!(matches!(
3716            oversized.encode(ProtocolVersion::V22),
3717            Err(CodecError::CountTooLarge { .. })
3718        ));
3719
3720        let mut invalid_port = vec![0; 68];
3721        invalid_port[..4].copy_from_slice(&1_u32.to_le_bytes());
3722        invalid_port[4..8].copy_from_slice(&65_536_u32.to_le_bytes());
3723        assert!(matches!(
3724            ClientMessage::decode_with_version(
3725                Frame::new(22, wire_id::MEDIA_PORT_LIST, invalid_port),
3726                ProtocolVersion::V22,
3727            ),
3728            Err(CodecError::InvalidValue {
3729                field: "RTP port",
3730                ..
3731            })
3732        ));
3733    }
3734
3735    #[test]
3736    fn supplemental_server_messages_have_typed_layouts() {
3737        for (message, id, payload) in [
3738            (
3739                ServerMessage::SetHookFlashDetect,
3740                wire_id::SET_HOOK_FLASH_DETECT,
3741                vec![],
3742            ),
3743            (
3744                ServerMessage::StartMediaReception,
3745                wire_id::START_MEDIA_RECEPTION,
3746                vec![],
3747            ),
3748            (
3749                ServerMessage::StopMediaReception {
3750                    conference_id: 0x0102_0304.into(),
3751                    passthrough_party_id: 0x0506_0708.into(),
3752                },
3753                wire_id::STOP_MEDIA_RECEPTION,
3754                vec![4, 3, 2, 1, 8, 7, 6, 5],
3755            ),
3756            (
3757                ServerMessage::EnunciatorCommand,
3758                wire_id::ENUNCIATOR_COMMAND,
3759                vec![],
3760            ),
3761            (
3762                ServerMessage::SpcpRegisterTokenAck {
3763                    features: 0x0102_0304,
3764                },
3765                wire_id::SPCP_REGISTER_TOKEN_ACK,
3766                vec![4, 3, 2, 1],
3767            ),
3768            (
3769                ServerMessage::SpcpRegisterTokenReject {
3770                    backoff_seconds: 60,
3771                },
3772                wire_id::SPCP_REGISTER_TOKEN_REJECT,
3773                vec![60, 0, 0, 0],
3774            ),
3775        ] {
3776            let frame = decode_frame(&message.encode(ProtocolVersion::V22).unwrap());
3777            assert_eq!(frame.message_id, id);
3778            assert_eq!(frame.payload, payload);
3779            assert_eq!(
3780                ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
3781                message
3782            );
3783        }
3784
3785        assert!(
3786            ServerMessage::decode(
3787                Frame::new(22, wire_id::SET_HOOK_FLASH_DETECT, vec![0; 4]),
3788                ProtocolVersion::V22,
3789            )
3790            .is_err()
3791        );
3792    }
3793
3794    #[test]
3795    fn unknown_messages_are_byte_lossless() {
3796        let unknown_payload = vec![9, 8, 7, 6];
3797        let unknown = ServerMessage::decode(
3798            Frame::new(19, 0xdead_beef, unknown_payload.clone()),
3799            ProtocolVersion::V19,
3800        )
3801        .unwrap();
3802        assert!(matches!(unknown, ServerMessage::Unknown(_)));
3803        let unknown_frame = decode_frame(&unknown.encode(ProtocolVersion::V22).unwrap());
3804        assert_eq!(unknown_frame.message_id, 0xdead_beef);
3805        assert_eq!(unknown_frame.protocol_version, 19);
3806        assert_eq!(unknown_frame.payload, unknown_payload);
3807    }
3808
3809    #[test]
3810    fn opaque_encoding_cannot_bypass_a_typed_contract() {
3811        let message = ClientMessage::KnownOpaque(KnownOpaqueMessage {
3812            id: MessageId::IpPort,
3813            protocol_version: ProtocolVersion::V22.wire(),
3814            payload: BoundedBytes::default(),
3815        });
3816
3817        assert!(matches!(
3818            message.encode(ProtocolVersion::V22),
3819            Err(CodecError::InvalidValue {
3820                message_id: wire_id::IP_PORT,
3821                field: "opaque preservation requires an opaque-only contract",
3822                ..
3823            })
3824        ));
3825    }
3826
3827    #[test]
3828    fn malformed_counts_and_oversized_text_are_rejected() {
3829        assert!(matches!(
3830            ClientMessage::decode(Frame::new(
3831                22,
3832                wire_id::CAPABILITIES_RES,
3833                19_u32.to_le_bytes().to_vec(),
3834            )),
3835            Err(CodecError::CountTooLarge { .. })
3836        ));
3837        assert!(matches!(
3838            ServerMessage::DisplayText {
3839                text: "x".repeat(32),
3840            }
3841            .encode(ProtocolVersion::V22),
3842            Err(CodecError::TextTooLong { .. })
3843        ));
3844        assert!(matches!(
3845            ClientMessage::DeviceToUserData(UserDataMessage {
3846                application_id: 1,
3847                line_instance: 1,
3848                call_reference: 1,
3849                transaction_id: 1,
3850                data: vec![0; 2001],
3851            })
3852            .encode(ProtocolVersion::V22),
3853            Err(CodecError::CountTooLarge { .. })
3854        ));
3855        assert!(matches!(
3856            ClientMessage::decode(Frame::new(
3857                22,
3858                wire_id::IP_PORT,
3859                70_000_u32.to_le_bytes().to_vec(),
3860            )),
3861            Err(CodecError::InvalidValue { .. })
3862        ));
3863        assert!(matches!(
3864            ServerMessage::StartMediaTransmission {
3865                call_reference: 1,
3866                passthrough_party_id: 1,
3867                endpoint: MediaEndpoint {
3868                    address: "2001:db8::1".parse().unwrap(),
3869                    rtp_port: 4000,
3870                    rtcp_port: 4001,
3871                    codec: Codec::Pcmu,
3872                    packet_ms: 20,
3873                    max_frames_per_packet: 1,
3874                    telephone_event_payload: 101,
3875                },
3876                silence_suppression: SilenceSuppression::Off,
3877                traffic_class: crate::types::MediaTrafficClass::default(),
3878                encryption: None,
3879                wire: None,
3880            }
3881            .encode(ProtocolVersion::V3),
3882            Err(CodecError::InvalidValue { .. })
3883        ));
3884        assert!(matches!(
3885            ControlMessage::CreateConferenceRequest(CreateConferenceRequest {
3886                conference_id: ConferenceId::new(1),
3887                reserved_participants: 2,
3888                resource_type: ConferenceResourceType::Conference,
3889                application_id: ApplicationId::new(1),
3890                application_conference_id: "conference-1".into(),
3891                application_data: String::new(),
3892                passthrough_data: vec![0; 2001],
3893            })
3894            .encode(ProtocolVersion::V22),
3895            Err(CodecError::CountTooLarge {
3896                field: "conference passthrough data",
3897                count: 2001,
3898                maximum: 2000,
3899                ..
3900            })
3901        ));
3902        assert!(matches!(
3903            ControlMessage::AuditConferenceResponse(AuditConferenceResponse {
3904                last: 1,
3905                entries: vec![
3906                    AuditConferenceEntry {
3907                        conference_id: ConferenceId::new(1),
3908                        resource_type: ConferenceResourceType::Conference,
3909                        reserved_participants: 2,
3910                        active_participants: 1,
3911                        application_id: ApplicationId::new(1),
3912                        application_conference_id: String::new(),
3913                        application_data: String::new(),
3914                    };
3915                    33
3916                ],
3917            })
3918            .encode(ProtocolVersion::V22),
3919            Err(CodecError::CountTooLarge {
3920                field: "conference audit entries",
3921                count: 33,
3922                maximum: 32,
3923                ..
3924            })
3925        ));
3926
3927        let mut oversized_conference_data = vec![0; 12];
3928        oversized_conference_data[8..12].copy_from_slice(&2001_u32.to_le_bytes());
3929        assert!(matches!(
3930            ControlMessage::decode(
3931                Frame::new(
3932                    22,
3933                    wire_id::CREATE_CONFERENCE_RES,
3934                    oversized_conference_data
3935                ),
3936                ProtocolVersion::V22,
3937            ),
3938            Err(CodecError::CountTooLarge {
3939                field: "conference passthrough data",
3940                count: 2001,
3941                maximum: 2000,
3942                ..
3943            })
3944        ));
3945
3946        let mut oversized_audit = vec![0; 8];
3947        oversized_audit[4..8].copy_from_slice(&33_u32.to_le_bytes());
3948        assert!(matches!(
3949            ControlMessage::decode(
3950                Frame::new(22, wire_id::AUDIT_CONFERENCE_RES, oversized_audit),
3951                ProtocolVersion::V22,
3952            ),
3953            Err(CodecError::CountTooLarge {
3954                field: "conference audit entries",
3955                count: 33,
3956                maximum: 32,
3957                ..
3958            })
3959        ));
3960    }
3961
3962    #[test]
3963    fn server_response_uses_the_negotiated_address_layout() {
3964        let message = ServerMessage::ServerResponse {
3965            servers: vec![
3966                SignalingServerEndpoint {
3967                    name: "primary".into(),
3968                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)),
3969                    port: NonZeroU16::new(2000).unwrap(),
3970                },
3971                SignalingServerEndpoint {
3972                    name: "secondary".into(),
3973                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 20)),
3974                    port: NonZeroU16::new(2001).unwrap(),
3975                },
3976            ],
3977        };
3978        let v3 = message.encode(ProtocolVersion::V3).unwrap();
3979        let v17 = message.encode(ProtocolVersion::V17).unwrap();
3980        assert_eq!(v3.len(), 292);
3981        assert_eq!(v17.len(), 372);
3982        assert_server_round_trip(message.clone(), ProtocolVersion::V3);
3983        assert_server_round_trip(message, ProtocolVersion::V17);
3984
3985        let mut zero_port = v3;
3986        zero_port[12 + 5 * 48..12 + 5 * 48 + 4].fill(0);
3987        assert!(matches!(
3988            ServerMessage::decode(decode_frame(&zero_port), ProtocolVersion::V3),
3989            Err(CodecError::InvalidValue {
3990                field: "server endpoint",
3991                value: 0,
3992                ..
3993            })
3994        ));
3995        assert_server_round_trip(
3996            ServerMessage::ServerResponse {
3997                servers: vec![SignalingServerEndpoint {
3998                    name: "sccp-v6".into(),
3999                    address: "2001:db8::20".parse().unwrap(),
4000                    port: NonZeroU16::new(2000).unwrap(),
4001                }],
4002            },
4003            ProtocolVersion::V17,
4004        );
4005
4006        let unspecified = ServerMessage::ServerResponse {
4007            servers: vec![SignalingServerEndpoint {
4008                name: "unroutable".into(),
4009                address: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
4010                port: NonZeroU16::new(2000).unwrap(),
4011            }],
4012        };
4013        assert!(matches!(
4014            unspecified.encode(ProtocolVersion::V17),
4015            Err(CodecError::InvalidValue {
4016                field: "server address",
4017                value: 0,
4018                ..
4019            })
4020        ));
4021
4022        let endpoints = |count: u8| {
4023            (0..count)
4024                .map(|index| SignalingServerEndpoint {
4025                    name: format!("node-{index}"),
4026                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, index + 1)),
4027                    port: NonZeroU16::new(2000).unwrap(),
4028                })
4029                .collect()
4030        };
4031        let empty = ServerMessage::ServerResponse {
4032            servers: Vec::new(),
4033        };
4034        assert!(matches!(
4035            empty.encode(ProtocolVersion::V17),
4036            Err(CodecError::InvalidValue {
4037                field: "server endpoints",
4038                value: 0,
4039                ..
4040            })
4041        ));
4042        assert_server_round_trip(
4043            ServerMessage::ServerResponse {
4044                servers: endpoints(5),
4045            },
4046            ProtocolVersion::V17,
4047        );
4048        let too_many = ServerMessage::ServerResponse {
4049            servers: endpoints(6),
4050        };
4051        assert!(matches!(
4052            too_many.encode(ProtocolVersion::V17),
4053            Err(CodecError::CountTooLarge {
4054                field: "server endpoints",
4055                count: 6,
4056                maximum: 5,
4057                ..
4058            })
4059        ));
4060    }
4061}