diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/CborShapeDeserializer.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/CborShapeDeserializer.h new file mode 100644 index 00000000000..c7714db8559 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/CborShapeDeserializer.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +namespace smithy { +namespace schema { + +class SMITHY_API CborShapeDeserializer final : public ShapeDeserializer { + public: + CborShapeDeserializer(const unsigned char* data, size_t length); + ~CborShapeDeserializer(); + + Aws::Crt::Optional ReadBoolean() override; + Aws::Crt::Optional ReadInteger() override; + Aws::Crt::Optional ReadLong() override; + Aws::Crt::Optional ReadFloat() override; + Aws::Crt::Optional ReadDouble() override; + Aws::Crt::Optional ReadString() override; + Aws::Crt::Optional ReadTimestamp() override; + Aws::Crt::Optional ReadBlob() override; + Aws::Crt::Optional ReadEnum() override; + + size_t BeginStruct() override; + void EndStruct() override; + + size_t BeginList() override; + void EndList() override; + + size_t BeginMap() override; + void EndMap() override; + + Aws::Crt::Optional ReadKey() override; + bool IsBreak() override; + bool IsNull() override; + void ReadNull() override; + void SkipValue() override; + + private: + class Impl; + Aws::UniquePtr m_impl; +}; + +} // namespace schema +} // namespace smithy diff --git a/src/aws-cpp-sdk-core/include/smithy/client/schema/ShapeDeserializer.h b/src/aws-cpp-sdk-core/include/smithy/client/schema/ShapeDeserializer.h new file mode 100644 index 00000000000..f28e66a6c3a --- /dev/null +++ b/src/aws-cpp-sdk-core/include/smithy/client/schema/ShapeDeserializer.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace smithy { +namespace schema { + +class SMITHY_API ShapeDeserializer { + public: + virtual ~ShapeDeserializer() = default; + + virtual Aws::Crt::Optional ReadBoolean() = 0; + virtual Aws::Crt::Optional ReadInteger() = 0; + virtual Aws::Crt::Optional ReadLong() = 0; + virtual Aws::Crt::Optional ReadFloat() = 0; + virtual Aws::Crt::Optional ReadDouble() = 0; + virtual Aws::Crt::Optional ReadString() = 0; + virtual Aws::Crt::Optional ReadTimestamp() = 0; + virtual Aws::Crt::Optional ReadBlob() = 0; + virtual Aws::Crt::Optional ReadEnum() = 0; + + virtual size_t BeginStruct() = 0; + virtual void EndStruct() = 0; + + virtual size_t BeginList() = 0; + virtual void EndList() = 0; + + virtual size_t BeginMap() = 0; + virtual void EndMap() = 0; + + virtual Aws::Crt::Optional ReadKey() = 0; + virtual bool IsBreak() = 0; + virtual bool IsNull() = 0; + virtual void ReadNull() = 0; + virtual void SkipValue() = 0; +}; + +} // namespace schema +} // namespace smithy diff --git a/src/aws-cpp-sdk-core/source/smithy/client/schema/CborShapeDeserializer.cpp b/src/aws-cpp-sdk-core/source/smithy/client/schema/CborShapeDeserializer.cpp new file mode 100644 index 00000000000..dff8c39525c --- /dev/null +++ b/src/aws-cpp-sdk-core/source/smithy/client/schema/CborShapeDeserializer.cpp @@ -0,0 +1,213 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include + +using namespace smithy::schema; +using namespace Aws::Utils; +using namespace Aws::Crt::Cbor; + +class CborShapeDeserializer::Impl { + public: + Impl(const unsigned char* data, size_t length) : m_decoder(Aws::Crt::ByteCursorFromArray(data, length)) {} + + Aws::Crt::Optional ReadBoolean() { + auto val = m_decoder.PopNextBooleanVal(); + if (!val.has_value()) { + return {}; + } + return val.value(); + } + + Aws::Crt::Optional ReadInteger() { + auto val = ReadLong(); + if (!val.has_value()) { + return {}; + } + return static_cast(val.value()); + } + + Aws::Crt::Optional ReadLong() { + auto type = m_decoder.PeekType(); + if (!type.has_value()) { + return {}; + } + if (*type == CborType::UInt) { + auto val = m_decoder.PopNextUnsignedIntVal(); + if (!val.has_value()) { + return {}; + } + return static_cast(val.value()); + } + if (*type == CborType::NegInt) { + auto val = m_decoder.PopNextNegativeIntVal(); + if (!val.has_value()) { + return {}; + } + return static_cast(-(static_cast(val.value()) + 1)); + } + return {}; + } + + Aws::Crt::Optional ReadFloat() { + auto val = ReadDouble(); + if (!val.has_value()) { + return {}; + } + return static_cast(val.value()); + } + + Aws::Crt::Optional ReadDouble() { + auto type = m_decoder.PeekType(); + if (!type.has_value()) { + return {}; + } + if (*type == CborType::Float) { + auto val = m_decoder.PopNextFloatVal(); + if (!val.has_value()) { + return {}; + } + return val.value(); + } + auto longVal = ReadLong(); + if (!longVal.has_value()) { + return {}; + } + return static_cast(longVal.value()); + } + + Aws::Crt::Optional ReadString() { + auto val = m_decoder.PopNextTextVal(); + if (!val.has_value()) { + return {}; + } + return Aws::String(reinterpret_cast(val->ptr), val->len); + } + + Aws::Crt::Optional ReadTimestamp() { + auto tag = m_decoder.PopNextTagVal(); + if (!tag.has_value() || tag.value() != 1) { + return {}; + } + auto type = m_decoder.PeekType(); + if (!type.has_value()) { + return {}; + } + if (*type == CborType::Float) { + auto val = m_decoder.PopNextFloatVal(); + if (!val.has_value() || val.value() < 0.0) { + return {}; + } + return DateTime(val.value()); + } + if (*type == CborType::NegInt) { + return {}; + } + auto val = ReadLong(); + if (!val.has_value()) { + return {}; + } + return DateTime(static_cast(val.value())); + } + + Aws::Crt::Optional ReadBlob() { + auto val = m_decoder.PopNextBytesVal(); + if (!val.has_value()) { + return {}; + } + return ByteBuffer(val->ptr, val->len); + } + + Aws::Crt::Optional ReadEnum() { return ReadInteger(); } + + size_t BeginStruct() { + auto type = m_decoder.PeekType(); + if (type.has_value() && *type == CborType::IndefMapStart) { + m_decoder.ConsumeNextSingleElement(); + return 0; + } + auto size = m_decoder.PopNextMapStart(); + if (!size.has_value()) { + return 0; + } + return static_cast(size.value()); + } + + void EndStruct() {} + + size_t BeginList() { + auto type = m_decoder.PeekType(); + if (type.has_value() && *type == CborType::IndefArrayStart) { + m_decoder.ConsumeNextSingleElement(); + return 0; + } + auto size = m_decoder.PopNextArrayStart(); + if (!size.has_value()) { + return 0; + } + return static_cast(size.value()); + } + + void EndList() {} + + size_t BeginMap() { + auto type = m_decoder.PeekType(); + if (type.has_value() && *type == CborType::IndefMapStart) { + m_decoder.ConsumeNextSingleElement(); + return 0; + } + auto size = m_decoder.PopNextMapStart(); + if (!size.has_value()) { + return 0; + } + return static_cast(size.value()); + } + + void EndMap() {} + + Aws::Crt::Optional ReadKey() { return ReadString(); } + + bool IsBreak() { + auto type = m_decoder.PeekType(); + return type.has_value() && *type == CborType::Break; + } + + bool IsNull() { + auto type = m_decoder.PeekType(); + return type.has_value() && *type == CborType::Null; + } + + void ReadNull() { m_decoder.ConsumeNextSingleElement(); } + + void SkipValue() { m_decoder.ConsumeNextWholeDataItem(); } + + private: + CborDecoder m_decoder; +}; + +CborShapeDeserializer::CborShapeDeserializer(const unsigned char* data, size_t length) + : m_impl(Aws::MakeUnique("CborShapeDeserializer", data, length)) {} +CborShapeDeserializer::~CborShapeDeserializer() = default; + +Aws::Crt::Optional CborShapeDeserializer::ReadBoolean() { return m_impl->ReadBoolean(); } +Aws::Crt::Optional CborShapeDeserializer::ReadInteger() { return m_impl->ReadInteger(); } +Aws::Crt::Optional CborShapeDeserializer::ReadLong() { return m_impl->ReadLong(); } +Aws::Crt::Optional CborShapeDeserializer::ReadFloat() { return m_impl->ReadFloat(); } +Aws::Crt::Optional CborShapeDeserializer::ReadDouble() { return m_impl->ReadDouble(); } +Aws::Crt::Optional CborShapeDeserializer::ReadString() { return m_impl->ReadString(); } +Aws::Crt::Optional CborShapeDeserializer::ReadTimestamp() { return m_impl->ReadTimestamp(); } +Aws::Crt::Optional CborShapeDeserializer::ReadBlob() { return m_impl->ReadBlob(); } +Aws::Crt::Optional CborShapeDeserializer::ReadEnum() { return m_impl->ReadEnum(); } +size_t CborShapeDeserializer::BeginStruct() { return m_impl->BeginStruct(); } +void CborShapeDeserializer::EndStruct() { m_impl->EndStruct(); } +size_t CborShapeDeserializer::BeginList() { return m_impl->BeginList(); } +void CborShapeDeserializer::EndList() { m_impl->EndList(); } +size_t CborShapeDeserializer::BeginMap() { return m_impl->BeginMap(); } +void CborShapeDeserializer::EndMap() { m_impl->EndMap(); } +Aws::Crt::Optional CborShapeDeserializer::ReadKey() { return m_impl->ReadKey(); } +bool CborShapeDeserializer::IsBreak() { return m_impl->IsBreak(); } +bool CborShapeDeserializer::IsNull() { return m_impl->IsNull(); } +void CborShapeDeserializer::ReadNull() { m_impl->ReadNull(); } +void CborShapeDeserializer::SkipValue() { m_impl->SkipValue(); } diff --git a/tests/aws-cpp-sdk-core-tests/smithy/client/schema/CborShapeDeserializerTest.cpp b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/CborShapeDeserializerTest.cpp new file mode 100644 index 00000000000..a08539687ea --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/smithy/client/schema/CborShapeDeserializerTest.cpp @@ -0,0 +1,634 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include +#include +#include + +using namespace smithy::schema; + +class CborShapeDeserializerTest : public Aws::Testing::AwsCppSdkGTestSuite {}; + +// --- Scalars --- + +TEST_F(CborShapeDeserializerTest, BooleanTrue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("enabled"); + s.WriteBoolean(root, true); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + ASSERT_FALSE(d.IsBreak()); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "enabled"); + auto val = d.ReadBoolean(); + ASSERT_TRUE(val.has_value()); + EXPECT_TRUE(val.value()); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, BooleanFalse) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("ok"); + s.WriteBoolean(root, false); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "ok"); + auto val = d.ReadBoolean(); + ASSERT_TRUE(val.has_value()); + EXPECT_FALSE(val.value()); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, IntegerSmall) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("n"); + s.WriteInteger(root, 7); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "n"); + auto val = d.ReadInteger(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), 7); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, IntegerNegative) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("n"); + s.WriteInteger(root, -42); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "n"); + auto val = d.ReadInteger(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), -42); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, LongValue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("big"); + s.WriteLong(root, 5000000000LL); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "big"); + auto val = d.ReadLong(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), 5000000000LL); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, DoubleValue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("d"); + s.WriteDouble(root, 3.14); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "d"); + auto val = d.ReadDouble(); + ASSERT_TRUE(val.has_value()); + EXPECT_DOUBLE_EQ(val.value(), 3.14); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, DoubleWholeNumber) { + // CRT encodes 5.0 as integer 5 — deserializer must handle this + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("d"); + s.WriteDouble(root, 5.0); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "d"); + auto val = d.ReadDouble(); + ASSERT_TRUE(val.has_value()); + EXPECT_DOUBLE_EQ(val.value(), 5.0); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, FloatValue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("f"); + s.WriteFloat(root, 1.5f); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "f"); + auto val = d.ReadFloat(); + ASSERT_TRUE(val.has_value()); + EXPECT_FLOAT_EQ(val.value(), 1.5f); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, StringValue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("name"); + s.WriteString(root, "hello"); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "name"); + auto val = d.ReadString(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), "hello"); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, BlobValue) { + CborShapeSerializer s; + Schema root; + Aws::Utils::ByteBuffer blob(4); + blob[0] = 0xDE; + blob[1] = 0xAD; + blob[2] = 0xBE; + blob[3] = 0xEF; + s.BeginStructure(root); + s.WriteMapKey("data"); + s.WriteBlob(root, blob); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "data"); + auto result = d.ReadBlob(); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result.value().GetLength(), 4u); + EXPECT_EQ(result.value()[0], 0xDE); + EXPECT_EQ(result.value()[1], 0xAD); + EXPECT_EQ(result.value()[2], 0xBE); + EXPECT_EQ(result.value()[3], 0xEF); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, TimestampValue) { + CborShapeSerializer s; + Schema root; + Aws::Utils::DateTime dt(1234567890.0); // seconds since epoch + s.BeginStructure(root); + s.WriteMapKey("ts"); + s.WriteTimestamp(root, dt); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "ts"); + auto result = d.ReadTimestamp(); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().Seconds(), 1234567890); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, NullValue) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("item"); + s.WriteNull(root); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "item"); + EXPECT_TRUE(d.IsNull()); + d.ReadNull(); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +// --- Collections --- + +TEST_F(CborShapeDeserializerTest, ListOfIntegers) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("nums"); + s.BeginList(root, 3); + s.WriteInteger(root, 10); + s.WriteInteger(root, 20); + s.WriteInteger(root, 30); + s.EndList(); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "nums"); + size_t count = d.BeginList(); + EXPECT_EQ(count, 3u); + auto v1 = d.ReadInteger(); + ASSERT_TRUE(v1.has_value()); + EXPECT_EQ(v1.value(), 10); + auto v2 = d.ReadInteger(); + ASSERT_TRUE(v2.has_value()); + EXPECT_EQ(v2.value(), 20); + auto v3 = d.ReadInteger(); + ASSERT_TRUE(v3.has_value()); + EXPECT_EQ(v3.value(), 30); + d.EndList(); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, MapOfStrings) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("headers"); + s.BeginMap(root, 2); + s.WriteMapKey("foo"); + s.WriteString(root, "bar"); + s.WriteMapKey("baz"); + s.WriteString(root, "qux"); + s.EndMap(); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "headers"); + size_t count = d.BeginMap(); + EXPECT_EQ(count, 2u); + auto k1 = d.ReadKey(); + ASSERT_TRUE(k1.has_value()); + EXPECT_EQ(k1.value(), "foo"); + auto v1 = d.ReadString(); + ASSERT_TRUE(v1.has_value()); + EXPECT_EQ(v1.value(), "bar"); + auto k2 = d.ReadKey(); + ASSERT_TRUE(k2.has_value()); + EXPECT_EQ(k2.value(), "baz"); + auto v2 = d.ReadString(); + ASSERT_TRUE(v2.has_value()); + EXPECT_EQ(v2.value(), "qux"); + d.EndMap(); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, NestedStructure) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("meta"); + s.BeginNestedStructure(root); + s.WriteMapKey("key"); + s.WriteString(root, "val"); + s.EndNestedStructure(); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "meta"); + d.BeginStruct(); + auto innerKey = d.ReadKey(); + ASSERT_TRUE(innerKey.has_value()); + EXPECT_EQ(innerKey.value(), "key"); + auto val = d.ReadString(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), "val"); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +// --- Skip unknown fields --- + +TEST_F(CborShapeDeserializerTest, SkipUnknownField) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("known"); + s.WriteInteger(root, 1); + s.WriteMapKey("unknown"); + s.WriteString(root, "skip me"); + s.WriteMapKey("also_known"); + s.WriteInteger(root, 2); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto k1 = d.ReadKey(); + ASSERT_TRUE(k1.has_value()); + EXPECT_EQ(k1.value(), "known"); + auto v1 = d.ReadInteger(); + ASSERT_TRUE(v1.has_value()); + EXPECT_EQ(v1.value(), 1); + auto k2 = d.ReadKey(); + ASSERT_TRUE(k2.has_value()); + EXPECT_EQ(k2.value(), "unknown"); + d.SkipValue(); + auto k3 = d.ReadKey(); + ASSERT_TRUE(k3.has_value()); + EXPECT_EQ(k3.value(), "also_known"); + auto v3 = d.ReadInteger(); + ASSERT_TRUE(v3.has_value()); + EXPECT_EQ(v3.value(), 2); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, SkipNestedUnknown) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("unknown_struct"); + s.BeginNestedStructure(root); + s.WriteMapKey("a"); + s.WriteInteger(root, 1); + s.WriteMapKey("b"); + s.WriteString(root, "nested"); + s.EndNestedStructure(); + s.WriteMapKey("known"); + s.WriteInteger(root, 42); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto k1 = d.ReadKey(); + ASSERT_TRUE(k1.has_value()); + EXPECT_EQ(k1.value(), "unknown_struct"); + d.SkipValue(); + auto k2 = d.ReadKey(); + ASSERT_TRUE(k2.has_value()); + EXPECT_EQ(k2.value(), "known"); + auto val = d.ReadInteger(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), 42); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +// --- Multiple fields --- + +TEST_F(CborShapeDeserializerTest, MultipleScalars) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("a"); + s.WriteBoolean(root, true); + s.WriteMapKey("b"); + s.WriteInteger(root, 7); + s.WriteMapKey("c"); + s.WriteString(root, "x"); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto k1 = d.ReadKey(); + ASSERT_TRUE(k1.has_value()); + EXPECT_EQ(k1.value(), "a"); + auto v1 = d.ReadBoolean(); + ASSERT_TRUE(v1.has_value()); + EXPECT_TRUE(v1.value()); + auto k2 = d.ReadKey(); + ASSERT_TRUE(k2.has_value()); + EXPECT_EQ(k2.value(), "b"); + auto v2 = d.ReadInteger(); + ASSERT_TRUE(v2.has_value()); + EXPECT_EQ(v2.value(), 7); + auto k3 = d.ReadKey(); + ASSERT_TRUE(k3.has_value()); + EXPECT_EQ(k3.value(), "c"); + auto v3 = d.ReadString(); + ASSERT_TRUE(v3.has_value()); + EXPECT_EQ(v3.value(), "x"); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +// --- Error handling (Optional returns empty on failure) --- + +TEST_F(CborShapeDeserializerTest, EmptyOptionalOnEmptyPayload) { + const unsigned char empty[1] = {0}; + CborShapeDeserializer d(empty, 0); + auto val = d.ReadBoolean(); + EXPECT_FALSE(val.has_value()); +} + +TEST_F(CborShapeDeserializerTest, EmptyOptionalOnTypeMismatch) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("val"); + s.WriteString(root, "hello"); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "val"); + auto val = d.ReadInteger(); + EXPECT_FALSE(val.has_value()); +} + +TEST_F(CborShapeDeserializerTest, ValidOptionalOnSuccess) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("n"); + s.WriteInteger(root, 42); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + d.BeginStruct(); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "n"); + auto val = d.ReadInteger(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), 42); +} + +// --- BeginStruct returns size for definite-length maps --- + +TEST_F(CborShapeDeserializerTest, BeginStructDefiniteLengthMap) { + // Definite-length map with 2 entries: + // A2 -- map(2) + // 61 61 -- text(1) "a" + // 01 -- unsigned(1) + // 61 62 -- text(1) "b" + // 02 -- unsigned(2) + const unsigned char data[] = {0xA2, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02}; + CborShapeDeserializer d(data, sizeof(data)); + size_t count = d.BeginStruct(); + EXPECT_EQ(count, 2u); + auto k1 = d.ReadKey(); + ASSERT_TRUE(k1.has_value()); + EXPECT_EQ(k1.value(), "a"); + auto v1 = d.ReadInteger(); + ASSERT_TRUE(v1.has_value()); + EXPECT_EQ(v1.value(), 1); + auto k2 = d.ReadKey(); + ASSERT_TRUE(k2.has_value()); + EXPECT_EQ(k2.value(), "b"); + auto v2 = d.ReadInteger(); + ASSERT_TRUE(v2.has_value()); + EXPECT_EQ(v2.value(), 2); + d.EndStruct(); +} + +TEST_F(CborShapeDeserializerTest, BeginStructIndefiniteLengthMap) { + CborShapeSerializer s; + Schema root; + s.BeginStructure(root); + s.WriteMapKey("x"); + s.WriteInteger(root, 99); + s.EndStructure(); + auto payload = s.GetPayload().GetResult(); + + CborShapeDeserializer d(reinterpret_cast(payload.data()), payload.size()); + size_t count = d.BeginStruct(); + EXPECT_EQ(count, 0u); + EXPECT_FALSE(d.IsBreak()); + auto key = d.ReadKey(); + ASSERT_TRUE(key.has_value()); + EXPECT_EQ(key.value(), "x"); + auto val = d.ReadInteger(); + ASSERT_TRUE(val.has_value()); + EXPECT_EQ(val.value(), 99); + EXPECT_TRUE(d.IsBreak()); + d.EndStruct(); +} + +// --- Timestamp handling --- + +TEST_F(CborShapeDeserializerTest, TimestampFromFloat) { + // tag(1) followed by float64 1234567890.5 + // C1 = tag(1), FB = float64, 41D26580B4A00000 = IEEE 754 1234567890.5 + const unsigned char data[] = {0xC1, 0xFB, 0x41, 0xD2, 0x65, 0x80, 0xB4, 0xA0, 0x00, 0x00}; + CborShapeDeserializer d(data, sizeof(data)); + auto ts = d.ReadTimestamp(); + ASSERT_TRUE(ts.has_value()); + EXPECT_DOUBLE_EQ(ts.value().SecondsWithMSPrecision(), 1234567890.5); +} + +TEST_F(CborShapeDeserializerTest, TimestampNegativeIntReturnsEmpty) { + // tag(1) followed by negative int: C1 = tag(1), 20 = negint(0) meaning -1 + const unsigned char data[] = {0xC1, 0x20}; + CborShapeDeserializer d(data, sizeof(data)); + auto ts = d.ReadTimestamp(); + EXPECT_FALSE(ts.has_value()); +} + +TEST_F(CborShapeDeserializerTest, TimestampNegativeFloatReturnsEmpty) { + // tag(1) followed by float64 -1.0 + // C1 = tag(1), FB = float64, BFF0000000000000 = IEEE 754 -1.0 + const unsigned char data[] = {0xC1, 0xFB, 0xBF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + CborShapeDeserializer d(data, sizeof(data)); + auto ts = d.ReadTimestamp(); + EXPECT_FALSE(ts.has_value()); +} + +TEST_F(CborShapeDeserializerTest, TimestampInvalidTagReturnsEmpty) { + // tag(2) followed by uint 100 — tag 2 is not epoch timestamp + // C2 = tag(2), 18 64 = uint(100) + const unsigned char data[] = {0xC2, 0x18, 0x64}; + CborShapeDeserializer d(data, sizeof(data)); + auto ts = d.ReadTimestamp(); + EXPECT_FALSE(ts.has_value()); +} + +TEST_F(CborShapeDeserializerTest, ReadKeyOnNonStringReturnsEmpty) { + // Raw uint 42 — not a text string + // 18 2A = uint(42) + const unsigned char data[] = {0x18, 0x2A}; + CborShapeDeserializer d(data, sizeof(data)); + auto key = d.ReadKey(); + EXPECT_FALSE(key.has_value()); +}