use num_enum::TryFromPrimitive;
use dvb_common::{Parse, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[repr(u8)]
pub enum Bandwidth {
Mhz1_7 = 0,
Mhz5 = 1,
Mhz6 = 2,
Mhz7 = 3,
Mhz8 = 4,
Mhz10 = 5,
}
impl From<Bandwidth> for u8 {
fn from(bw: Bandwidth) -> Self {
bw as u8
}
}
impl From<num_enum::TryFromPrimitiveError<Bandwidth>> for crate::error::Error {
fn from(_: num_enum::TryFromPrimitiveError<Bandwidth>) -> Self {
crate::error::Error::ReservedBitsViolation {
field: "bw",
reason: "Must be 0..=5 per ETSI TS 102 773 §5.2.7 Table 3",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct T2TimestampPayload {
pub bw: Bandwidth,
pub seconds_since_2000: u64,
pub subseconds: u32,
pub utco: u16,
}
const TIMESTAMP_HEADER_LEN: usize = 11;
impl<'a> Parse<'a> for T2TimestampPayload {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self, crate::error::Error> {
if bytes.len() < TIMESTAMP_HEADER_LEN {
return Err(crate::Error::BufferTooShort {
need: TIMESTAMP_HEADER_LEN,
have: bytes.len(),
what: "T2TimestampPayload header",
});
}
if bytes[0] & 0xF0 != 0 {
return Err(crate::Error::ReservedBitsViolation {
field: "4-bit RFU",
reason: "Must be zero (ETSI TS 102 773 §5.2.7)",
});
}
let bw = Bandwidth::try_from(bytes[0] & 0x0F)?;
let seconds_since_2000 = (bytes[1] as u64) << 32
| (bytes[2] as u64) << 24
| (bytes[3] as u64) << 16
| (bytes[4] as u64) << 8
| (bytes[5] as u64);
let subseconds = (bytes[6] as u32) << 19
| (bytes[7] as u32) << 11
| (bytes[8] as u32) << 3
| ((bytes[9] >> 5) as u32 & 0x7);
let utco = ((bytes[9] as u16 & 0x1F) << 8) | (bytes[10] as u16);
Ok(T2TimestampPayload {
bw,
seconds_since_2000,
subseconds,
utco,
})
}
}
impl<'a> crate::traits::PayloadDef<'a> for T2TimestampPayload {
const PACKET_TYPE: u8 = 0x20;
const NAME: &'static str = "TIMESTAMP";
}
impl Serialize for T2TimestampPayload {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
TIMESTAMP_HEADER_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize, crate::error::Error> {
if buf.len() < self.serialized_len() {
return Err(crate::Error::OutputBufferTooSmall {
need: self.serialized_len(),
have: buf.len(),
});
}
if self.seconds_since_2000 > 0xFF_FFFF_FFFF {
return Err(crate::Error::ReservedBitsViolation {
field: "seconds_since_2000",
reason: "Must fit in 40 bits",
});
}
if self.subseconds > 0x7FFFFFF {
return Err(crate::Error::ReservedBitsViolation {
field: "subseconds",
reason: "Must fit in 27 bits",
});
}
if self.utco > 0x1FFF {
return Err(crate::Error::ReservedBitsViolation {
field: "utco",
reason: "Must fit in 13 bits",
});
}
buf[0] = u8::from(self.bw) & 0x0F; buf[1] = (self.seconds_since_2000 >> 32 & 0xFF) as u8;
buf[2] = (self.seconds_since_2000 >> 24 & 0xFF) as u8;
buf[3] = (self.seconds_since_2000 >> 16 & 0xFF) as u8;
buf[4] = (self.seconds_since_2000 >> 8 & 0xFF) as u8;
buf[5] = (self.seconds_since_2000 & 0xFF) as u8;
buf[6] = (self.subseconds >> 19 & 0xFF) as u8;
buf[7] = (self.subseconds >> 11 & 0xFF) as u8;
buf[8] = (self.subseconds >> 3 & 0xFF) as u8;
buf[9] = ((self.subseconds & 0x7) as u8) << 5 | ((self.utco >> 8) as u8 & 0x1F);
buf[10] = (self.utco & 0xFF) as u8;
Ok(self.serialized_len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bandwidth_try_from_valid() {
assert_eq!(Bandwidth::try_from(0), Ok(Bandwidth::Mhz1_7));
assert_eq!(Bandwidth::try_from(5), Ok(Bandwidth::Mhz10));
}
#[test]
fn bandwidth_try_from_rejects_6() {
assert!(Bandwidth::try_from(6).is_err());
}
#[test]
fn exhaustive_byte_sweep() {
let mut matched = 0u16;
for byte in 0u8..=0xFF {
if let Ok(v) = Bandwidth::try_from(byte) {
assert_eq!(v as u8, byte, "round-trip failed for {byte:#04x}");
matched += 1;
}
}
assert_eq!(matched, 6, "expected 6 matched variants");
}
#[test]
fn parse_extracts_all_fields() {
let mut buf = [0u8; 11];
buf[0] = 0x02; buf[1] = 0x00;
buf[2] = 0x00;
buf[3] = 0x01; buf[6] = 0x00;
buf[7] = 0x00;
buf[8] = 0x00;
buf[9] = 0x00; buf[10] = 0x00;
let result = T2TimestampPayload::parse(&buf).unwrap();
assert_eq!(result.bw, Bandwidth::Mhz6);
assert_eq!(result.seconds_since_2000, 0x00_00_01_00_00);
}
#[test]
fn parse_rejects_nonzero_rfu() {
let buf = [
0x80u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(T2TimestampPayload::parse(&buf).is_err());
}
#[test]
fn parse_rejects_short_buffer() {
assert!(T2TimestampPayload::parse(&[0x00; 10]).is_err());
}
#[test]
fn serialize_round_trip() {
let orig = T2TimestampPayload {
bw: Bandwidth::Mhz8,
seconds_since_2000: 0x00_00_01_02_03,
subseconds: 0x0123456,
utco: 0x7FF,
};
let mut buf = [0u8; 11];
orig.serialize_into(&mut buf).unwrap();
let parsed = T2TimestampPayload::parse(&buf).unwrap();
assert_eq!(orig, parsed);
}
#[test]
fn null_timestamp_all_ones() {
let mut buf = [0xFFu8; 11];
buf[0] = 0x0F; buf[0] = 0x0F; buf[0] = 0x00; buf[0] = 0x02; buf[1..11].fill(0xFF);
let result = T2TimestampPayload::parse(&buf);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.seconds_since_2000, 0xFFFFFFFFFF); assert_eq!(parsed.subseconds, 0x7FFFFFF); assert_eq!(parsed.utco, 0x1FFF); }
}