diff --git a/fastpair/dataparser/BUILD b/fastpair/dataparser/BUILD new file mode 100644 index 00000000..6cbfe1b9 --- /dev/null +++ b/fastpair/dataparser/BUILD @@ -0,0 +1,35 @@ +licenses(["notice"]) + +cc_library( + name = "dataparser", + srcs = [ + "fast_pair_decoder.cc", + ], + hdrs = [ + "fast_pair_decoder.h", + ], + visibility = [ + "//fastpair:__subpackages__", + ], + deps = [ + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:optional", + ], +) + +cc_test( + name = "fast_pair_decoder_test", + size = "small", + srcs = [ + "fast_pair_decoder_test.cc", + ], + shard_count = 16, + deps = [ + ":dataparser", + "//fastpair/testing", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:optional", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/fastpair/dataparser/fast_pair_decoder.cc b/fastpair/dataparser/fast_pair_decoder.cc new file mode 100644 index 00000000..0e17b04e --- /dev/null +++ b/fastpair/dataparser/fast_pair_decoder.cc @@ -0,0 +1,105 @@ +// Copyright 2022 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 "fastpair/dataparser/fast_pair_decoder.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/escaping.h" + +namespace nearby { +namespace fastpair { + +namespace { + +constexpr int kHeaderIndex = 0; +constexpr int kHeaderLength = 1; +constexpr int kHeaderLengthBitmask = 0b00011110; +constexpr int kHeaderLengthOffset = 1; +constexpr int kHeaderVersionBitmask = 0b11100000; +constexpr int kHeaderVersionOffset = 5; +constexpr int kMinModelIdLength = 3; +constexpr int kMaxModelIdLength = 14; + +int GetIdLength(const std::vector* service_data) { + return service_data->size() == kMinModelIdLength + ? kMinModelIdLength + : ((*service_data)[kHeaderIndex] & kHeaderLengthBitmask) >> + kHeaderLengthOffset; +} + +bool IsIdLengthValid(const std::vector* service_data) { + int id_length = GetIdLength(service_data); + return kMinModelIdLength <= id_length && id_length <= kMaxModelIdLength && + id_length + kHeaderLength <= static_cast(service_data->size()); +} + +int GetVersion(const std::vector* service_data) { + return service_data->size() == kMinModelIdLength + ? 0 + : ((*service_data)[kHeaderIndex] & kHeaderVersionBitmask) >> + kHeaderVersionOffset; +} + +} // namespace + +bool FastPairDecoder::HasModelId(const std::vector* service_data) { + return service_data != nullptr && + (service_data->size() == kMinModelIdLength || + // Header byte exists. We support only format version 0. (A different + // version indicates a breaking change in the format.) + (service_data->size() > kMinModelIdLength && + GetVersion(service_data) == 0 && IsIdLengthValid(service_data))); +} + +absl::optional FastPairDecoder::GetHexModelIdFromServiceData( + const std::vector* service_data) { + if (service_data == nullptr || service_data->size() < kMinModelIdLength) { + return absl::nullopt; + } + + if (service_data->size() == kMinModelIdLength) { + // If the size is 3, all the bytes are the ID, + std::vector bytes = *service_data; + std::string model_id(bytes.begin(), bytes.end()); + return absl::BytesToHexString(model_id); + } + // Otherwise, the first byte is a header which contains the length of the + // big-endian model ID that follows. The model ID will be trimmed if it + // contains leading zeros. + int id_index = 1; + int end = id_index + GetIdLength(service_data); + + // Ignore leading zeros. + while ((*service_data)[id_index] == 0 && end - id_index > kMinModelIdLength) { + id_index++; + } + + // Copy appropriate bytes to new array. + int bytes_size = end - id_index; + std::vector bytes; + bytes.reserve(bytes_size); + for (int i = 0; i < bytes_size; i++) { + bytes.push_back((*service_data)[i + id_index]); + } + std::string model_id(bytes.begin(), bytes.end()); + return absl::BytesToHexString(model_id); +} + +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/dataparser/fast_pair_decoder.h b/fastpair/dataparser/fast_pair_decoder.h new file mode 100644 index 00000000..20831c16 --- /dev/null +++ b/fastpair/dataparser/fast_pair_decoder.h @@ -0,0 +1,37 @@ +// Copyright 2022 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_FASTPAIR_DATAPARSER_FAST_PAIR_DECODER_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_DATAPARSER_FAST_PAIR_DECODER_H_ + +#include +#include +#include + +#include "absl/types/optional.h" + +namespace nearby { +namespace fastpair { + +class FastPairDecoder { + public: + static bool HasModelId(const std::vector* service_data); + + static absl::optional GetHexModelIdFromServiceData( + const std::vector* service_data); +}; +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_DATAPARSER_FAST_PAIR_DECODER_H_ diff --git a/fastpair/dataparser/fast_pair_decoder_test.cc b/fastpair/dataparser/fast_pair_decoder_test.cc new file mode 100644 index 00000000..a958f04e --- /dev/null +++ b/fastpair/dataparser/fast_pair_decoder_test.cc @@ -0,0 +1,144 @@ +// Copyright 2022 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 "fastpair/dataparser/fast_pair_decoder.h" + +#include +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/escaping.h" +#include "absl/types/optional.h" +#include "fastpair/testing/fast_pair_service_data_creator.h" + +namespace nearby { +namespace fastpair { +namespace { +constexpr char kModelId[7] = "aabbcc"; +constexpr char kLongModelId[17] = "1122334455667788"; +constexpr char kPaddedModelId[9] = "00001111"; +constexpr char kTrimmedModelId[7] = "001111"; +constexpr uint8_t kLongModelIdHeader = 0b00010000; +constexpr uint8_t kPaddedLongModelIdHeader = 0b00001000; + +bool HasModelIdString(std::string model_id) { + std::string bytes_str = absl::HexStringToBytes(model_id); + std::vector model_id_bytes; + std::move(std::begin(bytes_str), std::end(bytes_str), + std::back_inserter(model_id_bytes)); + return FastPairDecoder::HasModelId(&model_id_bytes); +} + +TEST(FastPairDecoderTest, HasModelIdThreeByteFormat) { + EXPECT_TRUE(HasModelIdString(kModelId)); +} + +TEST(FastPairDecoderTest, HasModelIdTooShort) { + EXPECT_FALSE(HasModelIdString("11")); +} + +TEST(FastPairDecoderTest, HasModelIdLongFormat) { + std::vector bytes = FastPairServiceDataCreator::Builder() + .SetHeader(0b00001000) + .SetModelId("11223344") + .Build() + ->CreateServiceData(); + EXPECT_TRUE(FastPairDecoder::HasModelId(&bytes)); + + bytes = FastPairServiceDataCreator::Builder() + .SetHeader(0b00001010) + .SetModelId("1122334455") + .Build() + ->CreateServiceData(); + + EXPECT_TRUE(FastPairDecoder::HasModelId(&bytes)); +} + +TEST(FastPairDecoderTest, HasModelIdLongInvalidVersion) { + std::vector bytes = FastPairServiceDataCreator::Builder() + .SetHeader(0b00101000) + .SetModelId("11223344") + .Build() + ->CreateServiceData(); + EXPECT_FALSE(FastPairDecoder::HasModelId(&bytes)); +} + +TEST(FastPairDecoderTest, HasModelIdLongInvalidLength) { + std::vector bytes = FastPairServiceDataCreator::Builder() + .SetHeader(0b00001010) + .SetModelId("11223344") + .Build() + ->CreateServiceData(); + EXPECT_FALSE(FastPairDecoder::HasModelId(&bytes)); + + bytes = FastPairServiceDataCreator::Builder() + .SetHeader(0b00000010) + .SetModelId("11223344") + .Build() + ->CreateServiceData(); + + EXPECT_FALSE(FastPairDecoder::HasModelId(&bytes)); +} + +TEST(FastPairDecoderTest, GetHexModelIdFromServiceDataNoResultForNullData) { + EXPECT_EQ(FastPairDecoder::GetHexModelIdFromServiceData(nullptr), + absl::nullopt); +} + +TEST(FastPairDecoderTest, GetHexModelIdFromServiceDataNoResultForEmptyData) { + std::vector empty; + EXPECT_EQ(FastPairDecoder::GetHexModelIdFromServiceData(&empty), + absl::nullopt); +} + +TEST(FastPairDecoderTest, GetHexModelIdFromServiceDataThreeByteData) { + std::vector service_data = FastPairServiceDataCreator::Builder() + .SetModelId(kModelId) + .Build() + ->CreateServiceData(); + EXPECT_EQ(FastPairDecoder::GetHexModelIdFromServiceData(&service_data), + kModelId); +} + +TEST(FastPairDecoderTest, GetHexModelIdFromServiceDataLongModelId) { + std::vector service_data = FastPairServiceDataCreator::Builder() + .SetHeader(kLongModelIdHeader) + .SetModelId(kLongModelId) + .Build() + ->CreateServiceData(); + + EXPECT_EQ(FastPairDecoder::GetHexModelIdFromServiceData(&service_data), + kLongModelId); +} + +TEST(FastPairDecoderTest, GetHexModelIdFromServiceDataLongModelIdTrimmed) { + std::vector service_data = FastPairServiceDataCreator::Builder() + .SetHeader(kPaddedLongModelIdHeader) + .SetModelId(kPaddedModelId) + .Build() + ->CreateServiceData(); + + EXPECT_EQ(FastPairDecoder::GetHexModelIdFromServiceData(&service_data), + kTrimmedModelId); +} + +} // namespace +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/testing/BUILD b/fastpair/testing/BUILD new file mode 100644 index 00000000..53b0c65d --- /dev/null +++ b/fastpair/testing/BUILD @@ -0,0 +1,32 @@ +# Copyright 2022 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. + +licenses(["notice"]) + +cc_library( + name = "testing", + srcs = [ + "fast_pair_service_data_creator.cc", + ], + hdrs = [ + "fast_pair_service_data_creator.h", + ], + visibility = [ + "//fastpair:__subpackages__", + ], + deps = [ + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:optional", + ], +) diff --git a/fastpair/testing/fast_pair_service_data_creator.cc b/fastpair/testing/fast_pair_service_data_creator.cc new file mode 100644 index 00000000..7a414d80 --- /dev/null +++ b/fastpair/testing/fast_pair_service_data_creator.cc @@ -0,0 +1,100 @@ +// Copyright 2022 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 "fastpair/testing/fast_pair_service_data_creator.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/strings/escaping.h" + +namespace nearby { +namespace fastpair { + +FastPairServiceDataCreator::Builder::Builder() = default; + +FastPairServiceDataCreator::Builder::~Builder() = default; + +FastPairServiceDataCreator::Builder& +FastPairServiceDataCreator::Builder::SetHeader(uint8_t byte) { + header_ = byte; + return *this; +} + +FastPairServiceDataCreator::Builder& +FastPairServiceDataCreator::Builder::SetModelId(std::string model_id) { + model_id_ = model_id; + return *this; +} + +FastPairServiceDataCreator::Builder& +FastPairServiceDataCreator::Builder::AddExtraFieldHeader(uint8_t header) { + extra_field_headers_.push_back(header); + return *this; +} + +FastPairServiceDataCreator::Builder& +FastPairServiceDataCreator::Builder::AddExtraField(std::string field) { + extra_fields_.push_back(field); + return *this; +} + +std::unique_ptr +FastPairServiceDataCreator::Builder::Build() { + return std::make_unique( + header_, model_id_, extra_field_headers_, extra_fields_); +} + +FastPairServiceDataCreator::FastPairServiceDataCreator( + absl::optional header, absl::optional model_id, + std::vector extra_field_headers, + std::vector extra_fields) + : header_(header), + model_id_(model_id), + extra_field_headers_(extra_field_headers), + extra_fields_(extra_fields) {} + +FastPairServiceDataCreator::~FastPairServiceDataCreator() = default; + +std::vector FastPairServiceDataCreator::CreateServiceData() { + if (extra_field_headers_.size() != extra_fields_.size()) { + return std::vector(); + } + + std::vector service_data; + + if (header_) service_data.push_back(header_.value()); + + if (model_id_) { + std::string model_id_bytes = absl::HexStringToBytes(model_id_.value()); + std::move(std::begin(model_id_bytes), std::end(model_id_bytes), + std::back_inserter(service_data)); + } + + for (size_t i = 0; i < extra_field_headers_.size(); i++) { + service_data.push_back(extra_field_headers_[i]); + std::string extra_field_bytes = absl::HexStringToBytes(extra_fields_[i]); + std::move(std::begin(extra_field_bytes), std::end(extra_field_bytes), + std::back_inserter(service_data)); + } + + return service_data; +} + +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/testing/fast_pair_service_data_creator.h b/fastpair/testing/fast_pair_service_data_creator.h new file mode 100644 index 00000000..5507d9dd --- /dev/null +++ b/fastpair/testing/fast_pair_service_data_creator.h @@ -0,0 +1,73 @@ +// Copyright 2022 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_FASTPAIR_TESTING_FAST_PAIR_SERVICE_DATA_CREATOR_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_TESTING_FAST_PAIR_SERVICE_DATA_CREATOR_H_ + +#include +#include +#include +#include + +#include "absl/types/optional.h" + +namespace nearby { +namespace fastpair { + +// Convenience class with Builder to create byte arrays which represent Fast +// Pair Service Data. +class FastPairServiceDataCreator { + public: + class Builder { + public: + Builder(); + Builder(const Builder&) = delete; + Builder& operator=(const Builder&) = delete; + ~Builder(); + + Builder& SetHeader(uint8_t byte); + Builder& SetModelId(std::string model_id); + Builder& AddExtraFieldHeader(uint8_t header); + Builder& AddExtraField(std::string field); + std::unique_ptr Build(); + + private: + absl::optional header_; + absl::optional model_id_ = absl::nullopt; + std::vector extra_field_headers_; + std::vector extra_fields_; + }; + + FastPairServiceDataCreator(absl::optional header, + absl::optional model_id, + std::vector extra_field_headers, + std::vector extra_fields); + FastPairServiceDataCreator(const FastPairServiceDataCreator&) = delete; + FastPairServiceDataCreator& operator=(const FastPairServiceDataCreator&) = + delete; + ~FastPairServiceDataCreator(); + + std::vector CreateServiceData(); + + private: + absl::optional header_; + absl::optional model_id_; + std::vector extra_field_headers_; + std::vector extra_fields_; +}; + +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_TESTING_FAST_PAIR_SERVICE_DATA_CREATOR_H_