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 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 = u16::from_be_bytes([sel[pos], sel[pos + 1]]);
201            let loop_length = sel[pos + CELL_HEADER_LEN] as usize;
202            pos += CELL_HEADER_LEN + LOOP_LENGTH_LEN;
203
204            if pos + loop_length > sel.len() {
205                return Err(Error::BufferTooShort {
206                    need: pos + loop_length,
207                    have: sel.len(),
208                    what: "network_change_notify body",
209                });
210            }
211
212            let inner_end = pos + loop_length;
213            let mut changes = Vec::new();
214            while pos < inner_end {
215                let remaining = inner_end - pos;
216                // At minimum we need CHANGE_BASE_LEN bytes for a basic entry.
217                if remaining < CHANGE_BASE_LEN {
218                    return Err(Error::BufferTooShort {
219                        need: inner_end - remaining + CHANGE_BASE_LEN,
220                        have: sel.len(),
221                        what: "network_change_notify body",
222                    });
223                }
224                let network_change_id = sel[pos];
225                let network_change_version = sel[pos + 1];
226                let start_time_of_change = (u64::from(sel[pos + 2]) << 32)
227                    | (u64::from(sel[pos + 3]) << 24)
228                    | (u64::from(sel[pos + 4]) << 16)
229                    | (u64::from(sel[pos + 5]) << 8)
230                    | u64::from(sel[pos + 6]);
231                let change_duration = (u32::from(sel[pos + 7]) << 16)
232                    | (u32::from(sel[pos + 8]) << 8)
233                    | u32::from(sel[pos + 9]);
234                let packed = sel[pos + 10];
235                let receiver_category = packed >> 5;
236                let invariant_ts_present = (packed >> 4) & 1;
237                let change_type = ChangeType::from_u8(packed & 0x0F);
238                let message_id = sel[pos + 11];
239                pos += CHANGE_BASE_LEN;
240
241                let invariant_ts = if invariant_ts_present == 1 {
242                    if pos + INVARIANT_TS_LEN > inner_end {
243                        return Err(Error::BufferTooShort {
244                            need: pos + INVARIANT_TS_LEN,
245                            have: sel.len(),
246                            what: "network_change_notify body",
247                        });
248                    }
249                    let ts = InvariantTs {
250                        tsid: u16::from_be_bytes([sel[pos], sel[pos + 1]]),
251                        onid: u16::from_be_bytes([sel[pos + 2], sel[pos + 3]]),
252                    };
253                    pos += INVARIANT_TS_LEN;
254                    Some(ts)
255                } else {
256                    None
257                };
258
259                changes.push(NetworkChange {
260                    network_change_id,
261                    network_change_version,
262                    start_time_of_change,
263                    change_duration,
264                    receiver_category,
265                    change_type,
266                    message_id,
267                    invariant_ts,
268                });
269            }
270
271            if pos != inner_end {
272                return Err(invalid("network_change_notify: change entry overruns loop"));
273            }
274
275            cells.push(NetworkChangeCell { cell_id, changes });
276        }
277        Ok(NetworkChangeNotify { cells })
278    }
279}
280
281impl Serialize for NetworkChangeNotify {
282    type Error = crate::error::Error;
283    fn serialized_len(&self) -> usize {
284        self.cells
285            .iter()
286            .map(|cell| {
287                CELL_HEADER_LEN
288                    + LOOP_LENGTH_LEN
289                    + cell
290                        .changes
291                        .iter()
292                        .map(change_serialized_len)
293                        .sum::<usize>()
294            })
295            .sum()
296    }
297    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
298        let len = self.serialized_len();
299        if buf.len() < len {
300            return Err(Error::OutputBufferTooSmall {
301                need: len,
302                have: buf.len(),
303            });
304        }
305        let mut pos = 0;
306        for cell in &self.cells {
307            buf[pos..pos + 2].copy_from_slice(&cell.cell_id.to_be_bytes());
308            pos += 2;
309            let loop_length: usize = cell.changes.iter().map(change_serialized_len).sum();
310            buf[pos] = loop_length as u8;
311            pos += 1;
312            for ch in &cell.changes {
313                buf[pos] = ch.network_change_id;
314                buf[pos + 1] = ch.network_change_version;
315                let st = ch.start_time_of_change;
316                buf[pos + 2] = (st >> 32) as u8;
317                buf[pos + 3] = (st >> 24) as u8;
318                buf[pos + 4] = (st >> 16) as u8;
319                buf[pos + 5] = (st >> 8) as u8;
320                buf[pos + 6] = st as u8;
321                let dur = ch.change_duration;
322                buf[pos + 7] = (dur >> 16) as u8;
323                buf[pos + 8] = (dur >> 8) as u8;
324                buf[pos + 9] = dur as u8;
325                let packed = ((ch.receiver_category & 0x07) << 5)
326                    | ((ch.invariant_ts.is_some() as u8) << 4)
327                    | (ch.change_type.to_u8() & 0x0F);
328                buf[pos + 10] = packed;
329                buf[pos + 11] = ch.message_id;
330                pos += CHANGE_BASE_LEN;
331                if let Some(ref inv) = ch.invariant_ts {
332                    buf[pos..pos + 2].copy_from_slice(&inv.tsid.to_be_bytes());
333                    buf[pos + 2..pos + 4].copy_from_slice(&inv.onid.to_be_bytes());
334                    pos += INVARIANT_TS_LEN;
335                }
336            }
337        }
338        Ok(len)
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::descriptors::extension::test_support::*;
346    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
347
348    #[test]
349    fn parse_network_change_notify_structured() {
350        // 2 cells: one with 0 changes, one with 2 changes (one without
351        // invariant_ts, one with).
352        let sel = [
353            // cell 1: cell_id=0x0001, loop_length=0
354            0x00, 0x01, 0x00, // cell 2: cell_id=0x0002, loop_length=28 (0x1C)
355            0x00, 0x02, 0x1C,
356            //   change 1 (no invariant_ts): 12 bytes
357            0x10, // network_change_id
358            0x20, // network_change_version
359            0x00, 0x00, 0x00, 0x00, 0x01, // start_time_of_change = 1
360            0x00, 0x00, 0x01, // change_duration = 1
361            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
362            0x40, // message_id
363            //   change 2 (with invariant_ts): 16 bytes
364            0x30, // network_change_id
365            0x40, // network_change_version
366            0x00, 0x00, 0x00, 0x00, 0x02, // start_time_of_change = 2
367            0x00, 0x00, 0x02, // change_duration = 2
368            0x54, // packed: rec_cat=2, inv_ts=1, change_type=4
369            0x50, // message_id
370            0xAA, 0xAA, // tsid = 0xAAAA
371            0xBB, 0xBB, // onid = 0xBBBB
372        ];
373        let bytes = wrap(0x07, &sel);
374        let d = ExtensionDescriptor::parse(&bytes).unwrap();
375        match &d.body {
376            ExtensionBody::NetworkChangeNotify(b) => {
377                assert_eq!(b.cells.len(), 2);
378
379                assert_eq!(b.cells[0].cell_id, 0x0001);
380                assert!(b.cells[0].changes.is_empty());
381
382                assert_eq!(b.cells[1].cell_id, 0x0002);
383                assert_eq!(b.cells[1].changes.len(), 2);
384
385                let ch0 = &b.cells[1].changes[0];
386                assert_eq!(ch0.network_change_id, 0x10);
387                assert_eq!(ch0.network_change_version, 0x20);
388                assert_eq!(ch0.start_time_of_change, 1);
389                assert_eq!(ch0.change_duration, 1);
390                assert_eq!(ch0.receiver_category, 1);
391                assert_eq!(ch0.change_type, ChangeType::MinorServiceChanged);
392                assert_eq!(ch0.message_id, 0x40);
393                assert!(ch0.invariant_ts.is_none());
394
395                let ch1 = &b.cells[1].changes[1];
396                assert_eq!(ch1.network_change_id, 0x30);
397                assert_eq!(ch1.network_change_version, 0x40);
398                assert_eq!(ch1.start_time_of_change, 2);
399                assert_eq!(ch1.change_duration, 2);
400                assert_eq!(ch1.receiver_category, 2);
401                assert_eq!(ch1.change_type, ChangeType::MinorReserved(4));
402                assert_eq!(ch1.message_id, 0x50);
403                let inv = ch1.invariant_ts.as_ref().unwrap();
404                assert_eq!(inv.tsid, 0xAAAA);
405                assert_eq!(inv.onid, 0xBBBB);
406            }
407            other => panic!("expected NetworkChangeNotify, got {other:?}"),
408        }
409        round_trip(&d);
410    }
411
412    /// Byte-exact cross-check against a TSDuck-compiled descriptor (tsduck-test
413    /// test-015). Parse, assert decoded fields, assert kind(), then re-serialize
414    /// and verify byte-exact match.
415    #[test]
416    fn network_change_notify_tsduck_byte_exact() {
417        let bytes =
418            from_hex("7f230712340056781cabcde5cc2312340852030281ef67e5e20234561132453b83deadbeef");
419        let d = ExtensionDescriptor::parse(&bytes).unwrap();
420        assert_eq!(d.kind(), Some(ExtensionTag::NetworkChangeNotify));
421
422        match &d.body {
423            ExtensionBody::NetworkChangeNotify(b) => {
424                assert_eq!(b.cells.len(), 2);
425
426                // cell 0: cell_id=0x1234, no changes
427                assert_eq!(b.cells[0].cell_id, 0x1234);
428                assert!(b.cells[0].changes.is_empty());
429
430                // cell 1: cell_id=0x5678, 2 changes
431                assert_eq!(b.cells[1].cell_id, 0x5678);
432                assert_eq!(b.cells[1].changes.len(), 2);
433
434                // change 0: no invariant_ts
435                let ch0 = &b.cells[1].changes[0];
436                assert_eq!(ch0.network_change_id, 0xAB);
437                assert_eq!(ch0.network_change_version, 0xCD);
438                assert_eq!(ch0.start_time_of_change, 0xE5CC231234);
439                assert_eq!(ch0.change_duration, 0x085203);
440                assert_eq!(ch0.receiver_category, 0);
441                assert_eq!(ch0.change_type, ChangeType::MinorMultiplexRemoved);
442                assert_eq!(ch0.message_id, 0x81);
443                assert!(ch0.invariant_ts.is_none());
444
445                // change 1: with invariant_ts
446                let ch1 = &b.cells[1].changes[1];
447                assert_eq!(ch1.network_change_id, 0xEF);
448                assert_eq!(ch1.network_change_version, 0x67);
449                assert_eq!(ch1.start_time_of_change, 0xE5E2023456);
450                assert_eq!(ch1.change_duration, 0x113245);
451                assert_eq!(ch1.receiver_category, 1);
452                assert_eq!(ch1.change_type, ChangeType::MajorMultiplexAdded);
453                assert_eq!(ch1.message_id, 0x83);
454                let inv = ch1.invariant_ts.as_ref().unwrap();
455                assert_eq!(inv.tsid, 0xDEAD);
456                assert_eq!(inv.onid, 0xBEEF);
457            }
458            other => panic!("expected NetworkChangeNotify, got {other:?}"),
459        }
460
461        let mut out = vec![0u8; d.serialized_len()];
462        let n = d.serialize_into(&mut out).unwrap();
463        assert_eq!(
464            out[..n],
465            bytes[..],
466            "byte-exact re-serialize for TSDuck vector"
467        );
468    }
469
470    #[cfg(feature = "chrono")]
471    #[test]
472    fn chrono_accessors_decode_start_time_and_duration() {
473        use chrono::{Datelike, Timelike};
474        // One cell with one change entry, no invariant_ts.
475        // cell_id=0x0001, loop_length=12, then one 12-byte change entry.
476        // MJD 0xE409 = 2018-01-01, BCD 12:34:56
477        // duration BCD 01:30:45
478        let sel: Vec<u8> = vec![
479            0x00, 0x01, // cell_id
480            12,   // loop_length
481            0x10, // network_change_id
482            0x20, // network_change_version
483            0xE4, 0x09, 0x12, 0x34, 0x56, // start_time_of_change
484            0x01, 0x30, 0x45, // change_duration
485            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
486            0x40, // message_id
487        ];
488        let bytes = wrap(0x07, &sel);
489        let d = ExtensionDescriptor::parse(&bytes).unwrap();
490        match &d.body {
491            ExtensionBody::NetworkChangeNotify(b) => {
492                assert_eq!(b.cells.len(), 1);
493                let ch = &b.cells[0].changes[0];
494                let utc = ch.start_time_of_change_utc().expect("should decode");
495                assert_eq!(utc.year(), 2018);
496                assert_eq!((utc.hour(), utc.minute(), utc.second()), (12, 34, 56));
497                let secs = ch.change_duration_secs().expect("should decode");
498                assert_eq!(secs, 5445); // 1h30m45s
499            }
500            other => panic!("expected NetworkChangeNotify, got {other:?}"),
501        }
502    }
503
504    #[test]
505    fn change_type_full_range_round_trip() {
506        for v in 0u8..=0x0F {
507            let ct = ChangeType::from_u8(v);
508            assert_eq!(ct.to_u8(), v, "ChangeType round-trip failed for {v}");
509        }
510    }
511
512    #[test]
513    fn change_type_known_values() {
514        assert_eq!(ChangeType::from_u8(0), ChangeType::MessageOnly);
515        assert_eq!(ChangeType::from_u8(1), ChangeType::MinorDefault);
516        assert_eq!(ChangeType::from_u8(4), ChangeType::MinorReserved(4));
517        assert_eq!(ChangeType::from_u8(8), ChangeType::MajorDefault);
518        assert_eq!(ChangeType::from_u8(9), ChangeType::MajorFrequencyChanged);
519        assert_eq!(ChangeType::from_u8(10), ChangeType::MajorCoverageChanged);
520        assert_eq!(ChangeType::from_u8(11), ChangeType::MajorMultiplexAdded);
521        assert_eq!(ChangeType::from_u8(12), ChangeType::MajorReserved(12));
522        assert_eq!(ChangeType::MessageOnly.name(), "message only");
523        assert_eq!(ChangeType::MajorDefault.name(), "major - default");
524        assert_eq!(ChangeType::MinorReserved(5).name(), "reserved (minor)");
525        assert_eq!(ChangeType::MajorReserved(13).name(), "reserved (major)");
526    }
527}