Skip to main content

dvb_si/descriptors/
cell_list.rs

1//! Cell List Descriptor — ETSI EN 300 468 §6.2.7 (tag 0x6C, Table 24, PDF p. 58).
2//!
3//! Carried inside the NIT. Describes the geographic cells of a terrestrial
4//! network and their sub-cells. Body layout (Table 24):
5//!
6//! ```text
7//! for (i=0;i<N;i++) {
8//!   cell_id                  16
9//!   cell_latitude            16
10//!   cell_longitude           16
11//!   cell_extent_of_latitude  12
12//!   cell_extent_of_longitude 12
13//!   subcell_info_loop_length  8
14//!   for (j=0;j<N;j++) {
15//!     cell_id_extension       8
16//!     subcell_latitude        16
17//!     subcell_longitude       16
18//!     subcell_extent_of_latitude  12
19//!     subcell_extent_of_longitude 12
20//!   }
21//! }
22//! ```
23//!
24//! latitude/longitude are 16-bit two's-complement fractions: 90°/2^15 for
25//! latitude, 180°/2^15 for longitude (§6.2.7). The 12-bit extents use the
26//! same scale.
27
28use super::descriptor_body;
29use crate::error::{Error, Result};
30use dvb_common::{Parse, Serialize};
31
32/// Descriptor tag for cell_list_descriptor.
33pub const TAG: u8 = 0x6C;
34/// Length of the header (tag byte + length byte).
35pub const HEADER_LEN: usize = 2;
36/// Bytes per outer entry before the subcell loop:
37/// cell_id(2)+lat(2)+long(2)+extents(3)+loop_len(1) = 10.
38pub const OUTER_FIXED_LEN: usize = 10;
39/// Bytes per subcell entry: ext(1)+lat(2)+long(2)+extents(3) = 8.
40pub const SUBCELL_LEN: usize = 8;
41/// Mask for a 12-bit extent value.
42pub const EXTENT_MASK: u16 = 0x0FFF;
43
44/// Scale factor: 90°/2^15 for latitude (§6.2.7).
45const LAT_SCALE: f64 = 90.0 / 32768.0;
46/// Scale factor: 180°/2^15 for longitude (§6.2.7).
47const LONG_SCALE: f64 = 180.0 / 32768.0;
48
49/// Interpret a 16-bit big-endian value as a signed i16 (two's-complement).
50fn u16_to_i16(raw: u16) -> i16 {
51    raw as i16
52}
53
54/// One sub-cell within a cell.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize))]
57pub struct CellListSubcell {
58    /// 8-bit cell_id_extension.
59    pub cell_id_extension: u8,
60    /// 16-bit subcell_latitude (raw wire value, two's-complement).
61    pub subcell_latitude: u16,
62    /// 16-bit subcell_longitude (raw wire value, two's-complement).
63    pub subcell_longitude: u16,
64    /// 12-bit subcell_extent_of_latitude.
65    pub subcell_extent_of_latitude: u16,
66    /// 12-bit subcell_extent_of_longitude.
67    pub subcell_extent_of_longitude: u16,
68}
69
70impl CellListSubcell {
71    /// Decode subcell_latitude to degrees (90°/2^15 scaling).
72    #[must_use]
73    pub fn subcell_latitude_deg(&self) -> f64 {
74        u16_to_i16(self.subcell_latitude) as f64 * LAT_SCALE
75    }
76
77    /// Decode subcell_longitude to degrees (180°/2^15 scaling).
78    #[must_use]
79    pub fn subcell_longitude_deg(&self) -> f64 {
80        u16_to_i16(self.subcell_longitude) as f64 * LONG_SCALE
81    }
82
83    /// Decode subcell_extent_of_latitude to degrees (90°/2^15 scaling).
84    #[must_use]
85    pub fn subcell_extent_of_latitude_deg(&self) -> f64 {
86        (self.subcell_extent_of_latitude & EXTENT_MASK) as f64 * LAT_SCALE
87    }
88
89    /// Decode subcell_extent_of_longitude to degrees (180°/2^15 scaling).
90    #[must_use]
91    pub fn subcell_extent_of_longitude_deg(&self) -> f64 {
92        (self.subcell_extent_of_longitude & EXTENT_MASK) as f64 * LONG_SCALE
93    }
94}
95
96/// One cell with its sub-cell list.
97#[derive(Debug, Clone, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize))]
99pub struct CellListEntry {
100    /// 16-bit cell_id.
101    pub cell_id: u16,
102    /// 16-bit cell_latitude (raw wire value, two's-complement).
103    pub cell_latitude: u16,
104    /// 16-bit cell_longitude (raw wire value, two's-complement).
105    pub cell_longitude: u16,
106    /// 12-bit cell_extent_of_latitude.
107    pub cell_extent_of_latitude: u16,
108    /// 12-bit cell_extent_of_longitude.
109    pub cell_extent_of_longitude: u16,
110    /// Sub-cells for this cell.
111    pub subcells: Vec<CellListSubcell>,
112}
113
114impl CellListEntry {
115    /// Decode cell_latitude to degrees (90°/2^15 scaling).
116    #[must_use]
117    pub fn cell_latitude_deg(&self) -> f64 {
118        u16_to_i16(self.cell_latitude) as f64 * LAT_SCALE
119    }
120
121    /// Decode cell_longitude to degrees (180°/2^15 scaling).
122    #[must_use]
123    pub fn cell_longitude_deg(&self) -> f64 {
124        u16_to_i16(self.cell_longitude) as f64 * LONG_SCALE
125    }
126
127    /// Decode cell_extent_of_latitude to degrees (90°/2^15 scaling).
128    #[must_use]
129    pub fn cell_extent_of_latitude_deg(&self) -> f64 {
130        (self.cell_extent_of_latitude & EXTENT_MASK) as f64 * LAT_SCALE
131    }
132
133    /// Decode cell_extent_of_longitude to degrees (180°/2^15 scaling).
134    #[must_use]
135    pub fn cell_extent_of_longitude_deg(&self) -> f64 {
136        (self.cell_extent_of_longitude & EXTENT_MASK) as f64 * LONG_SCALE
137    }
138}
139
140/// Cell List Descriptor.
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct CellListDescriptor {
144    /// Outer cell entries in wire order.
145    pub entries: Vec<CellListEntry>,
146}
147
148/// Decode the packed 24-bit extent pair `lat(12) || long(12)` from 3 bytes.
149fn read_extents(b: &[u8]) -> (u16, u16) {
150    let lat = (u16::from(b[0]) << 4) | (u16::from(b[1]) >> 4);
151    let long = ((u16::from(b[1]) & 0x0F) << 8) | u16::from(b[2]);
152    (lat, long)
153}
154
155/// Encode a packed 24-bit extent pair into 3 bytes.
156fn write_extents(buf: &mut [u8], lat: u16, long: u16) {
157    let lat = lat & EXTENT_MASK;
158    let long = long & EXTENT_MASK;
159    buf[0] = (lat >> 4) as u8;
160    buf[1] = (((lat & 0x0F) << 4) | (long >> 8)) as u8;
161    buf[2] = long as u8;
162}
163
164impl<'a> Parse<'a> for CellListDescriptor {
165    type Error = crate::error::Error;
166    fn parse(bytes: &'a [u8]) -> Result<Self> {
167        let body = descriptor_body(
168            bytes,
169            TAG,
170            "CellListDescriptor",
171            "unexpected tag for cell_list_descriptor",
172        )?;
173        let mut entries = Vec::new();
174        let mut pos = 0;
175        while pos < body.len() {
176            if pos + OUTER_FIXED_LEN > body.len() {
177                return Err(Error::InvalidDescriptor {
178                    tag: TAG,
179                    reason: "cell_list outer entry truncated",
180                });
181            }
182            let cell_id = u16::from_be_bytes([body[pos], body[pos + 1]]);
183            let cell_latitude = u16::from_be_bytes([body[pos + 2], body[pos + 3]]);
184            let cell_longitude = u16::from_be_bytes([body[pos + 4], body[pos + 5]]);
185            let (cell_extent_of_latitude, cell_extent_of_longitude) =
186                read_extents(&body[pos + 6..pos + 9]);
187            let subcell_info_loop_length = body[pos + 9] as usize;
188            pos += OUTER_FIXED_LEN;
189            if subcell_info_loop_length % SUBCELL_LEN != 0 {
190                return Err(Error::InvalidDescriptor {
191                    tag: TAG,
192                    reason: "subcell_info_loop_length must be a multiple of 8",
193                });
194            }
195            if pos + subcell_info_loop_length > body.len() {
196                return Err(Error::InvalidDescriptor {
197                    tag: TAG,
198                    reason: "subcell_info_loop_length exceeds descriptor body",
199                });
200            }
201            let subcell_count = subcell_info_loop_length / SUBCELL_LEN;
202            let mut subcells = Vec::with_capacity(subcell_count);
203            for _ in 0..subcell_count {
204                let cell_id_extension = body[pos];
205                let subcell_latitude = u16::from_be_bytes([body[pos + 1], body[pos + 2]]);
206                let subcell_longitude = u16::from_be_bytes([body[pos + 3], body[pos + 4]]);
207                let (subcell_extent_of_latitude, subcell_extent_of_longitude) =
208                    read_extents(&body[pos + 5..pos + 8]);
209                subcells.push(CellListSubcell {
210                    cell_id_extension,
211                    subcell_latitude,
212                    subcell_longitude,
213                    subcell_extent_of_latitude,
214                    subcell_extent_of_longitude,
215                });
216                pos += SUBCELL_LEN;
217            }
218            entries.push(CellListEntry {
219                cell_id,
220                cell_latitude,
221                cell_longitude,
222                cell_extent_of_latitude,
223                cell_extent_of_longitude,
224                subcells,
225            });
226        }
227        Ok(Self { entries })
228    }
229}
230
231impl CellListDescriptor {
232    fn body_len(&self) -> usize {
233        self.entries
234            .iter()
235            .map(|e| OUTER_FIXED_LEN + e.subcells.len() * SUBCELL_LEN)
236            .sum()
237    }
238}
239
240impl Serialize for CellListDescriptor {
241    type Error = crate::error::Error;
242    fn serialized_len(&self) -> usize {
243        HEADER_LEN + self.body_len()
244    }
245
246    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
247        let body_len = self.body_len();
248        if body_len > u8::MAX as usize {
249            return Err(Error::InvalidDescriptor {
250                tag: TAG,
251                reason: "cell_list_descriptor body exceeds 255 bytes",
252            });
253        }
254        for e in &self.entries {
255            if e.subcells.len() * SUBCELL_LEN > u8::MAX as usize {
256                return Err(Error::InvalidDescriptor {
257                    tag: TAG,
258                    reason: "subcell_info_loop_length exceeds 255 bytes",
259                });
260            }
261        }
262        let len = self.serialized_len();
263        if buf.len() < len {
264            return Err(Error::OutputBufferTooSmall {
265                need: len,
266                have: buf.len(),
267            });
268        }
269        buf[0] = TAG;
270        buf[1] = body_len as u8;
271        let mut pos = HEADER_LEN;
272        for e in &self.entries {
273            buf[pos..pos + 2].copy_from_slice(&e.cell_id.to_be_bytes());
274            buf[pos + 2..pos + 4].copy_from_slice(&e.cell_latitude.to_be_bytes());
275            buf[pos + 4..pos + 6].copy_from_slice(&e.cell_longitude.to_be_bytes());
276            write_extents(
277                &mut buf[pos + 6..pos + 9],
278                e.cell_extent_of_latitude,
279                e.cell_extent_of_longitude,
280            );
281            buf[pos + 9] = (e.subcells.len() * SUBCELL_LEN) as u8;
282            pos += OUTER_FIXED_LEN;
283            for sc in &e.subcells {
284                buf[pos] = sc.cell_id_extension;
285                buf[pos + 1..pos + 3].copy_from_slice(&sc.subcell_latitude.to_be_bytes());
286                buf[pos + 3..pos + 5].copy_from_slice(&sc.subcell_longitude.to_be_bytes());
287                write_extents(
288                    &mut buf[pos + 5..pos + 8],
289                    sc.subcell_extent_of_latitude,
290                    sc.subcell_extent_of_longitude,
291                );
292                pos += SUBCELL_LEN;
293            }
294        }
295        Ok(len)
296    }
297}
298impl<'a> crate::traits::DescriptorDef<'a> for CellListDescriptor {
299    const TAG: u8 = TAG;
300    const NAME: &'static str = "CELL_LIST";
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn extents_pack_round_trips() {
309        let mut b = [0u8; 3];
310        write_extents(&mut b, 0xABC, 0xDEF);
311        assert_eq!(read_extents(&b), (0xABC, 0xDEF));
312    }
313
314    #[test]
315    fn cell_latitude_deg_zero() {
316        let e = CellListEntry {
317            cell_id: 0,
318            cell_latitude: 0,
319            cell_longitude: 0,
320            cell_extent_of_latitude: 0,
321            cell_extent_of_longitude: 0,
322            subcells: vec![],
323        };
324        assert_eq!(e.cell_latitude_deg(), 0.0);
325        assert_eq!(e.cell_longitude_deg(), 0.0);
326    }
327
328    #[test]
329    fn cell_latitude_deg_max() {
330        // Max positive = 32767/32768 * 90 ≈ 89.997
331        let e = CellListEntry {
332            cell_id: 0,
333            cell_latitude: 0x7FFF,
334            cell_longitude: 0,
335            cell_extent_of_latitude: 0,
336            cell_extent_of_longitude: 0,
337            subcells: vec![],
338        };
339        let deg = e.cell_latitude_deg();
340        assert!(deg > 89.9 && deg <= 90.0, "got {deg}");
341    }
342
343    #[test]
344    fn cell_longitude_deg_max() {
345        let e = CellListEntry {
346            cell_id: 0,
347            cell_latitude: 0,
348            cell_longitude: 0x7FFF,
349            cell_extent_of_latitude: 0,
350            cell_extent_of_longitude: 0,
351            subcells: vec![],
352        };
353        let deg = e.cell_longitude_deg();
354        assert!(deg > 179.9 && deg <= 180.0, "got {deg}");
355    }
356
357    #[test]
358    fn cell_extent_deg() {
359        // 4095/32768 * 90 ≈ 11.25 degrees
360        let e = CellListEntry {
361            cell_id: 0,
362            cell_latitude: 0,
363            cell_longitude: 0,
364            cell_extent_of_latitude: 4095,
365            cell_extent_of_longitude: 2048,
366            subcells: vec![],
367        };
368        assert!(e.cell_extent_of_latitude_deg() > 11.2);
369        assert!(e.cell_extent_of_longitude_deg() > 11.2);
370    }
371
372    #[test]
373    fn parse_entry_with_subcell() {
374        let bytes = [
375            TAG, 18, // body length: outer(10) + 1 subcell(8)
376            // outer: cell_id=0x1234, lat=0x5678, long=0x9ABC, extent_lat=0xDEF, extent_long=0x012, loop_len=8
377            0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 8,
378            // subcell: ext=0x07, lat=0x1111, long=0x2222, extent_lat=0x333, extent_long=0x444
379            0x07, 0x11, 0x11, 0x22, 0x22, 0x33, 0x34, 0x44,
380        ];
381        let d = CellListDescriptor::parse(&bytes).unwrap();
382        assert_eq!(d.entries.len(), 1);
383        let e = &d.entries[0];
384        assert_eq!(e.cell_id, 0x1234);
385        assert_eq!(e.cell_latitude, 0x5678);
386        assert_eq!(e.cell_longitude, 0x9ABC);
387        assert_eq!(e.cell_extent_of_latitude, 0xDEF);
388        assert_eq!(e.cell_extent_of_longitude, 0x012);
389        assert_eq!(e.subcells.len(), 1);
390        let sc = &e.subcells[0];
391        assert_eq!(sc.cell_id_extension, 0x07);
392        assert_eq!(sc.subcell_latitude, 0x1111);
393        assert_eq!(sc.subcell_longitude, 0x2222);
394        assert_eq!(sc.subcell_extent_of_latitude, 0x333);
395        assert_eq!(sc.subcell_extent_of_longitude, 0x444);
396
397        // Accessor tests
398        let lat_deg = e.cell_latitude_deg();
399        let long_deg = e.cell_longitude_deg();
400        assert!(lat_deg.abs() > 0.0);
401        assert!(long_deg.abs() > 0.0);
402        let sc_lat = sc.subcell_latitude_deg();
403        let sc_long = sc.subcell_longitude_deg();
404        assert!(sc_lat.abs() > 0.0);
405        assert!(sc_long.abs() > 0.0);
406    }
407
408    #[test]
409    fn parse_entry_no_subcells() {
410        let bytes = [
411            TAG, 10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
412        ];
413        let d = CellListDescriptor::parse(&bytes).unwrap();
414        assert_eq!(d.entries.len(), 1);
415        assert!(d.entries[0].subcells.is_empty());
416    }
417
418    #[test]
419    fn empty_body_is_valid() {
420        let d = CellListDescriptor::parse(&[TAG, 0]).unwrap();
421        assert!(d.entries.is_empty());
422    }
423
424    #[test]
425    fn parse_rejects_wrong_tag() {
426        assert!(matches!(
427            CellListDescriptor::parse(&[0x6D, 0]).unwrap_err(),
428            Error::InvalidDescriptor { tag: 0x6D, .. }
429        ));
430    }
431
432    #[test]
433    fn parse_rejects_truncated_outer() {
434        let bytes = [TAG, 5, 0, 0, 0, 0, 0];
435        assert!(matches!(
436            CellListDescriptor::parse(&bytes).unwrap_err(),
437            Error::InvalidDescriptor { tag: TAG, .. }
438        ));
439    }
440
441    #[test]
442    fn parse_rejects_subcell_loop_overrun() {
443        // loop_len=8 but no subcell bytes follow
444        let bytes = [
445            TAG, 10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 8,
446        ];
447        assert!(matches!(
448            CellListDescriptor::parse(&bytes).unwrap_err(),
449            Error::InvalidDescriptor { tag: TAG, .. }
450        ));
451    }
452
453    #[test]
454    fn parse_rejects_buffer_shorter_than_length() {
455        let bytes = [TAG, 10, 0x00, 0x01, 0x00];
456        assert!(matches!(
457            CellListDescriptor::parse(&bytes).unwrap_err(),
458            Error::BufferTooShort { .. }
459        ));
460    }
461
462    #[test]
463    fn serialize_round_trip() {
464        let d = CellListDescriptor {
465            entries: vec![
466                CellListEntry {
467                    cell_id: 0x1234,
468                    cell_latitude: 0x5678,
469                    cell_longitude: 0x9ABC,
470                    cell_extent_of_latitude: 0xDEF,
471                    cell_extent_of_longitude: 0x012,
472                    subcells: vec![CellListSubcell {
473                        cell_id_extension: 0x07,
474                        subcell_latitude: 0x1111,
475                        subcell_longitude: 0x2222,
476                        subcell_extent_of_latitude: 0x333,
477                        subcell_extent_of_longitude: 0x444,
478                    }],
479                },
480                CellListEntry {
481                    cell_id: 0xAAAA,
482                    cell_latitude: 0xBBBB,
483                    cell_longitude: 0xCCCC,
484                    cell_extent_of_latitude: 0x111,
485                    cell_extent_of_longitude: 0x222,
486                    subcells: vec![],
487                },
488            ],
489        };
490        let mut buf = vec![0u8; d.serialized_len()];
491        d.serialize_into(&mut buf).unwrap();
492        assert_eq!(CellListDescriptor::parse(&buf).unwrap(), d);
493    }
494
495    #[test]
496    fn serialize_rejects_too_small_buffer() {
497        let d = CellListDescriptor {
498            entries: vec![CellListEntry {
499                cell_id: 0,
500                cell_latitude: 0,
501                cell_longitude: 0,
502                cell_extent_of_latitude: 0,
503                cell_extent_of_longitude: 0,
504                subcells: vec![],
505            }],
506        };
507        let mut buf = vec![0u8; 3];
508        assert!(matches!(
509            d.serialize_into(&mut buf).unwrap_err(),
510            Error::OutputBufferTooSmall { .. }
511        ));
512    }
513
514    #[test]
515    fn serialize_rejects_over_range_body() {
516        // 26 empty entries × 10 = 260 > 255.
517        let d = CellListDescriptor {
518            entries: (0..26)
519                .map(|_| CellListEntry {
520                    cell_id: 0,
521                    cell_latitude: 0,
522                    cell_longitude: 0,
523                    cell_extent_of_latitude: 0,
524                    cell_extent_of_longitude: 0,
525                    subcells: vec![],
526                })
527                .collect(),
528        };
529        let mut buf = vec![0u8; d.serialized_len()];
530        assert!(matches!(
531            d.serialize_into(&mut buf).unwrap_err(),
532            Error::InvalidDescriptor { tag: TAG, .. }
533        ));
534    }
535
536    #[cfg(feature = "serde")]
537    #[test]
538    fn serde_round_trip() {
539        let d = CellListDescriptor {
540            entries: vec![CellListEntry {
541                cell_id: 0x1234,
542                cell_latitude: 0x5678,
543                cell_longitude: 0x9ABC,
544                cell_extent_of_latitude: 0xDEF,
545                cell_extent_of_longitude: 0x012,
546                subcells: vec![CellListSubcell {
547                    cell_id_extension: 0x07,
548                    subcell_latitude: 0x1111,
549                    subcell_longitude: 0x2222,
550                    subcell_extent_of_latitude: 0x333,
551                    subcell_extent_of_longitude: 0x444,
552                }],
553            }],
554        };
555        let json = serde_json::to_string(&d).unwrap();
556        // Serialize-only: assert the emitted JSON re-parses (serialize-stable).
557        let _v: serde_json::Value = serde_json::from_str(&json).unwrap();
558    }
559}