diff --git a/packages/interpose/README.md b/packages/interpose/README.md index d487ef1..f43be77 100644 --- a/packages/interpose/README.md +++ b/packages/interpose/README.md @@ -10,6 +10,6 @@ The injected simulator dylib that routes a guest app's CoreBluetooth calls to th It hooks the CoreBluetooth central surface with Objective-C runtime swizzling, holds a shadow registry for the framework's opaque objects, and carries each operation to the helper over the loopback transport, delivering host events back as the guest's delegate callbacks. Scan, connect, -read, write, notify, and RSSI are routed; service and characteristic discovery are not routed in the -central interposer, and the peripheral role is not implemented. The dylib is a simulator slice, -loaded only by a debug scheme and never by a shipped app (see `SECURITY.md` for the fence). +service and characteristic discovery, read, write, notify, and RSSI are routed; the peripheral role +is not implemented. The dylib is a simulator slice, loaded only by a debug scheme and never by a +shipped app (see `SECURITY.md` for the fence). diff --git a/packages/interpose/include/simble_interpose.h b/packages/interpose/include/simble_interpose.h index c3b489d..33e6575 100644 --- a/packages/interpose/include/simble_interpose.h +++ b/packages/interpose/include/simble_interpose.h @@ -52,11 +52,13 @@ void simble_uninstall_hooks(void); /** How many times each routed central operation has fired since install. */ typedef struct { - int scan_start; ///< scanForPeripheralsWithServices:options: calls routed to the helper. - int connect; ///< connectPeripheral:options: calls routed to the helper. - int read; ///< readValueForCharacteristic: calls routed to the helper. - int write; ///< writeValue:forCharacteristic:type: calls routed to the helper. - int set_notify; ///< setNotifyValue:forCharacteristic: calls routed to the helper. + int scan_start; ///< scanForPeripheralsWithServices:options: calls routed. + int connect; ///< connectPeripheral:options: calls routed to the helper. + int discover_services; ///< discoverServices: calls routed to the helper. + int discover_characteristics; ///< discoverCharacteristics:forService: calls routed. + int read; ///< readValueForCharacteristic: calls routed to the helper. + int write; ///< writeValue:forCharacteristic:type: calls routed. + int set_notify; ///< setNotifyValue:forCharacteristic: calls routed. } simble_hook_stats; /** diff --git a/packages/interpose/src/hooks/central_hooks.m b/packages/interpose/src/hooks/central_hooks.m index cee60d9..f886505 100644 --- a/packages/interpose/src/hooks/central_hooks.m +++ b/packages/interpose/src/hooks/central_hooks.m @@ -27,7 +27,7 @@ #import #import -static simble_hook_stats g_stats = {0, 0, 0, 0, 0}; +static simble_hook_stats g_stats = {0, 0, 0, 0, 0, 0, 0}; static pthread_mutex_t g_install_lock = PTHREAD_MUTEX_INITIALIZER; static int g_installed = 0; @@ -354,13 +354,69 @@ static BOOL peripheralRouteId(CBPeripheral *peripheral, uint8_t *out, size_t cap return simble_shadow_peripheral_id(peripheral, out, cap, outLen); } +// Convert a CBUUID array filter to parallel UTF-8 string and length arrays held by an NSArray, so +// the encoder reads stable pointers. Returns the count; sets *uuidsOut and *lensOut to malloc'd +// arrays the caller frees, or NULL when the filter is empty. +static size_t buildUUIDFilter(NSArray *uuids, NSMutableArray *held, + const char ***uuidsOut, size_t **lensOut) { + size_t count = uuids.count; + if (count == 0) { + *uuidsOut = NULL; + *lensOut = NULL; + return 0; + } + const char **strs = calloc(count, sizeof(char *)); + size_t *lens = calloc(count, sizeof(size_t)); + for (size_t i = 0; i < count; i++) { + NSString *s = uuids[i].UUIDString; + [held addObject:s]; + strs[i] = s.UTF8String; + lens[i] = strlen(strs[i]); + } + *uuidsOut = strs; + *lensOut = lens; + return count; +} + - (void)simble_discoverServices:(NSArray *)serviceUUIDs { if (!simble_shadow_is_managed_peripheral(self)) { [self simble_discoverServices:serviceUUIDs]; return; } - // Service discovery is not routed in the central interposer; a routed discoverServices is a - // no-op. The routed read, write, and notify paths address a characteristic by its UUID. + // Route DISCOVER_SERVICES, mint a shadow service for each discovered UUID on this peripheral, and + // deliver peripheral:didDiscoverServices:. + uint8_t pid[64]; + size_t idLen = 0; + if (!simble_shadow_peripheral_id(self, pid, sizeof(pid), &idLen)) + return; + NSMutableArray *held = [NSMutableArray array]; + const char **uuids = NULL; + size_t *lens = NULL; + size_t count = buildUUIDFilter(serviceUUIDs, held, &uuids, &lens); + simble_response resp; + simble_status st = simble_client_discover_services(pid, idLen, uuids, lens, count, &resp); + free(uuids); + free(lens); + g_stats.discover_services++; + CBPeripheral *peripheral = self; + if (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_SERVICES_DISCOVERED) { + for (size_t i = 0; i < resp.uuid_count; i++) { + CBUUID *uuid = [CBUUID UUIDWithString:[NSString stringWithUTF8String:resp.uuids[i]]]; + simble_shadow_service(peripheral, uuid); + } + } + SimbleManagerEntry *entry = simble_shadow_manager_entry(simble_shadow_owner(self)); + dispatchOnManagerQueue(entry, ^{ + id delegate = peripheral.delegate; + NSError *error = (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_SERVICES_DISCOVERED) + ? nil + : [NSError errorWithDomain:CBErrorDomain + code:resp.error_code + userInfo:nil]; + if ([delegate respondsToSelector:@selector(peripheral:didDiscoverServices:)]) { + [delegate peripheral:peripheral didDiscoverServices:error]; + } + }); } - (void)simble_discoverCharacteristics:(NSArray *)characteristicUUIDs @@ -369,7 +425,44 @@ - (void)simble_discoverCharacteristics:(NSArray *)characteristicUUIDs [self simble_discoverCharacteristics:characteristicUUIDs forService:service]; return; } - // Characteristic discovery is likewise not routed; a routed discoverCharacteristics is a no-op. + // Route DISCOVER_CHARACTERISTICS, mint a shadow characteristic for each discovered UUID on the + // service, and deliver peripheral:didDiscoverCharacteristics:forService:error:. + uint8_t pid[64]; + size_t idLen = 0; + CBUUID *serviceUUID = nil; + if (!simble_shadow_resolve_service(service, pid, sizeof(pid), &idLen, &serviceUUID)) + return; + const char *svc = serviceUUID.UUIDString.UTF8String; + NSMutableArray *held = [NSMutableArray array]; + const char **uuids = NULL; + size_t *lens = NULL; + size_t count = buildUUIDFilter(characteristicUUIDs, held, &uuids, &lens); + simble_response resp; + simble_status st = simble_client_discover_characteristics(pid, idLen, svc, strlen(svc), uuids, + lens, count, &resp); + free(uuids); + free(lens); + g_stats.discover_characteristics++; + CBPeripheral *peripheral = self; + if (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_CHARS_DISCOVERED) { + for (size_t i = 0; i < resp.uuid_count; i++) { + CBUUID *uuid = [CBUUID UUIDWithString:[NSString stringWithUTF8String:resp.uuids[i]]]; + simble_shadow_characteristic(service, uuid); + } + } + SimbleManagerEntry *entry = simble_shadow_manager_entry(simble_shadow_owner(self)); + dispatchOnManagerQueue(entry, ^{ + id delegate = peripheral.delegate; + NSError *error = (st == SIMBLE_OK && resp.kind == SIMBLE_RESP_CHARS_DISCOVERED) + ? nil + : [NSError errorWithDomain:CBErrorDomain + code:resp.error_code + userInfo:nil]; + if ([delegate + respondsToSelector:@selector(peripheral:didDiscoverCharacteristicsForService:error:)]) { + [delegate peripheral:peripheral didDiscoverCharacteristicsForService:service error:error]; + } + }); } - (void)simble_readValueForCharacteristic:(CBCharacteristic *)characteristic { diff --git a/packages/interpose/src/registry/shadow.h b/packages/interpose/src/registry/shadow.h index 70cc150..8f7fbc2 100644 --- a/packages/interpose/src/registry/shadow.h +++ b/packages/interpose/src/registry/shadow.h @@ -136,6 +136,20 @@ CBService *_Nullable simble_shadow_service(CBPeripheral *peripheral, CBUUID *ser CBCharacteristic *_Nullable simble_shadow_characteristic(CBService *service, CBUUID *characteristicUUID); +/** + * @brief Resolve a minted service to its peripheral id and service UUID, so a discovery on it can + * be routed by host identity. + * + * @param[in] service The minted service. + * @param[out] peripheralOut Buffer the peripheral id is copied into. + * @param[in] peripheralCap Capacity of @p peripheralOut. + * @param[out] peripheralLen Bytes written to @p peripheralOut. + * @param[out] serviceUUID Set to the service CBUUID. + * @return YES on a hit, NO if @p service was not minted here. + */ +BOOL simble_shadow_resolve_service(CBService *service, uint8_t *peripheralOut, size_t peripheralCap, + size_t *peripheralLen, CBUUID *_Nullable *_Nonnull serviceUUID); + /** * @brief Resolve a minted characteristic to its peripheral id, service UUID, and characteristic * UUID, so an operation on it can be routed by host identity. diff --git a/packages/interpose/src/registry/shadow.m b/packages/interpose/src/registry/shadow.m index b10b1c7..6cce46d 100644 --- a/packages/interpose/src/registry/shadow.m +++ b/packages/interpose/src/registry/shadow.m @@ -35,6 +35,8 @@ @interface SimbleShadowMeta : NSObject @property(nonatomic, strong, nullable) CBUUID *characteristicUUID; // characteristic only @property(nonatomic, weak, nullable) CBCentralManager *owner; // owning manager (peripheral) @property(nonatomic, weak, nullable) CBPeripheral *peripheral; // owning peripheral (service) +@property(nonatomic, strong, nullable) + NSMutableArray *children; // attached services or characteristics @end @implementation SimbleShadowMeta @@ -70,6 +72,11 @@ static void shadowDealloc(id self, SEL _cmd) { (void)_cmd; } +// Return the attached child list (services on a peripheral, characteristics on a service) the +// registry minted, snapshotted under the lock. CoreBluetooth's own getter reads private ivars a +// stand-in never had, so the stand-in answers from the registry instead. +static id shadowChildren(id self, SEL _cmd); + // Build a stand-in subclass of a CoreBluetooth class with the no-op dealloc, once per class. static Class makeShadowSubclass(Class base, const char *name) { Class cls = objc_allocateClassPair(base, name, 0); @@ -93,6 +100,12 @@ static Class makeShadowSubclass(Class base, const char *name) { g_serviceShadowClass = makeShadowSubclass([CBService class], "SimbleShadowService"); g_characteristicShadowClass = makeShadowSubclass([CBCharacteristic class], "SimbleShadowCharacteristic"); + // The stand-in peripheral's services and the stand-in service's characteristics answer from the + // registry's attached child list. + class_addMethod(g_peripheralShadowClass, sel_registerName("services"), (IMP)shadowChildren, + "@@:"); + class_addMethod(g_serviceShadowClass, sel_registerName("characteristics"), (IMP)shadowChildren, + "@@:"); } static NSValue *ptr(id object) { return [NSValue valueWithPointer:(__bridge const void *)object]; } @@ -112,6 +125,15 @@ static Class makeShadowSubclass(Class base, const char *name) { return object ? objc_getAssociatedObject(object, kShadowMetaKey) : nil; } +static id shadowChildren(id self, SEL _cmd) { + (void)_cmd; + SimbleShadowMeta *meta = metaOf(self); + [g_lock lock]; + NSArray *snapshot = meta.children ? [meta.children copy] : @[]; + [g_lock unlock]; + return snapshot; +} + void simble_shadow_register_manager(CBCentralManager *manager, id delegate, dispatch_queue_t queue) { if (!manager) @@ -215,6 +237,10 @@ BOOL simble_shadow_peripheral_id(CBPeripheral *peripheral, uint8_t *out, size_t objc_setAssociatedObject(minted, kShadowMetaKey, meta, OBJC_ASSOCIATION_RETAIN_NONATOMIC); g_services[key] = minted; [g_minted addObject:ptr(minted)]; + // Attach the minted service to the peripheral so its services getter returns it. + if (!pmeta.children) + pmeta.children = [NSMutableArray new]; + [pmeta.children addObject:minted]; [g_lock unlock]; return minted; } @@ -244,10 +270,33 @@ BOOL simble_shadow_peripheral_id(CBPeripheral *peripheral, uint8_t *out, size_t objc_setAssociatedObject(minted, kShadowMetaKey, meta, OBJC_ASSOCIATION_RETAIN_NONATOMIC); g_characteristics[key] = minted; [g_minted addObject:ptr(minted)]; + // Attach the minted characteristic to the service so its characteristics getter returns it. + if (!smeta.children) + smeta.children = [NSMutableArray new]; + [smeta.children addObject:minted]; [g_lock unlock]; return minted; } +BOOL simble_shadow_resolve_service(CBService *service, uint8_t *peripheralOut, size_t peripheralCap, + size_t *peripheralLen, CBUUID *_Nullable *_Nonnull serviceUUID) { + if (!service) + return NO; + [g_lock lock]; + BOOL minted = [g_minted containsObject:ptr(service)]; + [g_lock unlock]; + if (!minted) + return NO; + SimbleShadowMeta *meta = metaOf(service); + NSData *pidData = meta.peripheralId; + if (!pidData || (size_t)pidData.length > peripheralCap) + return NO; + memcpy(peripheralOut, pidData.bytes, pidData.length); + *peripheralLen = pidData.length; + *serviceUUID = meta.serviceUUID; + return YES; +} + BOOL simble_shadow_resolve_characteristic(CBCharacteristic *characteristic, uint8_t *peripheralOut, size_t peripheralCap, size_t *peripheralLen, CBUUID *_Nullable *_Nonnull serviceUUID, diff --git a/packages/interpose/src/transport/client.c b/packages/interpose/src/transport/client.c index 15c9238..498798f 100644 --- a/packages/interpose/src/transport/client.c +++ b/packages/interpose/src/transport/client.c @@ -210,6 +210,33 @@ simble_status simble_client_disconnect(const uint8_t *peripheral_id, size_t peri return do_request(payload, n, out); } +simble_status simble_client_discover_services(const uint8_t *peripheral_id, size_t peripheral_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, simble_response *out) { + uint8_t token[32]; + if (read_token(token) != 0) + return SIMBLE_ERR_TRUNCATED; + uint8_t payload[1024]; + int n = simble_encode_discover_services(token, sizeof(token), peripheral_id, peripheral_len, + uuids, uuid_lens, uuid_count, payload, sizeof(payload)); + return do_request(payload, n, out); +} + +simble_status simble_client_discover_characteristics(const uint8_t *peripheral_id, + size_t peripheral_len, const char *service, + size_t service_len, const char *const *uuids, + const size_t *uuid_lens, size_t uuid_count, + simble_response *out) { + uint8_t token[32]; + if (read_token(token) != 0) + return SIMBLE_ERR_TRUNCATED; + uint8_t payload[1024]; + int n = simble_encode_discover_characteristics(token, sizeof(token), peripheral_id, + peripheral_len, service, service_len, uuids, + uuid_lens, uuid_count, payload, sizeof(payload)); + return do_request(payload, n, out); +} + simble_status simble_client_read_characteristic(const uint8_t *peripheral_id, size_t peripheral_len, const char *service, size_t service_len, const char *characteristic, size_t char_len, diff --git a/packages/interpose/src/transport/client.h b/packages/interpose/src/transport/client.h index 23909de..6ae7fff 100644 --- a/packages/interpose/src/transport/client.h +++ b/packages/interpose/src/transport/client.h @@ -114,6 +114,40 @@ simble_status simble_client_connect(const uint8_t *peripheral_id, size_t periphe simble_status simble_client_disconnect(const uint8_t *peripheral_id, size_t peripheral_len, simble_response *out); +/** + * @brief Discover services on a connected peripheral, optionally filtered. + * + * @param[in] peripheral_id Host peripheral identifier bytes. + * @param[in] peripheral_len Length of @p peripheral_id. + * @param[in] uuids Array of UTF-8 service UUID strings to filter on, or NULL. + * @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids. + * @param[in] uuid_count Number of UUIDs; 0 omits the filter. + * @param[out] out The decoded response; ::SIMBLE_RESP_SERVICES_DISCOVERED on success. + * @return ::SIMBLE_OK when a response decoded cleanly, a ::simble_status error otherwise. + */ +simble_status simble_client_discover_services(const uint8_t *peripheral_id, size_t peripheral_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, simble_response *out); + +/** + * @brief Discover characteristics of a service on a connected peripheral, optionally filtered. + * + * @param[in] peripheral_id Host peripheral identifier bytes. + * @param[in] peripheral_len Length of @p peripheral_id. + * @param[in] service Service UUID, UTF-8. + * @param[in] service_len Length of @p service. + * @param[in] uuids Array of UTF-8 characteristic UUID strings to filter on, or NULL. + * @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids. + * @param[in] uuid_count Number of UUIDs; 0 omits the filter. + * @param[out] out The decoded response; ::SIMBLE_RESP_CHARS_DISCOVERED on success. + * @return ::SIMBLE_OK when a response decoded cleanly, a ::simble_status error otherwise. + */ +simble_status simble_client_discover_characteristics(const uint8_t *peripheral_id, + size_t peripheral_len, const char *service, + size_t service_len, const char *const *uuids, + const size_t *uuid_lens, size_t uuid_count, + simble_response *out); + /** * @brief Read a characteristic value from a connected peripheral. * diff --git a/packages/protocol/c/include/simble_protocol.h b/packages/protocol/c/include/simble_protocol.h index 1898b9a..f35c149 100644 --- a/packages/protocol/c/include/simble_protocol.h +++ b/packages/protocol/c/include/simble_protocol.h @@ -135,6 +135,47 @@ int simble_encode_peripheral_command(uint64_t op, const uint8_t *token, size_t t int simble_encode_scan_start(const uint8_t *token, size_t token_len, const char *const *uuids, const size_t *uuid_lens, size_t uuid_count, uint8_t *out, size_t cap); +/** + * @brief Encode a DISCOVER_SERVICES request. + * + * @param[in] token 32-byte capability token. + * @param[in] token_len Length of @p token. + * @param[in] peripheral_id Host peripheral identifier (key 30). + * @param[in] peripheral_len Length of @p peripheral_id. + * @param[in] uuids Array of UTF-8 service UUID strings to filter on, or NULL. + * @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids. + * @param[in] uuid_count Number of UUIDs; 0 omits key 50. + * @param[out] out Buffer the payload is written to. + * @param[in] cap Capacity of @p out. + * @return Bytes written, or -1 if @p cap is too small. + */ +int simble_encode_discover_services(const uint8_t *token, size_t token_len, + const uint8_t *peripheral_id, size_t peripheral_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, uint8_t *out, size_t cap); + +/** + * @brief Encode a DISCOVER_CHARACTERISTICS request. + * + * @param[in] token 32-byte capability token. + * @param[in] token_len Length of @p token. + * @param[in] peripheral_id Host peripheral identifier (key 30). + * @param[in] peripheral_len Length of @p peripheral_id. + * @param[in] service Service UUID, UTF-8 (key 31). + * @param[in] service_len Length of @p service. + * @param[in] uuids Array of UTF-8 characteristic UUID strings to filter on, or NULL. + * @param[in] uuid_lens Per-UUID lengths, parallel to @p uuids. + * @param[in] uuid_count Number of UUIDs; 0 omits key 51. + * @param[out] out Buffer the payload is written to. + * @param[in] cap Capacity of @p out. + * @return Bytes written, or -1 if @p cap is too small. + */ +int simble_encode_discover_characteristics(const uint8_t *token, size_t token_len, + const uint8_t *peripheral_id, size_t peripheral_len, + const char *service, size_t service_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, uint8_t *out, size_t cap); + /** * @brief Encode a READ_CHARACTERISTIC request. * @@ -245,9 +286,18 @@ typedef enum { SIMBLE_RESP_CHAR_VALUE, ///< READ_CHARACTERISTIC ok: peripheral, service, characteristic, ///< value are set. SIMBLE_RESP_NOTIFY_STATE, ///< SET_NOTIFY ok: peripheral, service, characteristic, notify set. + SIMBLE_RESP_SERVICES_DISCOVERED, ///< DISCOVER_SERVICES ok: peripheral and the uuids list are set. + SIMBLE_RESP_CHARS_DISCOVERED, ///< DISCOVER_CHARACTERISTICS ok: peripheral, service, and the uuids + ///< list are set. SIMBLE_RESP_ERROR, ///< The helper returned an error: error and error_code are set. } simble_resp_kind; +/** The most discovered UUIDs a single discover response surfaces. */ +#define SIMBLE_MAX_UUIDS 32 + +/** The buffer a single discovered UUID is copied into, NUL-terminated. */ +#define SIMBLE_UUID_CAP 40 + /** * A decoded response. Fixed buffers, no ownership: the decoder copies out of the * payload, and lengths say how much of each buffer is meaningful for the kind. @@ -265,8 +315,11 @@ typedef struct { size_t value_len; ///< Meaningful bytes in @c value. int64_t rssi; ///< RSSI on a READ_RSSI response. int notify; ///< Notification state on a SET_NOTIFY response: 0 or 1. - char error[256]; ///< NUL-terminated reason on an error; never load-bearing. - int64_t error_code; ///< CBError/CBATTError code on an error response, 0 otherwise. + char uuids[SIMBLE_MAX_UUIDS][SIMBLE_UUID_CAP]; ///< Discovered UUIDs, NUL-terminated, on a + ///< discover response. + size_t uuid_count; ///< Meaningful entries in @c uuids. + char error[256]; ///< NUL-terminated reason on an error; never load-bearing. + int64_t error_code; ///< CBError/CBATTError code on an error response, 0 otherwise. } simble_response; /** diff --git a/packages/protocol/c/src/simble_protocol.c b/packages/protocol/c/src/simble_protocol.c index cfb74f1..800e757 100644 --- a/packages/protocol/c/src/simble_protocol.c +++ b/packages/protocol/c/src/simble_protocol.c @@ -235,6 +235,38 @@ int simble_encode_scan_start(const uint8_t *token, size_t token_len, const char return w.overflow ? -1 : (int)w.pos; } +int simble_encode_discover_services(const uint8_t *token, size_t token_len, + const uint8_t *peripheral_id, size_t peripheral_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, uint8_t *out, size_t cap) { + int has_filter = uuids && uuid_count > 0; + writer w = {out, cap, 0, 0}; + w_head(&w, CBOR_MAP, 3 + has_filter); + w_kv_uint(&w, K_OP, OP_DISCOVER_SERVICES); + w_kv_bytes(&w, K_TOKEN, token, token_len); + w_kv_bytes(&w, K_PERIPHERAL, peripheral_id, peripheral_len); + if (has_filter) + w_kv_text_array(&w, K_SERVICE_UUIDS, uuids, uuid_lens, uuid_count); + return w.overflow ? -1 : (int)w.pos; +} + +int simble_encode_discover_characteristics(const uint8_t *token, size_t token_len, + const uint8_t *peripheral_id, size_t peripheral_len, + const char *service, size_t service_len, + const char *const *uuids, const size_t *uuid_lens, + size_t uuid_count, uint8_t *out, size_t cap) { + int has_filter = uuids && uuid_count > 0; + writer w = {out, cap, 0, 0}; + w_head(&w, CBOR_MAP, 4 + has_filter); + w_kv_uint(&w, K_OP, OP_DISCOVER_CHARACTERISTICS); + w_kv_bytes(&w, K_TOKEN, token, token_len); + w_kv_bytes(&w, K_PERIPHERAL, peripheral_id, peripheral_len); + w_kv_text(&w, K_SERVICE, service, service_len); + if (has_filter) + w_kv_text_array(&w, K_CHARACTERISTIC_UUIDS, uuids, uuid_lens, uuid_count); + return w.overflow ? -1 : (int)w.pos; +} + int simble_encode_read_characteristic(const uint8_t *token, size_t token_len, const uint8_t *peripheral_id, size_t peripheral_len, const char *service, size_t service_len, @@ -470,6 +502,37 @@ static simble_status copy_text(const entry *e, char *dst, size_t cap) { return SIMBLE_OK; } +// Copy an array of text strings into fixed NUL-terminated UUID buffers. The entry's span covers the +// array's encoded elements (recorded by r_map) and its uintval is the element count; this re-walks +// the span as text strings. An array longer than SIMBLE_MAX_UUIDS or a UUID longer than its buffer +// fails with SIMBLE_ERR_BUFFER. The discover responses carry the discovered UUIDs this way. +static simble_status copy_text_array(const entry *e, char uuids[][SIMBLE_UUID_CAP], size_t cap, + size_t *out_count) { + if (!e || e->major != CBOR_ARRAY) + return SIMBLE_ERR_MISSING; + if (e->uintval > cap) + return SIMBLE_ERR_BUFFER; + reader r = {e->span, e->span_len, 0}; + for (uint64_t i = 0; i < e->uintval; i++) { + uint8_t m; + uint64_t a; + simble_status st = r_head(&r, &m, &a); + if (st != SIMBLE_OK) + return st; + if (m != CBOR_TEXT) + return SIMBLE_ERR_TYPE; + if (a > r.len - r.off) + return SIMBLE_ERR_TRUNCATED; + if (a >= SIMBLE_UUID_CAP) + return SIMBLE_ERR_BUFFER; + memcpy(uuids[i], r.p + r.off, (size_t)a); + uuids[i][a] = '\0'; + r.off += (size_t)a; + } + *out_count = (size_t)e->uintval; + return SIMBLE_OK; +} + // Read a bounded signed code from an int entry (key 10 / RSSI / TX power). An out-of-range or // missing value yields 0 and clears the presence flag, so a hostile code never traps. static int64_t read_int(const entry *e, int *present) { @@ -607,10 +670,30 @@ simble_status simble_decode_response(const uint8_t *payload, size_t len, simble_ out->notify = flag->uintval != 0 ? 1 : 0; return SIMBLE_OK; } + case OP_DISCOVER_SERVICES: { + out->kind = SIMBLE_RESP_SERVICES_DISCOVERED; + st = copy_bytes(find(entries, count, K_PERIPHERAL), out->peripheral, sizeof(out->peripheral), + &out->peripheral_len); + if (st != SIMBLE_OK) + return st; + return copy_text_array(find(entries, count, K_SERVICE_UUIDS), out->uuids, SIMBLE_MAX_UUIDS, + &out->uuid_count); + } + case OP_DISCOVER_CHARACTERISTICS: { + out->kind = SIMBLE_RESP_CHARS_DISCOVERED; + st = copy_bytes(find(entries, count, K_PERIPHERAL), out->peripheral, sizeof(out->peripheral), + &out->peripheral_len); + if (st != SIMBLE_OK) + return st; + st = copy_text(find(entries, count, K_SERVICE), out->service, sizeof(out->service)); + if (st != SIMBLE_OK) + return st; + return copy_text_array(find(entries, count, K_CHARACTERISTIC_UUIDS), out->uuids, + SIMBLE_MAX_UUIDS, &out->uuid_count); + } default: - // DISCOVER_SERVICES / DISCOVER_CHARACTERISTICS / ADD_SERVICE / REMOVE_SERVICE carry array or - // service fields the interposer reads through a typed accessor, not this flat decoder. They - // are valid responses; a caller that does not request them gets SIMBLE_ERR_OPCODE here. + // ADD_SERVICE / REMOVE_SERVICE carry a service field the interposer reads through a typed + // accessor, not this flat decoder. A caller that does not request them gets SIMBLE_ERR_OPCODE. return SIMBLE_ERR_OPCODE; } } diff --git a/packages/protocol/c/tests/protocol_test.c b/packages/protocol/c/tests/protocol_test.c index 4d6728a..6b3dbcf 100644 --- a/packages/protocol/c/tests/protocol_test.c +++ b/packages/protocol/c/tests/protocol_test.c @@ -82,6 +82,39 @@ int main(void) { memcmp(buf + 38, connect_tail, sizeof(connect_tail)) == 0, "connect parity"); + // DISCOVER_SERVICES with a filter: map(4), ending peripheral (30) then key 50 array(1) "180D". + const char *svc_uuids[] = {"180D"}; + size_t svc_uuid_lens[] = {4}; + n = simble_encode_discover_services(token, 32, pid, 4, svc_uuids, svc_uuid_lens, 1, buf, + sizeof(buf)); + uint8_t disc_svc_tail[] = {0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, + 0x32, 0x81, 0x64, 0x31, 0x38, 0x30, 0x44}; + CHECK(n == 53 && buf[0] == 0xA4 && buf[2] == 0x07 && + memcmp(buf + 38, disc_svc_tail, sizeof(disc_svc_tail)) == 0, + "discover_services filter parity"); + + // DISCOVER_SERVICES with no filter is the peripheral-directed shape with op 7. + n = simble_encode_discover_services(token, 32, pid, 4, NULL, NULL, 0, buf, sizeof(buf)); + CHECK(n == 45 && buf[0] == 0xA3 && buf[2] == 0x07 && buf[38] == 0x18 && buf[39] == 0x1E, + "discover_services no filter"); + + // DISCOVER_CHARACTERISTICS with a filter: map(5), peripheral (30), service (31), key 51 array(1). + const char *chr_uuids[] = {"2A37"}; + size_t chr_uuid_lens[] = {4}; + n = simble_encode_discover_characteristics(token, 32, pid, 4, "180D", 4, chr_uuids, chr_uuid_lens, + 1, buf, sizeof(buf)); + uint8_t disc_chr_tail[] = {0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x1F, 0x64, 0x31, + 0x38, 0x30, 0x44, 0x18, 0x33, 0x81, 0x64, 0x32, 0x41, 0x33, 0x37}; + CHECK(n == 60 && buf[0] == 0xA5 && buf[2] == 0x08 && + memcmp(buf + 38, disc_chr_tail, sizeof(disc_chr_tail)) == 0, + "discover_characteristics filter parity"); + + // DISCOVER_CHARACTERISTICS with no filter drops key 51: map(4), ending service (31). + n = simble_encode_discover_characteristics(token, 32, pid, 4, "180D", 4, NULL, NULL, 0, buf, + sizeof(buf)); + CHECK(n == 52 && buf[0] == 0xA4 && buf[2] == 0x08 && buf[n - 7] == 0x18 && buf[n - 6] == 0x1F, + "discover_characteristics no filter"); + // READ_CHARACTERISTIC: map(5). n = simble_encode_read_characteristic(token, 32, pid, 4, "180D", 4, "2A37", 4, buf, sizeof(buf)); uint8_t read_tail[] = {0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x1F, 0x64, 0x31, @@ -145,6 +178,21 @@ int main(void) { memcmp(resp.peripheral, pid, 4) == 0, "decode connected response"); + uint8_t resp_services[] = {0xA4, 0x00, 0x07, 0x01, 0x00, 0x18, 0x1E, 0x44, 0x01, 0x02, + 0x03, 0x04, 0x18, 0x32, 0x81, 0x64, 0x31, 0x38, 0x30, 0x44}; + CHECK(simble_decode_response(resp_services, sizeof(resp_services), &resp) == SIMBLE_OK && + resp.kind == SIMBLE_RESP_SERVICES_DISCOVERED && resp.peripheral_len == 4 && + resp.uuid_count == 1 && strcmp(resp.uuids[0], "180D") == 0, + "decode services_discovered response"); + + uint8_t resp_chars[] = {0xA5, 0x00, 0x08, 0x01, 0x00, 0x18, 0x1E, 0x44, 0x01, + 0x02, 0x03, 0x04, 0x18, 0x1F, 0x64, 0x31, 0x38, 0x30, + 0x44, 0x18, 0x33, 0x81, 0x64, 0x32, 0x41, 0x33, 0x37}; + CHECK(simble_decode_response(resp_chars, sizeof(resp_chars), &resp) == SIMBLE_OK && + resp.kind == SIMBLE_RESP_CHARS_DISCOVERED && strcmp(resp.service, "180D") == 0 && + resp.uuid_count == 1 && strcmp(resp.uuids[0], "2A37") == 0, + "decode characteristics_discovered response"); + uint8_t resp_rssi[] = {0xA4, 0x00, 0x0C, 0x01, 0x00, 0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x22, 0x38, 0x29}; CHECK(simble_decode_response(resp_rssi, sizeof(resp_rssi), &resp) == SIMBLE_OK && diff --git a/packages/protocol/swift/Tests/SimBLEProtocolTests/WireTests.swift b/packages/protocol/swift/Tests/SimBLEProtocolTests/WireTests.swift index 984de00..eb6d280 100644 --- a/packages/protocol/swift/Tests/SimBLEProtocolTests/WireTests.swift +++ b/packages/protocol/swift/Tests/SimBLEProtocolTests/WireTests.swift @@ -262,6 +262,34 @@ final class WireTests: XCTestCase { XCTAssertEqual(Wire.encode(.connect(peripheralId: pid), token: token), want) } + func testDiscoverServicesMatchesCBytes() { + let want = Data([0xA4, 0x00, 0x07, 0x07, 0x58, 0x20] + [UInt8](repeating: 0xAB, count: 32) + + [0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x32, 0x81, 0x64, 0x31, 0x38, 0x30, 0x44]) + XCTAssertEqual(Wire.encode(.discoverServices(peripheralId: pid, serviceUUIDs: ["180D"]), + token: token), want) + } + + func testDiscoverCharacteristicsMatchesCBytes() { + let want = Data([0xA5, 0x00, 0x08, 0x07, 0x58, 0x20] + [UInt8](repeating: 0xAB, count: 32) + + [0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x1F, 0x64, 0x31, 0x38, 0x30, 0x44, + 0x18, 0x33, 0x81, 0x64, 0x32, 0x41, 0x33, 0x37]) + let payload = Wire.encode(.discoverCharacteristics(peripheralId: pid, serviceUUID: "180D", + characteristicUUIDs: ["2A37"]), token: token) + XCTAssertEqual(payload, want) + } + + func testDiscoverResponsesMatchCBytes() { + let services = Data([0xA4, 0x00, 0x07, 0x01, 0x00, 0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, + 0x18, 0x32, 0x81, 0x64, 0x31, 0x38, 0x30, 0x44]) + XCTAssertEqual(Wire.encode(.servicesDiscovered(peripheralId: pid, serviceUUIDs: ["180D"])), + services) + let chars = Data([0xA5, 0x00, 0x08, 0x01, 0x00, 0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, + 0x18, 0x1F, 0x64, 0x31, 0x38, 0x30, 0x44, 0x18, 0x33, 0x81, 0x64, 0x32, + 0x41, 0x33, 0x37]) + XCTAssertEqual(Wire.encode(.characteristicsDiscovered(peripheralId: pid, serviceUUID: "180D", + characteristicUUIDs: ["2A37"])), chars) + } + func testReadCharacteristicMatchesCBytes() { let want = Data([0xA5, 0x00, 0x09, 0x07, 0x58, 0x20] + [UInt8](repeating: 0xAB, count: 32) + [0x18, 0x1E, 0x44, 0x01, 0x02, 0x03, 0x04, 0x18, 0x1F, 0x64, 0x31, 0x38, 0x30, 0x44,