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
286        let audio_count = bounded_wire_count(
287            word(payload, 0, message_id)?,
288            MAX_AUDIO_CAPABILITIES,
289            "audio capabilities",
290            message_id,
291        )?;
292        let video_count = bounded_wire_count(
293            word(payload, 4, message_id)?,
294            MAX_VIDEO_CAPABILITIES,
295            "video capabilities",
296            message_id,
297        )?;
298        let data_count = bounded_wire_count(
299            word(payload, 8, message_id)?,
300            MAX_DATA_CAPABILITIES,
301            "data capabilities",
302            message_id,
303        )?;
304        let rtp_payload_format = word(payload, 12, message_id)?;
305        let picture_count = bounded_wire_count(
306            word(payload, 16, message_id)?,
307            MAX_CUSTOM_PICTURES,
308            "custom picture formats",
309            message_id,
310        )?;
311
312        require_declared_entries(
313            payload,
314            CUSTOM_PICTURES_OFFSET,
315            picture_count,
316            CUSTOM_PICTURE_SIZE,
317            message_id,
318        )?;
319        require_declared_entries(
320            payload,
321            AUDIO_OFFSET,
322            audio_count,
323            AUDIO_CAPABILITY_SIZE,
324            message_id,
325        )?;
326        require_declared_entries(payload, VIDEO_OFFSET, video_count, video_size, message_id)?;
327        require_declared_entries(
328            payload,
329            data_offset,
330            data_count,
331            variant.data_entry_size(),
332            message_id,
333        )?;
334
335        let mut custom_picture_formats = Vec::with_capacity(picture_count);
336        for index in 0..picture_count {
337            let offset = CUSTOM_PICTURES_OFFSET + index * CUSTOM_PICTURE_SIZE;
338            custom_picture_formats.push(CustomPictureFormat {
339                width: word(payload, offset, message_id)?,
340                height: word(payload, offset + 4, message_id)?,
341                pixel_aspect_ratio: word(payload, offset + 8, message_id)?,
342                pixel_clock_conversion: word(payload, offset + 12, message_id)?,
343                pixel_clock_divisor: word(payload, offset + 16, message_id)?,
344            });
345        }
346
347        let service_count = match optional_word(payload, CONFERENCE_OFFSET + 8) {
348            Some(count) => bounded_wire_count(
349                count,
350                MAX_CONFERENCE_SERVICES,
351                "conference services",
352                message_id,
353            )?,
354            None => 0,
355        };
356        let mut services = Vec::with_capacity(service_count);
357        require_declared_entries(
358            payload,
359            CONFERENCE_OFFSET + 12,
360            service_count,
361            CONFERENCE_SERVICE_SIZE,
362            message_id,
363        )?;
364        for index in 0..service_count {
365            let offset = CONFERENCE_OFFSET + 12 + index * CONFERENCE_SERVICE_SIZE;
366            let layout_count = bounded_wire_count(
367                word(payload, offset, message_id)?,
368                MAX_SERVICE_LAYOUTS,
369                "conference service layouts",
370                message_id,
371            )?;
372            let mut layouts = Vec::with_capacity(layout_count);
373            for layout in 0..layout_count {
374                layouts.push(word(payload, offset + 4 + layout * 4, message_id)?);
375            }
376            services.push(ConferenceServiceResource {
377                layouts,
378                service_number: word(payload, offset + 24, message_id)?,
379                max_streams: word(payload, offset + 28, message_id)?,
380                max_conferences: word(payload, offset + 32, message_id)?,
381                active_conference_on_registration: word(payload, offset + 36, message_id)?,
382            });
383        }
384        let conference = ConferenceResource {
385            active_streams_on_registration: optional_word(payload, CONFERENCE_OFFSET).unwrap_or(0),
386            max_bandwidth: optional_word(payload, CONFERENCE_OFFSET + 4).unwrap_or(0),
387            services,
388        };
389
390        let mut audio = Vec::with_capacity(audio_count);
391        for index in 0..audio_count {
392            let offset = AUDIO_OFFSET + index * AUDIO_CAPABILITY_SIZE;
393            audio.push(MediaCapability {
394                codec: Codec::from(word(payload, offset, message_id)?),
395                max_frames_per_packet: word(payload, offset + 4, message_id)?,
396                codec_parameters: payload[offset + 8..offset + 16]
397                    .try_into()
398                    .expect("validated fixed audio capability bounds"),
399            });
400        }
401
402        let mut video = Vec::with_capacity(video_count);
403        for index in 0..video_count {
404            let offset = VIDEO_OFFSET + index * video_size;
405            let level_count = bounded_wire_count(
406                word(payload, offset + 8, message_id)?,
407                MAX_LEVEL_PREFERENCES,
408                "video level preferences",
409                message_id,
410            )?;
411            let mut level_preferences = Vec::with_capacity(level_count);
412            for level in 0..level_count {
413                let level_offset = offset + 12 + level * 24;
414                level_preferences.push(VideoLevelPreference {
415                    transmit_preference: word(payload, level_offset, message_id)?,
416                    format: word(payload, level_offset + 4, message_id)?,
417                    max_bit_rate: word(payload, level_offset + 8, message_id)?,
418                    min_bit_rate: word(payload, level_offset + 12, message_id)?,
419                    minimum_picture_interval: word(payload, level_offset + 16, message_id)?,
420                    service_number: word(payload, level_offset + 20, message_id)?,
421                });
422            }
423            let parameters_offset =
424                offset + 108 + usize::from(variant == CapabilityUpdateVariant::Version3) * 4;
425            let codec_parameters = (0..variant.codec_parameter_words())
426                .map(|parameter| word(payload, parameters_offset + parameter * 4, message_id))
427                .collect::<Result<_, _>>()?;
428            video.push(VideoCapability {
429                codec: Codec::from(word(payload, offset, message_id)?),
430                direction: ReceiveTransmit::from_bits_retain(word(
431                    payload,
432                    offset + 4,
433                    message_id,
434                )?),
435                level_preferences,
436                codec_parameters,
437                encryption_capability: (variant == CapabilityUpdateVariant::Version3)
438                    .then(|| {
439                        word(payload, offset + 108, message_id).map(EncryptionCapability::from)
440                    })
441                    .transpose()?,
442                address_type: (variant == CapabilityUpdateVariant::Version3 && protocol >= 17)
443                    .then(|| word(payload, offset + 136, message_id).map(IpAddressType::from))
444                    .transpose()?,
445            });
446        }
447
448        let mut data = Vec::with_capacity(data_count);
449        for index in 0..data_count {
450            let offset = data_offset + index * variant.data_entry_size();
451            data.push(DataCapability {
452                payload_capability: word(payload, offset, message_id)?,
453                direction: ReceiveTransmit::from_bits_retain(word(
454                    payload,
455                    offset + 4,
456                    message_id,
457                )?),
458                protocol_dependent_data: word(payload, offset + 8, message_id)?,
459                max_bit_rate: word(payload, offset + 12, message_id)?,
460                encryption_capability: (variant == CapabilityUpdateVariant::Version3)
461                    .then(|| word(payload, offset + 16, message_id).map(EncryptionCapability::from))
462                    .transpose()?,
463            });
464        }
465
466        let trailing = payload.get(trailing_offset..).unwrap_or_default();
467        let trailing_words = (0..trailing.len() / 4)
468            .map(|index| {
469                let offset = index * 4;
470                u32::from_le_bytes([
471                    trailing[offset],
472                    trailing[offset + 1],
473                    trailing[offset + 2],
474                    trailing[offset + 3],
475                ])
476            })
477            .collect();
478
479        Ok(Self {
480            variant,
481            rtp_payload_format,
482            custom_picture_formats,
483            conference,
484            audio,
485            video,
486            data,
487            trailing_words,
488            raw_payload: payload.to_vec(),
489        })
490    }
491}
492
493fn bounded_wire_count(
494    count: u32,
495    maximum: usize,
496    field: &'static str,
497    message_id: u32,
498) -> Result<usize, CodecError> {
499    let count = usize::try_from(count).map_err(|_| CodecError::InvalidValue {
500        message_id,
501        field,
502        value: u64::from(count),
503    })?;
504    if count > maximum {
505        Err(CodecError::CountTooLarge {
506            message_id,
507            field,
508            count,
509            maximum,
510        })
511    } else {
512        Ok(count)
513    }
514}
515
516fn require_len(payload: &[u8], needed: usize, message_id: u32) -> Result<(), CodecError> {
517    if payload.len() < needed {
518        Err(CodecError::Truncated {
519            message_id,
520            needed,
521            actual: payload.len(),
522        })
523    } else {
524        Ok(())
525    }
526}
527
528fn require_declared_entries(
529    payload: &[u8],
530    offset: usize,
531    count: usize,
532    entry_size: usize,
533    message_id: u32,
534) -> Result<(), CodecError> {
535    if count == 0 {
536        Ok(())
537    } else {
538        require_len(payload, offset + count * entry_size, message_id)
539    }
540}
541
542fn word(payload: &[u8], offset: usize, message_id: u32) -> Result<u32, CodecError> {
543    require_len(payload, offset + 4, message_id)?;
544    Ok(u32::from_le_bytes(
545        payload[offset..offset + 4]
546            .try_into()
547            .expect("validated word bounds"),
548    ))
549}
550
551fn optional_word(payload: &[u8], offset: usize) -> Option<u32> {
552    payload
553        .get(offset..offset + 4)
554        .and_then(|bytes| bytes.try_into().ok())
555        .map(u32::from_le_bytes)
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::message::ClientMessage;
562    use crate::message::values::ProtocolVersion;
563    use crate::message::wire::{Frame, FrameDecoder};
564
565    fn put(payload: &mut [u8], offset: usize, value: u32) {
566        payload[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
567    }
568
569    fn fixture(source: &str) -> Vec<u8> {
570        source
571            .split_whitespace()
572            .map(|byte| u8::from_str_radix(byte, 16).expect("valid fixture byte"))
573            .collect()
574    }
575
576    fn assert_declared_table_boundaries(
577        protocol: u32,
578        count_offset: usize,
579        table_offset: usize,
580        entry_size: usize,
581        maximum: usize,
582        decoded_count: impl Fn(&CapabilityUpdate) -> usize,
583    ) {
584        for count in 1..=maximum {
585            let needed = table_offset + count * entry_size;
586            let mut truncated = vec![0; needed - 1];
587            put(&mut truncated, count_offset, count as u32);
588            assert!(matches!(
589                CapabilityUpdate::decode(
590                    CapabilityUpdateVariant::Version3,
591                    protocol,
592                    &truncated
593                ),
594                Err(CodecError::Truncated {
595                    needed: actual_needed,
596                    actual,
597                    ..
598                }) if actual_needed == needed && actual == needed - 1
599            ));
600
601            let mut complete = vec![0; needed];
602            put(&mut complete, count_offset, count as u32);
603            let update =
604                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, protocol, &complete)
605                    .unwrap();
606            assert_eq!(decoded_count(&update), count);
607            assert_eq!(update.raw_payload(), complete);
608        }
609    }
610
611    #[test]
612    fn version_three_update_exposes_every_capability_family() {
613        let mut payload = vec![0; 2_380];
614        put(&mut payload, 0, 1);
615        put(&mut payload, 4, 1);
616        put(&mut payload, 8, 1);
617        put(&mut payload, 12, 101);
618        put(&mut payload, 16, 1);
619        for (index, value) in [640, 480, 1, 2, 3].into_iter().enumerate() {
620            put(&mut payload, CUSTOM_PICTURES_OFFSET + index * 4, value);
621        }
622        put(&mut payload, CONFERENCE_OFFSET, 1);
623        put(&mut payload, CONFERENCE_OFFSET + 4, 2_048);
624        put(&mut payload, CONFERENCE_OFFSET + 8, 1);
625        put(&mut payload, CONFERENCE_OFFSET + 12, 1);
626        put(&mut payload, CONFERENCE_OFFSET + 16, 7);
627        put(&mut payload, CONFERENCE_OFFSET + 36, 9);
628        put(&mut payload, CONFERENCE_OFFSET + 40, 2);
629        put(&mut payload, CONFERENCE_OFFSET + 44, 1);
630        put(&mut payload, CONFERENCE_OFFSET + 48, 0);
631        put(&mut payload, AUDIO_OFFSET, Codec::Pcmu.wire_value());
632        put(&mut payload, AUDIO_OFFSET + 4, 4);
633        payload[AUDIO_OFFSET + 8..AUDIO_OFFSET + 16].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
634        put(&mut payload, VIDEO_OFFSET, Codec::H264.wire_value());
635        put(&mut payload, VIDEO_OFFSET + 4, 3);
636        put(&mut payload, VIDEO_OFFSET + 8, 1);
637        for (index, value) in [1, 5, 4_000, 128, 2, 7].into_iter().enumerate() {
638            put(&mut payload, VIDEO_OFFSET + 12 + index * 4, value);
639        }
640        put(&mut payload, VIDEO_OFFSET + 108, 1);
641        for (index, value) in [66, 31, 120, 240, 360, 480].into_iter().enumerate() {
642            put(&mut payload, VIDEO_OFFSET + 112 + index * 4, value);
643        }
644        put(&mut payload, VIDEO_OFFSET + 136, 2);
645        let data_offset = VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140;
646        for (index, value) in [0x120, 3, 8, 64_000, 1].into_iter().enumerate() {
647            put(&mut payload, data_offset + index * 4, value);
648        }
649        put(
650            &mut payload,
651            data_offset + MAX_DATA_CAPABILITIES * 20,
652            0xfeed_beef,
653        );
654
655        let update =
656            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
657        assert_eq!(update.rtp_payload_format(), 101);
658        assert_eq!(update.custom_picture_formats()[0].width, 640);
659        assert_eq!(update.conference().services[0].layouts, [7]);
660        assert_eq!(update.audio()[0].codec, Codec::Pcmu);
661        assert_eq!(update.audio()[0].codec_parameters, [1, 2, 3, 4, 5, 6, 7, 8]);
662        assert_eq!(update.video()[0].codec, Codec::H264);
663        assert_eq!(
664            update.video()[0].direction,
665            ReceiveTransmit::RECEIVE | ReceiveTransmit::TRANSMIT
666        );
667        assert_eq!(
668            update.video()[0].encryption_capability,
669            Some(EncryptionCapability::Capable)
670        );
671        assert_eq!(
672            update.video()[0].address_type,
673            Some(IpAddressType::Ipv4AndIpv6)
674        );
675        assert_eq!(update.data()[0].max_bit_rate, 64_000);
676        assert_eq!(
677            update.data()[0].encryption_capability,
678            Some(EncryptionCapability::Capable)
679        );
680        assert_eq!(update.trailing_words()[0], 0xfeed_beef);
681        assert_eq!(update.raw_payload(), payload);
682
683        let expected_audio = update.audio().to_vec();
684        let expected_video = update.video().to_vec();
685        let media = update.into_media_capabilities();
686        assert_eq!(media.audio(), expected_audio);
687        assert_eq!(media.video(), expected_video);
688    }
689
690    #[test]
691    fn version_three_accepts_every_bounded_progressive_length() {
692        for size in 20..=2_380 {
693            let payload = vec![0; size];
694            let update =
695                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
696            assert_eq!(update.raw_payload(), payload, "payload size {size}");
697        }
698
699        assert!(matches!(
700            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &[0; 19]),
701            Err(CodecError::Truncated { .. })
702        ));
703        assert!(matches!(
704            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &[0; 2_381]),
705            Err(CodecError::TrailingBytes { .. })
706        ));
707        assert!(
708            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &[0; 2_380]).is_ok()
709        );
710        assert!(matches!(
711            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &[0; 2_381]),
712            Err(CodecError::TrailingBytes { .. })
713        ));
714    }
715
716    #[test]
717    fn version_three_rejects_declared_tables_that_do_not_fit() {
718        for (count_offset, needed) in [
719            (0, AUDIO_OFFSET + AUDIO_CAPABILITY_SIZE),
720            (4, VIDEO_OFFSET + 140),
721            (8, VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140 + 20),
722            (16, CUSTOM_PICTURES_OFFSET + CUSTOM_PICTURE_SIZE),
723        ] {
724            let mut payload = vec![0; 20];
725            put(&mut payload, count_offset, 1);
726            assert!(matches!(
727                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload),
728                Err(CodecError::Truncated {
729                    needed: actual_needed,
730                    actual: 20,
731                    ..
732                }) if actual_needed == needed
733            ));
734        }
735
736        assert_declared_table_boundaries(
737            22,
738            16,
739            CUSTOM_PICTURES_OFFSET,
740            CUSTOM_PICTURE_SIZE,
741            MAX_CUSTOM_PICTURES,
742            |update| update.custom_picture_formats().len(),
743        );
744        assert_declared_table_boundaries(
745            22,
746            0,
747            AUDIO_OFFSET,
748            AUDIO_CAPABILITY_SIZE,
749            MAX_AUDIO_CAPABILITIES,
750            |update| update.audio().len(),
751        );
752        for (protocol, video_size) in [(16, 136), (17, 140)] {
753            assert_declared_table_boundaries(
754                protocol,
755                4,
756                VIDEO_OFFSET,
757                video_size,
758                MAX_VIDEO_CAPABILITIES,
759                |update| update.video().len(),
760            );
761            assert_declared_table_boundaries(
762                protocol,
763                8,
764                VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * video_size,
765                20,
766                MAX_DATA_CAPABILITIES,
767                |update| update.data().len(),
768            );
769        }
770    }
771
772    #[test]
773    fn version_three_rejects_declared_conference_services_that_do_not_fit() {
774        for count in 1..=MAX_CONFERENCE_SERVICES {
775            let needed = CONFERENCE_OFFSET + 12 + count * CONFERENCE_SERVICE_SIZE;
776            let mut truncated = vec![0; needed - 1];
777            put(&mut truncated, CONFERENCE_OFFSET + 8, count as u32);
778            assert!(matches!(
779                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &truncated),
780                Err(CodecError::Truncated {
781                    needed: actual_needed,
782                    actual,
783                    ..
784                }) if actual_needed == needed && actual == needed - 1
785            ));
786
787            let mut complete = vec![0; needed];
788            put(&mut complete, CONFERENCE_OFFSET + 8, count as u32);
789            let update =
790                CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &complete).unwrap();
791            assert_eq!(update.conference().services.len(), count);
792        }
793    }
794
795    #[test]
796    fn version_three_preserves_an_unstructured_suffix() {
797        let trailing_offset =
798            VIDEO_OFFSET + MAX_VIDEO_CAPABILITIES * 140 + MAX_DATA_CAPABILITIES * 20;
799        let mut payload = vec![0; trailing_offset + 7];
800        put(&mut payload, trailing_offset, 0xfeed_beef);
801        payload[trailing_offset + 4..].copy_from_slice(&[0xaa, 0xbb, 0xcc]);
802
803        let update =
804            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 22, &payload).unwrap();
805        assert_eq!(update.trailing_words(), [0xfeed_beef]);
806        assert_eq!(update.raw_payload(), payload);
807
808        let encoded = ClientMessage::CapabilitiesUpdate(update)
809            .encode(ProtocolVersion::V22)
810            .unwrap();
811        let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
812        assert_eq!(frame.payload, payload);
813    }
814
815    #[test]
816    fn version_three_video_entry_boundary_depends_on_protocol() {
817        let mut before = vec![0; 2_060];
818        put(&mut before, 4, 1);
819        put(&mut before, VIDEO_OFFSET, Codec::H264.wire_value());
820        let before =
821            CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 16, &before).unwrap();
822        assert_eq!(before.video().len(), 1);
823        assert_eq!(before.video()[0].address_type, None);
824
825        let mut from = vec![0; 2_100];
826        put(&mut from, 4, 1);
827        put(&mut from, VIDEO_OFFSET, Codec::H264.wire_value());
828        put(&mut from, VIDEO_OFFSET + 136, 2);
829        let from = CapabilityUpdate::decode(CapabilityUpdateVariant::Version3, 17, &from).unwrap();
830        assert_eq!(from.video().len(), 1);
831        assert_eq!(
832            from.video()[0].address_type,
833            Some(IpAddressType::Ipv4AndIpv6)
834        );
835    }
836
837    #[test]
838    fn update_rejects_truncation_and_every_oversized_count() {
839        assert!(matches!(
840            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &[0; 100]),
841            Err(CodecError::Truncated { .. })
842        ));
843
844        for (offset, count) in [(0, 19), (4, 11), (8, 6), (16, 7)] {
845            let mut payload = vec![0; 1_840];
846            put(&mut payload, offset, count);
847            assert!(matches!(
848                CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &payload),
849                Err(CodecError::CountTooLarge { .. })
850            ));
851        }
852
853        let mut services = vec![0; 1_840];
854        put(&mut services, CONFERENCE_OFFSET + 8, 5);
855        assert!(matches!(
856            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &services),
857            Err(CodecError::CountTooLarge { .. })
858        ));
859
860        let mut layouts = vec![0; 1_840];
861        put(&mut layouts, CONFERENCE_OFFSET + 8, 1);
862        put(&mut layouts, CONFERENCE_OFFSET + 12, 6);
863        assert!(matches!(
864            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &layouts),
865            Err(CodecError::CountTooLarge { .. })
866        ));
867
868        let mut levels = vec![0; 1_840];
869        put(&mut levels, 4, 1);
870        put(&mut levels, VIDEO_OFFSET + 8, 5);
871        assert!(matches!(
872            CapabilityUpdate::decode(CapabilityUpdateVariant::Version1, 3, &levels),
873            Err(CodecError::CountTooLarge { .. })
874        ));
875    }
876
877    #[test]
878    fn every_update_variant_round_trips_its_original_fixed_layout() {
879        for (variant, protocol, size) in [
880            (
881                CapabilityUpdateVariant::Version1,
882                ProtocolVersion::V3,
883                1_840,
884            ),
885            (
886                CapabilityUpdateVariant::Version1ExpandedVideo,
887                ProtocolVersion::V16,
888                2_000,
889            ),
890            (
891                CapabilityUpdateVariant::Version2,
892                ProtocolVersion::V22,
893                2_000,
894            ),
895            (
896                CapabilityUpdateVariant::Version3,
897                ProtocolVersion::V22,
898                2_380,
899            ),
900        ] {
901            let payload = vec![0; size];
902            let decoded = ClientMessage::decode_with_version(
903                Frame::new(protocol.wire(), variant.message_id(), payload.clone()),
904                protocol,
905            )
906            .unwrap();
907            assert!(matches!(
908                decoded,
909                ClientMessage::CapabilitiesUpdate(ref update) if update.variant() == variant
910            ));
911            let encoded = decoded.encode(protocol).unwrap();
912            let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
913            assert_eq!(frame.message_id, variant.message_id());
914            assert_eq!(frame.payload, payload);
915        }
916    }
917
918    #[test]
919    fn v22_7961_legacy_body_size_overrides_the_modern_session_protocol() {
920        let bytes = fixture(include_str!(
921            "../../tests/fixtures/golden/update_capabilities_7961_v22_legacy.hex"
922        ));
923        let frame = FrameDecoder::new().push(&bytes).unwrap().remove(0);
924        assert_eq!(frame.protocol_version, ProtocolVersion::V22.wire());
925        assert_eq!(
926            frame.message_id,
927            CapabilityUpdateVariant::Version1.message_id()
928        );
929        assert_eq!(frame.payload.len(), 1_840);
930        let decoded = ClientMessage::decode_with_version(frame, ProtocolVersion::V22).unwrap();
931
932        assert!(matches!(
933            decoded,
934            ClientMessage::CapabilitiesUpdate(ref update)
935                if update.variant() == CapabilityUpdateVariant::Version1
936        ));
937        let encoded = decoded.encode(ProtocolVersion::V22).unwrap();
938        let frame = FrameDecoder::new().push(&encoded).unwrap().remove(0);
939        assert_eq!(frame.protocol_version, ProtocolVersion::V22.wire());
940        assert_eq!(frame.payload.len(), 1_840);
941        assert_eq!(encoded, bytes);
942    }
943}