| // 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) | |
| ); | |
| } | |
| } |