From 00aedd05e8efaa6d49d0711ad8052414a4176279 Mon Sep 17 00:00:00 2001 From: FuzzTest Team Date: Mon, 3 Mar 2025 08:27:23 -0800 Subject: [PATCH] Add flatbuffers domain PiperOrigin-RevId: 732932597 --- .github/workflows/cmake_test.yml | 3 + CMakeLists.txt | 1 + MODULE.bazel | 6 +- cmake/BuildDependencies.cmake | 17 + cmake/generate_cmake_from_bazel.py | 1 + domain_tests/BUILD | 15 + domain_tests/CMakeLists.txt | 22 + .../arbitrary_domains_flatbuffers_test.cc | 243 ++++++ fuzztest/BUILD | 52 ++ fuzztest/CMakeLists.txt | 95 +++ fuzztest/flatbuffers.h | 19 + .../domains/flatbuffers_domain_impl.h | 751 ++++++++++++++++++ fuzztest/internal/meta.h | 16 + fuzztest/internal/test_flatbuffers.fbs | 45 ++ 14 files changed, 1284 insertions(+), 2 deletions(-) create mode 100644 domain_tests/arbitrary_domains_flatbuffers_test.cc create mode 100644 fuzztest/flatbuffers.h create mode 100644 fuzztest/internal/domains/flatbuffers_domain_impl.h create mode 100644 fuzztest/internal/test_flatbuffers.fbs diff --git a/.github/workflows/cmake_test.yml b/.github/workflows/cmake_test.yml index d23c692c..e4a5f440 100644 --- a/.github/workflows/cmake_test.yml +++ b/.github/workflows/cmake_test.yml @@ -77,6 +77,7 @@ jobs: -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ -D CMAKE_BUILD_TYPE=RelWithDebug \ -D FUZZTEST_BUILD_TESTING=on \ + -D FUZZTEST_BUILD_FLATBUFFERS=on \ && cmake --build build -j $(nproc) \ && ctest --test-dir build -j $(nproc) --output-on-failure - name: Run all tests in default mode with gcc @@ -90,6 +91,7 @@ jobs: -D CMAKE_CXX_COMPILER_LAUNCHER=ccache \ -D CMAKE_BUILD_TYPE=RelWithDebug \ -D FUZZTEST_BUILD_TESTING=on \ + -D FUZZTEST_BUILD_FLATBUFFERS=on \ && cmake --build build_gcc -j $(nproc) \ && ctest --test-dir build_gcc -j $(nproc) --output-on-failure - name: Run end-to-end tests in fuzzing mode @@ -104,6 +106,7 @@ jobs: -D CMAKE_BUILD_TYPE=RelWithDebug \ -D FUZZTEST_FUZZING_MODE=on \ -D FUZZTEST_BUILD_TESTING=on \ + -D FUZZTEST_BUILD_FLATBUFFERS=on \ && cmake --build build -j $(nproc) \ && ctest --test-dir build -j $(nproc) --output-on-failure -R "functional_test" - name: Save new cache based on main diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e34de3c..e3803d30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.19) project(fuzztest) option(FUZZTEST_BUILD_TESTING "Building the tests." OFF) +option(FUZZTEST_BUILD_FLATBUFFERS "Building the flatbuffers support." OFF) option(FUZZTEST_FUZZING_MODE "Building the fuzztest in fuzzing mode." OFF) set(FUZZTEST_COMPATIBILITY_MODE "" CACHE STRING "Compatibility mode. Available options: , libfuzzer") set(CMAKE_CXX_STANDARD 17) diff --git a/MODULE.bazel b/MODULE.bazel index 9fbd4794..28902ac8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -42,6 +42,10 @@ bazel_dep( name = "platforms", version = "0.0.10", ) +bazel_dep( + name = "flatbuffers", + version = "25.2.10" +) # GoogleTest is not a dev dependency, because it's needed when FuzzTest is used # with GoogleTest integration (e.g., googletest_adaptor). Note that the FuzzTest # framework can be used without GoogleTest integration as well. @@ -55,8 +59,6 @@ bazel_dep( name = "protobuf", version = "30.2", ) -# TODO(lszekeres): Make this a dev dependency, as the protobuf library is only -# required for testing. bazel_dep( name = "rules_proto", version = "7.1.0", diff --git a/cmake/BuildDependencies.cmake b/cmake/BuildDependencies.cmake index 5214fcef..2966c0b6 100644 --- a/cmake/BuildDependencies.cmake +++ b/cmake/BuildDependencies.cmake @@ -21,6 +21,9 @@ set(proto_TAG v30.2) set(nlohmann_json_URL https://github.com/nlohmann/json.git) set(nlohmann_json_TAG v3.11.3) +set(flatbuffers_URL https://github.com/google/flatbuffers.git) +set(flatbuffers_TAG v25.2.10) + if(POLICY CMP0135) cmake_policy(SET CMP0135 NEW) set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) @@ -50,6 +53,14 @@ FetchContent_Declare( URL_HASH MD5=${antlr_cpp_MD5} ) +if (FUZZTEST_BUILD_FLATBUFFERS) + FetchContent_Declare( + flatbuffers + GIT_REPOSITORY ${flatbuffers_URL} + GIT_TAG ${flatbuffers_TAG} + ) +endif() + if (FUZZTEST_BUILD_TESTING) FetchContent_Declare( @@ -87,3 +98,9 @@ if (FUZZTEST_BUILD_TESTING) FetchContent_MakeAvailable(nlohmann_json) endif () + +if (FUZZTEST_BUILD_FLATBUFFERS) + set(FLATBUFFERS_BUILD_TESTS OFF) + set(FLATBUFFERS_BUILD_INSTALL OFF) + FetchContent_MakeAvailable(flatbuffers) +endif() diff --git a/cmake/generate_cmake_from_bazel.py b/cmake/generate_cmake_from_bazel.py index 83d31d5d..0c9079cb 100755 --- a/cmake/generate_cmake_from_bazel.py +++ b/cmake/generate_cmake_from_bazel.py @@ -52,6 +52,7 @@ "@abseil-cpp//absl/types:optional": "absl::optional", "@abseil-cpp//absl/types:span": "absl::span", "@abseil-cpp//absl/types:variant": "absl::variant", + "@flatbuffers//:runtime_cc": "flatbuffers", "@googletest//:gtest": "GTest::gtest", "@googletest//:gtest_main": "GTest::gmock_main", "@protobuf//:protobuf": "protobuf::libprotobuf", diff --git a/domain_tests/BUILD b/domain_tests/BUILD index 436a2c9f..4e207e38 100644 --- a/domain_tests/BUILD +++ b/domain_tests/BUILD @@ -33,6 +33,21 @@ cc_test( ], ) +cc_test( + name = "arbitrary_domains_flatbuffers_test", + srcs = ["arbitrary_domains_flatbuffers_test.cc"], + deps = [ + ":domain_testing", + "@abseil-cpp//absl/random", + "@com_google_fuzztest//fuzztest:domain", + "@com_google_fuzztest//fuzztest:flatbuffers", + "@com_google_fuzztest//fuzztest:meta", + "@com_google_fuzztest//fuzztest:test_flatbuffers_cc_fbs", + "@flatbuffers//:runtime_cc", + "@googletest//:gtest_main", + ], +) + cc_test( name = "arbitrary_domains_protobuf_test", srcs = ["arbitrary_domains_protobuf_test.cc"], diff --git a/domain_tests/CMakeLists.txt b/domain_tests/CMakeLists.txt index a99142ab..89944a93 100644 --- a/domain_tests/CMakeLists.txt +++ b/domain_tests/CMakeLists.txt @@ -19,6 +19,28 @@ fuzztest_cc_test( GTest::gmock_main ) +if (FUZZTEST_BUILD_FLATBUFFERS) + fuzztest_cc_test( + NAME + arbitrary_domains_flatbuffers_test + SRCS + "arbitrary_domains_flatbuffers_test.cc" + DEPS + absl::flat_hash_set + absl::random_random + absl::random_bit_gen_ref + absl::strings + flatbuffers + fuzztest::domain + fuzztest::domain_testing + test_flatbuffers + GTest::gmock_main + ) + add_dependencies(fuzztest_arbitrary_domains_flatbuffers_test + GENERATE_test_flatbuffers + ) +endif() + fuzztest_cc_test( NAME arbitrary_domains_protobuf_test diff --git a/domain_tests/arbitrary_domains_flatbuffers_test.cc b/domain_tests/arbitrary_domains_flatbuffers_test.cc new file mode 100644 index 00000000..50bf416c --- /dev/null +++ b/domain_tests/arbitrary_domains_flatbuffers_test.cc @@ -0,0 +1,243 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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 +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/random/random.h" +#include "flatbuffers/base.h" +#include "flatbuffers/buffer.h" +#include "flatbuffers/flatbuffer_builder.h" +#include "flatbuffers/string.h" +#include "./fuzztest/domain.h" +#include "./domain_tests/domain_testing.h" +#include "./fuzztest/flatbuffers.h" +#include "./fuzztest/internal/meta.h" +#include "./fuzztest/internal/test_flatbuffers_generated.h" + +namespace fuzztest { +namespace { + +using ::fuzztest::internal::DefaultTestFbsTable; +using ::fuzztest::internal::OptionalTestFbsTable; +using ::fuzztest::internal::RequiredTestFbsTable; +using ::testing::Contains; +using ::testing::IsTrue; +using ::testing::ResultOf; + +TEST(FlatbuffersMetaTest, IsFlatbuffersTable) { + static_assert(internal::is_flatbuffers_table_v); + static_assert(!internal::is_flatbuffers_table_v); + static_assert(!internal::is_flatbuffers_table_v>); +} + +TEST(FlatbuffersTableImplTest, DefaultTestFbsTableValueRoundTrip) { + flatbuffers::FlatBufferBuilder fbb; + auto table_offset = internal::CreateDefaultTestFbsTableDirect( + fbb, + true, // b + 1, // i8 + 2, // i16 + 3, // i32 + 4, // i64 + 5, // u8 + 6, // u16 + 7, // u32 + 8, // u64 + 9.0, // f + 10.0, // d + "foo bar baz", // str + internal::TestFbsEnum_Second // e + ); + fbb.Finish(table_offset); + auto table = + flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto domain = Arbitrary(); + auto corpus = domain.FromValue(table); + ASSERT_TRUE(corpus.has_value()); + ASSERT_OK(domain.ValidateCorpusValue(*corpus)); + + auto ir = domain.SerializeCorpus(corpus.value()); + + auto new_corpus = domain.ParseCorpus(ir); + ASSERT_TRUE(new_corpus.has_value()); + ASSERT_OK(domain.ValidateCorpusValue(*new_corpus)); + + auto new_table = domain.GetValue(*new_corpus); + EXPECT_EQ(new_table->b(), true); + EXPECT_EQ(new_table->i8(), 1); + EXPECT_EQ(new_table->i16(), 2); + EXPECT_EQ(new_table->i32(), 3); + EXPECT_EQ(new_table->i64(), 4); + EXPECT_EQ(new_table->u8(), 5); + EXPECT_EQ(new_table->u16(), 6); + EXPECT_EQ(new_table->u32(), 7); + EXPECT_EQ(new_table->u64(), 8); + EXPECT_EQ(new_table->f(), 9.0); + EXPECT_EQ(new_table->d(), 10.0); + EXPECT_EQ(new_table->str()->str(), "foo bar baz"); + EXPECT_TRUE(new_table->e() == internal::TestFbsEnum_Second); +} + +TEST(FlatbuffersTableImplTest, InitGeneratesSeeds) { + flatbuffers::FlatBufferBuilder fbb; + auto table_offset = internal::CreateDefaultTestFbsTableDirect( + fbb, + true, // b + 1, // i8 + 2, // i16 + 3, // i32 + 4, // i64 + 5, // u8 + 6, // u16 + 7, // u32 + 8, // u64 + 9.0, // f + 10.0, // d + "foo bar baz", // str + internal::TestFbsEnum_Second // e + ); + fbb.Finish(table_offset); + auto table = + flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto domain = Arbitrary(); + domain.WithSeeds({table}); + + std::vector> values; + absl::BitGen bitgen; + values.reserve(1000); + for (int i = 0; i < 1000; ++i) { + Value value(domain, bitgen); + values.push_back(std::move(value)); + } + + EXPECT_THAT(values, + Contains(ResultOf( + [table](const auto& val) { + const auto is_str_eq = [](const flatbuffers::String* a, + const flatbuffers::String* b) { + return a == nullptr && b == nullptr || + a != nullptr && b != nullptr && + a->str() == b->str(); + }; + + return (val.user_value->b() == table->b() && + val.user_value->i8() == table->i8() && + val.user_value->i16() == table->i16() && + val.user_value->i32() == table->i32() && + val.user_value->i64() == table->i64() && + val.user_value->u8() == table->u8() && + val.user_value->u16() == table->u16() && + val.user_value->u32() == table->u32() && + val.user_value->u64() == table->u64() && + val.user_value->f() == table->f() && + val.user_value->d() == table->d() && + val.user_value->f() == table->f() && + val.user_value->e() == table->e() && + is_str_eq(val.user_value->str(), table->str())); + }, + IsTrue()))); +} + +TEST(FlatbuffersTableImplTest, EventuallyMutatesAllTableFields) { + auto domain = Arbitrary(); + + absl::BitGen bitgen; + Value val(domain, bitgen); + + const auto verify_field_changes = [&](std::string_view name, auto get) { + Set values; + + int iterations = 10'000; + while (--iterations > 0 && values.size() < 2) { + values.insert(get(val.user_value)); + val.Mutate(domain, bitgen, {}, false); + } + EXPECT_GT(iterations, 0) + << "Field: " << name << " -- " << testing::PrintToString(values); + }; + + verify_field_changes("b", [](auto v) { return v->b(); }); + verify_field_changes("i8", [](auto v) { return v->i8(); }); + verify_field_changes("i16", [](auto v) { return v->i16(); }); + verify_field_changes("i32", [](auto v) { return v->i32(); }); + verify_field_changes("i64", [](auto v) { return v->i64(); }); + verify_field_changes("u8", [](auto v) { return v->u8(); }); + verify_field_changes("u16", [](auto v) { return v->u16(); }); + verify_field_changes("u32", [](auto v) { return v->u32(); }); + verify_field_changes("u64", [](auto v) { return v->u64(); }); + verify_field_changes("f", [](auto v) { return v->f(); }); + verify_field_changes("d", [](auto v) { return v->d(); }); + verify_field_changes("str", [](auto v) { return v->str()->str(); }); + verify_field_changes("e", [](auto v) { return v->e(); }); +} + +TEST(FlatbuffersTableImplTest, OptionalTestFbsTableEventuallyBecomeEmpty) { + auto domain = Arbitrary(); + + absl::BitGen bitgen; + Value val(domain, bitgen); + + const auto verify_field_becomes_null = [&](std::string_view name, auto has) { + for (int i = 0; i < 10'000; ++i) { + val.Mutate(domain, bitgen, {}, false); + if (!has(val.user_value)) { + break; + } + } + EXPECT_FALSE(has(val.user_value)) << "Field never became unset: " << name; + }; + + verify_field_becomes_null("b", [](auto v) { return v->b().has_value(); }); + verify_field_becomes_null("i8", [](auto v) { return v->i8().has_value(); }); + verify_field_becomes_null("i16", [](auto v) { return v->i16().has_value(); }); + verify_field_becomes_null("i32", [](auto v) { return v->i32().has_value(); }); + verify_field_becomes_null("i64", [](auto v) { return v->i64().has_value(); }); + verify_field_becomes_null("u8", [](auto v) { return v->u8().has_value(); }); + verify_field_becomes_null("u16", [](auto v) { return v->u16().has_value(); }); + verify_field_becomes_null("u32", [](auto v) { return v->u32().has_value(); }); + verify_field_becomes_null("u64", [](auto v) { return v->u64().has_value(); }); + verify_field_becomes_null("f", [](auto v) { return v->f().has_value(); }); + verify_field_becomes_null("d", [](auto v) { return v->d().has_value(); }); + verify_field_becomes_null("str", [](auto v) { return v->str() != nullptr; }); + verify_field_becomes_null("e", [](auto v) { return v->e().has_value(); }); +} + +TEST(FlatbuffersTableImplTest, RequiredTestFbsTableFieldsAlwaysSet) { + auto domain = Arbitrary(); + + absl::BitGen bitgen; + Value val(domain, bitgen); + + const auto verify_field_always_set = [&](std::string_view name, auto has) { + for (int i = 0; i < 10'000; ++i) { + val.Mutate(domain, bitgen, {}, false); + if (!has(val.user_value)) { + break; + } + } + EXPECT_TRUE(has(val.user_value)) << "Field is not set: " << name; + }; + + verify_field_always_set("str", [](auto v) { return v->str() != nullptr; }); +} + +} // namespace +} // namespace fuzztest diff --git a/fuzztest/BUILD b/fuzztest/BUILD index 86a458e0..8cb4b455 100644 --- a/fuzztest/BUILD +++ b/fuzztest/BUILD @@ -345,6 +345,7 @@ cc_library( ":serialization", ":status", ":type_support", + "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/base:no_destructor", "@abseil-cpp//absl/container:flat_hash_map", @@ -422,6 +423,35 @@ cc_library( ], ) +cc_library( + name = "flatbuffers", + srcs = ["internal/domains/flatbuffers_domain_impl.h"], + hdrs = ["flatbuffers.h"], + deps = [ + ":any", + ":domain_core", + ":logging", + ":meta", + ":serialization", + ":status", + ":type_support", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/random", + "@abseil-cpp//absl/random:bit_gen_ref", + "@abseil-cpp//absl/random:distributions", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/synchronization", + "@flatbuffers//:runtime_cc", + ], +) + cc_library( name = "fixture_driver", srcs = ["internal/fixture_driver.cc"], @@ -801,6 +831,28 @@ cc_proto_library( deps = [":test_protobuf"], ) +# Derived from @flatbuffers//build_defs.bzl:flatbuffer_cc_library but allows output prefix for +# single source target and to have embedded schema file in the outputs. +genrule( + name = "test_flatbuffers_fbs", + srcs = ["internal/test_flatbuffers.fbs"], + outs = [ + "internal/test_flatbuffers_bfbs_generated.h", + "internal/test_flatbuffers_generated.h", + ], + cmd = "$(location @flatbuffers//:flatc) -c -o $(@D)/internal --bfbs-gen-embed --gen-name-strings $(SRCS)", + message = "Generating flatbuffer files for test_flatbuffers_fbs", + tools = ["@flatbuffers//:flatc"], +) + +cc_library( + name = "test_flatbuffers_cc_fbs", + srcs = [":test_flatbuffers_fbs"], + hdrs = [":test_flatbuffers_fbs"], + features = ["-parse_headers"], + deps = ["@flatbuffers//:runtime_cc"], +) + cc_library( name = "type_support", srcs = ["internal/type_support.cc"], diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt index d6f69759..78f578b1 100644 --- a/fuzztest/CMakeLists.txt +++ b/fuzztest/CMakeLists.txt @@ -56,6 +56,37 @@ fuzztest_cc_library( fuzztest::fuzztest_macros ) +if (FUZZTEST_BUILD_FLATBUFFERS) + fuzztest_cc_library( + NAME + fuzztest_flatbuffers + HDRS + "flatbuffers.h" + DEPS + fuzztest::any + fuzztest::domain_core + fuzztest::logging + fuzztest::meta + fuzztest::serialization + fuzztest::status + fuzztest::type_support + absl::container + absl::core_headers + absl::nullability + absl::flat_hash_map + absl::flat_hash_set + absl::random + absl::bit_gen_ref + absl::distributions + absl::status + absl::statusor + absl::strings + absl::str_format + absl::synchronization + flatbuffers + ) +endif() + fuzztest_cc_library( NAME fuzztest_macros @@ -804,6 +835,70 @@ if (FUZZTEST_BUILD_TESTING) "${CMAKE_CURRENT_BINARY_DIR}/.." ) + if (FUZZTEST_BUILD_FLATBUFFERS) + # Generate test flatbuffers + include_directories(${FLATBUFFERS_INCLUDE_DIR}) + set(FBS_SCHEMA_FILE "${CMAKE_CURRENT_LIST_DIR}/internal/test_flatbuffers.fbs") + set(FLATC_FLAGS "--bfbs-gen-embed" "--gen-name-strings") + + # Modified version of `flatbuffers_generate_headers` + # from https://github.com/google/flatbuffers/blob/master/CMake/BuildFlatBuffers.cmake + # Supports using an output prefix for single file header generation as well + # as the embedded schema header in the output set. + add_custom_command( + OUTPUT + "internal/test_flatbuffers_bfbs_generated.h" + "internal/test_flatbuffers_generated.h" + COMMAND + $ + -o "${CMAKE_CURRENT_BINARY_DIR}/internal" + -c + ${FBS_SCHEMA_FILE} + ${FLATC_FLAGS} + DEPENDS + flatc + ${FBS_SCHEMA_FILE} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building ${FBS_SCHEMA_FILE} flatbuffers..." + ) + + # Create an additional target as add_custom_command scope is only within + # same directory (CMakeFile.txt) + add_custom_target( + GENERATE_test_flatbuffers ALL + DEPENDS + "internal/test_flatbuffers_bfbs_generated.h" + "internal/test_flatbuffers_generated.h" + COMMENT "Generating flatbuffer target test_flatbuffers" + ) + + # Set up interface library + add_library(test_flatbuffers INTERFACE) + add_dependencies( + test_flatbuffers + flatc + ${FBS_SCHEMA_FILE} + ) + target_include_directories( + test_flatbuffers + INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/internal" + ) + + # Organize file layout for IDEs. + source_group( + TREE "${CMAKE_CURRENT_BINARY_DIR}/internal" + PREFIX "Flatbuffers/Generated/Headers Files" + FILES + "${CMAKE_CURRENT_BINARY_DIR}/internal/test_flatbuffers_bfbs_generated.h" + "${CMAKE_CURRENT_BINARY_DIR}/internal/test_flatbuffers_generated.h" + ) + source_group( + TREE "${CMAKE_CURRENT_SOURCE_DIR}/internal" + PREFIX "Flatbuffers/Schemas" + FILES ${FBS_SCHEMA_FILE} + ) + endif() + endif () fuzztest_cc_library( diff --git a/fuzztest/flatbuffers.h b/fuzztest/flatbuffers.h new file mode 100644 index 00000000..b70ed361 --- /dev/null +++ b/fuzztest/flatbuffers.h @@ -0,0 +1,19 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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. + +#ifndef FUZZTEST_FUZZTEST_FLATBUFFERS_H_ +#define FUZZTEST_FUZZTEST_FLATBUFFERS_H_ + +#include "./fuzztest/internal/domains/flatbuffers_domain_impl.h" // IWYU pragma: export +#endif // FUZZTEST_FUZZTEST_FLATBUFFERS_H_ diff --git a/fuzztest/internal/domains/flatbuffers_domain_impl.h b/fuzztest/internal/domains/flatbuffers_domain_impl.h new file mode 100644 index 00000000..6c1826b3 --- /dev/null +++ b/fuzztest/internal/domains/flatbuffers_domain_impl.h @@ -0,0 +1,751 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// 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. + +#ifndef FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_FLATBUFFERS_DOMAIN_IMPL_H_ +#define FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_FLATBUFFERS_DOMAIN_IMPL_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/base/nullability.h" +#include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_map.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/synchronization/mutex.h" +#include "flatbuffers/base.h" +#include "flatbuffers/flatbuffer_builder.h" +#include "flatbuffers/reflection.h" +#include "flatbuffers/reflection_generated.h" +#include "flatbuffers/string.h" +#include "flatbuffers/table.h" +#include "flatbuffers/verifier.h" +#include "./fuzztest/domain_core.h" +#include "./fuzztest/internal/any.h" +#include "./fuzztest/internal/domains/arbitrary_impl.h" +#include "./fuzztest/internal/domains/domain_base.h" +#include "./fuzztest/internal/domains/domain_type_erasure.h" +#include "./fuzztest/internal/domains/element_of_impl.h" +#include "./fuzztest/internal/logging.h" +#include "./fuzztest/internal/meta.h" +#include "./fuzztest/internal/serialization.h" +#include "./fuzztest/internal/status.h" + +namespace fuzztest::internal { + +template && + !std::is_same_v>> + +// +// Flatbuffers enum detection. +// +struct FlatbuffersEnumTag { + using type = Underlying; +}; + +template +struct is_flatbuffers_enum_tag : std::false_type {}; + +template +struct is_flatbuffers_enum_tag> + : std::true_type {}; + +template +inline constexpr bool is_flatbuffers_enum_tag_v = + is_flatbuffers_enum_tag::value; + +struct FlatbuffersArrayTag; +struct FlatbuffersObjTag; +struct FlatbuffersUnionTag; +struct FlatbuffersVectorTag; + +// Dynamic to static dispatch visitor pattern. +template +auto VisitFlatbufferField(const Field* absl_nonnull field, Visitor visitor) { + auto field_index = field->type()->index(); + switch (field->type()->base_type()) { + case reflection::BaseType::Bool: + visitor.template Visit(field); + break; + case reflection::BaseType::Byte: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::Short: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::Int: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::Long: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::UByte: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::UShort: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::UInt: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::ULong: + if (field_index >= 0) { + visitor.template Visit>(field); + } else { + visitor.template Visit(field); + } + break; + case reflection::BaseType::Float: + visitor.template Visit(field); + break; + case reflection::BaseType::Double: + visitor.template Visit(field); + break; + case reflection::BaseType::String: + visitor.template Visit(field); + break; + case reflection::BaseType::Vector: + case reflection::BaseType::Vector64: + visitor.template Visit(field); + break; + case reflection::BaseType::Array: + visitor.template Visit(field); + break; + case reflection::BaseType::Obj: + visitor.template Visit(field); + break; + case reflection::BaseType::Union: + visitor.template Visit(field); + break; + default: + FUZZTEST_INTERNAL_CHECK(false, "Unsupported base type"); + } +} + +// Domain implementation for flatbuffers generated table classes. +// It requires the corresponding reflection schema to be passed in the +// constructor. +// The Generated table class is the class that is generated by the flatbuffers +// compiler, and is used to access the fields of the table. +// The corpus type is a pair of: +// - A map of field ids to field values. +// - The serialized buffer of the table. +template +class FlatbuffersTableImpl + : public domain_implementor::DomainBase< + // Derived, for CRTP needs. See DomainBase for more details. + FlatbuffersTableImpl, + // ValueType - user facing type + const Generated*, + // CorpusType - internal representation of ValueType, + // a pair with a map of field ids to field values and a serialized + // buffer of the table. + std::pair< + absl::flat_hash_map< + // a.k.a. uint16_t + decltype(static_cast(nullptr)->id()), + // Fancy wrapper around `void*`: knows about the exact type of + // stored value and can copy it using exact type copy + // constructor via `CopyFrom` method. + GenericDomainCorpusType>, + std::vector>> { + public: + static_assert(Requires( + [](auto) -> decltype(Generated::BinarySchema::data()) {}), + "The flatbuffers generated class must be generated with the " + "`--bfbs-gen-embed` flag."); + static_assert(Requires( + [](auto) -> decltype(Generated::GetFullyQualifiedName()) { + }), + "The flatbuffers generated class must be generated with the " + "`--gen-name-strings` flag."); + + using typename FlatbuffersTableImpl::DomainBase::corpus_type; + using typename FlatbuffersTableImpl::DomainBase::value_type; + using FieldIdT = typename corpus_type::first_type::key_type; + + FlatbuffersTableImpl() { + flatbuffers::Verifier verifier(Generated::BinarySchema::data(), + Generated::BinarySchema::size()); + FUZZTEST_INTERNAL_CHECK(reflection::VerifySchemaBuffer(verifier), + "Invalid schema for flatbuffers table."); + schema_ = reflection::GetSchema(Generated::BinarySchema::data()); + table_object_ = + schema_->objects()->LookupByKey(Generated::GetFullyQualifiedName()); + } + + FlatbuffersTableImpl(const FlatbuffersTableImpl& other) + : schema_(other.schema_), table_object_(other.table_object_) { + absl::MutexLock l_other(&other.mutex_); + absl::MutexLock l_this(&mutex_); + domains_ = other.domains_; + } + + FlatbuffersTableImpl& operator=(const FlatbuffersTableImpl& other) { + schema_ = other.schema_; + table_object_ = other.table_object_; + absl::MutexLock l_other(&other.mutex_); + absl::MutexLock l_this(&mutex_); + domains_ = other.domains_; + return *this; + } + + FlatbuffersTableImpl(FlatbuffersTableImpl&& other) + : schema_(other.schema_), table_object_(other.table_object_) { + absl::MutexLock l_other(&other.mutex_); + absl::MutexLock l_this(&mutex_); + domains_ = std::move(other.domains_); + } + + FlatbuffersTableImpl& operator=(FlatbuffersTableImpl&& other) { + schema_ = other.schema_; + table_object_ = other.table_object_; + absl::MutexLock l_other(&other.mutex_); + absl::MutexLock l_this(&mutex_); + domains_ = std::move(other.domains_); + return *this; + } + + // Initializes the corpus value. + corpus_type Init(absl::BitGenRef prng) { + if (auto seed = this->MaybeGetRandomSeed(prng)) { + return *seed; + } + corpus_type val; + for (const auto* field : *table_object_->fields()) { + VisitFlatbufferField(field, InitializeVisitor{*this, prng, val}); + } + UpdateCorpusBuffer(val); + return val; + } + + // Mutates the corpus value. + void Mutate(corpus_type& val, absl::BitGenRef prng, + const domain_implementor::MutationMetadata& metadata, + bool only_shrink) { + using FieldsSizeT = decltype(table_object_->fields()->size()); + std::vector supported_fields; + for (FieldsSizeT i = 0; i < table_object_->fields()->size(); ++i) { + auto field = table_object_->fields()->Get(i); + if (IsTypeSupported(field->type()->base_type())) { + supported_fields.push_back(i); + } + } + if (supported_fields.empty()) return; + + const FieldIdT selected_field_index = + absl::Uniform(prng, 0ul, supported_fields.size()); + + MutateSelectedField(val, prng, metadata, only_shrink, + supported_fields[selected_field_index]); + } + + // Mutates the selected field. + // The selected field index is based on the flattened tree. + uint64_t MutateSelectedField( + corpus_type& val, absl::BitGenRef prng, + const domain_implementor::MutationMetadata& metadata, bool only_shrink, + FieldIdT selected_field_index) { + const reflection::Field* field = + table_object_->fields()->Get(selected_field_index); + VisitFlatbufferField( + field, MutateVisitor{*this, prng, metadata, only_shrink, val}); + UpdateCorpusBuffer(val); + return 1; + } + + auto GetPrinter() const { return Printer{*this}; } + + absl::Status ValidateCorpusValue(const corpus_type& corpus_value) const { + for (const auto& [id, field_corpus] : corpus_value.first) { + const reflection::Field* absl_nullable field = GetFieldById(id); + if (field == nullptr) continue; + absl::Status result; + VisitFlatbufferField(field, ValidateVisitor{*this, field_corpus, result}); + if (!result.ok()) return result; + } + return absl::OkStatus(); + } + + value_type GetValue(const corpus_type& value) const { + return flatbuffers::GetRoot(value.second.data()); + } + + // Converts the table pointer to a corpus value. + std::optional FromValue(const value_type& value) const { + using FieldsSizeT = decltype(table_object_->fields()->size()); + if (value == nullptr) { + return std::nullopt; + } + corpus_type ret; + for (FieldsSizeT i = 0; i < table_object_->fields()->size(); ++i) { + const auto* field = table_object_->fields()->Get(i); + VisitFlatbufferField(field, FromValueVisitor{*this, value, ret}); + } + UpdateCorpusBuffer(ret); + return ret; + } + + // Converts the IRObject to a corpus value. + std::optional ParseCorpus(const IRObject& obj) const { + corpus_type out; + auto subs = obj.Subs(); + if (!subs) { + return std::nullopt; + } + // Follows the structure created by `SerializeCorpus` to deserialize the + // IRObject. + + // subs->size() represents the number of fields in the table. + out.first.reserve(subs->size()); + for (const auto& sub : *subs) { + auto pair_subs = sub.Subs(); + // Each field is represented by a pair of field id and the serialized + // corpus value. + if (!pair_subs || pair_subs->size() != 2) { + return std::nullopt; + } + + // Deserialize the field id. + auto id = (*pair_subs)[0].GetScalar(); + if (!id.has_value()) { + return std::nullopt; + } + + // Get information about the field from reflection. + const reflection::Field* absl_nullable field = GetFieldById(id.value()); + if (field == nullptr) { + return std::nullopt; + } + + // Deserialize the field corpus value. + std::optional inner_parsed; + VisitFlatbufferField(field, + ParseVisitor{*this, (*pair_subs)[1], inner_parsed}); + if (!inner_parsed) { + return std::nullopt; + } + out.first[id.value()] = *std::move(inner_parsed); + } + + UpdateCorpusBuffer(out); + return out; + } + + // Converts the corpus value to an IRObject. + IRObject SerializeCorpus(const corpus_type& value) const { + IRObject out; + auto& subs = out.MutableSubs(); + subs.reserve(value.first.size()); + + // Each field is represented by a pair of field id and the serialized + // corpus value. + for (const auto& [id, field_corpus] : value.first) { + // Get information about the field from reflection. + const reflection::Field* absl_nullable field = GetFieldById(id); + if (field == nullptr) { + continue; + } + IRObject& pair = subs.emplace_back(); + auto& pair_subs = pair.MutableSubs(); + pair_subs.reserve(2); + + // Serialize the field id. + pair_subs.emplace_back(field->id()); + + // Serialize the field corpus value. + VisitFlatbufferField(field, SerializeVisitor{*this, field_corpus, + pair_subs.emplace_back()}); + } + return out; + } + + // Returns the domain for the given field. + // The domain is cached, and the same instance is returned for the same + // field. + template + auto& GetSubDomain(const reflection::Field* absl_nonnull field) const { + using DomainT = decltype(GetDomainForField(field)); + // Do the operation under a lock to prevent race conditions in `const` + // methods. + absl::MutexLock l(&mutex_); + auto it = domains_.find(field->id()); + if (it == domains_.end()) { + it = domains_ + .try_emplace(field->id(), std::in_place_type, + GetDomainForField(field)) + .first; + } + return it->second.template GetAs(); + } + + // Returns the domain for the given field. + template + auto GetDomainForField(const reflection::Field* absl_nonnull field) const { + if constexpr (std::is_same_v) { + FUZZTEST_INTERNAL_CHECK( + false, "Array in tables are not supported in flatbuffers."); + // Return a placeholder domain to make the compiler happy. + return Domain>(Arbitrary>()); + } else if constexpr (is_flatbuffers_enum_tag_v) { + auto enum_object = schema_->enums()->Get(field->type()->index()); + // For enums, build the list of valid labels. + std::vector values; + values.reserve(enum_object->values()->size()); + for (const auto* value : *enum_object->values()) { + values.push_back(value->value()); + } + // Delay instantiation. The Domain class is not fully defined at this + // point yet, and neither is ElementOfImpl. + using LazyInt = MakeDependentType; + auto domain = OptionalOf(ElementOfImpl(std::move(values))); + if (!field->optional()) { + domain.SetWithoutNull(); + } + return Domain>{domain}; + } else if constexpr (std::is_same_v) { + // TODO(b/399123660): Implement this. + return Domain>(OptionalOf(ArbitraryImpl())); + } else if constexpr (std::is_same_v) { + // TODO(b/399123660): Implement this. + return Domain>(OptionalOf(ArbitraryImpl())); + } else if constexpr (std::is_same_v) { + // TODO(b/399123660): Implement this. + return Domain>(OptionalOf(ArbitraryImpl())); + } else { + auto domain = OptionalOf(ArbitraryImpl()); + if (!field->optional()) { + domain.SetWithoutNull(); + } + return Domain>{domain}; + } + } + + private: + const reflection::Schema* absl_nonnull schema_; + const reflection::Object* absl_nonnull table_object_; + mutable absl::Mutex mutex_; + mutable absl::flat_hash_map domains_ + ABSL_GUARDED_BY(mutex_); + + const reflection::Field* absl_nullable GetFieldById(FieldIdT id) const { + const auto it = + absl::c_find_if(*table_object_->fields(), + [id](const auto* field) { return field->id() == id; }); + return it != table_object_->fields()->end() ? *it : nullptr; + } + + bool IsTypeSupported(reflection::BaseType base_type) const { + return flatbuffers::IsScalar(base_type) || + base_type == reflection::BaseType::String; + } + + void UpdateCorpusBuffer(corpus_type& value) const { + // TODO(b/399123660): The builder may come from a parent domain in case this + // is a nested table. + flatbuffers::FlatBufferBuilder builder; + + // Add all the fields to the builder and track their offsets. + std::unordered_map offsets; + for (const auto& [id, field_corpus] : value.first) { + const reflection::Field* absl_nullable field = GetFieldById(id); + if (field == nullptr) { + continue; + } + VisitFlatbufferField(field, TableFieldBuilderVisitor{ + *this, builder, offsets, field_corpus}); + } + // Build the table. + auto table_start = builder.StartTable(); + for (const auto& [id, field_corpus] : value.first) { + const reflection::Field* absl_nullable field = GetFieldById(id); + if (field == nullptr) { + continue; + } + VisitFlatbufferField( + field, TableBuilderVisitor{*this, builder, offsets, field_corpus}); + } + auto table_offset = builder.EndTable(table_start); + builder.Finish(table_offset, nullptr); + + // copy the buffer to the corpus value. + value.second = + std::vector(builder.GetBufferPointer(), + builder.GetBufferPointer() + builder.GetSize()); + } + + struct SerializeVisitor { + const FlatbuffersTableImpl& self; + const GenericDomainCorpusType& corpus_value; + IRObject& out; + + template + void Visit(const reflection::Field* absl_nonnull field) { + out = self.GetSubDomain(field).SerializeCorpus(corpus_value); + } + }; + + struct FromValueVisitor { + const FlatbuffersTableImpl& self; + value_type value; + corpus_type& out; + + template + void Visit(const reflection::Field* absl_nonnull field) const { + const flatbuffers::Table* table = (const flatbuffers::Table*)(value); + [[maybe_unused]] + reflection::BaseType base_type = field->type()->base_type(); + auto& domain = self.GetSubDomain(field); + value_type_t> inner_value; + + if constexpr (is_flatbuffers_enum_tag_v) { + assert(base_type >= reflection::BaseType::Byte && + base_type <= reflection::BaseType::ULong); + if (field->optional() && !table->CheckField(field->offset())) { + inner_value = std::nullopt; + } else { + inner_value = std::make_optional(table->GetField( + field->offset(), field->default_integer())); + } + } else if constexpr (std::is_integral_v) { + assert(base_type >= reflection::BaseType::Bool && + base_type <= reflection::BaseType::ULong); + if (field->optional() && !table->CheckField(field->offset())) { + inner_value = std::nullopt; + } else { + inner_value = std::make_optional( + table->GetField(field->offset(), field->default_integer())); + } + } else if constexpr (std::is_floating_point_v) { + assert(base_type >= reflection::BaseType::Float && + base_type <= reflection::BaseType::Double); + if (field->optional() && !table->CheckField(field->offset())) { + inner_value = std::nullopt; + } else { + inner_value = std::make_optional( + table->GetField(field->offset(), field->default_real())); + } + } else if constexpr (std::is_same_v) { + assert(base_type == reflection::BaseType::String); + if (!table->CheckField(field->offset())) { + inner_value = std::nullopt; + } else { + inner_value = std::make_optional( + table->GetPointer(field->offset())->str()); + } + } + + auto inner = domain.FromValue(inner_value); + if (inner) { + out.first[field->id()] = *std::move(inner); + } + }; + }; + + // Create out-of-line table fields, see `UpdateCorpusBuffer` for details. + struct TableFieldBuilderVisitor { + const FlatbuffersTableImpl& self; + flatbuffers::FlatBufferBuilder& builder; + std::unordered_map& offsets; + const typename corpus_type::first_type::value_type::second_type& + corpus_value; + + template + void Visit(const reflection::Field* absl_nonnull field) const { + if constexpr (std::is_same_v) { + auto& domain = self.GetSubDomain(field); + auto user_value = domain.GetValue(corpus_value); + if (user_value.has_value()) { + auto offset = + builder.CreateString(user_value->data(), user_value->size()).o; + offsets.insert({field->id(), offset}); + } + } + } + }; + + // Create complete table: store "inline fields" values inline, and store just + // offsets for "out-of-line fields". See `UpdateCorpusBuffer` for details. + struct TableBuilderVisitor { + const FlatbuffersTableImpl& self; + flatbuffers::FlatBufferBuilder& builder; + const std::unordered_map& offsets; + const typename corpus_type::first_type::value_type::second_type& + corpus_value; + + template + void Visit(const reflection::Field* absl_nonnull field) const { + auto size = flatbuffers::GetTypeSize(field->type()->base_type()); + if constexpr (std::is_integral_v || std::is_floating_point_v || + is_flatbuffers_enum_tag_v) { + auto& domain = self.GetSubDomain(field); + auto v = domain.GetValue(corpus_value); + if (!v) { + return; + } + // Store "inline field" value inline. + builder.Align(size); + builder.PushBytes(reinterpret_cast(&v), size); + builder.TrackField(field->offset(), builder.GetSize()); + } else if constexpr (std::is_same_v) { + // "Out-of-line field". Store just offset. + if (auto it = offsets.find(field->id()); it != offsets.end()) { + builder.AddOffset(field->offset(), flatbuffers::Offset<>(it->second)); + } + } + } + }; + + struct ParseVisitor { + const FlatbuffersTableImpl& self; + const IRObject& obj; + std::optional& out; + + template + void Visit(const reflection::Field* absl_nonnull field) { + out = self.GetSubDomain(field).ParseCorpus(obj); + } + }; + + struct ValidateVisitor { + const FlatbuffersTableImpl& self; + const GenericDomainCorpusType& corpus_value; + absl::Status& out; + + template + void Visit(const reflection::Field* absl_nonnull field) { + auto& domain = self.GetSubDomain(field); + out = domain.ValidateCorpusValue(corpus_value); + if (!out.ok()) { + out = Prefix(out, absl::StrCat("Invalid value for field ", + field->name()->str())); + } + } + }; + + struct InitializeVisitor { + FlatbuffersTableImpl& self; + absl::BitGenRef prng; + corpus_type& val; + + template + void Visit(const reflection::Field* absl_nonnull field) { + auto& domain = self.GetSubDomain(field); + val.first[field->id()] = domain.Init(prng); + } + }; + + struct MutateVisitor { + FlatbuffersTableImpl& self; + absl::BitGenRef prng; + const domain_implementor::MutationMetadata& metadata; + bool only_shrink; + corpus_type& val; + + template + void Visit(const reflection::Field* absl_nonnull field) { + auto& domain = self.GetSubDomain(field); + if (auto it = val.first.find(field->id()); it != val.first.end()) { + domain.Mutate(it->second, prng, metadata, only_shrink); + } else if (!only_shrink) { + val.first[field->id()] = domain.Init(prng); + } + } + }; + + struct Printer { + const FlatbuffersTableImpl& self; + + void PrintCorpusValue(const corpus_type& value, + domain_implementor::RawSink out, + domain_implementor::PrintMode mode) const { + absl::Format(out, "{"); + bool first = true; + for (const auto& [id, field_corpus] : value.first) { + if (!first) { + absl::Format(out, ", "); + } + const reflection::Field* absl_nullable field = self.GetFieldById(id); + if (field == nullptr) { + absl::Format(out, "", id); + } else { + VisitFlatbufferField(field, + PrinterVisitor{self, field_corpus, out, mode}); + } + first = false; + } + absl::Format(out, "}"); + } + }; + + struct PrinterVisitor { + const FlatbuffersTableImpl& self; + const GenericDomainCorpusType& val; + domain_implementor::RawSink out; + domain_implementor::PrintMode mode; + + template + void Visit(const reflection::Field* absl_nonnull field) const { + auto& domain = self.GetSubDomain(field); + absl::Format(out, "%s: ", field->name()->str()); + domain_implementor::PrintValue(domain, val, out, mode); + } + }; +}; + +template +class ArbitraryImpl>> + : public FlatbuffersTableImpl {}; +} // namespace fuzztest::internal +#endif // FUZZTEST_FUZZTEST_INTERNAL_DOMAINS_FLATBUFFERS_DOMAIN_IMPL_H_ diff --git a/fuzztest/internal/meta.h b/fuzztest/internal/meta.h index 4ddada10..c36d80b9 100644 --- a/fuzztest/internal/meta.h +++ b/fuzztest/internal/meta.h @@ -200,6 +200,22 @@ template inline constexpr bool is_protocol_buffer_enum_v = IsProtocolBufferEnumImpl(true); +template +inline constexpr bool is_flatbuffers_table_v = false; + +// Flatbuffers tables generated structs do not have a public base class, so we +// check for a few specific methods: +// - T is a struct. +// - T has a `Builder` type. +// - T has a `BinarySchema` type with a static method `data()` (only available +// when passing `--bfbs-gen-embed` to the flatbuffer compiler). +// - T has a static method called `GetFullyQualifiedName` (only available when +// passing `--gen-name-strings` to the flatbuffer compiler). +template +inline constexpr bool is_flatbuffers_table_v< + T, std::void_t> = true; + template inline constexpr bool has_size_v = Requires([](auto v) -> decltype(v.size()) {}); diff --git a/fuzztest/internal/test_flatbuffers.fbs b/fuzztest/internal/test_flatbuffers.fbs new file mode 100644 index 00000000..db56a187 --- /dev/null +++ b/fuzztest/internal/test_flatbuffers.fbs @@ -0,0 +1,45 @@ +namespace fuzztest.internal; + +enum TestFbsEnum: byte { + First, + Second, + Third +} + +table DefaultTestFbsTable { + b: bool; + i8: byte; + i16: short; + i32: int; + i64: long; + u8: ubyte; + u16: ushort; + u32: uint; + u64: ulong; + f: float; + d: double; + str: string; + e: TestFbsEnum; +} + +table OptionalTestFbsTable { + b: bool = null; + i8: byte = null; + i16: short = null; + i32: int = null; + i64: long = null; + u8: ubyte = null; + u16: ushort = null; + u32: uint = null; + u64: ulong = null; + f: float = null; + d: double = null; + str: string; + e: TestFbsEnum = null; +} + +table RequiredTestFbsTable { + str: string (required); +} + +root_type DefaultTestFbsTable;