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