use crate::error::{BleError, Result};
use crate::gatt::GattConnection;
use hap_model::format::CharFormat;
use hap_model::perms::Perms;
use hap_model::CharacteristicType;
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OpCode {
#[allow(dead_code)]
CharacteristicSignatureRead = 0x01,
CharacteristicWrite = 0x02,
CharacteristicRead = 0x03,
CharacteristicConfig = 0x07,
ProtocolConfig = 0x08,
}
pub(crate) mod param {
pub(crate) const VALUE: u8 = 0x01;
pub(crate) const CHAR_TYPE: u8 = 0x04;
pub(crate) const RETURN_RESPONSE: u8 = 0x09;
pub(crate) const PROPERTIES: u8 = 0x0A;
pub(crate) const PRESENTATION_FORMAT: u8 = 0x0C;
}
pub(crate) fn encode_request(op: OpCode, tid: u8, iid: u16, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(7 + body.len());
out.push(0x00); out.push(op as u8);
out.push(tid);
out.extend_from_slice(&iid.to_le_bytes());
if !body.is_empty() {
let len = u16::try_from(body.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(body);
}
out
}
#[cfg(test)]
pub(crate) fn encode_value_param(value: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut w = hap_tlv8::Tlv8Writer::new(&mut out);
w.push(param::VALUE, value);
out
}
pub(crate) fn encode_write_body(value: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut w = hap_tlv8::Tlv8Writer::new(&mut out);
w.push_u8(param::RETURN_RESPONSE, 1);
w.push(param::VALUE, value);
out
}
pub(crate) fn value_param(body: &[u8]) -> Result<Vec<u8>> {
let map = hap_tlv8::Tlv8Map::parse(body)?;
map.get(param::VALUE)
.map(<[u8]>::to_vec)
.ok_or(BleError::MalformedPdu("missing value param (0x01)"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Response {
pub tid: u8,
pub status: u8,
pub body: Vec<u8>,
}
pub(crate) fn decode_response(pdu: &[u8]) -> Result<Response> {
if pdu.len() < 3 {
return Err(BleError::MalformedPdu("response shorter than 3 bytes"));
}
let tid = pdu[1];
let status = pdu[2];
let body = if pdu.len() > 3 {
if pdu.len() < 5 {
return Err(BleError::MalformedPdu("response body length truncated"));
}
let len = usize::from(u16::from_le_bytes([pdu[3], pdu[4]]));
let start = 5;
if pdu.len() < start + len {
return Err(BleError::MalformedPdu(
"response body shorter than declared",
));
}
pdu[start..start + len].to_vec()
} else {
Vec::new()
};
Ok(Response { tid, status, body })
}
pub(crate) fn fragment(pdu: &[u8], frag_size: usize) -> Vec<Vec<u8>> {
let frag_size = frag_size.max(3);
if pdu.len() <= frag_size {
return vec![pdu.to_vec()];
}
let tid = pdu[2];
let mut frags = vec![pdu[..frag_size].to_vec()];
let mut rest = &pdu[frag_size..];
let cont_payload = frag_size.saturating_sub(2).max(1);
while !rest.is_empty() {
let take = rest.len().min(cont_payload);
let mut f = Vec::with_capacity(2 + take);
f.push(0x80); f.push(tid);
f.extend_from_slice(&rest[..take]);
frags.push(f);
rest = &rest[take..];
}
frags
}
pub(crate) fn reassemble(frags: &[Vec<u8>]) -> Result<Vec<u8>> {
let mut out = match frags.first() {
Some(first) => first.clone(),
None => return Err(BleError::MalformedPdu("no fragments to reassemble")),
};
for f in &frags[1..] {
if f.len() < 2 {
return Err(BleError::MalformedPdu("continuation fragment too short"));
}
out.extend_from_slice(&f[2..]);
}
Ok(out)
}
fn declared_total(first: &[u8]) -> usize {
if first.len() >= 5 {
5 + usize::from(u16::from_le_bytes([first[3], first[4]]))
} else {
first.len()
}
}
const MAX_RESPONSE_FRAGMENTS: usize = 64;
pub(crate) async fn request<G: GattConnection + ?Sized>(
gatt: &G,
char_uuid: &str,
op: OpCode,
tid: u8,
iid: u16,
body: &[u8],
frag_size: usize,
) -> Result<Response> {
let pdu = encode_request(op, tid, iid, body);
for frag in fragment(&pdu, frag_size) {
gatt.write(char_uuid, &frag).await?;
}
let mut frags = vec![gatt.read(char_uuid).await?];
while reassemble(&frags)?.len() < declared_total(&frags[0]) {
if frags.len() >= MAX_RESPONSE_FRAGMENTS {
return Err(BleError::MalformedPdu("too many response fragments"));
}
frags.push(gatt.read(char_uuid).await?);
}
decode_response(&reassemble(&frags)?)
}
#[allow(clippy::too_many_arguments)] pub(crate) async fn request_secure<G: GattConnection + ?Sized>(
gatt: &G,
session: &mut crate::session::BleSession,
char_uuid: &str,
op: OpCode,
tid: u8,
iid: u16,
body: &[u8],
frag_size: usize,
) -> Result<Response> {
let pdu = encode_request(op, tid, iid, body);
for frag in fragment(&pdu, frag_size.saturating_sub(16).max(1)) {
let sealed = session.seal(&frag)?;
gatt.write(char_uuid, &sealed).await?;
}
let mut frags = vec![session.open(&gatt.read(char_uuid).await?)?];
while reassemble(&frags)?.len() < declared_total(&frags[0]) {
if frags.len() >= MAX_RESPONSE_FRAGMENTS {
return Err(BleError::MalformedPdu("too many response fragments"));
}
frags.push(session.open(&gatt.read(char_uuid).await?)?);
}
decode_response(&reassemble(&frags)?)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Signature {
pub char_type: CharacteristicType,
pub format: CharFormat,
pub perms: Perms,
}
pub(crate) fn char_format_from_gatt(b: u8) -> Option<CharFormat> {
Some(match b {
0x01 => CharFormat::Bool,
0x04 => CharFormat::Uint8,
0x06 => CharFormat::Uint16,
0x08 => CharFormat::Uint32,
0x0A => CharFormat::Uint64,
0x10 => CharFormat::Int,
0x14 => CharFormat::Float,
0x19 => CharFormat::String,
0x1B => CharFormat::Data,
_ => return None,
})
}
pub(crate) fn perms_from_properties(bits: u16) -> Perms {
Perms {
read: bits & 0x0001 != 0 || bits & 0x0010 != 0,
write: bits & 0x0002 != 0 || bits & 0x0020 != 0,
events: bits & 0x0080 != 0,
hidden: bits & 0x0040 != 0,
..Perms::default()
}
}
pub(crate) fn le_bytes_to_uuid(le: &[u8]) -> Result<String> {
if le.len() != 16 {
return Err(BleError::MalformedPdu(
"characteristic type uuid not 16 bytes",
));
}
let mut be = le.to_vec();
be.reverse();
let h = be.iter().fold(String::with_capacity(32), |mut acc, b| {
use std::fmt::Write as _;
let _ = write!(acc, "{b:02x}");
acc
});
Ok(format!(
"{}-{}-{}-{}-{}",
&h[0..8],
&h[8..12],
&h[12..16],
&h[16..20],
&h[20..32]
))
}
pub(crate) fn parse_signature(body: &[u8]) -> Result<Signature> {
let map = hap_tlv8::Tlv8Map::parse(body)?;
let type_le = map
.get(param::CHAR_TYPE)
.ok_or(BleError::MalformedPdu("signature missing char type"))?;
let uuid = hap_model::Uuid::parse(&le_bytes_to_uuid(type_le)?)?;
let char_type = CharacteristicType::from_uuid(&uuid);
let prop_bytes = map
.get(param::PROPERTIES)
.ok_or(BleError::MalformedPdu("signature missing properties"))?;
if prop_bytes.len() < 2 {
return Err(BleError::MalformedPdu("properties descriptor too short"));
}
let perms = perms_from_properties(u16::from_le_bytes([prop_bytes[0], prop_bytes[1]]));
let format = map
.get(param::PRESENTATION_FORMAT)
.and_then(|pf| pf.first().copied())
.and_then(char_format_from_gatt)
.or_else(|| char_type.default_format())
.ok_or(BleError::MalformedPdu("no usable characteristic format"))?;
Ok(Signature {
char_type,
format,
perms,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_presentation_format_byte() {
use hap_model::format::CharFormat;
assert_eq!(char_format_from_gatt(0x01), Some(CharFormat::Bool));
assert_eq!(char_format_from_gatt(0x08), Some(CharFormat::Uint32));
assert_eq!(char_format_from_gatt(0x14), Some(CharFormat::Float));
assert_eq!(char_format_from_gatt(0x19), Some(CharFormat::String));
assert_eq!(char_format_from_gatt(0xEE), None);
}
#[test]
fn maps_properties_to_perms() {
let p = perms_from_properties(0b1000_0011);
assert!(p.read && p.write && p.events);
assert!(!p.hidden);
let p2 = perms_from_properties(0b0001_0000);
assert!(p2.read);
}
#[test]
#[allow(clippy::unwrap_used)] fn parses_a_signature_body() {
use hap_model::format::CharFormat;
let on_uuid_le = uuid_to_le_bytes("00000025-0000-1000-8000-0026bb765291");
let mut body = Vec::new();
let mut w = hap_tlv8::Tlv8Writer::new(&mut body);
w.push(param::CHAR_TYPE, &on_uuid_le);
w.push(param::PROPERTIES, &0x0003u16.to_le_bytes());
w.push(param::PRESENTATION_FORMAT, &[0x01, 0, 0, 0, 0, 0, 0]);
let sig = parse_signature(&body).unwrap();
assert_eq!(sig.char_type, hap_model::CharacteristicType::On);
assert_eq!(sig.format, CharFormat::Bool);
assert!(sig.perms.read && sig.perms.write);
}
#[test]
fn encodes_bodyless_request() {
let pdu = encode_request(OpCode::CharacteristicRead, 0x11, 0x0203, &[]);
assert_eq!(pdu, vec![0x00, 0x03, 0x11, 0x03, 0x02]);
}
#[test]
fn encodes_request_with_body() {
let pdu = encode_request(OpCode::CharacteristicWrite, 0x22, 0x0001, &[0xAA, 0xBB]);
assert_eq!(
pdu,
vec![0x00, 0x02, 0x22, 0x01, 0x00, 0x02, 0x00, 0xAA, 0xBB]
);
}
#[test]
#[allow(clippy::unwrap_used)] fn value_param_roundtrip() {
let body = encode_value_param(&[0x01, 0x02, 0x03]);
let got = value_param(&body).unwrap();
assert_eq!(got, vec![0x01, 0x02, 0x03]);
}
#[test]
fn write_body_has_return_response_then_value() {
let body = encode_write_body(&[0x06, 0x01, 0x01, 0x00, 0x01, 0x00]);
assert_eq!(
body,
vec![0x09, 0x01, 0x01, 0x01, 0x06, 0x06, 0x01, 0x01, 0x00, 0x01, 0x00]
);
}
#[test]
#[allow(clippy::unwrap_used)] fn decodes_bodyless_response() {
let resp = decode_response(&[0x02, 0x11, 0x00]).unwrap();
assert_eq!(resp.tid, 0x11);
assert_eq!(resp.status, 0x00);
assert!(resp.body.is_empty());
}
#[test]
#[allow(clippy::unwrap_used)] fn decodes_response_with_body() {
let resp = decode_response(&[0x02, 0x11, 0x00, 0x02, 0x00, 0xDE, 0xAD]).unwrap();
assert_eq!(resp.status, 0x00);
assert_eq!(resp.body, vec![0xDE, 0xAD]);
}
#[test]
fn rejects_short_response() {
assert!(matches!(
decode_response(&[0x02, 0x11]),
Err(crate::error::BleError::MalformedPdu(_))
));
}
#[test]
#[allow(clippy::unwrap_used)] fn fragments_and_reassembles_a_large_pdu() {
let pdu: Vec<u8> = (0..300u32)
.map(|i| u8::try_from(i % 251).unwrap())
.collect();
let frags = fragment(&pdu, 100);
assert!(frags.len() > 1);
assert_eq!(frags[0][0], pdu[0]);
assert_eq!(frags[1][0], 0x80);
let back = reassemble(&frags).unwrap();
assert_eq!(back, pdu);
}
#[test]
fn single_fragment_when_it_fits() {
let pdu = vec![0x00, 0x03, 0x11, 0x03, 0x02];
let frags = fragment(&pdu, 100);
assert_eq!(frags.len(), 1);
assert_eq!(frags[0], pdu);
}
#[allow(clippy::unwrap_used)] fn uuid_to_le_bytes(full: &str) -> Vec<u8> {
let hex: String = full.chars().filter(|c| *c != '-').collect();
let mut be: Vec<u8> = (0..16)
.map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).unwrap())
.collect();
be.reverse(); be
}
#[tokio::test]
#[allow(clippy::unwrap_used)] async fn request_writes_and_reads_back_response() {
use crate::gatt::MockGatt;
let gatt = MockGatt::new();
let body = encode_value_param(&[0xAB]);
let mut resp = vec![0x02, 0x05, 0x00];
resp.extend_from_slice(&u16::try_from(body.len()).unwrap().to_le_bytes());
resp.extend_from_slice(&body);
gatt.queue_read("pair", resp);
let got = request(
&gatt,
"pair",
OpCode::CharacteristicWrite,
0x05,
0x0001,
&encode_value_param(&[0x01]),
512,
)
.await
.unwrap();
assert_eq!(got.status, 0x00);
assert_eq!(value_param(&got.body).unwrap(), vec![0xAB]);
}
#[tokio::test]
#[allow(clippy::unwrap_used)] async fn request_reassembles_multi_fragment_response() {
use crate::gatt::MockGatt;
let gatt = MockGatt::new();
gatt.queue_read("c", vec![0x02, 0x05, 0x00, 0x06, 0x00, 0xAA, 0xBB, 0xCC]);
gatt.queue_read("c", vec![0x82, 0x05, 0xDD, 0xEE, 0xFF]);
let got = request(
&gatt,
"c",
OpCode::CharacteristicRead,
0x05,
0x0001,
&[],
512,
)
.await
.unwrap();
assert_eq!(got.status, 0x00);
assert_eq!(got.body, vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
}
}