| // Copyright 2026 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use thiserror::Error; |
| |
| use bt_common::packet_encoding::Decodable; |
| use bt_common::Uuid; |
| use bt_gatt::client::{PeerService, ServiceCharacteristic}; |
| use bt_gatt::types::Handle; |
| |
| use crate::types::*; |
| |
| #[derive(Debug, Error)] |
| pub enum Error { |
| #[error("GATT operation error: {0}")] |
| Gatt(#[from] bt_gatt::types::Error), |
| |
| #[error("Packet serialization/deserialization error: {0}")] |
| Packet(#[from] bt_common::packet_encoding::Error), |
| |
| #[error("Missing required characteristic: {0}")] |
| MissingCharacteristic(Uuid), |
| |
| #[error("Extra characteristic found: {0}")] |
| ExtraCharacteristic(Uuid), |
| |
| #[error("Invalid characteristic value: {0}")] |
| InvalidCharacteristic(String), |
| } |
| |
| /// A GATT client for interacting with the Coordinated Set Identification |
| /// Service (CSIS). See CSIS v1.1 Section 2 & Section 5 and CSIP v1.1 Section |
| /// 4.2 & Section 4.3. |
| pub struct CoordinatedSetIdentificationServiceClient<T: bt_gatt::GattTypes> { |
| #[allow(dead_code)] |
| gatt_client: T::PeerService, |
| #[allow(dead_code)] |
| sirk_handle: Handle, |
| #[allow(dead_code)] |
| size_handle: Option<Handle>, |
| #[allow(dead_code)] |
| lock_handle: Option<Handle>, |
| #[allow(dead_code)] |
| rank_handle: Option<Handle>, |
| |
| // Initial read values |
| sirk: SetIdentityResolvingKey, |
| size: Option<CoordinatedSetSize>, |
| rank: Option<SetMemberRank>, |
| } |
| |
| impl<T: bt_gatt::GattTypes> CoordinatedSetIdentificationServiceClient<T> { |
| /// Discovers CSIS characteristics and reads initial characteristic values. |
| /// See CSIS v1.1 Section 5, Table 5.1 and CSIP v1.1 Section 4.3. |
| pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error> { |
| let discovered = gatt_client.discover_characteristics(None).await?; |
| |
| let mut sirk_chars = Vec::new(); |
| let mut size_chars = Vec::new(); |
| let mut rank_chars = Vec::new(); |
| let mut lock_chars = Vec::new(); |
| |
| for ch in discovered { |
| let service_char = ServiceCharacteristic::new(&gatt_client, ch); |
| match service_char.uuid() { |
| SET_IDENTITY_RESOLVING_KEY_UUID => sirk_chars.push(service_char), |
| COORDINATED_SET_SIZE_UUID => size_chars.push(service_char), |
| SET_MEMBER_RANK_UUID => rank_chars.push(service_char), |
| SET_MEMBER_LOCK_UUID => lock_chars.push(service_char), |
| _ => {} |
| } |
| } |
| |
| let (sirk_handle, sirk) = Self::read_sirk(&sirk_chars).await?; |
| let (size_handle, size) = Self::read_size(&size_chars).await?; |
| let (rank_handle, rank) = Self::read_rank(&rank_chars).await?; |
| let lock_handle = Self::get_lock_handle(&lock_chars)?; |
| |
| if lock_handle.is_some() && rank_handle.is_none() { |
| // CSIS v1.1 Section 5, Table 5.1 Note C.1: Mandatory if Set Member Lock is |
| // supported |
| return Err(Error::MissingCharacteristic(SET_MEMBER_RANK_UUID)); |
| } |
| |
| if let (Some(r), Some(s)) = (rank, size) { |
| if r.0.get() > s.0.get() { |
| return Err(Error::InvalidCharacteristic( |
| "SetMemberRank cannot be greater than CoordinatedSetSize".to_string(), |
| )); |
| } |
| } |
| |
| Ok(Self { |
| gatt_client, |
| sirk_handle, |
| size_handle, |
| lock_handle, |
| rank_handle, |
| sirk, |
| size, |
| rank, |
| }) |
| } |
| |
| async fn read_sirk( |
| chars: &[ServiceCharacteristic<'_, T>], |
| ) -> Result<(Handle, SetIdentityResolvingKey), Error> { |
| if chars.is_empty() { |
| return Err(Error::MissingCharacteristic(SET_IDENTITY_RESOLVING_KEY_UUID)); |
| } |
| if chars.len() > 1 { |
| return Err(Error::ExtraCharacteristic(SET_IDENTITY_RESOLVING_KEY_UUID)); |
| } |
| let char_item = &chars[0]; |
| let handle = *char_item.handle(); |
| |
| let mut buf = [0; SetIdentityResolvingKey::BYTE_SIZE]; |
| let bytes_read = char_item.read(&mut buf).await?; |
| let (sirk_res, _) = SetIdentityResolvingKey::decode(&buf[..bytes_read]); |
| let sirk = sirk_res?; |
| |
| Ok((handle, sirk)) |
| } |
| |
| async fn read_size( |
| chars: &[ServiceCharacteristic<'_, T>], |
| ) -> Result<(Option<Handle>, Option<CoordinatedSetSize>), Error> { |
| if chars.is_empty() { |
| return Ok((None, None)); |
| } |
| if chars.len() > 1 { |
| return Err(Error::ExtraCharacteristic(COORDINATED_SET_SIZE_UUID)); |
| } |
| let char_item = &chars[0]; |
| let handle = *char_item.handle(); |
| |
| let mut buf = [0; CoordinatedSetSize::BYTE_SIZE]; |
| let bytes_read = char_item.read(&mut buf).await?; |
| let (size_res, _) = CoordinatedSetSize::decode(&buf[..bytes_read]); |
| let size = size_res?; |
| |
| Ok((Some(handle), Some(size))) |
| } |
| |
| async fn read_rank( |
| chars: &[ServiceCharacteristic<'_, T>], |
| ) -> Result<(Option<Handle>, Option<SetMemberRank>), Error> { |
| if chars.is_empty() { |
| return Ok((None, None)); |
| } |
| if chars.len() > 1 { |
| return Err(Error::ExtraCharacteristic(SET_MEMBER_RANK_UUID)); |
| } |
| let char_item = &chars[0]; |
| let handle = *char_item.handle(); |
| |
| let mut buf = [0; SetMemberRank::BYTE_SIZE]; |
| let bytes_read = char_item.read(&mut buf).await?; |
| let (rank_res, _) = SetMemberRank::decode(&buf[..bytes_read]); |
| let rank = rank_res?; |
| |
| Ok((Some(handle), Some(rank))) |
| } |
| |
| fn get_lock_handle(chars: &[ServiceCharacteristic<'_, T>]) -> Result<Option<Handle>, Error> { |
| if chars.is_empty() { |
| return Ok(None); |
| } |
| if chars.len() > 1 { |
| return Err(Error::ExtraCharacteristic(SET_MEMBER_LOCK_UUID)); |
| } |
| Ok(Some(*chars[0].handle())) |
| } |
| |
| pub fn sirk(&self) -> SetIdentityResolvingKey { |
| self.sirk |
| } |
| |
| pub fn size(&self) -> Option<CoordinatedSetSize> { |
| self.size |
| } |
| |
| pub fn rank(&self) -> Option<SetMemberRank> { |
| self.rank |
| } |
| |
| // TODO(b/534436497): Add lock() and unlock() methods for Exclusive |
| // Access & Set Member Locking. TODO(b/534436497): Add notification stream |
| // support for Lock and Size characteristics. |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use assert_matches::assert_matches; |
| use bt_gatt::test_utils::*; |
| use bt_gatt::types::{ |
| AttributePermissions, CharacteristicProperties, CharacteristicProperty, Handle, |
| }; |
| use bt_gatt::Characteristic; |
| use core::num::NonZeroU8; |
| use futures::{ |
| pin_mut, |
| task::{noop_waker_ref, Context, Poll}, |
| FutureExt, |
| }; |
| |
| const SIRK_HANDLE: Handle = Handle(1); |
| const SIZE_HANDLE: Handle = Handle(2); |
| const LOCK_HANDLE: Handle = Handle(3); |
| const RANK_HANDLE: Handle = Handle(4); |
| |
| fn add_char(service: &mut FakePeerService, handle: Handle, uuid: Uuid, value: Vec<u8>) { |
| service.add_characteristic( |
| Characteristic { |
| handle, |
| uuid, |
| properties: CharacteristicProperties(vec![CharacteristicProperty::Read]), |
| permissions: AttributePermissions::default(), |
| descriptors: vec![], |
| }, |
| value, |
| ); |
| } |
| |
| #[test] |
| fn create_success_all_chars() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| // Valid SIRK: Type 1 (Plaintext) + 16 bytes key |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, SIZE_HANDLE, COORDINATED_SET_SIZE_UUID, vec![0x02]); |
| add_char(&mut service, LOCK_HANDLE, SET_MEMBER_LOCK_UUID, vec![0x01]); // 0x01 = Unlocked per CSIS v1.1 Section 5.3, Table 5.4 |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x01]); |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(client) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| let client = client.unwrap(); |
| |
| assert_eq!(client.sirk_handle, SIRK_HANDLE); |
| assert_eq!(client.size_handle, Some(SIZE_HANDLE)); |
| assert_eq!(client.lock_handle, Some(LOCK_HANDLE)); |
| assert_eq!(client.rank_handle, Some(RANK_HANDLE)); |
| |
| assert_eq!(client.sirk().sirk_type, SirkType::Plaintext); |
| assert_eq!(client.size(), Some(CoordinatedSetSize(NonZeroU8::new(2).unwrap()))); |
| assert_eq!(client.rank(), Some(SetMemberRank(NonZeroU8::new(1).unwrap()))); |
| } |
| |
| #[test] |
| fn create_success_only_mandatory() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(client) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| let client = client.unwrap(); |
| |
| assert_eq!(client.sirk_handle, SIRK_HANDLE); |
| assert_eq!(client.size_handle, None); |
| assert_eq!(client.lock_handle, None); |
| assert_eq!(client.rank_handle, None); |
| } |
| |
| #[test] |
| fn create_success_rank_present_without_lock() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x01]); |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(client) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| let client = client.unwrap(); |
| |
| assert_eq!(client.sirk_handle, SIRK_HANDLE); |
| assert_eq!(client.size_handle, None); |
| assert_eq!(client.lock_handle, None); |
| assert_eq!(client.rank_handle, Some(RANK_HANDLE)); |
| assert_eq!(client.rank().unwrap().0.get(), 1); |
| } |
| |
| #[test] |
| fn create_fails_missing_sirk() { |
| let service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::MissingCharacteristic(uuid) if uuid == SET_IDENTITY_RESOLVING_KEY_UUID); |
| } |
| |
| #[test] |
| fn create_fails_duplicate_sirk() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val.clone()); |
| add_char(&mut service, Handle(5), SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::ExtraCharacteristic(uuid) if uuid == SET_IDENTITY_RESOLVING_KEY_UUID); |
| } |
| |
| #[test] |
| fn create_fails_lock_present_rank_missing() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, LOCK_HANDLE, SET_MEMBER_LOCK_UUID, vec![0x01]); |
| // Missing Rank |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::MissingCharacteristic(uuid) if uuid == SET_MEMBER_RANK_UUID); |
| } |
| |
| #[test] |
| fn create_fails_invalid_size() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, SIZE_HANDLE, COORDINATED_SET_SIZE_UUID, vec![0x00]); // Invalid Size 0 |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::Packet(_)); |
| } |
| |
| #[test] |
| fn create_fails_invalid_rank_zero() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x00]); // Invalid Rank 0 |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::Packet(_)); |
| } |
| |
| #[test] |
| fn create_fails_rank_greater_than_size() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| add_char(&mut service, SIZE_HANDLE, COORDINATED_SET_SIZE_UUID, vec![0x02]); |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x03]); // Rank 3 > Size 2 |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::InvalidCharacteristic(_)); |
| } |
| |
| #[test] |
| fn create_fails_duplicate_size() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, SIZE_HANDLE, COORDINATED_SET_SIZE_UUID, vec![0x02]); |
| add_char(&mut service, Handle(5), COORDINATED_SET_SIZE_UUID, vec![0x03]); // Duplicate |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::ExtraCharacteristic(uuid) if uuid == COORDINATED_SET_SIZE_UUID); |
| } |
| |
| #[test] |
| fn create_fails_duplicate_lock() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, LOCK_HANDLE, SET_MEMBER_LOCK_UUID, vec![0x01]); |
| add_char(&mut service, Handle(5), SET_MEMBER_LOCK_UUID, vec![0x01]); // Duplicate |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x01]); // Rank is mandatory if lock present per CSIS v1.1 Table 5.1 Note C.1 |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::ExtraCharacteristic(uuid) if uuid == SET_MEMBER_LOCK_UUID); |
| } |
| |
| #[test] |
| fn create_fails_duplicate_rank() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![0x01]); |
| add_char(&mut service, Handle(5), SET_MEMBER_RANK_UUID, vec![0x02]); // Duplicate |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!(result.err().unwrap(), Error::ExtraCharacteristic(uuid) if uuid == SET_MEMBER_RANK_UUID); |
| } |
| |
| #[test] |
| fn create_fails_sirk_invalid_length() { |
| let invalid_payloads = vec![ |
| vec![], // Empty (0 bytes) |
| vec![0x01; 5], // Too short (5 bytes < 17 bytes) |
| ]; |
| |
| for payload in invalid_payloads { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, payload); |
| |
| let create_fut = |
| CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!( |
| result.err().unwrap(), |
| Error::Packet(bt_common::packet_encoding::Error::UnexpectedDataLength) |
| ); |
| } |
| } |
| |
| #[test] |
| fn create_fails_size_invalid_length() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| add_char(&mut service, SIZE_HANDLE, COORDINATED_SET_SIZE_UUID, vec![]); // Empty (0 bytes) |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!( |
| result.err().unwrap(), |
| Error::Packet(bt_common::packet_encoding::Error::UnexpectedDataLength) |
| ); |
| } |
| |
| #[test] |
| fn create_fails_rank_invalid_length() { |
| let mut service = FakePeerService::new(); |
| let mut noop_cx = Context::from_waker(noop_waker_ref()); |
| |
| let mut sirk_val = vec![0x01]; |
| sirk_val.extend_from_slice(&[0xAB; 16]); |
| add_char(&mut service, SIRK_HANDLE, SET_IDENTITY_RESOLVING_KEY_UUID, sirk_val); |
| add_char(&mut service, RANK_HANDLE, SET_MEMBER_RANK_UUID, vec![]); // Empty (0 bytes) |
| |
| let create_fut = CoordinatedSetIdentificationServiceClient::<FakeTypes>::create(service); |
| pin_mut!(create_fut); |
| let Poll::Ready(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert!(result.is_err()); |
| assert_matches!( |
| result.err().unwrap(), |
| Error::Packet(bt_common::packet_encoding::Error::UnexpectedDataLength) |
| ); |
| } |
| } |