[bt-common] LtValue trait, CodecCapabilities Add LtValue trait and helper functions to help with defining types that are length-type-value structure. Add CompanyId, CodingFormat from core spec. Migrate core::fmt::Display to std::fmt::Display for PeerId to not conflict with core mod. Add packet_encoding mod. Bug: b/308483293 Tests: cargo test Change-Id: Ie4d4e26f83f632ee93853b62a2c1f41dc2b5c8a8 Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1320 Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-common/Cargo.toml b/rust/bt-common/Cargo.toml index 0b1c788..497acbb 100644 --- a/rust/bt-common/Cargo.toml +++ b/rust/bt-common/Cargo.toml
@@ -4,10 +4,10 @@ edition = "2021" license = "BSD-2-Clause" -[dependencies.uuid] -version = "1.4.1" -features = ["macro-diagnostics"] - [dependencies] bitflags = "2.4.0" thiserror = "1.0" + +[dependencies.uuid] +version = "1.4.1" +features = ["macro-diagnostics"]
diff --git a/rust/bt-common/src/company_id.rs b/rust/bt-common/src/company_id.rs new file mode 100644 index 0000000..78addec --- /dev/null +++ b/rust/bt-common/src/company_id.rs
@@ -0,0 +1,28 @@ +// Copyright 2023 Google LLC +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A Bluetooth Company ID, which is assigned by the Bluetotoh SIG. +/// Company identifiers are unique numbers assigned by the Bluetooth SIG to +/// member companies requesting one. Referenced in the Bluetooth Core Spec in +/// many commands and formats. See the Assigned Number Document for a reference. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CompanyId(u16); + +impl From<u16> for CompanyId { + fn from(value: u16) -> Self { + Self(value) + } +} + +impl From<CompanyId> for u16 { + fn from(value: CompanyId) -> Self { + value.0 + } +} + +impl core::fmt::Display for CompanyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Company (ID {:02x})", self.0) + } +}
diff --git a/rust/bt-common/src/core.rs b/rust/bt-common/src/core.rs index 16bb6ec..f7d8c7b 100644 --- a/rust/bt-common/src/core.rs +++ b/rust/bt-common/src/core.rs
@@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// Traits and utilities to handle length-type-value structures. +pub mod ltv; + use crate::packet_encoding::{Encodable, Error as PacketError}; /// Represents the Advertising Set ID which is 1 byte long. @@ -45,6 +48,74 @@ } } +/// Coding Format as defined by the Assigned Numbers Document. Section 2.11. +/// Referenced in the Core Spec 5.3, Volume 4, Part E, Section 7 as well as +/// various other profile specifications. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum CodingFormat { + MuLawLog, + ALawLog, + Cvsd, + Transparent, + LinearPcm, + Msbc, + Lc3, + G729a, + VendorSpecific, + Unrecognized(u8), +} + +impl From<u8> for CodingFormat { + fn from(value: u8) -> Self { + match value { + 0x00 => Self::MuLawLog, + 0x01 => Self::ALawLog, + 0x02 => Self::Cvsd, + 0x03 => Self::Transparent, + 0x04 => Self::LinearPcm, + 0x05 => Self::Msbc, + 0x06 => Self::Lc3, + 0x07 => Self::G729a, + 0xFF => Self::VendorSpecific, + x => Self::Unrecognized(x), + } + } +} + +impl From<CodingFormat> for u8 { + fn from(value: CodingFormat) -> Self { + match value { + CodingFormat::MuLawLog => 0x00, + CodingFormat::ALawLog => 0x01, + CodingFormat::Cvsd => 0x02, + CodingFormat::Transparent => 0x03, + CodingFormat::LinearPcm => 0x04, + CodingFormat::Msbc => 0x05, + CodingFormat::Lc3 => 0x06, + CodingFormat::G729a => 0x07, + CodingFormat::VendorSpecific => 0xFF, + CodingFormat::Unrecognized(x) => x, + } + } +} + +impl core::fmt::Display for CodingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CodingFormat::MuLawLog => write!(f, "µ-law log"), + CodingFormat::ALawLog => write!(f, "A-law log"), + CodingFormat::Cvsd => write!(f, "CVSD"), + CodingFormat::Transparent => write!(f, "Transparent"), + CodingFormat::LinearPcm => write!(f, "Linear PCM"), + CodingFormat::Msbc => write!(f, "mSBC"), + CodingFormat::Lc3 => write!(f, "LC3"), + CodingFormat::G729a => write!(f, "G.729A"), + CodingFormat::VendorSpecific => write!(f, "Vendor Specific"), + CodingFormat::Unrecognized(x) => write!(f, "Unrecognized ({x})"), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -60,7 +131,7 @@ #[test] fn encode_pa_interval_fails() { - let mut buf = [0; 1]; // Not enough buffer space. + let mut buf = [0; 1]; // Not enough buffer space. let interval = PaInterval(0x1004); interval.encode(&mut buf[..]).expect_err("should fail");
diff --git a/rust/bt-common/src/core/ltv.rs b/rust/bt-common/src/core/ltv.rs new file mode 100644 index 0000000..5baab7d --- /dev/null +++ b/rust/bt-common/src/core/ltv.rs
@@ -0,0 +1,313 @@ +// 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 crate::packet_encoding::{Decodable, Encodable}; + +/// Implement Ltv when a collection of types is represented in the Bluetooth +/// specifications as a length-type-value structure. They should have an +/// associated type which can be retrieved from a type byte. +pub trait LtValue: Sized { + type Type: Into<u8> + Copy; + + const NAME: &'static str; + + /// Given a type octet, return the associated Type if it is possible. + /// Returns None if the value is unrecognized. + fn type_from_octet(x: u8) -> Option<Self::Type>; + + /// Returns length bounds for the type indicated, **including** the type + /// byte. Note that the assigned numbers from the Bluetooth SIG include + /// the type byte in their Length specifications. + // TODO: use impl std::ops::RangeBounds when RPITIT is sufficiently stable + fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8>; + + /// Retrieve the type of the current value. + fn into_type(&self) -> Self::Type; + + /// The length of the encoded value, without the length and type byte. + /// This cannot be 255 in practice, as the length byte is only one octet + /// long. + fn value_encoded_len(&self) -> u8; + + /// Decodes the value from a buffer, which does not include the type or + /// length bytes. The `buf` slice length is exactly what was specified + /// for this value in the encoded source. + fn decode_value(ty: &Self::Type, buf: &[u8]) -> Result<Self, crate::packet_encoding::Error>; + + /// Encodes a value into `buf`, which is verified to be the correct length + /// as indicated by [LtValue::value_encoded_len]. + fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error>; + + /// Decode a collection of LtValue structures that are present in a buffer. + /// If it is possible to continue decoding after encountering an error, does + /// so and includes the error. If an unrecoverable error occurs, does + /// not consume the final item and the last element in the result is the + /// error. + fn decode_all(buf: &[u8]) -> (Vec<Result<Self, crate::packet_encoding::Error>>, usize) { + let mut results = Vec::new(); + let mut total_consumed = 0; + loop { + if buf.len() <= total_consumed { + return (results, std::cmp::min(buf.len(), total_consumed)); + } + let indicated_len = buf[total_consumed] as usize; + match Self::decode(&buf[total_consumed..=total_consumed + indicated_len]) { + Ok((item, consumed)) => { + results.push(Ok(item)); + total_consumed += consumed; + } + Err(e @ crate::packet_encoding::Error::UnexpectedDataLength) => { + results.push(Err(e)); + return (results, total_consumed); + } + Err(e) => { + results.push(Err(e)); + // Consume the bytes + total_consumed += indicated_len + 1; + } + } + } + } +} + +impl<T: LtValue> Encodable for T { + type Error = crate::packet_encoding::Error; + + fn encoded_len(&self) -> core::primitive::usize { + 2 + self.value_encoded_len() as usize + } + + fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { + if buf.len() < self.encoded_len() { + return Err(crate::packet_encoding::Error::BufferTooSmall); + } + buf[0] = self.value_encoded_len() + 1; + buf[1] = self.into_type().into(); + self.encode_value(&mut buf[2..self.encoded_len()])?; + Ok(()) + } +} + +impl<T: LtValue> Decodable for T { + type Error = crate::packet_encoding::Error; + + fn decode(buf: &[u8]) -> core::result::Result<(Self, usize), Self::Error> { + if buf.len() < 2 { + return Err(crate::packet_encoding::Error::UnexpectedDataLength); + } + let indicated_len = buf[0] as usize; + if buf.len() < indicated_len + 1 { + return Err(crate::packet_encoding::Error::UnexpectedDataLength); + } + + let Some(ty) = Self::type_from_octet(buf[1]) else { + return Err(crate::packet_encoding::Error::UnrecognizedType( + Self::NAME.to_owned(), + buf[1], + )); + }; + + if !Self::length_range_from_type(ty).contains(&((buf.len() - 1) as u8)) { + return Err(crate::packet_encoding::Error::UnexpectedDataLength); + } + + Ok((Self::decode_value(&ty, &buf[2..=indicated_len])?, indicated_len + 1)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Copy, Clone, PartialEq, Debug)] + enum TestType { + OneByte, + TwoBytes, + TwoBytesLittleEndian, + UnicodeString, + AlwaysError, + } + + impl From<TestType> for u8 { + fn from(value: TestType) -> Self { + match value { + TestType::OneByte => 1, + TestType::TwoBytes => 2, + TestType::TwoBytesLittleEndian => 3, + TestType::UnicodeString => 4, + TestType::AlwaysError => 0xFF, + } + } + } + + #[derive(PartialEq, Debug)] + enum TestValues { + OneByte(u8), + TwoBytes(u16), + TwoBytesLittleEndian(u16), + UnicodeString(String), + AlwaysError, + } + + impl LtValue for TestValues { + type Type = TestType; + + const NAME: &'static str = "TestValues"; + + fn type_from_octet(x: u8) -> Option<Self::Type> { + match x { + 1 => Some(TestType::OneByte), + 2 => Some(TestType::TwoBytes), + 3 => Some(TestType::TwoBytesLittleEndian), + 4 => Some(TestType::UnicodeString), + 0xFF => Some(TestType::AlwaysError), + _ => None, + } + } + + fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8> { + match ty { + TestType::OneByte => 2..=2, + TestType::TwoBytes => 3..=3, + TestType::TwoBytesLittleEndian => 3..=3, + TestType::UnicodeString => 2..=255, + // AlwaysError fields can be any length (value will be thrown away) + TestType::AlwaysError => 1..=255, + } + } + + fn into_type(&self) -> Self::Type { + match self { + TestValues::TwoBytes(_) => TestType::TwoBytes, + TestValues::TwoBytesLittleEndian(_) => TestType::TwoBytesLittleEndian, + TestValues::OneByte(_) => TestType::OneByte, + TestValues::UnicodeString(_) => TestType::UnicodeString, + TestValues::AlwaysError => TestType::AlwaysError, + } + } + + fn value_encoded_len(&self) -> u8 { + match self { + TestValues::TwoBytes(_) => 2, + TestValues::TwoBytesLittleEndian(_) => 2, + TestValues::OneByte(_) => 1, + TestValues::UnicodeString(s) => s.len() as u8, + TestValues::AlwaysError => 0, + } + } + + fn decode_value( + ty: &Self::Type, + buf: &[u8], + ) -> Result<Self, crate::packet_encoding::Error> { + match ty { + TestType::OneByte => Ok(TestValues::OneByte(buf[0])), + TestType::TwoBytes => { + Ok(TestValues::TwoBytes(u16::from_be_bytes([buf[0], buf[1]]))) + } + TestType::TwoBytesLittleEndian => { + Ok(TestValues::TwoBytesLittleEndian(u16::from_le_bytes([buf[0], buf[1]]))) + } + TestType::UnicodeString => { + Ok(TestValues::UnicodeString(String::from_utf8_lossy(buf).into_owned())) + } + TestType::AlwaysError => Err(crate::packet_encoding::Error::OutOfRange), + } + } + + fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error> { + match self { + TestValues::TwoBytes(x) => { + [buf[0], buf[1]] = x.to_be_bytes(); + } + TestValues::TwoBytesLittleEndian(x) => { + [buf[0], buf[1]] = x.to_le_bytes(); + } + TestValues::OneByte(x) => buf[0] = *x, + TestValues::UnicodeString(s) => { + buf.copy_from_slice(s.as_bytes()); + } + TestValues::AlwaysError => { + return Err(crate::packet_encoding::Error::InvalidParameter("test".to_owned())); + } + } + Ok(()) + } + } + + #[test] + fn decode_twobytes() { + let encoded = [0x03, 0x02, 0x10, 0x01, 0x03, 0x03, 0x10, 0x01]; + let (decoded, consumed) = TestValues::decode_all(&encoded); + assert_eq!(consumed, encoded.len()); + assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097))); + assert_eq!(decoded[1], Ok(TestValues::TwoBytesLittleEndian(272))); + } + + #[test] + fn decode_unrecognized() { + let encoded = [0x03, 0x02, 0x10, 0x01, 0x03, 0x06, 0x10, 0x01]; + let (decoded, consumed) = TestValues::decode_all(&encoded); + assert_eq!(consumed, encoded.len()); + assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097))); + assert_eq!( + decoded[1], + Err(crate::packet_encoding::Error::UnrecognizedType("TestValues".to_owned(), 6)) + ); + } + + #[track_caller] + fn u8char(c: char) -> u8 { + c.try_into().unwrap() + } + + #[test] + fn decode_variable_lengths() { + let encoded = [ + 0x03, + 0x02, + 0x10, + 0x01, + 0x0A, + 0x04, + u8char('B'), + u8char('l'), + u8char('u'), + u8char('e'), + u8char('t'), + u8char('o'), + u8char('o'), + u8char('t'), + u8char('h'), + 0x02, + 0x01, + 0x01, + ]; + let (decoded, consumed) = TestValues::decode_all(&encoded); + assert_eq!(consumed, encoded.len()); + assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097))); + assert_eq!(decoded[1], Ok(TestValues::UnicodeString("Bluetooth".to_owned()))); + assert_eq!(decoded[2], Ok(TestValues::OneByte(1))); + } + + #[test] + fn decode_with_error() { + let encoded = [0x03, 0x02, 0x10, 0x01, 0x02, 0xFF, 0xFF, 0x02, 0x01, 0x03]; + let (decoded, consumed) = TestValues::decode_all(&encoded); + assert_eq!(consumed, encoded.len()); + assert_eq!(decoded[0], Ok(TestValues::TwoBytes(4097))); + assert_eq!(decoded[1], Err(crate::packet_encoding::Error::OutOfRange),); + assert_eq!(decoded[2], Ok(TestValues::OneByte(3))); + } + + #[test] + fn encode_with_error() { + let mut buf = [0; 10]; + let value = TestValues::AlwaysError; + assert!(matches!( + value.encode(&mut buf), + Err(crate::packet_encoding::Error::InvalidParameter(_)), + )); + } +}
diff --git a/rust/bt-common/src/generic_audio.rs b/rust/bt-common/src/generic_audio.rs index 904506c..c2f72f0 100644 --- a/rust/bt-common/src/generic_audio.rs +++ b/rust/bt-common/src/generic_audio.rs
@@ -2,4 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +pub mod codec_capabilities; + pub mod metadata_ltv;
diff --git a/rust/bt-common/src/generic_audio/codec_capabilities.rs b/rust/bt-common/src/generic_audio/codec_capabilities.rs new file mode 100644 index 0000000..eeff6cd --- /dev/null +++ b/rust/bt-common/src/generic_audio/codec_capabilities.rs
@@ -0,0 +1,256 @@ +// 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 std::collections::HashSet; +use std::ops::RangeBounds; + +use crate::core::ltv::LtValue; + +#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] +pub enum CodecCapabilityType { + SupportedSamplingFrequencies, + SupportedFrameDurations, + SupportedAudioChannelCounts, + SupportedOctetsPerCodecFrame, + SupportedMaxCodecFramesPerSdu, +} + +impl From<CodecCapabilityType> for u8 { + fn from(value: CodecCapabilityType) -> Self { + match value { + CodecCapabilityType::SupportedSamplingFrequencies => 1, + CodecCapabilityType::SupportedFrameDurations => 2, + CodecCapabilityType::SupportedAudioChannelCounts => 3, + CodecCapabilityType::SupportedOctetsPerCodecFrame => 4, + CodecCapabilityType::SupportedMaxCodecFramesPerSdu => 5, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum FrameDurationSupport { + SevenFiveMs, + TenMs, + BothNoPreference, + PreferSevenFiveMs, + PreferTenMs, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum SamplingFrequency { + F8000Hz, + F11025Hz, + F16000Hz, + F22050Hz, + F24000Hz, + F32000Hz, + F44100Hz, + F48000Hz, + F88200Hz, + F96000Hz, + F176400Hz, + F192000Hz, + F384000Hz, +} + +impl SamplingFrequency { + fn bitpos(&self) -> u8 { + match self { + SamplingFrequency::F8000Hz => 0, + SamplingFrequency::F11025Hz => 1, + SamplingFrequency::F16000Hz => 2, + SamplingFrequency::F22050Hz => 3, + SamplingFrequency::F24000Hz => 4, + SamplingFrequency::F32000Hz => 5, + SamplingFrequency::F44100Hz => 6, + SamplingFrequency::F48000Hz => 7, + SamplingFrequency::F88200Hz => 8, + SamplingFrequency::F96000Hz => 9, + SamplingFrequency::F176400Hz => 10, + SamplingFrequency::F192000Hz => 11, + SamplingFrequency::F384000Hz => 12, + } + } + + fn from_bitpos(value: u8) -> Option<Self> { + match value { + 0 => Some(SamplingFrequency::F8000Hz), + 1 => Some(SamplingFrequency::F11025Hz), + 2 => Some(SamplingFrequency::F16000Hz), + 3 => Some(SamplingFrequency::F22050Hz), + 4 => Some(SamplingFrequency::F24000Hz), + 5 => Some(SamplingFrequency::F32000Hz), + 6 => Some(SamplingFrequency::F44100Hz), + 7 => Some(SamplingFrequency::F48000Hz), + 8 => Some(SamplingFrequency::F88200Hz), + 9 => Some(SamplingFrequency::F96000Hz), + 10 => Some(SamplingFrequency::F176400Hz), + 11 => Some(SamplingFrequency::F192000Hz), + 12 => Some(SamplingFrequency::F384000Hz), + _ => None, + } + } +} + +/// Codec Capability LTV Structures +/// +/// Defined in Assigned Numbers Section 6.12.4. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum CodecCapability { + SupportedSamplingFrequencies(std::collections::HashSet<SamplingFrequency>), + SupportedFrameDurations(FrameDurationSupport), + SupportedAudioChannelCounts(std::collections::HashSet<u8>), + SupportedMaxCodecFramesPerSdu(u8), + SupportedOctetsPerCodecFrame { min: u16, max: u16 }, +} + +impl LtValue for CodecCapability { + type Type = CodecCapabilityType; + + const NAME: &'static str = "Codec Compatability"; + + fn type_from_octet(x: u8) -> Option<Self::Type> { + match x { + 1 => Some(CodecCapabilityType::SupportedSamplingFrequencies), + 2 => Some(CodecCapabilityType::SupportedFrameDurations), + 3 => Some(CodecCapabilityType::SupportedAudioChannelCounts), + 4 => Some(CodecCapabilityType::SupportedOctetsPerCodecFrame), + 5 => Some(CodecCapabilityType::SupportedMaxCodecFramesPerSdu), + _ => None, + } + } + + fn length_range_from_type(ty: Self::Type) -> std::ops::RangeInclusive<u8> { + match ty { + CodecCapabilityType::SupportedAudioChannelCounts => 2..=2, + CodecCapabilityType::SupportedFrameDurations => 2..=2, + CodecCapabilityType::SupportedMaxCodecFramesPerSdu => 2..=2, + CodecCapabilityType::SupportedOctetsPerCodecFrame => 5..=5, + CodecCapabilityType::SupportedSamplingFrequencies => 3..=3, + } + } + + fn into_type(&self) -> Self::Type { + match self { + CodecCapability::SupportedAudioChannelCounts(_) => { + CodecCapabilityType::SupportedAudioChannelCounts + } + CodecCapability::SupportedFrameDurations(_) => { + CodecCapabilityType::SupportedFrameDurations + } + CodecCapability::SupportedMaxCodecFramesPerSdu(_) => { + CodecCapabilityType::SupportedMaxCodecFramesPerSdu + } + CodecCapability::SupportedOctetsPerCodecFrame { .. } => { + CodecCapabilityType::SupportedOctetsPerCodecFrame + } + CodecCapability::SupportedSamplingFrequencies(_) => { + CodecCapabilityType::SupportedSamplingFrequencies + } + } + } + + fn value_encoded_len(&self) -> u8 { + // All the CodecCapabilities are constant length. Remove the type octet. + let range = Self::length_range_from_type(self.into_type()); + let std::ops::Bound::Included(len) = range.start_bound() else { + unreachable!(); + }; + (len - 1).into() + } + + fn encode_value(&self, buf: &mut [u8]) -> Result<(), crate::packet_encoding::Error> { + match self { + CodecCapability::SupportedAudioChannelCounts(counts) => { + buf[0] = counts + .iter() + .fold(0, |acc, count| if *count > 8 { acc } else { acc | (1 << (*count - 1)) }); + } + CodecCapability::SupportedFrameDurations(support) => { + buf[0] = match support { + FrameDurationSupport::SevenFiveMs => 0b000001, + FrameDurationSupport::TenMs => 0b000010, + FrameDurationSupport::BothNoPreference => 0b000011, + FrameDurationSupport::PreferSevenFiveMs => 0b010011, + FrameDurationSupport::PreferTenMs => 0b100011, + }; + } + CodecCapability::SupportedMaxCodecFramesPerSdu(max) => { + buf[0] = *max; + } + CodecCapability::SupportedOctetsPerCodecFrame { min, max } => { + let min_bytes = min.to_le_bytes(); + buf[0..=1].copy_from_slice(&min_bytes); + + let max_bytes = max.to_le_bytes(); + buf[2..=3].copy_from_slice(&max_bytes); + } + CodecCapability::SupportedSamplingFrequencies(supported) => { + let sup_bytes = supported + .iter() + .fold(0u16, |acc, freq| acc | (1 << freq.bitpos())) + .to_le_bytes(); + buf[0..=1].copy_from_slice(&sup_bytes) + } + }; + Ok(()) + } + + fn decode_value( + ty: &CodecCapabilityType, + buf: &[u8], + ) -> Result<Self, crate::packet_encoding::Error> { + match ty { + CodecCapabilityType::SupportedAudioChannelCounts => { + let mut supported = HashSet::new(); + for bitpos in 0..8 { + if (buf[0] & (1 << bitpos)) != 0 { + supported.insert(bitpos + 1); + } + } + Ok(Self::SupportedAudioChannelCounts(supported)) + } + CodecCapabilityType::SupportedFrameDurations => { + let support = match buf[0] & 0b110011 { + 0b000001 => FrameDurationSupport::SevenFiveMs, + 0b000010 => FrameDurationSupport::TenMs, + 0b000011 => FrameDurationSupport::BothNoPreference, + 0b010011 => FrameDurationSupport::PreferSevenFiveMs, + 0b100011 => FrameDurationSupport::PreferTenMs, + _ => { + return Err(crate::packet_encoding::Error::InvalidParameter(format!( + "Unrecognized bit pattern: {:#b}", + buf[0] + ))); + } + }; + Ok(Self::SupportedFrameDurations(support)) + } + CodecCapabilityType::SupportedMaxCodecFramesPerSdu => { + Ok(Self::SupportedMaxCodecFramesPerSdu(buf[0])) + } + CodecCapabilityType::SupportedOctetsPerCodecFrame => { + let min = u16::from_le_bytes([buf[0], buf[1]]); + let max = u16::from_le_bytes([buf[2], buf[3]]); + Ok(Self::SupportedOctetsPerCodecFrame { min, max }) + } + CodecCapabilityType::SupportedSamplingFrequencies => { + let bitflags = u16::from_le_bytes([buf[0], buf[1]]); + let mut supported = HashSet::new(); + for bitpos in 0..13 { + if bitflags & (1 << bitpos) != 0 { + let Some(f) = SamplingFrequency::from_bitpos(bitpos) else { + continue; + }; + let _ = supported.insert(f); + } + } + Ok(Self::SupportedSamplingFrequencies(supported)) + } + } + } +} + +#[cfg(test)] +mod tests {}
diff --git a/rust/bt-common/src/generic_audio/metadata_ltv.rs b/rust/bt-common/src/generic_audio/metadata_ltv.rs index e29b636..3630f4e 100644 --- a/rust/bt-common/src/generic_audio/metadata_ltv.rs +++ b/rust/bt-common/src/generic_audio/metadata_ltv.rs
@@ -31,10 +31,10 @@ } else { Err(PacketError::UnexpectedDataLength) } - } + } - // Total size range including all Length, Type, and Value parameters. - fn total_size_range(&self) -> std::ops::Range<usize> { + // 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, @@ -44,7 +44,7 @@ Type::AudioActiveState => 3..4, Type::BroadcastAudioImmediateRenderingFlag => 2..3, } - } + } } impl TryFrom<u8> for Type { @@ -92,13 +92,13 @@ 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. + // 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, @@ -124,10 +124,12 @@ Metadatum::CCIDList(_) => Type::CCIDList, Metadatum::ParentalRating(_) => Type::ParentalRating, Metadatum::ProgramInfoURI(_) => Type::ProgramInfoURI, - Metadatum::ExtendedMetadata{..} => Type::ExtendedMetadata, - Metadatum::VendorSpecific{..} => Type::VendorSpecific, + Metadatum::ExtendedMetadata { .. } => Type::ExtendedMetadata, + Metadatum::VendorSpecific { .. } => Type::VendorSpecific, Metadatum::AudioActiveState(_) => Type::AudioActiveState, - Metadatum::BroadcastAudioImmediateRenderingFlag => Type::BroadcastAudioImmediateRenderingFlag, + Metadatum::BroadcastAudioImmediateRenderingFlag => { + Type::BroadcastAudioImmediateRenderingFlag + } } } } @@ -148,15 +150,17 @@ 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()))?; + 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 { + } 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}")))?; + 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 { @@ -164,55 +168,49 @@ } } Type::Language => { - let code = String::from_utf8(buf[2..total_len].to_vec()).map_err(|e| PacketError::InvalidParameter(format!("{e}")))?; + 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::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![] - }; + 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, - } + Self::ExtendedMetadata { type_: type_or_id, metadata: data } } else { - Self::VendorSpecific{ - company_id: type_or_id, - metadata: data, - } + 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, + } + Type::BroadcastAudioImmediateRenderingFlag => { + Self::BroadcastAudioImmediateRenderingFlag + } }; - return Ok((m, total_len)) + 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. + /// 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). + // 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()))?; @@ -231,7 +229,7 @@ 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} => { + 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()); } @@ -261,7 +259,7 @@ 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} => { + Metadatum::ExtendedMetadata { type_, metadata } => { write!(f, "Extended Metadata: type(0x{type_:02x}) metadata({metadata:?})") } Metadatum::VendorSpecific { company_id, metadata } => { @@ -303,7 +301,6 @@ } Ok(ContextType::from_bits_truncate(value)) } - } /// Represents recommended minimum age of the viewer. @@ -337,7 +334,9 @@ 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}"))); + return Err(PacketError::InvalidParameter(format!( + "minimum recommended age must be at least 5. Got {age}" + ))); } Ok(Rating::Age(age)) } @@ -364,10 +363,9 @@ 0x00 => Rating::NoRating, 0x01 => Rating::AllAge, value => { - let age = value.checked_add(Self::AGE_OFFSET) - .ok_or(PacketError::OutOfRange)?; + let age = value.checked_add(Self::AGE_OFFSET).ok_or(PacketError::OutOfRange)?; Rating::min_recommended(age).unwrap() - }, + } }; Ok((rating, 1)) } @@ -482,7 +480,7 @@ #[test] fn metadatum_vendor_specific() { // Encoding. - let test = Metadatum::VendorSpecific{company_id: 0x00E0, metadata: vec![0x01, 0x02]}; + 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");
diff --git a/rust/bt-common/src/lib.rs b/rust/bt-common/src/lib.rs index 67e9360..232ec7d 100644 --- a/rust/bt-common/src/lib.rs +++ b/rust/bt-common/src/lib.rs
@@ -4,22 +4,33 @@ extern crate core as rust_core; -/// Peers are identified by ids, which should be treated as opaque by service libraries. -/// Stack implementations should ensure that each PeerId identifies a single peer over a single -/// instance of the stack - a [`bt_gatt::Central::connect`] should always attempt to connect to the -/// same peer as long as the PeerId was retrieved after the `Central` was instantiated. -/// PeerIds can be valid longer than that (often if the peer is bonded) +/// Peers are identified by ids, which should be treated as opaque by service +/// libraries. Stack implementations should ensure that each PeerId identifies a +/// single peer over a single instance of the stack - a +/// [`bt_gatt::Central::connect`] should always attempt to connect to the +/// same peer as long as the PeerId was retrieved after the `Central` was +/// instantiated. PeerIds can be valid longer than that (often if the peer is +/// bonded) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PeerId(pub u64); impl rust_core::fmt::Display for PeerId { - fn fmt(&self, f: &mut rust_core::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + fn fmt( + &self, + f: &mut rust_core::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { write!(f, "{:x}", self.0) } } pub mod core; + +pub mod company_id; +pub use company_id::CompanyId; + pub mod generic_audio; + pub mod packet_encoding; + pub mod uuid; pub use crate::uuid::Uuid;
diff --git a/rust/bt-common/src/packet_encoding.rs b/rust/bt-common/src/packet_encoding.rs index fa01309..978430b 100644 --- a/rust/bt-common/src/packet_encoding.rs +++ b/rust/bt-common/src/packet_encoding.rs
@@ -9,12 +9,13 @@ pub trait Decodable: ::core::marker::Sized { type Error; - /// Decodes into a new object with the number of bytes that were decoded, or returns an error. + /// Decodes into a new object with the number of bytes that were decoded, or + /// returns an error. fn decode(buf: &[u8]) -> ::core::result::Result<(Self, usize), Self::Error>; } /// An encodable type can write itself into a byte buffer. -pub trait Encodable: ::core::marker::Sized { +pub trait Encodable { type Error; /// Returns the number of bytes necessary to encode |self|. @@ -38,4 +39,7 @@ #[error("Buffer being decoded is invalid length")] UnexpectedDataLength, + + #[error("Unrecognized type for {0}: {1}")] + UnrecognizedType(String, u8), }
diff --git a/rust/bt-common/src/uuid.rs b/rust/bt-common/src/uuid.rs index a47f790..2f11ed8 100644 --- a/rust/bt-common/src/uuid.rs +++ b/rust/bt-common/src/uuid.rs
@@ -7,7 +7,8 @@ pub struct Uuid(uuid::Uuid); impl Uuid { - // Non-changing parts of the Bluetooth Base UUID, for easy comparison and construction. + // Non-changing parts of the Bluetooth Base UUID, for easy comparison and + // construction. const BASE_UUID_B_PART: u16 = 0x0000; const BASE_UUID_C_PART: u16 = 0x1000; const BASE_UUID_D_PART: [u8; 8] = [0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB];