Skip to main content

dvb_si/descriptors/extension/
image_icon.rs

1//! Image Icon Descriptor — ETSI EN 300 468 §6.4.7 (tag_extension 0x00).
2use super::*;
3
4/// Icon transport mode — ETSI EN 300 468 §6.4.8 Table 146
5/// (`docs/en_300_468.md`, Table 146 — Icon transport mode coding).
6///
7/// 2-bit field. Selects where the icon data is carried.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10#[non_exhaustive]
11pub enum IconTransportMode {
12    /// `0` — the icon is delivered in the `icon_data_byte` sequence of bytes.
13    InlineData,
14    /// `1` — the location of the icon file is identified by the URL conveyed in
15    /// the `url_char` sequence of bytes.
16    Url,
17    /// `2`–`3` — reserved for future use.
18    Reserved(u8),
19}
20
21impl IconTransportMode {
22    #[must_use]
23    /// Creates a value from a 2-bit wire nibble (upper bits masked off),
24    /// preserving every possible value for lossless round-trip.
25    pub fn from_u8(v: u8) -> Self {
26        match v & 0x03 {
27            0 => Self::InlineData,
28            1 => Self::Url,
29            v => Self::Reserved(v),
30        }
31    }
32
33    #[must_use]
34    /// Returns the 2-bit wire nibble for this value.
35    pub fn to_u8(self) -> u8 {
36        match self {
37            Self::InlineData => 0,
38            Self::Url => 1,
39            Self::Reserved(v) => v,
40        }
41    }
42
43    #[must_use]
44    /// Returns the spec token for this value.
45    pub fn name(self) -> &'static str {
46        match self {
47            Self::InlineData => "inline data",
48            Self::Url => "URL",
49            Self::Reserved(_) => "reserved",
50        }
51    }
52}
53dvb_common::impl_spec_display!(IconTransportMode, Reserved);
54
55impl<'a> ExtensionBodyDef<'a> for ImageIcon<'a> {
56    const TAG_EXTENSION: u8 = 0x00;
57    const NAME: &'static str = "IMAGE_ICON";
58}
59
60/// image_icon body (Table 145). One descriptor instance; a full icon
61/// spans `descriptor_number` 0..=`last_descriptor_number`, reassembled
62/// by the caller.
63///
64/// Icon transport mode: Table 146
65/// (`0` = data bytes, `1` = URL, `2`-`3` = reserved).
66/// Coordinate system: Table 147
67/// (`0` = 720×576, `1` = 1280×720, `2` = 1920×1080,
68/// `3`-`6` = reserved, `7` = user defined).
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize))]
71#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
72pub struct ImageIcon<'a> {
73    /// `descriptor_number` (4 bits) — `[7:4]` of byte 0.
74    pub descriptor_number: u8,
75    /// `last_descriptor_number` (4 bits) — `[3:0]` of byte 0.
76    pub last_descriptor_number: u8,
77    /// `icon_id` (3 bits) — `[2:0]` of byte 1.
78    pub icon_id: u8,
79    /// First-segment metadata vs. continuation payload (keyed by `descriptor_number == 0`).
80    pub body: ImageIconBody<'a>,
81}
82
83/// First-segment metadata vs. continuation payload (keyed by `descriptor_number == 0`).
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
87#[non_exhaustive]
88pub enum ImageIconBody<'a> {
89    /// `descriptor_number == 0`: icon metadata + first payload chunk.
90    First(ImageIconFirst<'a>),
91    /// `descriptor_number != 0`: a continuation icon_data chunk (length-prefixed).
92    Continuation {
93        /// Length-delimited icon_data_byte run.
94        icon_data: &'a [u8],
95    },
96}
97
98/// First-segment metadata (`descriptor_number == 0`).
99#[derive(Debug, Clone, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize))]
101#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
102pub struct ImageIconFirst<'a> {
103    /// `icon_transport_mode` (2 bits) — Table 146.
104    pub icon_transport_mode: IconTransportMode,
105    /// `position` is `Some` iff `position_flag == 1`.
106    pub position: Option<IconPosition>,
107    /// `icon_type_char` text run (length-delimited in the wire).
108    pub icon_type: crate::text::DvbText<'a>,
109    /// Transport-mode-dependent payload (Table 146):
110    /// `0` → icon data bytes; `1` → URL text; `2`-`3` → empty.
111    pub payload: IconLocation<'a>,
112}
113
114/// Transport-mode-dependent payload for the first segment of an image_icon
115/// descriptor (Table 146, §6.4.8).
116#[derive(Debug, Clone, PartialEq, Eq)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
119#[non_exhaustive]
120pub enum IconLocation<'a> {
121    /// `icon_transport_mode` 0: raw icon data bytes (PNG, JPEG, etc.).
122    Data(&'a [u8]),
123    /// `icon_transport_mode` 1: `url_char` text pointing to the icon resource.
124    Url(crate::text::DvbText<'a>),
125    /// `icon_transport_mode` 2–3: reserved/empty.
126    None,
127}
128
129/// Position data (present iff `position_flag == 1`).
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize))]
132pub struct IconPosition {
133    /// `coordinate_system` (3 bits) — Table 147.
134    pub coordinate_system: u8,
135    /// `icon_horizontal_origin` (12 bits).
136    pub icon_horizontal_origin: u16,
137    /// `icon_vertical_origin` (12 bits).
138    pub icon_vertical_origin: u16,
139}
140
141impl<'a> Parse<'a> for ImageIcon<'a> {
142    type Error = crate::error::Error;
143    fn parse(sel: &'a [u8]) -> Result<Self> {
144        // Need at least byte0 (descriptor_number / last_descriptor_number)
145        // and byte1 (reserved + icon_id).
146        if sel.len() < 2 {
147            return Err(Error::BufferTooShort {
148                need: 2,
149                have: sel.len(),
150                what: "image_icon body",
151            });
152        }
153        let descriptor_number = sel[0] >> 4;
154        let last_descriptor_number = sel[0] & 0x0F;
155        let icon_id = sel[1] & 0x07;
156
157        let body = if descriptor_number == 0x00 {
158            // First segment: metadata + payload.
159            if sel.len() < 3 {
160                return Err(Error::BufferTooShort {
161                    need: 3,
162                    have: sel.len(),
163                    what: "image_icon body",
164                });
165            }
166            let packed = sel[2];
167            let icon_transport_mode = IconTransportMode::from_u8(packed >> 6);
168            let position_flag = (packed >> 5) & 0x01;
169            let mut pos = 3usize;
170
171            let position = if position_flag == 1 {
172                let coordinate_system = (packed >> 2) & 0x07;
173                if sel.len() < pos + 3 {
174                    return Err(Error::BufferTooShort {
175                        need: pos + 3,
176                        have: sel.len(),
177                        what: "image_icon body",
178                    });
179                }
180                let b0 = sel[pos];
181                let b1 = sel[pos + 1];
182                let b2 = sel[pos + 2];
183                let icon_horizontal_origin = (u16::from(b0) << 4) | (u16::from(b1) >> 4);
184                let icon_vertical_origin = ((u16::from(b1) & 0x0F) << 8) | u16::from(b2);
185                pos += 3;
186                Some(IconPosition {
187                    coordinate_system,
188                    icon_horizontal_origin,
189                    icon_vertical_origin,
190                })
191            } else {
192                None
193            };
194
195            // icon_type_length + run
196            if sel.len() < pos + 1 {
197                return Err(Error::BufferTooShort {
198                    need: pos + 1,
199                    have: sel.len(),
200                    what: "image_icon body",
201                });
202            }
203            let icon_type_length = sel[pos] as usize;
204            pos += 1;
205            if sel.len() < pos + icon_type_length {
206                return Err(Error::BufferTooShort {
207                    need: pos + icon_type_length,
208                    have: sel.len(),
209                    what: "image_icon body",
210                });
211            }
212            let icon_type = crate::text::DvbText::new(&sel[pos..pos + icon_type_length]);
213            pos += icon_type_length;
214
215            // Transport-mode-dependent payload
216            let payload = match icon_transport_mode {
217                IconTransportMode::InlineData => {
218                    if sel.len() < pos + 1 {
219                        return Err(Error::BufferTooShort {
220                            need: pos + 1,
221                            have: sel.len(),
222                            what: "image_icon body",
223                        });
224                    }
225                    let payload_len = sel[pos] as usize;
226                    pos += 1;
227                    if sel.len() < pos + payload_len {
228                        return Err(Error::BufferTooShort {
229                            need: pos + payload_len,
230                            have: sel.len(),
231                            what: "image_icon body",
232                        });
233                    }
234                    let p = &sel[pos..pos + payload_len];
235                    pos += payload_len;
236                    IconLocation::Data(p)
237                }
238                IconTransportMode::Url => {
239                    if sel.len() < pos + 1 {
240                        return Err(Error::BufferTooShort {
241                            need: pos + 1,
242                            have: sel.len(),
243                            what: "image_icon body",
244                        });
245                    }
246                    let payload_len = sel[pos] as usize;
247                    pos += 1;
248                    if sel.len() < pos + payload_len {
249                        return Err(Error::BufferTooShort {
250                            need: pos + payload_len,
251                            have: sel.len(),
252                            what: "image_icon body",
253                        });
254                    }
255                    let t = crate::text::DvbText::new(&sel[pos..pos + payload_len]);
256                    pos += payload_len;
257                    IconLocation::Url(t)
258                }
259                _ => IconLocation::None,
260            };
261
262            if pos != sel.len() {
263                return Err(invalid("image_icon: trailing bytes"));
264            }
265
266            ImageIconBody::First(ImageIconFirst {
267                icon_transport_mode,
268                position,
269                icon_type,
270                payload,
271            })
272        } else {
273            // Continuation segment.
274            if sel.len() < 3 {
275                return Err(Error::BufferTooShort {
276                    need: 3,
277                    have: sel.len(),
278                    what: "image_icon body",
279                });
280            }
281            let icon_data_length = sel[2] as usize;
282            if sel.len() < 3 + icon_data_length {
283                return Err(Error::BufferTooShort {
284                    need: 3 + icon_data_length,
285                    have: sel.len(),
286                    what: "image_icon body",
287                });
288            }
289            let icon_data = &sel[3..3 + icon_data_length];
290            if 3 + icon_data_length != sel.len() {
291                return Err(invalid("image_icon: trailing bytes"));
292            }
293            ImageIconBody::Continuation { icon_data }
294        };
295
296        Ok(ImageIcon {
297            descriptor_number,
298            last_descriptor_number,
299            icon_id,
300            body,
301        })
302    }
303}
304
305impl Serialize for ImageIcon<'_> {
306    type Error = crate::error::Error;
307    fn serialized_len(&self) -> usize {
308        2 + match &self.body {
309            ImageIconBody::First(f) => {
310                1 // packed byte
311                    + if f.position.is_some() { 3 } else { 0 }
312                    + 1 // icon_type_length
313                    + f.icon_type.len()
314                    + match &f.payload {
315                        IconLocation::Data(d) => 1 + d.len(),
316                        IconLocation::Url(u) => 1 + u.len(),
317                        IconLocation::None => 0,
318                    }
319            }
320            ImageIconBody::Continuation { icon_data } => 1 + icon_data.len(),
321        }
322    }
323    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
324        let len = self.serialized_len();
325        if buf.len() < len {
326            return Err(Error::OutputBufferTooSmall {
327                need: len,
328                have: buf.len(),
329            });
330        }
331        // byte0: descriptor_number(4) | last_descriptor_number(4)
332        buf[0] = (self.descriptor_number << 4) | (self.last_descriptor_number & 0x0F);
333        // byte1: reserved_future_use(5)=1 | icon_id(3)
334        buf[1] = 0xF8 | (self.icon_id & 0x07);
335        let mut p = 2;
336        match &self.body {
337            ImageIconBody::First(f) => {
338                // Packed byte: icon_transport_mode(2) | position_flag(1) | ...
339                let position_flag = u8::from(f.position.is_some());
340                let itm = f.icon_transport_mode.to_u8() & 0x03;
341                if let Some(pos) = &f.position {
342                    // ... | coordinate_system(3) | reserved_future_use(2)=1
343                    buf[p] = (itm << 6)
344                        | (position_flag << 5)
345                        | ((pos.coordinate_system & 0x07) << 2)
346                        | 0x03;
347                    p += 1;
348                    // 3 origin bytes: 12+12 bits packed
349                    let h = pos.icon_horizontal_origin & 0x0FFF;
350                    let v = pos.icon_vertical_origin & 0x0FFF;
351                    buf[p] = (h >> 4) as u8;
352                    buf[p + 1] = (((h & 0x0F) << 4) | ((v >> 8) & 0x0F)) as u8;
353                    buf[p + 2] = v as u8;
354                    p += 3;
355                } else {
356                    // ... | position_flag(1) | reserved_future_use(5)=1
357                    buf[p] = (itm << 6) | (position_flag << 5) | 0x1F;
358                    p += 1;
359                }
360                // icon_type_length + run
361                buf[p] = f.icon_type.len() as u8;
362                p += 1;
363                buf[p..p + f.icon_type.len()].copy_from_slice(f.icon_type.raw());
364                p += f.icon_type.len();
365                // Payload
366                match &f.payload {
367                    IconLocation::Data(d) => {
368                        buf[p] = d.len() as u8;
369                        p += 1;
370                        buf[p..p + d.len()].copy_from_slice(d);
371                    }
372                    IconLocation::Url(u) => {
373                        buf[p] = u.len() as u8;
374                        p += 1;
375                        buf[p..p + u.len()].copy_from_slice(u.raw());
376                    }
377                    IconLocation::None => {}
378                }
379            }
380            ImageIconBody::Continuation { icon_data } => {
381                buf[p] = icon_data.len() as u8;
382                p += 1;
383                buf[p..p + icon_data.len()].copy_from_slice(icon_data);
384            }
385        }
386        Ok(len)
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use crate::descriptors::extension::test_support::*;
394    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
395
396    #[test]
397    fn parse_image_icon_first_position_mode0() {
398        // descriptor_number=0, last=3, icon_id=5
399        // position_flag=1, coordinate_system=1, h_origin=100, v_origin=200
400        // icon_type=[0xDE, 0xAD], mode=0 icon_data=[0x01, 0x02, 0x03]
401        // byte0: (0<<4)|3 = 0x03
402        // byte1: icon_id=5, reserved=0 = 0x05
403        // packed: (0<<6)|(1<<5)|(1<<2) = 0x24
404        // origin: h=100=0x0064, v=200=0x00C8 → b0=0x06, b1=0x40, b2=0xC8
405        let sel = [
406            0x03, 0x05, 0x24, 0x06, 0x40, 0xC8, 0x02, 0xDE, 0xAD, 0x03, 0x01, 0x02, 0x03,
407        ];
408        let bytes = wrap(0x00, &sel);
409        let d = ExtensionDescriptor::parse(&bytes).unwrap();
410        assert_eq!(d.kind(), Some(ExtensionTag::ImageIcon));
411        match &d.body {
412            ExtensionBody::ImageIcon(b) => {
413                assert_eq!(b.descriptor_number, 0);
414                assert_eq!(b.last_descriptor_number, 3);
415                assert_eq!(b.icon_id, 5);
416                match &b.body {
417                    ImageIconBody::First(f) => {
418                        assert_eq!(f.icon_transport_mode, IconTransportMode::InlineData);
419                        assert!(f.position.is_some());
420                        let pos = f.position.as_ref().unwrap();
421                        assert_eq!(pos.coordinate_system, 1);
422                        assert_eq!(pos.icon_horizontal_origin, 100);
423                        assert_eq!(pos.icon_vertical_origin, 200);
424                        assert_eq!(f.icon_type.raw(), &[0xDE, 0xAD]);
425                        match &f.payload {
426                            IconLocation::Data(d) => assert_eq!(*d, &[0x01, 0x02, 0x03]),
427                            other => panic!("expected Data, got {other:?}"),
428                        }
429                    }
430                    other => panic!("expected First, got {other:?}"),
431                }
432            }
433            other => panic!("expected ImageIcon, got {other:?}"),
434        }
435        round_trip(&d);
436    }
437
438    #[test]
439    fn parse_image_icon_first_no_position_mode1() {
440        // descriptor_number=0, last=1, icon_id=7
441        // position_flag=0, mode=1 (URL), icon_type=[0xAB], url=b"http"
442        // byte0: (0<<4)|1 = 0x01; byte1: 0x07
443        // packed: (1<<6)|(0<<5) = 0x40
444        let sel = [0x01, 0x07, 0x40, 0x01, 0xAB, 0x04, b'h', b't', b't', b'p'];
445        let bytes = wrap(0x00, &sel);
446        let d = ExtensionDescriptor::parse(&bytes).unwrap();
447        match &d.body {
448            ExtensionBody::ImageIcon(b) => match &b.body {
449                ImageIconBody::First(f) => {
450                    assert_eq!(f.icon_transport_mode, IconTransportMode::Url);
451                    assert!(f.position.is_none());
452                    assert_eq!(f.icon_type.raw(), &[0xAB]);
453                    match &f.payload {
454                        IconLocation::Url(u) => assert_eq!(u.decode(), "http"),
455                        other => panic!("expected Url, got {other:?}"),
456                    }
457                }
458                other => panic!("expected First, got {other:?}"),
459            },
460            other => panic!("expected ImageIcon, got {other:?}"),
461        }
462        round_trip(&d);
463    }
464
465    #[test]
466    fn parse_image_icon_first_mode2_empty_payload() {
467        // descriptor_number=0, last=0, icon_id=0, mode=2 (reserved),
468        // position_flag=0, icon_type=0 bytes, empty payload
469        // byte0: 0x00; byte1: 0x00; packed: (2<<6) = 0x80
470        let sel = [0x00, 0x00, 0x80, 0x00];
471        let bytes = wrap(0x00, &sel);
472        let d = ExtensionDescriptor::parse(&bytes).unwrap();
473        match &d.body {
474            ExtensionBody::ImageIcon(b) => match &b.body {
475                ImageIconBody::First(f) => {
476                    assert_eq!(f.icon_transport_mode, IconTransportMode::Reserved(2));
477                    assert!(f.position.is_none());
478                    assert!(f.icon_type.is_empty());
479                    assert!(matches!(f.payload, IconLocation::None));
480                }
481                other => panic!("expected First, got {other:?}"),
482            },
483            other => panic!("expected ImageIcon, got {other:?}"),
484        }
485        round_trip(&d);
486    }
487
488    #[test]
489    fn parse_image_icon_continuation() {
490        // descriptor_number=2, last=3, icon_id=1
491        // icon_data=[0xAA, 0xBB, 0xCC, 0xDD]
492        // byte0: (2<<4)|3 = 0x23; byte1: 0x01; length=4
493        let sel = [0x23, 0x01, 0x04, 0xAA, 0xBB, 0xCC, 0xDD];
494        let bytes = wrap(0x00, &sel);
495        let d = ExtensionDescriptor::parse(&bytes).unwrap();
496        assert_eq!(d.kind(), Some(ExtensionTag::ImageIcon));
497        match &d.body {
498            ExtensionBody::ImageIcon(b) => {
499                assert_eq!(b.descriptor_number, 2);
500                assert_eq!(b.last_descriptor_number, 3);
501                assert_eq!(b.icon_id, 1);
502                match &b.body {
503                    ImageIconBody::Continuation { icon_data } => {
504                        assert_eq!(icon_data, &[0xAA, 0xBB, 0xCC, 0xDD]);
505                    }
506                    other => panic!("expected Continuation, got {other:?}"),
507                }
508            }
509            other => panic!("expected ImageIcon, got {other:?}"),
510        }
511        round_trip(&d);
512    }
513
514    #[test]
515    fn parse_image_icon_rejects_trailing_bytes() {
516        // First segment with an extra trailing byte.
517        // mode=2, one extra byte 0xFF after the complete parse.
518        let sel = [0x00, 0x00, 0x80, 0x00, 0xFF];
519        let bytes = wrap(0x00, &sel);
520        assert!(matches!(
521            ExtensionDescriptor::parse(&bytes).unwrap_err(),
522            crate::error::Error::InvalidDescriptor {
523                tag: super::TAG,
524                ..
525            }
526        ));
527    }
528
529    #[test]
530    fn parse_image_icon_rejects_truncated_continuation() {
531        // Continuation with length=5 but only 3 data bytes.
532        let sel = [0x23, 0x01, 0x05, 0xAA, 0xBB, 0xCC];
533        let bytes = wrap(0x00, &sel);
534        assert!(matches!(
535            ExtensionDescriptor::parse(&bytes).unwrap_err(),
536            crate::error::Error::BufferTooShort { .. }
537        ));
538    }
539
540    #[cfg(feature = "serde")]
541    #[test]
542    fn serde_serialize_image_icon() {
543        let d = ExtensionDescriptor {
544            tag_extension: 0x00,
545            body: ExtensionBody::ImageIcon(ImageIcon {
546                descriptor_number: 2,
547                last_descriptor_number: 3,
548                icon_id: 1,
549                body: ImageIconBody::Continuation {
550                    icon_data: &[0xAA, 0xBB],
551                },
552            }),
553        };
554        let json = serde_json::to_string(&d).unwrap();
555        assert!(json.contains("\"tag_extension\":0"));
556        assert!(json.contains("\"imageIcon\""));
557    }
558
559    #[test]
560    fn icon_transport_mode_full_range_round_trip() {
561        for v in 0u8..=0x03 {
562            let itm = IconTransportMode::from_u8(v);
563            assert_eq!(
564                itm.to_u8(),
565                v,
566                "IconTransportMode round-trip failed for {v}"
567            );
568        }
569    }
570
571    #[test]
572    fn icon_transport_mode_known_values() {
573        assert_eq!(IconTransportMode::from_u8(0), IconTransportMode::InlineData);
574        assert_eq!(IconTransportMode::from_u8(1), IconTransportMode::Url);
575        assert_eq!(
576            IconTransportMode::from_u8(2),
577            IconTransportMode::Reserved(2)
578        );
579        assert_eq!(IconTransportMode::InlineData.name(), "inline data");
580        assert_eq!(IconTransportMode::Url.name(), "URL");
581        assert_eq!(IconTransportMode::Reserved(3).name(), "reserved");
582    }
583}