| // 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"); |
| } |
| } |