Skip to main content

dvb_si/descriptors/extension/
network_change_notify.rs

1//! Network Change Notify Descriptor — ETSI EN 300 468 §6.4.9 (tag_extension 0x07).
2use super::*;
3use alloc::vec::Vec;
4
5/// Change type — ETSI EN 300 468 §6.4.10 Table 151
6/// (`docs/en_300_468.md`, Table 151 — Change type coding).
7///
8/// 4-bit field. Classifies the nature of the network change event.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize))]
11#[non_exhaustive]
12pub enum ChangeType {
13    /// `0` — message only.
14    MessageOnly,
15    /// `1` — minor - default.
16    MinorDefault,
17    /// `2` — minor - multiplex removed.
18    MinorMultiplexRemoved,
19    /// `3` — minor - service changed.
20    MinorServiceChanged,
21    /// `4`–`7` — reserved for future use for other minor changes.
22    MinorReserved(u8),
23    /// `8` — major - default.
24    MajorDefault,
25    /// `9` — major - multiplex frequency changed.
26    MajorFrequencyChanged,
27    /// `10` — major - multiplex coverage changed.
28    MajorCoverageChanged,
29    /// `11` — major - multiplex added.
30    MajorMultiplexAdded,
31    /// `12`–`15` — reserved for future use for other major changes.
32    MajorReserved(u8),
33}
34
35impl ChangeType {
36    #[must_use]
37    /// Creates a value from a 4-bit wire nibble (upper bits masked off),
38    /// preserving every possible value for lossless round-trip.
39    pub fn from_u8(v: u8) -> Self {
40        match v & 0x0F {
41            0 => Self::MessageOnly,
42            1 => Self::MinorDefault,
43            2 => Self::MinorMultiplexRemoved,
44            3 => Self::MinorServiceChanged,
45            v @ 4..=7 => Self::MinorReserved(v),
46            8 => Self::MajorDefault,
47            9 => Self::MajorFrequencyChanged,
48            10 => Self::MajorCoverageChanged,
49            11 => Self::MajorMultiplexAdded,
50            v => Self::MajorReserved(v),
51        }
52    }
53
54    #[must_use]
55    /// Returns the 4-bit wire nibble for this value.
56    pub const fn to_u8(self) -> u8 {
57        match self {
58            Self::MessageOnly => 0,
59            Self::MinorDefault => 1,
60            Self::MinorMultiplexRemoved => 2,
61            Self::MinorServiceChanged => 3,
62            Self::MinorReserved(v) | Self::MajorReserved(v) => v,
63            Self::MajorDefault => 8,
64            Self::MajorFrequencyChanged => 9,
65            Self::MajorCoverageChanged => 10,
66            Self::MajorMultiplexAdded => 11,
67        }
68    }
69
70    #[must_use]
71    /// Returns the spec token for this value.
72    pub fn name(self) -> &'static str {
73        match self {
74            Self::MessageOnly => "message only",
75            Self::MinorDefault => "minor - default",
76            Self::MinorMultiplexRemoved => "minor - multiplex removed",
77            Self::MinorServiceChanged => "minor - service changed",
78            Self::MinorReserved(_) => "reserved (minor)",
79            Self::MajorDefault => "major - default",
80            Self::MajorFrequencyChanged => "major - multiplex frequency changed",
81            Self::MajorCoverageChanged => "major - multiplex coverage changed",
82            Self::MajorMultiplexAdded => "major - multiplex added",
83            Self::MajorReserved(_) => "reserved (major)",
84        }
85    }
86}
87dvb_common::impl_spec_display!(ChangeType, MinorReserved, MajorReserved);
88
89const CELL_HEADER_LEN: usize = 2; // cell_id(16)
90const LOOP_LENGTH_LEN: usize = 1; // loop_length(8)
91const CHANGE_BASE_LEN: usize = 12; // id(1)+ver(1)+start(5)+dur(3)+packed(1)+msg(1)
92const INVARIANT_TS_LEN: usize = 4; // tsid(16)+onid(16)
93
94/// network_change_notify body (Table 149, §6.4.9). The two-level cell/change loop is unfolded.
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize))]
97pub struct NetworkChangeNotify {
98    /// Per-cell change lists.
99    pub cells: Vec<NetworkChangeCell>,
100}
101
102/// A cell in the network_change_notify outer loop.
103#[derive(Debug, Clone, PartialEq, Eq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize))]
105pub struct NetworkChangeCell {
106    /// cell_id(16).
107    pub cell_id: u16,
108    /// The cell's change entries.
109    pub changes: Vec<NetworkChange>,
110}
111
112/// A change entry in the network_change_notify inner loop.
113#[derive(Debug, Clone, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize))]
115pub struct NetworkChange {
116    /// network_change_id(8).
117    pub network_change_id: u8,
118    /// network_change_version(8).
119    pub network_change_version: u8,
120    /// start_time_of_change(40) — raw 40-bit value (MJD date + UTC BCD time), big-endian.
121    pub start_time_of_change: u64,
122    /// change_duration(24) — raw 24-bit BCD value, big-endian.
123    pub change_duration: u32,
124    /// receiver_category(3).
125    pub receiver_category: u8,
126    /// change_type(4) — [`ChangeType`].
127    pub change_type: ChangeType,
128    /// message_id(8).
129    pub message_id: u8,
130    /// invariant_ts (tsid, onid), present iff invariant_ts_present==1.
131    pub invariant_ts: Option<InvariantTs>,
132}
133
134/// Conditional invariant-TS fields in a network_change_notify entry.
135#[derive(Debug, Clone, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize))]
137pub struct InvariantTs {
138    /// invariant_ts_tsid(16).
139    pub tsid: u16,
140    /// invariant_ts_onid(16).
141    pub onid: u16,
142}
143
144impl<'a> ExtensionBodyDef<'a> for NetworkChangeNotify {
145    const TAG_EXTENSION: u8 = 0x07;
146    const NAME: &'static str = "NETWORK_CHANGE_NOTIFY";
147}
148
149impl NetworkChange {
150    /// Decode `start_time_of_change` (40-bit MJD+BCD UTC) to a
151    /// [`chrono::DateTime<chrono::Utc>`]. `None` if the date/time fields
152    /// are out of range. Reuses the same helper as EIT/TOT/TDT.
153    #[cfg(feature = "chrono")]
154    #[must_use]
155    pub fn start_time_of_change_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
156        let raw = self.start_time_of_change;
157        let bytes = [
158            (raw >> 32) as u8,
159            (raw >> 24) as u8,
160            (raw >> 16) as u8,
161            (raw >> 8) as u8,
162            raw as u8,
163        ];
164        dvb_common::time::decode_mjd_bcd_utc(bytes)
165    }
166
167    /// Decode `change_duration` (24-bit BCD `HHMMSS`) to seconds.
168    /// `None` if the BCD nibbles are out of range.
169    #[cfg(feature = "chrono")]
170    #[must_use]
171    pub fn change_duration_secs(&self) -> Option<u32> {
172        let raw = self.change_duration;
173        let bytes = [(raw >> 16) as u8, (raw >> 8) as u8, raw as u8];
174        dvb_common::time::decode_bcd_duration(bytes).map(|d| d.as_secs() as u32)
175    }
176}
177
178fn change_serialized_len(ch: &NetworkChange) -> usize {
179    CHANGE_BASE_LEN
180        + if ch.invariant_ts.is_some() {
181            INVARIANT_TS_LEN
182        } else {
183            0
184        }
185}
186
187impl<'a> Parse<'a> for NetworkChangeNotify {
188    type Error = crate::error::Error;
189    fn parse(sel: &'a [u8]) -> Result<Self> {
190        let mut cells = Vec::new();
191        let mut pos = 0;
192        while pos < sel.len() {
193            if pos + CELL_HEADER_LEN + LOOP_LENGTH_LEN > sel.len() {
194                return Err(Error::BufferTooShort {
195                    need: pos + CELL_HEADER_LEN + LOOP_LENGTH_LEN,
196                    have: sel.len(),
197                    what: "network_change_notify body",
198                });
199            }
200            let (cell_id_bytes, _) = sel
201                .get(pos..)
202                .and_then(|s| s.split_first_chunk::<2>())
203                .ok_or(Error::BufferTooShort {
204                    need: pos + CELL_HEADER_LEN + LOOP_LENGTH_LEN,
205                    have: sel.len(),
206                    what: "network_change_notify body",
207                })?;
208            let cell_id = u16::from_be_bytes(*cell_id_bytes);
209            let loop_length = sel[pos + CELL_HEADER_LEN] as usize;
210            pos += CELL_HEADER_LEN + LOOP_LENGTH_LEN;
211
212            if pos + loop_length > sel.len() {
213                return Err(Error::BufferTooShort {
214                    need: pos + loop_length,
215                    have: sel.len(),
216                    what: "network_change_notify body",
217                });
218            }
219
220            let inner_end = pos + loop_length;
221            let mut changes = Vec::new();
222            while pos < inner_end {
223                let remaining = inner_end - pos;
224                // At minimum we need CHANGE_BASE_LEN bytes for a basic entry.
225                if remaining < CHANGE_BASE_LEN {
226                    return Err(Error::BufferTooShort {
227                        need: inner_end - remaining + CHANGE_BASE_LEN,
228                        have: sel.len(),
229                        what: "network_change_notify body",
230                    });
231                }
232                let network_change_id = sel[pos];
233                let network_change_version = sel[pos + 1];
234                let start_time_of_change = (u64::from(sel[pos + 2]) << 32)
235                    | (u64::from(sel[pos + 3]) << 24)
236                    | (u64::from(sel[pos + 4]) << 16)
237                    | (u64::from(sel[pos + 5]) << 8)
238                    | u64::from(sel[pos + 6]);
239                let change_duration = (u32::from(sel[pos + 7]) << 16)
240                    | (u32::from(sel[pos + 8]) << 8)
241                    | u32::from(sel[pos + 9]);
242                let packed = sel[pos + 10];
243                let receiver_category = packed >> 5;
244                let invariant_ts_present = (packed >> 4) & 1;
245                let change_type = ChangeType::from_u8(packed & 0x0F);
246                let message_id = sel[pos + 11];
247                pos += CHANGE_BASE_LEN;
248
249                let invariant_ts = if invariant_ts_present == 1 {
250                    if pos + INVARIANT_TS_LEN > inner_end {
251                        return Err(Error::BufferTooShort {
252                            need: pos + INVARIANT_TS_LEN,
253                            have: sel.len(),
254                            what: "network_change_notify body",
255                        });
256                    }
257                    let (inv_bytes, _) = sel
258                        .get(pos..)
259                        .and_then(|s| s.split_first_chunk::<INVARIANT_TS_LEN>())
260                        .ok_or(Error::BufferTooShort {
261                            need: pos + INVARIANT_TS_LEN,
262                            have: sel.len(),
263                            what: "network_change_notify body",
264                        })?;
265                    let ts = InvariantTs {
266                        tsid: u16::from_be_bytes([inv_bytes[0], inv_bytes[1]]),
267                        onid: u16::from_be_bytes([inv_bytes[2], inv_bytes[3]]),
268                    };
269                    pos += INVARIANT_TS_LEN;
270                    Some(ts)
271                } else {
272                    None
273                };
274
275                changes.push(NetworkChange {
276                    network_change_id,
277                    network_change_version,
278                    start_time_of_change,
279                    change_duration,
280                    receiver_category,
281                    change_type,
282                    message_id,
283                    invariant_ts,
284                });
285            }
286
287            if pos != inner_end {
288                return Err(invalid("network_change_notify: change entry overruns loop"));
289            }
290
291            cells.push(NetworkChangeCell { cell_id, changes });
292        }
293        Ok(NetworkChangeNotify { cells })
294    }
295}
296
297impl Serialize for NetworkChangeNotify {
298    type Error = crate::error::Error;
299    fn serialized_len(&self) -> usize {
300        self.cells
301            .iter()
302            .map(|cell| {
303                CELL_HEADER_LEN
304                    + LOOP_LENGTH_LEN
305                    + cell
306                        .changes
307                        .iter()
308                        .map(change_serialized_len)
309                        .sum::<usize>()
310            })
311            .sum()
312    }
313    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
314        let len = self.serialized_len();
315        if buf.len() < len {
316            return Err(Error::OutputBufferTooSmall {
317                need: len,
318                have: buf.len(),
319            });
320        }
321        let mut pos = 0;
322        for cell in &self.cells {
323            buf[pos..pos + 2].copy_from_slice(&cell.cell_id.to_be_bytes());
324            pos += 2;
325            let loop_length: usize = cell.changes.iter().map(change_serialized_len).sum();
326            buf[pos] = loop_length as u8;
327            pos += 1;
328            for ch in &cell.changes {
329                buf[pos] = ch.network_change_id;
330                buf[pos + 1] = ch.network_change_version;
331                let st = ch.start_time_of_change;
332                buf[pos + 2] = (st >> 32) as u8;
333                buf[pos + 3] = (st >> 24) as u8;
334                buf[pos + 4] = (st >> 16) as u8;
335                buf[pos + 5] = (st >> 8) as u8;
336                buf[pos + 6] = st as u8;
337                let dur = ch.change_duration;
338                buf[pos + 7] = (dur >> 16) as u8;
339                buf[pos + 8] = (dur >> 8) as u8;
340                buf[pos + 9] = dur as u8;
341                let packed = ((ch.receiver_category & 0x07) << 5)
342                    | ((ch.invariant_ts.is_some() as u8) << 4)
343                    | (ch.change_type.to_u8() & 0x0F);
344                buf[pos + 10] = packed;
345                buf[pos + 11] = ch.message_id;
346                pos += CHANGE_BASE_LEN;
347                if let Some(ref inv) = ch.invariant_ts {
348                    buf[pos..pos + 2].copy_from_slice(&inv.tsid.to_be_bytes());
349                    buf[pos + 2..pos + 4].copy_from_slice(&inv.onid.to_be_bytes());
350                    pos += INVARIANT_TS_LEN;
351                }
352            }
353        }
354        Ok(len)
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::descriptors::extension::test_support::*;
362    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
363
364    #[test]
365    fn parse_network_change_notify_structured() {
366        // 2 cells: one with 0 changes, one with 2 changes (one without
367        // invariant_ts, one with).
368        let sel = [
369            // cell 1: cell_id=0x0001, loop_length=0
370            0x00, 0x01, 0x00, // cell 2: cell_id=0x0002, loop_length=28 (0x1C)
371            0x00, 0x02, 0x1C,
372            //   change 1 (no invariant_ts): 12 bytes
373            0x10, // network_change_id
374            0x20, // network_change_version
375            0x00, 0x00, 0x00, 0x00, 0x01, // start_time_of_change = 1
376            0x00, 0x00, 0x01, // change_duration = 1
377            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
378            0x40, // message_id
379            //   change 2 (with invariant_ts): 16 bytes
380            0x30, // network_change_id
381            0x40, // network_change_version
382            0x00, 0x00, 0x00, 0x00, 0x02, // start_time_of_change = 2
383            0x00, 0x00, 0x02, // change_duration = 2
384            0x54, // packed: rec_cat=2, inv_ts=1, change_type=4
385            0x50, // message_id
386            0xAA, 0xAA, // tsid = 0xAAAA
387            0xBB, 0xBB, // onid = 0xBBBB
388        ];
389        let bytes = wrap(0x07, &sel);
390        let d = ExtensionDescriptor::parse(&bytes).unwrap();
391        match &d.body {
392            ExtensionBody::NetworkChangeNotify(b) => {
393                assert_eq!(b.cells.len(), 2);
394
395                assert_eq!(b.cells[0].cell_id, 0x0001);
396                assert!(b.cells[0].changes.is_empty());
397
398                assert_eq!(b.cells[1].cell_id, 0x0002);
399                assert_eq!(b.cells[1].changes.len(), 2);
400
401                let ch0 = &b.cells[1].changes[0];
402                assert_eq!(ch0.network_change_id, 0x10);
403                assert_eq!(ch0.network_change_version, 0x20);
404                assert_eq!(ch0.start_time_of_change, 1);
405                assert_eq!(ch0.change_duration, 1);
406                assert_eq!(ch0.receiver_category, 1);
407                assert_eq!(ch0.change_type, ChangeType::MinorServiceChanged);
408                assert_eq!(ch0.message_id, 0x40);
409                assert!(ch0.invariant_ts.is_none());
410
411                let ch1 = &b.cells[1].changes[1];
412                assert_eq!(ch1.network_change_id, 0x30);
413                assert_eq!(ch1.network_change_version, 0x40);
414                assert_eq!(ch1.start_time_of_change, 2);
415                assert_eq!(ch1.change_duration, 2);
416                assert_eq!(ch1.receiver_category, 2);
417                assert_eq!(ch1.change_type, ChangeType::MinorReserved(4));
418                assert_eq!(ch1.message_id, 0x50);
419                let inv = ch1.invariant_ts.as_ref().unwrap();
420                assert_eq!(inv.tsid, 0xAAAA);
421                assert_eq!(inv.onid, 0xBBBB);
422            }
423            other => panic!("expected NetworkChangeNotify, got {other:?}"),
424        }
425        round_trip(&d);
426    }
427
428    /// Byte-exact cross-check against a TSDuck-compiled descriptor (tsduck-test
429    /// test-015). Parse, assert decoded fields, assert kind(), then re-serialize
430    /// and verify byte-exact match.
431    #[test]
432    fn network_change_notify_tsduck_byte_exact() {
433        let bytes =
434            from_hex("7f230712340056781cabcde5cc2312340852030281ef67e5e20234561132453b83deadbeef");
435        let d = ExtensionDescriptor::parse(&bytes).unwrap();
436        assert_eq!(d.kind(), Some(ExtensionTag::NetworkChangeNotify));
437
438        match &d.body {
439            ExtensionBody::NetworkChangeNotify(b) => {
440                assert_eq!(b.cells.len(), 2);
441
442                // cell 0: cell_id=0x1234, no changes
443                assert_eq!(b.cells[0].cell_id, 0x1234);
444                assert!(b.cells[0].changes.is_empty());
445
446                // cell 1: cell_id=0x5678, 2 changes
447                assert_eq!(b.cells[1].cell_id, 0x5678);
448                assert_eq!(b.cells[1].changes.len(), 2);
449
450                // change 0: no invariant_ts
451                let ch0 = &b.cells[1].changes[0];
452                assert_eq!(ch0.network_change_id, 0xAB);
453                assert_eq!(ch0.network_change_version, 0xCD);
454                assert_eq!(ch0.start_time_of_change, 0xE5CC231234);
455                assert_eq!(ch0.change_duration, 0x085203);
456                assert_eq!(ch0.receiver_category, 0);
457                assert_eq!(ch0.change_type, ChangeType::MinorMultiplexRemoved);
458                assert_eq!(ch0.message_id, 0x81);
459                assert!(ch0.invariant_ts.is_none());
460
461                // change 1: with invariant_ts
462                let ch1 = &b.cells[1].changes[1];
463                assert_eq!(ch1.network_change_id, 0xEF);
464                assert_eq!(ch1.network_change_version, 0x67);
465                assert_eq!(ch1.start_time_of_change, 0xE5E2023456);
466                assert_eq!(ch1.change_duration, 0x113245);
467                assert_eq!(ch1.receiver_category, 1);
468                assert_eq!(ch1.change_type, ChangeType::MajorMultiplexAdded);
469                assert_eq!(ch1.message_id, 0x83);
470                let inv = ch1.invariant_ts.as_ref().unwrap();
471                assert_eq!(inv.tsid, 0xDEAD);
472                assert_eq!(inv.onid, 0xBEEF);
473            }
474            other => panic!("expected NetworkChangeNotify, got {other:?}"),
475        }
476
477        let mut out = vec![0u8; d.serialized_len()];
478        let n = d.serialize_into(&mut out).unwrap();
479        assert_eq!(
480            out[..n],
481            bytes[..],
482            "byte-exact re-serialize for TSDuck vector"
483        );
484    }
485
486    #[cfg(feature = "chrono")]
487    #[test]
488    fn chrono_accessors_decode_start_time_and_duration() {
489        use chrono::{Datelike, Timelike};
490        // One cell with one change entry, no invariant_ts.
491        // cell_id=0x0001, loop_length=12, then one 12-byte change entry.
492        // MJD 0xE409 = 2018-01-01, BCD 12:34:56
493        // duration BCD 01:30:45
494        let sel: Vec<u8> = vec![
495            0x00, 0x01, // cell_id
496            12,   // loop_length
497            0x10, // network_change_id
498            0x20, // network_change_version
499            0xE4, 0x09, 0x12, 0x34, 0x56, // start_time_of_change
500            0x01, 0x30, 0x45, // change_duration
501            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
502            0x40, // message_id
503        ];
504        let bytes = wrap(0x07, &sel);
505        let d = ExtensionDescriptor::parse(&bytes).unwrap();
506        match &d.body {
507            ExtensionBody::NetworkChangeNotify(b) => {
508                assert_eq!(b.cells.len(), 1);
509                let ch = &b.cells[0].changes[0];
510                let utc = ch.start_time_of_change_utc().expect("should decode");
511                assert_eq!(utc.year(), 2018);
512                assert_eq!((utc.hour(), utc.minute(), utc.second()), (12, 34, 56));
513                let secs = ch.change_duration_secs().expect("should decode");
514                assert_eq!(secs, 5445); // 1h30m45s
515            }
516            other => panic!("expected NetworkChangeNotify, got {other:?}"),
517        }
518    }
519
520    #[test]
521    fn change_type_full_range_round_trip() {
522        for v in 0u8..=0x0F {
523            let ct = ChangeType::from_u8(v);
524            assert_eq!(ct.to_u8(), v, "ChangeType round-trip failed for {v}");
525        }
526    }
527
528    #[test]
529    fn change_type_known_values() {
530        assert_eq!(ChangeType::from_u8(0), ChangeType::MessageOnly);
531        assert_eq!(ChangeType::from_u8(1), ChangeType::MinorDefault);
532        assert_eq!(ChangeType::from_u8(4), ChangeType::MinorReserved(4));
533        assert_eq!(ChangeType::from_u8(8), ChangeType::MajorDefault);
534        assert_eq!(ChangeType::from_u8(9), ChangeType::MajorFrequencyChanged);
535        assert_eq!(ChangeType::from_u8(10), ChangeType::MajorCoverageChanged);
536        assert_eq!(ChangeType::from_u8(11), ChangeType::MajorMultiplexAdded);
537        assert_eq!(ChangeType::from_u8(12), ChangeType::MajorReserved(12));
538        assert_eq!(ChangeType::MessageOnly.name(), "message only");
539        assert_eq!(ChangeType::MajorDefault.name(), "major - default");
540        assert_eq!(ChangeType::MinorReserved(5).name(), "reserved (minor)");
541        assert_eq!(ChangeType::MajorReserved(13).name(), "reserved (major)");
542    }
543}