diff --git a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m index 5174ad6d..a3844225 100644 --- a/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m +++ b/internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.m @@ -57,6 +57,9 @@ static NSData *PrefixLengthData(NSData *data) { @property(nonatomic) BOOL handledReceivedL2CAPRequestDataConnectionPacket; @property(nonatomic) BOOL handledReceivedL2CAPResponseDataConnectionReadyPacket; @property(nonatomic) BOOL handledReceivedBLEIntroPacket; +@property(nonatomic) NSUInteger expectedDataLength; +@property(nonatomic) NSMutableData *undeliveredData; +@property(nonatomic) BOOL verboseLoggingEnabled; @end @implementation GNCBLEL2CAPConnection @@ -75,6 +78,8 @@ static NSData *PrefixLengthData(NSData *data) { dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1))]; [connection setIncomingConnection:incomingConnection]; + [connection setUndeliveredData:[NSMutableData data]]; + [connection setExpectedDataLength:0]; return connection; } @@ -91,7 +96,9 @@ static NSData *PrefixLengthData(NSData *data) { // Prefix the service ID hash. packet = PrefixLengthData(PrefixDataWithServiceIDHash(_serviceIDHash, data)); - GTMLoggerInfo(@"[NEARBY] GNCBLEL2CAPConnection data to be sent: %@", [packet description]); + if (_verboseLoggingEnabled) { + GTMLoggerDebug(@"[NEARBY] GNCBLEL2CAPConnection data to be sent: %@", [packet description]); + } [_stream sendData:packet completionBlock:^(BOOL result) { dispatch_async(_callbackQueue, ^{ @@ -104,45 +111,33 @@ static NSData *PrefixLengthData(NSData *data) { #pragma mark GNCBLEL2CAPStreamDelegate - (void)stream:(GNCBLEL2CAPStream *)stream didReceiveData:(NSData *)data { - GTMLoggerDebug(@"[NEARBY] BLEL2CAPConnection didReceiveData, data: %@", [data description]); - - NSData *realData = [self extractRealDataFromData:data]; - if (realData == nil) { - return; + if (_verboseLoggingEnabled) { + GTMLoggerDebug(@"[NEARBY] BLEL2CAPConnection didReceiveData, data: %@, length: %lu", + [data debugDescription], data.length); } - - // Validate the L2CAP packet. - // TODO: b/399815436 - Refactor the validation logic to connections layer. - if (_incomingConnection) { - if (!_handledIncomingConnectionL2CAPPacket) { - [self handleIncomingConnectionL2CAPPacketFromData:realData]; - return; - } - if (!_handledReceivedBLEIntroPacket) { - [self handleBLEIntroPacketFromData:realData]; - return; - } - } else { - if (!_handledOutgoingConnectionL2CAPPacket) { - [self handleOutgoingConnectionL2CAPPacketFromData:realData]; - return; - } - } - - // Extract the service ID prefix from each data packet and validate it. - NSUInteger prefixLength = _serviceIDHash.length; - if (![[realData subdataWithRange:NSMakeRange(0, prefixLength)] isEqual:_serviceIDHash]) { - GTMLoggerInfo(@"[NEARBY]: Received wrong data packet and discarded"); + if (!data.length) { return; } + __weak __typeof__(self) weakSelf = self; dispatch_async(_selfQueue, ^{ - if (_connectionHandlers.payloadHandler) { - dispatch_async(_callbackQueue, ^{ - _connectionHandlers.payloadHandler([NSData - dataWithData:[realData subdataWithRange:NSMakeRange(prefixLength, - realData.length - prefixLength)]]); - }); + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [strongSelf->_undeliveredData appendData:data]; + + while (strongSelf->_undeliveredData.length) { + NSUInteger bytesProcessed = [self processReceivedPacket]; + if (bytesProcessed == 0) { + // Don't have a full packet yet, wait for more data from device. + break; + } + // Clear processed data. + strongSelf->_expectedDataLength = 0; + [strongSelf->_undeliveredData replaceBytesInRange:NSMakeRange(0, bytesProcessed) + withBytes:nil + length:0]; } }); } @@ -159,20 +154,79 @@ static NSData *PrefixLengthData(NSData *data) { #pragma mark Private +- (NSUInteger)processReceivedPacket { + dispatch_assert_queue(_selfQueue); + + NSUInteger bytesProcessed = 0; + NSData *realData = [self extractRealDataFromData:_undeliveredData]; + if (!realData) { + return bytesProcessed; + } + bytesProcessed = realData.length + kL2CAPPacketLength; + + // Validate the L2CAP packet. + // TODO: b/399815436 - Refactor the validation logic to connections layer. + if (_incomingConnection) { + if (!_handledIncomingConnectionL2CAPPacket) { + [self handleIncomingConnectionL2CAPPacketFromData:realData]; + return bytesProcessed; + } + if (!_handledReceivedBLEIntroPacket) { + [self handleBLEIntroPacketFromData:realData]; + return bytesProcessed; + } + } else { + if (!_handledOutgoingConnectionL2CAPPacket) { + [self handleOutgoingConnectionL2CAPPacketFromData:realData]; + return bytesProcessed; + } + } + + // Extract the service ID prefix from each data packet and validate it. + NSUInteger prefixLength = _serviceIDHash.length; + if (![[realData subdataWithRange:NSMakeRange(0, prefixLength)] isEqual:_serviceIDHash]) { + GTMLoggerError(@"[NEARBY]: Received wrong data packet and discarded"); + return bytesProcessed; + } + + if (_connectionHandlers.payloadHandler) { + dispatch_async(_callbackQueue, ^{ + _connectionHandlers.payloadHandler([NSData + dataWithData:[realData subdataWithRange:NSMakeRange(prefixLength, + realData.length - prefixLength)]]); + }); + } + return bytesProcessed; +} + +/// Checks whether |data| at least has length |_expectedDataLength| and returns the real data; +/// Returns nil in case it has not enough data. - (NSData *_Nullable)extractRealDataFromData:(NSData *)data { - if (data.length < kL2CAPPacketLength) { - GTMLoggerError(@"[NEARBY] Packet length mismatch. Expected: > %d, Actual: %lu", - kL2CAPPacketLength, data.length); - return nil; + // Store the expected data length if it is not set yet. + if (!_expectedDataLength) { + if (data.length < kL2CAPPacketLength) { + GTMLoggerError(@"[NEARBY] Data length mismatch. Expected: > %d, Actual: %lu", + kL2CAPPacketLength, data.length); + return nil; + } + _expectedDataLength = CFSwapInt32BigToHost( + *(int *)([[data subdataWithRange:NSMakeRange(0, kL2CAPPacketLength)] bytes])); } - int realDataLength = CFSwapInt32BigToHost( - *(int *)([[data subdataWithRange:NSMakeRange(0, kL2CAPPacketLength)] bytes])); - if (realDataLength != (data.length - kL2CAPPacketLength)) { - GTMLoggerError(@"[NEARBY] Data length mismatch. Expected: %d, Actual: %lu", realDataLength, - data.length - kL2CAPPacketLength); + NSUInteger realDataLength = data.length - kL2CAPPacketLength; + if (realDataLength < _expectedDataLength) { + GTMLoggerError(@"[NEARBY] Insufficient data. Expected: %lu, Actual: %lu", _expectedDataLength, + realDataLength); return nil; + } else if (realDataLength == _expectedDataLength) { + return [data subdataWithRange:NSMakeRange(kL2CAPPacketLength, realDataLength)]; + } else { + // This is a case where the data length is larger than the expected length. + // The first |_expectedDataLength| bytes are copied and used as the actual data. The + // remaining bytes will be stored in _undeliveredData for future use. + GTMLoggerInfo(@"[NEARBY] Data length mismatch. Expected: %lu, Actual: %lu", _expectedDataLength, + realDataLength); + return [data subdataWithRange:NSMakeRange(kL2CAPPacketLength, _expectedDataLength)]; } - return [data subdataWithRange:NSMakeRange(kL2CAPPacketLength, data.length - kL2CAPPacketLength)]; } - (void)handleIncomingConnectionL2CAPPacketFromData:(NSData *)data { @@ -182,17 +236,11 @@ static NSData *PrefixLengthData(NSData *data) { } switch (l2capPacket.command) { case GNCMBLEL2CAPCommandRequestDataConnection: { - NSData *packet = PrefixLengthData( - GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommandResponseDataConnectionReady, nil)); - __weak __typeof__(self) weakSelf = self; dispatch_async(_selfQueue, ^{ - __typeof__(self) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - [strongSelf->_stream sendData:packet - completionBlock:^(BOOL result){ - }]; + [_stream sendData:PrefixLengthData(GNCMGenerateBLEL2CAPPacket( + GNCMBLEL2CAPCommandResponseDataConnectionReady, nil)) + completionBlock:^(BOOL result){ + }]; }); _handledReceivedL2CAPRequestDataConnectionPacket = YES; } break; @@ -216,16 +264,10 @@ static NSData *PrefixLengthData(NSData *data) { switch (l2capPacket.command) { case GNCMBLEL2CAPCommandResponseDataConnectionReady: { _handledReceivedL2CAPResponseDataConnectionReadyPacket = YES; - NSData *introData = GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash); - __weak __typeof__(self) weakSelf = self; dispatch_async(_selfQueue, ^{ - __strong __typeof__(weakSelf) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - [strongSelf->_stream sendData:introData - completionBlock:^(BOOL result){ - }]; + [_stream sendData:PrefixLengthData(GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash)) + completionBlock:^(BOOL result){ + }]; }); } break; case GNCMBLEL2CAPCommandRequestAdvertisement: diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index 7a655712..4ff67ab7 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -29,6 +29,7 @@ objc_library( "GNCBLEGATTServer+Testing.h", "GNCBLEGATTServerTest.m", "GNCBLEL2CAPClientTest.m", + "GNCBLEL2CAPConnectionTest.m", "GNCBLEL2CAPFakeInputOutputStream.h", "GNCBLEL2CAPFakeInputOutputStream.m", "GNCBLEL2CAPServerTest.m", diff --git a/internal/platform/implementation/apple/Tests/GNCBLEL2CAPConnectionTest.m b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPConnectionTest.m new file mode 100644 index 00000000..d0cfd19a --- /dev/null +++ b/internal/platform/implementation/apple/Tests/GNCBLEL2CAPConnectionTest.m @@ -0,0 +1,134 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPConnection.h" + +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLEv2/GNCBLEL2CAPStream.h" +#import "internal/platform/implementation/apple/Tests/GNCBLEL2CAPFakeInputOutputStream.h" + +// TODO: b/399815436 - More tests for GNCBLEL2CAPConnection. +@interface GNCBLEL2CAPConnection (Testing) +- (NSData *_Nullable)extractRealDataFromData:(NSData *)data; +@property(nonatomic) NSUInteger expectedDataLength; +@property(nonatomic) dispatch_queue_t selfQueue; +@end + +@interface GNCBLEL2CAPConnectionTest : XCTestCase +@property(nonatomic) GNCBLEL2CAPFakeInputOutputStream *fakeInputOutputStream; +@property(nonatomic) GNCBLEL2CAPStream *stream; +@property(nonatomic) GNCBLEL2CAPConnection *connection; +@property(nonatomic) dispatch_queue_t testCallbackQueue; +@property(nonatomic) NSString *serviceID; +@end + +@implementation GNCBLEL2CAPConnectionTest + +- (void)setUp { + [super setUp]; + _fakeInputOutputStream = [[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:100]; + _stream = [[GNCBLEL2CAPStream alloc] + initWithClosedBlock:^{ + } + inputStream:_fakeInputOutputStream.inputStream + outputStream:_fakeInputOutputStream.outputStream]; + _testCallbackQueue = dispatch_queue_create("com.google.test.callback", DISPATCH_QUEUE_SERIAL); + _serviceID = @"testServiceID"; +} + +- (void)tearDown { + _connection = nil; + [_stream tearDown]; + [_fakeInputOutputStream tearDown]; + _stream = nil; + [super tearDown]; +} + +#pragma mark - Test Helpers + +- (GNCBLEL2CAPConnection *)createConnectionWithIncoming:(BOOL)incoming { + GNCBLEL2CAPConnection *connection = + [GNCBLEL2CAPConnection connectionWithStream:_stream + serviceID:_serviceID + incomingConnection:incoming + callbackQueue:_testCallbackQueue]; + // Wait for the selfQueue to be created + dispatch_sync(connection.selfQueue, ^{ + }); + return connection; +} + +- (NSData *)createDataWithLength:(NSUInteger)length prefix:(NSData *)prefix { + NSMutableData *data = [NSMutableData dataWithLength:length]; + if (prefix) { + [data appendData:prefix]; + } + return data; +} + +- (NSData *)prefixLengthData:(NSData *)data { + uint32_t dataLength = (uint32_t)data.length; + uint32_t lengthBigEndian = CFSwapInt32HostToBig(dataLength); + + NSMutableData *packet = [NSMutableData dataWithCapacity:sizeof(uint32_t)]; + [packet appendBytes:&lengthBigEndian length:sizeof(uint32_t)]; + [packet appendData:data]; + + return packet; +} + +- (void)waitForCallbackQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for callback queue"]; + dispatch_async(_testCallbackQueue, ^{ + [expectation fulfill]; + }); + [self waitForExpectations:@[ expectation ] timeout:1]; +} + +#pragma mark - Tests + +- (void)testExtractRealDataFromData_validData { + _connection = [self createConnectionWithIncoming:YES]; + NSData *testData = [self createDataWithLength:10 prefix:nil]; + NSData *prefixData = [self prefixLengthData:[self createDataWithLength:testData.length + prefix:nil]]; + _connection.expectedDataLength = testData.length; + NSData *realData = [_connection extractRealDataFromData:prefixData]; + XCTAssertEqualObjects(realData, testData); +} + +- (void)testExtractRealDataFromData_notEnoughData { + _connection = [self createConnectionWithIncoming:YES]; + NSData *prefixData = [self createDataWithLength:2 prefix:nil]; + NSData *realData = [_connection extractRealDataFromData:prefixData]; + XCTAssertNil(realData); +} + +- (void)testExtractRealDataFromData_moreThanExpectedData { + _connection = [self createConnectionWithIncoming:YES]; + NSData *testData = [self createDataWithLength:10 prefix:nil]; + NSData *moreData = [self createDataWithLength:5 prefix:nil]; + NSMutableData *combinedData = [NSMutableData dataWithData:testData]; + [combinedData appendData:moreData]; + NSData *prefixData = [self prefixLengthData:[self createDataWithLength:combinedData.length + prefix:nil]]; + _connection.expectedDataLength = testData.length; + NSData *receivedData = [NSMutableData dataWithData:prefixData]; + NSData *realData = [_connection extractRealDataFromData:receivedData]; + XCTAssertEqualObjects(realData, testData); +} + +@end