From ed1bf56b625dbcd8df6f57ed5292d37b789e5534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Sonderfeld?= Date: Fri, 27 Nov 2020 23:36:16 +0000 Subject: [PATCH 1/2] Add Extended Inquiry Response (EIR) parser. --- src/eir.rs | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 251 insertions(+) create mode 100644 src/eir.rs diff --git a/src/eir.rs b/src/eir.rs new file mode 100644 index 0000000..070354d --- /dev/null +++ b/src/eir.rs @@ -0,0 +1,250 @@ +use bytes::*; +use bytes::buf::BufExt; +use enumflags2::BitFlags; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use thiserror::Error; + +#[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>, + uuid16: Vec, + uuid32: Vec, + uuid128: Vec, + name: Option, + tx_power_level: Vec, + uri: Vec, + manufacturer_specific_data: Vec, +} + +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)] +enum EIRDataTypes { + Flags = 0x01, + UUID16Incomplete = 0x02, + UUID16Complete = 0x03, + UUID32Incomplete = 0x04, + UUID32Complete = 0x05, + UUID128Incomplete = 0x06, + UUID128Complete = 0x07, + NameShort = 0x08, + NameComplete = 0x09, + TxPowerLevel = 0x0A, + URI = 0x24, + ManufacturerSpecificData = 0xFF, +} + +pub fn parse_eir(mut buf: T) -> Result { + 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) => { // 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 + }, + } + data.advance(data.remaining()); + buf = data.into_inner(); + } + + 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()); + } +} diff --git a/src/lib.rs b/src/lib.rs index fa7d1c9..a9548c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ pub use address::Address; pub use result::{Error, Result}; pub mod client; +pub mod eir; pub mod interface; pub mod result; From 22026717a76852f7ef4294bb811f81f7b8921b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BCdiger=20Sonderfeld?= Date: Sat, 28 Nov 2020 14:12:26 +0000 Subject: [PATCH 2/2] Add docstrings and use rustfmt on EIR module. --- src/eir.rs | 86 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/src/eir.rs b/src/eir.rs index 070354d..59eadf9 100644 --- a/src/eir.rs +++ b/src/eir.rs @@ -1,14 +1,21 @@ -use bytes::*; +//! 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_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, + LELimitedDiscoverableMode = 1 << 0, LEGeneralDiscoverableMode = 1 << 1, BREDRNotSupported = 1 << 2, ControllerSimultaneousLEBREDR = 1 << 3, @@ -76,15 +83,14 @@ pub enum EIRError { #[error("More than one name block found.")] RepeatedName, #[error("Unexpected data length {}.", len)] - UnexpectedDataLength { - len: usize, - }, + UnexpectedDataLength { len: usize }, #[error("UTF-8 encoding error in URI.")] InvalidURI, } #[repr(u8)] #[derive(FromPrimitive)] +#[non_exhaustive] enum EIRDataTypes { Flags = 0x01, UUID16Incomplete = 0x02, @@ -100,6 +106,10 @@ enum EIRDataTypes { 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(mut buf: T) -> Result { let mut eir = EIR::new(); @@ -124,51 +134,61 @@ pub fn parse_eir(mut buf: T) -> Result { // Core Specification Supplement // EIRDataType values https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ match FromPrimitive::from_u8(data_type) { - Some(EIRDataTypes::Flags) => { // Flags + 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) => { + } + Some(EIRDataTypes::UUID16Incomplete) | Some(EIRDataTypes::UUID16Complete) => { if data.remaining() % 2 != 0 { - return Err(EIRError::UnexpectedDataLength { len:data.remaining() }); + return Err(EIRError::UnexpectedDataLength { + len: data.remaining(), + }); } while data.has_remaining() { eir.uuid16.push(data.get_u16_le()); } - }, - Some(EIRDataTypes::UUID32Incomplete)|Some(EIRDataTypes::UUID32Complete) => { + } + Some(EIRDataTypes::UUID32Incomplete) | Some(EIRDataTypes::UUID32Complete) => { if data.remaining() % 4 != 0 { - return Err(EIRError::UnexpectedDataLength { len:data.remaining() }); + return Err(EIRError::UnexpectedDataLength { + len: data.remaining(), + }); } while data.has_remaining() { eir.uuid32.push(data.get_u32_le()); } - }, - Some(EIRDataTypes::UUID128Incomplete)|Some(EIRDataTypes::UUID128Complete) => { + } + Some(EIRDataTypes::UUID128Incomplete) | Some(EIRDataTypes::UUID128Complete) => { if data.remaining() % 16 != 0 { - return Err(EIRError::UnexpectedDataLength { len:data.remaining() }); + 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())); - }, + 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())); - }, + 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 { @@ -180,21 +200,22 @@ pub fn parse_eir(mut buf: T) -> Result { } else { // TODO: URI scheme translation. Skip for now. } - }, + } Some(EIRDataTypes::ManufacturerSpecificData) => { if data.remaining() < 2 { - return Err(EIRError::UnexpectedDataLength { len:data.remaining() }); + return Err(EIRError::UnexpectedDataLength { + len: data.remaining(), + }); } - eir.manufacturer_specific_data.push( - ManufacturerSpecificData { + eir.manufacturer_specific_data + .push(ManufacturerSpecificData { company_identifier_code: data.get_u16_le(), data: Bytes::copy_from_slice(data.bytes()), - } - ); - }, + }); + } _ => { // Skip unknown data - }, + } } data.advance(data.remaining()); buf = data.into_inner(); @@ -234,7 +255,10 @@ mod tests { let eir = eir.unwrap(); assert!(eir.flags.is_some()); let flags = eir.flags.unwrap(); - assert_eq!(flags, EIRFlags::BREDRNotSupported | EIRFlags::LEGeneralDiscoverableMode); + assert_eq!( + flags, + EIRFlags::BREDRNotSupported | EIRFlags::LEGeneralDiscoverableMode + ); assert!(!eir.uuid16.is_empty()); assert_eq!(eir.uuid16, vec![0xACAB]); assert!(eir.uuid32.is_empty());