diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8927ffb..356f089 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,5 +13,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Build + - name: Build examples run: make -j4 + + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON + + - name: Build tests + run: cmake --build build --parallel 4 + + - name: Run tests + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..66dc2d7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2025 The Falco 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 +# +# http://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. + +cmake_minimum_required(VERSION 3.12) +project(plugin-sdk-cpp VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Header-only library +add_library(plugin-sdk-cpp INTERFACE) +target_include_directories(plugin-sdk-cpp INTERFACE + $ + $ +) + +# Enable testing +option(BUILD_TESTING "Build tests" ON) +if(BUILD_TESTING) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/Makefile b/Makefile index facef83..16fac64 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ clean: $(examples_clean) format: +find ./include -iname *.h -o -iname *.cpp | grep -v "/deps/" | xargs $(CLANG_FORMAT) -i +find ./examples -iname *.h -o -iname *.cpp | grep -v "/deps/" | xargs $(CLANG_FORMAT) -i + +find ./tests -iname *.h -o -iname *.cpp | grep -v "/deps/" | xargs $(CLANG_FORMAT) -i .PHONY: deps deps: $(DEPS_INCLUDEDIR)/plugin_types.h $(DEPS_INCLUDEDIR)/plugin_api.h $(DEPS_INCLUDEDIR)/nlohmann/json.hpp diff --git a/include/falcosecurity/events/codec.h b/include/falcosecurity/events/codec.h new file mode 100644 index 0000000..03cf055 --- /dev/null +++ b/include/falcosecurity/events/codec.h @@ -0,0 +1,291 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace codec +{ + +// Helpers to get member type and class. +template struct member_type; + +template +struct member_type +{ + using type = MemberT; +}; + +template +using member_type_t = typename member_type::type; + +template struct class_type; + +template +struct class_type +{ + using type = ClassT; +}; + +template +using class_type_t = typename class_type::type; + +template struct is_size_ptr : std::false_type +{ +}; + +template +struct is_size_ptr : std::true_type +{ +}; + +// Schema + +template struct Field +{ + + using type = member_type_t; + static constexpr auto member_ptr = MemberPtr; + static constexpr auto member_len_ptr = MemberLenPtr; + static constexpr size_t member_len = sizeof(type); + + static_assert(MemberLenPtr == nullptr || is_size_ptr::value, + "Length member must be size_t"); + + static_assert(MemberLenPtr == nullptr || std::is_array_v || + std::is_pointer_v, + "A variable type must be an array or a pointer"); +}; + +template struct Schema +{ + static constexpr size_t num_fields = sizeof...(Fields); + using fields = std::tuple; +}; + +// Encoder + +template class Encoder +{ + public: + using StructT = class_type_t< + std::tuple_element_t<0, typename SchemaT::fields>::member_ptr>; + explicit Encoder(const StructT& obj): m_obj(obj) {} + + size_t encode(uint8_t* buf, size_t buf_size) + { + uint8_t* curr_ptr = buf; + size_t available = buf_size; + encode_fields(curr_ptr, available); + return buf_size - available; + } + + private: + template + void encode_fixed_field(uint8_t*& buf, size_t& available) + { + using FieldType = typename Field::type; + static_assert(std::is_trivially_copyable_v, + "Fixed fields must be trivially copyable"); + + if(available >= Field::member_len) + { + const FieldType& src = m_obj.*(Field::member_ptr); + std::memcpy(buf, &src, Field::member_len); + buf += Field::member_len; + available -= Field::member_len; + } + } + + template + void encode_variable_field(uint8_t*& buf, size_t& available) + { + using FieldMemberPtrClass = class_type_t; + using FieldMemberLenPtrClass = class_type_t; + static_assert( + std::is_same_v, + "The len of a variable field must belong to the same struct of " + "the variable field."); + + using FieldType = typename Field::type; + const size_t& len = m_obj.*(Field::member_len_ptr); + + if(available >= sizeof(size_t) + len) + { + // First we write the len + std::memcpy(buf, &len, sizeof(size_t)); + buf += sizeof(size_t); + available -= sizeof(size_t); + + // then we write the actual content + const FieldType& src = m_obj.*(Field::member_ptr); + std::memcpy(buf, &src, len); + buf += len; + available -= len; + } + } + + template void encode_field(uint8_t*& buf, size_t& available) + { + using Field = std::tuple_element_t; + if constexpr(Field::member_len_ptr == nullptr) + { + encode_fixed_field(buf, available); + } + else + { + encode_variable_field(buf, available); + } + } + + template + void encode_fields_impl(uint8_t*& buf, size_t& available, + std::index_sequence) + { + (encode_field(buf, available), ...); + } + + void encode_fields(uint8_t*& buf, size_t& available) + { + encode_fields_impl(buf, available, + std::make_index_sequence{}); + } + + // Compile time checks + + using SchemaFields = typename SchemaT::fields; + template + using FieldClassT = + class_type_t::member_ptr>; + + template + static constexpr bool check_same_struct(std::index_sequence) + { + return (std::is_same_v> && ...); + } + + static_assert( + check_same_struct(std::make_index_sequence{}), + "All fields must belong to the same struct"); + + static_assert(SchemaT::num_fields > 0, + "The schema must define at least a field."); + + const StructT& m_obj; +}; + +template class Decoder +{ + public: + using StructT = class_type_t< + std::tuple_element_t<0, typename SchemaT::fields>::member_ptr>; + explicit Decoder(StructT& obj): m_obj(obj) {} + + void decode(uint8_t* buf, const size_t& available) + { + uint8_t* cur_ptr = buf; + size_t left = available; + + decode_fields(cur_ptr, left); + } + + private: + template + void decode_fixed_field(uint8_t*& buf, size_t& left) + { + using FieldType = typename Field::type; + static_assert(std::is_trivially_copyable_v, + "Fixed fields must be trivially copyable"); + + if(left >= Field::member_len) + { + FieldType& dst = m_obj.*(Field::member_ptr); + std::memcpy(&dst, buf, Field::member_len); + buf += Field::member_len; + left -= Field::member_len; + } + } + + template + void decode_variable_field(uint8_t*& buf, size_t& left) + { + using FieldMemberPtrClass = class_type_t; + using FieldMemberLenPtrClass = class_type_t; + static_assert( + std::is_same_v, + "The len of a variable field must belong to the same struct of " + "the variable field."); + + size_t& len = m_obj.*(Field::member_len_ptr); + + if(left >= sizeof(size_t)) + { + std::memcpy(&len, buf, sizeof(size_t)); + buf += sizeof(size_t); + left -= sizeof(size_t); + + if(left >= len) + { + + using FieldType = typename Field::type; + FieldType& dst = m_obj.*(Field::member_ptr); + std::memcpy(&dst, buf, len); + buf += len; + left -= len; + } + } + } + + template void decode_field(uint8_t*& buf, size_t& left) + { + using Field = std::tuple_element_t; + if constexpr(Field::member_len_ptr == nullptr) + { + decode_fixed_field(buf, left); + } + else + { + decode_variable_field(buf, left); + } + } + + // Compile time checks + + template + void decode_fields_impl(uint8_t*& buf, size_t& left, + std::index_sequence) + { + (decode_field(buf, left), ...); + } + + void decode_fields(uint8_t*& buf, size_t& left) + { + decode_fields_impl(buf, left, + std::make_index_sequence{}); + } + + using SchemaFields = typename SchemaT::fields; + template + using FieldClassT = + class_type_t::member_ptr>; + + template + static constexpr bool check_same_struct(std::index_sequence) + { + return (std::is_same_v> && ...); + } + + static_assert( + check_same_struct(std::make_index_sequence{}), + "All fields must belong to the same struct"); + + static_assert(SchemaT::num_fields > 0, + "The schema must define at least a field."); + + StructT& m_obj; +}; + +} // namespace codec diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..1c9b750 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2025 The Falco 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 +# +# http://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(FetchContent) + +# Fetch Google Test +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) + +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Include Google Test's CMake functions +include(GoogleTest) + +# Codec test +add_executable(codec_test codec_test.cpp) +target_link_libraries(codec_test PRIVATE + plugin-sdk-cpp + GTest::gtest_main +) + +# Discover tests +gtest_discover_tests(codec_test) diff --git a/tests/codec_test.cpp b/tests/codec_test.cpp new file mode 100644 index 0000000..e8d7bfb --- /dev/null +++ b/tests/codec_test.cpp @@ -0,0 +1,675 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright (C) 2025 The Falco 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 +// +// http://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 + +#include + +#include + +// Test structures + +struct SimpleStruct +{ + uint32_t a; + uint64_t b; + uint8_t c; +}; + +struct VariableLengthStruct +{ + uint32_t id; + char data[256]; + size_t data_len; +}; + +struct MixedStruct +{ + uint32_t fixed1; + char variable[128]; + size_t variable_len; + uint64_t fixed2; +}; + +struct MultiVariableStruct +{ + char field1[64]; + size_t field1_len; + uint8_t fixed; + char field2[64]; + size_t field2_len; +}; + +// Tests for member_type + +TEST(MemberTypeTest, Uint32Type) +{ + using MemberType = codec::member_type_t<&SimpleStruct::a>; + EXPECT_TRUE((std::is_same_v)); +} + +TEST(MemberTypeTest, Uint64Type) +{ + using MemberType = codec::member_type_t<&SimpleStruct::b>; + EXPECT_TRUE((std::is_same_v)); +} + +TEST(MemberTypeTest, Uint8Type) +{ + using MemberType = codec::member_type_t<&SimpleStruct::c>; + EXPECT_TRUE((std::is_same_v)); +} + +TEST(MemberTypeTest, ArrayType) +{ + using MemberType = codec::member_type_t<&VariableLengthStruct::data>; + EXPECT_TRUE((std::is_same_v)); +} + +// Tests for class_type + +TEST(ClassTypeTest, SimpleStruct) +{ + using ClassType = codec::class_type_t<&SimpleStruct::a>; + EXPECT_TRUE((std::is_same_v)); +} + +TEST(ClassTypeTest, VariableLengthStruct) +{ + using ClassType = codec::class_type_t<&VariableLengthStruct::data>; + EXPECT_TRUE((std::is_same_v)); +} + +// Tests for is_size_ptr + +TEST(IsSizePtrTest, TrueForSizeT) +{ + EXPECT_TRUE((codec::is_size_ptr<&VariableLengthStruct::data_len>::value)); +} + +TEST(IsSizePtrTest, FalseForNonSizeT) +{ + EXPECT_FALSE((codec::is_size_ptr<&VariableLengthStruct::id>::value)); +} + +// Tests for Field + +TEST(FieldTest, FixedField) +{ + using TestField = codec::Field<&SimpleStruct::a>; + EXPECT_TRUE((std::is_same_v)); + EXPECT_EQ(TestField::member_len, sizeof(uint32_t)); + EXPECT_TRUE(TestField::member_len_ptr == nullptr); +} + +TEST(FieldTest, VariableField) +{ + using TestField = codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>; + EXPECT_TRUE((std::is_same_v)); + EXPECT_TRUE(TestField::member_len_ptr != nullptr); +} + +// Tests for Schema + +TEST(SchemaTest, NumFields) +{ + using TestSchema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + EXPECT_EQ(TestSchema::num_fields, 3); +} + +// Tests for Encoder - fixed fields only + +TEST(EncoderTest, SimpleStruct) +{ + SimpleStruct obj{0x12345678, 0xABCDEF0123456789ULL, 0x42}; + + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Encoder encoder(obj); + + uint8_t buffer[256] = {0}; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Expected size: 4 + 8 + 1 = 13 bytes + EXPECT_EQ(encoded_size, 13); + + // Check encoded values + uint32_t decoded_a; + uint64_t decoded_b; + uint8_t decoded_c; + + std::memcpy(&decoded_a, buffer, sizeof(uint32_t)); + std::memcpy(&decoded_b, buffer + 4, sizeof(uint64_t)); + std::memcpy(&decoded_c, buffer + 12, sizeof(uint8_t)); + + EXPECT_EQ(decoded_a, 0x12345678); + EXPECT_EQ(decoded_b, 0xABCDEF0123456789ULL); + EXPECT_EQ(decoded_c, 0x42); +} + +TEST(EncoderTest, InsufficientBuffer) +{ + SimpleStruct obj{0x12345678, 0xABCDEF0123456789ULL, 0x42}; + + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Encoder encoder(obj); + + // Buffer too small - only 5 bytes + // Will encode field a (4 bytes), skip field b (needs 8), encode field c (1 + // byte) + uint8_t buffer[5] = {0}; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Should encode first and third fields: 4 + 1 = 5 bytes + EXPECT_EQ(encoded_size, 5); + + uint32_t decoded_a; + uint8_t decoded_c; + std::memcpy(&decoded_a, buffer, sizeof(uint32_t)); + std::memcpy(&decoded_c, buffer + 4, sizeof(uint8_t)); + EXPECT_EQ(decoded_a, 0x12345678); + EXPECT_EQ(decoded_c, 0x42); +} + +// Tests for Encoder - variable fields + +TEST(EncoderTest, VariableField) +{ + VariableLengthStruct obj; + obj.id = 0x1234; + const char* test_data = "Hello, World!"; + std::strcpy(obj.data, test_data); + obj.data_len = std::strlen(test_data); + + using Schema = codec::Schema, + codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>>; + + codec::Encoder encoder(obj); + + uint8_t buffer[256] = {0}; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Expected size: 4 (id) + 8 (length) + 13 (data) = 25 bytes + EXPECT_EQ(encoded_size, 25); + + // Decode and verify + uint32_t decoded_id; + size_t decoded_len; + char decoded_data[256]; + + std::memcpy(&decoded_id, buffer, sizeof(uint32_t)); + std::memcpy(&decoded_len, buffer + 4, sizeof(size_t)); + std::memcpy(decoded_data, buffer + 4 + sizeof(size_t), decoded_len); + decoded_data[decoded_len] = '\0'; + + EXPECT_EQ(decoded_id, 0x1234); + EXPECT_EQ(decoded_len, obj.data_len); + EXPECT_STREQ(decoded_data, test_data); +} + +TEST(EncoderTest, MixedFields) +{ + MixedStruct obj; + obj.fixed1 = 0xAABBCCDD; + const char* test_data = "Test"; + std::strcpy(obj.variable, test_data); + obj.variable_len = std::strlen(test_data); + obj.fixed2 = 0x1122334455667788ULL; + + using Schema = codec::Schema< + codec::Field<&MixedStruct::fixed1>, + codec::Field<&MixedStruct::variable, &MixedStruct::variable_len>, + codec::Field<&MixedStruct::fixed2>>; + + codec::Encoder encoder(obj); + + uint8_t buffer[256] = {0}; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Expected size: 4 (fixed1) + 8 (len) + 4 (data) + 8 (fixed2) = 24 bytes + EXPECT_EQ(encoded_size, 24); + + // Verify structure + size_t offset = 0; + + uint32_t decoded_fixed1; + std::memcpy(&decoded_fixed1, buffer + offset, sizeof(uint32_t)); + EXPECT_EQ(decoded_fixed1, 0xAABBCCDD); + offset += sizeof(uint32_t); + + size_t decoded_len; + std::memcpy(&decoded_len, buffer + offset, sizeof(size_t)); + EXPECT_EQ(decoded_len, 4); + offset += sizeof(size_t); + + char decoded_data[128]; + std::memcpy(decoded_data, buffer + offset, decoded_len); + decoded_data[decoded_len] = '\0'; + EXPECT_STREQ(decoded_data, test_data); + offset += decoded_len; + + uint64_t decoded_fixed2; + std::memcpy(&decoded_fixed2, buffer + offset, sizeof(uint64_t)); + EXPECT_EQ(decoded_fixed2, 0x1122334455667788ULL); +} + +TEST(EncoderTest, MultipleVariableFields) +{ + MultiVariableStruct obj; + const char* test1 = "First"; + const char* test2 = "Second"; + std::strcpy(obj.field1, test1); + obj.field1_len = std::strlen(test1); + obj.fixed = 0x99; + std::strcpy(obj.field2, test2); + obj.field2_len = std::strlen(test2); + + using Schema = + codec::Schema, + codec::Field<&MultiVariableStruct::fixed>, + codec::Field<&MultiVariableStruct::field2, + &MultiVariableStruct::field2_len>>; + + codec::Encoder encoder(obj); + + uint8_t buffer[256] = {0}; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Expected size: 8 (len1) + 5 (field1) + 1 (fixed) + 8 (len2) + 6 (field2) + // = 28 bytes + EXPECT_EQ(encoded_size, 28); +} + +// Tests for Decoder - fixed fields only + +TEST(DecoderTest, SimpleStruct) +{ + // Prepare encoded data + uint8_t buffer[256]; + size_t offset = 0; + + uint32_t val_a = 0x12345678; + uint64_t val_b = 0xABCDEF0123456789ULL; + uint8_t val_c = 0x42; + + std::memcpy(buffer + offset, &val_a, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::memcpy(buffer + offset, &val_b, sizeof(uint64_t)); + offset += sizeof(uint64_t); + std::memcpy(buffer + offset, &val_c, sizeof(uint8_t)); + offset += sizeof(uint8_t); + + // Decode + SimpleStruct obj{}; + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Decoder decoder(obj); + decoder.decode(buffer, offset); + + EXPECT_EQ(obj.a, 0x12345678); + EXPECT_EQ(obj.b, 0xABCDEF0123456789ULL); + EXPECT_EQ(obj.c, 0x42); +} + +TEST(DecoderTest, InsufficientBuffer) +{ + // Prepare encoded data - only 5 bytes (partial) + uint8_t buffer[5]; + uint32_t val_a = 0x12345678; + std::memcpy(buffer, &val_a, sizeof(uint32_t)); + buffer[4] = 0xFF; + + // Decode - will decode field a (4 bytes), skip field b (needs 8), decode + // field c (1 byte) + SimpleStruct obj{}; + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Decoder decoder(obj); + decoder.decode(buffer, 5); + + EXPECT_EQ(obj.a, 0x12345678); + // b should remain uninitialized (zero) as there wasn't enough data + EXPECT_EQ(obj.b, 0); + // c should be decoded from buffer[4] + EXPECT_EQ(obj.c, 0xFF); +} + +// Tests for Decoder - variable fields + +TEST(DecoderTest, VariableField) +{ + // Prepare encoded data + uint8_t buffer[256]; + size_t offset = 0; + + uint32_t val_id = 0x1234; + const char* test_data = "Hello, World!"; + size_t data_len = std::strlen(test_data); + + std::memcpy(buffer + offset, &val_id, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::memcpy(buffer + offset, &data_len, sizeof(size_t)); + offset += sizeof(size_t); + std::memcpy(buffer + offset, test_data, data_len); + offset += data_len; + + // Decode + VariableLengthStruct obj{}; + using Schema = codec::Schema, + codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>>; + + codec::Decoder decoder(obj); + decoder.decode(buffer, offset); + + EXPECT_EQ(obj.id, 0x1234); + EXPECT_EQ(obj.data_len, data_len); + obj.data[obj.data_len] = '\0'; + EXPECT_STREQ(obj.data, test_data); +} + +TEST(DecoderTest, MixedFields) +{ + // Prepare encoded data + uint8_t buffer[256]; + size_t offset = 0; + + uint32_t val_fixed1 = 0xAABBCCDD; + const char* test_data = "Test"; + size_t var_len = std::strlen(test_data); + uint64_t val_fixed2 = 0x1122334455667788ULL; + + std::memcpy(buffer + offset, &val_fixed1, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::memcpy(buffer + offset, &var_len, sizeof(size_t)); + offset += sizeof(size_t); + std::memcpy(buffer + offset, test_data, var_len); + offset += var_len; + std::memcpy(buffer + offset, &val_fixed2, sizeof(uint64_t)); + offset += sizeof(uint64_t); + + // Decode + MixedStruct obj{}; + using Schema = codec::Schema< + codec::Field<&MixedStruct::fixed1>, + codec::Field<&MixedStruct::variable, &MixedStruct::variable_len>, + codec::Field<&MixedStruct::fixed2>>; + + codec::Decoder decoder(obj); + decoder.decode(buffer, offset); + + EXPECT_EQ(obj.fixed1, 0xAABBCCDD); + EXPECT_EQ(obj.variable_len, var_len); + obj.variable[obj.variable_len] = '\0'; + EXPECT_STREQ(obj.variable, test_data); + EXPECT_EQ(obj.fixed2, 0x1122334455667788ULL); +} + +TEST(DecoderTest, MultipleVariableFields) +{ + // Prepare encoded data + uint8_t buffer[256]; + size_t offset = 0; + + const char* test1 = "First"; + size_t len1 = std::strlen(test1); + uint8_t val_fixed = 0x99; + const char* test2 = "Second"; + size_t len2 = std::strlen(test2); + + std::memcpy(buffer + offset, &len1, sizeof(size_t)); + offset += sizeof(size_t); + std::memcpy(buffer + offset, test1, len1); + offset += len1; + std::memcpy(buffer + offset, &val_fixed, sizeof(uint8_t)); + offset += sizeof(uint8_t); + std::memcpy(buffer + offset, &len2, sizeof(size_t)); + offset += sizeof(size_t); + std::memcpy(buffer + offset, test2, len2); + offset += len2; + + // Decode + MultiVariableStruct obj{}; + using Schema = + codec::Schema, + codec::Field<&MultiVariableStruct::fixed>, + codec::Field<&MultiVariableStruct::field2, + &MultiVariableStruct::field2_len>>; + + codec::Decoder decoder(obj); + decoder.decode(buffer, offset); + + EXPECT_EQ(obj.field1_len, len1); + obj.field1[obj.field1_len] = '\0'; + EXPECT_STREQ(obj.field1, test1); + EXPECT_EQ(obj.fixed, 0x99); + EXPECT_EQ(obj.field2_len, len2); + obj.field2[obj.field2_len] = '\0'; + EXPECT_STREQ(obj.field2, test2); +} + +// Round-trip tests (encode then decode) + +TEST(RoundTripTest, SimpleStruct) +{ + SimpleStruct original{0xDEADBEEF, 0xCAFEBABEDEADBEEFULL, 0xFF}; + + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + // Encode + codec::Encoder encoder(original); + uint8_t buffer[256]; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Decode + SimpleStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + // Verify + EXPECT_EQ(decoded.a, original.a); + EXPECT_EQ(decoded.b, original.b); + EXPECT_EQ(decoded.c, original.c); +} + +TEST(RoundTripTest, VariableField) +{ + VariableLengthStruct original; + original.id = 0x9876; + const char* test_data = "Round trip test data!"; + std::strcpy(original.data, test_data); + original.data_len = std::strlen(test_data); + + using Schema = codec::Schema, + codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>>; + + // Encode + codec::Encoder encoder(original); + uint8_t buffer[256]; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Decode + VariableLengthStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + // Verify + EXPECT_EQ(decoded.id, original.id); + EXPECT_EQ(decoded.data_len, original.data_len); + decoded.data[decoded.data_len] = '\0'; + EXPECT_STREQ(decoded.data, original.data); +} + +TEST(RoundTripTest, MixedFields) +{ + MixedStruct original; + original.fixed1 = 0x11223344; + const char* test_data = "Mixed fields test"; + std::strcpy(original.variable, test_data); + original.variable_len = std::strlen(test_data); + original.fixed2 = 0xFFEEDDCCBBAA9988ULL; + + using Schema = codec::Schema< + codec::Field<&MixedStruct::fixed1>, + codec::Field<&MixedStruct::variable, &MixedStruct::variable_len>, + codec::Field<&MixedStruct::fixed2>>; + + // Encode + codec::Encoder encoder(original); + uint8_t buffer[256]; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Decode + MixedStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + // Verify + EXPECT_EQ(decoded.fixed1, original.fixed1); + EXPECT_EQ(decoded.variable_len, original.variable_len); + decoded.variable[decoded.variable_len] = '\0'; + EXPECT_STREQ(decoded.variable, original.variable); + EXPECT_EQ(decoded.fixed2, original.fixed2); +} + +TEST(RoundTripTest, EmptyVariableField) +{ + VariableLengthStruct original; + original.id = 0x5555; + original.data[0] = '\0'; + original.data_len = 0; + + using Schema = codec::Schema, + codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>>; + + // Encode + codec::Encoder encoder(original); + uint8_t buffer[256]; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Should be: 4 (id) + 8 (len=0) + 0 (data) = 12 bytes + EXPECT_EQ(encoded_size, 12); + + // Decode + VariableLengthStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + // Verify + EXPECT_EQ(decoded.id, original.id); + EXPECT_EQ(decoded.data_len, 0); +} + +// Edge case tests + +TEST(EdgeCaseTest, ZeroBuffer) +{ + SimpleStruct obj{0x12345678, 0xABCDEF0123456789ULL, 0x42}; + + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Encoder encoder(obj); + + uint8_t buffer[1] = {0}; + size_t encoded_size = encoder.encode(buffer, 0); + + // Should encode nothing + EXPECT_EQ(encoded_size, 0); +} + +TEST(EdgeCaseTest, ExactBufferSize) +{ + SimpleStruct obj{0x12345678, 0xABCDEF0123456789ULL, 0x42}; + + using Schema = codec::Schema, + codec::Field<&SimpleStruct::b>, + codec::Field<&SimpleStruct::c>>; + + codec::Encoder encoder(obj); + + // Exact size buffer + uint8_t buffer[13] = {0}; + size_t encoded_size = encoder.encode(buffer, 13); + + EXPECT_EQ(encoded_size, 13); + + // Verify all fields encoded + SimpleStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + EXPECT_EQ(decoded.a, obj.a); + EXPECT_EQ(decoded.b, obj.b); + EXPECT_EQ(decoded.c, obj.c); +} + +TEST(EdgeCaseTest, LargeVariableData) +{ + VariableLengthStruct obj; + obj.id = 0xABCD; + + // Fill with large data (200 bytes) + for(size_t i = 0; i < 200; i++) + { + obj.data[i] = static_cast('A' + (i % 26)); + } + obj.data[200] = '\0'; + obj.data_len = 200; + + using Schema = codec::Schema, + codec::Field<&VariableLengthStruct::data, + &VariableLengthStruct::data_len>>; + + // Encode + codec::Encoder encoder(obj); + uint8_t buffer[512]; + size_t encoded_size = encoder.encode(buffer, sizeof(buffer)); + + // Expected: 4 (id) + 8 (len) + 200 (data) = 212 bytes + EXPECT_EQ(encoded_size, 212); + + // Decode + VariableLengthStruct decoded{}; + codec::Decoder decoder(decoded); + decoder.decode(buffer, encoded_size); + + EXPECT_EQ(decoded.id, obj.id); + EXPECT_EQ(decoded.data_len, obj.data_len); + EXPECT_EQ(std::memcmp(decoded.data, obj.data, obj.data_len), 0); +}