Skip to main content

dvb_si/descriptors/
mosaic.rs

1//! Mosaic Descriptor — ETSI EN 300 468 §6.2.21 (tag 0x51).
2//!
3//! Table 71 (PDF p. 91, verified against the vendored PDF). Describes a mosaic
4//! service: a grid of elementary cells, each logical cell grouping one or more
5//! elementary cells and optionally linking to a bouquet / service / event.
6//!
7//! Wire layout:
8//!   header byte: mosaic_entry_point(1) | num_horizontal_cells(3)
9//!                | reserved(1) | num_vertical_cells(3)
10//!   for each logical cell:
11//!     2 bytes: logical_cell_id(6) | reserved(7) | logical_cell_presentation_info(3)
12//!     elementary_cell_field_length(8)
13//!     elementary_cell_field_length × { reserved(2) | elementary_cell_id(6) }
14//!     cell_linkage_info(8)
15//!     conditional linkage payload (see CellLinkage)
16//!
17//! The first loop level (logical cells) is typed. Within each cell, the
18//! elementary_cell_id list is exposed as `Vec<u8>` of 6-bit ids and the
19//! linkage payload as the typed `CellLinkage` enum.
20
21use super::descriptor_body;
22use crate::error::{Error, Result};
23use alloc::vec::Vec;
24use dvb_common::{Parse, Serialize};
25
26/// Descriptor tag for mosaic_descriptor.
27pub const TAG: u8 = 0x51;
28const HEADER_LEN: usize = 2;
29const GRID_HEADER_LEN: usize = 1;
30const CELL_FIXED_LEN: usize = 3; // 2 id/presentation bytes + elementary_cell_field_length
31/// Maximum body length expressible in the 8-bit `descriptor_length` field.
32const MAX_BODY_LEN: usize = u8::MAX as usize;
33/// Maximum elementary_cell_field_length (8-bit field).
34const MAX_ELEM_FIELD: usize = u8::MAX as usize;
35
36const ENTRY_POINT_MASK: u8 = 0x80; // header bit 7
37const ELEM_CELL_ID_MASK: u8 = 0x3F; // low 6 bits
38
39/// Conditional linkage payload selected by `cell_linkage_info` (Table 75).
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42#[non_exhaustive]
43pub enum CellLinkage {
44    /// 0x00 — undefined (no payload).
45    Undefined,
46    /// 0x01 — bouquet related.
47    Bouquet {
48        /// Linked bouquet_id.
49        bouquet_id: u16,
50    },
51    /// 0x02 — service related.
52    Service {
53        /// Linked original_network_id.
54        original_network_id: u16,
55        /// Linked transport_stream_id.
56        transport_stream_id: u16,
57        /// Linked service_id.
58        service_id: u16,
59    },
60    /// 0x03 — other mosaic related.
61    OtherMosaic {
62        /// Linked original_network_id.
63        original_network_id: u16,
64        /// Linked transport_stream_id.
65        transport_stream_id: u16,
66        /// Linked service_id.
67        service_id: u16,
68    },
69    /// 0x04 — event related.
70    Event {
71        /// Linked original_network_id.
72        original_network_id: u16,
73        /// Linked transport_stream_id.
74        transport_stream_id: u16,
75        /// Linked service_id.
76        service_id: u16,
77        /// Linked event_id.
78        event_id: u16,
79    },
80    /// 0x05..=0xFF — reserved; the raw byte is preserved so unknown linkage
81    /// values round-trip (they carry no further payload per Table 75).
82    Reserved {
83        /// The raw cell_linkage_info byte.
84        value: u8,
85    },
86}
87
88impl CellLinkage {
89    /// The cell_linkage_info byte this variant serializes to.
90    fn info_byte(&self) -> u8 {
91        match self {
92            CellLinkage::Undefined => 0x00,
93            CellLinkage::Bouquet { .. } => 0x01,
94            CellLinkage::Service { .. } => 0x02,
95            CellLinkage::OtherMosaic { .. } => 0x03,
96            CellLinkage::Event { .. } => 0x04,
97            CellLinkage::Reserved { value } => *value,
98        }
99    }
100
101    /// Length of the conditional payload (excluding the info byte).
102    fn payload_len(&self) -> usize {
103        match self {
104            CellLinkage::Undefined | CellLinkage::Reserved { .. } => 0,
105            CellLinkage::Bouquet { .. } => 2,
106            CellLinkage::Service { .. } | CellLinkage::OtherMosaic { .. } => 6,
107            CellLinkage::Event { .. } => 8,
108        }
109    }
110}
111
112/// One logical cell within the mosaic.
113#[derive(Debug, Clone, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize))]
115pub struct MosaicLogicalCell {
116    /// 6-bit logical_cell_id.
117    pub logical_cell_id: u8,
118    /// 3-bit logical_cell_presentation_info (Table 74): 1 = video,
119    /// 2 = still picture, 3 = graphics/text.
120    pub presentation_info: u8,
121    /// elementary_cell_id values (each 6 bits) composing this logical cell.
122    pub elementary_cell_ids: Vec<u8>,
123    /// Linkage of this logical cell to a bouquet / service / event.
124    pub linkage: CellLinkage,
125}
126
127/// Mosaic Descriptor.
128#[derive(Debug, Clone, PartialEq, Eq)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize))]
130pub struct MosaicDescriptor {
131    /// mosaic_entry_point: set when this mosaic is the top of a hierarchy.
132    pub mosaic_entry_point: bool,
133    /// 3-bit number_of_horizontal_elementary_cells (Table 72; 0 = one cell).
134    pub num_horizontal_cells: u8,
135    /// 3-bit number_of_vertical_elementary_cells (Table 73; 0 = one cell).
136    pub num_vertical_cells: u8,
137    /// Logical cells in wire order.
138    pub logical_cells: Vec<MosaicLogicalCell>,
139}
140
141fn read_u16(b: &[u8], at: usize) -> u16 {
142    u16::from_be_bytes([b[at], b[at + 1]])
143}
144
145impl<'a> Parse<'a> for MosaicDescriptor {
146    type Error = crate::error::Error;
147    fn parse(bytes: &'a [u8]) -> Result<Self> {
148        let body = descriptor_body(
149            bytes,
150            TAG,
151            "MosaicDescriptor",
152            "unexpected tag for mosaic_descriptor",
153        )?;
154        if body.len() < GRID_HEADER_LEN {
155            return Err(Error::InvalidDescriptor {
156                tag: TAG,
157                reason: "mosaic_descriptor missing grid header byte",
158            });
159        }
160        // grid header: reserved bit (bit 3) ignored on parse (§5.1).
161        let mosaic_entry_point = body[0] & ENTRY_POINT_MASK != 0;
162        let num_horizontal_cells = (body[0] >> 4) & 0x07;
163        let num_vertical_cells = body[0] & 0x07;
164
165        let mut logical_cells = Vec::new();
166        let mut pos = GRID_HEADER_LEN;
167        while pos < body.len() {
168            if pos + CELL_FIXED_LEN > body.len() {
169                return Err(Error::InvalidDescriptor {
170                    tag: TAG,
171                    reason: "truncated mosaic logical cell header",
172                });
173            }
174            // logical_cell_id(6) | reserved(7) | presentation_info(3)
175            let logical_cell_id = (body[pos] >> 2) & 0x3F;
176            let presentation_info = body[pos + 1] & 0x07;
177            let elem_field_len = body[pos + 2] as usize;
178            pos += CELL_FIXED_LEN;
179            if pos + elem_field_len > body.len() {
180                return Err(Error::InvalidDescriptor {
181                    tag: TAG,
182                    reason: "elementary_cell_field_length exceeds descriptor body",
183                });
184            }
185            let mut elementary_cell_ids = Vec::with_capacity(elem_field_len);
186            for &b in &body[pos..pos + elem_field_len] {
187                // reserved(2) ignored on parse; low 6 bits are the id.
188                elementary_cell_ids.push(b & ELEM_CELL_ID_MASK);
189            }
190            pos += elem_field_len;
191            if pos >= body.len() {
192                return Err(Error::InvalidDescriptor {
193                    tag: TAG,
194                    reason: "missing cell_linkage_info byte",
195                });
196            }
197            let info = body[pos];
198            pos += 1;
199            let linkage = match info {
200                0x00 => CellLinkage::Undefined,
201                0x01 => {
202                    if pos + 2 > body.len() {
203                        return Err(Error::InvalidDescriptor {
204                            tag: TAG,
205                            reason: "truncated bouquet linkage payload",
206                        });
207                    }
208                    let l = CellLinkage::Bouquet {
209                        bouquet_id: read_u16(body, pos),
210                    };
211                    pos += 2;
212                    l
213                }
214                0x02 | 0x03 => {
215                    if pos + 6 > body.len() {
216                        return Err(Error::InvalidDescriptor {
217                            tag: TAG,
218                            reason: "truncated service/mosaic linkage payload",
219                        });
220                    }
221                    let original_network_id = read_u16(body, pos);
222                    let transport_stream_id = read_u16(body, pos + 2);
223                    let service_id = read_u16(body, pos + 4);
224                    pos += 6;
225                    if info == 0x02 {
226                        CellLinkage::Service {
227                            original_network_id,
228                            transport_stream_id,
229                            service_id,
230                        }
231                    } else {
232                        CellLinkage::OtherMosaic {
233                            original_network_id,
234                            transport_stream_id,
235                            service_id,
236                        }
237                    }
238                }
239                0x04 => {
240                    if pos + 8 > body.len() {
241                        return Err(Error::InvalidDescriptor {
242                            tag: TAG,
243                            reason: "truncated event linkage payload",
244                        });
245                    }
246                    let l = CellLinkage::Event {
247                        original_network_id: read_u16(body, pos),
248                        transport_stream_id: read_u16(body, pos + 2),
249                        service_id: read_u16(body, pos + 4),
250                        event_id: read_u16(body, pos + 6),
251                    };
252                    pos += 8;
253                    l
254                }
255                // 0x05..=0xFF reserved: no defined payload (Table 75).
256                other => CellLinkage::Reserved { value: other },
257            };
258            logical_cells.push(MosaicLogicalCell {
259                logical_cell_id,
260                presentation_info,
261                elementary_cell_ids,
262                linkage,
263            });
264        }
265        Ok(Self {
266            mosaic_entry_point,
267            num_horizontal_cells,
268            num_vertical_cells,
269            logical_cells,
270        })
271    }
272}
273
274impl Serialize for MosaicDescriptor {
275    type Error = crate::error::Error;
276    fn serialized_len(&self) -> usize {
277        let cells: usize = self
278            .logical_cells
279            .iter()
280            .map(|c| CELL_FIXED_LEN + c.elementary_cell_ids.len() + 1 + c.linkage.payload_len())
281            .sum();
282        HEADER_LEN + GRID_HEADER_LEN + cells
283    }
284
285    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
286        let len = self.serialized_len();
287        if buf.len() < len {
288            return Err(Error::OutputBufferTooSmall {
289                need: len,
290                have: buf.len(),
291            });
292        }
293        let body_len = len - HEADER_LEN;
294        // 8-bit descriptor_length field: error rather than silently truncate.
295        if body_len > MAX_BODY_LEN {
296            return Err(Error::InvalidDescriptor {
297                tag: TAG,
298                reason: "mosaic_descriptor body exceeds 255 bytes",
299            });
300        }
301        buf[0] = TAG;
302        buf[1] = body_len as u8;
303        // grid header: reserved bit (bit 3) emitted as 1 (§5.1).
304        buf[2] = if self.mosaic_entry_point {
305            ENTRY_POINT_MASK
306        } else {
307            0
308        } | ((self.num_horizontal_cells & 0x07) << 4)
309            | 0x08
310            | (self.num_vertical_cells & 0x07);
311        let mut pos = HEADER_LEN + GRID_HEADER_LEN;
312        for cell in &self.logical_cells {
313            // 8-bit elementary_cell_field_length: error on over-range.
314            if cell.elementary_cell_ids.len() > MAX_ELEM_FIELD {
315                return Err(Error::InvalidDescriptor {
316                    tag: TAG,
317                    reason: "elementary_cell_field exceeds 255 entries (8-bit length field)",
318                });
319            }
320            // byte0: logical_cell_id(6) | top 2 reserved bits emitted 1s.
321            buf[pos] = ((cell.logical_cell_id & 0x3F) << 2) | 0x03;
322            // byte1: 5 reserved bits emitted 1s | presentation_info(3).
323            buf[pos + 1] = 0xF8 | (cell.presentation_info & 0x07);
324            buf[pos + 2] = cell.elementary_cell_ids.len() as u8;
325            pos += CELL_FIXED_LEN;
326            for &id in &cell.elementary_cell_ids {
327                // reserved(2) emitted 1s | elementary_cell_id(6).
328                buf[pos] = 0xC0 | (id & ELEM_CELL_ID_MASK);
329                pos += 1;
330            }
331            buf[pos] = cell.linkage.info_byte();
332            pos += 1;
333            match &cell.linkage {
334                CellLinkage::Undefined | CellLinkage::Reserved { .. } => {}
335                CellLinkage::Bouquet { bouquet_id } => {
336                    buf[pos..pos + 2].copy_from_slice(&bouquet_id.to_be_bytes());
337                    pos += 2;
338                }
339                CellLinkage::Service {
340                    original_network_id,
341                    transport_stream_id,
342                    service_id,
343                }
344                | CellLinkage::OtherMosaic {
345                    original_network_id,
346                    transport_stream_id,
347                    service_id,
348                } => {
349                    buf[pos..pos + 2].copy_from_slice(&original_network_id.to_be_bytes());
350                    buf[pos + 2..pos + 4].copy_from_slice(&transport_stream_id.to_be_bytes());
351                    buf[pos + 4..pos + 6].copy_from_slice(&service_id.to_be_bytes());
352                    pos += 6;
353                }
354                CellLinkage::Event {
355                    original_network_id,
356                    transport_stream_id,
357                    service_id,
358                    event_id,
359                } => {
360                    buf[pos..pos + 2].copy_from_slice(&original_network_id.to_be_bytes());
361                    buf[pos + 2..pos + 4].copy_from_slice(&transport_stream_id.to_be_bytes());
362                    buf[pos + 4..pos + 6].copy_from_slice(&service_id.to_be_bytes());
363                    buf[pos + 6..pos + 8].copy_from_slice(&event_id.to_be_bytes());
364                    pos += 8;
365                }
366            }
367        }
368        Ok(len)
369    }
370}
371impl<'a> crate::traits::DescriptorDef<'a> for MosaicDescriptor {
372    const TAG: u8 = TAG;
373    const NAME: &'static str = "MOSAIC";
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn parse_single_cell_undefined_linkage() {
382        // grid header 0x80|0x08 (entry point + reserved), 2x2 grid coded as 1,1
383        // logical cell: id=5, presentation=1, 2 elem cells [3,7], linkage 0x00
384        let bytes = [
385            TAG,
386            0x07,
387            0x80 | (1 << 4) | 0x08 | 1, // header
388            (5 << 2) | 0x03,            // logical_cell_id=5
389            0xF8 | 1,                   // presentation_info=1
390            0x02,                       // elementary_cell_field_length=2
391            0xC0 | 3,
392            0xC0 | 7,
393            0x00, // cell_linkage_info = undefined
394        ];
395        let d = MosaicDescriptor::parse(&bytes).unwrap();
396        assert!(d.mosaic_entry_point);
397        assert_eq!(d.num_horizontal_cells, 1);
398        assert_eq!(d.num_vertical_cells, 1);
399        assert_eq!(d.logical_cells.len(), 1);
400        let c = &d.logical_cells[0];
401        assert_eq!(c.logical_cell_id, 5);
402        assert_eq!(c.presentation_info, 1);
403        assert_eq!(c.elementary_cell_ids, vec![3, 7]);
404        assert_eq!(c.linkage, CellLinkage::Undefined);
405    }
406
407    #[test]
408    fn parse_service_linkage() {
409        let d = MosaicDescriptor {
410            mosaic_entry_point: false,
411            num_horizontal_cells: 0,
412            num_vertical_cells: 0,
413            logical_cells: vec![MosaicLogicalCell {
414                logical_cell_id: 1,
415                presentation_info: 2,
416                elementary_cell_ids: vec![0],
417                linkage: CellLinkage::Service {
418                    original_network_id: 0x1111,
419                    transport_stream_id: 0x2222,
420                    service_id: 0x3333,
421                },
422            }],
423        };
424        let mut buf = vec![0u8; d.serialized_len()];
425        d.serialize_into(&mut buf).unwrap();
426        let p = MosaicDescriptor::parse(&buf).unwrap();
427        assert_eq!(p, d);
428        assert!(matches!(
429            p.logical_cells[0].linkage,
430            CellLinkage::Service {
431                original_network_id: 0x1111,
432                transport_stream_id: 0x2222,
433                service_id: 0x3333,
434            }
435        ));
436    }
437
438    #[test]
439    fn parse_event_linkage_round_trip() {
440        let d = MosaicDescriptor {
441            mosaic_entry_point: true,
442            num_horizontal_cells: 3,
443            num_vertical_cells: 2,
444            logical_cells: vec![MosaicLogicalCell {
445                logical_cell_id: 10,
446                presentation_info: 3,
447                elementary_cell_ids: vec![1, 2, 3],
448                linkage: CellLinkage::Event {
449                    original_network_id: 0xAAAA,
450                    transport_stream_id: 0xBBBB,
451                    service_id: 0xCCCC,
452                    event_id: 0xDDDD,
453                },
454            }],
455        };
456        let mut buf = vec![0u8; d.serialized_len()];
457        d.serialize_into(&mut buf).unwrap();
458        assert_eq!(MosaicDescriptor::parse(&buf).unwrap(), d);
459    }
460
461    #[test]
462    fn parse_reserved_linkage_round_trips() {
463        let bytes = [
464            TAG,
465            0x05,
466            0x08, // grid header, no entry point, grid 0,0
467            (1 << 2) | 0x03,
468            0xF8,
469            0x00, // no elementary cells
470            0x42, // reserved linkage value
471        ];
472        let d = MosaicDescriptor::parse(&bytes).unwrap();
473        assert_eq!(
474            d.logical_cells[0].linkage,
475            CellLinkage::Reserved { value: 0x42 }
476        );
477        let mut buf = vec![0u8; d.serialized_len()];
478        d.serialize_into(&mut buf).unwrap();
479        assert_eq!(MosaicDescriptor::parse(&buf).unwrap(), d);
480    }
481
482    #[test]
483    fn parse_multiple_cells() {
484        let d = MosaicDescriptor {
485            mosaic_entry_point: false,
486            num_horizontal_cells: 1,
487            num_vertical_cells: 1,
488            logical_cells: vec![
489                MosaicLogicalCell {
490                    logical_cell_id: 0,
491                    presentation_info: 1,
492                    elementary_cell_ids: vec![0],
493                    linkage: CellLinkage::Bouquet { bouquet_id: 0x1234 },
494                },
495                MosaicLogicalCell {
496                    logical_cell_id: 1,
497                    presentation_info: 1,
498                    elementary_cell_ids: vec![1],
499                    linkage: CellLinkage::Undefined,
500                },
501            ],
502        };
503        let mut buf = vec![0u8; d.serialized_len()];
504        d.serialize_into(&mut buf).unwrap();
505        let p = MosaicDescriptor::parse(&buf).unwrap();
506        assert_eq!(p.logical_cells.len(), 2);
507        assert_eq!(p, d);
508    }
509
510    #[test]
511    fn parse_rejects_wrong_tag() {
512        assert!(matches!(
513            MosaicDescriptor::parse(&[0x52, 1, 0x08]).unwrap_err(),
514            Error::InvalidDescriptor { tag: 0x52, .. }
515        ));
516    }
517
518    #[test]
519    fn parse_rejects_short_buffer() {
520        // declares 5 body bytes, only 2 present
521        let bytes = [TAG, 5, 0x08, 0x00];
522        assert!(matches!(
523            MosaicDescriptor::parse(&bytes).unwrap_err(),
524            Error::BufferTooShort { .. }
525        ));
526    }
527
528    #[test]
529    fn parse_rejects_elem_field_overrun() {
530        // elementary_cell_field_length=10 but body has no room
531        let bytes = [TAG, 4, 0x08, (1 << 2) | 0x03, 0xF8, 0x0A];
532        assert!(matches!(
533            MosaicDescriptor::parse(&bytes).unwrap_err(),
534            Error::InvalidDescriptor { tag: TAG, .. }
535        ));
536    }
537
538    #[test]
539    fn parse_rejects_missing_linkage_byte() {
540        // cell header present, elem_field_len=0, but no cell_linkage_info byte
541        let bytes = [TAG, 4, 0x08, (1 << 2) | 0x03, 0xF8, 0x00];
542        // length=4 means body is [0x08, hdr0, hdr1, 0x00]; after the 3 fixed
543        // cell bytes pos == body.len() so the linkage byte is missing.
544        assert!(matches!(
545            MosaicDescriptor::parse(&bytes).unwrap_err(),
546            Error::InvalidDescriptor { tag: TAG, .. }
547        ));
548    }
549
550    #[test]
551    fn parse_rejects_zero_length() {
552        assert!(matches!(
553            MosaicDescriptor::parse(&[TAG, 0]).unwrap_err(),
554            Error::InvalidDescriptor { tag: TAG, .. }
555        ));
556    }
557
558    #[test]
559    fn serialize_emits_reserved_ones() {
560        let d = MosaicDescriptor {
561            mosaic_entry_point: false,
562            num_horizontal_cells: 0,
563            num_vertical_cells: 0,
564            logical_cells: vec![],
565        };
566        let mut buf = vec![0u8; d.serialized_len()];
567        d.serialize_into(&mut buf).unwrap();
568        // grid header reserved bit (bit 3) set
569        assert_eq!(buf[2] & 0x08, 0x08);
570    }
571
572    #[test]
573    fn serialize_round_trip_empty_grid() {
574        let d = MosaicDescriptor {
575            mosaic_entry_point: true,
576            num_horizontal_cells: 7,
577            num_vertical_cells: 7,
578            logical_cells: vec![],
579        };
580        let mut buf = vec![0u8; d.serialized_len()];
581        d.serialize_into(&mut buf).unwrap();
582        assert_eq!(MosaicDescriptor::parse(&buf).unwrap(), d);
583    }
584
585    #[cfg(feature = "serde")]
586    #[test]
587    fn serde_round_trip() {
588        let d = MosaicDescriptor {
589            mosaic_entry_point: true,
590            num_horizontal_cells: 1,
591            num_vertical_cells: 1,
592            logical_cells: vec![MosaicLogicalCell {
593                logical_cell_id: 5,
594                presentation_info: 1,
595                elementary_cell_ids: vec![3, 7],
596                linkage: CellLinkage::Bouquet { bouquet_id: 0x1234 },
597            }],
598        };
599        let json = serde_json::to_string(&d).unwrap();
600        // Serialize-only: assert the emitted JSON re-parses (serialize-stable).
601        let _v: serde_json::Value = serde_json::from_str(&json).unwrap();
602    }
603}