Skip to main content

dvb_si/descriptors/extension/
service_prominence.rs

1//! Service Prominence Descriptor — ETSI EN 300 468 §6.4.18 (tag_extension 0x22).
2use super::*;
3use alloc::vec::Vec;
4
5impl<'a> ExtensionBodyDef<'a> for ServiceProminence<'a> {
6    const TAG_EXTENSION: u8 = 0x22;
7    const NAME: &'static str = "SERVICE_PROMINENCE";
8}
9
10/// `[4]` of the SOGI-entry packed byte: `reserved_future_use` = 1 per DVB convention.
11const SOGI_RESERVED_FUTURE_USE: u8 = 0x10;
12
13/// service_prominence body (Table 162c). The SOGI loop is unfolded;
14/// each entry's target_region loop uses the typed
15/// [`TargetRegionEntry`]/[`RegionCodes`] from `target_region.rs`
16/// (Table 156, same shape as `target_region_descriptor`).
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
20pub struct ServiceProminence<'a> {
21    /// SOGI entries (the `SOGI_list_length`-delimited loop).
22    pub sogi_list: Vec<SogiEntry>,
23    /// Trailing `private_data_byte` run.
24    pub private_data: &'a [u8],
25}
26
27/// One SOGI entry (Table 162c inner loop).
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30pub struct SogiEntry {
31    /// `SOGI_flag` (1 bit).
32    pub sogi_flag: bool,
33    /// `SOGI_priority` (12 bits).
34    pub sogi_priority: u16,
35    /// `service_id` (16 bits), present iff `service_flag` wire bit is set.
36    pub service_id: Option<u16>,
37    /// Typed `target_region` loop (Table 156 format), present iff `target_region_flag` wire bit is set.
38    pub target_region_loop: Option<Vec<super::target_region::TargetRegionEntry>>,
39}
40
41impl<'a> Parse<'a> for ServiceProminence<'a> {
42    type Error = crate::error::Error;
43    fn parse(sel: &'a [u8]) -> Result<Self> {
44        if sel.is_empty() {
45            return Err(Error::BufferTooShort {
46                need: 1,
47                have: sel.len(),
48                what: "service_prominence body",
49            });
50        }
51        let sogi_list_length = sel[0] as usize;
52        if sel.len() < 1 + sogi_list_length {
53            return Err(Error::BufferTooShort {
54                need: 1 + sogi_list_length,
55                have: sel.len(),
56                what: "service_prominence body",
57            });
58        }
59        let sogi_slice = &sel[1..1 + sogi_list_length];
60        let mut sogi_list = Vec::new();
61        let mut k = 0;
62        while k < sogi_slice.len() {
63            if sogi_slice.len() - k < 2 {
64                return Err(Error::BufferTooShort {
65                    need: 1 + k + 2,
66                    have: sel.len(),
67                    what: "service_prominence body",
68                });
69            }
70            let byte0 = sogi_slice[k];
71            let byte1 = sogi_slice[k + 1];
72            let sogi_flag = (byte0 >> 7) != 0;
73            let target_region_flag = ((byte0 >> 6) & 0x01) != 0;
74            let service_flag = ((byte0 >> 5) & 0x01) != 0;
75            let sogi_priority = ((u16::from(byte0) & 0x0F) << 8) | u16::from(byte1);
76            k += 2;
77            let service_id = if service_flag {
78                if sogi_slice.len() - k < 2 {
79                    return Err(Error::BufferTooShort {
80                        need: 1 + k + 2,
81                        have: sel.len(),
82                        what: "service_prominence body",
83                    });
84                }
85                let id = u16::from_be_bytes([sogi_slice[k], sogi_slice[k + 1]]);
86                k += 2;
87                Some(id)
88            } else {
89                None
90            };
91            let target_region_loop = if target_region_flag {
92                if sogi_slice.len() - k < 1 {
93                    return Err(Error::BufferTooShort {
94                        need: 1 + k + 1,
95                        have: sel.len(),
96                        what: "service_prominence body",
97                    });
98                }
99                let region_len = sogi_slice[k] as usize;
100                k += 1;
101                if sogi_slice.len() - k < region_len {
102                    return Err(Error::BufferTooShort {
103                        need: 1 + k + region_len,
104                        have: sel.len(),
105                        what: "service_prominence body",
106                    });
107                }
108                let region_bytes = &sogi_slice[k..k + region_len];
109                k += region_len;
110                let entries = super::target_region::parse_region_entries(region_bytes, 0)?;
111                Some(entries)
112            } else {
113                None
114            };
115            sogi_list.push(SogiEntry {
116                sogi_flag,
117                sogi_priority,
118                service_id,
119                target_region_loop,
120            });
121        }
122        Ok(ServiceProminence {
123            sogi_list,
124            private_data: &sel[1 + sogi_list_length..],
125        })
126    }
127}
128
129impl Serialize for ServiceProminence<'_> {
130    type Error = crate::error::Error;
131    fn serialized_len(&self) -> usize {
132        let sogi_list_length: usize = self
133            .sogi_list
134            .iter()
135            .map(|e| {
136                2 + if e.service_id.is_some() { 2 } else { 0 }
137                    + if e.target_region_loop.is_some() {
138                        1 + e.target_region_loop.as_ref().map_or(0, |entries| {
139                            super::target_region::region_entries_serialized_len(entries)
140                        })
141                    } else {
142                        0
143                    }
144            })
145            .sum();
146        1 + sogi_list_length + self.private_data.len()
147    }
148    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
149        let len = self.serialized_len();
150        if buf.len() < len {
151            return Err(Error::OutputBufferTooSmall {
152                need: len,
153                have: buf.len(),
154            });
155        }
156        let sogi_len = len - 1 - self.private_data.len();
157        buf[0] = sogi_len as u8;
158        let mut p = 1;
159        for e in &self.sogi_list {
160            buf[p] = ((e.sogi_flag as u8) << 7)
161                | ((e.target_region_loop.is_some() as u8) << 6)
162                | ((e.service_id.is_some() as u8) << 5)
163                | SOGI_RESERVED_FUTURE_USE
164                | ((e.sogi_priority >> 8) as u8 & 0x0F);
165            buf[p + 1] = e.sogi_priority as u8;
166            p += 2;
167            if let Some(id) = e.service_id {
168                buf[p..p + 2].copy_from_slice(&id.to_be_bytes());
169                p += 2;
170            }
171            if let Some(entries) = &e.target_region_loop {
172                let entries_len = super::target_region::region_entries_serialized_len(entries);
173                buf[p] = entries_len as u8;
174                p += 1;
175                super::target_region::write_region_entries(entries, buf, p);
176                p += entries_len;
177            }
178        }
179        buf[p..p + self.private_data.len()].copy_from_slice(self.private_data);
180        Ok(len)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::descriptors::extension::test_support::*;
188    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
189
190    #[test]
191    fn parse_service_prominence_one_entry_service_only() {
192        // One SOGI entry: service_flag=1, target_region_flag=0,
193        // sogi_priority=0x123, service_id=0x4567, private_data [0xAB]
194        let sel = [0x04, 0x21, 0x23, 0x45, 0x67, 0xAB];
195        let bytes = wrap(0x22, &sel);
196        let d = ExtensionDescriptor::parse(&bytes).unwrap();
197        assert_eq!(d.kind(), Some(ExtensionTag::ServiceProminence));
198        match &d.body {
199            ExtensionBody::ServiceProminence(b) => {
200                assert_eq!(b.sogi_list.len(), 1);
201                let e = &b.sogi_list[0];
202                assert!(!e.sogi_flag);
203                assert!(e.target_region_loop.is_none());
204                assert!(e.service_id.is_some());
205                assert_eq!(e.sogi_priority, 0x0123);
206                assert_eq!(e.service_id, Some(0x4567));
207                assert_eq!(b.private_data, &[0xAB]);
208            }
209            other => panic!("expected ServiceProminence, got {other:?}"),
210        }
211        round_trip(&d);
212    }
213
214    #[test]
215    fn parse_service_prominence_one_entry_target_region() {
216        // One SOGI entry: service_flag=0, target_region_flag=1,
217        // sogi_priority=0x001, target_region_loop = one entry (depth=0, no cc).
218        let sel = [0x04, 0x40, 0x01, 0x01, 0xF8];
219        let bytes = wrap(0x22, &sel);
220        let d = ExtensionDescriptor::parse(&bytes).unwrap();
221        match &d.body {
222            ExtensionBody::ServiceProminence(b) => {
223                assert_eq!(b.sogi_list.len(), 1);
224                let e = &b.sogi_list[0];
225                assert!(!e.sogi_flag);
226                assert_eq!(e.sogi_priority, 0x0001);
227                assert!(e.service_id.is_none());
228                assert!(e.target_region_loop.is_some());
229                let tr = e.target_region_loop.as_ref().unwrap();
230                assert_eq!(tr.len(), 1);
231                assert_eq!(tr[0].country_code, None);
232                assert_eq!(
233                    tr[0].region_codes,
234                    super::super::target_region::RegionCodes::None
235                );
236                assert!(b.private_data.is_empty());
237            }
238            other => panic!("expected ServiceProminence, got {other:?}"),
239        }
240        round_trip(&d);
241    }
242
243    #[test]
244    fn parse_service_prominence_two_entries_plus_private() {
245        // Two SOGI entries + private_data tail.
246        // Entry 0: service_flag=1, sogi_priority=0xABC, service_id=0x1111.
247        // Entry 1: target_region_flag=1, sogi_priority=0x345, region=[0xF8] (depth=0 entry).
248        let sel = [0x08, 0x2A, 0xBC, 0x11, 0x11, 0x43, 0x45, 0x01, 0xF8, 0xDD];
249        let bytes = wrap(0x22, &sel);
250        let d = ExtensionDescriptor::parse(&bytes).unwrap();
251        match &d.body {
252            ExtensionBody::ServiceProminence(b) => {
253                assert_eq!(b.sogi_list.len(), 2);
254                let e0 = &b.sogi_list[0];
255                assert_eq!(e0.sogi_priority, 0x0ABC);
256                assert_eq!(e0.service_id, Some(0x1111));
257                assert!(e0.target_region_loop.is_none());
258                let e1 = &b.sogi_list[1];
259                assert!(!e1.sogi_flag);
260                assert!(e1.service_id.is_none());
261                assert_eq!(e1.sogi_priority, 0x0345);
262                assert!(e1.target_region_loop.is_some());
263                assert_eq!(e1.target_region_loop.as_ref().unwrap().len(), 1);
264                assert_eq!(b.private_data, &[0xDD]);
265            }
266            other => panic!("expected ServiceProminence, got {other:?}"),
267        }
268        round_trip(&d);
269    }
270
271    #[test]
272    fn parse_service_prominence_empty_list_private_only() {
273        // SOGI_list_length=0, private=[0x01, 0x02]
274        let sel = [0x00, 0x01, 0x02];
275        let bytes = wrap(0x22, &sel);
276        let d = ExtensionDescriptor::parse(&bytes).unwrap();
277        match &d.body {
278            ExtensionBody::ServiceProminence(b) => {
279                assert!(b.sogi_list.is_empty());
280                assert_eq!(b.private_data, &[0x01, 0x02]);
281            }
282            other => panic!("expected ServiceProminence, got {other:?}"),
283        }
284        round_trip(&d);
285    }
286
287    #[test]
288    fn parse_service_prominence_rejects_overrun() {
289        // SOGI_list_length=5 but only 3 bytes follow
290        let sel = [0x05, 0xAA, 0xBB, 0xCC];
291        let bytes = wrap(0x22, &sel);
292        assert!(matches!(
293            ExtensionDescriptor::parse(&bytes).unwrap_err(),
294            crate::error::Error::BufferTooShort { .. }
295        ));
296    }
297
298    #[test]
299    fn parse_service_prominence_rejects_entry_overrun() {
300        // SOGI_list_length=3, service_flag=1 but no service_id bytes follow
301        let sel = [0x03, 0x20, 0x00, 0x00];
302        let bytes = wrap(0x22, &sel);
303        assert!(matches!(
304            ExtensionDescriptor::parse(&bytes).unwrap_err(),
305            crate::error::Error::BufferTooShort { .. }
306        ));
307    }
308
309    #[cfg(feature = "serde")]
310    #[test]
311    fn serde_serialize_service_prominence() {
312        let d = ExtensionDescriptor {
313            tag_extension: 0x22,
314            body: ExtensionBody::ServiceProminence(ServiceProminence {
315                sogi_list: vec![SogiEntry {
316                    sogi_flag: false,
317                    sogi_priority: 0x123,
318                    service_id: Some(0x4567),
319                    target_region_loop: None,
320                }],
321                private_data: &[0xAB],
322            }),
323        };
324        let json = serde_json::to_string(&d).unwrap();
325        assert!(json.contains("\"tag_extension\":34"));
326        assert!(json.contains("\"serviceProminence\""));
327    }
328}