| // Copyright 2020 The Chromium 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 "protocol_core.h" |
| |
| #include <memory> |
| |
| #include "cbor.h" |
| #include "maybe.h" |
| #include "status_test_support.h" |
| #include "test_platform.h" |
| |
| namespace v8_crdtp { |
| |
| // Test-only. Real-life bindings use UTF8/16 conversions as needed. |
| template <> |
| struct ProtocolTypeTraits<std::string> { |
| static bool Deserialize(DeserializerState* state, std::string* value) { |
| if (state->tokenizer()->TokenTag() == cbor::CBORTokenTag::STRING8) { |
| auto cbor_span = state->tokenizer()->GetString8(); |
| value->assign(reinterpret_cast<const char*>(cbor_span.data()), |
| cbor_span.size()); |
| return true; |
| } |
| state->RegisterError(Error::BINDINGS_STRING8_VALUE_EXPECTED); |
| return false; |
| } |
| |
| static void Serialize(const std::string& value, std::vector<uint8_t>* bytes) { |
| cbor::EncodeString8( |
| span<uint8_t>(reinterpret_cast<const uint8_t*>(value.data()), |
| value.size()), |
| bytes); |
| } |
| }; |
| |
| namespace { |
| using ::testing::Eq; |
| |
| template <typename TResult, typename TArg> |
| std::unique_ptr<TResult> RoundtripToType(const TArg& obj) { |
| std::vector<uint8_t> bytes; |
| obj.AppendSerialized(&bytes); |
| |
| StatusOr<std::unique_ptr<TResult>> result = |
| TResult::ReadFrom(std::move(bytes)); |
| return std::move(result).value(); |
| } |
| |
| template <typename T> |
| std::unique_ptr<T> Roundtrip(const T& obj) { |
| return RoundtripToType<T, T>(obj); |
| } |
| |
| // These TestTypeFOO classes below would normally be generated |
| // by the protocol generator. |
| |
| class TestTypeBasic : public ProtocolObject<TestTypeBasic> { |
| public: |
| TestTypeBasic() = default; |
| |
| const std::string& GetValue() const { return value_; } |
| void SetValue(std::string value) { value_ = std::move(value); } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| std::string value_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeBasic) |
| V8_CRDTP_DESERIALIZE_FIELD("value", value_) |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeBasic) |
| V8_CRDTP_SERIALIZE_FIELD("value", value_); |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST(ProtocolCoreTest, Basic) { |
| TestTypeBasic obj1; |
| obj1.SetValue("foo"); |
| |
| auto obj2 = Roundtrip(obj1); |
| ASSERT_THAT(obj2, Not(testing::IsNull())); |
| EXPECT_THAT(obj2->GetValue(), Eq("foo")); |
| } |
| |
| TEST(ProtocolCoreTest, FailedToDeserializeTestTypeBasic) { |
| std::vector<uint8_t> garbage = {'g', 'a', 'r', 'b', 'a', 'g', 'e'}; |
| StatusOr<std::unique_ptr<TestTypeBasic>> result = |
| TestTypeBasic::ReadFrom(std::move(garbage)); |
| EXPECT_THAT(result.status(), StatusIs(Error::CBOR_INVALID_STRING8, 0)); |
| } |
| |
| class TestTypeBasicDouble : public ProtocolObject<TestTypeBasicDouble> { |
| public: |
| TestTypeBasicDouble() = default; |
| |
| double GetValue() const { return value_; } |
| void SetValue(double value) { value_ = value; } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| double value_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeBasicDouble) |
| V8_CRDTP_DESERIALIZE_FIELD("value", value_) |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeBasicDouble) |
| V8_CRDTP_SERIALIZE_FIELD("value", value_); |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST(TestBasicDouble, ParserAllowsAllowsDoubleEncodedAsInt) { |
| // We allow double's encoded as INT32, because this is what a roundtrip via |
| // JSON would produce. |
| std::vector<uint8_t> encoded; |
| crdtp::cbor::EnvelopeEncoder envelope; |
| envelope.EncodeStart(&encoded); |
| encoded.push_back(crdtp::cbor::EncodeIndefiniteLengthMapStart()); |
| crdtp::cbor::EncodeString8(crdtp::SpanFrom("value"), &encoded); |
| crdtp::cbor::EncodeInt32( |
| 42, &encoded); // It's a double field, but we encode an int. |
| encoded.push_back(crdtp::cbor::EncodeStop()); |
| envelope.EncodeStop(&encoded); |
| auto obj = TestTypeBasicDouble::ReadFrom(encoded).value(); |
| ASSERT_THAT(obj, Not(testing::IsNull())); |
| EXPECT_THAT(obj->GetValue(), Eq(42)); |
| } |
| |
| class TestTypeComposite : public ProtocolObject<TestTypeComposite> { |
| public: |
| bool GetBoolField() const { return bool_field_; } |
| void SetBoolField(bool value) { bool_field_ = value; } |
| |
| int GetIntField() const { return int_field_; } |
| void SetIntField(int value) { int_field_ = value; } |
| |
| double GetDoubleField() const { return double_field_; } |
| void SetDoubleField(double value) { double_field_ = value; } |
| |
| const std::string& GetStrField() const { return str_field_; } |
| void SetStrField(std::string value) { str_field_ = std::move(value); } |
| |
| const TestTypeBasic* GetTestTypeBasicField() { |
| return test_type1_field_.get(); |
| } |
| void SetTestTypeBasicField(std::unique_ptr<TestTypeBasic> value) { |
| test_type1_field_ = std::move(value); |
| } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| bool bool_field_ = false; |
| int int_field_ = 0; |
| double double_field_ = 0.0; |
| std::string str_field_; |
| std::unique_ptr<TestTypeBasic> test_type1_field_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeComposite) |
| V8_CRDTP_DESERIALIZE_FIELD("bool_field", bool_field_), |
| V8_CRDTP_DESERIALIZE_FIELD("double_field", double_field_), |
| V8_CRDTP_DESERIALIZE_FIELD("int_field", int_field_), |
| V8_CRDTP_DESERIALIZE_FIELD("str_field", str_field_), |
| V8_CRDTP_DESERIALIZE_FIELD("test_type1_field", test_type1_field_), |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeComposite) |
| V8_CRDTP_SERIALIZE_FIELD("bool_field", bool_field_), |
| V8_CRDTP_SERIALIZE_FIELD("double_field", double_field_), |
| V8_CRDTP_SERIALIZE_FIELD("int_field", int_field_), |
| V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_), |
| V8_CRDTP_SERIALIZE_FIELD("test_type1_field", test_type1_field_), |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST(ProtocolCoreTest, Composite) { |
| TestTypeComposite obj1; |
| obj1.SetBoolField(true); |
| obj1.SetIntField(42); |
| obj1.SetDoubleField(2.718281828); |
| obj1.SetStrField("bar"); |
| auto val1 = std::make_unique<TestTypeBasic>(); |
| val1->SetValue("bazzzz"); |
| obj1.SetTestTypeBasicField(std::move(val1)); |
| |
| auto obj2 = Roundtrip(obj1); |
| ASSERT_THAT(obj2, Not(testing::IsNull())); |
| EXPECT_THAT(obj2->GetBoolField(), Eq(true)); |
| EXPECT_THAT(obj2->GetIntField(), Eq(42)); |
| EXPECT_THAT(obj2->GetDoubleField(), Eq(2.718281828)); |
| EXPECT_THAT(obj2->GetStrField(), Eq("bar")); |
| EXPECT_THAT(obj2->GetTestTypeBasicField()->GetValue(), Eq("bazzzz")); |
| } |
| |
| class CompositeParsingTest : public testing::Test { |
| public: |
| CompositeParsingTest() { |
| TestTypeComposite top; |
| top.SetIntField(42); |
| top.SetBoolField(true); |
| top.SetIntField(42); |
| top.SetDoubleField(2.718281828); |
| top.SetStrField("junk"); |
| auto child = std::make_unique<TestTypeBasic>(); |
| child->SetValue("child_value"); |
| top.SetTestTypeBasicField(std::move(child)); |
| |
| // Let's establish that |serialized_| is a properly serialized |
| // representation of |top|, by checking that it deserializes ok. |
| top.AppendSerialized(&serialized_); |
| TestTypeComposite::ReadFrom(serialized_).value(); |
| } |
| |
| protected: |
| std::vector<uint8_t> serialized_; |
| }; |
| |
| TEST_F(CompositeParsingTest, DecodingFailure_CBORTokenizer) { |
| // Mutates |serialized_| so that it won't parse correctly. In this case, |
| // we're changing a string value so that it's invalid, making CBORTokenizer |
| // unhappy. |
| size_t position = |
| std::string(reinterpret_cast<const char*>(serialized_.data()), |
| serialized_.size()) |
| .find("child_value"); |
| EXPECT_GT(position, 0ul); |
| // We override the byte just before so that it's still a string |
| // (3 << 5), but the length is encoded in the bytes that follows. |
| // So, we override that with 0xff (255), which exceeds the length |
| // of the message and thereby makes the string8 invalid. |
| --position; |
| serialized_[position] = 3 << 5 | // major type: STRING |
| 25; // length in encoded in byte that follows. |
| serialized_[position + 1] = 0xff; // length |
| auto result = TestTypeComposite::ReadFrom(serialized_); |
| |
| EXPECT_THAT(result.status(), StatusIs(Error::CBOR_INVALID_STRING8, position)); |
| } |
| |
| TEST_F(CompositeParsingTest, DecodingFailure_MandatoryFieldMissingShallow) { |
| // We're changing the string key "int_field" to something else ("lnt_field"), |
| // so that the mandatory field value won't be found. Unknown fields are |
| // ignored for compatibility, so that's why this simple technique works here. |
| size_t position = |
| std::string(reinterpret_cast<const char*>(serialized_.data()), |
| serialized_.size()) |
| .find("int_field"); |
| serialized_[position] = 'l'; // Change 'i' to 'l'. |
| // serialized_.size() - 1 is the STOP character for the entire message, |
| size_t expected_error_pos = serialized_.size() - 1; |
| auto result = TestTypeComposite::ReadFrom(serialized_); |
| EXPECT_THAT(result.status(), StatusIs(Error::BINDINGS_MANDATORY_FIELD_MISSING, |
| expected_error_pos)); |
| } |
| |
| TEST_F(CompositeParsingTest, DecodingFailure_MandatoryFieldMissingNested) { |
| // We're changing the string key "value" to something else ("falue"), so that |
| // the mandatory field value in TestTypeBasic in the child won't be found. |
| size_t position = |
| std::string(reinterpret_cast<const char*>(serialized_.data()), |
| serialized_.size()) |
| .find("value"); |
| serialized_[position] = 'f'; // Change 'v' to 'f'. |
| // serialized_.size() - 1 is the STOP character for the enclosing message, |
| // and serialized_.size() - 2 is the STOP character for TestTypeBasic. |
| size_t expected_error_pos = serialized_.size() - 2; |
| auto result = TestTypeComposite::ReadFrom(serialized_); |
| EXPECT_THAT(result.status(), StatusIs(Error::BINDINGS_MANDATORY_FIELD_MISSING, |
| expected_error_pos)); |
| } |
| |
| TEST_F(CompositeParsingTest, DecodingFailure_BoolValueExpected) { |
| // We're changing the bool value (true) to null; we do this by looking |
| // for bool_field, and searching from there for TRUE; both TRUE and null |
| // are just one byte in the serialized buffer, so this swap is convenient. |
| std::string serialized_view(reinterpret_cast<const char*>(serialized_.data()), |
| serialized_.size()); |
| size_t position = serialized_view.find("bool_field"); |
| for (; position < serialized_.size(); ++position) { |
| if (serialized_[position] == crdtp::cbor::EncodeTrue()) { |
| serialized_[position] = crdtp::cbor::EncodeNull(); |
| break; |
| } |
| } |
| auto result = TestTypeComposite::ReadFrom(serialized_); |
| EXPECT_THAT(result.status(), |
| StatusIs(Error::BINDINGS_BOOL_VALUE_EXPECTED, position)); |
| } |
| |
| class TestTypeArrays : public ProtocolObject<TestTypeArrays> { |
| public: |
| const std::vector<int>* GetIntArray() const { return &int_array_; } |
| void SetIntArray(std::vector<int> value) { int_array_ = std::move(value); } |
| |
| const std::vector<double>* GetDoubleArray() const { return &double_array_; } |
| void SetDoubleArray(std::vector<double> value) { |
| double_array_ = std::move(value); |
| } |
| |
| const std::vector<std::string>* GetStrArray() const { return &str_array_; } |
| void SetStrArray(std::vector<std::string> value) { |
| str_array_ = std::move(value); |
| } |
| |
| const std::vector<std::unique_ptr<TestTypeBasic>>* GetTestTypeBasicArray() |
| const { |
| return &test_type_basic_array_; |
| } |
| |
| void SetTestTypeBasicArray( |
| std::vector<std::unique_ptr<TestTypeBasic>> value) { |
| test_type_basic_array_ = std::move(value); |
| } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| std::vector<int> int_array_; |
| std::vector<double> double_array_; |
| std::vector<std::string> str_array_; |
| std::vector<std::unique_ptr<TestTypeBasic>> test_type_basic_array_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeArrays) |
| V8_CRDTP_DESERIALIZE_FIELD("int_array", int_array_), |
| V8_CRDTP_DESERIALIZE_FIELD("str_array", str_array_), |
| V8_CRDTP_DESERIALIZE_FIELD("test_type_basic_array", test_type_basic_array_), |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeArrays) |
| V8_CRDTP_SERIALIZE_FIELD("int_array", int_array_), |
| V8_CRDTP_SERIALIZE_FIELD("str_array", str_array_), |
| V8_CRDTP_SERIALIZE_FIELD("test_type_basic_array", test_type_basic_array_), |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST_F(CompositeParsingTest, Arrays) { |
| TestTypeArrays obj1; |
| obj1.SetIntArray(std::vector<int>{1, 3, 5, 7}); |
| std::vector<std::string> strs; |
| strs.emplace_back("foo"); |
| strs.emplace_back(std::string("bar")); |
| obj1.SetStrArray(std::move(strs)); |
| auto val1 = std::make_unique<TestTypeBasic>(); |
| val1->SetValue("bazzzz"); |
| std::vector<std::unique_ptr<TestTypeBasic>> vec1; |
| vec1.emplace_back(std::move(val1)); |
| obj1.SetTestTypeBasicArray(std::move(vec1)); |
| |
| auto obj2 = Roundtrip(obj1); |
| ASSERT_THAT(obj2, Not(testing::IsNull())); |
| EXPECT_THAT(*obj2->GetIntArray(), testing::ElementsAre(1, 3, 5, 7)); |
| EXPECT_THAT(*obj2->GetStrArray(), testing::ElementsAre("foo", "bar")); |
| EXPECT_THAT(obj2->GetDoubleArray()->size(), Eq(0ul)); |
| EXPECT_THAT(obj2->GetTestTypeBasicArray()->size(), Eq(1ul)); |
| EXPECT_THAT(obj2->GetTestTypeBasicArray()->front()->GetValue(), Eq("bazzzz")); |
| } |
| |
| class TestTypeOptional : public ProtocolObject<TestTypeOptional> { |
| public: |
| TestTypeOptional() = default; |
| |
| bool HasIntField() const { return int_field_.isJust(); } |
| int GetIntField() const { return int_field_.fromJust(); } |
| void SetIntField(int value) { int_field_ = value; } |
| |
| bool HasStrField() { return str_field_.isJust(); } |
| const std::string& GetStrField() const { return str_field_.fromJust(); } |
| void SetStrField(std::string value) { str_field_ = std::move(value); } |
| |
| bool HasTestTypeBasicField() { return test_type_basic_field_.isJust(); } |
| const TestTypeBasic* GetTestTypeBasicField() const { |
| return test_type_basic_field_.isJust() ? test_type_basic_field_.fromJust() |
| : nullptr; |
| } |
| void SetTestTypeBasicField(std::unique_ptr<TestTypeBasic> value) { |
| test_type_basic_field_ = std::move(value); |
| } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| Maybe<int> int_field_; |
| Maybe<std::string> str_field_; |
| Maybe<TestTypeBasic> test_type_basic_field_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeOptional) |
| V8_CRDTP_DESERIALIZE_FIELD_OPT("int_field", int_field_), |
| V8_CRDTP_DESERIALIZE_FIELD_OPT("str_field", str_field_), |
| V8_CRDTP_DESERIALIZE_FIELD_OPT("test_type_basic_field", test_type_basic_field_), |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeOptional) |
| V8_CRDTP_SERIALIZE_FIELD("int_field", int_field_), |
| V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_), |
| V8_CRDTP_SERIALIZE_FIELD("test_type_basic_field", test_type_basic_field_), |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST(ProtocolCoreTest, OptionalAbsent) { |
| TestTypeOptional obj1; |
| auto obj2 = Roundtrip(obj1); |
| ASSERT_THAT(obj2, Not(testing::IsNull())); |
| |
| EXPECT_THAT(obj2->HasIntField(), Eq(false)); |
| EXPECT_THAT(obj2->HasStrField(), Eq(false)); |
| EXPECT_THAT(obj2->HasTestTypeBasicField(), Eq(false)); |
| } |
| |
| TEST(ProtocolCoreTest, OptionalPresent) { |
| TestTypeOptional obj1; |
| obj1.SetIntField(42); |
| obj1.SetStrField("foo"); |
| |
| auto val1 = std::make_unique<TestTypeBasic>(); |
| val1->SetValue("bar"); |
| obj1.SetTestTypeBasicField(std::move(val1)); |
| |
| auto obj2 = Roundtrip(obj1); |
| ASSERT_THAT(obj2, Not(testing::IsNull())); |
| |
| EXPECT_THAT(obj2->HasIntField(), Eq(true)); |
| EXPECT_THAT(obj2->GetIntField(), Eq(42)); |
| EXPECT_THAT(obj2->HasStrField(), Eq(true)); |
| EXPECT_THAT(obj2->GetStrField(), Eq("foo")); |
| EXPECT_THAT(obj2->HasTestTypeBasicField(), Eq(true)); |
| EXPECT_THAT(obj2->GetTestTypeBasicField()->GetValue(), Eq("bar")); |
| } |
| |
| class TestTypeLazy : public ProtocolObject<TestTypeLazy> { |
| public: |
| TestTypeLazy() = default; |
| |
| const std::string& GetStrField() const { return str_field_; } |
| void SetStrField(std::string value) { str_field_ = std::move(value); } |
| |
| const DeferredMessage* deferred_test_type1_field() const { |
| return test_type1_field_.get(); |
| } |
| |
| private: |
| DECLARE_SERIALIZATION_SUPPORT(); |
| |
| std::string str_field_; |
| std::unique_ptr<DeferredMessage> test_type1_field_; |
| }; |
| |
| // clang-format off |
| V8_CRDTP_BEGIN_DESERIALIZER(TestTypeLazy) |
| V8_CRDTP_DESERIALIZE_FIELD("str_field", str_field_), |
| V8_CRDTP_DESERIALIZE_FIELD_OPT("test_type1_field", test_type1_field_), |
| V8_CRDTP_END_DESERIALIZER() |
| |
| V8_CRDTP_BEGIN_SERIALIZER(TestTypeLazy) |
| V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_), |
| V8_CRDTP_SERIALIZE_FIELD("test_type1_field", test_type1_field_), |
| V8_CRDTP_END_SERIALIZER(); |
| // clang-format on |
| |
| TEST(ProtocolCoreTest, TestDeferredMessage) { |
| TestTypeComposite obj1; |
| obj1.SetStrField("bar"); |
| auto val1 = std::make_unique<TestTypeBasic>(); |
| val1->SetValue("bazzzz"); |
| obj1.SetTestTypeBasicField(std::move(val1)); |
| |
| auto obj2 = RoundtripToType<TestTypeLazy>(obj1); |
| EXPECT_THAT(obj2->GetStrField(), Eq("bar")); |
| |
| TestTypeBasic basic_val; |
| auto deserializer = obj2->deferred_test_type1_field()->MakeDeserializer(); |
| EXPECT_THAT(TestTypeBasic::Deserialize(&deserializer, &basic_val), Eq(true)); |
| EXPECT_THAT(basic_val.GetValue(), Eq("bazzzz")); |
| |
| StatusOr<std::unique_ptr<TestTypeBasic>> maybe_parsed = |
| TestTypeBasic::ReadFrom(*obj2->deferred_test_type1_field()); |
| ASSERT_THAT(maybe_parsed.status(), StatusIsOk()); |
| ASSERT_THAT((*maybe_parsed), Not(testing::IsNull())); |
| ASSERT_EQ((*maybe_parsed)->GetValue(), "bazzzz"); |
| } |
| |
| } // namespace |
| } // namespace v8_crdtp |