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::*;
3
4const CELL_HEADER_LEN: usize = 2; // cell_id(16)
5const LOOP_LENGTH_LEN: usize = 1; // loop_length(8)
6const CHANGE_BASE_LEN: usize = 12; // id(1)+ver(1)+start(5)+dur(3)+packed(1)+msg(1)
7const INVARIANT_TS_LEN: usize = 4; // tsid(16)+onid(16)
8
9/// network_change_notify body (Table 149, §6.4.9). The two-level cell/change loop is unfolded.
10#[derive(Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize))]
12pub struct NetworkChangeNotify {
13    /// Per-cell change lists.
14    pub cells: Vec<NetworkChangeCell>,
15}
16
17/// A cell in the network_change_notify outer loop.
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20pub struct NetworkChangeCell {
21    /// cell_id(16).
22    pub cell_id: u16,
23    /// The cell's change entries.
24    pub changes: Vec<NetworkChange>,
25}
26
27/// A change entry in the network_change_notify inner loop.
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30pub struct NetworkChange {
31    /// network_change_id(8).
32    pub network_change_id: u8,
33    /// network_change_version(8).
34    pub network_change_version: u8,
35    /// start_time_of_change(40) — raw 40-bit value (MJD date + UTC BCD time), big-endian.
36    pub start_time_of_change: u64,
37    /// change_duration(24) — raw 24-bit BCD value, big-endian.
38    pub change_duration: u32,
39    /// receiver_category(3).
40    pub receiver_category: u8,
41    /// change_type(4).
42    pub change_type: u8,
43    /// message_id(8).
44    pub message_id: u8,
45    /// invariant_ts (tsid, onid), present iff invariant_ts_present==1.
46    pub invariant_ts: Option<InvariantTs>,
47}
48
49/// Conditional invariant-TS fields in a network_change_notify entry.
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52pub struct InvariantTs {
53    /// invariant_ts_tsid(16).
54    pub tsid: u16,
55    /// invariant_ts_onid(16).
56    pub onid: u16,
57}
58
59impl<'a> ExtensionBodyDef<'a> for NetworkChangeNotify {
60    const TAG_EXTENSION: u8 = 0x07;
61    const NAME: &'static str = "NETWORK_CHANGE_NOTIFY";
62}
63
64impl NetworkChange {
65    /// Decode `start_time_of_change` (40-bit MJD+BCD UTC) to a
66    /// [`chrono::DateTime<chrono::Utc>`]. `None` if the date/time fields
67    /// are out of range. Reuses the same helper as EIT/TOT/TDT.
68    #[cfg(feature = "chrono")]
69    #[must_use]
70    pub fn start_time_of_change_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
71        let raw = self.start_time_of_change;
72        let bytes = [
73            (raw >> 32) as u8,
74            (raw >> 24) as u8,
75            (raw >> 16) as u8,
76            (raw >> 8) as u8,
77            raw as u8,
78        ];
79        dvb_common::time::decode_mjd_bcd_utc(bytes)
80    }
81
82    /// Decode `change_duration` (24-bit BCD `HHMMSS`) to seconds.
83    /// `None` if the BCD nibbles are out of range.
84    #[cfg(feature = "chrono")]
85    #[must_use]
86    pub fn change_duration_secs(&self) -> Option<u32> {
87        let raw = self.change_duration;
88        let bytes = [(raw >> 16) as u8, (raw >> 8) as u8, raw as u8];
89        dvb_common::time::decode_bcd_duration(bytes).map(|d| d.as_secs() as u32)
90    }
91}
92
93fn change_serialized_len(ch: &NetworkChange) -> usize {
94    CHANGE_BASE_LEN
95        + if ch.invariant_ts.is_some() {
96            INVARIANT_TS_LEN
97        } else {
98            0
99        }
100}
101
102impl<'a> Parse<'a> for NetworkChangeNotify {
103    type Error = crate::error::Error;
104    fn parse(sel: &'a [u8]) -> Result<Self> {
105        let mut cells = Vec::new();
106        let mut pos = 0;
107        while pos < sel.len() {
108            if pos + CELL_HEADER_LEN + LOOP_LENGTH_LEN > sel.len() {
109                return Err(Error::BufferTooShort {
110                    need: pos + CELL_HEADER_LEN + LOOP_LENGTH_LEN,
111                    have: sel.len(),
112                    what: "network_change_notify body",
113                });
114            }
115            let cell_id = u16::from_be_bytes([sel[pos], sel[pos + 1]]);
116            let loop_length = sel[pos + CELL_HEADER_LEN] as usize;
117            pos += CELL_HEADER_LEN + LOOP_LENGTH_LEN;
118
119            if pos + loop_length > sel.len() {
120                return Err(Error::BufferTooShort {
121                    need: pos + loop_length,
122                    have: sel.len(),
123                    what: "network_change_notify body",
124                });
125            }
126
127            let inner_end = pos + loop_length;
128            let mut changes = Vec::new();
129            while pos < inner_end {
130                let remaining = inner_end - pos;
131                // At minimum we need CHANGE_BASE_LEN bytes for a basic entry.
132                if remaining < CHANGE_BASE_LEN {
133                    return Err(Error::BufferTooShort {
134                        need: inner_end - remaining + CHANGE_BASE_LEN,
135                        have: sel.len(),
136                        what: "network_change_notify body",
137                    });
138                }
139                let network_change_id = sel[pos];
140                let network_change_version = sel[pos + 1];
141                let start_time_of_change = (u64::from(sel[pos + 2]) << 32)
142                    | (u64::from(sel[pos + 3]) << 24)
143                    | (u64::from(sel[pos + 4]) << 16)
144                    | (u64::from(sel[pos + 5]) << 8)
145                    | u64::from(sel[pos + 6]);
146                let change_duration = (u32::from(sel[pos + 7]) << 16)
147                    | (u32::from(sel[pos + 8]) << 8)
148                    | u32::from(sel[pos + 9]);
149                let packed = sel[pos + 10];
150                let receiver_category = packed >> 5;
151                let invariant_ts_present = (packed >> 4) & 1;
152                let change_type = packed & 0x0F;
153                let message_id = sel[pos + 11];
154                pos += CHANGE_BASE_LEN;
155
156                let invariant_ts = if invariant_ts_present == 1 {
157                    if pos + INVARIANT_TS_LEN > inner_end {
158                        return Err(Error::BufferTooShort {
159                            need: pos + INVARIANT_TS_LEN,
160                            have: sel.len(),
161                            what: "network_change_notify body",
162                        });
163                    }
164                    let ts = InvariantTs {
165                        tsid: u16::from_be_bytes([sel[pos], sel[pos + 1]]),
166                        onid: u16::from_be_bytes([sel[pos + 2], sel[pos + 3]]),
167                    };
168                    pos += INVARIANT_TS_LEN;
169                    Some(ts)
170                } else {
171                    None
172                };
173
174                changes.push(NetworkChange {
175                    network_change_id,
176                    network_change_version,
177                    start_time_of_change,
178                    change_duration,
179                    receiver_category,
180                    change_type,
181                    message_id,
182                    invariant_ts,
183                });
184            }
185
186            if pos != inner_end {
187                return Err(invalid("network_change_notify: change entry overruns loop"));
188            }
189
190            cells.push(NetworkChangeCell { cell_id, changes });
191        }
192        Ok(NetworkChangeNotify { cells })
193    }
194}
195
196impl Serialize for NetworkChangeNotify {
197    type Error = crate::error::Error;
198    fn serialized_len(&self) -> usize {
199        self.cells
200            .iter()
201            .map(|cell| {
202                CELL_HEADER_LEN
203                    + LOOP_LENGTH_LEN
204                    + cell
205                        .changes
206                        .iter()
207                        .map(change_serialized_len)
208                        .sum::<usize>()
209            })
210            .sum()
211    }
212    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
213        let len = self.serialized_len();
214        if buf.len() < len {
215            return Err(Error::OutputBufferTooSmall {
216                need: len,
217                have: buf.len(),
218            });
219        }
220        let mut pos = 0;
221        for cell in &self.cells {
222            buf[pos..pos + 2].copy_from_slice(&cell.cell_id.to_be_bytes());
223            pos += 2;
224            let loop_length: usize = cell.changes.iter().map(change_serialized_len).sum();
225            buf[pos] = loop_length as u8;
226            pos += 1;
227            for ch in &cell.changes {
228                buf[pos] = ch.network_change_id;
229                buf[pos + 1] = ch.network_change_version;
230                let st = ch.start_time_of_change;
231                buf[pos + 2] = (st >> 32) as u8;
232                buf[pos + 3] = (st >> 24) as u8;
233                buf[pos + 4] = (st >> 16) as u8;
234                buf[pos + 5] = (st >> 8) as u8;
235                buf[pos + 6] = st as u8;
236                let dur = ch.change_duration;
237                buf[pos + 7] = (dur >> 16) as u8;
238                buf[pos + 8] = (dur >> 8) as u8;
239                buf[pos + 9] = dur as u8;
240                let packed = ((ch.receiver_category & 0x07) << 5)
241                    | ((ch.invariant_ts.is_some() as u8) << 4)
242                    | (ch.change_type & 0x0F);
243                buf[pos + 10] = packed;
244                buf[pos + 11] = ch.message_id;
245                pos += CHANGE_BASE_LEN;
246                if let Some(ref inv) = ch.invariant_ts {
247                    buf[pos..pos + 2].copy_from_slice(&inv.tsid.to_be_bytes());
248                    buf[pos + 2..pos + 4].copy_from_slice(&inv.onid.to_be_bytes());
249                    pos += INVARIANT_TS_LEN;
250                }
251            }
252        }
253        Ok(len)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::descriptors::extension::test_support::*;
261    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
262
263    #[test]
264    fn parse_network_change_notify_structured() {
265        // 2 cells: one with 0 changes, one with 2 changes (one without
266        // invariant_ts, one with).
267        let sel = [
268            // cell 1: cell_id=0x0001, loop_length=0
269            0x00, 0x01, 0x00, // cell 2: cell_id=0x0002, loop_length=28 (0x1C)
270            0x00, 0x02, 0x1C,
271            //   change 1 (no invariant_ts): 12 bytes
272            0x10, // network_change_id
273            0x20, // network_change_version
274            0x00, 0x00, 0x00, 0x00, 0x01, // start_time_of_change = 1
275            0x00, 0x00, 0x01, // change_duration = 1
276            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
277            0x40, // message_id
278            //   change 2 (with invariant_ts): 16 bytes
279            0x30, // network_change_id
280            0x40, // network_change_version
281            0x00, 0x00, 0x00, 0x00, 0x02, // start_time_of_change = 2
282            0x00, 0x00, 0x02, // change_duration = 2
283            0x54, // packed: rec_cat=2, inv_ts=1, change_type=4
284            0x50, // message_id
285            0xAA, 0xAA, // tsid = 0xAAAA
286            0xBB, 0xBB, // onid = 0xBBBB
287        ];
288        let bytes = wrap(0x07, &sel);
289        let d = ExtensionDescriptor::parse(&bytes).unwrap();
290        match &d.body {
291            ExtensionBody::NetworkChangeNotify(b) => {
292                assert_eq!(b.cells.len(), 2);
293
294                assert_eq!(b.cells[0].cell_id, 0x0001);
295                assert!(b.cells[0].changes.is_empty());
296
297                assert_eq!(b.cells[1].cell_id, 0x0002);
298                assert_eq!(b.cells[1].changes.len(), 2);
299
300                let ch0 = &b.cells[1].changes[0];
301                assert_eq!(ch0.network_change_id, 0x10);
302                assert_eq!(ch0.network_change_version, 0x20);
303                assert_eq!(ch0.start_time_of_change, 1);
304                assert_eq!(ch0.change_duration, 1);
305                assert_eq!(ch0.receiver_category, 1);
306                assert_eq!(ch0.change_type, 3);
307                assert_eq!(ch0.message_id, 0x40);
308                assert!(ch0.invariant_ts.is_none());
309
310                let ch1 = &b.cells[1].changes[1];
311                assert_eq!(ch1.network_change_id, 0x30);
312                assert_eq!(ch1.network_change_version, 0x40);
313                assert_eq!(ch1.start_time_of_change, 2);
314                assert_eq!(ch1.change_duration, 2);
315                assert_eq!(ch1.receiver_category, 2);
316                assert_eq!(ch1.change_type, 4);
317                assert_eq!(ch1.message_id, 0x50);
318                let inv = ch1.invariant_ts.as_ref().unwrap();
319                assert_eq!(inv.tsid, 0xAAAA);
320                assert_eq!(inv.onid, 0xBBBB);
321            }
322            other => panic!("expected NetworkChangeNotify, got {other:?}"),
323        }
324        round_trip(&d);
325    }
326
327    /// Byte-exact cross-check against a TSDuck-compiled descriptor (tsduck-test
328    /// test-015). Parse, assert decoded fields, assert kind(), then re-serialize
329    /// and verify byte-exact match.
330    #[test]
331    fn network_change_notify_tsduck_byte_exact() {
332        let bytes =
333            from_hex("7f230712340056781cabcde5cc2312340852030281ef67e5e20234561132453b83deadbeef");
334        let d = ExtensionDescriptor::parse(&bytes).unwrap();
335        assert_eq!(d.kind(), Some(ExtensionTag::NetworkChangeNotify));
336
337        match &d.body {
338            ExtensionBody::NetworkChangeNotify(b) => {
339                assert_eq!(b.cells.len(), 2);
340
341                // cell 0: cell_id=0x1234, no changes
342                assert_eq!(b.cells[0].cell_id, 0x1234);
343                assert!(b.cells[0].changes.is_empty());
344
345                // cell 1: cell_id=0x5678, 2 changes
346                assert_eq!(b.cells[1].cell_id, 0x5678);
347                assert_eq!(b.cells[1].changes.len(), 2);
348
349                // change 0: no invariant_ts
350                let ch0 = &b.cells[1].changes[0];
351                assert_eq!(ch0.network_change_id, 0xAB);
352                assert_eq!(ch0.network_change_version, 0xCD);
353                assert_eq!(ch0.start_time_of_change, 0xE5CC231234);
354                assert_eq!(ch0.change_duration, 0x085203);
355                assert_eq!(ch0.receiver_category, 0);
356                assert_eq!(ch0.change_type, 2);
357                assert_eq!(ch0.message_id, 0x81);
358                assert!(ch0.invariant_ts.is_none());
359
360                // change 1: with invariant_ts
361                let ch1 = &b.cells[1].changes[1];
362                assert_eq!(ch1.network_change_id, 0xEF);
363                assert_eq!(ch1.network_change_version, 0x67);
364                assert_eq!(ch1.start_time_of_change, 0xE5E2023456);
365                assert_eq!(ch1.change_duration, 0x113245);
366                assert_eq!(ch1.receiver_category, 1);
367                assert_eq!(ch1.change_type, 0xB);
368                assert_eq!(ch1.message_id, 0x83);
369                let inv = ch1.invariant_ts.as_ref().unwrap();
370                assert_eq!(inv.tsid, 0xDEAD);
371                assert_eq!(inv.onid, 0xBEEF);
372            }
373            other => panic!("expected NetworkChangeNotify, got {other:?}"),
374        }
375
376        let mut out = vec![0u8; d.serialized_len()];
377        let n = d.serialize_into(&mut out).unwrap();
378        assert_eq!(
379            out[..n],
380            bytes[..],
381            "byte-exact re-serialize for TSDuck vector"
382        );
383    }
384
385    #[cfg(feature = "chrono")]
386    #[test]
387    fn chrono_accessors_decode_start_time_and_duration() {
388        use chrono::{Datelike, Timelike};
389        // One cell with one change entry, no invariant_ts.
390        // cell_id=0x0001, loop_length=12, then one 12-byte change entry.
391        // MJD 0xE409 = 2018-01-01, BCD 12:34:56
392        // duration BCD 01:30:45
393        let sel: Vec<u8> = vec![
394            0x00, 0x01, // cell_id
395            12,   // loop_length
396            0x10, // network_change_id
397            0x20, // network_change_version
398            0xE4, 0x09, 0x12, 0x34, 0x56, // start_time_of_change
399            0x01, 0x30, 0x45, // change_duration
400            0x23, // packed: rec_cat=1, inv_ts=0, change_type=3
401            0x40, // message_id
402        ];
403        let bytes = wrap(0x07, &sel);
404        let d = ExtensionDescriptor::parse(&bytes).unwrap();
405        match &d.body {
406            ExtensionBody::NetworkChangeNotify(b) => {
407                assert_eq!(b.cells.len(), 1);
408                let ch = &b.cells[0].changes[0];
409                let utc = ch.start_time_of_change_utc().expect("should decode");
410                assert_eq!(utc.year(), 2018);
411                assert_eq!((utc.hour(), utc.minute(), utc.second()), (12, 34, 56));
412                let secs = ch.change_duration_secs().expect("should decode");
413                assert_eq!(secs, 5445); // 1h30m45s
414            }
415            other => panic!("expected NetworkChangeNotify, got {other:?}"),
416        }
417    }
418}