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