blob: 3630f4e44bd7e39f35b0542a64023456a8d2997e [file] [log] [blame]
// Copyright 2023 Google LLC
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use bitflags::bitflags;
use crate::packet_encoding::{Decodable, Encodable, Error as PacketError};
pub type Metadata = Vec<Metadatum>;
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Type {
PreferredAudioContexts = 0x01,
StreamingAudioContexts = 0x02,
ProgramInfo = 0x03,
Language = 0x04,
CCIDList = 0x05,
ParentalRating = 0x06,
ProgramInfoURI = 0x07,
ExtendedMetadata = 0xfe,
VendorSpecific = 0xff,
AudioActiveState = 0x08,
BroadcastAudioImmediateRenderingFlag = 0x09,
}
impl Type {
fn length_check(&self, provided: usize) -> Result<(), PacketError> {
if self.total_size_range().contains(&provided) {
Ok(())
} else {
Err(PacketError::UnexpectedDataLength)
}
}
// Total size range including all Length, Type, and Value parameters.
fn total_size_range(&self) -> std::ops::Range<usize> {
match self {
Type::PreferredAudioContexts | Type::StreamingAudioContexts => 4..5,
Type::ProgramInfo | Type::CCIDList | Type::ProgramInfoURI => 2..usize::MAX,
Type::Language => 5..6,
Type::ParentalRating => 3..4,
Type::ExtendedMetadata | Type::VendorSpecific => 4..256,
Type::AudioActiveState => 3..4,
Type::BroadcastAudioImmediateRenderingFlag => 2..3,
}
}
}
impl TryFrom<u8> for Type {
type Error = PacketError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
let t = match value {
0x01 => Type::PreferredAudioContexts,
0x02 => Type::StreamingAudioContexts,
0x03 => Type::ProgramInfo,
0x04 => Type::Language,
0x05 => Type::CCIDList,
0x06 => Type::ParentalRating,
0x07 => Type::ProgramInfoURI,
0xFE => Type::ExtendedMetadata,
0xFF => Type::VendorSpecific,
0x08 => Type::AudioActiveState,
0x09 => Type::BroadcastAudioImmediateRenderingFlag,
_ => return Err(PacketError::OutOfRange),
};
Ok(t)
}
}
#[derive(Debug, PartialEq)]
pub enum Metadatum {
PreferredAudioContexts(ContextType),
StreamingAudioContexts(ContextType),
ProgramInfo(String),
/// 3 byte, lower case language code as defined in ISO 639-­3
LanguageCode(String),
CCIDList(Vec<u8>),
ParentalRating(Rating),
ProgramInfoURI(String),
ExtendedMetadata {
type_: u16,
metadata: Vec<u8>,
},
VendorSpecific {
company_id: u16,
metadata: Vec<u8>,
},
AudioActiveState(bool),
// Flag for Broadcast Audio Immediate Rendering.
BroadcastAudioImmediateRenderingFlag,
}
impl Metadatum {
const LENGTH_BYTE_SIZE: usize = 1;
const TYPE_BYTE_SIZE: usize = 1;
const MIN_PACKET_SIZE: usize = Self::LENGTH_BYTE_SIZE + Self::TYPE_BYTE_SIZE;
// Size of this metadatum's Value parameter. Does not include the Length or Type
// size.
pub fn value_encoded_len(&self) -> usize {
match self {
Metadatum::PreferredAudioContexts(_) => 2,
Metadatum::StreamingAudioContexts(_) => 2,
Metadatum::ProgramInfo(value) => value.len(),
Metadatum::LanguageCode(_) => 3,
Metadatum::CCIDList(ccids) => ccids.len(),
Metadatum::ParentalRating(_) => 1,
Metadatum::ProgramInfoURI(uri) => uri.len(),
Metadatum::ExtendedMetadata { type_: _, metadata } => 2 + metadata.len(),
Metadatum::VendorSpecific { company_id: _, metadata } => 2 + metadata.len(),
Metadatum::AudioActiveState(_) => 1,
Metadatum::BroadcastAudioImmediateRenderingFlag => 0,
}
}
pub fn type_(&self) -> Type {
match self {
Metadatum::PreferredAudioContexts(_) => Type::PreferredAudioContexts,
Metadatum::StreamingAudioContexts(_) => Type::StreamingAudioContexts,
Metadatum::ProgramInfo(_) => Type::ProgramInfo,
Metadatum::LanguageCode(_) => Type::Language,
Metadatum::CCIDList(_) => Type::CCIDList,
Metadatum::ParentalRating(_) => Type::ParentalRating,
Metadatum::ProgramInfoURI(_) => Type::ProgramInfoURI,
Metadatum::ExtendedMetadata { .. } => Type::ExtendedMetadata,
Metadatum::VendorSpecific { .. } => Type::VendorSpecific,
Metadatum::AudioActiveState(_) => Type::AudioActiveState,
Metadatum::BroadcastAudioImmediateRenderingFlag => {
Type::BroadcastAudioImmediateRenderingFlag
}
}
}
}
impl Decodable for Metadatum {
type Error = PacketError;
fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> {
if buf.len() < Self::MIN_PACKET_SIZE {
return Err(PacketError::UnexpectedDataLength);
}
// Total length of the buffer including all Length, Type, and Value parameters.
let total_len = 1 + buf[0] as usize;
if buf.len() < total_len {
return Err(PacketError::UnexpectedDataLength);
}
let type_ = Type::try_from(buf[1])?;
let _ = type_.length_check(total_len)?;
let m: Metadatum = match type_ {
Type::PreferredAudioContexts | Type::StreamingAudioContexts => {
let context_type =
ContextType::try_from(u16::from_le_bytes(buf[2..4].try_into().unwrap()))?;
if type_ == Type::PreferredAudioContexts {
Self::PreferredAudioContexts(context_type)
} else {
Self::StreamingAudioContexts(context_type)
}
}
Type::ProgramInfo | Type::ProgramInfoURI => {
let value = String::from_utf8(buf[2..total_len].to_vec())
.map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
if type_ == Type::ProgramInfo {
Self::ProgramInfo(value)
} else {
Self::ProgramInfoURI(value)
}
}
Type::Language => {
let code = String::from_utf8(buf[2..total_len].to_vec())
.map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
Self::LanguageCode(code)
}
Type::CCIDList => Self::CCIDList(buf[2..total_len].to_vec()),
Type::ParentalRating => Self::ParentalRating(Rating::decode(&buf[2..3])?.0),
Type::ExtendedMetadata | Type::VendorSpecific => {
let type_or_id = u16::from_le_bytes(buf[2..4].try_into().unwrap());
let data = if buf.len() >= 5 { buf[4..total_len].to_vec() } else { vec![] };
if type_ == Type::ExtendedMetadata {
Self::ExtendedMetadata { type_: type_or_id, metadata: data }
} else {
Self::VendorSpecific { company_id: type_or_id, metadata: data }
}
}
Type::AudioActiveState => {
if buf[2] > 1 {
return Err(PacketError::UnexpectedDataLength);
}
Self::AudioActiveState(buf[2] != 0)
}
Type::BroadcastAudioImmediateRenderingFlag => {
Self::BroadcastAudioImmediateRenderingFlag
}
};
return Ok((m, total_len));
}
}
impl Encodable for Metadatum {
type Error = PacketError;
/// Encodes the object into a provided buffer in a Metadata LTV structure.
/// Bytes are in LSO.
// See Assigned Numbers section 6.12.6 for details about Metadata LTV
// structures.
fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> {
if buf.len() < self.encoded_len() {
return Err(PacketError::BufferTooSmall);
}
// Length parameter value excludes the length of Length parameter itself (which
// is 1 byte).
buf[0] = (self.encoded_len() - 1)
.try_into()
.map_err(|_| PacketError::InvalidParameter("Metadata too big".to_string()))?;
buf[1] = self.type_() as u8;
match self {
Self::PreferredAudioContexts(type_) | Self::StreamingAudioContexts(type_) => {
buf[2..4].copy_from_slice(&type_.bits().to_le_bytes())
}
Self::ProgramInfo(value) | Self::ProgramInfoURI(value) => {
buf[2..2 + value.len()].copy_from_slice(value.as_bytes())
}
Self::LanguageCode(value) if value.len() != 3 => {
return Err(PacketError::InvalidParameter(format!("{self}")));
}
Self::LanguageCode(value) => buf[2..5].copy_from_slice(&value.as_bytes()[..3]),
Self::CCIDList(value) => buf[2..2 + value.len()].copy_from_slice(&value.as_slice()),
Self::ParentalRating(value) => buf[2] = value.into(),
Self::ExtendedMetadata { type_, metadata } => {
buf[2..4].copy_from_slice(&type_.to_le_bytes());
buf[4..4 + metadata.len()].copy_from_slice(&metadata.as_slice());
}
Self::VendorSpecific { company_id, metadata } => {
buf[2..4].copy_from_slice(&company_id.to_le_bytes());
buf[4..4 + metadata.len()].copy_from_slice(&metadata.as_slice());
}
Self::AudioActiveState(value) => buf[2] = *value as u8,
Self::BroadcastAudioImmediateRenderingFlag => {}
}
Ok(())
}
fn encoded_len(&self) -> core::primitive::usize {
Self::MIN_PACKET_SIZE + self.value_encoded_len()
}
}
impl std::fmt::Display for Metadatum {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
// TODO(b/308483171): Consider using the Display for the inner fields instead of Debug.
Metadatum::PreferredAudioContexts(v) => write!(f, "Preferred Audio Contexts: {v:?}"),
Metadatum::StreamingAudioContexts(v) => write!(f, "Streaming Audio Contexts: {v:?}"),
Metadatum::ProgramInfo(v) => write!(f, "Progaam Info: {v:?}"),
Metadatum::LanguageCode(v) => write!(f, "Language: {v:?}"),
Metadatum::CCIDList(v) => write!(f, "CCID List: {v:?}"),
Metadatum::ParentalRating(v) => write!(f, "Parental Rating: {v:?}"),
Metadatum::ProgramInfoURI(v) => write!(f, "Program Info URI: {v:?}"),
Metadatum::ExtendedMetadata { type_, metadata } => {
write!(f, "Extended Metadata: type(0x{type_:02x}) metadata({metadata:?})")
}
Metadatum::VendorSpecific { company_id, metadata } => {
write!(f, "Vendor Specific: company(0x{company_id:02x}) metadata({metadata:?})")
}
Metadatum::AudioActiveState(v) => write!(f, "Audio Active State: {v}"),
Metadatum::BroadcastAudioImmediateRenderingFlag => {
write!(f, "Broadcast Audio Immediate Rendering Flag")
}
}
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ContextType: u16 {
// 0x0000 value is prohibited.
const UNSPECIFIED = 0x0001;
const CONVERSATIONAL = 0x0002;
const MEDIA = 0x0004;
const GAME = 0x0008;
const INSTRUCTIONAL = 0x0010;
const VOICE_ASSISTANTS = 0x0020;
const LIVE = 0x0040;
const SOUND_EFFECTS = 0x0080;
const NOTIFICATIONS = 0x0100;
const RINGTONE = 0x0200;
const ALERT = 0x0400;
const EMERGENCY_ALARM = 0x0800;
}
}
impl TryFrom<u16> for ContextType {
type Error = PacketError;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if u16::count_ones(value) == 0 {
return Err(PacketError::InvalidParameter(format!("ContextType 0x0000 is prohibited")));
}
Ok(ContextType::from_bits_truncate(value))
}
}
/// Represents recommended minimum age of the viewer.
/// The numbering scheme aligns with Annex F of EN 300 707 v1.2.1
/// published by ETSI.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Rating {
NoRating,
AllAge,
// Recommended for listeners of age x years, where x is the
// value of u8 that's greater than or equal to 5.
Age(u8),
}
impl Rating {
// Minimum age that can be recommended.
const MIN_RECOMMENDED_AGE: u8 = 5;
// When recommending for listeners of age Y years,
// Subtract 3 from the recommended age to get the encoded value.
// E.g., to indicate recommended age of 8 years or older, encode 5.
const AGE_OFFSET: u8 = 3;
const fn no_rating() -> Self {
Self::NoRating
}
const fn all_age() -> Self {
Self::AllAge
}
pub fn min_recommended(age: u8) -> Result<Self, PacketError> {
if age < Self::MIN_RECOMMENDED_AGE {
return Err(PacketError::InvalidParameter(format!(
"minimum recommended age must be at least 5. Got {age}"
)));
}
Ok(Rating::Age(age))
}
}
impl From<&Rating> for u8 {
fn from(value: &Rating) -> Self {
match value {
Rating::NoRating => 0x00,
Rating::AllAge => 0x01,
Rating::Age(a) => a - Rating::AGE_OFFSET,
}
}
}
impl Decodable for Rating {
type Error = PacketError;
fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> {
if buf.len() == 0 {
return Err(PacketError::UnexpectedDataLength);
}
let rating = match buf[0] {
0x00 => Rating::NoRating,
0x01 => Rating::AllAge,
value => {
let age = value.checked_add(Self::AGE_OFFSET).ok_or(PacketError::OutOfRange)?;
Rating::min_recommended(age).unwrap()
}
};
Ok((rating, 1))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadataum_preferred_audio_contexts() {
// Encoding.
let test = Metadatum::PreferredAudioContexts(ContextType::CONVERSATIONAL);
assert_eq!(test.encoded_len(), 4);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x03, 0x01, 0x02, 0x00];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_streaming_audio_contexts() {
// Encoding.
let test = Metadatum::StreamingAudioContexts(ContextType::RINGTONE | ContextType::ALERT);
assert_eq!(test.encoded_len(), 4);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x03, 0x02, 0x00, 0x06];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_program_info() {
// Encoding.
let test = Metadatum::ProgramInfo("a".to_string());
assert_eq!(test.encoded_len(), 3);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x02, 0x03, 0x61];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 3);
}
#[test]
fn metadatum_language_code() {
// Encoding.
let test = Metadatum::LanguageCode("eng".to_string());
assert_eq!(test.encoded_len(), 5);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x04, 0x04, 0x65, 0x6E, 0x67];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 5);
}
#[test]
fn metadatum_ccid_list() {
// Encoding.
let test = Metadatum::CCIDList(vec![0x01, 0x02]);
assert_eq!(test.encoded_len(), 4);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x03, 0x05, 0x01, 0x02];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_parental_rating() {
// Encding.
let test = Metadatum::ParentalRating(Rating::Age(8));
assert_eq!(test.encoded_len(), 0x03);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x02, 0x06, 0x05];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 3);
}
#[test]
fn metadatum_vendor_specific() {
// Encoding.
let test = Metadatum::VendorSpecific { company_id: 0x00E0, metadata: vec![0x01, 0x02] };
assert_eq!(test.encoded_len(), 0x06);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x05, 0xFF, 0xE0, 0x00, 0x01, 0x02];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 6);
}
#[test]
fn metadatum_audio_active_state() {
// Encoding.
let test = Metadatum::AudioActiveState(true);
assert_eq!(test.encoded_len(), 0x03);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x02, 0x08, 0x01];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 3);
}
#[test]
fn metadatum_broadcast_audio_immediate_rendering() {
// Encoding.
let test = Metadatum::BroadcastAudioImmediateRenderingFlag;
assert_eq!(test.encoded_len(), 0x02);
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect("should not fail");
let bytes = vec![0x01, 0x09];
assert_eq!(buf, bytes);
// Decoding.
let decoded = Metadatum::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 2);
}
#[test]
fn invalid_metadataum() {
// Language code must be 3-lettered.
let test = Metadatum::LanguageCode("illegal".to_string());
let mut buf = vec![0; test.encoded_len()];
let _ = test.encode(&mut buf[..]).expect_err("should fail");
// Not enough length for Length and Type for decoding.
let buf = vec![0x03];
let _ = Metadatum::decode(&buf).expect_err("should fail");
// Not enough length for Value field for decoding.
let buf = vec![0x02, 0x01, 0x02];
let _ = Metadatum::decode(&buf).expect_err("should fail");
// Buffer length does not match Length value for decoding.
let buf = vec![0x03, 0x03, 0x61];
let _ = Metadatum::decode(&buf).expect_err("should fail");
}
#[test]
fn rating() {
let no_rating = Rating::no_rating();
let all_age = Rating::all_age();
let for_age = Rating::min_recommended(5).expect("should succeed");
assert_eq!(<&Rating as Into<u8>>::into(&no_rating), 0x00);
assert_eq!(<&Rating as Into<u8>>::into(&all_age), 0x01);
assert_eq!(<&Rating as Into<u8>>::into(&for_age), 0x02);
}
#[test]
fn decode_rating() {
let res = Rating::decode(&[0]).expect("should not fail");
assert_eq!(res.0, Rating::NoRating);
assert_eq!(res.1, 1);
let res = Rating::decode(&[1]).expect("should not fail");
assert_eq!(res.0, Rating::AllAge);
assert_eq!(res.1, 1);
let res = Rating::decode(&[2]).expect("should not fail");
assert_eq!(res.0, Rating::Age(5));
assert_eq!(res.1, 1);
let res = Rating::decode(&[10]).expect("should not fail");
assert_eq!(res.0, Rating::Age(13));
assert_eq!(res.1, 1);
}
#[test]
fn invalid_rating() {
let _ = Rating::min_recommended(4).expect_err("should have failed");
let _ = Rating::decode(&[255]).expect_err("should fail");
}
}