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