Skip to main content

sccp_protocol/message/
mod.rs

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