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                let (b, _) =
79                    sogi_slice[k..]
80                        .split_first_chunk::<2>()
81                        .ok_or(Error::BufferTooShort {
82                            need: 1 + k + 2,
83                            have: sel.len(),
84                            what: "service_prominence body",
85                        })?;
86                let id = u16::from_be_bytes(*b);
87                k += 2;
88                Some(id)
89            } else {
90                None
91            };
92            let target_region_loop = if target_region_flag {
93                if sogi_slice.len() - k < 1 {
94                    return Err(Error::BufferTooShort {
95                        need: 1 + k + 1,
96                        have: sel.len(),
97                        what: "service_prominence body",
98                    });
99                }
100                let region_len = sogi_slice[k] as usize;
101                k += 1;
102                if sogi_slice.len() - k < region_len {
103                    return Err(Error::BufferTooShort {
104                        need: 1 + k + region_len,
105                        have: sel.len(),
106                        what: "service_prominence body",
107                    });
108                }
109                let region_bytes = &sogi_slice[k..k + region_len];
110                k += region_len;
111                let entries = super::target_region::parse_region_entries(region_bytes, 0)?;
112                Some(entries)
113            } else {
114                None
115            };
116            sogi_list.push(SogiEntry {
117                sogi_flag,
118                sogi_priority,
119                service_id,
120                target_region_loop,
121            });
122        }
123        Ok(ServiceProminence {
124            sogi_list,
125            private_data: &sel[1 + sogi_list_length..],
126        })
127    }
128}
129
130impl Serialize for ServiceProminence<'_> {
131    type Error = crate::error::Error;
132    fn serialized_len(&self) -> usize {
133        let sogi_list_length: usize = self
134            .sogi_list
135            .iter()
136            .map(|e| {
137                2 + if e.service_id.is_some() { 2 } else { 0 }
138                    + if e.target_region_loop.is_some() {
139                        1 + e.target_region_loop.as_ref().map_or(0, |entries| {
140                            super::target_region::region_entries_serialized_len(entries)
141                        })
142                    } else {
143                        0
144                    }
145            })
146            .sum();
147        1 + sogi_list_length + self.private_data.len()
148    }
149    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
150        let len = self.serialized_len();
151        if buf.len() < len {
152            return Err(Error::OutputBufferTooSmall {
153                need: len,
154                have: buf.len(),
155            });
156        }
157        let sogi_len = len - 1 - self.private_data.len();
158        buf[0] = sogi_len as u8;
159        let mut p = 1;
160        for e in &self.sogi_list {
161            buf[p] = ((e.sogi_flag as u8) << 7)
162                | ((e.target_region_loop.is_some() as u8) << 6)
163                | ((e.service_id.is_some() as u8) << 5)
164                | SOGI_RESERVED_FUTURE_USE
165                | ((e.sogi_priority >> 8) as u8 & 0x0F);
166            buf[p + 1] = e.sogi_priority as u8;
167            p += 2;
168            if let Some(id) = e.service_id {
169                buf[p..p + 2].copy_from_slice(&id.to_be_bytes());
170                p += 2;
171            }
172            if let Some(entries) = &e.target_region_loop {
173                let entries_len = super::target_region::region_entries_serialized_len(entries);
174                buf[p] = entries_len as u8;
175                p += 1;
176                super::target_region::write_region_entries(entries, buf, p);
177                p += entries_len;
178            }
179        }
180        buf[p..p + self.private_data.len()].copy_from_slice(self.private_data);
181        Ok(len)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use crate::descriptors::extension::test_support::*;
189    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
190
191    #[test]
192    fn parse_service_prominence_one_entry_service_only() {
193        // One SOGI entry: service_flag=1, target_region_flag=0,
194        // sogi_priority=0x123, service_id=0x4567, private_data [0xAB]
195        let sel = [0x04, 0x21, 0x23, 0x45, 0x67, 0xAB];
196        let bytes = wrap(0x22, &sel);
197        let d = ExtensionDescriptor::parse(&bytes).unwrap();
198        assert_eq!(d.kind(), Some(ExtensionTag::ServiceProminence));
199        match &d.body {
200            ExtensionBody::ServiceProminence(b) => {
201                assert_eq!(b.sogi_list.len(), 1);
202                let e = &b.sogi_list[0];
203                assert!(!e.sogi_flag);
204                assert!(e.target_region_loop.is_none());
205                assert!(e.service_id.is_some());
206                assert_eq!(e.sogi_priority, 0x0123);
207                assert_eq!(e.service_id, Some(0x4567));
208                assert_eq!(b.private_data, &[0xAB]);
209            }
210            other => panic!("expected ServiceProminence, got {other:?}"),
211        }
212        round_trip(&d);
213    }
214
215    #[test]
216    fn parse_service_prominence_one_entry_target_region() {
217        // One SOGI entry: service_flag=0, target_region_flag=1,
218        // sogi_priority=0x001, target_region_loop = one entry (depth=0, no cc).
219        let sel = [0x04, 0x40, 0x01, 0x01, 0xF8];
220        let bytes = wrap(0x22, &sel);
221        let d = ExtensionDescriptor::parse(&bytes).unwrap();
222        match &d.body {
223            ExtensionBody::ServiceProminence(b) => {
224                assert_eq!(b.sogi_list.len(), 1);
225                let e = &b.sogi_list[0];
226                assert!(!e.sogi_flag);
227                assert_eq!(e.sogi_priority, 0x0001);
228                assert!(e.service_id.is_none());
229                assert!(e.target_region_loop.is_some());
230                let tr = e.target_region_loop.as_ref().unwrap();
231                assert_eq!(tr.len(), 1);
232                assert_eq!(tr[0].country_code, None);
233                assert_eq!(
234                    tr[0].region_codes,
235                    super::super::target_region::RegionCodes::None
236                );
237                assert!(b.private_data.is_empty());
238            }
239            other => panic!("expected ServiceProminence, got {other:?}"),
240        }
241        round_trip(&d);
242    }
243
244    #[test]
245    fn parse_service_prominence_two_entries_plus_private() {
246        // Two SOGI entries + private_data tail.
247        // Entry 0: service_flag=1, sogi_priority=0xABC, service_id=0x1111.
248        // Entry 1: target_region_flag=1, sogi_priority=0x345, region=[0xF8] (depth=0 entry).
249        let sel = [0x08, 0x2A, 0xBC, 0x11, 0x11, 0x43, 0x45, 0x01, 0xF8, 0xDD];
250        let bytes = wrap(0x22, &sel);
251        let d = ExtensionDescriptor::parse(&bytes).unwrap();
252        match &d.body {
253            ExtensionBody::ServiceProminence(b) => {
254                assert_eq!(b.sogi_list.len(), 2);
255                let e0 = &b.sogi_list[0];
256                assert_eq!(e0.sogi_priority, 0x0ABC);
257                assert_eq!(e0.service_id, Some(0x1111));
258                assert!(e0.target_region_loop.is_none());
259                let e1 = &b.sogi_list[1];
260                assert!(!e1.sogi_flag);
261                assert!(e1.service_id.is_none());
262                assert_eq!(e1.sogi_priority, 0x0345);
263                assert!(e1.target_region_loop.is_some());
264                assert_eq!(e1.target_region_loop.as_ref().unwrap().len(), 1);
265                assert_eq!(b.private_data, &[0xDD]);
266            }
267            other => panic!("expected ServiceProminence, got {other:?}"),
268        }
269        round_trip(&d);
270    }
271
272    #[test]
273    fn parse_service_prominence_empty_list_private_only() {
274        // SOGI_list_length=0, private=[0x01, 0x02]
275        let sel = [0x00, 0x01, 0x02];
276        let bytes = wrap(0x22, &sel);
277        let d = ExtensionDescriptor::parse(&bytes).unwrap();
278        match &d.body {
279            ExtensionBody::ServiceProminence(b) => {
280                assert!(b.sogi_list.is_empty());
281                assert_eq!(b.private_data, &[0x01, 0x02]);
282            }
283            other => panic!("expected ServiceProminence, got {other:?}"),
284        }
285        round_trip(&d);
286    }
287
288    #[test]
289    fn parse_service_prominence_rejects_overrun() {
290        // SOGI_list_length=5 but only 3 bytes follow
291        let sel = [0x05, 0xAA, 0xBB, 0xCC];
292        let bytes = wrap(0x22, &sel);
293        assert!(matches!(
294            ExtensionDescriptor::parse(&bytes).unwrap_err(),
295            crate::error::Error::BufferTooShort { .. }
296        ));
297    }
298
299    #[test]
300    fn parse_service_prominence_rejects_entry_overrun() {
301        // SOGI_list_length=3, service_flag=1 but no service_id bytes follow
302        let sel = [0x03, 0x20, 0x00, 0x00];
303        let bytes = wrap(0x22, &sel);
304        assert!(matches!(
305            ExtensionDescriptor::parse(&bytes).unwrap_err(),
306            crate::error::Error::BufferTooShort { .. }
307        ));
308    }
309
310    #[cfg(feature = "serde")]
311    #[test]
312    fn serde_serialize_service_prominence() {
313        let d = ExtensionDescriptor {
314            tag_extension: 0x22,
315            body: ExtensionBody::ServiceProminence(ServiceProminence {
316                sogi_list: vec![SogiEntry {
317                    sogi_flag: false,
318                    sogi_priority: 0x123,
319                    service_id: Some(0x4567),
320                    target_region_loop: None,
321                }],
322                private_data: &[0xAB],
323            }),
324        };
325        let json = serde_json::to_string(&d).unwrap();
326        assert!(json.contains("\"tag_extension\":34"));
327        assert!(json.contains("\"serviceProminence\""));
328    }
329}