| // Copyright 2017 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. |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/advertising_data.h" |
| |
| #include <limits> |
| #include <variant> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/log.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/random.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/uuid.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" |
| #include "src/connectivity/bluetooth/lib/cpp-string/string_printf.h" |
| |
| namespace bt { |
| namespace { |
| |
| constexpr uint16_t kGattUuid = 0x1801; |
| constexpr uint16_t kEddystoneUuid = 0xFEAA; |
| constexpr uint16_t kHeartRateServiceUuid = 0x180D; |
| |
| constexpr uint16_t kId1As16 = 0x0212; |
| constexpr uint16_t kId2As16 = 0x1122; |
| |
| constexpr size_t kRandomDataSize = 100; |
| |
| TEST(AdvertisingDataTest, MakeEmpty) { |
| AdvertisingData data; |
| |
| EXPECT_EQ(0u, data.CalculateBlockSize()); |
| } |
| |
| TEST(AdvertisingDataTest, CopyLeavesNoRemnants) { |
| AdvertisingData data; |
| data.SetFlags(0x4); |
| data.SetTxPower(4); |
| data.SetAppearance(0x4567); |
| EXPECT_TRUE(data.SetLocalName("fuchsia")); |
| EXPECT_TRUE(data.AddUri("http://fuchsia.cl")); |
| |
| StaticByteBuffer bytes(0x01, 0x02, 0x03); |
| EXPECT_TRUE(data.SetManufacturerData(0x0123, bytes.view())); |
| |
| auto service_uuid = UUID(kId1As16); |
| EXPECT_TRUE(data.AddServiceUuid(service_uuid)); |
| |
| StaticByteBuffer service_bytes(0x01, 0x02); |
| EXPECT_TRUE(data.SetServiceData(service_uuid, service_bytes.view())); |
| |
| AdvertisingData new_data; |
| new_data.SetTxPower(1); |
| new_data.Copy(&data); |
| |
| EXPECT_EQ(1, data.tx_power().value()); |
| EXPECT_FALSE(data.appearance().has_value()); |
| EXPECT_FALSE(data.local_name().has_value()); |
| EXPECT_FALSE(data.flags().has_value()); |
| EXPECT_EQ(0u, data.uris().size()); |
| EXPECT_EQ(0u, data.manufacturer_data_ids().size()); |
| EXPECT_EQ(0u, data.service_uuids().size()); |
| EXPECT_EQ(0u, data.service_data_uuids().size()); |
| } |
| |
| TEST(AdvertisingDataTest, EncodeKnownURI) { |
| AdvertisingData data; |
| EXPECT_TRUE(data.AddUri("https://abc.xyz")); |
| |
| StaticByteBuffer bytes( |
| 0x0B, 0x24, 0x17, '/', '/', 'a', 'b', 'c', '.', 'x', 'y', 'z'); |
| |
| EXPECT_EQ(bytes.size(), data.CalculateBlockSize()); |
| DynamicByteBuffer block(data.CalculateBlockSize()); |
| data.WriteBlock(&block, std::nullopt); |
| EXPECT_TRUE(ContainersEqual(bytes, block)); |
| } |
| |
| TEST(AdvertisingDataTest, EncodeUnknownURI) { |
| AdvertisingData data; |
| EXPECT_TRUE(data.AddUri("flubs:xyz")); |
| |
| StaticByteBuffer bytes( |
| 0x0B, 0x24, 0x01, 'f', 'l', 'u', 'b', 's', ':', 'x', 'y', 'z'); |
| |
| size_t block_size = data.CalculateBlockSize(); |
| EXPECT_EQ(bytes.size(), block_size); |
| DynamicByteBuffer block(block_size); |
| data.WriteBlock(&block, std::nullopt); |
| EXPECT_TRUE(ContainersEqual(bytes, block)); |
| } |
| |
| TEST(AdvertisingDataTest, CompressServiceUUIDs) { |
| AdvertisingData data; |
| std::unordered_set<UUID> uuids{UUID(kId1As16), UUID(kId2As16)}; |
| for (auto& uuid : uuids) { |
| SCOPED_TRACE(bt_str(uuid)); |
| EXPECT_TRUE(data.AddServiceUuid(uuid)); |
| } |
| |
| uint8_t expected_block_size = 1 // length byte |
| + 1 // type byte |
| + (sizeof(uint16_t) * 2); // 2 16-bit UUIDs |
| EXPECT_EQ(expected_block_size, data.CalculateBlockSize()); |
| |
| StaticByteBuffer expected_header{expected_block_size - 1, |
| DataType::kIncomplete16BitServiceUuids}; |
| |
| DynamicByteBuffer block(expected_block_size); |
| data.WriteBlock(&block, std::nullopt); |
| |
| EXPECT_TRUE( |
| ContainersEqual(expected_header, block.view(/*pos=*/0, /*size=*/2))); |
| auto to_uuid = [](const ByteBuffer& b, size_t pos) { |
| return UUID(b.view(pos, /*size=*/2).To<uint16_t>()); |
| }; |
| EXPECT_TRUE(uuids.find(to_uuid(block, 2)) != uuids.end()); |
| EXPECT_TRUE(uuids.find(to_uuid(block, 4)) != uuids.end()); |
| } |
| |
| TEST(AdvertisingDataTest, ParseBlock) { |
| StaticByteBuffer bytes( |
| // Complete 16-bit UUIDs |
| 0x05, |
| 0x03, |
| 0x12, |
| 0x02, |
| 0x22, |
| 0x11, |
| // Incomplete list of 32-bit UUIDs |
| 0x05, |
| 0x04, |
| 0x34, |
| 0x12, |
| 0x34, |
| 0x12, |
| // Local name |
| 0x09, |
| 0x09, |
| 'T', |
| 'e', |
| 's', |
| 't', |
| 0xF0, |
| 0x9F, |
| 0x92, |
| 0x96, |
| // TX Power |
| 0x02, |
| 0x0A, |
| 0x8F); |
| |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), data); |
| |
| EXPECT_EQ(3u, data->service_uuids().size()); |
| EXPECT_TRUE(data->local_name()); |
| EXPECT_EQ("Test💖", data->local_name()->name); |
| EXPECT_TRUE(data->tx_power()); |
| EXPECT_EQ(-113, *(data->tx_power())); |
| } |
| |
| TEST(AdvertisingDataTest, ParseBlockUnknownDataType) { |
| AdvertisingData expected_ad; |
| constexpr uint8_t lower_byte = 0x12, upper_byte = 0x22; |
| constexpr uint16_t uuid_value = (upper_byte << 8) + lower_byte; |
| // The only field present in the expected AD is one complete 16-bit UUID. |
| EXPECT_TRUE(expected_ad.AddServiceUuid(UUID(uuid_value))); |
| |
| StaticByteBuffer bytes{// Complete 16-bit UUIDs |
| 0x03, |
| 0x03, |
| lower_byte, |
| upper_byte, |
| // 0x40, the second octet, is not a recognized DataType |
| // (see common/supplement_data.h). |
| 0x05, |
| 0x40, |
| 0x34, |
| 0x12, |
| 0x34, |
| 0x12}; |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), data); |
| |
| // The second field of `bytes` was valid (in that its length byte matched its |
| // length), but its Data Type was unknown, so it should be ignored (i.e. the |
| // only field in the `data` should be the single 16-bit UUID, matching |
| // expected AD). |
| EXPECT_EQ(expected_ad, *data); |
| } |
| |
| TEST(AdvertisingDataTest, ParseBlockNameTooLong) { |
| // A block with a name of exactly kMaxNameLength (==248) bytes should be |
| // parsed correctly. |
| { |
| StaticByteBuffer<2> leading_bytes{kMaxNameLength + 1, |
| DataType::kCompleteLocalName}; |
| auto bytes = DynamicByteBuffer(kMaxNameLength + 2); |
| bytes.Write(leading_bytes); |
| DynamicByteBuffer name(kMaxNameLength); |
| name.Fill('a'); |
| bytes.Write(name, /*pos=*/2); |
| AdvertisingData::ParseResult result = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), result); |
| EXPECT_EQ(result->local_name()->name, std::string(kMaxNameLength, 'a')); |
| } |
| // Repeat previous test with shortened name. |
| { |
| auto leading_bytes = |
| StaticByteBuffer<2>{kMaxNameLength + 1, DataType::kShortenedLocalName}; |
| auto bytes = DynamicByteBuffer(kMaxNameLength + 2); |
| bytes.Write(leading_bytes); |
| DynamicByteBuffer name(kMaxNameLength); |
| name.Fill('a'); |
| bytes.Write(name, /*pos=*/2); |
| AdvertisingData::ParseResult result = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), result); |
| EXPECT_EQ(result->local_name()->name, std::string(kMaxNameLength, 'a')); |
| } |
| // A block with a name of kMaxNameLength+1 (==249) bytes should be rejected. |
| { |
| StaticByteBuffer<2> leading_bytes{kMaxNameLength + 2, |
| DataType::kCompleteLocalName}; |
| auto bytes = DynamicByteBuffer(kMaxNameLength + 3); |
| bytes.Write(leading_bytes); |
| DynamicByteBuffer name(kMaxNameLength + 1); |
| name.Fill('a'); |
| bytes.Write(name, /*pos=*/2); |
| AdvertisingData::ParseResult result = AdvertisingData::FromBytes(bytes); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kLocalNameTooLong, |
| result.error_value()); |
| } |
| // Repeat previous test with shortened name. |
| { |
| auto leading_bytes = |
| StaticByteBuffer<2>{kMaxNameLength + 2, DataType::kShortenedLocalName}; |
| auto bytes = DynamicByteBuffer(kMaxNameLength + 3); |
| bytes.Write(leading_bytes); |
| DynamicByteBuffer name(kMaxNameLength + 1); |
| name.Fill('a'); |
| bytes.Write(name, /*pos=*/2); |
| AdvertisingData::ParseResult result = AdvertisingData::FromBytes(bytes); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kLocalNameTooLong, |
| result.error_value()); |
| } |
| } |
| |
| TEST(AdvertisingDataTest, ManufacturerZeroLength) { |
| StaticByteBuffer bytes( |
| // Complete 16-bit UUIDs |
| 0x05, |
| 0x03, |
| 0x12, |
| 0x02, |
| 0x22, |
| 0x11, |
| // Manufacturer Data with no data |
| 0x03, |
| 0xFF, |
| 0x34, |
| 0x12); |
| |
| EXPECT_EQ(0u, AdvertisingData().manufacturer_data_ids().size()); |
| |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), data); |
| |
| EXPECT_EQ(1u, data->manufacturer_data_ids().count(0x1234)); |
| EXPECT_EQ(0u, data->manufacturer_data(0x1234).size()); |
| } |
| |
| TEST(AdvertisingDataTest, ServiceData) { |
| // A typical Eddystone-URL beacon advertisement |
| // to "https://fuchsia.cl" |
| StaticByteBuffer bytes( |
| // Complete 16-bit UUIDs, 0xFEAA |
| 0x03, |
| 0x03, |
| 0xAA, |
| 0xFE, |
| // Eddystone Service (0xFEAA) Data: |
| 0x10, |
| 0x16, |
| 0xAA, |
| 0xFE, |
| 0x10, // Eddystone-Uri type |
| 0xEE, // TX Power level -18dBm |
| 0x03, // "https://" |
| 'f', |
| 'u', |
| 'c', |
| 'h', |
| 's', |
| 'i', |
| 'a', |
| '.', |
| 'c', |
| 'l'); |
| |
| EXPECT_EQ(0u, AdvertisingData().service_data_uuids().size()); |
| |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), data); |
| |
| UUID eddystone(uint16_t{0xFEAA}); |
| |
| EXPECT_EQ(1u, data->service_data_uuids().size()); |
| EXPECT_EQ(13u, data->service_data(eddystone).size()); |
| |
| EXPECT_TRUE(ContainersEqual(bytes.view(8), data->service_data(eddystone))); |
| } |
| |
| // Per CSS v9 Part A 1.1.1, "A packet or data block shall not contain more than |
| // one instance for each Service UUID data size". We enforce this by failing to |
| // parse AdvertisingData with UUIDs of a particular size which exceed the amount |
| // that can fit in one TLV field. |
| TEST(AdvertisingDataTest, TooManyUuidsOfSizeRejected) { |
| // Space for the maximum # of 16 bit UUIDs + length + type fields. |
| const uint64_t kMaxAllowed16BitUuidsSize = |
| (2 + kMax16BitUuids * UUIDElemSize::k16Bit); |
| // Space for one more UUID + type and length fields |
| const uint64_t kExpectedBuffSize = |
| kMaxAllowed16BitUuidsSize + (2 + UUIDElemSize::k16Bit); |
| |
| DynamicByteBuffer bytes(kExpectedBuffSize); |
| uint64_t offset = 0; |
| // Write first TLV field with maximum # of UUIDs |
| bytes.Write(StaticByteBuffer{ |
| kMax16BitUuids * UUIDElemSize::k16Bit + 1, // Size byte |
| static_cast<uint8_t>(DataType::kComplete16BitServiceUuids) // Type byte |
| }); |
| offset += 2; |
| for (uint16_t i = 0; i < kMax16BitUuids; ++i) { |
| UUID uuid(static_cast<uint16_t>(i + 'a')); |
| bytes.Write(uuid.CompactView(), offset); |
| offset += uuid.CompactSize(); |
| } |
| // Verify that we successfully parse an AD with the maximum amount of 16 bit |
| // UUIDs |
| AdvertisingData::ParseResult adv_result = AdvertisingData::FromBytes( |
| bytes.view(/*pos=*/0, /*size=*/kMaxAllowed16BitUuidsSize)); |
| ASSERT_EQ(fit::ok(), adv_result); |
| EXPECT_EQ(kMax16BitUuids, adv_result->service_uuids().size()); |
| // Write second Complete 16 bit Service UUIDs TLV field with one more UUID |
| bytes.Write( |
| StaticByteBuffer{ |
| UUIDElemSize::k16Bit + 1, // Size byte |
| static_cast<uint8_t>( |
| DataType::kComplete16BitServiceUuids) // Type byte |
| }, |
| offset); |
| offset += 2; |
| UUID uuid(static_cast<uint16_t>(kMax16BitUuids + 'a')); |
| bytes.Write(uuid.CompactView(), offset); |
| |
| adv_result = AdvertisingData::FromBytes(bytes); |
| ASSERT_TRUE(adv_result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kUuidsMalformed, |
| adv_result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, Missing) { |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(DynamicByteBuffer()); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kMissing, result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, InvalidTlvFormat) { |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(StaticByteBuffer(0x03)); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kInvalidTlvFormat, |
| result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, TxPowerLevelMalformed) { |
| StaticByteBuffer service_data{/*length=*/0x01, |
| static_cast<uint8_t>(DataType::kTxPowerLevel)}; |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(service_data); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kTxPowerLevelMalformed, |
| result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, UuidsMalformed) { |
| StaticByteBuffer service_data{ |
| 0x02, // Length |
| static_cast<uint8_t>(DataType::kComplete16BitServiceUuids), |
| 0x12 // The length of a valid 16-bit UUID byte array be a multiple of 2 |
| // (and 1 % 2 == 1). |
| }; |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(service_data); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kUuidsMalformed, result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, ManufacturerSpecificDataTooSmall) { |
| StaticByteBuffer service_data{ |
| 0x02, // Length |
| static_cast<uint8_t>(DataType::kManufacturerSpecificData), |
| 0x12 // Manufacturer-specific data must be at least 2 bytes |
| }; |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(service_data); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kManufacturerSpecificDataTooSmall, |
| result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, DecodeServiceDataWithIncompleteUuid) { |
| StaticByteBuffer service_data( |
| 0x02, // Length |
| static_cast<uint8_t>(DataType::kServiceData16Bit), // Data type |
| 0xAA // First byte of incomplete UUID |
| ); |
| |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(service_data); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kServiceDataTooSmall, |
| result.error_value()); |
| } |
| |
| TEST(AdvertisingDataTest, AppearanceMalformed) { |
| StaticByteBuffer service_data{ |
| 0x02, // Length |
| static_cast<uint8_t>(DataType::kAppearance), |
| 0x12 // Appearance is supposed to be 2 bytes |
| }; |
| AdvertisingData::ParseResult result = |
| AdvertisingData::FromBytes(service_data); |
| ASSERT_TRUE(result.is_error()); |
| EXPECT_EQ(AdvertisingData::ParseError::kAppearanceMalformed, |
| result.error_value()); |
| } |
| TEST(AdvertisingDataTest, Equality) { |
| AdvertisingData one, two; |
| |
| UUID gatt(kGattUuid); |
| UUID eddy(kEddystoneUuid); |
| |
| // Service UUIDs |
| EXPECT_EQ(two, one); |
| EXPECT_TRUE(one.AddServiceUuid(gatt)); |
| EXPECT_NE(two, one); |
| EXPECT_TRUE(two.AddServiceUuid(gatt)); |
| EXPECT_EQ(two, one); |
| |
| // Even when the bytes are the same but from different places |
| StaticByteBuffer bytes(0x01, 0x02, 0x03, 0x04); |
| StaticByteBuffer same(0x01, 0x02, 0x03, 0x04); |
| EXPECT_TRUE(two.SetManufacturerData(0x0123, bytes.view())); |
| EXPECT_NE(two, one); |
| EXPECT_TRUE(one.SetManufacturerData(0x0123, same.view())); |
| EXPECT_EQ(two, one); |
| |
| // When TX Power is different |
| two.SetTxPower(-34); |
| EXPECT_NE(two, one); |
| one.SetTxPower(-30); |
| EXPECT_NE(two, one); |
| one.SetTxPower(-34); |
| EXPECT_EQ(two, one); |
| |
| // Even if the fields were added in different orders |
| AdvertisingData three, four; |
| EXPECT_TRUE(three.AddServiceUuid(eddy)); |
| EXPECT_TRUE(three.AddServiceUuid(gatt)); |
| EXPECT_NE(three, four); |
| |
| EXPECT_TRUE(four.AddServiceUuid(gatt)); |
| EXPECT_TRUE(four.AddServiceUuid(eddy)); |
| EXPECT_EQ(three, four); |
| } |
| |
| TEST(AdvertisingDataTest, Copy) { |
| UUID gatt(kGattUuid); |
| UUID eddy(kEddystoneUuid); |
| StaticByteBuffer<kRandomDataSize> rand_data; |
| random_generator()->Get(rand_data.mutable_subspan()); |
| |
| AdvertisingData source; |
| EXPECT_TRUE(source.AddUri("http://fuchsia.cl")); |
| EXPECT_TRUE(source.AddUri("https://ru.st")); |
| EXPECT_TRUE(source.SetManufacturerData(0x0123, rand_data.view())); |
| EXPECT_TRUE(source.AddServiceUuid(gatt)); |
| EXPECT_TRUE(source.AddServiceUuid(eddy)); |
| |
| AdvertisingData dest; |
| source.Copy(&dest); |
| |
| EXPECT_EQ(source, dest); |
| |
| // Modifying the source shouldn't mess with the copy |
| EXPECT_TRUE(source.SetLocalName("fuchsia")); |
| EXPECT_FALSE(dest.local_name()); |
| |
| StaticByteBuffer bytes(0x01, 0x02, 0x03); |
| EXPECT_TRUE(source.SetManufacturerData(0x0123, bytes.view())); |
| EXPECT_TRUE(ContainersEqual(rand_data, dest.manufacturer_data(0x0123))); |
| } |
| |
| TEST(AdvertisingDataTest, Move) { |
| UUID gatt(kGattUuid); |
| UUID eddy(kEddystoneUuid); |
| StaticByteBuffer<kRandomDataSize> rand_data; |
| random_generator()->Get(rand_data.mutable_subspan()); |
| |
| UUID heart_rate_uuid(kHeartRateServiceUuid); |
| int8_t tx_power = 18; // arbitrary TX power |
| uint16_t appearance = 0x4567; // arbitrary appearance value |
| uint8_t flags = 0x48; // arbitrary flags value |
| AdvertisingData source; |
| EXPECT_TRUE(source.SetLocalName("test")); |
| source.SetFlags(flags); |
| source.SetTxPower(tx_power); |
| source.SetAppearance(appearance); |
| EXPECT_TRUE(source.AddUri("http://fuchsia.cl")); |
| EXPECT_TRUE(source.AddUri("https://ru.st")); |
| EXPECT_TRUE(source.SetManufacturerData(0x0123, rand_data.view())); |
| EXPECT_TRUE(source.AddServiceUuid(gatt)); |
| EXPECT_TRUE(source.AddServiceUuid(eddy)); |
| EXPECT_TRUE(source.SetServiceData(heart_rate_uuid, rand_data.view())); |
| |
| auto verify_advertising_data = [&](const AdvertisingData& dest, |
| const char* type) { |
| SCOPED_TRACE(type); |
| // Dest should have the data we set. |
| EXPECT_EQ("test", dest.local_name()->name); |
| EXPECT_EQ(tx_power, dest.tx_power().value()); |
| EXPECT_EQ(appearance, dest.appearance().value()); |
| EXPECT_EQ( |
| std::unordered_set<std::string>({"http://fuchsia.cl", "https://ru.st"}), |
| dest.uris()); |
| EXPECT_TRUE(ContainersEqual(rand_data, dest.manufacturer_data(0x0123))); |
| EXPECT_EQ(std::unordered_set<UUID>({gatt, eddy}), dest.service_uuids()); |
| EXPECT_TRUE(ContainersEqual(rand_data, dest.service_data(heart_rate_uuid))); |
| EXPECT_EQ(flags, dest.flags().value()); |
| }; |
| |
| AdvertisingData move_constructed(std::move(source)); |
| |
| // source should be empty. |
| EXPECT_EQ(AdvertisingData(), source); |
| verify_advertising_data(move_constructed, "move_constructed"); |
| |
| AdvertisingData move_assigned{}; |
| move_assigned = std::move(move_constructed); |
| EXPECT_EQ(AdvertisingData(), move_constructed); |
| verify_advertising_data(move_assigned, "move_assigned"); |
| } |
| |
| TEST(AdvertisingDataTest, Flags) { |
| // A zero-byte flags is allowed, and sets the flags field to zeroes. |
| StaticByteBuffer flags_empty(0x01, DataType::kFlags); |
| // Extra bytes are accepted but ignored. |
| StaticByteBuffer flags_extra(0x04, DataType::kFlags, 0x03, 0x42, 0x49); |
| |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(flags_empty); |
| ASSERT_EQ(fit::ok(), data); |
| ASSERT_TRUE(data->flags().has_value()); |
| ASSERT_EQ(0x00, data->flags().value()); |
| |
| data = AdvertisingData::FromBytes(flags_extra); |
| ASSERT_EQ(fit::ok(), data); |
| ASSERT_TRUE(data->flags().has_value()); |
| ASSERT_EQ(0x03, data->flags().value()); |
| } |
| |
| TEST(AdvertisingDataTest, Uris) { |
| // The encoding scheme is represented by the first UTF-8 code-point in the URI |
| // string. Per |
| // https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping/, |
| // 0xBA is the highest code point corresponding to an encoding scheme. |
| // However, 0xBA > 0x7F, so representing too-large encoding schemes (i.e. |
| // code-points > 0xBA) in UTF-8 requires two bytes. |
| const uint8_t kLargestKnownSchemeByte1 = 0xC2, |
| kLargestKnownSchemeByte2 = 0xBA; |
| // These bytes represent the (valid) UTF-8 code point for the (unknown |
| // encoding scheme) U+00BB. |
| const uint8_t kUnknownSchemeByte1 = 0xC2, kUnknownSchemeByte2 = 0xBB; |
| StaticByteBuffer bytes( |
| // Uri: "https://abc.xyz" |
| 0x0B, |
| DataType::kURI, |
| 0x17, |
| '/', |
| '/', |
| 'a', |
| 'b', |
| 'c', |
| '.', |
| 'x', |
| 'y', |
| 'z', |
| // Empty URI should be ignored: |
| 0x01, |
| DataType::kURI, |
| // Uri: "flubs:abc" |
| 0x0B, |
| DataType::kURI, |
| 0x01, |
| 'f', |
| 'l', |
| 'u', |
| 'b', |
| 's', |
| ':', |
| 'a', |
| 'b', |
| 'c', |
| // Uri: "ms-settings-cloudstorage:flub" |
| 0x07, |
| DataType::kURI, |
| kLargestKnownSchemeByte1, |
| kLargestKnownSchemeByte2, |
| 'f', |
| 'l', |
| 'u', |
| 'b', |
| // Invalid URI should be ignored - UTF-8 U+00BB doesn't correspond to an |
| // encoding scheme. |
| 0x07, |
| DataType::kURI, |
| kUnknownSchemeByte1, |
| kUnknownSchemeByte2, |
| 'f', |
| 'l', |
| 'u', |
| 'b', |
| // Invalid URI should be ignored - UTF-8 U+0000 doesn't correspond to an |
| // encoding scheme. |
| 0x03, |
| DataType::kURI, |
| 0x00, |
| 0x00); |
| |
| AdvertisingData::ParseResult data = AdvertisingData::FromBytes(bytes); |
| ASSERT_EQ(fit::ok(), data); |
| |
| auto uris = data->uris(); |
| EXPECT_EQ(3u, uris.size()); |
| |
| EXPECT_TRUE(std::find(uris.begin(), uris.end(), "https://abc.xyz") != |
| uris.end()); |
| EXPECT_TRUE(std::find(uris.begin(), uris.end(), "flubs:abc") != uris.end()); |
| EXPECT_TRUE(std::find(uris.begin(), |
| uris.end(), |
| "ms-settings-cloudstorage:flub") != uris.end()); |
| } |
| |
| // Tests writing a fully populated |AdvertisingData| to |
| // an output buffer succeeds. |
| TEST(AdvertisingDataTest, WriteBlockSuccess) { |
| AdvertisingData data; |
| |
| data.SetTxPower(4); |
| data.SetAppearance(0x4567); |
| EXPECT_TRUE(data.SetLocalName("fuchsia")); |
| |
| StaticByteBuffer bytes(0x01, 0x02, 0x03); |
| EXPECT_TRUE(data.SetManufacturerData(0x0123, bytes.view())); |
| |
| auto service_uuid = UUID(kId1As16); |
| StaticByteBuffer service_bytes(0x01, 0x02); |
| EXPECT_TRUE(data.AddServiceUuid(service_uuid)); |
| EXPECT_TRUE(data.SetServiceData(service_uuid, service_bytes.view())); |
| |
| EXPECT_TRUE(data.AddUri("http://fuchsia.cl")); |
| |
| DynamicByteBuffer write_buf(data.CalculateBlockSize()); |
| EXPECT_TRUE(data.WriteBlock(&write_buf, std::nullopt)); |
| |
| StaticByteBuffer expected_buf(0x02, |
| 0x0a, |
| 0x04, // tx_power_level_: 4 |
| 0x03, |
| 0x19, |
| 0x67, |
| 0x45, // appearance_: 0x4567 |
| 0x08, |
| 0x09, |
| 0x66, |
| 0x75, |
| 0x63, |
| 0x68, |
| 0x73, |
| 0x69, |
| 0x61, // local_name_: "fuchsia" |
| 0x06, |
| 0xff, |
| 0x23, |
| 0x01, |
| 0x01, |
| 0x02, |
| 0x03, // manufacturer_data_ |
| 0x05, |
| 0x16, |
| 0x12, |
| 0x02, |
| 0x01, |
| 0x02, // service_data_ |
| 0x0e, |
| 0x24, |
| 0x16, |
| 0x2f, |
| 0x2f, |
| 0x66, |
| 0x75, |
| 0x63, |
| 0x68, |
| 0x73, |
| 0x69, |
| 0x61, |
| 0x2e, |
| 0x63, |
| 0x6c, |
| 0x03, |
| 0x02, |
| 0x12, |
| 0x02); // uris_ |
| EXPECT_TRUE(ContainersEqual(expected_buf, write_buf)); |
| } |
| |
| // Tests writing |AdvertisingData| to an output buffer that |
| // is too small fails gracefully and returns early. |
| TEST(AdvertisingDataTest, WriteBlockSmallBufError) { |
| AdvertisingData data; |
| |
| data.SetTxPower(4); |
| data.SetAppearance(0x4567); |
| EXPECT_TRUE(data.SetLocalName("fuchsia")); |
| |
| DynamicByteBuffer write_buf(data.CalculateBlockSize() - 1); |
| // The buffer is too small. No write should occur, and should return false. |
| EXPECT_FALSE(data.WriteBlock(&write_buf, std::nullopt)); |
| } |
| |
| // Tests writing a fully populated |AdvertisingData| with provided flags to |
| // an output buffer succeeds. |
| TEST(AdvertisingDataTest, WriteBlockWithFlagsSuccess) { |
| AdvertisingData data; |
| |
| data.SetTxPower(4); |
| data.SetAppearance(0x4567); |
| EXPECT_TRUE(data.SetLocalName("fuchsia")); |
| |
| StaticByteBuffer bytes(0x01, 0x02, 0x03); |
| EXPECT_TRUE(data.SetManufacturerData(0x0123, bytes.view())); |
| |
| auto service_uuid = UUID(kId1As16); |
| StaticByteBuffer service_bytes(0x01, 0x02); |
| EXPECT_TRUE(data.AddServiceUuid(service_uuid)); |
| EXPECT_TRUE(data.SetServiceData(service_uuid, service_bytes.view())); |
| |
| EXPECT_TRUE(data.AddUri("http://fuchsia.cl")); |
| |
| DynamicByteBuffer write_buf(data.CalculateBlockSize(/*include_flags=*/true)); |
| EXPECT_TRUE(data.WriteBlock(&write_buf, AdvFlag::kLEGeneralDiscoverableMode)); |
| |
| StaticByteBuffer expected_buf(0x02, |
| 0x01, |
| 0x02, // flags: 2 |
| 0x02, |
| 0x0a, |
| 0x04, // tx_power_level_: 4 |
| 0x03, |
| 0x19, |
| 0x67, |
| 0x45, // appearance_: 0x4567 |
| 0x08, |
| 0x09, |
| 0x66, |
| 0x75, |
| 0x63, |
| 0x68, |
| 0x73, |
| 0x69, |
| 0x61, // local_name_: "fuchsia" |
| 0x06, |
| 0xff, |
| 0x23, |
| 0x01, |
| 0x01, |
| 0x02, |
| 0x03, // manufacturer_data_ |
| 0x05, |
| 0x16, |
| 0x12, |
| 0x02, |
| 0x01, |
| 0x02, // service_data_ |
| 0x0e, |
| 0x24, |
| 0x16, |
| 0x2f, |
| 0x2f, |
| 0x66, |
| 0x75, |
| 0x63, |
| 0x68, |
| 0x73, |
| 0x69, |
| 0x61, |
| 0x2e, |
| 0x63, |
| 0x6c, |
| 0x03, |
| 0x02, |
| 0x12, |
| 0x02); // uris_ |
| EXPECT_TRUE(ContainersEqual(expected_buf, write_buf)); |
| } |
| |
| TEST(AdvertisingDataTest, WriteBlockWithFlagsBufError) { |
| AdvertisingData data; |
| |
| data.SetTxPower(6); |
| EXPECT_TRUE(data.SetLocalName("Fuchsia")); |
| data.SetAppearance(0x1234); |
| |
| DynamicByteBuffer write_buf(data.CalculateBlockSize(/*include_flags=*/true) - |
| 1); |
| EXPECT_FALSE( |
| data.WriteBlock(&write_buf, AdvFlag::kLEGeneralDiscoverableMode)); |
| } |
| |
| // Adds `n_(consecutively_increasing)_uuids` to `input` and returns the "next" |
| // UUID in the sequence. UUIDs may wrap around - this is OK, as we only care |
| // that they are all distinct. |
| UUID AddNDistinctUuids(AdvertisingData& input, |
| std::variant<uint16_t, uint32_t, UInt128> starting_uuid, |
| uint8_t n_uuids) { |
| UUID next; |
| for (uint8_t i = 0; true; ++i) { |
| std::visit( |
| [&](auto arg) { |
| using T = std::decay_t<decltype(arg)>; |
| if constexpr (std::is_same_v<T, UInt128>) { |
| arg[0] += i; |
| next = UUID(arg); |
| } else { |
| next = UUID(static_cast<T>(arg + i)); |
| } |
| }, |
| starting_uuid); |
| SCOPED_TRACE( |
| bt_lib_cpp_string::StringPrintf("i: %du UUID: %s", i, bt_str(next))); |
| if (i >= n_uuids) { |
| return next; |
| } |
| EXPECT_TRUE(input.AddServiceUuid(next)); |
| } |
| } |
| |
| TEST(AdvertisingDataTest, SetFieldsWithTooLongParameters) { |
| AdvertisingData data; |
| { |
| // Use the https URI encoding scheme. This prefix will be compressed to one |
| // byte when encoded. |
| std::string uri = "https:"; |
| uri += std::string(kMaxEncodedUriLength - 1, '.'); |
| EXPECT_TRUE(data.AddUri(uri)); |
| uri += '.'; |
| EXPECT_FALSE(data.AddUri(uri)); |
| } |
| // Attempt to set slightly too long service data. |
| { |
| UUID two_byte_uuid{kHeartRateServiceUuid}; |
| DynamicByteBuffer long_data(kMaxEncodedServiceDataLength - 1); |
| long_data.Fill(0xAB); |
| EXPECT_FALSE(data.SetServiceData(two_byte_uuid, long_data)); |
| // An empty DynamicByteBuffer represents unset service data per the header. |
| EXPECT_TRUE( |
| ContainersEqual(DynamicByteBuffer(), data.service_data(two_byte_uuid))); |
| // Now use a view that is just small enough to fit when encoded |
| BufferView view = long_data.view(/*pos=*/0, /*size=*/long_data.size() - 1); |
| EXPECT_TRUE(data.SetServiceData(two_byte_uuid, view)); |
| EXPECT_TRUE(ContainersEqual(view, data.service_data(two_byte_uuid))); |
| } |
| // Attempt to set slightly too long manufacturer data. |
| { |
| uint16_t manufacturer_id{0xABBA}; |
| DynamicByteBuffer long_data(kMaxManufacturerDataLength + 1); |
| long_data.Fill(0xAB); |
| EXPECT_FALSE(data.SetManufacturerData(manufacturer_id, long_data.view())); |
| // An empty DynamicByteBuffer represents unset service data per the header. |
| EXPECT_TRUE(ContainersEqual(DynamicByteBuffer(), |
| data.manufacturer_data(manufacturer_id))); |
| // Now use a view that is just small enough to fit when encoded |
| BufferView view = long_data.view(/*pos=*/0, /*size=*/long_data.size() - 1); |
| EXPECT_TRUE(data.SetManufacturerData(manufacturer_id, view)); |
| EXPECT_TRUE(ContainersEqual(view, data.manufacturer_data(manufacturer_id))); |
| } |
| // Ensure that service UUIDs are truncated when they do not fit. |
| { |
| uint16_t starting_16bit_uuid = 0x0001; |
| UUID should_fail = AddNDistinctUuids( |
| data, |
| std::variant<uint16_t, uint32_t, UInt128>{starting_16bit_uuid}, |
| kMax16BitUuids); |
| EXPECT_FALSE(data.AddServiceUuid(should_fail)); |
| EXPECT_TRUE(data.service_uuids().find(should_fail) == |
| data.service_uuids().end()); |
| |
| // This value must not fit in a 16 bit number in order to count as a "32 |
| // bit" UUID |
| uint32_t starting_32bit_uuid = std::numeric_limits<uint16_t>::max() + 1; |
| should_fail = AddNDistinctUuids( |
| data, |
| std::variant<uint16_t, uint32_t, UInt128>{starting_32bit_uuid}, |
| kMax32BitUuids); |
| EXPECT_FALSE(data.AddServiceUuid(should_fail)); |
| EXPECT_TRUE(data.service_uuids().find(should_fail) == |
| data.service_uuids().end()); |
| |
| UInt128 starting_128bit_uuid = {0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB, |
| 0xAB}; |
| should_fail = AddNDistinctUuids( |
| data, |
| std::variant<uint16_t, uint32_t, UInt128>{starting_128bit_uuid}, |
| kMax128BitUuids); |
| EXPECT_FALSE(data.AddServiceUuid(should_fail)); |
| EXPECT_TRUE(data.service_uuids().find(should_fail) == |
| data.service_uuids().end()); |
| } |
| // Ensures names exceeding kMaxNameLength are rejected. |
| { |
| std::string name_that_fits(kMaxNameLength, 'a'); |
| std::string too_long_name(kMaxNameLength + 1, 'b'); |
| EXPECT_TRUE(data.SetLocalName(name_that_fits)); |
| EXPECT_EQ(name_that_fits, data.local_name()->name); |
| EXPECT_FALSE(data.SetLocalName(too_long_name)); |
| EXPECT_EQ(name_that_fits, data.local_name()->name); |
| } |
| // Write the data out to ensure no assertions are triggered |
| DynamicByteBuffer block(data.CalculateBlockSize()); |
| EXPECT_TRUE(data.WriteBlock(&block, std::nullopt)); |
| } |
| |
| // Tests that setting a complete local name overwrites an existing shortened |
| // local name and that setting a shortened local name has no effect if a |
| // complete local name is currently stored. |
| TEST(AdvertisingDataTest, CompleteLocalNameFavored) { |
| AdvertisingData data; |
| std::string short_name = "short"; |
| std::string complete_name = "complete"; |
| |
| EXPECT_TRUE(data.SetLocalName(short_name, /*is_complete=*/false)); |
| EXPECT_EQ(short_name, data.local_name()->name); |
| EXPECT_TRUE(data.SetLocalName(complete_name, /*is_complete=*/true)); |
| EXPECT_EQ(complete_name, data.local_name()->name); |
| |
| EXPECT_FALSE(data.SetLocalName(short_name, /*is_complete=*/false)); |
| EXPECT_EQ(complete_name, data.local_name()->name); |
| } |
| |
| // Tests that even when the maximum number of distinct UUIDs for a certain size |
| // have been added to an AD, we do not reject additional UUIDs that are |
| // duplicates of already-added UUIDs. |
| TEST(AdvertisingDataTest, AddDuplicateServiceUuidsWhenFullSucceeds) { |
| AdvertisingData data; |
| uint16_t starting_16bit_uuid = 0x0001; |
| UUID should_fail = AddNDistinctUuids( |
| data, |
| std::variant<uint16_t, uint32_t, UInt128>{starting_16bit_uuid}, |
| kMax16BitUuids); |
| // Verify that adding another distinct UUID fails - i.e. we are at the limit. |
| EXPECT_FALSE(data.AddServiceUuid(should_fail)); |
| EXPECT_TRUE(data.service_uuids().find(should_fail) == |
| data.service_uuids().end()); |
| // Verify that we are notified of success when adding an existing UUID |
| EXPECT_TRUE(data.AddServiceUuid(UUID(starting_16bit_uuid))); |
| } |
| } // namespace |
| } // namespace bt |