diff --git a/Package.swift b/Package.swift index 86b592b1..93c65700 100644 --- a/Package.swift +++ b/Package.swift @@ -467,6 +467,7 @@ let package = Package( "connections/core_test.cc", "connections/status_test.cc", "connections/payload_test.cc", + "internal/base/bluetooth_address_test.cc", "internal/crypto/aead_unittest.cc", "internal/crypto/ec_private_key_unittest.cc", "internal/crypto/ec_signature_creator_unittest.cc", diff --git a/internal/base/BUILD b/internal/base/BUILD index 94a668c2..3d1268ba 100644 --- a/internal/base/BUILD +++ b/internal/base/BUILD @@ -18,3 +18,46 @@ cc_library( "@com_google_absl//absl/container:flat_hash_set", ], ) + +cc_library( + name = "bluetooth_address", + srcs = [ + "bluetooth_address.cc", + ], + hdrs = [ + "bluetooth_address.h", + ], + copts = [ + "-Ithird_party", + ], + visibility = [ + "//fastpair:__subpackages__", + "//location/nearby/cpp/fastpair:__subpackages__", + ], + deps = [ + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/types:optional", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "base_test", + size = "small", + timeout = "short", + srcs = [ + "bluetooth_address_test.cc", + ], + copts = [ + "-Ithird_party", + ], + shard_count = 8, + deps = [ + ":bluetooth_address", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/internal/base/bluetooth_address.cc b/internal/base/bluetooth_address.cc new file mode 100644 index 00000000..5cdfba29 --- /dev/null +++ b/internal/base/bluetooth_address.cc @@ -0,0 +1,124 @@ +// Copyright 2021 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. + +#include "internal/base/bluetooth_address.h" + +#include +#include +#include + +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" + +namespace nearby { +namespace device { +namespace { + +template +// Note that some of the methods return absl::optional instead +// of std::optional, because iOS platform is still in C++14. +absl::optional CharToDigit(CHAR c) { + static_assert(1 <= BASE && BASE <= 36, "BASE needs to be in [1, 36]"); + if (c >= '0' && c < '0' + std::min(BASE, 10)) return c - '0'; + + if (c >= 'a' && c < 'a' + BASE - 10) return c - 'a' + 10; + + if (c >= 'A' && c < 'A' + BASE - 10) return c - 'A' + 10; + + return absl::nullopt; +} + +template +static bool HexStringToByteContainer(absl::string_view input, OutIter output) { + size_t count = input.size(); + if (count == 0 || (count % 2) != 0) return false; + for (uintptr_t i = 0; i < count / 2; ++i) { + // most significant 4 bits + absl::optional msb = CharToDigit<16>(input[i * 2]); + // least significant 4 bits + absl::optional lsb = CharToDigit<16>(input[i * 2 + 1]); + if (!msb.has_value() || !lsb.has_value()) { + return false; + } + *(output++) = (*msb << 4) | *lsb; + } + return true; +} + +bool HexStringToSpan(absl::string_view input, absl::Span output) { + if (input.size() / 2 != output.size()) return false; + + return HexStringToByteContainer(input, output.begin()); +} + +} // namespace + +bool ParseBluetoothAddress(absl::string_view input, + absl::Span output) { + if (output.size() != 6) return false; + + // Try parsing addresses that lack separators, like "1A2B3C4D5E6F". + if (input.size() == 12) return HexStringToSpan(input, output); + + // Try parsing MAC address with separators like: "00:11:22:33:44:55" or + // "00-11-22-33-44-55". Separator can be either '-' or ':', but must use the + // same style throughout. + if (input.size() == 17) { + const char separator = input[2]; + if (separator != '-' && separator != ':') return false; + return (input[2] == separator) && (input[5] == separator) && + (input[8] == separator) && (input[11] == separator) && + (input[14] == separator) && + HexStringToSpan(input.substr(0, 2), output.subspan(0, 1)) && + HexStringToSpan(input.substr(3, 2), output.subspan(1, 1)) && + HexStringToSpan(input.substr(6, 2), output.subspan(2, 1)) && + HexStringToSpan(input.substr(9, 2), output.subspan(3, 1)) && + HexStringToSpan(input.substr(12, 2), output.subspan(4, 1)) && + HexStringToSpan(input.substr(15, 2), output.subspan(5, 1)); + } + + return false; +} + +std::string ConvertBluetoothAddressUIntToString(uint64_t address) { + std::string mac_address = absl::StrFormat( + "%02llX:%02llX:%02llX:%02llX:%02llX:%02llX", address >> 40, + (address >> 32) & 0xff, (address >> 24) & 0xff, (address >> 16) & 0xff, + (address >> 8) & 0xff, address & 0xff); + return CanonicalizeBluetoothAddress(mac_address); +} + +std::string CanonicalizeBluetoothAddress(absl::string_view address) { + std::array bytes; + + if (!ParseBluetoothAddress(address, absl::MakeSpan(bytes.data(), 6))) + return std::string(); + + return CanonicalizeBluetoothAddress(bytes); +} + +std::string CanonicalizeBluetoothAddress( + const std::array& address_bytes) { + return absl::StrFormat("%02X:%02X:%02X:%02X:%02X:%02X", address_bytes[0], + address_bytes[1], address_bytes[2], address_bytes[3], + address_bytes[4], address_bytes[5]); +} + +std::string CanonicalizeBluetoothAddress(uint64_t address) { + return ConvertBluetoothAddressUIntToString(address); +} + +} // namespace device +} // namespace nearby diff --git a/internal/base/bluetooth_address.h b/internal/base/bluetooth_address.h new file mode 100644 index 00000000..81c4e750 --- /dev/null +++ b/internal/base/bluetooth_address.h @@ -0,0 +1,51 @@ +// Copyright 2021 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. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_BASE_BLUETOOTH_ADDRESS_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_BASE_BLUETOOTH_ADDRESS_H_ + +#include +#include + +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" + +namespace nearby { +namespace device { + +// Parses a Bluetooth address to an output buffer. The output buffer must be +// exactly 6 bytes in size. The address can be formatted in one of three ways: +// +// 1A:2B:3C:4D:5E:6F +// 1A-2B-3C-4D-5E-6F +// 1A2B3C4D5E6F +bool ParseBluetoothAddress(absl::string_view input, absl::Span output); + +// Converts a uint64_t Bluetooth address to string. +std::string ConvertBluetoothAddressUIntToString(uint64_t address); + +// Returns |address| in the canonical format: XX:XX:XX:XX:XX:XX, where each 'X' +// is a hex digit. If the input |address| is invalid, returns an empty string. +std::string CanonicalizeBluetoothAddress(absl::string_view address); +std::string CanonicalizeBluetoothAddress( + const std::array& address_bytes); +std::string CanonicalizeBluetoothAddress(uint64_t address); + +} // namespace device +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_INTERNAL_BASE_BLUETOOTH_ADDRESS_H_ diff --git a/internal/base/bluetooth_address_test.cc b/internal/base/bluetooth_address_test.cc new file mode 100644 index 00000000..1438391d --- /dev/null +++ b/internal/base/bluetooth_address_test.cc @@ -0,0 +1,59 @@ +// Copyright 2021 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. + +#include "internal/base/bluetooth_address.h" + +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" + +namespace nearby { +namespace device { +namespace { + +TEST(BluetoothUtil, ParseBluetoothAddress) { + std::array output; + std::array expected_output{{26, 43, 60, 77, 94, 111}}; + EXPECT_TRUE(ParseBluetoothAddress( + "1A:2B:3C:4D:5E:6F", absl::MakeSpan(output.data(), output.size()))); + EXPECT_EQ(output, expected_output); +} + +TEST(BluetoothUtil, ConvertBluetoothAddressUIntToString) { + const uint64_t input = 0x00001A2B3C4D5E6F; + std::string expected_output = "1A:2B:3C:4D:5E:6F"; + std::string output = ConvertBluetoothAddressUIntToString(input); + EXPECT_EQ(output, expected_output); +} + +TEST(BluetoothUtil, CanonicalizeBluetoothAddress) { + std::array address{{26, 43, 60, 77, 94, 111}}; + EXPECT_EQ(CanonicalizeBluetoothAddress(address), "1A:2B:3C:4D:5E:6F"); + EXPECT_EQ(CanonicalizeBluetoothAddress("1A-2B-3C-4D-5E-6F"), + "1A:2B:3C:4D:5E:6F"); + EXPECT_EQ(CanonicalizeBluetoothAddress(0x00001A2B3C4D5E6F), + "1A:2B:3C:4D:5E:6F"); + + // Canonicalizes invalid address + EXPECT_EQ(CanonicalizeBluetoothAddress("1A-2B-3C-4D-5E-6F-89"), ""); + EXPECT_EQ(CanonicalizeBluetoothAddress("nearby"), ""); + EXPECT_EQ(CanonicalizeBluetoothAddress("MA-2M-3C-4D-5E-6F"), ""); + EXPECT_EQ(CanonicalizeBluetoothAddress(0x001A2B3C4D5E6F89), ""); +} + +} // namespace +} // namespace device +} // namespace nearby