diff options
author | Xin Li <delphij@google.com> | 2023-08-14 15:38:30 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2023-08-14 15:38:30 -0700 |
commit | bddf63953e111d742b591c1c0c7c34bcda8a51c7 (patch) | |
tree | 3a93128bff4b737b24b0c9581922c0b20410f0f4 /pw_protobuf/codegen_message_test.cc | |
parent | ee890da55c82b95deca3518d5f3777e3d8ca9f0e (diff) | |
parent | fbb9890f8922aa55fde183655a0017e69127ea4b (diff) | |
download | pigweed-bddf63953e111d742b591c1c0c7c34bcda8a51c7.tar.gz |
Merge Android U (ab/10368041)tmp_amf_298295554
Bug: 291102124
Merged-In: I10c41adb8fe3e126cfa4ff2f49b15863fff379de
Change-Id: I66f7a6cccaafc173d3924dae62a736c6c53520c7
Diffstat (limited to 'pw_protobuf/codegen_message_test.cc')
-rw-r--r-- | pw_protobuf/codegen_message_test.cc | 2027 |
1 files changed, 2027 insertions, 0 deletions
diff --git a/pw_protobuf/codegen_message_test.cc b/pw_protobuf/codegen_message_test.cc new file mode 100644 index 000000000..f049715ea --- /dev/null +++ b/pw_protobuf/codegen_message_test.cc @@ -0,0 +1,2027 @@ +// Copyright 2023 The Pigweed Authors +// +// 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 <array> +#include <string_view> +#include <tuple> + +#include "gtest/gtest.h" +#include "pw_preprocessor/compiler.h" +#include "pw_protobuf/internal/codegen.h" +#include "pw_span/span.h" +#include "pw_status/status.h" +#include "pw_status/status_with_size.h" +#include "pw_stream/memory_stream.h" + +// These header files contain the code generated by the pw_protobuf plugin. +// They are re-generated every time the tests are built and are used by the +// tests to ensure that the interface remains consistent. +// +// The purpose of the tests in this file is primarily to verify that the +// generated C++ interface is valid rather than the correctness of the +// low-level encoder. +#include "pw_protobuf_test_protos/full_test.pwpb.h" +#include "pw_protobuf_test_protos/importer.pwpb.h" +#include "pw_protobuf_test_protos/optional.pwpb.h" +#include "pw_protobuf_test_protos/repeated.pwpb.h" + +namespace pw::protobuf { +namespace { + +using namespace ::pw::protobuf::test::pwpb; + +PW_MODIFY_DIAGNOSTICS_PUSH(); +PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers"); + +TEST(CodegenMessage, Equality) { + const Pigweed::Message one{ + .magic_number = 0x49u, + .ziggy = -111, + .cycles = 0x40302010fecaaddeu, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::FILE_NOT_FOUND}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x10}, + std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}}, + .bungle = -111, + }; + + const Pigweed::Message two{ + .magic_number = 0x49u, + .ziggy = -111, + .cycles = 0x40302010fecaaddeu, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::FILE_NOT_FOUND}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x10}, + std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}}, + .bungle = -111, + }; + + EXPECT_TRUE(one == two); +} + +TEST(CodegenMessage, CopyEquality) { + Pigweed::Message one{ + .magic_number = 0x49u, + .ziggy = -111, + .cycles = 0x40302010fecaaddeu, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::FILE_NOT_FOUND}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x10}, + std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}}, + .bungle = -111, + }; + Pigweed::Message two = one; + + EXPECT_TRUE(one == two); +} + +TEST(CodegenMessage, EmptyEquality) { + const Pigweed::Message one{}; + const Pigweed::Message two{}; + + EXPECT_TRUE(one == two); +} + +TEST(CodegenMessage, Inequality) { + const Pigweed::Message one{ + .magic_number = 0x49u, + .ziggy = -111, + .cycles = 0x40302010fecaaddeu, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::FILE_NOT_FOUND}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x10}, + std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}}, + .bungle = -111, + }; + + const Pigweed::Message two{ + .magic_number = 0x43u, + .ziggy = 128, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::TRUE}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}, + std::byte{0x90}}, + }; + + EXPECT_FALSE(one == two); +} + +TEST(CodegenMessage, TriviallyComparable) { + static_assert(IsTriviallyComparable<IntegerMetadata::Message>()); + static_assert(IsTriviallyComparable<KeyValuePair::Message>()); + static_assert(!IsTriviallyComparable<Pigweed::Message>()); +} + +TEST(CodegenMessage, ConstCopyable) { + const Pigweed::Message one{ + .magic_number = 0x49u, + .ziggy = -111, + .cycles = 0x40302010fecaaddeu, + .ratio = -1.42f, + .error_message = "not a typewriter", + .pigweed = {.status = Bool::FILE_NOT_FOUND}, + .bin = Pigweed::Protobuf::Binary::ZERO, + .proto = {.bin = Proto::Binary::OFF, + .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, + .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, + .meta = + { + .file_name = "/etc/passwd", + .status = Pigweed::Protobuf::Compiler::Status::FUBAR, + .protobuf_bin = Pigweed::Protobuf::Binary::ONE, + .pigweed_bin = Pigweed::Pigweed::Binary::ONE, + }}, + .data = {std::byte{0x10}, + std::byte{0x20}, + std::byte{0x30}, + std::byte{0x40}, + std::byte{0x50}, + std::byte{0x60}, + std::byte{0x70}, + std::byte{0x80}}, + .bungle = -111, + }; + Pigweed::Message two = one; + + EXPECT_TRUE(one == two); +} + +TEST(CodegenMessage, FixReservedIdentifiers) { + // This test checks that the code was generated as expected, so it will simply + // fail to compile if its expectations are not met. + + // Make sure that the `signed` field was renamed to `signed_`. + std::ignore = IntegerMetadata::Message{ + .bits = 32, + .signed_ = true, + .null = false, + }; + + // Make sure that the internal enum describing the struct's fields was + // generated as expected: + // - `BITS` doesn't need an underscore. + // - `SIGNED_` has an underscore to match the corresponding `signed_` field. + // - `NULL_` has an underscore to avoid a collision with `NULL` (even though + // the field `null` doesn't have or need an underscore). + std::ignore = IntegerMetadata::Fields::kBits; + std::ignore = IntegerMetadata::Fields::kSigned; + std::ignore = IntegerMetadata::Fields::kNull; + + // Make sure that the `ReservedWord` enum values were renamed as expected. + // Specifically, only enum-value names that are reserved in UPPER_SNAKE_CASE + // should be modified. Names that are only reserved in lower_snake_case should + // be left alone since they'll never appear in that form in the generated + // code. + std::ignore = ReservedWord::NULL_; // Add underscore since NULL is a macro. + std::ignore = ReservedWord::kNull; // No underscore necessary. + std::ignore = ReservedWord::INT; // No underscore necessary. + std::ignore = ReservedWord::kInt; // No underscore necessary. + std::ignore = ReservedWord::RETURN; // No underscore necessary. + std::ignore = ReservedWord::kReturn; // No underscore necessary. + std::ignore = ReservedWord::BREAK; // No underscore necessary. + std::ignore = ReservedWord::kBreak; // No underscore necessary. + std::ignore = ReservedWord::FOR; // No underscore necessary. + std::ignore = ReservedWord::kFor; // No underscore necessary. + std::ignore = ReservedWord::DO; // No underscore necessary. + std::ignore = ReservedWord::kDo; // No underscore necessary. + + // Instantiate an extremely degenerately named set of nested types in order to + // make sure that name conflicts with the codegen internals are properly + // prevented. + std::ignore = Function::Message{ + .description = + Function::Message_::Message{ + .content = "multiplication (mod 5)", + }, + .domain_field = Function::Fields_::INTEGERS_MOD_5, + .codomain_field = Function::Fields_::INTEGERS_MOD_5, + }; + + // Check for expected values of `enum class Function::Fields`: + std::ignore = Function::Fields::kDescription; + std::ignore = Function::Fields::kDomainField; + std::ignore = Function::Fields::kCodomainField; + + // Check for expected values of `enum class Function::Message_::Fields`: + std::ignore = Function::Message_::Fields::kContent; + + // Check for expected values of `enum class Function::Fields_`: + std::ignore = Function::Fields_::NONE; + std::ignore = Function::Fields_::kNone; + std::ignore = Function::Fields_::COMPLEX_NUMBERS; + std::ignore = Function::Fields_::kComplexNumbers; + std::ignore = Function::Fields_::INTEGERS_MOD_5; + std::ignore = Function::Fields_::kIntegersMod5; + std::ignore = Function::Fields_::MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE; + std::ignore = Function::Fields_::kMeromorphicFunctionsOnComplexPlane; + std::ignore = Function::Fields_::OTHER; + std::ignore = Function::Fields_::kOther; +} + +PW_MODIFY_DIAGNOSTICS_POP(); + +TEST(CodegenMessage, Read) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.magic_number + 0x08, 0x49, + // pigweed.ziggy + 0x10, 0xdd, 0x01, + // pigweed.cycles + 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40, + // pigweed.ratio + 0x25, 0x8f, 0xc2, 0xb5, 0xbf, + // pigweed.error_message + 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', + 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', + // pigweed.bin + 0x40, 0x01, + // pigweed.bungle + 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); + + constexpr std::string_view kExpectedErrorMessage{"not a typewriter"}; + + EXPECT_EQ(message.magic_number, 0x49u); + EXPECT_EQ(message.ziggy, -111); + EXPECT_EQ(message.cycles, 0x40302010fecaaddeu); + EXPECT_EQ(message.ratio, -1.42f); + EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size()); + EXPECT_EQ(std::memcmp(message.error_message.data(), + kExpectedErrorMessage.data(), + kExpectedErrorMessage.size()), + 0); + EXPECT_EQ(message.bin, Pigweed::Protobuf::Binary::ZERO); + EXPECT_EQ(message.bungle, -111); +} + +TEST(CodegenMessage, ReadNonPackedScalar) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint32s[], v={0, 16, 32, 48} + 0x08, 0x00, + 0x08, 0x10, + 0x08, 0x20, + 0x08, 0x30, + // fixed32s[]. v={0, 16, 32, 48} + 0x35, 0x00, 0x00, 0x00, 0x00, + 0x35, 0x10, 0x00, 0x00, 0x00, + 0x35, 0x20, 0x00, 0x00, 0x00, + 0x35, 0x30, 0x00, 0x00, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + ASSERT_EQ(message.uint32s.size(), 4u); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(message.uint32s[i], i * 16u); + } + + ASSERT_EQ(message.fixed32s.size(), 4u); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(message.fixed32s[i], i * 16u); + } +} + +TEST(CodegenMessage, ReadPackedScalar) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint32s[], v={0, 16, 32, 48} + 0x0a, 0x04, + 0x00, + 0x10, + 0x20, + 0x30, + // fixed32s[]. v={0, 16, 32, 48} + 0x32, 0x10, + 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + ASSERT_EQ(message.uint32s.size(), 4u); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(message.uint32s[i], i * 16u); + } + + ASSERT_EQ(message.fixed32s.size(), 4u); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(message.fixed32s[i], i * 16u); + } +} + +TEST(CodegenMessage, ReadPackedScalarRepeated) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint32s[], v={0, 16, 32, 48} + 0x0a, 0x04, + 0x00, + 0x10, + 0x20, + 0x30, + // uint32s[], v={64, 80, 96, 112} + 0x0a, 0x04, + 0x40, + 0x50, + 0x60, + 0x70, + // fixed32s[]. v={0, 16, 32, 48} + 0x32, 0x10, + 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, + // fixed32s[]. v={64, 80, 96, 112} + 0x32, 0x10, + 0x40, 0x00, 0x00, 0x00, + 0x50, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, + 0x70, 0x00, 0x00, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + ASSERT_EQ(message.uint32s.size(), 8u); + for (int i = 0; i < 8; ++i) { + EXPECT_EQ(message.uint32s[i], i * 16u); + } + + ASSERT_EQ(message.fixed32s.size(), 8u); + for (int i = 0; i < 8; ++i) { + EXPECT_EQ(message.fixed32s[i], i * 16u); + } +} + +TEST(CodegenMessage, ReadPackedScalarExhausted) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint32s[], v={0, 16, 32, 48, 64, 80, 96, 112, 128} + 0x0a, 0x09, + 0x00, + 0x10, + 0x20, + 0x30, + 0x40, + 0x50, + 0x60, + 0x70, + 0x80, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + // uint32s has max_size=8, so this will exhaust the vector. + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, Status::ResourceExhausted()); +} + +TEST(CodegenMessage, ReadPackedScalarCallback) { + // clang-format off + constexpr uint8_t proto_data[] = { + // sint32s[], v={-25, -1, 0, 1, 25} + 0x12, 0x05, + 0x31, + 0x01, + 0x00, + 0x02, + 0x32, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + // sint32s is a repeated field declared without max_count, so requirses a + // callback to be decoded. + RepeatedTest::Message message{}; + message.sint32s.SetDecoder([](RepeatedTest::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kSint32s); + + pw::Vector<int32_t, 8> sint32s{}; + const auto status = decoder.ReadSint32s(sint32s); + EXPECT_EQ(status, OkStatus()); + + EXPECT_EQ(sint32s.size(), 5u); + EXPECT_EQ(sint32s[0], -25); + EXPECT_EQ(sint32s[1], -1); + EXPECT_EQ(sint32s[2], 0); + EXPECT_EQ(sint32s[3], 1); + EXPECT_EQ(sint32s[4], 25); + + return status; + }); + + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadPackedScalarFixedLength) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint64s[], v={1000, 2000, 3000, 4000} + 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, + // doubles[], v={3.14159, 2.71828} + 0x22, 0x10, + 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, + 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.uint64s[0], 1000u); + EXPECT_EQ(message.uint64s[1], 2000u); + EXPECT_EQ(message.uint64s[2], 3000u); + EXPECT_EQ(message.uint64s[3], 4000u); + + EXPECT_EQ(message.doubles[0], 3.14159); + EXPECT_EQ(message.doubles[1], 2.71828); +} + +TEST(CodegenMessage, ReadPackedScalarFixedLengthShort) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint64s[], v={1000, 2000} + 0x42, 0x04, 0xe8, 0x07, 0xd0, 0x0f, + // doubles[], v={3.14159} + 0x22, 0x08, + 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.uint64s[0], 1000u); + EXPECT_EQ(message.uint64s[1], 2000u); + EXPECT_EQ(message.uint64s[2], 0u); + EXPECT_EQ(message.uint64s[3], 0u); + + EXPECT_EQ(message.doubles[0], 3.14159); + EXPECT_EQ(message.doubles[1], 0); +} + +TEST(CodegenMessage, ReadPackedScalarVarintFixedLengthExhausted) { + // clang-format off + constexpr uint8_t proto_data[] = { + // uint64s[], v={0, 1000, 2000, 3000, 4000} + 0x42, 0x09, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, Status::ResourceExhausted()); +} + +TEST(CodegenMessage, ReadPackedScalarFixedLengthExhausted) { + // clang-format off + constexpr uint8_t proto_data[] = { + // doubles[], v={3.14159, 2.71828, 1.41429, 1.73205} + 0x22, 0x20, + 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, + 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, + 0x1b, 0xf5, 0x10, 0x8d, 0xee, 0xa0, 0xf6, 0x3f, + 0xbc, 0x96, 0x90, 0x0f, 0x7a, 0xb6, 0xfb, 0x3f, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, Status::ResourceExhausted()); +} + +TEST(CodegenMessage, ReadPackedEnum) { + // clang-format off + constexpr uint8_t proto_data[] = { + // enums[], v={RED, GREEN, AMBER, RED} + 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + ASSERT_EQ(message.enums.size(), 4u); + for (int i = 0; i < 4; ++i) { + EXPECT_TRUE(IsValidEnum(message.enums[i])); + } + + EXPECT_EQ(message.enums[0], Enum::RED); + EXPECT_EQ(message.enums[1], Enum::GREEN); + EXPECT_EQ(message.enums[2], Enum::AMBER); + EXPECT_EQ(message.enums[3], Enum::RED); +} + +TEST(CodegenMessage, ReadStringExhausted) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.error_message + 0x2a, 0xd3, 0x01, 'T', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'i', + 's', ' ', 't', 'h', 'e', ' ', 't', 'a', 'r', 'g', 'e', 't', ' ', 'o', 'f', + ' ', 'a', ' ', 'g', 'o', 't', 'o', ' ', 'f', 'r', 'o', 'm', ' ', 'o', 'u', + 't', 's', 'i', 'd', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'b', 'l', + 'o', 'c', 'k', ' ', 'c', 'o', 'n', 't', 'a', 'i', 'n', 'i', 'n', 'g', ' ', + 't', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'A', 'N', 'D', ' ', + 't', 'h', 'i', 's', ' ', 'b', 'l', 'o', 'c', 'k', ' ', 'h', 'a', 's', ' ', + 'a', 'n', ' ', 'a', 'u', 't', 'o', 'm', 'a', 't', 'i', 'c', ' ', 'v', 'a', + 'r', 'i', 'a', 'b', 'l', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'a', 'n', ' ', + 'i', 'n', 'i', 't', 'i', 'a', 'l', 'i', 'z', 'e', 'r', ' ', 'A', 'N', 'D', + ' ', 'y', 'o', 'u', 'r', ' ', 'w', 'i', 'n', 'd', 'o', 'w', ' ', 'w', 'a', + 's', 'n', '\'', 't', ' ', 'w', 'i', 'd', 'e', ' ', 'e', 'n', 'o', 'u', 'g', + 'h', ' ', 't', 'o', ' ', 'r', 'e', 'a', 'd', ' ', 't', 'h', 'i', 's', ' ', + 'w', 'h', 'o', 'l', 'e', ' ', 'e', 'r', 'r', 'o', 'r', ' ', 'm', 'e', 's', + 's', 'a', 'g', 'e' + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, Status::ResourceExhausted()); +} + +TEST(CodegenMessage, ReadStringCallback) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.description + 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c', + 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f', + ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e', + 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o', + 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o', + ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd', + 'u', 'l', 'e', 's' + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + // pigweed.description has no max_size specified so a callback must be + // set to read the value if present. + Pigweed::Message message{}; + message.description.SetDecoder([](Pigweed::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDescription); + + constexpr std::string_view kExpectedDescription{ + "an open source collection of embedded-targeted libraries-or as we " + "like to call them, modules"}; + + std::array<char, 128> description{}; + const auto sws = decoder.ReadDescription(description); + EXPECT_EQ(sws.status(), OkStatus()); + EXPECT_EQ(sws.size(), kExpectedDescription.size()); + EXPECT_EQ(std::memcmp(description.data(), + kExpectedDescription.data(), + kExpectedDescription.size()), + 0); + + return sws.status(); + }); + + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadMultipleString) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.error_message + 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', + 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', + // pigweed.error_message + 0x02a, 0x07, 'o', 'n', ' ', 'f', 'i', 'r', 'e' + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); + + constexpr std::string_view kExpectedErrorMessage{"on fire"}; + + EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size()); + EXPECT_EQ(std::memcmp(message.error_message.data(), + kExpectedErrorMessage.data(), + kExpectedErrorMessage.size()), + 0); +} + +TEST(CodegenMessage, ReadRepeatedStrings) { + // clang-format off + constexpr uint8_t proto_data[] = { + // repeated.strings + 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', + 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', + 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', + // repeated.strings + 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', + 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', + ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', + // repeated.strings + 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', + 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', + ' ', 's', 'o', ' ', 'd', 'i', 'e', + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + // Repeated strings require a callback to avoid forcing multi-dimensional + // arrays upon the caller. + RepeatedTest::Message message{}; + int i = 0; + message.strings.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings); + + constexpr std::string_view kExpectedStrings[] = { + {"if music be the food of love, play on"}, + {"give me excess of it, that, surfeiting"}, + {"the appetite may sicken, and so die"}}; + + std::array<char, 40> strings{}; + const StatusWithSize sws = decoder.ReadStrings(strings); + EXPECT_EQ(sws.status(), OkStatus()); + EXPECT_EQ(sws.size(), kExpectedStrings[i].size()); + EXPECT_EQ(std::memcmp(strings.data(), + kExpectedStrings[i].data(), + kExpectedStrings[i].size()), + 0); + + ++i; + return sws.status(); + }); + + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadForcedCallback) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.special_property + 0x68, 0x2a, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + // pigweed.special_property has use_callback=true to force the use of a + // callback even though it's a simple scalar. + Pigweed::Message message{}; + message.special_property.SetDecoder([](Pigweed::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kSpecialProperty); + + pw::Result<uint32_t> result = decoder.ReadSpecialProperty(); + EXPECT_EQ(result.status(), OkStatus()); + EXPECT_EQ(result.value(), 42u); + + return result.status(); + }); + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadMissingCallback) { + // clang-format off + constexpr uint8_t proto_data[] = { + // repeated.strings + 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', + 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', + 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', + // repeated.strings + 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', + 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', + ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', + // repeated.strings + 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', + 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', + ' ', 's', 'o', ' ', 'd', 'i', 'e', + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + // Failing to set a callback will give a DataLoss error if that field is + // present in the decoded data. + RepeatedTest::Message message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, Status::DataLoss()); +} + +TEST(CodegenMessage, ReadFixedLength) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.data + 0x5a, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.data[0], std::byte{0x01}); + EXPECT_EQ(message.data[1], std::byte{0x02}); + EXPECT_EQ(message.data[2], std::byte{0x03}); + EXPECT_EQ(message.data[3], std::byte{0x04}); + EXPECT_EQ(message.data[4], std::byte{0x05}); + EXPECT_EQ(message.data[5], std::byte{0x06}); + EXPECT_EQ(message.data[6], std::byte{0x07}); + EXPECT_EQ(message.data[7], std::byte{0x08}); +} + +TEST(CodegenMessage, ReadFixedLengthShort) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.data + 0x5a, 0x04, 0x01, 0x02, 0x03, 0x04 + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.data[0], std::byte{0x01}); + EXPECT_EQ(message.data[1], std::byte{0x02}); + EXPECT_EQ(message.data[2], std::byte{0x03}); + EXPECT_EQ(message.data[3], std::byte{0x04}); + // Remaining bytes are whatever you initialized them to. + EXPECT_EQ(message.data[4], std::byte{0x00}); + EXPECT_EQ(message.data[5], std::byte{0x00}); + EXPECT_EQ(message.data[6], std::byte{0x00}); + EXPECT_EQ(message.data[7], std::byte{0x00}); +} + +TEST(CodegenMessage, ReadFixedLengthExhausted) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.data + 0x5a, 0x0c, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, Status::ResourceExhausted()); +} + +TEST(CodegenMessage, ReadNested) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.magic_number + 0x08, 0x49, + // pigweed.pigweed + 0x3a, 0x02, + // pigweed.pigweed.status + 0x08, 0x02, + // pigweed.ziggy + 0x10, 0xdd, 0x01, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + Pigweed::Message message{}; + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.magic_number, 0x49u); + EXPECT_EQ(message.pigweed.status, Bool::FILE_NOT_FOUND); + EXPECT_EQ(message.ziggy, -111); +} + +TEST(CodegenMessage, ReadNestedImported) { + // clang-format off + constexpr uint8_t proto_data[] = { + // period.start + 0x0a, 0x08, + // period.start.seconds v=1517949900 + 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05, + // period.start.nanoseconds v=0 + 0x10, 0x00, + // period.end + 0x12, 0x08, + // period.end.seconds, v=1517950378 + 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05, + // period.end.nanoseconds, v=0 + 0x10, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Period::StreamDecoder period(reader); + + // Messages imported from another file can be directly embedded in a message. + Period::Message message{}; + const auto status = period.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.start.seconds, 1517949900u); + EXPECT_EQ(message.start.nanoseconds, 0u); + EXPECT_EQ(message.end.seconds, 1517950378u); + EXPECT_EQ(message.end.nanoseconds, 0u); +} + +TEST(CodegenMessage, ReadNestedRepeated) { + // clang-format off + constexpr uint8_t proto_data[] = { + // repeated.structs + 0x2a, 0x04, + // repeated.structs.one v=16 + 0x08, 0x10, + // repeated.structs.two v=32 + 0x10, 0x20, + // repeated.structs + 0x2a, 0x04, + // repeated.structs.one v=48 + 0x08, 0x30, + // repeated.structs.two v=64 + 0x10, 0x40, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + // Repeated nested messages require a callback since there would otherwise be + // no way to set callbacks on the nested message. + RepeatedTest::Message message{}; + int i = 0; + message.structs.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStructs); + + Struct::Message structs_message{}; + auto structs_decoder = decoder.GetStructsDecoder(); + const auto status = structs_decoder.Read(structs_message); + EXPECT_EQ(status, OkStatus()); + + EXPECT_LT(i, 2); + EXPECT_EQ(structs_message.one, i * 32 + 16u); + EXPECT_EQ(structs_message.two, i * 32 + 32u); + ++i; + + return status; + }); + + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadNestedForcedCallback) { + // clang-format off + constexpr uint8_t proto_data[] = { + // pigweed.device_info + 0x32, 0x0e, + // pigweed.device_info.device_name + 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l', + // pigweed.device_info.device_id + 0x15, 0x08, 0x08, 0x08, 0x08, + // pigweed.device_info.status + 0x18, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + Pigweed::StreamDecoder pigweed(reader); + + // pigweed.device_info has use_callback=true to force the use of a callback. + Pigweed::Message message{}; + message.device_info.SetDecoder([](Pigweed::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDeviceInfo); + + DeviceInfo::Message device_info{}; + DeviceInfo::StreamDecoder device_info_decoder = + decoder.GetDeviceInfoDecoder(); + const auto status = device_info_decoder.Read(device_info); + EXPECT_EQ(status, OkStatus()); + + constexpr std::string_view kExpectedDeviceName{"pixel"}; + + EXPECT_EQ(device_info.device_name.size(), kExpectedDeviceName.size()); + EXPECT_EQ(std::memcmp(device_info.device_name.data(), + kExpectedDeviceName.data(), + kExpectedDeviceName.size()), + 0); + EXPECT_EQ(device_info.device_id, 0x08080808u); + EXPECT_EQ(device_info.status, DeviceInfo::DeviceStatus::OK); + + return status; + }); + const auto status = pigweed.Read(message); + ASSERT_EQ(status, OkStatus()); +} + +TEST(CodegenMessage, ReadOptionalPresent) { + // clang-format off + constexpr uint8_t proto_data[] = { + // optional.sometimes_present_fixed + 0x0d, 0x2a, 0x00, 0x00, 0x00, + // optional.sometimes_present_varint + 0x10, 0x2a, + // optional.explicitly_present_fixed + 0x1d, 0x45, 0x00, 0x00, 0x00, + // optional.explicitly_present_varint + 0x20, 0x45, + // optional.sometimes_empty_fixed + 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00, + // optional.sometimes_empty_varint + 0x32, 0x01, 0x63, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + OptionalTest::StreamDecoder optional_test(reader); + + OptionalTest::Message message{}; + const auto status = optional_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + EXPECT_EQ(message.sometimes_present_fixed, 0x2a); + EXPECT_EQ(message.sometimes_present_varint, 0x2a); + EXPECT_TRUE(message.explicitly_present_fixed); + EXPECT_EQ(*message.explicitly_present_fixed, 0x45); + EXPECT_TRUE(message.explicitly_present_varint); + EXPECT_EQ(*message.explicitly_present_varint, 0x45); + EXPECT_FALSE(message.sometimes_empty_fixed.empty()); + EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u); + EXPECT_EQ(message.sometimes_empty_fixed[0], 0x63); + EXPECT_FALSE(message.sometimes_empty_varint.empty()); + EXPECT_EQ(message.sometimes_empty_varint.size(), 1u); + EXPECT_EQ(message.sometimes_empty_varint[0], 0x63); +} + +TEST(CodegenMessage, ReadOptionalNotPresent) { + constexpr std::array<std::byte, 0> proto_data{}; + + stream::MemoryReader reader(proto_data); + OptionalTest::StreamDecoder optional_test(reader); + + OptionalTest::Message message{}; + const auto status = optional_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + // Non-optional fields have their default value. + EXPECT_EQ(message.sometimes_present_fixed, 0); + EXPECT_EQ(message.sometimes_present_varint, 0); + EXPECT_TRUE(message.sometimes_empty_fixed.empty()); + EXPECT_TRUE(message.sometimes_empty_varint.empty()); + + // Optional fields are explicitly not present. + EXPECT_FALSE(message.explicitly_present_fixed); + EXPECT_FALSE(message.explicitly_present_varint); +} + +TEST(CodegenMessage, ReadOptionalPresentDefaults) { + // clang-format off + constexpr uint8_t proto_data[] = { + // optional.sometimes_present_fixed + 0x0d, 0x00, 0x00, 0x00, 0x00, + // optional.sometimes_present_varint + 0x10, 0x00, + // optional.explicitly_present_fixed + 0x1d, 0x00, 0x00, 0x00, 0x00, + // optional.explicitly_present_varint + 0x20, 0x00, + // optional.sometimes_empty_fixed + 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00, + // optional.sometimes_empty_varint + 0x32, 0x01, 0x00, + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + OptionalTest::StreamDecoder optional_test(reader); + + OptionalTest::Message message{}; + const auto status = optional_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + // Non-optional fields have their default value and aren't meaningfully + // different from missing. + EXPECT_EQ(message.sometimes_present_fixed, 0x00); + EXPECT_EQ(message.sometimes_present_varint, 0x00); + + // Optional fields are explicitly present with a default value. + EXPECT_TRUE(message.explicitly_present_fixed); + EXPECT_EQ(*message.explicitly_present_fixed, 0x00); + EXPECT_TRUE(message.explicitly_present_varint); + EXPECT_EQ(*message.explicitly_present_varint, 0x00); + + // Repeated fields with a default value are meaningfully non-empty. + EXPECT_FALSE(message.sometimes_empty_fixed.empty()); + EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u); + EXPECT_EQ(message.sometimes_empty_fixed[0], 0x00); + EXPECT_FALSE(message.sometimes_empty_varint.empty()); + EXPECT_EQ(message.sometimes_empty_varint.size(), 1u); + EXPECT_EQ(message.sometimes_empty_varint[0], 0x00); +} + +TEST(CodegenMessage, ReadImportedOptions) { + // clang-format off + constexpr uint8_t proto_data[] = { + // notice + 0x0a, 0x0f, + // notice.message + 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y' + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + TestMessage::StreamDecoder test_message(reader); + + // The options file for the imported proto is applied, making the string + // field a vector rather than requiring a callback. + TestMessage::Message message{}; + const auto status = test_message.Read(message); + ASSERT_EQ(status, OkStatus()); + + constexpr std::string_view kExpectedMessage{"Press any key"}; + + EXPECT_EQ(message.notice.message.size(), kExpectedMessage.size()); + EXPECT_EQ(std::memcmp(message.notice.message.data(), + kExpectedMessage.data(), + kExpectedMessage.size()), + 0); +} + +TEST(CodegenMessage, ReadImportedFromDepsOptions) { + // clang-format off + constexpr uint8_t proto_data[] = { + // debug + 0x12, 0x0f, + // debug.message + 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y' + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + TestMessage::StreamDecoder test_message(reader); + + // The options file for the imported proto is applied, making the string + // field a vector rather than requiring a callback. + TestMessage::Message message{}; + const auto status = test_message.Read(message); + ASSERT_EQ(status, OkStatus()); + + constexpr std::string_view kExpectedMessage{"Press any key"}; + + EXPECT_EQ(message.debug.message.size(), kExpectedMessage.size()); + EXPECT_EQ(std::memcmp(message.debug.message.data(), + kExpectedMessage.data(), + kExpectedMessage.size()), + 0); +} + +class BreakableDecoder : public KeyValuePair::StreamDecoder { + public: + constexpr BreakableDecoder(stream::Reader& reader) : StreamDecoder(reader) {} + + Status Read(KeyValuePair::Message& message, + span<const internal::MessageField> table) { + return ::pw::protobuf::StreamDecoder::Read( + as_writable_bytes(span(&message, 1)), table); + } +}; + +TEST(CodegenMessage, DISABLED_ReadDoesNotOverrun) { + // Deliberately construct a message table that attempts to violate the bounds + // of the structure. We're not testing that a developer can't do this, rather + // that the protobuf decoder can't be exploited in this way. + constexpr internal::MessageField kMessageFields[] = { + {1, + WireType::kDelimited, + sizeof(std::byte), + static_cast<internal::VarintType>(0), + false, + false, + false, + false, + false, + 0, + sizeof(KeyValuePair::Message) * 2, + {}}, + }; + + // clang-format off + constexpr uint8_t proto_data[] = { + // id=1, len=9, + 0x0a, 0x08, 'd', 'o', 'n', 't', 'e', 'a', 't', 'm', 'e', + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + BreakableDecoder decoder(reader); + + KeyValuePair::Message message{}; + // ASSERT_CRASH + std::ignore = decoder.Read(message, kMessageFields); +} + +TEST(CodegenMessage, Write) { + constexpr uint8_t pigweed_data[] = { + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80}; + + Pigweed::Message message{}; + message.magic_number = 0x49u; + message.ziggy = -111; + message.cycles = 0x40302010fecaaddeu; + message.ratio = -1.42f; + message.error_message = "not a typewriter"; + message.pigweed.status = Bool::FILE_NOT_FOUND; + message.bin = Pigweed::Protobuf::Binary::ZERO; + message.bungle = -111; + message.proto.bin = Proto::Binary::OFF; + message.proto.pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO; + message.proto.pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO; + message.proto.meta.file_name = "/etc/passwd"; + message.proto.meta.status = Pigweed::Protobuf::Compiler::Status::FUBAR; + message.proto.meta.protobuf_bin = Pigweed::Protobuf::Binary::ONE; + message.proto.meta.pigweed_bin = Pigweed::Pigweed::Binary::ONE; + std::memcpy(message.data.data(), pigweed_data, sizeof(pigweed_data)); + + std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; + std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Pigweed::StreamEncoder pigweed(writer, temp_buffer); + + const auto status = pigweed.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // pigweed.magic_number + 0x08, 0x49, + // pigweed.ziggy + 0x10, 0xdd, 0x01, + // pigweed.cycles + 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40, + // pigweed.ratio + 0x25, 0x8f, 0xc2, 0xb5, 0xbf, + // pigweed.error_message + 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', + 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', + // pigweed.pigweed + 0x3a, 0x02, + // pigweed.pigweed.status + 0x08, 0x02, + // pigweed.bin + 0x40, 0x01, + // pigweed.proto + 0x4a, 0x15, + // pigweed.proto.pigweed_protobuf_bin + 0x20, 0x01, + // pigweed.proto.meta + 0x2a, 0x11, + // pigweed.proto.meta.file_name + 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd', + // pigweed.proto.meta.status + 0x10, 0x02, + // pigweed.proto.meta.pigweed_bin + 0x20, 0x01, + // pigweed.bytes + 0x5a, 0x08, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + // pigweed.bungle + 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteDefaults) { + Pigweed::Message message{}; + + std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; + std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Pigweed::StreamEncoder pigweed(writer, temp_buffer); + + const auto status = pigweed.Write(message); + ASSERT_EQ(status, OkStatus()); + + // Since all fields are at their default, the output should be zero sized. + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), 0u); +} + +TEST(CodegenMessage, WritePackedScalar) { + RepeatedTest::Message message{}; + for (int i = 0; i < 4; ++i) { + message.uint32s.push_back(i * 16u); + message.fixed32s.push_back(i * 16u); + } + + std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); + + const auto status = repeated_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // uint32s[], v={0, 16, 32, 48} + 0x0a, 0x04, + 0x00, + 0x10, + 0x20, + 0x30, + // fixed32s[]. v={0, 16, 32, 48} + 0x32, 0x10, + 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x00, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WritePackedScalarFixedLength) { + RepeatedTest::Message message{}; + for (int i = 0; i < 4; ++i) { + message.uint64s[i] = (i + 1) * 1000u; + } + message.doubles[0] = 3.14159; + message.doubles[1] = 2.71828; + + std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); + + const auto status = repeated_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // doubles[], v={3.14159, 2.71828} + 0x22, 0x10, + 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, + 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, + // uint64s[], v={1000, 2000, 3000, 4000} + 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WritePackedScalarCallback) { + RepeatedTest::Message message{}; + message.sint32s.SetEncoder([](RepeatedTest::StreamEncoder& encoder) { + constexpr int32_t sint32s[] = {-25, -1, 0, 1, 25}; + return encoder.WriteSint32s(sint32s); + }); + + std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes + + varint::kMaxVarint32SizeBytes * 5]; + + stream::MemoryWriter writer(encode_buffer); + RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); + + const auto status = repeated_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // sint32s[], v={-25, -1, 0, 1, 25} + 0x12, 0x05, + 0x31, + 0x01, + 0x00, + 0x02, + 0x32, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WritePackedEnum) { + RepeatedTest::Message message{}; + message.enums.push_back(Enum::RED); + message.enums.push_back(Enum::GREEN); + message.enums.push_back(Enum::AMBER); + message.enums.push_back(Enum::RED); + + std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); + + const auto status = repeated_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // enums[], v={RED, GREEN, AMBER, RED} + 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteStringCallback) { + Pigweed::Message message{}; + // pigweed.description has no max_size specified so a callback must be + // set to write the value. + message.description.SetEncoder([](Pigweed::StreamEncoder& encoder) { + return encoder.WriteDescription( + "an open source collection of embedded-targeted " + "libraries-or as we like to call them, modules"); + }); + + std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes + 92]; + std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Pigweed::StreamEncoder pigweed(writer, temp_buffer); + + const auto status = pigweed.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // pigweed.description + 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c', + 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f', + ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e', + 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o', + 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o', + ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd', + 'u', 'l', 'e', 's', + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteForcedCallback) { + Pigweed::Message message{}; + // pigweed.special_property has use_callback=true to force the use of a + // callback even though it's a simple scalar. + message.special_property.SetEncoder([](Pigweed::StreamEncoder& encoder) { + return encoder.WriteSpecialProperty(42u); + }); + + std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; + std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Pigweed::StreamEncoder pigweed(writer, temp_buffer); + + const auto status = pigweed.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // pigweed.special_property + 0x68, 0x2a, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteNestedImported) { + Period::Message message{}; + message.start.seconds = 1517949900u; + message.end.seconds = 1517950378u; + + std::byte encode_buffer[Period::kMaxEncodedSizeBytes]; + std::byte temp_buffer[Period::kScratchBufferSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Period::StreamEncoder period(writer, temp_buffer); + + const auto status = period.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // period.start + 0x0a, 0x06, + // period.start.seconds v=1517949900 + 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05, + // period.end + 0x12, 0x06, + // period.end.seconds, v=1517950378 + 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteNestedRepeated) { + RepeatedTest::Message message{}; + // Repeated nested messages require a callback since there would otherwise be + // no way to set callbacks on the nested message. + message.structs.SetEncoder([](RepeatedTest::StreamEncoder& encoder) { + for (int i = 0; i < 2; ++i) { + Struct::Message struct_message{}; + struct_message.one = i * 32 + 16u; + struct_message.two = i * 32 + 32u; + + const auto status = encoder.GetStructsEncoder().Write(struct_message); + EXPECT_EQ(status, OkStatus()); + } + return OkStatus(); + }); + + std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes + + Struct::kMaxEncodedSizeBytes * 2]; + std::byte temp_buffer[RepeatedTest::kScratchBufferSizeBytes + + Struct::kMaxEncodedSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + RepeatedTest::StreamEncoder repeated_test(writer, temp_buffer); + + const auto status = repeated_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // repeated.structs + 0x2a, 0x04, + // repeated.structs.one v=16 + 0x08, 0x10, + // repeated.structs.two v=32 + 0x10, 0x20, + // repeated.structs + 0x2a, 0x04, + // repeated.structs.one v=48 + 0x08, 0x30, + // repeated.structs.two v=64 + 0x10, 0x40, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteNestedForcedCallback) { + Pigweed::Message message{}; + // pigweed.device_info has use_callback=true to force the use of a callback. + message.device_info.SetEncoder([](Pigweed::StreamEncoder& encoder) { + DeviceInfo::Message device_info{}; + device_info.device_name = "pixel"; + device_info.device_id = 0x08080808u; + device_info.status = DeviceInfo::DeviceStatus::OK; + + // Use the callback to set nested callbacks. + device_info.attributes.SetEncoder( + [](DeviceInfo::StreamEncoder& device_info_encoder) { + KeyValuePair::Message attribute{}; + + attribute.key = "version"; + attribute.value = "5.3.1"; + PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute)); + + attribute.key = "chip"; + attribute.value = "left-soc"; + PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute)); + + return OkStatus(); + }); + + return encoder.GetDeviceInfoEncoder().Write(device_info); + }); + + std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes + + DeviceInfo::kMaxEncodedSizeBytes]; + std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes + + DeviceInfo::kMaxEncodedSizeBytes]; + + stream::MemoryWriter writer(encode_buffer); + Pigweed::StreamEncoder pigweed(writer, temp_buffer); + + const auto status = pigweed.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // pigweed.device_info + 0x32, 0x30, + // pigweed.device_info.device_name + 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l', + // pigweed.device_info.device_id + 0x15, 0x08, 0x08, 0x08, 0x08, + // pigweed.device_info.attributes[0] + 0x22, 0x10, + // pigweed.device_info.attributes[0].key + 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', + // pigweed.device_info.attributes[0].value + 0x12, 0x05, '5', '.', '3', '.', '1', + // pigweed.device_info.attributes[1] + 0x22, 0x10, + // pigweed.device_info.attributes[1].key + 0x0a, 0x04, 'c', 'h', 'i', 'p', + // pigweed.device_info.attributes[1].value + 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c', + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, EnumAliases) { + // Unprefixed enum. + EXPECT_EQ(Bool::kTrue, Bool::TRUE); + EXPECT_EQ(Bool::kFalse, Bool::FALSE); + EXPECT_EQ(Bool::kFileNotFound, Bool::FILE_NOT_FOUND); + + // Prefixed enum has the prefix removed. + EXPECT_EQ(Error::kNone, Error::ERROR_NONE); + EXPECT_EQ(Error::kNotFound, Error::ERROR_NOT_FOUND); + EXPECT_EQ(Error::kUnknown, Error::ERROR_UNKNOWN); + + // Single-value enum. + EXPECT_EQ(AlwaysBlue::kBlue, AlwaysBlue::BLUE); +} + +TEST(CodegenMessage, WriteOptionalPresent) { + OptionalTest::Message message{}; + message.sometimes_present_fixed = 0x2a; + message.sometimes_present_varint = 0x2a; + message.explicitly_present_fixed = 0x45; + message.explicitly_present_varint = 0x45; + message.sometimes_empty_fixed.push_back(0x63); + message.sometimes_empty_varint.push_back(0x63); + + std::byte encode_buffer[512]; + + stream::MemoryWriter writer(encode_buffer); + OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); + + const auto status = optional_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // optional.sometimes_present_fixed + 0x0d, 0x2a, 0x00, 0x00, 0x00, + // optional.sometimes_present_varint + 0x10, 0x2a, + // optional.explicitly_present_fixed + 0x1d, 0x45, 0x00, 0x00, 0x00, + // optional.explicitly_present_varint + 0x20, 0x45, + // optional.sometimes_empty_fixed + 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00, + // optional.sometimes_empty_varint + 0x32, 0x01, 0x63, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +TEST(CodegenMessage, WriteOptionalNotPresent) { + OptionalTest::Message message{}; + + std::byte encode_buffer[512]; + + stream::MemoryWriter writer(encode_buffer); + OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); + + const auto status = optional_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // The expected proto is empty; no bytes should be written. + + ConstByteSpan result = writer.WrittenData(); + EXPECT_TRUE(result.empty()); +} + +TEST(CodegenMessage, WriteOptionalPresentDefaults) { + OptionalTest::Message message{}; + // Non-optional fields with a default value are not explicitly encoded, so + // aren't meaningfully different from one that's just ommitted. + message.sometimes_present_fixed = 0x00; + message.sometimes_present_varint = 0x00; + // Optional fields, even with a default value, are explicitly encoded. + message.explicitly_present_fixed = 0x00; + message.explicitly_present_varint = 0x00; + // Repeated fields with a default value are meaningfully non-empty. + message.sometimes_empty_fixed.push_back(0x00); + message.sometimes_empty_varint.push_back(0x00); + + std::byte encode_buffer[512]; + + stream::MemoryWriter writer(encode_buffer); + OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); + + const auto status = optional_test.Write(message); + ASSERT_EQ(status, OkStatus()); + + // clang-format off + constexpr uint8_t expected_proto[] = { + // optional.explicitly_present_fixed + 0x1d, 0x00, 0x00, 0x00, 0x00, + // optional.explicitly_present_varint + 0x20, 0x00, + // optional.sometimes_empty_fixed + 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00, + // optional.sometimes_empty_varint + 0x32, 0x01, 0x00, + }; + // clang-format on + + ConstByteSpan result = writer.WrittenData(); + EXPECT_EQ(result.size(), sizeof(expected_proto)); + EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), + 0); +} + +class BreakableEncoder : public KeyValuePair::MemoryEncoder { + public: + constexpr BreakableEncoder(ByteSpan buffer) + : KeyValuePair::MemoryEncoder(buffer) {} + + Status Write(const KeyValuePair::Message& message, + span<const internal::MessageField> table) { + return ::pw::protobuf::StreamEncoder::Write(as_bytes(span(&message, 1)), + table); + } +}; + +TEST(CodegenMessage, DISABLED_WriteDoesNotOverrun) { + // Deliberately construct a message table that attempts to violate the bounds + // of the structure. We're not testing that a developer can't do this, rather + // that the protobuf encoder can't be exploited in this way. + constexpr internal::MessageField kMessageFields[] = { + {1, + WireType::kDelimited, + sizeof(std::byte), + static_cast<internal::VarintType>(0), + false, + false, + false, + false, + false, + 0, + sizeof(KeyValuePair::Message) * 2, + {}}, + }; + + std::byte encode_buffer[64]; + + BreakableEncoder encoder(encode_buffer); + KeyValuePair::Message message{}; + // ASSERT_CRASH + std::ignore = encoder.Write(message, kMessageFields); +} + +// The following tests cover using the codegen struct Message and callbacks in +// different ways. + +// Check that the callback function object is large enough to implement a +// "call a function on this" lambda. +class StringChecker { + public: + StringChecker() = default; + ~StringChecker() = default; + + Status Check(RepeatedTest::StreamDecoder& repeated_test) { + RepeatedTest::Message message{}; + message.strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) { + return this->CheckOne(decoder); + }); + return repeated_test.Read(message); + } + + private: + Status CheckOne(RepeatedTest::StreamDecoder& decoder) { + EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings); + + std::array<char, 40> strings{}; + const StatusWithSize sws = decoder.ReadStrings(strings); + EXPECT_EQ(sws.status(), OkStatus()); + EXPECT_EQ(sws.size(), kExpectedStrings[i_].size()); + EXPECT_EQ(std::memcmp(strings.data(), + kExpectedStrings[i_].data(), + kExpectedStrings[i_].size()), + 0); + + ++i_; + return sws.status(); + } + + int i_ = 0; + constexpr static std::string_view kExpectedStrings[] = { + {"if music be the food of love, play on"}, + {"give me excess of it, that, surfeiting"}, + {"the appetite may sicken, and so die"}}; +}; + +TEST(CodegenMessage, CallbackInClass) { + // clang-format off + constexpr uint8_t proto_data[] = { + // repeated.strings + 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', + 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', + 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', + // repeated.strings + 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', + 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', + ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', + // repeated.strings + 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', + 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', + ' ', 's', 'o', ' ', 'd', 'i', 'e', + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + StringChecker checker{}; + const auto status = checker.Check(repeated_test); + ASSERT_EQ(status, OkStatus()); +} + +// Check that we can create a custom subclass of the message struct that sets +// its own callbacks to member functions that populate fields added in the +// subclass. +struct CustomMessage : RepeatedTest::Message { + CustomMessage() : RepeatedTest::Message() { + strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) { + return this->ParseStrings(decoder); + }); + } + + pw::Vector<std::array<char, 40>, 8> all_strings{}; + + private: + Status ParseStrings(RepeatedTest::StreamDecoder& decoder) { + PW_ASSERT(decoder.Field().value() == RepeatedTest::Fields::kStrings); + + std::array<char, 40> one_strings{}; + const auto sws = decoder.ReadStrings(one_strings); + if (!sws.ok()) { + return sws.status(); + } + + one_strings[sws.size()] = '\0'; + all_strings.push_back(one_strings); + + return OkStatus(); + } +}; + +TEST(CodegenMessage, CallbackInSubclass) { + // clang-format off + constexpr uint8_t proto_data[] = { + // repeated.strings + 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', + 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', + 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', + // repeated.strings + 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', + 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', + ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', + // repeated.strings + 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', + 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', + ' ', 's', 'o', ' ', 'd', 'i', 'e', + }; + // clang-format on + + stream::MemoryReader reader(as_bytes(span(proto_data))); + RepeatedTest::StreamDecoder repeated_test(reader); + + CustomMessage message{}; + const auto status = repeated_test.Read(message); + ASSERT_EQ(status, OkStatus()); + + constexpr static std::string_view kExpectedStrings[] = { + {"if music be the food of love, play on"}, + {"give me excess of it, that, surfeiting"}, + {"the appetite may sicken, and so die"}}; + + EXPECT_EQ(message.all_strings.size(), 3u); + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(std::memcmp(message.all_strings[i].data(), + kExpectedStrings[i].data(), + kExpectedStrings[i].size()), + 0); + EXPECT_EQ(message.all_strings[i].data()[kExpectedStrings[i].size()], '\0'); + } +} + +} // namespace +} // namespace pw::protobuf |