blob: 5e952153db7b007fc70205ee75e069014c20c2f3 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::core::ltv::LtValue;
use crate::packet_encoding::Error as PacketError;
use crate::{decodable_enum, CompanyId};
use crate::generic_audio::ContextType;
decodable_enum! {
pub enum MetadataType<u8, PacketError, OutOfRange> {
PreferredAudioContexts = 0x01,
StreamingAudioContexts = 0x02,
ProgramInfo = 0x03,
Language = 0x04,
CCIDList = 0x05,
ParentalRating = 0x06,
ProgramInfoURI = 0x07,
AudioActiveState = 0x08,
BroadcastAudioImmediateRenderingFlag = 0x09,
ExtendedMetadata = 0xfe,
VendorSpecific = 0xff,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Metadata {
PreferredAudioContexts(Vec<ContextType>),
StreamingAudioContexts(Vec<ContextType>),
ProgramInfo(String),
/// 3 byte, lower case language code as defined in ISO 639-­3
Language(String),
CCIDList(Vec<u8>),
ParentalRating(Rating),
ProgramInfoURI(String),
ExtendedMetadata {
type_: u16,
data: Vec<u8>,
},
VendorSpecific {
company_id: CompanyId,
data: Vec<u8>,
},
AudioActiveState(bool),
// Flag for Broadcast Audio Immediate Rendering.
BroadcastAudioImmediateRenderingFlag,
}
impl LtValue for Metadata {
type Type = MetadataType;
const NAME: &'static str = "Metadata";
fn type_from_octet(x: u8) -> Option<Self::Type> {
x.try_into().ok()
}
fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8> {
match ty {
MetadataType::PreferredAudioContexts | MetadataType::StreamingAudioContexts => 3..=3,
MetadataType::ProgramInfo | MetadataType::CCIDList | MetadataType::ProgramInfoURI => {
1..=u8::MAX
}
MetadataType::Language => 4..=4,
MetadataType::ParentalRating => 2..=2,
MetadataType::ExtendedMetadata | MetadataType::VendorSpecific => 3..=u8::MAX,
MetadataType::AudioActiveState => 2..=2,
MetadataType::BroadcastAudioImmediateRenderingFlag => 1..=1,
}
}
fn into_type(&self) -> Self::Type {
match self {
Metadata::PreferredAudioContexts(_) => MetadataType::PreferredAudioContexts,
Metadata::StreamingAudioContexts(_) => MetadataType::StreamingAudioContexts,
Metadata::ProgramInfo(_) => MetadataType::ProgramInfo,
Metadata::Language(_) => MetadataType::Language,
Metadata::CCIDList(_) => MetadataType::CCIDList,
Metadata::ParentalRating(_) => MetadataType::ParentalRating,
Metadata::ProgramInfoURI(_) => MetadataType::ProgramInfoURI,
Metadata::ExtendedMetadata { .. } => MetadataType::ExtendedMetadata,
Metadata::VendorSpecific { .. } => MetadataType::VendorSpecific,
Metadata::AudioActiveState(_) => MetadataType::AudioActiveState,
Metadata::BroadcastAudioImmediateRenderingFlag => {
MetadataType::BroadcastAudioImmediateRenderingFlag
}
}
}
fn value_encoded_len(&self) -> u8 {
match self {
Metadata::PreferredAudioContexts(_) => 2,
Metadata::StreamingAudioContexts(_) => 2,
Metadata::ProgramInfo(value) => value.len() as u8,
Metadata::Language(_) => 3,
Metadata::CCIDList(ccids) => ccids.len() as u8,
Metadata::ParentalRating(_) => 1,
Metadata::ProgramInfoURI(uri) => uri.len() as u8,
Metadata::ExtendedMetadata { type_: _, data } => 2 + data.len() as u8,
Metadata::VendorSpecific { company_id: _, data } => 2 + data.len() as u8,
Metadata::AudioActiveState(_) => 1,
Metadata::BroadcastAudioImmediateRenderingFlag => 0,
}
}
fn decode_value(ty: &Self::Type, buf: &[u8]) -> Result<Self, crate::packet_encoding::Error> {
let m: Metadata = match ty {
MetadataType::PreferredAudioContexts | MetadataType::StreamingAudioContexts => {
let context_type =
ContextType::from_bits(u16::from_le_bytes([buf[0], buf[1]])).collect();
if ty == &MetadataType::PreferredAudioContexts {
Self::PreferredAudioContexts(context_type)
} else {
Self::StreamingAudioContexts(context_type)
}
}
MetadataType::ProgramInfo | MetadataType::ProgramInfoURI => {
let value = String::from_utf8(buf[..].to_vec())
.map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
if ty == &MetadataType::ProgramInfo {
Self::ProgramInfo(value)
} else {
Self::ProgramInfoURI(value)
}
}
MetadataType::Language => {
let code = String::from_utf8(buf[..].to_vec())
.map_err(|e| PacketError::InvalidParameter(format!("{e}")))?;
Self::Language(code)
}
MetadataType::CCIDList => Self::CCIDList(buf[..].to_vec()),
MetadataType::ParentalRating => Self::ParentalRating(buf[0].into()),
MetadataType::ExtendedMetadata | MetadataType::VendorSpecific => {
let type_or_id = u16::from_le_bytes(buf[0..2].try_into().unwrap());
let data = if buf.len() >= 2 { buf[2..].to_vec() } else { vec![] };
if ty == &MetadataType::ExtendedMetadata {
Self::ExtendedMetadata { type_: type_or_id, data }
} else {
Self::VendorSpecific { company_id: type_or_id.into(), data }
}
}
MetadataType::AudioActiveState => Self::AudioActiveState(buf[0] != 0),
MetadataType::BroadcastAudioImmediateRenderingFlag => {
Self::BroadcastAudioImmediateRenderingFlag
}
};
Ok(m)
}
fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error> {
match self {
Self::PreferredAudioContexts(type_) | Self::StreamingAudioContexts(type_) => {
[buf[0], buf[1]] = ContextType::to_bits(type_.iter()).to_le_bytes();
}
Self::ProgramInfo(value) | Self::ProgramInfoURI(value) => {
buf.copy_from_slice(value.as_bytes())
}
Self::Language(value) if value.len() != 3 => {
return Err(PacketError::InvalidParameter(format!("{self}")));
}
Self::Language(value) => buf.copy_from_slice(&value.as_bytes()[..3]),
Self::CCIDList(value) => buf.copy_from_slice(&value.as_slice()),
Self::ParentalRating(value) => buf[0] = value.into(),
Self::ExtendedMetadata { type_, data } => {
buf[0..2].copy_from_slice(&type_.to_le_bytes());
buf[2..].copy_from_slice(&data.as_slice());
}
Self::VendorSpecific { company_id, data } => {
[buf[0], buf[1]] = company_id.to_le_bytes();
buf[2..].copy_from_slice(&data.as_slice());
}
Self::AudioActiveState(value) => buf[0] = *value as u8,
Self::BroadcastAudioImmediateRenderingFlag => {}
}
Ok(())
}
}
impl std::fmt::Display for Metadata {
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.
Metadata::PreferredAudioContexts(v) => write!(f, "Preferred Audio Contexts: {v:?}"),
Metadata::StreamingAudioContexts(v) => write!(f, "Streaming Audio Contexts: {v:?}"),
Metadata::ProgramInfo(v) => write!(f, "Progaam Info: {v:?}"),
Metadata::Language(v) => write!(f, "Language: {v:?}"),
Metadata::CCIDList(v) => write!(f, "CCID List: {v:?}"),
Metadata::ParentalRating(v) => write!(f, "Parental Rating: {v:?}"),
Metadata::ProgramInfoURI(v) => write!(f, "Program Info URI: {v:?}"),
Metadata::ExtendedMetadata { type_, data } => {
write!(f, "Extended Metadata: type(0x{type_:02x}) data({data:?})")
}
Metadata::VendorSpecific { company_id, data } => {
write!(f, "Vendor Specific: {company_id} data({data:?})")
}
Metadata::AudioActiveState(v) => write!(f, "Audio Active State: {v}"),
Metadata::BroadcastAudioImmediateRenderingFlag => {
write!(f, "Broadcast Audio Immediate Rendering Flag")
}
}
}
}
/// 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, Eq)]
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(u16),
}
impl Rating {
// Minimum age that can be recommended.
const MIN_RECOMMENDED_AGE: u16 = 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: u16 = 3;
pub const fn no_rating() -> Self {
Self::NoRating
}
pub const fn all_age() -> Self {
Self::AllAge
}
pub fn min_recommended(age: u16) -> Result<Self, PacketError> {
if age < Self::MIN_RECOMMENDED_AGE {
return Err(PacketError::InvalidParameter(format!(
"minimum recommended age must be at least 5. Got {age}"
)));
}
// We can represent up to 255 + 3 (age 258) using this encoding.
if age > u8::MAX as u16 + Self::AGE_OFFSET {
return Err(PacketError::OutOfRange);
}
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) as u8,
}
}
}
impl From<u8> for Rating {
fn from(value: u8) -> Self {
match value {
0x00 => Rating::NoRating,
0x01 => Rating::AllAge,
value => {
let age = value as u16 + Self::AGE_OFFSET;
Rating::min_recommended(age).unwrap()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packet_encoding::{Decodable, Encodable};
#[test]
fn metadataum_preferred_audio_contexts() {
// Encoding.
let test = Metadata::PreferredAudioContexts(vec![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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_streaming_audio_contexts() {
// Encoding.
let test =
Metadata::StreamingAudioContexts(vec![ContextType::Ringtone, ContextType::Alerts]);
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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_program_info() {
// Encoding.
let test = Metadata::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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 3);
}
#[test]
fn metadatum_language_code() {
// Encoding.
let test = Metadata::Language("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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 5);
}
#[test]
fn metadatum_ccid_list() {
// Encoding.
let test = Metadata::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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 4);
}
#[test]
fn metadatum_parental_rating() {
// Encding.
let test = Metadata::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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 3);
}
#[test]
fn metadatum_vendor_specific() {
// Encoding.
let test = Metadata::VendorSpecific { company_id: 0x00E0.into(), data: 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 = Metadata::decode(&buf).expect("should succeed");
assert_eq!(decoded.0, test);
assert_eq!(decoded.1, 6);
}
#[test]
fn metadatum_audio_active_state() {
// Encoding.
let test = Metadata::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 = Metadata::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 = Metadata::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 = Metadata::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 = Metadata::Language("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 _ = Metadata::decode(&buf).expect_err("should fail");
// Not enough length for Value field for decoding.
let buf = vec![0x02, 0x01, 0x02];
let _ = Metadata::decode(&buf).expect_err("should fail");
// Buffer length does not match Length value for decoding.
let buf = vec![0x03, 0x03, 0x61];
let _ = Metadata::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::from(0);
assert_eq!(res, Rating::NoRating);
let res = Rating::from(1);
assert_eq!(res, Rating::AllAge);
let res = Rating::from(2);
assert_eq!(res, Rating::Age(5));
let res = Rating::from(255);
assert_eq!(res, Rating::Age(258));
let res = Rating::min_recommended(258).unwrap();
assert_eq!(255u8, (&res).into());
}
#[test]
fn invalid_rating() {
let _ = Rating::min_recommended(4).expect_err("should have failed");
let _ = Rating::min_recommended(350).expect_err("can't recommend for elves");
}
}