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_frames_per_packet: 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 of one station line.
1931    /// Maps a line instance to its directory number and display name.
1932    LineStatus {
1933        instance: u32,
1934        number: String,
1935        display_name: String,
1936    },
1937    /// Supplies one chunk of the station's logical button layout.
1938    /// Uses offset and total counts to span layouts across multiple frames.
1939    ButtonTemplate {
1940        offset: u32,
1941        total: u32,
1942        buttons: Vec<ButtonTemplateEntry>,
1943    },
1944    /// Supplies the firmware or load version assigned to the station.
1945    /// Answers the station's version request during provisioning.
1946    Version { firmware: String },
1947    /// Supplies the station's signaling-server list.
1948    /// Provides primary and failover endpoints for server discovery.
1949    ServerResponse {
1950        servers: Vec<SignalingServerEndpoint>,
1951    },
1952    /// Supplies the server's current date and time.
1953    /// Synchronizes the station clock with calendar and Unix time fields.
1954    TimeDate {
1955        year: u32,
1956        month: u32,
1957        weekday: u32,
1958        day: u32,
1959        hour: u32,
1960        minute: u32,
1961        second: u32,
1962        milliseconds: u32,
1963        unix_seconds: u32,
1964    },
1965    /// Supplies the soft-key actions supported by the server.
1966    /// Defines the action identifiers referenced by soft-key sets.
1967    SoftKeyTemplate { actions: Vec<values::SoftKey> },
1968    /// Supplies the station's soft-key set profile.
1969    /// Maps call modes to ordered soft-key action lists.
1970    SoftKeySet { profile: SoftKeyProfile },
1971    /// Selects the soft-key set shown for a call.
1972    /// Uses a validity mask to enable positions in the selected set.
1973    SelectSoftKeys {
1974        line_instance: u32,
1975        call_reference: u32,
1976        set: KeyMode,
1977        /// Bit mask over positions in the selected soft-key set.
1978        valid_mask: u32,
1979    },
1980    /// Updates the station's state for a call appearance.
1981    /// Associates the new call state with a line and call reference.
1982    CallState {
1983        state: CallState,
1984        line_instance: u32,
1985        call_reference: u32,
1986    },
1987    /// Supplies calling and called party information for a call.
1988    /// Updates the station's call-information display and metadata.
1989    CallInfo {
1990        info: CallInfo,
1991        line_instance: u32,
1992        call_reference: u32,
1993    },
1994    /// Displays a call-specific prompt on the station.
1995    /// Sets its text, timeout, line, and call context.
1996    DisplayPrompt {
1997        timeout_seconds: u32,
1998        text: String,
1999        line_instance: u32,
2000        call_reference: u32,
2001    },
2002    /// Clears a call-specific prompt from the station.
2003    /// Targets the prompt associated with a line and call reference.
2004    ClearPrompt {
2005        line_instance: u32,
2006        call_reference: u32,
2007    },
2008    /// Displays a transient notification on the station.
2009    /// Supplies notification text and its timeout.
2010    DisplayNotify { timeout_seconds: u32, text: String },
2011    /// Clears the station's transient notification.
2012    /// Removes the notification created by a display-notify message.
2013    ClearNotify,
2014    /// Displays a prioritized transient notification.
2015    /// Supplies its priority, text, and timeout.
2016    DisplayPriorityNotify {
2017        timeout_seconds: u32,
2018        priority: NotificationPriority,
2019        text: String,
2020    },
2021    /// Clears notifications at a specified priority.
2022    /// Leaves notifications at other priorities unaffected.
2023    ClearPriorityNotify { priority: NotificationPriority },
2024    /// Notifies the station of a DTMF tone state.
2025    /// Carries digit and call context without requesting local tone generation.
2026    NotifyDtmfTone(DtmfToneControl),
2027    /// Instructs the station to send a DTMF tone.
2028    /// Carries the digit and call context for media signaling.
2029    SendDtmfTone(DtmfToneControl),
2030    /// Starts an announcement for a conference.
2031    /// Carries the announcement sequence, participants, mask, and play mode.
2032    StartAnnouncement {
2033        announcements: Vec<AnnouncementEntry>,
2034        end_of_ack: u32,
2035        conference_id: u32,
2036        matrix_conference_party_ids: Vec<u32>,
2037        hearing_conference_party_mask: u32,
2038        play_mode: u32,
2039    },
2040    /// Stops the active announcement for a conference.
2041    /// Targets the announcement by conference identifier.
2042    StopAnnouncement { conference_id: u32 },
2043    /// Reports or confirms announcement completion to the station.
2044    /// Carries the conference identifier and final play status.
2045    AnnouncementFinish {
2046        conference_id: u32,
2047        play_status: u32,
2048    },
2049    /// Clears conference state maintained by the station.
2050    /// Identifies the conference and associated service number.
2051    ClearConference {
2052        conference_id: ConferenceId,
2053        service_number: u32,
2054    },
2055    /// Requests creation of a station-managed conference.
2056    /// Carries the conference attributes and correlation identifiers.
2057    CreateConferenceRequest(CreateConferenceRequest),
2058    /// Requests deletion of a station-managed conference.
2059    /// Targets the conference by identifier.
2060    DeleteConferenceRequest { conference_id: ConferenceId },
2061    /// Requests changes to a station-managed conference.
2062    /// Carries the updated conference attributes and identifiers.
2063    ModifyConferenceRequest(ModifyConferenceRequest),
2064    /// Requests the station's current conference state.
2065    /// Prompts an audit response for conference reconciliation.
2066    AuditConferenceRequest,
2067    /// Requests adding a participant to a conference.
2068    /// Carries the conference, call, and participant details.
2069    AddParticipantRequest(AddParticipantRequest),
2070    /// Requests removal of a participant from a conference.
2071    /// Targets the participant by conference and call reference.
2072    DropParticipantRequest {
2073        conference_id: ConferenceId,
2074        call_reference: CallReference,
2075    },
2076    /// Requests the station's state for conference participants.
2077    /// Targets the participant set associated with a conference.
2078    AuditParticipantRequest { conference_id: ConferenceId },
2079    /// Requests changes to a conference participant.
2080    /// Carries updated participant attributes and identifiers.
2081    ChangeParticipantRequest(ChangeParticipantRequest),
2082    /// Stops a station multimedia transmit stream.
2083    /// Identifies the conference, party, and call owning the stream.
2084    StopMultimediaTransmission(MultimediaStreamControl),
2085    /// Commands a station video stream to change its flow.
2086    /// Carries the requested bit rate and stream identifiers.
2087    FlowControlCommand(VideoFlowControl),
2088    /// Closes a station multimedia receive channel.
2089    /// Identifies the conference, party, and call owning the channel.
2090    CloseMultimediaReceiveChannel(MultimediaStreamControl),
2091    /// Selects the station's video display layout for a call.
2092    /// Carries conference, call, and layout identifiers.
2093    VideoDisplayCommand {
2094        conference_id: ConferenceId,
2095        call_reference: CallReference,
2096        layout_id: u32,
2097    },
2098    /// Notifies the station of video flow-control state.
2099    /// Reports stream identifiers and the applicable bit rate.
2100    FlowControlNotify(VideoFlowControl),
2101    /// Activates the station call-control plane for a line.
2102    /// Makes the selected line instance the active call plane.
2103    ActivateCallPlane { line_instance: u32 },
2104    /// Deactivates the station call-control plane.
2105    /// Removes the currently active call-plane selection.
2106    DeactivateCallPlane,
2107    /// Confirms processing of a dial-string backspace.
2108    /// Associates the response with its line and call context.
2109    BackspaceResponse {
2110        line_instance: u32,
2111        call_reference: u32,
2112    },
2113    /// Accepts a station registration-token request.
2114    /// Allows the station to proceed with full registration.
2115    RegisterTokenAck,
2116    /// Rejects a station registration-token request.
2117    /// Supplies the delay before the station should retry.
2118    RegisterTokenReject { backoff_seconds: u32 },
2119    /// Accepts an SPCP registration-token request with a feature word.
2120    /// Allows the station to continue its SPCP registration sequence.
2121    SpcpRegisterTokenAck { features: u32 },
2122    /// Rejects an SPCP registration-token request temporarily.
2123    /// Supplies the delay before the station should request another token.
2124    SpcpRegisterTokenReject { backoff_seconds: u32 },
2125    /// Sets the station ringer behavior for a call.
2126    /// Carries mode, duration, line, and call context.
2127    SetRinger {
2128        mode: RingerMode,
2129        duration: RingDuration,
2130        line_instance: u32,
2131        call_reference: u32,
2132    },
2133    /// Sets the lamp state for a station button.
2134    /// Targets a button type and instance with the requested lamp mode.
2135    SetLamp {
2136        stimulus: ButtonType,
2137        instance: u32,
2138        mode: LampMode,
2139    },
2140    /// Enables hook-flash detection on stations that expose that capability.
2141    /// Causes subsequent hook-flash actions to be reported to call control.
2142    SetHookFlashDetect,
2143    /// Starts local tone generation on the station.
2144    /// Selects the tone, direction, line, and call context.
2145    StartTone {
2146        tone: Tone,
2147        direction: ToneDirection,
2148        line_instance: u32,
2149        call_reference: u32,
2150    },
2151    /// Stops local tone generation for a call.
2152    /// Targets the tone associated with a line and call reference.
2153    StopTone {
2154        line_instance: u32,
2155        call_reference: u32,
2156    },
2157    /// Starts station reception of a multicast media stream.
2158    /// Supplies multicast endpoint, codec, and stream identifiers.
2159    StartMulticastMediaReception(MulticastMediaReception),
2160    /// Starts station transmission to a multicast media stream.
2161    /// Supplies multicast endpoint, codec, and stream identifiers.
2162    StartMulticastMediaTransmission(MulticastMediaTransmission),
2163    /// Stops station reception of a multicast media stream.
2164    /// Targets the stream by conference, party, and call identifiers.
2165    StopMulticastMediaReception {
2166        conference_id: ConferenceId,
2167        passthrough_party_id: crate::types::PassthroughPartyId,
2168        call_reference: CallReference,
2169    },
2170    /// Stops station transmission to a multicast media stream.
2171    /// Targets the stream by conference, party, and call identifiers.
2172    StopMulticastMediaTransmission {
2173        conference_id: ConferenceId,
2174        passthrough_party_id: crate::types::PassthroughPartyId,
2175        call_reference: CallReference,
2176    },
2177    /// Opens a station audio receive channel.
2178    /// Supplies codec, packetization, source, encryption, and stream details.
2179    OpenReceiveChannel {
2180        call_reference: u32,
2181        passthrough_party_id: u32,
2182        packet_ms: u32,
2183        codec: Codec,
2184        echo_cancellation: EchoCancellation,
2185        /// Dynamic RTP payload type used for telephone-event DTMF, or zero for signaling DTMF.
2186        telephone_event_payload: u8,
2187        source_address: IpAddr,
2188        source_port: u16,
2189        encryption: Option<MediaEncryption>,
2190        /// Exact auxiliary wire fields, or encoder defaults when absent on a
2191        /// runtime-created message.
2192        wire: Option<OpenReceiveChannelWire>,
2193    },
2194    /// Closes a station audio receive channel.
2195    /// Identifies the conference, party, and call owning the stream.
2196    CloseReceiveChannel(AudioStreamControl),
2197    /// Requests media connection statistics from the station.
2198    /// Selects the call, directory number, and statistics processing mode.
2199    ConnectionStatisticsRequest {
2200        directory_number: String,
2201        call_reference: u32,
2202        processing: StatisticsProcessing,
2203    },
2204    /// Starts station audio transmission to a media endpoint.
2205    /// Supplies endpoint, traffic class, encryption, and stream details.
2206    StartMediaTransmission {
2207        call_reference: u32,
2208        passthrough_party_id: u32,
2209        endpoint: MediaEndpoint,
2210        silence_suppression: SilenceSuppression,
2211        /// Full traffic-class octet; configuration DSCP is shifted left by two.
2212        traffic_class: crate::types::MediaTrafficClass,
2213        encryption: Option<MediaEncryption>,
2214        /// Exact auxiliary wire fields, or encoder defaults when absent on a
2215        /// runtime-created message.
2216        wire: Option<StartMediaTransmissionWire>,
2217    },
2218    /// Stops a station audio transmit stream.
2219    /// Identifies the conference, party, and call owning the stream.
2220    StopMediaTransmission(AudioStreamControl),
2221    /// Starts the station's legacy receive-side media function.
2222    /// Carries no stream identity, endpoint, or codec parameters.
2223    StartMediaReception,
2224    /// Stops a legacy media-reception path for one conference party.
2225    /// Identifies the active reception by conference and passthrough party.
2226    StopMediaReception {
2227        conference_id: ConferenceId,
2228        passthrough_party_id: crate::types::PassthroughPartyId,
2229    },
2230    /// Requests subscription to an RTP DTMF payload.
2231    /// Carries the payload and transaction identity to subscribe.
2232    SubscribeDtmfPayloadRequest(DtmfPayloadRequest),
2233    /// Reports failure to establish a DTMF payload subscription.
2234    /// Identifies the payload and transaction that failed.
2235    SubscribeDtmfPayloadError(DtmfPayloadIdentity),
2236    /// Requests removal of an RTP DTMF payload subscription.
2237    /// Carries the payload and transaction identity to remove.
2238    UnsubscribeDtmfPayloadRequest(DtmfPayloadRequest),
2239    /// Reports failure to remove a DTMF payload subscription.
2240    /// Identifies the payload and transaction that failed.
2241    UnsubscribeDtmfPayloadError(DtmfPayloadIdentity),
2242    /// Sets the station speakerphone mode.
2243    /// Controls whether the station speaker audio path is active.
2244    SetSpeakerMode(SpeakerMode),
2245    /// Sets the station microphone mode.
2246    /// Controls whether the station microphone audio path is active.
2247    SetMicrophoneMode(MicrophoneMode),
2248    /// Requests a station reset or restart.
2249    /// Selects the reset behavior defined by the reset type.
2250    Reset(ResetType),
2251    /// Displays text in the station's general display area.
2252    /// Replaces the current non-call-specific display text.
2253    DisplayText { text: String },
2254    /// Clears the station's general display area.
2255    /// Removes text previously sent with a display-text message.
2256    ClearDisplay,
2257    /// Supplies call-forwarding state for one line.
2258    /// Carries destinations for all, busy, and no-answer forwarding.
2259    ForwardStatus {
2260        line_instance: u32,
2261        forward_all: Option<String>,
2262        forward_busy: Option<String>,
2263        forward_no_answer: Option<String>,
2264    },
2265    /// Supplies the contents of one station speed-dial entry.
2266    /// Maps an entry instance to its number and display name.
2267    SpeedDialStatus {
2268        instance: u32,
2269        number: String,
2270        display_name: String,
2271    },
2272    /// Displays or records the dialed number for a call.
2273    /// Associates the number with its line and call reference.
2274    DialedNumber {
2275        number: String,
2276        line_instance: u32,
2277        call_reference: u32,
2278    },
2279    /// Starts station monitoring for media-path failure.
2280    /// Supplies thresholds and stream identifiers used for detection.
2281    StartMediaFailureDetection(MediaFailureDetection),
2282    /// Carries legacy application data from the server to the station.
2283    /// Uses the fixed-format user-to-device data layout.
2284    UserToDeviceData(UserDataMessage),
2285    /// Carries version-one application data from the server.
2286    /// Supports the extended variable-length user-data layout.
2287    UserToDeviceDataV1(UserDataV1Message),
2288    /// Supplies the state of a provisioned feature button.
2289    /// Carries its type, label, instance, and feature-specific state.
2290    FeatureStatus {
2291        instance: u32,
2292        button_type: ButtonType,
2293        label: String,
2294        /// Feature-specific state word interpreted according to `button_type`.
2295        state: u32,
2296    },
2297    /// Supplies the configured service URL at an index.
2298    /// Carries its URL, label, and optional extension text.
2299    ServiceUrlStatus {
2300        index: u32,
2301        url: String,
2302        label: String,
2303        /// Additional dynamic-layout text; empty in layouts that do not carry it.
2304        extension_text: String,
2305    },
2306    /// Updates whether a call is selected on the station.
2307    /// Associates the selection state with its line and call.
2308    CallSelectStatus {
2309        /// Selection-state word retained as an extensible numeric value.
2310        status: u32,
2311        call_reference: u32,
2312        line_instance: u32,
2313    },
2314    /// Requests endpoint information for a station port.
2315    /// Carries the port identity and addressing parameters to resolve.
2316    PortRequest(PortRequest),
2317    /// Requests closure of a station port endpoint.
2318    /// Identifies the endpoint and port resources to release.
2319    PortClose(PortClose),
2320    /// Opens a station multimedia receive channel.
2321    /// Supplies codec, endpoint, and stream negotiation details.
2322    OpenMultimediaChannel(OpenMultimediaChannel),
2323    /// Starts station transmission of multimedia.
2324    /// Supplies destination, codec, bandwidth, and stream identifiers.
2325    StartMultimediaTransmission(StartMultimediaTransmission),
2326    /// Sends a stream-specific multimedia control command.
2327    /// Carries command data for video, picture, or recovery behavior.
2328    MiscellaneousCommand(MiscellaneousCommand),
2329    /// Supplies the result or state of a feature subscription.
2330    /// Carries transaction, feature, timer, and cause values.
2331    SubscriptionStatus {
2332        transaction_id: u32,
2333        feature_id: u32,
2334        timer_seconds: u32,
2335        cause: SubscriptionCause,
2336    },
2337    /// Sends a feature-subscription notification to the station.
2338    /// Carries transaction state, feature state, and display text.
2339    Notification {
2340        transaction_id: u32,
2341        feature_id: u32,
2342        status: BusyLampFieldState,
2343        text: String,
2344    },
2345    /// Sets how a call is represented in station call history.
2346    /// Associates the disposition with its line and call reference.
2347    CallHistoryDisposition {
2348        disposition: CallHistoryDisposition,
2349        line_instance: u32,
2350        call_reference: u32,
2351    },
2352    /// Returns the server's current per-line call capacity.
2353    CallCountResponse(CallCountResponse),
2354    /// Updates the station's recording indicator for a call.
2355    /// Carries the call reference and whether recording is active.
2356    RecordingStatus { call_reference: u32, active: bool },
2357    /// Preserves a recognized server-to-station message without typed decoding.
2358    /// Retains its catalog identifier and payload bytes for lossless handling.
2359    KnownOpaque(KnownOpaqueMessage),
2360    /// Preserves an unrecognized server-to-station message.
2361    /// Retains the unknown identifier and raw payload for diagnostics or forwarding.
2362    Unknown(RawMessage),
2363}
2364
2365#[cfg(test)]
2366mod tests {
2367    use super::wire::{CodecError, Frame, FrameDecoder};
2368    use super::*;
2369
2370    #[test]
2371    fn protocol_fillers_have_semantic_defaults() {
2372        assert_eq!(
2373            ButtonTemplateEntry::default(),
2374            ButtonTemplateEntry {
2375                instance: 0,
2376                button_type: ButtonType::Unused,
2377            }
2378        );
2379        assert_eq!(
2380            MessageWaitingCounts::default(),
2381            MessageWaitingCounts { new: 0, old: 0 }
2382        );
2383    }
2384
2385    const fn test_rtp_payload_number(value: u32) -> RtpPayloadNumber {
2386        match RtpPayloadNumber::new(value) {
2387            Ok(value) => value,
2388            Err(_) => panic!("test RTP payload number is out of range"),
2389        }
2390    }
2391
2392    fn decode_frame(bytes: &[u8]) -> Frame {
2393        FrameDecoder::new().push(bytes).unwrap().remove(0)
2394    }
2395
2396    fn assert_contract_alignment(frame: &Frame) {
2397        use super::catalog::PayloadLayout;
2398
2399        let contract = frame.message_type().contract().unwrap();
2400        if !matches!(
2401            contract.payload_layout,
2402            PayloadLayout::Opaque
2403                | PayloadLayout::BoundedOpaque
2404                | PayloadLayout::BoundedPreserved
2405                | PayloadLayout::VersionAndLengthSelected
2406                | PayloadLayout::MinimumLengthPreserved
2407        ) {
2408            assert_eq!(frame.payload.len() % 4, 0, "{}", contract.id);
2409        }
2410    }
2411
2412    fn assert_client_round_trip(message: ClientMessage, protocol: ProtocolVersion) {
2413        let frame = decode_frame(&message.encode(protocol).unwrap());
2414        assert_contract_alignment(&frame);
2415        assert_eq!(
2416            ClientMessage::decode_with_version(frame, protocol).unwrap(),
2417            message
2418        );
2419    }
2420
2421    fn assert_server_round_trip(message: ServerMessage, protocol: ProtocolVersion) {
2422        let frame = decode_frame(&message.encode(protocol).unwrap());
2423        assert_contract_alignment(&frame);
2424        assert_eq!(ServerMessage::decode(frame, protocol).unwrap(), message);
2425    }
2426
2427    fn assert_control_round_trip(message: ControlMessage, protocol: ProtocolVersion) {
2428        let frame = decode_frame(&message.encode(protocol).unwrap());
2429        assert_contract_alignment(&frame);
2430        assert_eq!(ControlMessage::decode(frame, protocol).unwrap(), message);
2431    }
2432
2433    #[test]
2434    fn multimedia_payload_exposes_only_typed_construction() {
2435        let capability = MultimediaVideoCapability::new(
2436            1_024,
2437            [MultimediaPictureFormat {
2438                format: VideoFormat::Cif4,
2439                minimum_picture_interval: 2,
2440            }],
2441            7,
2442            MultimediaVideoCapabilityArm::H264 {
2443                profile: 100,
2444                level: 42,
2445                custom_max_mbps: 40_500,
2446                custom_max_fs: 1_620,
2447                custom_max_dpb: 8_100,
2448                custom_max_br_and_cpb: 10_000,
2449            },
2450        )
2451        .unwrap();
2452        let payload = MultimediaPayload::new(test_rtp_payload_number(97), capability.clone());
2453        assert_eq!(payload.payload_number().get(), 97);
2454        assert_eq!(payload.descriptor().rfc_number(), 0);
2455        assert_eq!(payload.codec(), Codec::H264);
2456        assert_eq!(payload.video_capability(), Some(&capability));
2457
2458        let packetized = MultimediaPayload::with_descriptor(
2459            MultimediaPayloadDescriptor::new(4, payload.payload_number()),
2460            capability.clone(),
2461        );
2462        assert_eq!(packetized.descriptor().rfc_number(), 4);
2463        assert_eq!(packetized.payload_number(), payload.payload_number());
2464
2465        let debug = format!("{capability:?}");
2466        assert!(debug.contains("bit_rate: 1024"));
2467        assert!(!debug.contains("preserved_wire"));
2468        assert_eq!(
2469            RtpPayloadNumber::new(128),
2470            Err(RtpPayloadNumberError { actual: 128 })
2471        );
2472    }
2473
2474    #[test]
2475    fn multimedia_picture_formats_are_bounded_before_payload_construction() {
2476        let formats = [MultimediaPictureFormat {
2477            format: VideoFormat::Cif,
2478            minimum_picture_interval: 1,
2479        }; MAX_MULTIMEDIA_PICTURE_FORMATS + 1];
2480        assert_eq!(
2481            MultimediaVideoCapability::new(
2482                1_024,
2483                formats,
2484                0,
2485                MultimediaVideoCapabilityArm::H261 {
2486                    temporal_spatial_trade_off_capability: 0,
2487                    still_image_transmission: 0,
2488                },
2489            )
2490            .unwrap_err(),
2491            MultimediaCapabilityError {
2492                maximum: MAX_MULTIMEDIA_PICTURE_FORMATS,
2493                actual: MAX_MULTIMEDIA_PICTURE_FORMATS + 1,
2494            }
2495        );
2496    }
2497
2498    #[test]
2499    fn media_request_identity_is_nonzero_and_exhaustion_never_wraps() {
2500        assert_eq!(MediaRequestToken::new(0), None);
2501        let token = MediaRequestToken::new(7).unwrap();
2502        assert_eq!(MediaRequestIdentity::new(0, token), None);
2503
2504        let first = MediaRequestIdentity::new(1, token).unwrap();
2505        let second = first.checked_next().unwrap();
2506        assert_eq!(second.generation(), 2);
2507        assert_eq!(second.token().get(), 8);
2508
2509        assert_eq!(
2510            MediaRequestToken::new(u32::MAX).unwrap().checked_next(),
2511            None
2512        );
2513        let exhausted_generation =
2514            MediaRequestIdentity::new(u64::MAX, MediaRequestToken::new(1).unwrap()).unwrap();
2515        assert_eq!(exhausted_generation.checked_next(), None);
2516    }
2517
2518    #[test]
2519    fn media_request_identity_matches_only_the_current_wire_token() {
2520        let identity =
2521            MediaRequestIdentity::new(2, MediaRequestToken::new(0x1020_3040).unwrap()).unwrap();
2522
2523        assert!(identity.accepts_ack(0x1020_3040, 0, 77));
2524        assert!(identity.accepts_ack(0x1020_3040, 77, 77));
2525        assert!(!identity.accepts_ack(0x1020_3040, 78, 77));
2526        assert!(!identity.accepts_ack(0x1020_303f, 77, 77));
2527    }
2528
2529    #[test]
2530    fn zero_party_fallback_cannot_settle_a_reopened_media_generation() {
2531        let first = MediaRequestIdentity::new(1, MediaRequestToken::new(700).unwrap()).unwrap();
2532        let reopened = first.checked_next().unwrap();
2533
2534        // A zero-party ACK must carry the stable call reference.
2535        assert!(first.accepts_ack(0, 42, 42));
2536        assert!(!first.accepts_ack(0, 0, 42));
2537
2538        // The same delayed ACK is ambiguous after a reopen and fails closed.
2539        assert!(!reopened.accepts_ack(0, 42, 42));
2540        assert!(!reopened.accepts_ack(first.token().get(), 42, 42));
2541        assert!(reopened.accepts_ack(reopened.token().get(), 42, 42));
2542    }
2543
2544    #[test]
2545    fn decodes_7962_off_hook_capture_shape() {
2546        let frame = Frame::new(22, wire_id::OFF_HOOK, vec![1, 0, 0, 0, 42, 0, 0, 0]);
2547        assert_eq!(
2548            ClientMessage::decode(frame).unwrap(),
2549            ClientMessage::OffHook {
2550                line_instance: 1,
2551                call_reference: 42
2552            }
2553        );
2554    }
2555
2556    #[test]
2557    fn decodes_7961_v22_three_word_keypad_capture_shape() {
2558        let payload: Vec<_> = [8_u32, 1, 1]
2559            .into_iter()
2560            .flat_map(u32::to_le_bytes)
2561            .collect();
2562        let frame = Frame::new(22, wire_id::KEYPAD_BUTTON, payload.clone());
2563        let decoded = ClientMessage::decode(frame).unwrap();
2564        assert_eq!(
2565            decoded,
2566            ClientMessage::KeypadButton {
2567                button: Digit::Number(8),
2568                line_instance: 1,
2569                call_reference: 1,
2570                wire_layout: Some(KeypadButtonWireLayout::WithCallIdentity),
2571            }
2572        );
2573        let encoded = FrameDecoder::new()
2574            .push(&decoded.encode(ProtocolVersion::V22).unwrap())
2575            .unwrap()
2576            .remove(0);
2577        assert_eq!(encoded.payload, payload);
2578    }
2579
2580    #[test]
2581    fn register_ack_is_protocol_zero_and_has_expected_fields() {
2582        let bytes = ServerMessage::RegisterAck {
2583            keepalive_seconds: 30,
2584            secondary_keepalive_seconds: 45,
2585            protocol: ProtocolVersion::V22,
2586            features: PhoneFeatures::UTF8 | PhoneFeatures::DYNAMIC_MESSAGES,
2587            date_template: DateTemplate::default(),
2588        }
2589        .encode(ProtocolVersion::V22)
2590        .unwrap();
2591        let frame = FrameDecoder::new().push(&bytes).unwrap().remove(0);
2592        assert_eq!(frame.protocol_version, 0);
2593        assert_eq!(frame.message_id, wire_id::REGISTER_ACK);
2594        assert_eq!(
2595            ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
2596            ServerMessage::RegisterAck {
2597                keepalive_seconds: 30,
2598                secondary_keepalive_seconds: 45,
2599                protocol: ProtocolVersion::V22,
2600                features: PhoneFeatures::UTF8 | PhoneFeatures::DYNAMIC_MESSAGES,
2601                date_template: DateTemplate::default(),
2602            }
2603        );
2604    }
2605
2606    #[test]
2607    fn media_layout_sizes_match_supported_wire_specs() {
2608        let endpoint = MediaEndpoint {
2609            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
2610            rtp_port: 4000,
2611            rtcp_port: 4001,
2612            codec: Codec::Pcmu,
2613            packet_ms: 20,
2614            max_frames_per_packet: 1,
2615            telephone_event_payload: 101,
2616        };
2617        let start = ServerMessage::StartMediaTransmission {
2618            call_reference: 7,
2619            passthrough_party_id: 9,
2620            endpoint,
2621            silence_suppression: SilenceSuppression::Off,
2622            traffic_class: crate::types::MediaTrafficClass::from_wire(184),
2623            encryption: None,
2624            wire: None,
2625        }
2626        .encode(ProtocolVersion::V17)
2627        .unwrap();
2628        assert_eq!(start.len(), 144); // 12-byte header + 132-byte payload
2629        assert_eq!(&start[52..56], &184_u32.to_le_bytes());
2630        assert_eq!(&start[140..144], &1_u32.to_le_bytes());
2631        let open = ServerMessage::OpenReceiveChannel {
2632            call_reference: 7,
2633            passthrough_party_id: 9,
2634            packet_ms: 20,
2635            codec: Codec::Pcmu,
2636            echo_cancellation: EchoCancellation::On,
2637            telephone_event_payload: 101,
2638            source_address: endpoint.address,
2639            source_port: endpoint.rtp_port,
2640            encryption: None,
2641            wire: None,
2642        }
2643        .encode(ProtocolVersion::V17)
2644        .unwrap();
2645        assert_eq!(open.len(), 140); // 12-byte header + 128-byte payload
2646        assert_eq!(&open[108..112], &1_u32.to_le_bytes());
2647
2648        let start_v3 = ServerMessage::StartMediaTransmission {
2649            call_reference: 7,
2650            passthrough_party_id: 9,
2651            endpoint,
2652            silence_suppression: SilenceSuppression::Off,
2653            traffic_class: crate::types::MediaTrafficClass::default(),
2654            encryption: None,
2655            wire: None,
2656        }
2657        .encode(ProtocolVersion::V3)
2658        .unwrap();
2659        assert_eq!(start_v3.len(), 120); // 12-byte header + 108-byte payload
2660        let open_v3 = ServerMessage::OpenReceiveChannel {
2661            call_reference: 7,
2662            passthrough_party_id: 9,
2663            packet_ms: 20,
2664            codec: Codec::Pcmu,
2665            echo_cancellation: EchoCancellation::On,
2666            telephone_event_payload: 101,
2667            source_address: endpoint.address,
2668            source_port: endpoint.rtp_port,
2669            encryption: None,
2670            wire: None,
2671        }
2672        .encode(ProtocolVersion::V3)
2673        .unwrap();
2674        assert_eq!(open_v3.len(), 104); // 12-byte header + 92-byte payload
2675
2676        let start_v22 = ServerMessage::StartMediaTransmission {
2677            call_reference: 7,
2678            passthrough_party_id: 9,
2679            endpoint,
2680            silence_suppression: SilenceSuppression::Off,
2681            traffic_class: crate::types::MediaTrafficClass::default(),
2682            encryption: None,
2683            wire: None,
2684        }
2685        .encode(ProtocolVersion::V22)
2686        .unwrap();
2687        assert_eq!(start_v22.len(), 180); // 12-byte header + 168-byte payload
2688        let open_v22 = ServerMessage::OpenReceiveChannel {
2689            call_reference: 7,
2690            passthrough_party_id: 9,
2691            packet_ms: 20,
2692            codec: Codec::Pcmu,
2693            echo_cancellation: EchoCancellation::On,
2694            telephone_event_payload: 101,
2695            source_address: endpoint.address,
2696            source_port: endpoint.rtp_port,
2697            encryption: None,
2698            wire: None,
2699        }
2700        .encode(ProtocolVersion::V22)
2701        .unwrap();
2702        assert_eq!(open_v22.len(), 180); // 12-byte header + 168-byte payload
2703    }
2704
2705    #[test]
2706    fn media_close_layouts_consume_the_reference_fields_exactly() {
2707        let close = ServerMessage::CloseReceiveChannel(AudioStreamControl {
2708            conference_id: 6.into(),
2709            passthrough_party_id: 9.into(),
2710            call_reference: 7.into(),
2711            port_handling_flag: 11,
2712        });
2713        let close_v3 = close.encode(ProtocolVersion::V3).unwrap();
2714        assert_eq!(close_v3.len(), 28);
2715        assert_eq!(
2716            ServerMessage::decode(decode_frame(&close_v3), ProtocolVersion::V3).unwrap(),
2717            close
2718        );
2719        let close_v5 = close.encode(ProtocolVersion::V5).unwrap();
2720        assert_eq!(close_v5.len(), 28);
2721        assert_eq!(
2722            ServerMessage::decode(decode_frame(&close_v5), ProtocolVersion::V5).unwrap(),
2723            close
2724        );
2725
2726        let stop = ServerMessage::StopMediaTransmission(AudioStreamControl {
2727            conference_id: 6.into(),
2728            passthrough_party_id: 9.into(),
2729            call_reference: 7.into(),
2730            port_handling_flag: 11,
2731        });
2732        let bytes = stop.encode(ProtocolVersion::V22).unwrap();
2733        assert_eq!(bytes.len(), 28);
2734        assert_eq!(
2735            ServerMessage::decode(decode_frame(&bytes), ProtocolVersion::V22).unwrap(),
2736            stop
2737        );
2738
2739        let mut trailing = decode_frame(&bytes);
2740        trailing.payload.extend_from_slice(&[0; 4]);
2741        assert!(matches!(
2742            ServerMessage::decode(trailing, ProtocolVersion::V22),
2743            Err(CodecError::TrailingBytes { count: 4, .. })
2744        ));
2745    }
2746
2747    #[test]
2748    fn audio_packetization_round_trips_without_default_substitution() {
2749        let endpoint = MediaEndpoint {
2750            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
2751            rtp_port: 4000,
2752            rtcp_port: 4001,
2753            codec: Codec::G72264k,
2754            packet_ms: 30,
2755            max_frames_per_packet: 2,
2756            telephone_event_payload: 101,
2757        };
2758        for protocol in [
2759            ProtocolVersion::V3,
2760            ProtocolVersion::V17,
2761            ProtocolVersion::V22,
2762        ] {
2763            let (source_address, source_port) = if protocol.wire() < 12 {
2764                (IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
2765            } else {
2766                (endpoint.address, endpoint.rtp_port)
2767            };
2768            assert_server_round_trip(
2769                ServerMessage::OpenReceiveChannel {
2770                    call_reference: 7,
2771                    passthrough_party_id: 9,
2772                    packet_ms: 30,
2773                    codec: Codec::G72264k,
2774                    echo_cancellation: EchoCancellation::On,
2775                    telephone_event_payload: 101,
2776                    source_address,
2777                    source_port,
2778                    encryption: None,
2779                    wire: None,
2780                },
2781                protocol,
2782            );
2783            assert_server_round_trip(
2784                ServerMessage::StartMediaTransmission {
2785                    call_reference: 7,
2786                    passthrough_party_id: 9,
2787                    endpoint,
2788                    silence_suppression: SilenceSuppression::On,
2789                    traffic_class: crate::types::MediaTrafficClass::default(),
2790                    encryption: None,
2791                    wire: None,
2792                },
2793                protocol,
2794            );
2795            assert_client_round_trip(
2796                ClientMessage::MediaTransmissionFailure {
2797                    conference_id: 7,
2798                    passthrough_party_id: 9,
2799                    address: endpoint.address,
2800                    port: endpoint.rtp_port,
2801                    call_reference: 7,
2802                    status: MediaStatus::UnspecifiedError,
2803                },
2804                protocol,
2805            );
2806        }
2807    }
2808
2809    #[test]
2810    fn ipv6_audio_endpoints_require_and_round_trip_extended_layouts() {
2811        let address: IpAddr = "2001:db8::42".parse().unwrap();
2812        let endpoint = MediaEndpoint {
2813            address,
2814            rtp_port: 40_000,
2815            rtcp_port: 40_001,
2816            codec: Codec::G72264k,
2817            packet_ms: 20,
2818            max_frames_per_packet: 1,
2819            telephone_event_payload: 101,
2820        };
2821        let start = ServerMessage::StartMediaTransmission {
2822            call_reference: 7,
2823            passthrough_party_id: 9,
2824            endpoint,
2825            silence_suppression: SilenceSuppression::Off,
2826            traffic_class: crate::types::MediaTrafficClass::default(),
2827            encryption: None,
2828            wire: None,
2829        };
2830        let receive_ack = ClientMessage::OpenReceiveChannelAck {
2831            status: MediaStatus::Ok,
2832            address,
2833            port: endpoint.rtp_port,
2834            passthrough_party_id: 9,
2835            call_reference: 7,
2836        };
2837        let transmit_ack = ClientMessage::StartMediaTransmissionAck(MediaTransmissionAck {
2838            conference_id: 6,
2839            passthrough_party_id: 9,
2840            call_reference: 7,
2841            status: MediaStatus::Ok,
2842            address,
2843            port: endpoint.rtp_port,
2844            wire: None,
2845        });
2846        let failure = ClientMessage::MediaTransmissionFailure {
2847            conference_id: 7,
2848            passthrough_party_id: 9,
2849            address,
2850            port: endpoint.rtp_port,
2851            call_reference: 7,
2852            status: MediaStatus::UnspecifiedError,
2853        };
2854
2855        for protocol in [ProtocolVersion::V17, ProtocolVersion::V22] {
2856            assert_server_round_trip(start.clone(), protocol);
2857            assert_client_round_trip(receive_ack.clone(), protocol);
2858            assert_client_round_trip(transmit_ack.clone(), protocol);
2859            assert_client_round_trip(failure.clone(), protocol);
2860        }
2861        for result in [
2862            start.encode(ProtocolVersion::V16),
2863            receive_ack.encode(ProtocolVersion::V16),
2864            transmit_ack.encode(ProtocolVersion::V16),
2865            failure.encode(ProtocolVersion::V16),
2866            failure.encode(ProtocolVersion::V3),
2867        ] {
2868            assert!(matches!(
2869                result,
2870                Err(CodecError::InvalidValue {
2871                    field: "IP address family for pre-v17 protocol"
2872                        | "IP address family for this protocol version",
2873                    ..
2874                })
2875            ));
2876        }
2877    }
2878
2879    #[test]
2880    fn skinny_dtmf_disables_the_telephone_event_payload_in_both_directions() {
2881        let endpoint = MediaEndpoint {
2882            address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
2883            rtp_port: 4000,
2884            rtcp_port: 4001,
2885            codec: Codec::Pcmu,
2886            packet_ms: 20,
2887            max_frames_per_packet: 1,
2888            telephone_event_payload: 0,
2889        };
2890        for protocol in [
2891            ProtocolVersion::V3,
2892            ProtocolVersion::V17,
2893            ProtocolVersion::V22,
2894        ] {
2895            let (source_address, source_port) = if protocol.wire() < 12 {
2896                (IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
2897            } else {
2898                (endpoint.address, endpoint.rtp_port)
2899            };
2900            assert_server_round_trip(
2901                ServerMessage::OpenReceiveChannel {
2902                    call_reference: 7,
2903                    passthrough_party_id: 9,
2904                    packet_ms: 20,
2905                    codec: Codec::Pcmu,
2906                    echo_cancellation: EchoCancellation::On,
2907                    telephone_event_payload: 0,
2908                    source_address,
2909                    source_port,
2910                    encryption: None,
2911                    wire: None,
2912                },
2913                protocol,
2914            );
2915            assert_server_round_trip(
2916                ServerMessage::StartMediaTransmission {
2917                    call_reference: 7,
2918                    passthrough_party_id: 9,
2919                    endpoint,
2920                    silence_suppression: SilenceSuppression::Off,
2921                    traffic_class: crate::types::MediaTrafficClass::default(),
2922                    encryption: None,
2923                    wire: None,
2924                },
2925                protocol,
2926            );
2927        }
2928    }
2929
2930    #[test]
2931    fn open_receive_wildcard_source_round_trips_for_all_supported_layouts() {
2932        for protocol in [
2933            ProtocolVersion::V3,
2934            ProtocolVersion::V17,
2935            ProtocolVersion::V22,
2936        ] {
2937            assert_server_round_trip(
2938                ServerMessage::OpenReceiveChannel {
2939                    call_reference: 1,
2940                    passthrough_party_id: 1,
2941                    packet_ms: 20,
2942                    codec: Codec::Pcma,
2943                    echo_cancellation: EchoCancellation::Off,
2944                    telephone_event_payload: 101,
2945                    source_address: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
2946                    source_port: 0,
2947                    encryption: None,
2948                    wire: None,
2949                },
2950                protocol,
2951            );
2952        }
2953    }
2954
2955    #[test]
2956    fn media_encryption_round_trips_without_exposing_key_material() {
2957        let key = b"private-key-1234";
2958        let salt = b"private-salt-123";
2959        let encryption =
2960            MediaEncryption::new(EncryptionMethod::Aes128HmacSha1_80, key, salt, 1, 64).unwrap();
2961        assert_eq!(encryption.key(), key);
2962        assert_eq!(encryption.salt(), salt);
2963
2964        let debug = format!("{encryption:?}");
2965        assert!(debug.contains("<redacted>"));
2966        assert!(!debug.contains("112, 114, 105, 118, 97, 116, 101"));
2967        assert!(!debug.contains("private-key"));
2968        let endpoint = MediaEndpoint {
2969            address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)),
2970            rtp_port: 40_000,
2971            rtcp_port: 40_001,
2972            codec: Codec::Pcmu,
2973            packet_ms: 20,
2974            max_frames_per_packet: 1,
2975            telephone_event_payload: 101,
2976        };
2977
2978        for protocol in [
2979            ProtocolVersion::new(12).unwrap(),
2980            ProtocolVersion::V17,
2981            ProtocolVersion::V22,
2982        ] {
2983            let open = ServerMessage::OpenReceiveChannel {
2984                call_reference: 7,
2985                passthrough_party_id: 9,
2986                packet_ms: 20,
2987                codec: Codec::Pcmu,
2988                echo_cancellation: EchoCancellation::On,
2989                telephone_event_payload: 101,
2990                source_address: endpoint.address,
2991                source_port: endpoint.rtp_port,
2992                encryption: Some(encryption.clone()),
2993                wire: None,
2994            };
2995            let open_debug = format!("{open:?}");
2996            assert!(open_debug.contains("<redacted>"));
2997            assert!(!open_debug.contains("112, 114, 105, 118, 97, 116, 101"));
2998            assert_server_round_trip(open, protocol);
2999            assert_server_round_trip(
3000                ServerMessage::StartMediaTransmission {
3001                    call_reference: 7,
3002                    passthrough_party_id: 9,
3003                    endpoint,
3004                    silence_suppression: SilenceSuppression::Off,
3005                    traffic_class: crate::types::MediaTrafficClass::default(),
3006                    encryption: Some(encryption.clone()),
3007                    wire: None,
3008                },
3009                protocol,
3010            );
3011        }
3012    }
3013
3014    #[test]
3015    fn media_encryption_rejects_oversized_secrets_with_metadata_only_errors() {
3016        let oversized_key = [0xa5; 17];
3017        let error = MediaEncryption::new(
3018            EncryptionMethod::Aes128HmacSha1_32,
3019            &oversized_key,
3020            &[],
3021            0,
3022            0,
3023        )
3024        .unwrap_err();
3025        assert!(matches!(
3026            error,
3027            CodecError::SecretTooLong {
3028                field: "media encryption key",
3029                actual: 17,
3030                maximum: 16,
3031            }
3032        ));
3033        assert!(!error.to_string().contains("165"));
3034
3035        let oversized_salt = [0x5a; 17];
3036        let error = MediaEncryption::new(
3037            EncryptionMethod::Aes128HmacSha1_32,
3038            &[],
3039            &oversized_salt,
3040            0,
3041            0,
3042        )
3043        .unwrap_err();
3044        assert!(matches!(
3045            error,
3046            CodecError::SecretTooLong {
3047                field: "media encryption salt",
3048                actual: 17,
3049                maximum: 16,
3050            }
3051        ));
3052        assert!(!error.to_string().contains("90"));
3053    }
3054
3055    #[test]
3056    fn common_client_messages_round_trip_semantically() {
3057        assert_client_round_trip(
3058            ClientMessage::FeatureStatusRequest {
3059                index: 7,
3060                capabilities: 1,
3061            },
3062            ProtocolVersion::V22,
3063        );
3064        assert_client_round_trip(
3065            ClientMessage::OffHookWithCallingParty {
3066                calling_party_number: "1001".into(),
3067                voice_mailbox: "5001".into(),
3068                line_instance: 1,
3069            },
3070            ProtocolVersion::V3,
3071        );
3072        assert_client_round_trip(
3073            ClientMessage::RegisterToken(RegisterTokenMessage {
3074                device_id: DeviceId::new("SEP001122334455").unwrap(),
3075                device_instance: 2,
3076                address: "2001:db8::42".parse().unwrap(),
3077                device_type: DeviceType::Cisco7962,
3078                flags: 6,
3079            }),
3080            ProtocolVersion::V22,
3081        );
3082        assert_control_round_trip(
3083            ControlMessage::MediaResourceNotification(MediaResourceNotification {
3084                device_type: DeviceType::Unknown(0xfeed),
3085                in_service_streams: 2,
3086                max_streams_per_conference: 4,
3087                out_of_service_streams: 1,
3088            }),
3089            ProtocolVersion::V17,
3090        );
3091        assert_client_round_trip(
3092            ClientMessage::SubscriptionStatusRequest(SubscriptionRequest {
3093                transaction_id: 0x4b,
3094                feature_id: 1,
3095                timer_seconds: 30,
3096                subscription_id: "4000".into(),
3097            }),
3098            ProtocolVersion::V22,
3099        );
3100        for message in [
3101            ClientMessage::SubscribeDtmfPayloadResponse(DtmfPayloadIdentity {
3102                payload_type: 101,
3103                conference_id: 42,
3104                passthrough_party_id: 7,
3105            }),
3106            ClientMessage::UnsubscribeDtmfPayloadResponse(DtmfPayloadIdentity {
3107                payload_type: 102,
3108                conference_id: 43,
3109                passthrough_party_id: 8,
3110            }),
3111        ] {
3112            let encoded = message.encode(ProtocolVersion::V22).unwrap();
3113            let frame = decode_frame(&encoded);
3114            assert_eq!(frame.payload.len(), 12);
3115            assert_eq!(
3116                ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3117                message
3118            );
3119        }
3120        assert_client_round_trip(
3121            ClientMessage::DeviceToUserDataV1(UserDataV1Message {
3122                application_id: 7,
3123                line_instance: 1,
3124                call_reference: 42,
3125                transaction_id: 9,
3126                sequence_flag: 1,
3127                display_priority: 2,
3128                conference_id: 42,
3129                application_instance_id: 3,
3130                routing: 4,
3131                data: b"<CiscoIPPhoneText/>".to_vec(),
3132            }),
3133            ProtocolVersion::V17,
3134        );
3135        assert_client_round_trip(
3136            ClientMessage::DeviceToUserDataResponse(UserDataMessage {
3137                application_id: 8,
3138                line_instance: 2,
3139                call_reference: 43,
3140                transaction_id: 10,
3141                data: b"<CiscoIPPhoneResponse/>".to_vec(),
3142            }),
3143            ProtocolVersion::V17,
3144        );
3145        assert_client_round_trip(
3146            ClientMessage::DeviceToUserData(UserDataMessage {
3147                application_id: 9,
3148                line_instance: 2,
3149                call_reference: 44,
3150                transaction_id: 11,
3151                data: b"<CiscoIPPhoneInput/>".to_vec(),
3152            }),
3153            ProtocolVersion::V17,
3154        );
3155        assert_client_round_trip(
3156            ClientMessage::DeviceToUserDataResponseV1(UserDataV1Message {
3157                application_id: 9,
3158                line_instance: 2,
3159                call_reference: 44,
3160                transaction_id: 11,
3161                sequence_flag: 2,
3162                display_priority: 1,
3163                conference_id: 44,
3164                application_instance_id: 9,
3165                routing: 1,
3166                data: b"<CiscoIPPhoneResponse/>".to_vec(),
3167            }),
3168            ProtocolVersion::V17,
3169        );
3170        assert_client_round_trip(
3171            ClientMessage::LocationInfo {
3172                xml: "<location><building>west</building></location>".into(),
3173            },
3174            ProtocolVersion::V22,
3175        );
3176        assert_client_round_trip(
3177            ClientMessage::XmlAlarm(
3178                XmlAlarmMessage::from_xml(b"<alarm><severity>warning</severity></alarm>").unwrap(),
3179            ),
3180            ProtocolVersion::V22,
3181        );
3182        assert_client_round_trip(
3183            ClientMessage::CallCountRequest(CallCountRequestPayload::LegacyWord(2)),
3184            ProtocolVersion::V22,
3185        );
3186        assert_control_round_trip(
3187            ControlMessage::PortResponse(PortEndpoint {
3188                conference_id: 42,
3189                call_reference: 42,
3190                passthrough_party_id: 8,
3191                address: "2001:db8::8".parse().unwrap(),
3192                rtp_port: 16_000,
3193                rtcp_port: 16_001,
3194                media_type: Some(MediaType::Audio),
3195            }),
3196            ProtocolVersion::V22,
3197        );
3198        assert_control_round_trip(
3199            ControlMessage::CreateConferenceResponse(CreateConferenceResponse {
3200                conference_id: ConferenceId::new(42),
3201                result: CreateConferenceResult::Ok,
3202                passthrough_data: vec![1, 2, 3],
3203            }),
3204            ProtocolVersion::V22,
3205        );
3206        assert_control_round_trip(
3207            ControlMessage::DeleteConferenceResponse {
3208                conference_id: ConferenceId::new(42),
3209                result: DeleteConferenceResult::ConferenceDoesNotExist,
3210            },
3211            ProtocolVersion::V22,
3212        );
3213        assert_control_round_trip(
3214            ControlMessage::ModifyConferenceResponse(ModifyConferenceResponse {
3215                conference_id: ConferenceId::new(42),
3216                result: ModifyConferenceResult::MoreActiveCallsThanReserved,
3217                passthrough_data: vec![4, 5],
3218            }),
3219            ProtocolVersion::V22,
3220        );
3221        assert_control_round_trip(
3222            ControlMessage::AuditConferenceResponse(AuditConferenceResponse {
3223                last: 1,
3224                entries: vec![AuditConferenceEntry {
3225                    conference_id: ConferenceId::new(42),
3226                    resource_type: ConferenceResourceType::Conference,
3227                    reserved_participants: 8,
3228                    active_participants: 3,
3229                    application_id: ApplicationId::new(7),
3230                    application_conference_id: "festival-42".into(),
3231                    application_data: "main-stage".into(),
3232                }],
3233            }),
3234            ProtocolVersion::V22,
3235        );
3236        assert_control_round_trip(
3237            ControlMessage::AddParticipantResponse(AddParticipantResponse {
3238                conference_id: ConferenceId::new(42),
3239                call_reference: CallReference::new(100),
3240                result: AddParticipantResult::Ok,
3241                bridge_participant_id: BoundedBytes::try_from(vec![3; 257]).unwrap(),
3242            }),
3243            ProtocolVersion::V22,
3244        );
3245        assert_control_round_trip(
3246            ControlMessage::AuditParticipantResponse(AuditParticipantResponse {
3247                result: AuditParticipantResult::Ok,
3248                last: 1,
3249                conference_id: ConferenceId::new(42),
3250                number_of_entries: 2,
3251                participant_entries: vec![1, 2, 3, 4],
3252            }),
3253            ProtocolVersion::V22,
3254        );
3255    }
3256
3257    #[test]
3258    fn common_server_messages_round_trip_semantically() {
3259        assert_server_round_trip(
3260            ServerMessage::SpeedDialStatus {
3261                instance: 7,
3262                number: "2001".into(),
3263                display_name: "Reception".into(),
3264            },
3265            ProtocolVersion::V3,
3266        );
3267        assert_server_round_trip(
3268            ServerMessage::ServiceUrlStatus {
3269                index: 4,
3270                url: "http://services.invalid/directory".into(),
3271                label: "Directory".into(),
3272                extension_text: String::new(),
3273            },
3274            ProtocolVersion::V3,
3275        );
3276        for protocol in [
3277            ProtocolVersion::V3,
3278            ProtocolVersion::V17,
3279            ProtocolVersion::V22,
3280        ] {
3281            assert_server_round_trip(
3282                ServerMessage::ConnectionStatisticsRequest {
3283                    directory_number: "1001".into(),
3284                    call_reference: 42,
3285                    processing: StatisticsProcessing::DoNotClear,
3286                },
3287                protocol,
3288            );
3289        }
3290        assert_server_round_trip(
3291            ServerMessage::DisplayPriorityNotify {
3292                timeout_seconds: 5,
3293                priority: NotificationPriority::Voicemail,
3294                text: "Incoming call".into(),
3295            },
3296            ProtocolVersion::V17,
3297        );
3298        assert_server_round_trip(
3299            ServerMessage::FeatureStatus {
3300                instance: 2,
3301                button_type: ButtonType::BlfSpeedDial,
3302                label: "Support".into(),
3303                state: 0x0002_0101,
3304            },
3305            ProtocolVersion::V22,
3306        );
3307        assert_server_round_trip(
3308            ServerMessage::PortRequest(PortRequest {
3309                conference_id: 42.into(),
3310                call_reference: 42.into(),
3311                passthrough_party_id: 9.into(),
3312                transport: MediaTransport::Rtp,
3313                address_type: Some(IpAddressType::Ipv4AndIpv6),
3314                media_type: Some(MediaType::Audio),
3315            }),
3316            ProtocolVersion::V22,
3317        );
3318        assert_server_round_trip(
3319            ServerMessage::Notification {
3320                transaction_id: 3,
3321                feature_id: 1,
3322                status: BusyLampFieldState::Unknown(77),
3323                text: "4000".into(),
3324            },
3325            ProtocolVersion::V22,
3326        );
3327        assert_server_round_trip(
3328            ServerMessage::SubscriptionStatus {
3329                transaction_id: 3,
3330                feature_id: 1,
3331                timer_seconds: 30,
3332                cause: SubscriptionCause::Ok,
3333            },
3334            ProtocolVersion::V22,
3335        );
3336        assert_server_round_trip(
3337            ServerMessage::UserToDeviceData(UserDataMessage {
3338                application_id: 7,
3339                line_instance: 1,
3340                call_reference: 42,
3341                transaction_id: 9,
3342                data: b"<CiscoIPPhoneText/>".to_vec(),
3343            }),
3344            ProtocolVersion::V17,
3345        );
3346        assert_server_round_trip(
3347            ServerMessage::UserToDeviceDataV1(UserDataV1Message {
3348                application_id: 7,
3349                line_instance: 1,
3350                call_reference: 42,
3351                transaction_id: 9,
3352                sequence_flag: 2,
3353                display_priority: 1,
3354                conference_id: 42,
3355                application_instance_id: 7,
3356                routing: 1,
3357                data: b"<CiscoIPPhoneMenu/>".to_vec(),
3358            }),
3359            ProtocolVersion::V17,
3360        );
3361        assert_server_round_trip(
3362            ServerMessage::CallHistoryDisposition {
3363                disposition: CallHistoryDisposition::Missed,
3364                line_instance: 1,
3365                call_reference: 42,
3366            },
3367            ProtocolVersion::V22,
3368        );
3369        assert_server_round_trip(
3370            ServerMessage::CallCountResponse(CallCountResponse {
3371                total_configured_lines: 2,
3372                starting_line_instance: 1,
3373                line_data: vec![
3374                    CallCountLineData {
3375                        max_calls: 4,
3376                        busy_trigger: 2,
3377                    },
3378                    CallCountLineData {
3379                        max_calls: 2,
3380                        busy_trigger: 1,
3381                    },
3382                ],
3383            }),
3384            ProtocolVersion::V22,
3385        );
3386        for message in [
3387            ServerMessage::SubscribeDtmfPayloadRequest(DtmfPayloadRequest {
3388                payload_type: 101,
3389                conference_id: 42,
3390                passthrough_party_id: 7,
3391                dtmf_type: 2,
3392            }),
3393            ServerMessage::SubscribeDtmfPayloadError(DtmfPayloadIdentity {
3394                payload_type: 102,
3395                conference_id: 43,
3396                passthrough_party_id: 8,
3397            }),
3398            ServerMessage::UnsubscribeDtmfPayloadRequest(DtmfPayloadRequest {
3399                payload_type: 103,
3400                conference_id: 44,
3401                passthrough_party_id: 9,
3402                dtmf_type: 3,
3403            }),
3404            ServerMessage::UnsubscribeDtmfPayloadError(DtmfPayloadIdentity {
3405                payload_type: 104,
3406                conference_id: 45,
3407                passthrough_party_id: 10,
3408            }),
3409        ] {
3410            let encoded = message.encode(ProtocolVersion::V22).unwrap();
3411            let frame = decode_frame(&encoded);
3412            assert!(matches!(frame.payload.len(), 12 | 16));
3413            assert_eq!(
3414                ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
3415                message
3416            );
3417        }
3418        assert_server_round_trip(
3419            ServerMessage::RecordingStatus {
3420                call_reference: 42,
3421                active: true,
3422            },
3423            ProtocolVersion::V22,
3424        );
3425        assert_control_round_trip(
3426            ControlMessage::StartAnnouncement {
3427                announcements: vec![
3428                    AnnouncementEntry {
3429                        locale: 1,
3430                        country: 46,
3431                        tone: Tone::Zip,
3432                    },
3433                    AnnouncementEntry {
3434                        locale: 0,
3435                        country: 0,
3436                        tone: Tone::Silence,
3437                    },
3438                    AnnouncementEntry {
3439                        locale: 2,
3440                        country: 1,
3441                        tone: Tone::RecorderWarning,
3442                    },
3443                ],
3444                end_of_ack: EndOfAnnouncementAck::Required,
3445                conference_id: 42,
3446                matrix_conference_party_ids: vec![7, 0, 9],
3447                hearing_conference_party_mask: 0b101,
3448                play_mode: AnnouncementPlayMode::Continuous,
3449            },
3450            ProtocolVersion::V22,
3451        );
3452        assert_control_round_trip(
3453            ControlMessage::StopAnnouncement { conference_id: 42 },
3454            ProtocolVersion::V22,
3455        );
3456        assert_control_round_trip(
3457            ControlMessage::AnnouncementFinish {
3458                conference_id: 42,
3459                play_status: AnnouncementPlayStatus::Unknown(3),
3460            },
3461            ProtocolVersion::V22,
3462        );
3463        assert_control_round_trip(
3464            ControlMessage::ClearConference {
3465                conference_id: ConferenceId::new(42),
3466                service_number: 3,
3467            },
3468            ProtocolVersion::V22,
3469        );
3470        assert_control_round_trip(
3471            ControlMessage::CreateConferenceRequest(CreateConferenceRequest {
3472                conference_id: ConferenceId::new(42),
3473                reserved_participants: 8,
3474                resource_type: ConferenceResourceType::Conference,
3475                application_id: ApplicationId::new(7),
3476                application_conference_id: "festival-42".into(),
3477                application_data: "main-stage".into(),
3478                passthrough_data: vec![1, 2, 3],
3479            }),
3480            ProtocolVersion::V22,
3481        );
3482        assert_control_round_trip(
3483            ControlMessage::DeleteConferenceRequest {
3484                conference_id: ConferenceId::new(42),
3485            },
3486            ProtocolVersion::V22,
3487        );
3488        assert_control_round_trip(
3489            ControlMessage::ModifyConferenceRequest(ModifyConferenceRequest {
3490                conference_id: ConferenceId::new(42),
3491                reserved_participants: 12,
3492                application_id: ApplicationId::new(7),
3493                application_conference_id: "festival-42".into(),
3494                application_data: "main-stage".into(),
3495                passthrough_data: vec![4, 5],
3496            }),
3497            ProtocolVersion::V22,
3498        );
3499        assert_control_round_trip(ControlMessage::AuditConferenceRequest, ProtocolVersion::V22);
3500        assert_control_round_trip(
3501            ControlMessage::AddParticipantRequest(AddParticipantRequest {
3502                conference_id: ConferenceId::new(42),
3503                participant: ConferenceParticipant {
3504                    call_reference: CallReference::new(100),
3505                    presentation_restrictions: PartyInformationRestrictions::CALLING_NUMBER,
3506                    name: "Festival Caller".into(),
3507                    number: "1001".into(),
3508                    conference_name: "Main Stage".into(),
3509                },
3510            }),
3511            ProtocolVersion::V22,
3512        );
3513        assert_control_round_trip(
3514            ControlMessage::DropParticipantRequest {
3515                conference_id: ConferenceId::new(42),
3516                call_reference: CallReference::new(100),
3517            },
3518            ProtocolVersion::V22,
3519        );
3520        assert_control_round_trip(
3521            ControlMessage::AuditParticipantRequest {
3522                conference_id: ConferenceId::new(42),
3523            },
3524            ProtocolVersion::V22,
3525        );
3526    }
3527
3528    #[test]
3529    fn connection_statistics_round_trip_all_layouts_and_redact_opaque_fields() {
3530        let statistics = ConnectionStatistics {
3531            directory_number: "2002".into(),
3532            call_reference: 42,
3533            processing: StatisticsProcessing::Clear,
3534            packets_sent: 100,
3535            octets_sent: 8_000,
3536            packets_received: 98,
3537            octets_received: 7_840,
3538            packets_lost: 2,
3539            jitter_millis: 7,
3540            latency_millis: 18,
3541            quality: ConnectionQualityStatistics::new(b"MLQK=4.5;Secret=opaque".to_vec()).unwrap(),
3542        };
3543        for protocol in [
3544            ProtocolVersion::V3,
3545            ProtocolVersion::V19,
3546            ProtocolVersion::V22,
3547        ] {
3548            assert_client_round_trip(
3549                ClientMessage::ConnectionStatisticsResponse(statistics.clone()),
3550                protocol,
3551            );
3552        }
3553        let debug = format!("{statistics:?}");
3554        assert!(!debug.contains("2002"));
3555        assert!(!debug.contains("Secret"));
3556        assert!(debug.contains("byte_count"));
3557        assert!(matches!(
3558            ConnectionQualityStatistics::new(vec![0; CONNECTION_QUALITY_MAX_BYTES + 1]),
3559            Err(CodecError::CountTooLarge {
3560                field: "quality statistics",
3561                maximum: CONNECTION_QUALITY_MAX_BYTES,
3562                ..
3563            })
3564        ));
3565    }
3566
3567    #[test]
3568    fn dtmf_subscription_messages_require_their_exact_word_layouts() {
3569        for message_id in [
3570            wire_id::SUBSCRIBE_DTMF_PAYLOAD_RES,
3571            wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_RES,
3572        ] {
3573            assert!(ClientMessage::decode(Frame::new(22, message_id, Vec::new())).is_err());
3574            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 11])).is_err());
3575            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 12])).is_ok());
3576            assert!(ClientMessage::decode(Frame::new(22, message_id, vec![0; 13])).is_err());
3577        }
3578        for (message_id, size) in [
3579            (wire_id::SUBSCRIBE_DTMF_PAYLOAD_REQ, 16),
3580            (wire_id::SUBSCRIBE_DTMF_PAYLOAD_ERR, 12),
3581            (wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_REQ, 16),
3582            (wire_id::UNSUBSCRIBE_DTMF_PAYLOAD_ERR, 12),
3583        ] {
3584            assert!(
3585                ServerMessage::decode(
3586                    Frame::new(22, message_id, vec![0; size - 1]),
3587                    ProtocolVersion::V22,
3588                )
3589                .is_err()
3590            );
3591            assert!(
3592                ServerMessage::decode(
3593                    Frame::new(22, message_id, vec![0; size]),
3594                    ProtocolVersion::V22,
3595                )
3596                .is_ok()
3597            );
3598            assert!(
3599                ServerMessage::decode(
3600                    Frame::new(22, message_id, vec![0; size + 1]),
3601                    ProtocolVersion::V22,
3602                )
3603                .is_err()
3604            );
3605        }
3606    }
3607
3608    #[test]
3609    fn announcement_lists_enforce_station_bounds() {
3610        let error = ServerMessage::StartAnnouncement {
3611            announcements: vec![
3612                AnnouncementEntry {
3613                    locale: 1,
3614                    country: 1,
3615                    tone: Tone::Zip,
3616                };
3617                33
3618            ],
3619            end_of_ack: 0,
3620            conference_id: 1,
3621            matrix_conference_party_ids: Vec::new(),
3622            hearing_conference_party_mask: 0,
3623            play_mode: 0,
3624        }
3625        .encode(ProtocolVersion::V22)
3626        .unwrap_err();
3627        assert!(matches!(
3628            error,
3629            CodecError::CountTooLarge {
3630                field: "announcements",
3631                count: 33,
3632                maximum: 32,
3633                ..
3634            }
3635        ));
3636
3637        let error = ServerMessage::StartAnnouncement {
3638            announcements: Vec::new(),
3639            end_of_ack: 0,
3640            conference_id: 1,
3641            matrix_conference_party_ids: (1..=17).collect(),
3642            hearing_conference_party_mask: 0,
3643            play_mode: 0,
3644        }
3645        .encode(ProtocolVersion::V22)
3646        .unwrap_err();
3647        assert!(matches!(
3648            error,
3649            CodecError::CountTooLarge {
3650                field: "matrix conference party identifiers",
3651                count: 17,
3652                maximum: 16,
3653                ..
3654            }
3655        ));
3656    }
3657
3658    #[test]
3659    fn enbloc_uses_the_protocol_19_text_width_boundary() {
3660        for (protocol, payload_len, line_offset) in [
3661            (ProtocolVersion::V18, 28, 24),
3662            (ProtocolVersion::V19, 32, 28),
3663        ] {
3664            let message = ClientMessage::EnblocCall {
3665                called_party: "9801".into(),
3666                line_instance: 3,
3667            };
3668            let frame = FrameDecoder::new()
3669                .push(&message.encode(protocol).unwrap())
3670                .unwrap()
3671                .remove(0);
3672            assert_eq!(frame.payload.len(), payload_len);
3673            assert_eq!(
3674                &frame.payload[line_offset..line_offset + 4],
3675                &3_u32.to_le_bytes()
3676            );
3677            assert_eq!(
3678                ClientMessage::decode_with_version(frame, protocol).unwrap(),
3679                message
3680            );
3681        }
3682    }
3683
3684    #[test]
3685    fn supplemental_client_messages_have_typed_layouts() {
3686        let ports = ClientMessage::MediaPortList(MediaPortList {
3687            rtp_ports: vec![16_000, 16_002],
3688        });
3689        let frame = decode_frame(&ports.encode(ProtocolVersion::V22).unwrap());
3690        assert_eq!(frame.message_id, wire_id::MEDIA_PORT_LIST);
3691        assert_eq!(frame.payload.len(), 68);
3692        assert_eq!(
3693            &frame.payload[..12],
3694            &[2, 0, 0, 0, 0x80, 0x3e, 0, 0, 0x82, 0x3e, 0, 0]
3695        );
3696        assert_eq!(
3697            ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3698            ports
3699        );
3700
3701        let token = ClientMessage::SpcpRegisterToken(SpcpRegisterTokenMessage {
3702            device_id: DeviceId::new("SEP001122334455").unwrap(),
3703            device_instance: 2,
3704            address: Ipv4Addr::new(192, 0, 2, 10),
3705            device_type: DeviceType::Cisco7962,
3706            max_streams: 0x0102_0304,
3707        });
3708        let frame = decode_frame(&token.encode(ProtocolVersion::V22).unwrap());
3709        assert_eq!(frame.message_id, wire_id::SPCP_REGISTER_TOKEN_REQ);
3710        assert_eq!(frame.payload.len(), 36);
3711        assert_eq!(&frame.payload[16..20], &[0; 4]);
3712        assert_eq!(&frame.payload[24..28], &[10, 2, 0, 192]);
3713        assert_eq!(&frame.payload[32..36], &[4, 3, 2, 1]);
3714        assert_eq!(
3715            ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap(),
3716            token
3717        );
3718
3719        let oversized = ClientMessage::MediaPortList(MediaPortList {
3720            rtp_ports: vec![16_000; MEDIA_PORT_LIST_MAX_PORTS + 1],
3721        });
3722        assert!(matches!(
3723            oversized.encode(ProtocolVersion::V22),
3724            Err(CodecError::CountTooLarge { .. })
3725        ));
3726
3727        let mut invalid_port = vec![0; 68];
3728        invalid_port[..4].copy_from_slice(&1_u32.to_le_bytes());
3729        invalid_port[4..8].copy_from_slice(&65_536_u32.to_le_bytes());
3730        assert!(matches!(
3731            ClientMessage::decode_with_version(
3732                Frame::new(22, wire_id::MEDIA_PORT_LIST, invalid_port),
3733                ProtocolVersion::V22,
3734            ),
3735            Err(CodecError::InvalidValue {
3736                field: "RTP port",
3737                ..
3738            })
3739        ));
3740    }
3741
3742    #[test]
3743    fn supplemental_server_messages_have_typed_layouts() {
3744        for (message, id, payload) in [
3745            (
3746                ServerMessage::SetHookFlashDetect,
3747                wire_id::SET_HOOK_FLASH_DETECT,
3748                vec![],
3749            ),
3750            (
3751                ServerMessage::StartMediaReception,
3752                wire_id::START_MEDIA_RECEPTION,
3753                vec![],
3754            ),
3755            (
3756                ServerMessage::StopMediaReception {
3757                    conference_id: 0x0102_0304.into(),
3758                    passthrough_party_id: 0x0506_0708.into(),
3759                },
3760                wire_id::STOP_MEDIA_RECEPTION,
3761                vec![4, 3, 2, 1, 8, 7, 6, 5],
3762            ),
3763            (
3764                ServerMessage::EnunciatorCommand,
3765                wire_id::ENUNCIATOR_COMMAND,
3766                vec![],
3767            ),
3768            (
3769                ServerMessage::SpcpRegisterTokenAck {
3770                    features: 0x0102_0304,
3771                },
3772                wire_id::SPCP_REGISTER_TOKEN_ACK,
3773                vec![4, 3, 2, 1],
3774            ),
3775            (
3776                ServerMessage::SpcpRegisterTokenReject {
3777                    backoff_seconds: 60,
3778                },
3779                wire_id::SPCP_REGISTER_TOKEN_REJECT,
3780                vec![60, 0, 0, 0],
3781            ),
3782        ] {
3783            let frame = decode_frame(&message.encode(ProtocolVersion::V22).unwrap());
3784            assert_eq!(frame.message_id, id);
3785            assert_eq!(frame.payload, payload);
3786            assert_eq!(
3787                ServerMessage::decode(frame, ProtocolVersion::V22).unwrap(),
3788                message
3789            );
3790        }
3791
3792        assert!(
3793            ServerMessage::decode(
3794                Frame::new(22, wire_id::SET_HOOK_FLASH_DETECT, vec![0; 4]),
3795                ProtocolVersion::V22,
3796            )
3797            .is_err()
3798        );
3799    }
3800
3801    #[test]
3802    fn unknown_messages_are_byte_lossless() {
3803        let unknown_payload = vec![9, 8, 7, 6];
3804        let unknown = ServerMessage::decode(
3805            Frame::new(19, 0xdead_beef, unknown_payload.clone()),
3806            ProtocolVersion::V19,
3807        )
3808        .unwrap();
3809        assert!(matches!(unknown, ServerMessage::Unknown(_)));
3810        let unknown_frame = decode_frame(&unknown.encode(ProtocolVersion::V22).unwrap());
3811        assert_eq!(unknown_frame.message_id, 0xdead_beef);
3812        assert_eq!(unknown_frame.protocol_version, 19);
3813        assert_eq!(unknown_frame.payload, unknown_payload);
3814    }
3815
3816    #[test]
3817    fn opaque_encoding_cannot_bypass_a_typed_contract() {
3818        let message = ClientMessage::KnownOpaque(KnownOpaqueMessage {
3819            id: MessageId::IpPort,
3820            protocol_version: ProtocolVersion::V22.wire(),
3821            payload: BoundedBytes::default(),
3822        });
3823
3824        assert!(matches!(
3825            message.encode(ProtocolVersion::V22),
3826            Err(CodecError::InvalidValue {
3827                message_id: wire_id::IP_PORT,
3828                field: "opaque preservation requires an opaque-only contract",
3829                ..
3830            })
3831        ));
3832    }
3833
3834    #[test]
3835    fn malformed_counts_and_oversized_text_are_rejected() {
3836        let mut capabilities = vec![0; 4 + 18 * 16];
3837        capabilities[..4].copy_from_slice(&19_u32.to_le_bytes());
3838        assert!(matches!(
3839            ClientMessage::decode(Frame::new(22, wire_id::CAPABILITIES_RES, capabilities,)),
3840            Err(CodecError::CountTooLarge { .. })
3841        ));
3842        assert!(matches!(
3843            ServerMessage::DisplayText {
3844                text: "x".repeat(32),
3845            }
3846            .encode(ProtocolVersion::V22),
3847            Err(CodecError::TextTooLong { .. })
3848        ));
3849        assert!(matches!(
3850            ClientMessage::DeviceToUserData(UserDataMessage {
3851                application_id: 1,
3852                line_instance: 1,
3853                call_reference: 1,
3854                transaction_id: 1,
3855                data: vec![0; 2001],
3856            })
3857            .encode(ProtocolVersion::V22),
3858            Err(CodecError::CountTooLarge { .. })
3859        ));
3860        assert!(matches!(
3861            ClientMessage::decode(Frame::new(
3862                22,
3863                wire_id::IP_PORT,
3864                70_000_u32.to_le_bytes().to_vec(),
3865            )),
3866            Err(CodecError::InvalidValue { .. })
3867        ));
3868        assert!(matches!(
3869            ServerMessage::StartMediaTransmission {
3870                call_reference: 1,
3871                passthrough_party_id: 1,
3872                endpoint: MediaEndpoint {
3873                    address: "2001:db8::1".parse().unwrap(),
3874                    rtp_port: 4000,
3875                    rtcp_port: 4001,
3876                    codec: Codec::Pcmu,
3877                    packet_ms: 20,
3878                    max_frames_per_packet: 1,
3879                    telephone_event_payload: 101,
3880                },
3881                silence_suppression: SilenceSuppression::Off,
3882                traffic_class: crate::types::MediaTrafficClass::default(),
3883                encryption: None,
3884                wire: None,
3885            }
3886            .encode(ProtocolVersion::V3),
3887            Err(CodecError::InvalidValue { .. })
3888        ));
3889        assert!(matches!(
3890            ControlMessage::CreateConferenceRequest(CreateConferenceRequest {
3891                conference_id: ConferenceId::new(1),
3892                reserved_participants: 2,
3893                resource_type: ConferenceResourceType::Conference,
3894                application_id: ApplicationId::new(1),
3895                application_conference_id: "conference-1".into(),
3896                application_data: String::new(),
3897                passthrough_data: vec![0; 2001],
3898            })
3899            .encode(ProtocolVersion::V22),
3900            Err(CodecError::CountTooLarge {
3901                field: "conference passthrough data",
3902                count: 2001,
3903                maximum: 2000,
3904                ..
3905            })
3906        ));
3907        assert!(matches!(
3908            ControlMessage::AuditConferenceResponse(AuditConferenceResponse {
3909                last: 1,
3910                entries: vec![
3911                    AuditConferenceEntry {
3912                        conference_id: ConferenceId::new(1),
3913                        resource_type: ConferenceResourceType::Conference,
3914                        reserved_participants: 2,
3915                        active_participants: 1,
3916                        application_id: ApplicationId::new(1),
3917                        application_conference_id: String::new(),
3918                        application_data: String::new(),
3919                    };
3920                    33
3921                ],
3922            })
3923            .encode(ProtocolVersion::V22),
3924            Err(CodecError::CountTooLarge {
3925                field: "conference audit entries",
3926                count: 33,
3927                maximum: 32,
3928                ..
3929            })
3930        ));
3931
3932        let mut oversized_conference_data = vec![0; 12];
3933        oversized_conference_data[8..12].copy_from_slice(&2001_u32.to_le_bytes());
3934        assert!(matches!(
3935            ControlMessage::decode(
3936                Frame::new(
3937                    22,
3938                    wire_id::CREATE_CONFERENCE_RES,
3939                    oversized_conference_data
3940                ),
3941                ProtocolVersion::V22,
3942            ),
3943            Err(CodecError::CountTooLarge {
3944                field: "conference passthrough data",
3945                count: 2001,
3946                maximum: 2000,
3947                ..
3948            })
3949        ));
3950
3951        let mut oversized_audit = vec![0; 8];
3952        oversized_audit[4..8].copy_from_slice(&33_u32.to_le_bytes());
3953        assert!(matches!(
3954            ControlMessage::decode(
3955                Frame::new(22, wire_id::AUDIT_CONFERENCE_RES, oversized_audit),
3956                ProtocolVersion::V22,
3957            ),
3958            Err(CodecError::CountTooLarge {
3959                field: "conference audit entries",
3960                count: 33,
3961                maximum: 32,
3962                ..
3963            })
3964        ));
3965    }
3966
3967    #[test]
3968    fn server_response_uses_the_negotiated_address_layout() {
3969        let message = ServerMessage::ServerResponse {
3970            servers: vec![
3971                SignalingServerEndpoint {
3972                    name: "primary".into(),
3973                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)),
3974                    port: NonZeroU16::new(2000).unwrap(),
3975                },
3976                SignalingServerEndpoint {
3977                    name: "secondary".into(),
3978                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 20)),
3979                    port: NonZeroU16::new(2001).unwrap(),
3980                },
3981            ],
3982        };
3983        let v3 = message.encode(ProtocolVersion::V3).unwrap();
3984        let v17 = message.encode(ProtocolVersion::V17).unwrap();
3985        assert_eq!(v3.len(), 292);
3986        assert_eq!(v17.len(), 372);
3987        assert_server_round_trip(message.clone(), ProtocolVersion::V3);
3988        assert_server_round_trip(message, ProtocolVersion::V17);
3989
3990        let mut zero_port = v3;
3991        zero_port[12 + 5 * 48..12 + 5 * 48 + 4].fill(0);
3992        assert!(matches!(
3993            ServerMessage::decode(decode_frame(&zero_port), ProtocolVersion::V3),
3994            Err(CodecError::InvalidValue {
3995                field: "server endpoint",
3996                value: 0,
3997                ..
3998            })
3999        ));
4000        assert_server_round_trip(
4001            ServerMessage::ServerResponse {
4002                servers: vec![SignalingServerEndpoint {
4003                    name: "sccp-v6".into(),
4004                    address: "2001:db8::20".parse().unwrap(),
4005                    port: NonZeroU16::new(2000).unwrap(),
4006                }],
4007            },
4008            ProtocolVersion::V17,
4009        );
4010
4011        let unspecified = ServerMessage::ServerResponse {
4012            servers: vec![SignalingServerEndpoint {
4013                name: "unroutable".into(),
4014                address: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
4015                port: NonZeroU16::new(2000).unwrap(),
4016            }],
4017        };
4018        assert!(matches!(
4019            unspecified.encode(ProtocolVersion::V17),
4020            Err(CodecError::InvalidValue {
4021                field: "server address",
4022                value: 0,
4023                ..
4024            })
4025        ));
4026
4027        let endpoints = |count: u8| {
4028            (0..count)
4029                .map(|index| SignalingServerEndpoint {
4030                    name: format!("node-{index}"),
4031                    address: IpAddr::V4(Ipv4Addr::new(192, 0, 2, index + 1)),
4032                    port: NonZeroU16::new(2000).unwrap(),
4033                })
4034                .collect()
4035        };
4036        let empty = ServerMessage::ServerResponse {
4037            servers: Vec::new(),
4038        };
4039        assert!(matches!(
4040            empty.encode(ProtocolVersion::V17),
4041            Err(CodecError::InvalidValue {
4042                field: "server endpoints",
4043                value: 0,
4044                ..
4045            })
4046        ));
4047        assert_server_round_trip(
4048            ServerMessage::ServerResponse {
4049                servers: endpoints(5),
4050            },
4051            ProtocolVersion::V17,
4052        );
4053        let too_many = ServerMessage::ServerResponse {
4054            servers: endpoints(6),
4055        };
4056        assert!(matches!(
4057            too_many.encode(ProtocolVersion::V17),
4058            Err(CodecError::CountTooLarge {
4059                field: "server endpoints",
4060                count: 6,
4061                maximum: 5,
4062                ..
4063            })
4064        ));
4065    }
4066}