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