| // 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 std::sync::Arc; |
| |
| use futures::stream::{SelectAll, StreamExt}; |
| use parking_lot::Mutex; |
| use thiserror::Error; |
| |
| use bt_common::packet_encoding::Decodable; |
| use bt_common::Uuid; |
| use bt_gatt::client::{PeerService, ServiceCharacteristic}; |
| use bt_gatt::types::{CharacteristicProperty, Handle}; |
| |
| use crate::types::*; |
| |
| pub mod event; |
| |
| pub use event::{CsisNotification, NotificationStream}; |
| |
| #[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), |
| |
| #[error("Notification from unexpected characteristic handle: {0:?}")] |
| UnexpectedNotification(Handle), |
| } |
| |
| /// The values of the characteristics exposed by the CSIS service. |
| #[derive(Debug, Clone)] |
| pub(crate) struct CsisClientState { |
| sirk: SetIdentityResolvingKey, |
| size: Option<CoordinatedSetSize>, |
| rank: Option<SetMemberRank>, |
| lock_state: Option<SetMemberLock>, |
| } |
| |
| impl CsisClientState { |
| pub(crate) fn sirk(&self) -> SetIdentityResolvingKey { |
| self.sirk |
| } |
| |
| pub(crate) fn set_sirk(&mut self, sirk: SetIdentityResolvingKey) { |
| self.sirk = sirk; |
| } |
| |
| pub(crate) fn size(&self) -> Option<CoordinatedSetSize> { |
| self.size |
| } |
| |
| pub(crate) fn set_size(&mut self, size: CoordinatedSetSize) { |
| self.size = Some(size); |
| } |
| |
| pub(crate) fn rank(&self) -> Option<SetMemberRank> { |
| self.rank |
| } |
| |
| pub(crate) fn lock_state(&self) -> Option<SetMemberLock> { |
| self.lock_state |
| } |
| |
| pub(crate) fn set_lock_state(&mut self, lock_state: SetMemberLock) { |
| self.lock_state = Some(lock_state); |
| } |
| } |
| |
| /// 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> { |
| // The connection to this CSIS service instance. Held to keep it, and the |
| // notification subscriptions registered through it, alive. |
| gatt_client: T::PeerService, |
| sirk_handle: Handle, |
| size_handle: Option<Handle>, |
| lock_handle: Option<Handle>, |
| /// Set Member Rank is not notifiable, so a change is only seen by |
| /// re-reading. |
| // TODO(b/534436497): implement re-reading. |
| _rank_handle: Option<Handle>, |
| |
| /// Shared with the [`NotificationStream`] so that a value updated by a |
| /// GATT notification is visible through this client's accessors. |
| state: Arc<Mutex<CsisClientState>>, |
| |
| /// Handles of the notifiable characteristics. Taken by |
| /// [`Self::take_notification_stream`]. |
| notifiable_handles: Option<Vec<Handle>>, |
| } |
| |
| impl<T: bt_gatt::GattTypes> CoordinatedSetIdentificationServiceClient<T> { |
| /// Discovers CSIS characteristics and reads their initial values. See CSIS |
| /// v1.1 Section 5, Table 5.1 and CSIP v1.1 Section 4.3. |
| /// |
| /// Does not register for notifications; call |
| /// [`Self::take_notification_stream`] to subscribe. |
| pub async fn create(gatt_client: T::PeerService) -> Result<Self, Error> { |
| let discovered = gatt_client.discover_characteristics(None).await?; |
| |
| let mut sirk = None; |
| let mut size = None; |
| let mut rank = None; |
| let mut lock = None; |
| |
| let mut notifiable_handles = Vec::new(); |
| |
| // The CSIS service exposes at most one of each characteristic. |
| for ch in discovered { |
| let chr = ServiceCharacteristic::new(&gatt_client, ch); |
| let uuid = chr.uuid(); |
| let handle = *chr.handle(); |
| |
| match uuid { |
| SET_IDENTITY_RESOLVING_KEY_UUID => { |
| if sirk.is_some() { |
| return Err(Error::ExtraCharacteristic(uuid)); |
| } |
| sirk = Some((handle, Self::read_sirk(&chr).await?)); |
| } |
| COORDINATED_SET_SIZE_UUID => { |
| if size.is_some() { |
| return Err(Error::ExtraCharacteristic(uuid)); |
| } |
| size = Some((handle, Self::read_size(&chr).await?)); |
| } |
| SET_MEMBER_LOCK_UUID => { |
| if lock.is_some() { |
| return Err(Error::ExtraCharacteristic(uuid)); |
| } |
| lock = Some((handle, Self::read_lock(&chr).await?)); |
| } |
| SET_MEMBER_RANK_UUID => { |
| if rank.is_some() { |
| return Err(Error::ExtraCharacteristic(uuid)); |
| } |
| rank = Some((handle, Self::read_rank(&chr).await?)); |
| } |
| _ => continue, |
| } |
| |
| if chr.characteristic().properties.contains(CharacteristicProperty::Notify) { |
| notifiable_handles.push(handle); |
| } |
| } |
| |
| let (sirk_handle, sirk) = |
| sirk.ok_or(Error::MissingCharacteristic(SET_IDENTITY_RESOLVING_KEY_UUID))?; |
| let (size_handle, size) = size.unzip(); |
| let (rank_handle, rank) = rank.unzip(); |
| let (lock_handle, lock_state) = lock.unzip(); |
| |
| 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.value().get() > s.value().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: rank_handle, |
| state: Arc::new(Mutex::new(CsisClientState { sirk, size, rank, lock_state })), |
| notifiable_handles: Some(notifiable_handles), |
| }) |
| } |
| |
| // TODO(b/534436497): Implement FromCharacteristic for the CSIS value types |
| // in place of these readers. |
| async fn read_sirk( |
| chr: &ServiceCharacteristic<'_, T>, |
| ) -> Result<SetIdentityResolvingKey, Error> { |
| let mut buf = [0; SetIdentityResolvingKey::BYTE_SIZE]; |
| let bytes_read = chr.read(&mut buf).await?; |
| let (sirk_res, _) = SetIdentityResolvingKey::decode(&buf[..bytes_read]); |
| Ok(sirk_res?) |
| } |
| |
| async fn read_size(chr: &ServiceCharacteristic<'_, T>) -> Result<CoordinatedSetSize, Error> { |
| let mut buf = [0; CoordinatedSetSize::BYTE_SIZE]; |
| let bytes_read = chr.read(&mut buf).await?; |
| let (size_res, _) = CoordinatedSetSize::decode(&buf[..bytes_read]); |
| Ok(size_res?) |
| } |
| |
| async fn read_rank(chr: &ServiceCharacteristic<'_, T>) -> Result<SetMemberRank, Error> { |
| let mut buf = [0; SetMemberRank::BYTE_SIZE]; |
| let bytes_read = chr.read(&mut buf).await?; |
| let (rank_res, _) = SetMemberRank::decode(&buf[..bytes_read]); |
| Ok(rank_res?) |
| } |
| |
| async fn read_lock(chr: &ServiceCharacteristic<'_, T>) -> Result<SetMemberLock, Error> { |
| // CSIS v1.1 Table 5.1: Notify is mandatory for Set Member Lock. |
| if !chr.characteristic().properties.contains(CharacteristicProperty::Notify) { |
| return Err(Error::InvalidCharacteristic( |
| "Set Member Lock characteristic must support notifications".to_string(), |
| )); |
| } |
| |
| let mut buf = [0; SetMemberLock::BYTE_SIZE]; |
| let bytes_read = chr.read(&mut buf).await?; |
| let (lock_res, _) = SetMemberLock::decode(&buf[..bytes_read]); |
| Ok(lock_res?) |
| } |
| |
| pub fn sirk(&self) -> SetIdentityResolvingKey { |
| self.state.lock().sirk() |
| } |
| |
| /// The last known value of the Coordinated Set Size characteristic, or |
| /// `None` if the Set Member does not support it. |
| pub fn size(&self) -> Option<CoordinatedSetSize> { |
| self.state.lock().size() |
| } |
| |
| /// The last known value of the Set Member Rank characteristic, or `None` if |
| /// the Set Member does not support it. |
| pub fn rank(&self) -> Option<SetMemberRank> { |
| self.state.lock().rank() |
| } |
| |
| /// The last known value of the Set Member Lock characteristic, or `None` if |
| /// the Set Member does not support it. |
| pub fn lock_state(&self) -> Option<SetMemberLock> { |
| self.state.lock().lock_state() |
| } |
| |
| /// Subscribes to the CSIS characteristics that support being notified and |
| /// takes the stream of the value changes they report. |
| /// |
| /// The stream is only available once; subsequent calls return `None`. It |
| /// yields no events if the Set Member does not support notifications on |
| /// any CSIS characteristic. |
| pub fn take_notification_stream(&mut self) -> Option<NotificationStream> |
| where |
| <T as bt_gatt::GattTypes>::NotificationStream: Send, |
| { |
| let handles = self.notifiable_handles.take()?; |
| let mut notification_streams = SelectAll::new(); |
| for handle in handles { |
| notification_streams.push(self.gatt_client.subscribe(&handle).boxed()); |
| } |
| Some(NotificationStream::new( |
| notification_streams, |
| event::NotifiedHandles { |
| sirk: self.sirk_handle, |
| size: self.size_handle, |
| lock: self.lock_handle, |
| }, |
| self.state.clone(), |
| )) |
| } |
| |
| // TODO(b/534436497): Add lock() and unlock(). See CSIS v1.1 Section 5.3.1. |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use assert_matches::assert_matches; |
| use bt_gatt::test_utils::*; |
| use bt_gatt::types::{AttributePermissions, 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: CharacteristicProperty::Read.into(), |
| permissions: AttributePermissions::default(), |
| descriptors: vec![], |
| }, |
| value, |
| ); |
| } |
| |
| /// Adds a Set Member Lock characteristic with Read, Write and Notify |
| /// properties. |
| fn add_lock_char(service: &mut FakePeerService, handle: Handle, value: Vec<u8>) { |
| service.add_characteristic( |
| Characteristic { |
| handle, |
| uuid: SET_MEMBER_LOCK_UUID, |
| properties: (&[ |
| CharacteristicProperty::Read, |
| CharacteristicProperty::Write, |
| CharacteristicProperty::Notify, |
| ]) |
| .into(), |
| 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_lock_char(&mut service, LOCK_HANDLE, 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::new(NonZeroU8::new(2).unwrap()))); |
| assert_eq!(client.rank(), Some(SetMemberRank::new(NonZeroU8::new(1).unwrap()))); |
| assert_eq!(client.lock_state(), Some(SetMemberLock::Unlocked)); |
| } |
| |
| #[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); |
| assert_eq!(client.lock_state(), 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().value().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_lock_char(&mut service, LOCK_HANDLE, 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_lock_char(&mut service, LOCK_HANDLE, vec![0x01]); |
| add_lock_char(&mut service, Handle(5), 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_lock_without_notify() { |
| 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); |
| |
| // A Lock characteristic without the mandatory Notify property. |
| add_char(&mut service, LOCK_HANDLE, SET_MEMBER_LOCK_UUID, vec![0x01]); |
| 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(result) = create_fut.poll_unpin(&mut noop_cx) else { |
| panic!("Expected create to be ready"); |
| }; |
| assert_matches!(result.err().unwrap(), Error::InvalidCharacteristic(_)); |
| } |
| |
| #[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) |
| ); |
| } |
| } |