-
Notifications
You must be signed in to change notification settings - Fork 10
Add Extended Inquiry Response (EIR) parser. #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,274 @@ | ||
| //! This module provides functionality to parse Extended Inquiry Response (EIR) Data. | ||
| //! | ||
| //! This code follows Bluetooth Core Specification (CS) v5.2 and Core | ||
| //! Specification Supplement (CSS) v9 | ||
| //! (https://www.bluetooth.com/specifications/bluetooth-core-specification/). | ||
|
|
||
| use bytes::buf::BufExt; | ||
| use bytes::*; | ||
| use enumflags2::BitFlags; | ||
| use num_derive::FromPrimitive; | ||
| use num_traits::FromPrimitive; | ||
| use thiserror::Error; | ||
|
|
||
| /// See CSS v9 Part A 1.3.2 for flag meaning. | ||
| #[repr(u8)] | ||
| #[derive(Debug, Copy, Clone, BitFlags, Eq, PartialEq)] | ||
| pub enum EIRFlags { | ||
| LELimitedDiscoverableMode = 1 << 0, | ||
| LEGeneralDiscoverableMode = 1 << 1, | ||
| BREDRNotSupported = 1 << 2, | ||
| ControllerSimultaneousLEBREDR = 1 << 3, | ||
| HostSimultaneousLEBREDR = 1 << 4, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct EIRName { | ||
| name: String, | ||
| complete: bool, | ||
| } | ||
|
|
||
| impl EIRName { | ||
| fn short_name(name: String) -> Self { | ||
| EIRName { | ||
| name: name, | ||
| complete: false, | ||
| } | ||
| } | ||
| fn complete_name(name: String) -> Self { | ||
| EIRName { | ||
| name: name, | ||
| complete: true, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct ManufacturerSpecificData { | ||
| company_identifier_code: u16, | ||
| data: Bytes, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct EIR { | ||
| flags: Option<BitFlags<EIRFlags>>, | ||
| uuid16: Vec<u16>, | ||
| uuid32: Vec<u32>, | ||
| uuid128: Vec<u128>, | ||
| name: Option<EIRName>, | ||
| tx_power_level: Vec<i8>, | ||
| uri: Vec<String>, | ||
| manufacturer_specific_data: Vec<ManufacturerSpecificData>, | ||
| } | ||
|
|
||
| impl EIR { | ||
| fn new() -> Self { | ||
| EIR { | ||
| flags: None, | ||
| uuid16: Vec::new(), | ||
| uuid32: Vec::new(), | ||
| uuid128: Vec::new(), | ||
| name: None, | ||
| tx_power_level: Vec::new(), | ||
| uri: Vec::new(), | ||
| manufacturer_specific_data: Vec::new(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Error, Debug)] | ||
| pub enum EIRError { | ||
| #[error("More than one flag block found.")] | ||
| RepeatedFlag, | ||
| #[error("More than one name block found.")] | ||
| RepeatedName, | ||
| #[error("Unexpected data length {}.", len)] | ||
| UnexpectedDataLength { len: usize }, | ||
| #[error("UTF-8 encoding error in URI.")] | ||
| InvalidURI, | ||
| } | ||
|
|
||
| #[repr(u8)] | ||
| #[derive(FromPrimitive)] | ||
| #[non_exhaustive] | ||
| enum EIRDataTypes { | ||
|
ruediger marked this conversation as resolved.
|
||
| Flags = 0x01, | ||
| UUID16Incomplete = 0x02, | ||
| UUID16Complete = 0x03, | ||
| UUID32Incomplete = 0x04, | ||
| UUID32Complete = 0x05, | ||
| UUID128Incomplete = 0x06, | ||
| UUID128Complete = 0x07, | ||
| NameShort = 0x08, | ||
| NameComplete = 0x09, | ||
| TxPowerLevel = 0x0A, | ||
| URI = 0x24, | ||
| ManufacturerSpecificData = 0xFF, | ||
| } | ||
|
|
||
| /// Parses Extended Inquiry Response (EIR) Data. | ||
| /// | ||
| /// This will silently skip any unknown data types or URIs using | ||
| /// encoded schemes. | ||
| pub fn parse_eir<T: Buf>(mut buf: T) -> Result<EIR, EIRError> { | ||
| let mut eir = EIR::new(); | ||
|
|
||
| while buf.has_remaining() { | ||
| // Bluetooth Specification Version 5.2, Vol 3, part C, 8 EXTENDED INQUIRY RESPONSE DATA FORMAT | ||
| // [EIRStructure0, EIRStructure1, ..., EIRStructureN, 0...] | ||
| // EIRStructure: | ||
| // -- 1 octet -- -- Length octets -- | ||
| // [ Length , Data ] | ||
| // Data: | ||
| // -- n octet -- -- Length - n octets -- | ||
| // [ EIRDataType , EIRData ] | ||
| let len = buf.get_u8(); | ||
| if len == 0 { | ||
| break; | ||
| } | ||
|
|
||
| // data types are all 1 octet | ||
| let data_type = buf.get_u8(); | ||
| let mut data = buf.take((len - 1).into()); | ||
|
|
||
| // Core Specification Supplement | ||
| // EIRDataType values https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ | ||
| match FromPrimitive::from_u8(data_type) { | ||
| Some(EIRDataTypes::Flags) => { | ||
| if eir.flags.is_some() { | ||
| return Err(EIRError::RepeatedFlag); | ||
| } | ||
| eir.flags = Some(BitFlags::from_bits_truncate(data.get_u8())); | ||
| } | ||
| Some(EIRDataTypes::UUID16Incomplete) | Some(EIRDataTypes::UUID16Complete) => { | ||
| if data.remaining() % 2 != 0 { | ||
| return Err(EIRError::UnexpectedDataLength { | ||
| len: data.remaining(), | ||
| }); | ||
| } | ||
| while data.has_remaining() { | ||
| eir.uuid16.push(data.get_u16_le()); | ||
| } | ||
| } | ||
| Some(EIRDataTypes::UUID32Incomplete) | Some(EIRDataTypes::UUID32Complete) => { | ||
| if data.remaining() % 4 != 0 { | ||
| return Err(EIRError::UnexpectedDataLength { | ||
| len: data.remaining(), | ||
| }); | ||
| } | ||
| while data.has_remaining() { | ||
| eir.uuid32.push(data.get_u32_le()); | ||
| } | ||
| } | ||
| Some(EIRDataTypes::UUID128Incomplete) | Some(EIRDataTypes::UUID128Complete) => { | ||
| if data.remaining() % 16 != 0 { | ||
| return Err(EIRError::UnexpectedDataLength { | ||
| len: data.remaining(), | ||
| }); | ||
| } | ||
| while data.has_remaining() { | ||
| eir.uuid128.push(data.get_u128_le()); | ||
| } | ||
| } | ||
| Some(EIRDataTypes::NameShort) => { | ||
| if eir.name.is_some() { | ||
| return Err(EIRError::RepeatedName); | ||
| } | ||
| eir.name = Some(EIRName::short_name( | ||
| String::from_utf8_lossy(data.bytes()).to_string(), | ||
| )); | ||
| } | ||
| Some(EIRDataTypes::NameComplete) => { | ||
| if eir.name.is_some() { | ||
| return Err(EIRError::RepeatedName); | ||
| } | ||
| eir.name = Some(EIRName::complete_name( | ||
| String::from_utf8_lossy(data.bytes()).to_string(), | ||
| )); | ||
| } | ||
| Some(EIRDataTypes::TxPowerLevel) => { | ||
| eir.tx_power_level.push(data.get_i8()); | ||
| } | ||
| Some(EIRDataTypes::URI) => { | ||
| let uri_scheme = data.get_u8(); | ||
| if uri_scheme == 0x01 { | ||
| let uri = String::from_utf8(data.bytes().to_vec()); | ||
| if uri.is_err() { | ||
| return Err(EIRError::InvalidURI); | ||
| } | ||
| eir.uri.push(uri.unwrap()); | ||
| } else { | ||
| // TODO: URI scheme translation. Skip for now. | ||
| } | ||
| } | ||
| Some(EIRDataTypes::ManufacturerSpecificData) => { | ||
| if data.remaining() < 2 { | ||
| return Err(EIRError::UnexpectedDataLength { | ||
| len: data.remaining(), | ||
| }); | ||
| } | ||
| eir.manufacturer_specific_data | ||
| .push(ManufacturerSpecificData { | ||
| company_identifier_code: data.get_u16_le(), | ||
| data: Bytes::copy_from_slice(data.bytes()), | ||
| }); | ||
| } | ||
| _ => { | ||
| // Skip unknown data | ||
|
ruediger marked this conversation as resolved.
|
||
| } | ||
| } | ||
| data.advance(data.remaining()); | ||
| buf = data.into_inner(); | ||
|
Comment on lines
+220
to
+221
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of these two lines?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to restore buf after moving it earlier. Otherwise I get the following error. I'm fairly new to Rust. So there might be an obvious way of doing this I'm missing.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not really clear why you need to call Note that |
||
| } | ||
|
|
||
| Ok(eir) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| pub fn eir_name_test() { | ||
| let input = Bytes::copy_from_slice(b"\x04\x09ABC"); | ||
| let eir = parse_eir(input); | ||
| assert!(eir.is_ok()); | ||
| let eir = eir.unwrap(); | ||
| assert!(eir.flags.is_none()); | ||
| assert!(eir.uuid16.is_empty()); | ||
| assert!(eir.uuid32.is_empty()); | ||
| assert!(eir.uuid128.is_empty()); | ||
| assert!(eir.name.is_some()); | ||
| let name = eir.name.unwrap(); | ||
| assert!(name.complete); | ||
| assert_eq!(name.name, "ABC"); | ||
| assert!(eir.tx_power_level.is_empty()); | ||
| assert!(eir.uri.is_empty()); | ||
| assert!(eir.manufacturer_specific_data.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| pub fn eir_multiple_test() { | ||
| let input = Bytes::copy_from_slice(b"\x02\x01\x06\x03\x03\xAB\xAC\x03\x08Hi"); | ||
| let eir = parse_eir(input); | ||
| assert!(eir.is_ok()); | ||
| let eir = eir.unwrap(); | ||
| assert!(eir.flags.is_some()); | ||
| let flags = eir.flags.unwrap(); | ||
| assert_eq!( | ||
| flags, | ||
| EIRFlags::BREDRNotSupported | EIRFlags::LEGeneralDiscoverableMode | ||
| ); | ||
| assert!(!eir.uuid16.is_empty()); | ||
| assert_eq!(eir.uuid16, vec![0xACAB]); | ||
| assert!(eir.uuid32.is_empty()); | ||
| assert!(eir.uuid128.is_empty()); | ||
| assert!(eir.name.is_some()); | ||
| let name = eir.name.unwrap(); | ||
| assert!(!name.complete); | ||
| assert_eq!(name.name, "Hi"); | ||
| assert!(eir.tx_power_level.is_empty()); | ||
| assert!(eir.uri.is_empty()); | ||
| assert!(eir.manufacturer_specific_data.is_empty()); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider making this
EIRtype represent a single EIR tag, and then return aResult<Vec<EIR>>fromparse_eir.Then you could write
EIRas a tagged enum:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was wondering what the best API would be here. Enums and Vec (or maybe HashSet?) might make it a bit harder to match. But it feels clunky having a struct full of optionals. I'll try with Enums and see how it goes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An attempt at changing the return type to Vec: ruediger@cc91569 Please take a look if you prefer this style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thank you, and please keep it up. One note:
EIR::Namehas more than one property, and it's not very obvious what the boolean is for, so maybe consider making the properties of that one named?Also, should the name be a
Stringor aCString? I don't think the Bluetooth spec says device names have to be in UTF-8, so it might be good to give the end-user a chance to deal with this themselves.