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