Skip to main content

sccp_protocol/message/
capabilities.rs

1//! Structured station capability updates.
2//!
3//! [`CapabilityUpdate`] decodes the fixed-capacity capability tables while
4//! retaining the original payload for byte-lossless re-encoding. Inspect its
5//! audio, video, data, picture, and conference-resource views through the
6//! accessor methods rather than depending on table offsets.
7
8use std::sync::Arc;
9
10use super::MediaCapability;
11use super::values::{Codec, EncryptionCapability, IpAddressType, ReceiveTransmit};
12use super::wire::CodecError;
13
14const MAX_AUDIO_CAPABILITIES: usize = 18;
15const MAX_VIDEO_CAPABILITIES: usize = 10;
16const MAX_DATA_CAPABILITIES: usize = 5;
17const MAX_CUSTOM_PICTURES: usize = 6;
18const MAX_CONFERENCE_SERVICES: usize = 4;
19const MAX_SERVICE_LAYOUTS: usize = 5;
20const MAX_LEVEL_PREFERENCES: usize = 4;
21
22const CUSTOM_PICTURES_OFFSET: usize = 20;
23const CUSTOM_PICTURE_SIZE: usize = 20;
24const CONFERENCE_OFFSET: usize = CUSTOM_PICTURES_OFFSET + MAX_CUSTOM_PICTURES * CUSTOM_PICTURE_SIZE;
25const CONFERENCE_SERVICE_SIZE: usize = 40;
26const CONFERENCE_SIZE: usize = 12 + MAX_CONFERENCE_SERVICES * CONFERENCE_SERVICE_SIZE;
27const AUDIO_OFFSET: usize = CONFERENCE_OFFSET + CONFERENCE_SIZE;
28const AUDIO_CAPABILITY_SIZE: usize = 16;
29const VIDEO_OFFSET: usize = AUDIO_OFFSET + MAX_AUDIO_CAPABILITIES * AUDIO_CAPABILITY_SIZE;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32/// Wire-layout family used by a station capability update.
33pub enum CapabilityUpdateVariant {
34    /// Original 1,840-byte update layout. The frame version does not select
35    /// between the two message 0x0030 body sizes.
36    Version1,
37    /// Message 0x0030 carrying the expanded 2,000-byte video layout.
38    Version1ExpandedVideo,
39    /// Fixed 2,000-byte update carried by message identifier `0x0043`.
40    Version2,
41    /// Progressively decoded update carried by message identifier `0x0044`.
42    Version3,
43}
44
45impl CapabilityUpdateVariant {
46    /// Returns the message identifier that carries this layout.
47    pub const fn message_id(self) -> u32 {
48        match self {
49            Self::Version1 | Self::Version1ExpandedVideo => 0x0030,
50            Self::Version2 => 0x0043,
51            Self::Version3 => 0x0044,
52        }
53    }
54
55    const fn video_entry_size(self, protocol: u32) -> usize {
56        match self {
57            Self::Version1 => 116,
58            Self::Version1ExpandedVideo | Self::Version2 => 132,
59            Self::Version3 if protocol < 17 => 136,
60            Self::Version3 => 140,
61        }
62    }
63
64    const fn data_entry_size(self) -> usize {
65        match self {
66            Self::Version3 => 20,
67            _ => 16,
68        }
69    }
70
71    const fn codec_parameter_words(self) -> usize {
72        match self {
73            Self::Version1 => 2,
74            _ => 6,
75        }
76    }
77
78    pub(crate) const fn minimum_payload_bytes(self, protocol: u32) -> usize {
79        let data_offset = VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * self.video_entry_size(protocol);
80        data_offset + MAX_DATA_CAPABILITIES * self.data_entry_size()
81    }
82
83    pub(crate) const fn maximum_payload_bytes(self, protocol: u32) -> usize {
84        if matches!(self, Self::Version3) {
85            2_380
86        } else {
87            self.minimum_payload_bytes(protocol)
88        }
89    }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
93/// One station-provided custom video picture format.
94pub struct CustomPictureFormat {
95    /// Picture width in pixels.
96    pub width: u32,
97    /// Picture height in pixels.
98    pub height: u32,
99    /// Encoded pixel aspect-ratio value.
100    pub pixel_aspect_ratio: u32,
101    /// Pixel-clock conversion numerator.
102    pub pixel_clock_conversion: u32,
103    /// Pixel-clock conversion divisor.
104    pub pixel_clock_divisor: u32,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108/// Capacity and layouts for one conference service number.
109pub struct ConferenceServiceResource {
110    pub layouts: Vec<u32>,
111    pub service_number: u32,
112    pub max_streams: u32,
113    pub max_conferences: u32,
114    pub active_conference_on_registration: u32,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
118/// Aggregate conference resources advertised by a station.
119pub struct ConferenceResource {
120    pub active_streams_on_registration: u32,
121    /// Maximum bandwidth in the protocol's rate units.
122    pub max_bandwidth: u32,
123    pub services: Vec<ConferenceServiceResource>,
124}
125
126#[derive(Clone, Debug, Eq, PartialEq)]
127/// One quality/rate preference within a video codec capability.
128pub struct VideoLevelPreference {
129    /// Preference word whose interpretation includes transmit selection.
130    pub transmit_preference: u32,
131    pub format: u32,
132    pub max_bit_rate: u32,
133    pub min_bit_rate: u32,
134    pub minimum_picture_interval: u32,
135    pub service_number: u32,
136}
137
138#[derive(Clone, Debug, Eq, PartialEq)]
139/// One advertised video codec and its supported operating levels.
140pub struct VideoCapability {
141    pub codec: Codec,
142    pub direction: ReceiveTransmit,
143    pub level_preferences: Vec<VideoLevelPreference>,
144    /// Codec-specific words. Their interpretation is selected by `codec`.
145    pub codec_parameters: Vec<u32>,
146    /// Optional encryption support in layouts that carry it.
147    pub encryption_capability: Option<EncryptionCapability>,
148    /// Optional network-address family in layouts that carry it.
149    pub address_type: Option<IpAddressType>,
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
153/// One advertised non-audio/non-video data capability.
154pub struct DataCapability {
155    pub payload_capability: u32,
156    pub direction: ReceiveTransmit,
157    /// Capability-specific data retained as a wire word.
158    pub protocol_dependent_data: u32,
159    pub max_bit_rate: u32,
160    /// Optional encryption support in layouts that carry it.
161    pub encryption_capability: Option<EncryptionCapability>,
162}
163
164/// Application-facing audio and video capabilities for one station session.
165///
166/// Clones share the immutable capability tables. Protocol-only fields and the
167/// preserved wire payload are deliberately excluded from this runtime view.
168#[derive(Clone, Debug, Default, Eq, PartialEq)]
169pub struct StationMediaCapabilities {
170    audio: Arc<[MediaCapability]>,
171    video: Arc<[VideoCapability]>,
172}
173
174impl StationMediaCapabilities {
175    /// Builds an immutable snapshot from typed capability tables.
176    pub fn new(audio: Vec<MediaCapability>, video: Vec<VideoCapability>) -> Self {
177        Self {
178            audio: audio.into(),
179            video: video.into(),
180        }
181    }
182
183    pub fn audio(&self) -> &[MediaCapability] {
184        &self.audio
185    }
186
187    pub fn video(&self) -> &[VideoCapability] {
188        &self.video
189    }
190
191    pub fn is_empty(&self) -> bool {
192        self.audio.is_empty() && self.video.is_empty()
193    }
194}
195
196impl From<Vec<MediaCapability>> for StationMediaCapabilities {
197    fn from(audio: Vec<MediaCapability>) -> Self {
198        Self::new(audio, Vec::new())
199    }
200}
201
202/// A decoded fixed-layout capability update. The original payload is retained
203/// so decoding and re-encoding is byte-lossless even for reserved fields.
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct CapabilityUpdate {
206    variant: CapabilityUpdateVariant,
207    rtp_payload_format: u32,
208    custom_picture_formats: Vec<CustomPictureFormat>,
209    conference: ConferenceResource,
210    audio: Vec<MediaCapability>,
211    video: Vec<VideoCapability>,
212    data: Vec<DataCapability>,
213    trailing_words: Vec<u32>,
214    raw_payload: Vec<u8>,
215}
216
217impl CapabilityUpdate {
218    /// Moves the typed media tables into an application-facing snapshot.
219    ///
220    /// Picture, conference, data, trailing, and raw wire fields remain codec
221    /// concerns and are discarded by this projection.
222    pub fn into_media_capabilities(self) -> StationMediaCapabilities {
223        StationMediaCapabilities::new(self.audio, self.video)
224    }
225
226    pub const fn variant(&self) -> CapabilityUpdateVariant {
227        self.variant
228    }
229
230    pub const fn rtp_payload_format(&self) -> u32 {
231        self.rtp_payload_format
232    }
233
234    pub fn custom_picture_formats(&self) -> &[CustomPictureFormat] {
235        &self.custom_picture_formats
236    }
237
238    pub const fn conference(&self) -> &ConferenceResource {
239        &self.conference
240    }
241
242    pub fn audio(&self) -> &[MediaCapability] {
243        &self.audio
244    }
245
246    pub fn video(&self) -> &[VideoCapability] {
247        &self.video
248    }
249
250    pub fn data(&self) -> &[DataCapability] {
251        &self.data
252    }
253
254    /// Returns complete trailing words understood structurally but not yet named.
255    pub fn trailing_words(&self) -> &[u32] {
256        &self.trailing_words
257    }
258
259    pub(crate) fn raw_payload(&self) -> &[u8] {
260        &self.raw_payload
261    }
262
263    pub(crate) fn decode(
264        variant: CapabilityUpdateVariant,
265        protocol: u32,
266        payload: &[u8],
267    ) -> Result<Self, CodecError> {
268        let message_id = variant.message_id();
269        let video_size = variant.video_entry_size(protocol);
270        let data_offset = VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * video_size;
271        let trailing_offset = data_offset + MAX_DATA_CAPABILITIES * variant.data_entry_size();
272        let required = if matches!(variant, CapabilityUpdateVariant::Version3) {
273            20
274        } else {
275            variant.minimum_payload_bytes(protocol)
276        };
277        require_len(payload, required, message_id)?;
278        let maximum = variant.maximum_payload_bytes(protocol);
279        if payload.len() > maximum {
280            return Err(CodecError::TrailingBytes {
281                message_id,
282                count: payload.len() - maximum,
283            });
284        }
285        let cursor = CapabilityCursor::new(payload, message_id);
286
287        let audio_count = bounded_wire_count(
288            cursor.word(0)?,
289            MAX_AUDIO_CAPABILITIES,
290            "audio capabilities",
291            message_id,
292        )?;
293        let video_count = bounded_wire_count(
294            cursor.word(4)?,
295            MAX_VIDEO_CAPABILITIES,
296            "video capabilities",
297            message_id,
298        )?;
299        let data_count = bounded_wire_count(
300            cursor.word(8)?,
301            MAX_DATA_CAPABILITIES,
302            "data capabilities",
303            message_id,
304        )?;
305        let rtp_payload_format = cursor.word(12)?;
306        let picture_count = bounded_wire_count(
307            cursor.word(16)?,
308            MAX_CUSTOM_PICTURES,
309            "custom picture formats",
310            message_id,
311        )?;
312
313        require_declared_entries(
314            payload,
315            CUSTOM_PICTURES_OFFSET,
316            picture_count,
317            CUSTOM_PICTURE_SIZE,
318            message_id,
319        )?;
320        require_declared_entries(
321            payload,
322            AUDIO_OFFSET,
323            audio_count,
324            AUDIO_CAPABILITY_SIZE,
325            message_id,
326        )?;
327        require_declared_entries(payload, VIDEO_OFFSET, video_count, video_size, message_id)?;
328        require_declared_entries(
329            payload,
330            data_offset,
331            data_count,
332            variant.data_entry_size(),
333            message_id,
334        )?;
335
336        let mut custom_picture_formats = Vec::with_capacity(picture_count);
337        for index in 0..picture_count {
338            let offset = CUSTOM_PICTURES_OFFSET + index * CUSTOM_PICTURE_SIZE;
339            custom_picture_formats.push(decode_picture_entry(&cursor, offset)?);
340        }
341
342        let service_count = match cursor.optional_word(CONFERENCE_OFFSET + 8) {
343            Some(count) => bounded_wire_count(
344                count,
345                MAX_CONFERENCE_SERVICES,
346                "conference services",
347                message_id,
348            )?,
349            None => 0,
350        };
351        let mut services = Vec::with_capacity(service_count);
352        require_declared_entries(
353            payload,
354            CONFERENCE_OFFSET + 12,
355            service_count,
356            CONFERENCE_SERVICE_SIZE,
357            message_id,
358        )?;
359        for index in 0..service_count {
360            let offset = CONFERENCE_OFFSET + 12 + index * CONFERENCE_SERVICE_SIZE;
361            services.push(decode_conference_service_entry(&cursor, offset)?);
362        }
363        let conference = ConferenceResource {
364            active_streams_on_registration: cursor.optional_word(CONFERENCE_OFFSET).unwrap_or(0),
365            max_bandwidth: cursor.optional_word(CONFERENCE_OFFSET + 4).unwrap_or(0),
366            services,
367        };
368
369        let mut audio = Vec::with_capacity(audio_count);
370        for index in 0..audio_count {
371            let offset = AUDIO_OFFSET + index * AUDIO_CAPABILITY_SIZE;
372            audio.push(decode_audio_entry(&cursor, offset)?);
373        }
374
375        let mut video = Vec::with_capacity(video_count);
376        for index in 0..video_count {
377            let offset = VIDEO_OFFSET + index * video_size;
378            video.push(decode_video_entry(&cursor, offset, variant, protocol)?);
379        }
380
381        let mut data = Vec::with_capacity(data_count);
382        for index in 0..data_count {
383            let offset = data_offset + index * variant.data_entry_size();
384            data.push(decode_data_entry(&cursor, offset, variant)?);
385        }
386
387        let trailing = payload.get(trailing_offset..).unwrap_or_default();
388        let trailing_words = (0..trailing.len() / 4)
389            .map(|index| {
390                let offset = index * 4;
391                u32::from_le_bytes([
392                    trailing[offset],
393                    trailing[offset + 1],
394                    trailing[offset + 2],
395                    trailing[offset + 3],
396                ])
397            })
398            .collect();
399
400        Ok(Self {
401            variant,
402            rtp_payload_format,
403            custom_picture_formats,
404            conference,
405            audio,
406            video,
407            data,
408            trailing_words,
409            raw_payload: payload.to_vec(),
410        })
411    }
412}
413
414/// Offset-aware bounded reader for the fixed capability tables. Each family
415/// decoder receives this cursor instead of indexing the raw body directly.
416struct CapabilityCursor<'a> {
417    payload: &'a [u8],
418    message_id: u32,
419}
420
421impl<'a> CapabilityCursor<'a> {
422    const fn new(payload: &'a [u8], message_id: u32) -> Self {
423        Self {
424            payload,
425            message_id,
426        }
427    }
428
429    fn bytes(&self, offset: usize, length: usize) -> Result<&'a [u8], CodecError> {
430        let end = offset.checked_add(length).ok_or(CodecError::Truncated {
431            message_id: self.message_id,
432            needed: usize::MAX,
433            actual: self.payload.len(),
434        })?;
435        require_len(self.payload, end, self.message_id)?;
436        Ok(&self.payload[offset..end])
437    }
438
439    fn word(&self, offset: usize) -> Result<u32, CodecError> {
440        Ok(u32::from_le_bytes(
441            self.bytes(offset, 4)?
442                .try_into()
443                .expect("bounded capability word"),
444        ))
445    }
446
447    fn optional_word(&self, offset: usize) -> Option<u32> {
448        self.payload
449            .get(offset..offset + 4)
450            .and_then(|bytes| bytes.try_into().ok())
451            .map(u32::from_le_bytes)
452    }
453}
454
455fn decode_picture_entry(
456    cursor: &CapabilityCursor<'_>,
457    offset: usize,
458) -> Result<CustomPictureFormat, CodecError> {
459    Ok(CustomPictureFormat {
460        width: cursor.word(offset)?,
461        height: cursor.word(offset + 4)?,
462        pixel_aspect_ratio: cursor.word(offset + 8)?,
463        pixel_clock_conversion: cursor.word(offset + 12)?,
464        pixel_clock_divisor: cursor.word(offset + 16)?,
465    })
466}
467
468fn decode_conference_service_entry(
469    cursor: &CapabilityCursor<'_>,
470    offset: usize,
471) -> Result<ConferenceServiceResource, CodecError> {
472    let layout_count = bounded_wire_count(
473        cursor.word(offset)?,
474        MAX_SERVICE_LAYOUTS,
475        "conference service layouts",
476        cursor.message_id,
477    )?;
478    let layouts = (0..layout_count)
479        .map(|layout| cursor.word(offset + 4 + layout * 4))
480        .collect::<Result<_, _>>()?;
481    Ok(ConferenceServiceResource {
482        layouts,
483        service_number: cursor.word(offset + 24)?,
484        max_streams: cursor.word(offset + 28)?,
485        max_conferences: cursor.word(offset + 32)?,
486        active_conference_on_registration: cursor.word(offset + 36)?,
487    })
488}
489
490fn decode_audio_entry(
491    cursor: &CapabilityCursor<'_>,
492    offset: usize,
493) -> Result<MediaCapability, CodecError> {
494    Ok(MediaCapability {
495        codec: Codec::from(cursor.word(offset)?),
496        max_frames_per_packet: cursor.word(offset + 4)?,
497        codec_parameters: cursor
498            .bytes(offset + 8, 8)?
499            .try_into()
500            .expect("bounded audio capability parameters"),
501    })
502}
503
504fn decode_video_entry(
505    cursor: &CapabilityCursor<'_>,
506    offset: usize,
507    variant: CapabilityUpdateVariant,
508    protocol: u32,
509) -> Result<VideoCapability, CodecError> {
510    let level_count = bounded_wire_count(
511        cursor.word(offset + 8)?,
512        MAX_LEVEL_PREFERENCES,
513        "video level preferences",
514        cursor.message_id,
515    )?;
516    let level_preferences = (0..level_count)
517        .map(|level| {
518            let level_offset = offset + 12 + level * 24;
519            Ok(VideoLevelPreference {
520                transmit_preference: cursor.word(level_offset)?,
521                format: cursor.word(level_offset + 4)?,
522                max_bit_rate: cursor.word(level_offset + 8)?,
523                min_bit_rate: cursor.word(level_offset + 12)?,
524                minimum_picture_interval: cursor.word(level_offset + 16)?,
525                service_number: cursor.word(level_offset + 20)?,
526            })
527        })
528        .collect::<Result<_, CodecError>>()?;
529    let parameters_offset =
530        offset + 108 + usize::from(variant == CapabilityUpdateVariant::Version3) * 4;
531    let codec_parameters = (0..variant.codec_parameter_words())
532        .map(|parameter| cursor.word(parameters_offset + parameter * 4))
533        .collect::<Result<_, _>>()?;
534    Ok(VideoCapability {
535        codec: Codec::from(cursor.word(offset)?),
536        direction: ReceiveTransmit::from_bits_retain(cursor.word(offset + 4)?),
537        level_preferences,
538        codec_parameters,
539        encryption_capability: (variant == CapabilityUpdateVariant::Version3)
540            .then(|| cursor.word(offset + 108).map(EncryptionCapability::from))
541            .transpose()?,
542        address_type: (variant == CapabilityUpdateVariant::Version3 && protocol >= 17)
543            .then(|| cursor.word(offset + 136).map(IpAddressType::from))
544            .transpose()?,
545    })
546}
547
548fn decode_data_entry(
549    cursor: &CapabilityCursor<'_>,
550    offset: usize,
551    variant: CapabilityUpdateVariant,
552) -> Result<DataCapability, CodecError> {
553    Ok(DataCapability {
554        payload_capability: cursor.word(offset)?,
555        direction: ReceiveTransmit::from_bits_retain(cursor.word(offset + 4)?),
556        protocol_dependent_data: cursor.word(offset + 8)?,
557        max_bit_rate: cursor.word(offset + 12)?,
558        encryption_capability: (variant == CapabilityUpdateVariant::Version3)
559            .then(|| cursor.word(offset + 16).map(EncryptionCapability::from))
560            .transpose()?,
561    })
562}
563
564fn bounded_wire_count(
565    count: u32,
566    maximum: usize,
567    field: &'static str,
568    message_id: u32,
569) -> Result<usize, CodecError> {
570    let count = usize::try_from(count).map_err(|_| CodecError::InvalidValue {
571        message_id,
572        field,
573        value: u64::from(count),
574    })?;
575    if count > maximum {
576        Err(CodecError::CountTooLarge {
577            message_id,
578            field,
579            count,
580            maximum,
581        })
582    } else {
583        Ok(count)
584    }
585}
586
587fn require_len(payload: &[u8], needed: usize, message_id: u32) -> Result<(), CodecError> {
588    if payload.len() < needed {
589        Err(CodecError::Truncated {
590            message_id,
591            needed,
592            actual: payload.len(),
593        })
594    } else {
595        Ok(())
596    }
597}
598
599fn require_declared_entries(
600    payload: &[u8],
601    offset: usize,
602    count: usize,
603    entry_size: usize,
604    message_id: u32,
605) -> Result<(), CodecError> {
606    if count == 0 {
607        Ok(())
608    } else {
609        require_len(payload, offset + count * entry_size, message_id)
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use crate::message::ClientMessage;
617    use crate::message::values::ProtocolVersion;
618    use crate::message::wire::{Frame, FrameDecoder};
619
620    fn put(payload: &mut [u8], offset: usize, value: u32) {
621        payload[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
622    }
623
624    fn fixture(source: &str) -> Vec<u8> {
625        source
626            .split_whitespace()
627            .map(|byte| u8::from_str_radix(byte, 16).expect("valid fixture byte"))
628            .collect()
629    }
630
631    fn assert_declared_table_boundaries(
632        protocol: u32,
633        count_offset: usize,
634        table_offset: usize,
635        entry_size: usize,
636        maximum: usize,
637        decoded_count: impl Fn(&CapabilityUpdate) -> usize,
638    ) {
639        for count in 1..=maximum {
640            let needed = table_offset + count * entry_size;
641            let mut truncated = vec![0; needed - 1];
642            put(&mut truncated, count_offset, count as u32);
643            assert!(matches!(
644                CapabilityUpdate::decode(
645                    CapabilityUpdateVariant::Version3,
646                    protocol,
647                    &truncated
648                ),
649                Err(CodecError::Truncated {
650                    needed: actual_needed,
651                    actual,
652                    ..
653                }) if actual_needed == needed && actual == needed - 1
654            ));
655
656            let mut complete = vec![0; needed];
657            put(&mut complete, count_offset, count as u32);
658            let update =
659                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, protocol, &complete)
660                    .unwrap();
661            assert_eq!(decoded_count(&update), count);
662            assert_eq!(update.raw_payload(), complete);
663        }
664    }
665
666    #[test]
667    fn version_three_update_exposes_every_capability_family() {
668        let mut payload = vec![0; 2_380];
669        put(&mut payload, 0, 1);
670        put(&mut payload, 4, 1);
671        put(&mut payload, 8, 1);
672        put(&mut payload, 12, 101);
673        put(&mut payload, 16, 1);
674        for (index, value) in [640, 480, 1, 2, 3].into_iter().enumerate() {
675            put(&mut payload, CUSTOM_PICTURES_OFFSET + index * 4, value);
676        }
677        put(&mut payload, CONFERENCE_OFFSET, 1);
678        put(&mut payload, CONFERENCE_OFFSET + 4, 2_048);
679        put(&mut payload, CONFERENCE_OFFSET + 8, 1);
680        put(&mut payload, CONFERENCE_OFFSET + 12, 1);
681        put(&mut payload, CONFERENCE_OFFSET + 16, 7);
682        put(&mut payload, CONFERENCE_OFFSET + 36, 9);
683        put(&mut payload, CONFERENCE_OFFSET + 40, 2);
684        put(&mut payload, CONFERENCE_OFFSET + 44, 1);
685        put(&mut payload, CONFERENCE_OFFSET + 48, 0);
686        put(&mut payload, AUDIO_OFFSET, Codec::Pcmu.wire_value());
687        put(&mut payload, AUDIO_OFFSET + 4, 4);
688        payload[AUDIO_OFFSET + 8..AUDIO_OFFSET + 16].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
689        put(&mut payload, VIDEO_OFFSET, Codec::H264.wire_value());
690        put(&mut payload, VIDEO_OFFSET + 4, 3);
691        put(&mut payload, VIDEO_OFFSET + 8, 1);
692        for (index, value) in [1, 5, 4_000, 128, 2, 7].into_iter().enumerate() {
693            put(&mut payload, VIDEO_OFFSET + 12 + index * 4, value);
694        }
695        put(&mut payload, VIDEO_OFFSET + 108, 1);
696        for (index, value) in [66, 31, 120, 240, 360, 480].into_iter().enumerate() {
697            put(&mut payload, VIDEO_OFFSET + 112 + index * 4, value);
698        }
699        put(&mut payload, VIDEO_OFFSET + 136, 2);
700        let data_offset = VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140;
701        for (index, value) in [0x120, 3, 8, 64_000, 1].into_iter().enumerate() {
702            put(&mut payload, data_offset + index * 4, value);
703        }
704        put(
705            &mut payload,
706            data_offset + MAX_DATA_CAPABILITIES * 20,
707            0xfeed_beef,
708        );
709
710        let update =
711            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
712        assert_eq!(update.rtp_payload_format(), 101);
713        assert_eq!(update.custom_picture_formats()[0].width, 640);
714        assert_eq!(update.conference().services[0].layouts, [7]);
715        assert_eq!(update.audio()[0].codec, Codec::Pcmu);
716        assert_eq!(update.audio()[0].codec_parameters, [1, 2, 3, 4, 5, 6, 7, 8]);
717        assert_eq!(update.video()[0].codec, Codec::H264);
718        assert_eq!(
719            update.video()[0].direction,
720            ReceiveTransmit::RECEIVE | ReceiveTransmit::TRANSMIT
721        );
722        assert_eq!(
723            update.video()[0].encryption_capability,
724            Some(EncryptionCapability::Capable)
725        );
726        assert_eq!(
727            update.video()[0].address_type,
728            Some(IpAddressType::Ipv4AndIpv6)
729        );
730        assert_eq!(update.data()[0].max_bit_rate, 64_000);
731        assert_eq!(
732            update.data()[0].encryption_capability,
733            Some(EncryptionCapability::Capable)
734        );
735        assert_eq!(update.trailing_words()[0], 0xfeed_beef);
736        assert_eq!(update.raw_payload(), payload);
737
738        let expected_audio = update.audio().to_vec();
739        let expected_video = update.video().to_vec();
740        let media = update.into_media_capabilities();
741        assert_eq!(media.audio(), expected_audio);
742        assert_eq!(media.video(), expected_video);
743    }
744
745    #[test]
746    fn version_three_accepts_every_bounded_progressive_length() {
747        for size in 20..=2_380 {
748            let payload = vec![0; size];
749            let update =
750                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
751            assert_eq!(update.raw_payload(), payload, "payload size {size}");
752        }
753
754        assert!(matches!(
755            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &[0; 19]),
756            Err(CodecError::Truncated { .. })
757        ));
758        assert!(matches!(
759            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &[0; 2_381]),
760            Err(CodecError::TrailingBytes { .. })
761        ));
762        assert!(
763            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &[0; 2_380]).is_ok()
764        );
765        assert!(matches!(
766            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &[0; 2_381]),
767            Err(CodecError::TrailingBytes { .. })
768        ));
769    }
770
771    #[test]
772    fn version_three_rejects_declared_tables_that_do_not_fit() {
773        for (count_offset, needed) in [
774            (0, AUDIO_OFFSET + AUDIO_CAPABILITY_SIZE),
775            (4, VIDEO_OFFSET + 140),
776            (8, VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140 + 20),
777            (16, CUSTOM_PICTURES_OFFSET + CUSTOM_PICTURE_SIZE),
778        ] {
779            let mut payload = vec![0; 20];
780            put(&mut payload, count_offset, 1);
781            assert!(matches!(
782                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload),
783                Err(CodecError::Truncated {
784                    needed: actual_needed,
785                    actual: 20,
786                    ..
787                }) if actual_needed == needed
788            ));
789        }
790
791        assert_declared_table_boundaries(
792            22,
793            16,
794            CUSTOM_PICTURES_OFFSET,
795            CUSTOM_PICTURE_SIZE,
796            MAX_CUSTOM_PICTURES,
797            |update| update.custom_picture_formats().len(),
798        );
799        assert_declared_table_boundaries(
800            22,
801            0,
802            AUDIO_OFFSET,
803            AUDIO_CAPABILITY_SIZE,
804            MAX_AUDIO_CAPABILITIES,
805            |update| update.audio().len(),
806        );
807        for (protocol, video_size) in [(16, 136), (17, 140)] {
808            assert_declared_table_boundaries(
809                protocol,
810                4,
811                VIDEO_OFFSET,
812                video_size,
813                MAX_VIDEO_CAPABILITIES,
814                |update| update.video().len(),
815            );
816            assert_declared_table_boundaries(
817                protocol,
818                8,
819                VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * video_size,
820                20,
821                MAX_DATA_CAPABILITIES,
822                |update| update.data().len(),
823            );
824        }
825    }
826
827    #[test]
828    fn version_three_rejects_declared_conference_services_that_do_not_fit() {
829        for count in 1..=MAX_CONFERENCE_SERVICES {
830            let needed = CONFERENCE_OFFSET + 12 + count * CONFERENCE_SERVICE_SIZE;
831            let mut truncated = vec![0; needed - 1];
832            put(&mut truncated, CONFERENCE_OFFSET + 8, count as u32);
833            assert!(matches!(
834                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &truncated),
835                Err(CodecError::Truncated {
836                    needed: actual_needed,
837                    actual,
838                    ..
839                }) if actual_needed == needed && actual == needed - 1
840            ));
841
842            let mut complete = vec![0; needed];
843            put(&mut complete, CONFERENCE_OFFSET + 8, count as u32);
844            let update =
845                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &complete).unwrap();
846            assert_eq!(update.conference().services.len(), count);
847        }
848    }
849
850    #[test]
851    fn version_three_preserves_an_unstructured_suffix() {
852        let trailing_offset =
853            VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140 + MAX_DATA_CAPABILITIES * 20;
854        let mut payload = vec![0; trailing_offset + 7];
855        put(&mut payload, trailing_offset, 0xfeed_beef);
856        payload[trailing_offset + 4..].copy_from_slice(&[0xaa, 0xbb, 0xcc]);
857
858        let update =
859            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
860        assert_eq!(update.trailing_words(), [0xfeed_beef]);
861        assert_eq!(update.raw_payload(), payload);
862
863        let encoded = ClientMessage::CapabilitiesUpdate(update)
864            .encode(ProtocolVersion::V22)
865            .unwrap();
866        let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
867        assert_eq!(frame.payload, payload);
868    }
869
870    #[test]
871    fn version_three_video_entry_boundary_depends_on_protocol() {
872        let mut before = vec![0; 2_060];
873        put(&mut before, 4, 1);
874        put(&mut before, VIDEO_OFFSET, Codec::H264.wire_value());
875        let before =
876            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &before).unwrap();
877        assert_eq!(before.video().len(), 1);
878        assert_eq!(before.video()[0].address_type, None);
879
880        let mut from = vec![0; 2_100];
881        put(&mut from, 4, 1);
882        put(&mut from, VIDEO_OFFSET, Codec::H264.wire_value());
883        put(&mut from, VIDEO_OFFSET + 136, 2);
884        let from = CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 17, &from).unwrap();
885        assert_eq!(from.video().len(), 1);
886        assert_eq!(
887            from.video()[0].address_type,
888            Some(IpAddressType::Ipv4AndIpv6)
889        );
890    }
891
892    #[test]
893    fn update_rejects_truncation_and_every_oversized_count() {
894        assert!(matches!(
895            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &[0; 100]),
896            Err(CodecError::Truncated { .. })
897        ));
898
899        for (offset, count) in [(0, 19), (4, 11), (8, 6), (16, 7)] {
900            let mut payload = vec![0; 1_840];
901            put(&mut payload, offset, count);
902            assert!(matches!(
903                CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &payload),
904                Err(CodecError::CountTooLarge { .. })
905            ));
906        }
907
908        let mut services = vec![0; 1_840];
909        put(&mut services, CONFERENCE_OFFSET + 8, 5);
910        assert!(matches!(
911            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &services),
912            Err(CodecError::CountTooLarge { .. })
913        ));
914
915        let mut layouts = vec![0; 1_840];
916        put(&mut layouts, CONFERENCE_OFFSET + 8, 1);
917        put(&mut layouts, CONFERENCE_OFFSET + 12, 6);
918        assert!(matches!(
919            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &layouts),
920            Err(CodecError::CountTooLarge { .. })
921        ));
922
923        let mut levels = vec![0; 1_840];
924        put(&mut levels, 4, 1);
925        put(&mut levels, VIDEO_OFFSET + 8, 5);
926        assert!(matches!(
927            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &levels),
928            Err(CodecError::CountTooLarge { .. })
929        ));
930    }
931
932    #[test]
933    fn every_update_variant_round_trips_its_original_fixed_layout() {
934        for (variant, protocol, size) in [
935            (
936                CapabilityUpdateVariant::Version1,
937                ProtocolVersion::V3,
938                1_840,
939            ),
940            (
941                CapabilityUpdateVariant::Version1ExpandedVideo,
942                ProtocolVersion::V16,
943                2_000,
944            ),
945            (
946                CapabilityUpdateVariant::Version2,
947                ProtocolVersion::V22,
948                2_000,
949            ),
950            (
951                CapabilityUpdateVariant::Version3,
952                ProtocolVersion::V22,
953                2_380,
954            ),
955        ] {
956            let payload = vec![0; size];
957            let decoded = ClientMessage::decode_with_version(
958                Frame::new(protocol.wire(), variant.message_id(), payload.clone()),
959                protocol,
960            )
961            .unwrap();
962            assert!(matches!(
963                decoded,
964                ClientMessage::CapabilitiesUpdate(ref update) if update.variant() == variant
965            ));
966            let encoded = decoded.encode(protocol).unwrap();
967            let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
968            assert_eq!(frame.message_id, variant.message_id());
969            assert_eq!(frame.payload, payload);
970        }
971    }
972
973    #[test]
974    fn v22_7961_legacy_body_size_overrides_the_modern_session_protocol() {
975        let bytes = fixture(include_str!(
976            "../../tests/fixtures/golden/update_capabilities_7961_v22_legacy.hex"
977        ));
978        let frame = FrameDecoder::new().push(&bytes).unwrap().remove(0);
979        assert_eq!(frame.protocol_version, ProtocolVersion::V22.wire());
980        assert_eq!(
981            frame.message_id,
982            CapabilityUpdateVariant::Version1.message_id()
983        );
984        assert_eq!(frame.payload.len(), 1_840);
985        let decoded = ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap();
986
987        assert!(matches!(
988            decoded,
989            ClientMessage::CapabilitiesUpdate(ref update)
990                if update.variant() == CapabilityUpdateVariant::Version1
991        ));
992        let encoded = decoded.encode(ProtocolVersion::V22).unwrap();
993        let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
994        assert_eq!(frame.protocol_version, ProtocolVersion::V22.wire());
995        assert_eq!(frame.payload.len(), 1_840);
996        assert_eq!(encoded, bytes);
997    }
998}