From 46c0f153fe16981301c0f40622c719a3c6182187 Mon Sep 17 00:00:00 2001 From: Guogang Li Date: Wed, 19 Feb 2025 10:37:22 -0800 Subject: [PATCH] handle data element PiperOrigin-RevId: 728727545 --- Package.swift | 2 + .../mediums/advertisements/BUILD | 39 +++++ .../mediums/advertisements/data_element.cc | 159 ++++++++++++++++++ .../mediums/advertisements/data_element.h | 53 ++++++ .../advertisements/data_element_test.cc | 104 ++++++++++++ 5 files changed, 357 insertions(+) create mode 100644 connections/implementation/mediums/advertisements/BUILD create mode 100644 connections/implementation/mediums/advertisements/data_element.cc create mode 100644 connections/implementation/mediums/advertisements/data_element.h create mode 100644 connections/implementation/mediums/advertisements/data_element_test.cc diff --git a/Package.swift b/Package.swift index 1395545d..34eb73bf 100644 --- a/Package.swift +++ b/Package.swift @@ -406,6 +406,7 @@ let package = Package( // build files "connections/implementation/analytics/BUILD", "connections/implementation/flags/BUILD", + "connections/implementation/mediums/advertisements/BUILD", "connections/implementation/mediums/ble_v2/BUILD", "connections/implementation/mediums/multiplex/BUILD", "connections/implementation/mediums/BUILD", @@ -454,6 +455,7 @@ let package = Package( "connections/implementation/wifi_hotspot_bwu_test.cc", "connections/implementation/analytics/analytics_recorder_test.cc", "connections/implementation/analytics/throughput_recorder_test.cc", + "connections/implementation/mediums/advertisements/data_element_test.cc", "connections/implementation/mediums/ble_v2_test.cc", "connections/implementation/mediums/ble_v2/bloom_filter_test.cc", "connections/implementation/mediums/ble_v2/ble_packet_test.cc", diff --git a/connections/implementation/mediums/advertisements/BUILD b/connections/implementation/mediums/advertisements/BUILD new file mode 100644 index 00000000..0b1f48d9 --- /dev/null +++ b/connections/implementation/mediums/advertisements/BUILD @@ -0,0 +1,39 @@ +# Copyright 2020 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 = "common", + srcs = ["data_element.cc"], + hdrs = ["data_element.h"], + deps = [ + "//internal/platform:base", + "//internal/platform:logging", + "//internal/platform:util", + "@com_google_absl//absl/strings", + ], +) + +cc_test( + name = "data_element_test", + srcs = ["data_element_test.cc"], + deps = [ + ":common", + "//internal/platform:base", + "//internal/platform:util", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/connections/implementation/mediums/advertisements/data_element.cc b/connections/implementation/mediums/advertisements/data_element.cc new file mode 100644 index 00000000..9aa9ac66 --- /dev/null +++ b/connections/implementation/mediums/advertisements/data_element.cc @@ -0,0 +1,159 @@ +// 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. + +#include "connections/implementation/mediums/advertisements/data_element.h" + +#include +#include +#include + +#include "internal/platform/byte_array.h" +#include "internal/platform/logging.h" +#include "internal/platform/stream_reader.h" +#include "internal/platform/stream_writer.h" + +namespace nearby::connections::advertisements { + +std::optional DataElement::From(uint8_t type, std::string value) { + if (type > kMaxTypeValue || type == 0 || value.size() > kMaxLengthValue) { + LOG(WARNING) << "Invalid data element: type=" << type + << ", length=" << value.size(); + return std::nullopt; + } + + DataElement data_element; + data_element.type_ = type; + data_element.value_ = value; + return data_element; +} + +std::optional DataElement::FromStreamReader(StreamReader& reader) { + std::optional is_two_bytes = reader.ReadBits(1); + if (!is_two_bytes.has_value()) { + LOG(WARNING) << "Failed to read type"; + return std::nullopt; + } + + uint8_t type; + uint8_t length; + std::string value; + + if (*is_two_bytes) { + // two bytes DE header + std::optional length_result = reader.ReadBits(7); + if (!length_result.has_value()) { + LOG(WARNING) << "Failed to read length"; + return std::nullopt; + } + std::optional type_result = reader.ReadUint8(); + if (!type_result.has_value() || *type_result == 0) { + LOG(WARNING) << "Failed to read type"; + return std::nullopt; + } + + type = *type_result; + length = *length_result; + + } else { + // one byte DE header + std::optional length_result = reader.ReadBits(3); + if (!length_result.has_value()) { + LOG(WARNING) << "Failed to read length"; + return std::nullopt; + } + std::optional type_result = reader.ReadBits(4); + if (!type_result.has_value() || *type_result == 0) { + LOG(WARNING) << "Failed to read type"; + return std::nullopt; + } + + type = *type_result; + length = *length_result; + } + + if (length > 0) { + std::optional bytes = reader.ReadBytes(length); + if (!bytes.has_value()) { + LOG(WARNING) << "Failed to read bytes"; + return std::nullopt; + } + value = bytes->string_data(); + } + + DataElement data_element; + data_element.type_ = type; + data_element.value_ = value; + return data_element; +} + +std::optional DataElement::FromData(const std::string& data) { + ByteArray bytes(data); + StreamReader reader(bytes); + return FromStreamReader(reader); +} + +std::optional DataElement::ToData() const { + StreamWriter writer; + + if (!ToStreamWriter(writer)) { + LOG(WARNING) << "Failed to write data element"; + return std::nullopt; + } + + return writer.GetData(); +} + +bool DataElement::ToStreamWriter(StreamWriter& writer) const { + bool is_one_byte = true; + if (type_ > 15 || value_.size() > 7) { + is_one_byte = false; + } + + if (is_one_byte) { + if (!writer.WriteBits(0, 1).Ok()) { + LOG(WARNING) << "Failed to write is_one_byte"; + return false; + } + if (!writer.WriteBits(value_.size(), 3).Ok()) { + LOG(WARNING) << "Failed to write length"; + return false; + } + if (!writer.WriteBits(type_, 4).Ok()) { + LOG(WARNING) << "Failed to write type"; + return false; + } + } else { + if (!writer.WriteBits(1, 1).Ok()) { + LOG(WARNING) << "Failed to write is_one_byte"; + return false; + } + if (!writer.WriteBits(value_.size(), 7).Ok()) { + LOG(WARNING) << "Failed to write length"; + return false; + } + if (!writer.WriteUint8(type_).Ok()) { + LOG(WARNING) << "Failed to write type"; + return false; + } + } + + if (!writer.WriteBytes(value_).Ok()) { + LOG(WARNING) << "Failed to write value"; + return false; + } + + return true; +} + +} // namespace nearby::connections::advertisements diff --git a/connections/implementation/mediums/advertisements/data_element.h b/connections/implementation/mediums/advertisements/data_element.h new file mode 100644 index 00000000..2e633266 --- /dev/null +++ b/connections/implementation/mediums/advertisements/data_element.h @@ -0,0 +1,53 @@ +// 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. + +#ifndef THIRD_PARTY_NEARBY_CONNECTIONS_IMPLEMENTATION_MEDIUMS_ADVERTISEMENTS_DATA_ELEMENT_H_ +#define THIRD_PARTY_NEARBY_CONNECTIONS_IMPLEMENTATION_MEDIUMS_ADVERTISEMENTS_DATA_ELEMENT_H_ + +#include +#include +#include + +#include "internal/platform/stream_reader.h" +#include "internal/platform/stream_writer.h" + +namespace nearby::connections::advertisements { + +class DataElement { + public: + constexpr static uint8_t kMaxLengthValue = 0x7f; + constexpr static uint8_t kMaxTypeValue = 0xff; + + ~DataElement() = default; + + static std::optional From(uint8_t type, std::string value); + static std::optional FromStreamReader(StreamReader& reader); + static std::optional FromData(const std::string& data); + + uint8_t type() const { return type_; } + uint8_t length() const { return value_.size(); } + std::string value() const { return value_; } + + std::optional ToData() const; + bool ToStreamWriter(StreamWriter& writer) const; + + private: + // Each DE has a defined meaning and follows a common TLV structure. + uint8_t type_{0}; + std::string value_; +}; + +} // namespace nearby::connections::advertisements + +#endif // THIRD_PARTY_NEARBY_CONNECTIONS_IMPLEMENTATION_MEDIUMS_ADVERTISEMENTS_DATA_ELEMENT_H_ diff --git a/connections/implementation/mediums/advertisements/data_element_test.cc b/connections/implementation/mediums/advertisements/data_element_test.cc new file mode 100644 index 00000000..ca56d46c --- /dev/null +++ b/connections/implementation/mediums/advertisements/data_element_test.cc @@ -0,0 +1,104 @@ +// 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. + +#include "connections/implementation/mediums/advertisements/data_element.h" + +#include +#include + +#include "gtest/gtest.h" +#include "internal/platform/byte_array.h" +#include "internal/platform/stream_reader.h" +#include "internal/platform/stream_writer.h" + +namespace nearby::connections::advertisements { +namespace { + +TEST(DataElementTest, From) { + std::optional data_element = + DataElement::From(0x01, "test_value"); + ASSERT_TRUE(data_element.has_value()); + EXPECT_EQ(data_element->type(), 0x01); + EXPECT_EQ(data_element->length(), 10); + EXPECT_EQ(data_element->value(), "test_value"); +} + +TEST(DataElementTest, FromInvalidType) { + std::optional data_element = + DataElement::From(0x00, "test_value"); + EXPECT_FALSE(data_element.has_value()); +} + +TEST(DataElementTest, FromInvalidLength) { + std::optional data_element = + DataElement::From(0x01, std::string(128, 'a')); + EXPECT_FALSE(data_element.has_value()); +} + +TEST(DataElementTest, OneByteFromStreamReader) { + ByteArray bytes("\x21\x01\x02"); + StreamReader reader(bytes); + std::optional data_element = + DataElement::FromStreamReader(reader); + ASSERT_TRUE(data_element.has_value()); + EXPECT_EQ(data_element->type(), 1); + EXPECT_EQ(data_element->length(), 2); + EXPECT_EQ(data_element->value(), "\x01\x02"); +} + +TEST(DataElementTest, TwoByteFromStreamReader) { + ByteArray bytes("\x84\x01\x01\x02\x03\x04"); + StreamReader reader(bytes); + std::optional data_element = + DataElement::FromStreamReader(reader); + ASSERT_TRUE(data_element.has_value()); + EXPECT_EQ(data_element->type(), 1); + EXPECT_EQ(data_element->length(), 4); + EXPECT_EQ(data_element->value(), "\x01\x02\x03\x04"); +} + +TEST(DataElementTest, FromData) { + std::string data = "\x21\x01\x02"; + std::optional data_element = DataElement::FromData(data); + ASSERT_TRUE(data_element.has_value()); + EXPECT_EQ(data_element->type(), 1); + EXPECT_EQ(data_element->length(), 2); + EXPECT_EQ(data_element->value(), "\x01\x02"); +} + +TEST(DataElementTest, InvalidData) { + EXPECT_FALSE(DataElement::FromData("").has_value()); + EXPECT_FALSE(DataElement::FromData("\x20").has_value()); + EXPECT_FALSE(DataElement::FromData("\x21\x00").has_value()); + EXPECT_FALSE(DataElement::FromData("\x91\x00").has_value()); + EXPECT_FALSE(DataElement::FromData("\x91\x01").has_value()); + EXPECT_TRUE(DataElement::FromData("\x81\x01\x03").has_value()); +} + +TEST(DataElementTest, ToData) { + std::string data = "\x21\x01\x03"; + EXPECT_EQ(DataElement::FromData(data)->ToData(), data); + data = "\x81\x23\x21"; + EXPECT_EQ(DataElement::FromData(data)->ToData(), data); +} + +TEST(DataElementTest, ToStreamWriter) { + StreamWriter writer; + std::string data = "\x21\x01\x03"; + DataElement::FromData(data)->ToStreamWriter(writer); + EXPECT_EQ(writer.GetData(), data); +} + +} // namespace +} // namespace nearby::connections::advertisements