blob: 125d6e2c6270ab285175959ad886b5cdff30bf12 [file]
// 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.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: 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(NonZeroU8::new(2).unwrap())));
assert_eq!(client.rank(), Some(SetMemberRank(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().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_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)
);
}
}