diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 78ad9b2e6..e93f1547a 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -71,7 +71,7 @@ jobs: if: matrix.bazel-config == 'bl-x86_64-linux' run: | # Test that baselibs works when consumed as an external module dependency - cd examples/integration && bazel build //... && bazel mod deps --lockfile_mode=update + cd examples/integration && bazel build //... && bazel test //... && bazel mod deps --lockfile_mode=update - name: Upload test logs on failure if: failure() uses: actions/upload-artifact@v7 diff --git a/MODULE.bazel b/MODULE.bazel index a17f66996..e056e3e52 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -133,7 +133,7 @@ single_version_override( ## Configure the python toolchain -bazel_dep(name = "rules_python", version = "1.8.3", dev_dependency = True) +bazel_dep(name = "rules_python", version = "1.8.3") PYTHON_VERSION = "3.12" diff --git a/docs/baselibs/components/flatbuffers/docs/architecture/index.rst b/docs/baselibs/components/flatbuffers/docs/architecture/index.rst index 14b8d5ecf..af1902fdc 100644 --- a/docs/baselibs/components/flatbuffers/docs/architecture/index.rst +++ b/docs/baselibs/components/flatbuffers/docs/architecture/index.rst @@ -21,5 +21,5 @@ FlatBuffers Component Architecture :id: comp__baselibs_flatbuffers :security: YES :safety: ASIL_B - :status: invalid + :status: valid :belongs_to: feat__baselibs diff --git a/docs/baselibs/components/flatbuffers/docs/requirements/index.rst b/docs/baselibs/components/flatbuffers/docs/requirements/index.rst index 6ed93f4e8..a1848c447 100644 --- a/docs/baselibs/components/flatbuffers/docs/requirements/index.rst +++ b/docs/baselibs/components/flatbuffers/docs/requirements/index.rst @@ -163,6 +163,7 @@ Buffer Identification and Versioning :safety: ASIL_B :derived_from: feat_req__baselibs__flatbuffers_library, feat_req__baselibs__safety :status: valid + :testcovered: YES :satisfied_by: comp__baselibs_flatbuffers The FlatBuffers-Library shall provide a common opt-in buffer identification mechanism consisting @@ -175,6 +176,7 @@ Buffer Identification and Versioning :safety: ASIL_B :derived_from: feat_req__baselibs__flatbuffers_library, feat_req__baselibs__safety :status: valid + :testcovered: YES :satisfied_by: comp__baselibs_flatbuffers The FlatBuffers-Library shall provide a common opt-in version check mechanism that validates diff --git a/examples/integration/MODULE.bazel b/examples/integration/MODULE.bazel index 08cb4b295..d3803cd17 100644 --- a/examples/integration/MODULE.bazel +++ b/examples/integration/MODULE.bazel @@ -19,6 +19,8 @@ local_path_override( bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "googletest", version = "1.17.0.bcr.2", dev_dependency = True) + # Upgrade grpc-java to 1.70.0 for compatibility with newer grpc (1.70.1). # The transitive grpc dependency (via flatbuffers) resolves to grpc 1.70.1, which removed # the com_envoyproxy_protoc_gen_validate repo that grpc-java@1.66.0 requires. diff --git a/score/flatbuffers/examples/config_usecase/BUILD b/examples/integration/flatbuffers/config_usecase/BUILD similarity index 54% rename from score/flatbuffers/examples/config_usecase/BUILD rename to examples/integration/flatbuffers/config_usecase/BUILD index ad8eb43b8..cce78176e 100644 --- a/score/flatbuffers/examples/config_usecase/BUILD +++ b/examples/integration/flatbuffers/config_usecase/BUILD @@ -13,7 +13,7 @@ load("@rules_cc//cc:defs.bzl", "cc_test") load("@score_baselibs//score/flatbuffers/bazel:codegen.bzl", "generate_cpp") -load("@score_baselibs//score/flatbuffers/bazel:tools.bzl", "generate_json_schema", "serialize_buffer") +load("@score_baselibs//score/flatbuffers/bazel:tools.bzl", "generate_json_schema", "serialize_buffer", "serialize_versioned_buffer") # Generate C++ header from FlatBuffer schema generate_cpp( @@ -37,19 +37,52 @@ serialize_buffer( schema = "demo.fbs", ) -# Demo test: loads demo_config.bin at runtime, verifies and reads the config -# using filesystem and flatbufferscpp library. +# Demo test: loads demo_config.bin at runtime, verifies and reads the config. cc_test( - name = "demo_app", + name = "demo_config", + size = "small", srcs = [ - "demo_app_test.cpp", + "demo_config_test.cpp", ":demo_component_config", ], data = [":demo_config_bin"], deps = [ "@googletest//:gtest_main", - "@score_baselibs//score/filesystem", - "@score_baselibs//score/flatbuffers:flatbufferscpp", + "@score_baselibs//score/flatbuffers:flatbufferutils", + ], +) + +# Generate C++ header from the versioned FlatBuffer schema, which includes +# buffer_version.fbs and defines version_info at id: 0 in the root table. +generate_cpp( + name = "demo_component_config_versioned", + output = "component_config_versioned.h", + schema = "demo_versioned.fbs", +) + +# Generate binary config with injected version (major=1, minor=0) from the +# same JSON data as the unversioned demo. +serialize_versioned_buffer( + name = "demo_config_versioned_bin", + buffer_major_version = 1, + buffer_minor_version = 0, + data = "demo_config.json", + output = "demo_config_versioned.bin", + schema = "demo_versioned.fbs", +) + +# Demo versioned test: loads demo_config_versioned.bin, calls VerifyVersion +# before accessing the data, then reads and asserts config values. +cc_test( + name = "demo_config_versioned", + size = "small", + srcs = [ + "demo_config_versioned_test.cpp", + ":demo_component_config_versioned", + ], + data = [":demo_config_versioned_bin"], + deps = [ + "@googletest//:gtest_main", "@score_baselibs//score/flatbuffers:flatbufferutils", ], ) diff --git a/score/flatbuffers/examples/config_usecase/demo.fbs b/examples/integration/flatbuffers/config_usecase/demo.fbs similarity index 100% rename from score/flatbuffers/examples/config_usecase/demo.fbs rename to examples/integration/flatbuffers/config_usecase/demo.fbs diff --git a/score/flatbuffers/examples/config_usecase/demo_config.json b/examples/integration/flatbuffers/config_usecase/demo_config.json similarity index 100% rename from score/flatbuffers/examples/config_usecase/demo_config.json rename to examples/integration/flatbuffers/config_usecase/demo_config.json diff --git a/score/flatbuffers/examples/config_usecase/demo_app_test.cpp b/examples/integration/flatbuffers/config_usecase/demo_config_test.cpp similarity index 77% rename from score/flatbuffers/examples/config_usecase/demo_app_test.cpp rename to examples/integration/flatbuffers/config_usecase/demo_config_test.cpp index c82bd714e..5722fcfa2 100644 --- a/score/flatbuffers/examples/config_usecase/demo_app_test.cpp +++ b/examples/integration/flatbuffers/config_usecase/demo_config_test.cpp @@ -11,18 +11,13 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// @file demo_app_test.cpp -/// @brief Demonstrates loading and reading a FlatBuffer binary config -/// using flatbufferscpp (FlatBuffers C++ API) -/// -/// Build and run via Bazel: -/// bazel test //demo_config_usecase:demo_app +/// @file demo_config_test.cpp +/// @brief Demonstrates loading of a FlatBuffer binary config and reading the config fields. #include "score/flatbuffers/load_buffer.hpp" -// Generated C++ accessor header produced by generate_cpp() – depends on flatbufferscpp -#include "score/flatbuffers/examples/config_usecase/component_config.h" +// Generated C++ accessor header produced by generate_cpp() for demo.fbs +#include "flatbuffers/config_usecase/component_config.h" -// FlatBuffers verifier (part of flatbufferscpp / @flatbuffers headers) #include "flatbuffers/verifier.h" #include @@ -35,20 +30,20 @@ TEST(DemoAppTest, LoadsAndVerifiesBuffer) // ------------------------------------------------------------------------- // Step 1: Load the binary FlatBuffer file from disk (standard file I/O) // ------------------------------------------------------------------------- - const std::string_view bin_path = "score/flatbuffers/examples/config_usecase/demo_config.bin"; + const std::string_view bin_path = "flatbuffers/config_usecase/demo_config.bin"; const auto buffer = score::flatbuffers::LoadBuffer(bin_path); ASSERT_TRUE(buffer.has_value()) << buffer.error().ToString(); // ------------------------------------------------------------------------- - // Step 2: Verify buffer integrity (flatbuffercpp / FlatBuffers verifier) + // Step 2: Verify structural integrity of the FlatBuffer // ------------------------------------------------------------------------- flatbuffers::Verifier verifier(buffer.value().data(), buffer.value().size()); ASSERT_TRUE(my_component::demo::VerifyMyComponentConfigBuffer(verifier)) - << "FlatBuffer verification failed for '" << bin_path << "'"; + << "FlatBuffer structural verification failed for '" << bin_path << "'"; // ------------------------------------------------------------------------- - // Step 3: Access config values via the generated flatbuffercpp API + // Step 3: Access config values via the generated APIs // ------------------------------------------------------------------------- const my_component::demo::MyComponentConfig* config = my_component::demo::GetMyComponentConfig(buffer.value().data()); diff --git a/examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp b/examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp new file mode 100644 index 000000000..10d5d7966 --- /dev/null +++ b/examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp @@ -0,0 +1,88 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file demo_config_versioned_test.cpp +/// @brief Demonstrates loading of a versioned FlatBuffer binary config, +/// verifying its version via score::flatbuffers::VerifyVersion before +/// accessing any data, and then reading the config fields. + +#include "score/flatbuffers/buffer_version_info.hpp" +#include "score/flatbuffers/load_buffer.hpp" +#include "score/flatbuffers/version_reader.hpp" +// Generated C++ accessor header produced by generate_cpp() for demo_versioned.fbs +#include "flatbuffers/config_usecase/component_config_versioned.h" + +#include "flatbuffers/verifier.h" + +#include + +#include + +TEST(DemoConfigVersionedTest, VerifiesVersionThenLoadsAndReadsBuffer) +{ + // ------------------------------------------------------------------------- + // Step 1: Load the binary FlatBuffer file from disk + // ------------------------------------------------------------------------- + const std::string_view bin_path = "flatbuffers/config_usecase/demo_config_versioned.bin"; + const auto buffer = score::flatbuffers::LoadBuffer(bin_path); + ASSERT_TRUE(buffer.has_value()) << buffer.error().ToString(); + + // ------------------------------------------------------------------------- + // Step 2: Verify buffer version before accessing any data + // + // The expected BufferVersionInfo must match: + // - file_identifier : "DEMO" (set in demo_versioned.fbs) + // - major_version : 1 (set in serialize_versioned_buffer rule) + // - minor_version : 0 (set in serialize_versioned_buffer rule) + // ------------------------------------------------------------------------- + const score::flatbuffers::BufferVersionInfo expected{"DEMO", 1U, 0U}; + // Free function variant is used for VerifyVersion. + // Use score::flatbuffers::VersionReader (implements IVersionReader) instead + // when the caller needs to inject or mock the reader. + const auto version_result = score::flatbuffers::VerifyVersion(buffer.value(), expected); + ASSERT_TRUE(version_result.has_value()) << version_result.error(); + + // ------------------------------------------------------------------------- + // Step 3: Verify structural integrity of the FlatBuffer + // ------------------------------------------------------------------------- + flatbuffers::Verifier verifier(buffer.value().data(), buffer.value().size()); + ASSERT_TRUE(my_component::demo::VerifyMyComponentConfigBuffer(verifier)) + << "FlatBuffer structural verification failed for '" << bin_path << "'"; + + // ------------------------------------------------------------------------- + // Step 4: Access config values via the generated APIs + // ------------------------------------------------------------------------- + const my_component::demo::MyComponentConfig* config = + my_component::demo::GetMyComponentConfig(buffer.value().data()); + + ASSERT_NE(config, nullptr); + EXPECT_EQ(config->component_name()->str(), "demo_component"); + EXPECT_EQ(config->component_id(), 42U); + EXPECT_EQ(config->thread_pool_size(), 4U); + EXPECT_EQ(config->max_memory_mb(), 64U); + + const my_component::demo::AdvancedSettings* adv = config->advanced_settings(); + ASSERT_NE(adv, nullptr); + EXPECT_EQ(adv->mode(), my_component::demo::OperationMode::PERIODIC); + + ASSERT_NE(adv->allowed_priorities(), nullptr); + EXPECT_EQ(adv->allowed_priorities()->size(), 2U); + EXPECT_EQ(adv->allowed_priorities()->Get(0), 1U); + EXPECT_EQ(adv->allowed_priorities()->Get(1), 2U); + + ASSERT_NE(adv->feature_flags(), nullptr); + ASSERT_EQ(adv->feature_flags()->size(), 1U); + const my_component::demo::FeatureFlag* flag = adv->feature_flags()->Get(0); + EXPECT_EQ(flag->name()->str(), "enable_logging"); + EXPECT_TRUE(flag->enabled()); +} diff --git a/examples/integration/flatbuffers/config_usecase/demo_versioned.fbs b/examples/integration/flatbuffers/config_usecase/demo_versioned.fbs new file mode 100644 index 000000000..ebe3bc757 --- /dev/null +++ b/examples/integration/flatbuffers/config_usecase/demo_versioned.fbs @@ -0,0 +1,85 @@ +/// Versioned variant of the demo component config schema. +/// +/// Follows the common buffer identification convention: +/// - includes buffer_version.fbs +/// - declares version_info:score.flatbuffers.BufferVersion as the FIRST field +/// of the root table (vtable slot 0) +/// +/// The binary buffer is produced by serialize_versioned_buffer, which injects +/// the major/minor version into the JSON data before calling flatc --binary. +/// At runtime, score::flatbuffers::VerifyVersion() reads the embedded version +/// and compares it against the caller-supplied BufferVersionInfo. +/// +/// Explicit field IDs (id: N) are used throughout this schema. +/// Benefits: field order in the .fbs source can be changed freely without +/// breaking binary compatibility with buffers produced by older schema versions, +/// because the vtable slot is determined by the id attribute, not by declaration +/// order. This makes schema evolution safer: new fields can be inserted anywhere +/// and deprecated fields can be removed without affecting existing binaries. + +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// MUST be field 0 – required by the universal buffer identification convention. + version_info:score.flatbuffers.BufferVersion (id: 0); + + /// Component identification + component_name:string (required, id: 1); + component_id:uint32 (id: 2); // Implicit default is 0 + + thread_pool_size:uint8 = 4 (id: 3); // Explicit default value + max_memory_mb:uint16 = 64 (id: 4); + + /// Nested table + advanced_settings:AdvancedSettings (required, id: 5); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode (id: 0); + + /// Vector examples + allowed_priorities:[uint8] (id: 1); // Vector of primitives + feature_flags:[FeatureFlag] (id: 2); // Vector of tables + + /// Union example: flexible data payload + /// Note: unions occupy two consecutive IDs – one for the type discriminator + /// and one for the value offset. data_type takes id: 3, data takes id: 4. + data:DataPayloadUnion (id: 4); +} + +/// Table used in vector +table FeatureFlag { + name:string (required, id: 0); + enabled:bool = false (id: 1); +} + +table StringData { + value:string (id: 0); +} + +table BinaryData { + value:[uint8] (id: 0); +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier – distinct from the unversioned "DEMO" +// so that VerifyVersion can distinguish the two buffer formats. +file_identifier "DEMO"; diff --git a/score/flatbuffers/BUILD b/score/flatbuffers/BUILD index cd7a8a3fd..28cb1ae21 100644 --- a/score/flatbuffers/BUILD +++ b/score/flatbuffers/BUILD @@ -43,15 +43,27 @@ cc_library( cc_library( name = "flatbufferutils", srcs = [ + "details/buffer_version_info.cpp", + "details/error.cpp", "details/load_buffer.cpp", "details/load_buffer_internal.hpp", + "details/version_reader.cpp", + "details/version_reader_impl.hpp", ], hdrs = [ + "buffer_version_info.hpp", + "error.hpp", + "i_version_reader.hpp", "load_buffer.hpp", + "version_reader.hpp", ], visibility = ["//visibility:public"], deps = [ + "//score/flatbuffers:flatbufferscpp", + "//score/flatbuffers/common:buffer_version_envelope", + "//score/flatbuffers/common:buffer_version_generated_bare", "@score_baselibs//score/filesystem", + "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/os:fcntl", "@score_baselibs//score/os:stat", "@score_baselibs//score/os:unistd", @@ -59,8 +71,42 @@ cc_library( ], ) +cc_test( + name = "buffer_version_info_unit_test", + size = "small", + srcs = ["details/buffer_version_info_test.cpp"], + tags = ["unit"], + deps = [ + ":flatbufferutils", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "error_unit_test", + size = "small", + srcs = ["details/error_test.cpp"], + tags = ["unit"], + deps = [ + ":flatbufferutils", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "idl_parser_unit_test", + size = "small", + srcs = ["details/idl_parser_test.cpp"], + tags = ["unit"], + deps = [ + ":flatbufferscpp", + "@googletest//:gtest_main", + ], +) + cc_test( name = "load_buffer_unit_test", + size = "small", srcs = ["details/load_buffer_test.cpp"], tags = ["unit"], deps = [ @@ -72,8 +118,21 @@ cc_test( ], ) +cc_test( + name = "version_reader_unit_test", + size = "small", + srcs = ["details/version_reader_test.cpp"], + tags = ["unit"], + deps = [ + ":flatbufferutils", + "//score/flatbuffers/common:buffer_version_envelope", + "@googletest//:gtest_main", + ], +) + cc_test( name = "load_buffer_test", + size = "small", srcs = ["test/load_buffer_test.cpp"], deps = [ ":flatbufferutils", @@ -93,3 +152,23 @@ cc_test( "@score_baselibs//score/language/safecpp/coverage_termination_handler", ], ) + +cc_test( + name = "version_reader_test", + size = "small", + srcs = ["test/version_reader_test.cpp"], + data = [ + "//score/flatbuffers/test/testdata:gen_versioned_buffer_bin", + "//score/flatbuffers/test/testdata:gen_versioned_buffer_explicit_ids_bin", + "//score/flatbuffers/test/testdata:gen_versioned_buffer_version_info_wrong_field", + "//score/flatbuffers/test/testdata:gen_versioned_buffer_version_info_wrong_field_similar_type", + "//score/flatbuffers/test/testdata:gen_versioned_max_buffer_bin", + "//score/flatbuffers/test/testdata:gen_versioned_max_range_buffer_bin", + "//score/flatbuffers/test/testdata:gen_versioned_min_buffer_bin", + "//score/flatbuffers/test/testdata:gen_versioned_min_range_buffer_bin", + ], + deps = [ + ":flatbufferutils", + "@googletest//:gtest_main", + ], +) diff --git a/score/flatbuffers/bazel/BUILD b/score/flatbuffers/bazel/BUILD index e69de29bb..34da686c2 100644 --- a/score/flatbuffers/bazel/BUILD +++ b/score/flatbuffers/bazel/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_python//python:defs.bzl", "py_binary") + +py_binary( + name = "inject_buffer_version", + srcs = ["inject_buffer_version.py"], + # "manual" excludes this target from wildcard builds (e.g. //score/...) so it is not + # built directly for a cross-compilation target platform. It is only ever built for the + # exec (host) platform when referenced as a tool via cfg = "exec" in the starlark rules. + tags = ["manual"], + visibility = ["//visibility:public"], +) diff --git a/score/flatbuffers/bazel/codegen.bzl b/score/flatbuffers/bazel/codegen.bzl index 019f8c7c8..21a1154ae 100644 --- a/score/flatbuffers/bazel/codegen.bzl +++ b/score/flatbuffers/bazel/codegen.bzl @@ -22,6 +22,10 @@ def _generate_cpp_impl(ctx): # Get the flatc compiler using absolute path from flatbuffers repository flatc = ctx.executable._flatc + # Collect include directories from included .fbs files + include_files = ctx.files.includes + [ctx.file._buffer_version_fbs] + include_dirs = {files.dirname: True for files in include_files} + # flatc generates _generated.h by default # Use temporary subdirectory based on target to avoid conflicts default_name = fbs_file.basename.replace(".fbs", "_generated.h") @@ -71,7 +75,10 @@ def _generate_cpp_impl(ctx): # # --no-includes (NOT USED) # Don't generate include statements for included schemas. - # DECISION: Not used - includes are standard practice for dependent schemas + # DECISION: Not used - suppressing the #include directives does NOT inline or generate + # the dependent types. The generated code still references types from included schemas + # (e.g. score::flatbuffers::BufferVersion) so the dependency must be satisfied by the + # consumer anyway, just without the generated header's guidance. # # --cpp-include (NOT USED) # Add custom #include in generated file (e.g., --cpp-include "my_custom_include.h"). @@ -81,11 +88,13 @@ def _generate_cpp_impl(ctx): args.add("--cpp") args.add("--cpp-std", "c++11") args.add("--scoped-enums") + for inc_dir in include_dirs: + args.add("-I", inc_dir) args.add("-o", generated_file.dirname) args.add(fbs_file.path) ctx.actions.run( - inputs = [fbs_file], + inputs = [fbs_file] + include_files, outputs = [generated_file], executable = flatc, arguments = [args], @@ -110,23 +119,59 @@ generate_cpp = rule( mandatory = True, doc = "The name of the generated C++ header file", ), + "includes": attr.label_list( + allow_files = [".fbs"], + default = [], + doc = "Additional .fbs files required to resolve include directives in the schema.", + ), "_flatc": attr.label( default = "@flatbuffers//:flatc", executable = True, cfg = "exec", doc = "The flatc compiler (absolute path from flatbuffers repository)", ), + "_buffer_version_fbs": attr.label( + default = "@score_baselibs//score/flatbuffers/common:buffer_version.fbs", + allow_single_file = [".fbs"], + doc = "Automatically included buffer_version.fbs for common buffer version support.", + ), }, doc = """Generates a C++ header file from a FlatBuffer schema (.fbs) file. - + This rule uses the flatc compiler from the @flatbuffers repository with absolute paths, making it usable outside of this repository. - + + @score_baselibs//score/flatbuffers/common:buffer_version.fbs is always included + automatically. The schema must include buffer_version.fbs manually if it uses + the common buffer version (e.g. `include "buffer_version.fbs";`). + + Included schema headers are NOT generated automatically + ------------------------------------------------------- + flatc only generates a C++ header for the schema passed directly to it. + Schemas referenced via `include` directives are used for type resolution at + compile time but their corresponding `*_generated.h` headers are NOT emitted + as side-effects of this rule. + + The generated header will still contain `#include "_generated.h"` + directives for every included schema. Those headers must therefore be made + available separately: + + * For `buffer_version.fbs` (the standard version envelope): the header is + already available via + `@score_baselibs//score/flatbuffers/common:buffer_version_generated_bare`, + which is re-exported by `@score_baselibs//score/flatbuffers:flatbufferutils`. + Any target that depends on `flatbufferutils` automatically inherits it and + no extra dep is needed. + + * For any other included schema: create a separate `generate_cpp` target for + that schema and add it to the `deps` of the consuming `cc_*` target. + Example: generate_cpp( name = "demo_flatbuffer", schema = "demo.fbs", output = "demo_config.h", + includes = ["some_other.fbs"], ) """, ) diff --git a/score/flatbuffers/bazel/inject_buffer_version.py b/score/flatbuffers/bazel/inject_buffer_version.py new file mode 100644 index 000000000..3f69ef083 --- /dev/null +++ b/score/flatbuffers/bazel/inject_buffer_version.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Patches a JSON data file with buffer version information. + +This script is invoked by e.g. serialize_versioned_buffer Bazel macro to inject +major_version and minor_version into the user's JSON data file before +flatc serialization. The version is stored as a 'version_info' object at +the top level of the JSON. + +Usage: + inject_buffer_version.py --input --output \ + --major --minor +""" + +import argparse +import json + + +def main(): + parser = argparse.ArgumentParser( + description="Patch a JSON data file with buffer version information." + ) + parser.add_argument("--input", required=True, help="Input JSON file path") + parser.add_argument("--output", required=True, help="Output patched JSON file path") + parser.add_argument( + "--major", type=int, required=True, help="Major version number (0-65535)" + ) + parser.add_argument( + "--minor", type=int, required=True, help="Minor version number (0-65535)" + ) + + args = parser.parse_args() + + _UINT16_MAX = 65535 + if not (0 <= args.major <= _UINT16_MAX): + parser.error( + f"--major {args.major} is out of uint16_t range [0, {_UINT16_MAX}]" + ) + if not (0 <= args.minor <= _UINT16_MAX): + parser.error( + f"--minor {args.minor} is out of uint16_t range [0, {_UINT16_MAX}]" + ) + + with open(args.input, "r") as f: + data = json.load(f) + + data["version_info"] = { + "major_version": args.major, + "minor_version": args.minor, + # NOTE: This value must be kept in sync with the type_marker constant in + # version_reader.cpp. Changing it here without updating version_reader.cpp + # (or vice versa) will cause buffer version validation to fail at runtime. + "type_marker": "BV", + } + + with open(args.output, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +if __name__ == "__main__": + main() diff --git a/score/flatbuffers/bazel/tools.bzl b/score/flatbuffers/bazel/tools.bzl index 4cae9d200..b6ca9935d 100644 --- a/score/flatbuffers/bazel/tools.bzl +++ b/score/flatbuffers/bazel/tools.bzl @@ -23,6 +23,10 @@ def _serialize_buffer_impl(ctx): # Get the flatc compiler using absolute path from flatbuffers repository flatc = ctx.executable._flatc + # Collect include directories from included .fbs files + include_files = ctx.files.includes + include_dirs = {files.dirname: True for files in include_files} + # When converting JSON to binary, flatc generates a file named after the JSON file default_name = data_file.basename.replace(".json", ".bin") temp_subdir = "tmp_{}".format(ctx.label.name) @@ -90,12 +94,14 @@ def _serialize_buffer_impl(ctx): args = ctx.actions.args() args.add("--binary") args.add("--strict-json") + for inc_dir in include_dirs: + args.add("-I", inc_dir) args.add("-o", generated_file.dirname) args.add(schema_file.path) args.add(data_file.path) ctx.actions.run( - inputs = [data_file, schema_file], + inputs = [data_file, schema_file] + include_files, outputs = [generated_file], executable = flatc, arguments = [args], @@ -125,6 +131,11 @@ serialize_buffer = rule( mandatory = True, doc = "The name of the generated binary data file (should end with .bin)", ), + "includes": attr.label_list( + allow_files = [".fbs"], + default = [], + doc = "Additional .fbs files required to resolve include directives in the schema.", + ), "_flatc": attr.label( default = "@flatbuffers//:flatc", executable = True, @@ -140,6 +151,7 @@ serialize_buffer = rule( data = "demo_data.json", schema = "demo.fbs", output = "demo_data.bin", + includes = ["some_other.fbs"], ) """, ) @@ -153,6 +165,10 @@ def _serialize_multiple_buffers_impl(ctx): # Get the flatc compiler using absolute path from flatbuffers repository flatc = ctx.executable._flatc + # Collect include directories from included .fbs files + include_files = ctx.files.includes + include_dirs = {files.dirname: True for files in include_files} + # Parse the data files dict and process each data file output_files = [] @@ -172,12 +188,14 @@ def _serialize_multiple_buffers_impl(ctx): args = ctx.actions.args() args.add("--binary") args.add("--strict-json") + for inc_dir in include_dirs: + args.add("-I", inc_dir) args.add("-o", generated_file.dirname) args.add(schema_file.path) args.add(data_file.path) ctx.actions.run( - inputs = [data_file, schema_file], + inputs = [data_file, schema_file] + include_files, outputs = [generated_file], executable = flatc, arguments = [args], @@ -205,6 +223,11 @@ serialize_multiple_buffers = rule( mandatory = True, doc = "A mapping of input JSON file paths to output binary buffer file paths (e.g., {':demo_data.json': 'demo_data.bin', ':subdir/demo_data2.json': 'subdir/demo_data2.bin'})", ), + "includes": attr.label_list( + allow_files = [".fbs"], + default = [], + doc = "Additional .fbs files required to resolve include directives in the schema.", + ), "_flatc": attr.label( default = "@flatbuffers//:flatc", executable = True, @@ -222,10 +245,217 @@ serialize_multiple_buffers = rule( "demo_data.json": "demo_data.bin", "subdir/demo_data2.json": "subdir/demo_data2.bin", }, + includes = ["some_other.fbs"], ) """, ) +def _inject_buffer_version_impl(ctx): + """Implementation of the _inject_buffer_version rule. + + Runs inject_buffer_version.py to inject major/minor version fields into a JSON file. + The output is a patched copy of the input JSON, suitable for passing to + serialize_buffer as the data source. + """ + data_file = ctx.file.data + patched_json = ctx.actions.declare_file(ctx.attr.output) + + patch_args = ctx.actions.args() + patch_args.add("--input", data_file.path) + patch_args.add("--output", patched_json.path) + patch_args.add("--major", str(ctx.attr.major_version)) + patch_args.add("--minor", str(ctx.attr.minor_version)) + + ctx.actions.run( + inputs = [data_file], + outputs = [patched_json], + executable = ctx.executable._inject_buffer_version, + arguments = [patch_args], + mnemonic = "PatchBufferVersion", + progress_message = "Patching version into %s" % data_file.short_path, + ) + + return [DefaultInfo(files = depset([patched_json]))] + +_inject_buffer_version = rule( + implementation = _inject_buffer_version_impl, + attrs = { + "data": attr.label( + allow_single_file = [".json"], + mandatory = True, + doc = "The JSON data file to patch with version information.", + ), + "output": attr.string( + mandatory = True, + doc = "The name of the patched JSON output file.", + ), + "major_version": attr.int( + mandatory = True, + doc = "Major version number to inject. Must be in [0, 65535] (uint16_t).", + ), + "minor_version": attr.int( + mandatory = True, + doc = "Minor version number to inject. Must be in [0, 65535] (uint16_t).", + ), + "_inject_buffer_version": attr.label( + default = "//score/flatbuffers/bazel:inject_buffer_version", + executable = True, + cfg = "exec", + doc = "The inject_buffer_version helper script for JSON version injection.", + ), + }, + doc = "Injects major/minor version fields into a JSON file for use with serialize_versioned_buffer.", +) + +def serialize_versioned_buffer( + name, + data, + schema, + output, + buffer_major_version, + buffer_minor_version, + includes = [], + tags = [], + **kwargs): + """Injects version information into a JSON file and serializes it to a binary FlatBuffer. + + This macro composes _inject_buffer_version and serialize_buffer: it first injects the + given major/minor version into the JSON data file, then passes the patched + JSON to serialize_buffer for compilation. + + Args: + name: Target name. + data: The JSON data file label to version-patch and serialize. + schema: The .fbs FlatBuffer schema file label. The schema must include + buffer_version.fbs manually (e.g. `include "buffer_version.fbs";`). + output: The name of the generated binary output file (should end with .bin). + buffer_major_version: Major version to inject (uint16_t). + buffer_minor_version: Minor version to inject (uint16_t). + includes: Additional .fbs files required to resolve include directives in the schema. + @score_baselibs//score/flatbuffers/common:buffer_version.fbs is always added + automatically and must not be listed here. + tags: Tags forwarded to both the intermediate and final targets. Must be forwarded + to the intermediate because tags like "manual" control wildcard build inclusion: + without it, `bazel build //...` would build the intermediate even when the + final target is excluded. + **kwargs: Additional standard Bazel attributes (e.g. visibility, testonly, + deprecation, target_compatible_with) forwarded only to the final + serialize_buffer target. Constraint attributes and testonly need not be set on + the intermediate: Bazel propagates incompatibility transitively, and testonly + only restricts who may depend on a testonly target (not what it depends on). + + Example: + serialize_versioned_buffer( + name = "demo_data", + data = "demo_data.json", + schema = "demo.fbs", + output = "demo_data.bin", + includes = ["some_other.fbs"], + buffer_major_version = 2, + buffer_minor_version = 3, + ) + """ + patched_name = name + "_patched_json" + patched_output = name + "_patched.json" + + _inject_buffer_version( + name = patched_name, + data = data, + output = patched_output, + major_version = buffer_major_version, + minor_version = buffer_minor_version, + visibility = ["//visibility:private"], + tags = tags, + ) + + serialize_buffer( + name = name, + data = ":" + patched_name, + schema = schema, + output = output, + includes = ["@score_baselibs//score/flatbuffers/common:buffer_version.fbs"] + includes, + tags = tags, + **kwargs + ) + +def serialize_multiple_versioned_buffers( + name, + data_dict, + schema, + buffer_major_version, + buffer_minor_version, + includes = [], + tags = [], + **kwargs): + """Injects version information into multiple JSON files and serializes them to binary FlatBuffers. + + This macro composes _inject_buffer_version and serialize_multiple_buffers: for each entry in + data_dict it creates an _inject_buffer_version target that injects the given major/minor version, + then passes all patched JSON files to serialize_multiple_buffers. + + Args: + name: Target name. + data_dict: A dict mapping input JSON file labels to output binary file paths. + schema: The .fbs FlatBuffer schema file label. The schema must include + buffer_version.fbs manually (e.g. `include "buffer_version.fbs";`). + buffer_major_version: Major version to inject into every buffer (uint16_t). + buffer_minor_version: Minor version to inject into every buffer (uint16_t). + includes: Additional .fbs files required to resolve include directives in the schema. + @score_baselibs//score/flatbuffers/common:buffer_version.fbs is always added + automatically and must not be listed here. + tags: Tags forwarded to both the intermediate and final targets. Must be forwarded + to the intermediates because tags like "manual" control wildcard build inclusion: + without it, `bazel build //...` would build the intermediates even when the + final target is excluded. + **kwargs: Additional standard Bazel attributes (e.g. visibility, testonly, + deprecation, target_compatible_with) forwarded only to the final + serialize_multiple_buffers target. Constraint attributes and testonly need not + be set on the intermediates: Bazel propagates incompatibility transitively, and + testonly only restricts who may depend on a testonly target (not what it + depends on). + + Example: + serialize_multiple_versioned_buffers( + name = "demo_data", + schema = "demo.fbs", + data_dict = { + "demo_data.json": "demo_data.bin", + "subdir/demo_data2.json": "subdir/demo_data2.bin", + }, + includes = ["some_other.fbs"], + buffer_major_version = 1, + buffer_minor_version = 0, + ) + """ + patched_data_dict = {} + + for data_label, output_path in data_dict.items(): + # Derive a stable target name from the output path + safe_key = output_path.replace("/", "_").replace(".", "_") + patched_name = name + "_patched_" + safe_key + patched_output = name + "_patched_" + safe_key + ".json" + + _inject_buffer_version( + name = patched_name, + data = data_label, + output = patched_output, + major_version = buffer_major_version, + minor_version = buffer_minor_version, + visibility = ["//visibility:private"], + tags = tags, + ) + + patched_data_dict[":" + patched_name] = output_path + + serialize_multiple_buffers( + name = name, + schema = schema, + data_dict = patched_data_dict, + includes = ["@score_baselibs//score/flatbuffers/common:buffer_version.fbs"] + includes, + tags = tags, + **kwargs + ) + def _generate_json_schema_impl(ctx): """Implementation of the generate_json_schema rule.""" @@ -235,6 +465,10 @@ def _generate_json_schema_impl(ctx): # Get the flatc compiler using absolute path from flatbuffers repository flatc = ctx.executable._flatc + # Collect include directories from included .fbs files + include_files = ctx.files.includes + [ctx.file._buffer_version_fbs] + include_dirs = {f.dirname: True for f in include_files} + # When generating JSON schema, flatc generates a file named after the schema file default_name = schema_file.basename.replace(".fbs", ".schema.json") temp_subdir = "tmp_{}".format(ctx.label.name) @@ -259,11 +493,13 @@ def _generate_json_schema_impl(ctx): args = ctx.actions.args() args.add("--jsonschema") + for inc_dir in include_dirs: + args.add("-I", inc_dir) args.add("-o", generated_file.dirname) args.add(schema_file.path) ctx.actions.run( - inputs = [schema_file], + inputs = [schema_file] + include_files, outputs = [generated_file], executable = flatc, arguments = [args], @@ -288,20 +524,35 @@ generate_json_schema = rule( mandatory = True, doc = "The name of the generated JSON schema file (should end with .schema.json)", ), + "includes": attr.label_list( + allow_files = [".fbs"], + default = [], + doc = "Additional .fbs files required to resolve include directives in the schema.", + ), "_flatc": attr.label( default = "@flatbuffers//:flatc", executable = True, cfg = "exec", doc = "The flatc compiler (absolute path from flatbuffers repository)", ), + "_buffer_version_fbs": attr.label( + default = "@score_baselibs//score/flatbuffers/common:buffer_version.fbs", + allow_single_file = [".fbs"], + doc = "Automatically included buffer_version.fbs for common buffer version support.", + ), }, doc = """Generates a JSON schema from a FlatBuffer schema. + @score_baselibs//score/flatbuffers/common:buffer_version.fbs is always included + automatically. The schema must include buffer_version.fbs manually if it uses + the common buffer version (e.g. `include "buffer_version.fbs";`). + Example: generate_json_schema( name = "demo_schema", schema = "demo.fbs", output = "demo.schema.json", + includes = ["some_other.fbs"], ) """, ) diff --git a/score/flatbuffers/buffer_version_info.hpp b/score/flatbuffers/buffer_version_info.hpp new file mode 100644 index 000000000..03d81429d --- /dev/null +++ b/score/flatbuffers/buffer_version_info.hpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_BUFFER_VERSION_INFO_HPP +#define SCORE_LIB_FLATBUFFERS_BUFFER_VERSION_INFO_HPP + +#include +#include +#include + +namespace score +{ +namespace flatbuffers +{ + +/// @brief Data extracted from a conforming FlatBuffer using the universal +/// buffer identification convention. +/// +/// The field names mirror the `BufferVersion` FlatBuffers table defined in +/// `buffer_version.fbs` and the 4-char `file_identifier`. +/// +/// Buffers passed to `IVersionReader::GetVersion` and +/// `IVersionReader::VerifyVersion` must have +/// `version_info:score.flatbuffers.BufferVersion` as field 0 of their root +/// table, with version values explicitly stored in the binary. +/// This is guaranteed when the buffer was produced by e.g. the +/// `serialize_versioned_buffer` Bazel rule. +class BufferVersionInfo +{ + public: + /// Number of characters in a FlatBuffers file identifier. + static constexpr std::size_t kIdentifierLength = 4U; + + /// @brief Convenience constructor. + /// + /// @param id Exactly a 4-character string literal (e.g. `"DEMO"`). + /// @param major Major version number. + /// @param minor Minor version number. + constexpr BufferVersionInfo(const char (&id)[kIdentifierLength + 1U], uint16_t major, uint16_t minor) noexcept + : major_version{major}, minor_version{minor}, identifier_{id[0], id[1], id[2], id[3]} + { + } + + /// @brief Default constructor – leaves all fields zero-initialised. + BufferVersionInfo() noexcept : major_version{0U}, minor_version{0U}, identifier_{} {} + + uint16_t major_version; + uint16_t minor_version; + + /// @brief Returns a view of the 4-char file identifier. + std::string_view identifier() const noexcept; + + /// @brief Equality: identifier, major_version, and minor_version must all match. + bool operator==(const BufferVersionInfo& rhs) const noexcept; + + /// @brief Inequality: true when any field differs. + bool operator!=(const BufferVersionInfo& rhs) const noexcept; + + private: + char identifier_[kIdentifierLength]; +}; + +/// @brief Controls how `IVersionReader::VerifyVersion` compares the minor version. +enum class VersionMatchMode +{ + /// Both `major_version` and `minor_version` must match exactly. + kExact, + /// `major_version` must match exactly; `minor_version` of the buffer must + /// be greater than or equal to the expected value (backwards-compatible + /// minimum-version check). + kMinorMinimum, +}; + +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_BUFFER_VERSION_INFO_HPP diff --git a/score/flatbuffers/common/BUILD.bazel b/score/flatbuffers/common/BUILD.bazel new file mode 100644 index 000000000..8c5961cd6 --- /dev/null +++ b/score/flatbuffers/common/BUILD.bazel @@ -0,0 +1,62 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("//score/flatbuffers/bazel:codegen.bzl", "generate_cpp") + +exports_files( + [ + "buffer_version.fbs", + ], + visibility = ["//visibility:public"], +) + +# Generate C++ header from the BufferVersion schema +generate_cpp( + name = "gen_buffer_version_h", + output = "buffer_version_generated.h", + schema = "buffer_version.fbs", +) + +# Generate C++ header from the BufferVersionEnvelope schema (includes buffer_version.fbs) +generate_cpp( + name = "gen_buffer_version_envelope_h", + includes = [":buffer_version.fbs"], + output = "buffer_version_envelope_generated.h", + schema = "buffer_version_envelope.fbs", +) + +# C++ library exposing the generated envelope and version headers +cc_library( + name = "buffer_version_envelope", + hdrs = [ + ":gen_buffer_version_envelope_h", + ":gen_buffer_version_h", + ], + include_prefix = "score/flatbuffers/common", + visibility = ["//score/flatbuffers:__pkg__"], + deps = [ + "//score/flatbuffers:flatbufferscpp", + ], +) + +# Exposes buffer_version_generated.h as a bare #include "buffer_version_generated.h". +# Required by schemas that include buffer_version.fbs: flatc emits a relative +# #include "buffer_version_generated.h" in their generated C++ header. +cc_library( + name = "buffer_version_generated_bare", + hdrs = [":gen_buffer_version_h"], + strip_include_prefix = "/score/flatbuffers/common", + visibility = ["//visibility:public"], + deps = ["//score/flatbuffers:flatbufferscpp"], +) diff --git a/score/flatbuffers/common/buffer_version.fbs b/score/flatbuffers/common/buffer_version.fbs new file mode 100644 index 000000000..a541120da --- /dev/null +++ b/score/flatbuffers/common/buffer_version.fbs @@ -0,0 +1,62 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// Shared buffer identification table. +/// Included by any schema that wants to support universal buffer identification. +/// All instances use the same structure and zero defaults. +/// +/// Zero defaults ensure that non-zero version values injected at serialization +/// time will always be explicitly stored in the binary (they differ from the +/// default), enabling a universal non-template reader. +/// +/// Usage +/// ----- +/// 1. Include this file at the top of your schema: +/// +/// include "buffer_version.fbs"; +/// +/// 2. Declare `version_info` as the FIRST field of your root table: +/// +/// table MyConfig { +/// version_info:score.flatbuffers.BufferVersion; // MUST be field 0 +/// // ... application fields follow +/// } +/// +/// The field MUST be named `version_info` and MUST occupy vtable slot 0 (i.e. +/// be the first declared field, or use the `(id: 0)` attribute explicitly). +/// The universal buffer utilities (`score::flatbuffers::GetVersion` +/// and `score::flatbuffers::VerifyVersion`) rely on both the +/// field name and its vtable slot: a different name or a non-zero slot will +/// cause buffer identification to fail at runtime. + +namespace score.flatbuffers; + +/// Open for extension: additional fields may be appended in future revisions. +/// FlatBuffers forward/backward compatibility guarantees that existing readers +/// will safely ignore unknown fields added here. +table BufferVersion { + /// Type marker used to detect wrong-field placement at runtime. + /// + /// The FlatBuffers verifier is structurally type-blind: it checks only that + /// offsets are in-bounds and sizes are valid, not that the data at a given + /// vtable slot actually belongs to the expected table type. A `required` + /// non-scalar field closes this gap. The universal reader (`GetVersion`) + /// rejects any buffer where `type_marker != "BV"` with + /// `ErrorCode::kVerificationFailed`, catching cases where + /// `version_info` was accidentally placed in a non-zero vtable slot and + /// the verifier mistook another field's bytes as a valid `BufferVersion`. + type_marker:string; + + major_version:uint16 = 0; + minor_version:uint16 = 0; +} diff --git a/score/flatbuffers/common/buffer_version_envelope.fbs b/score/flatbuffers/common/buffer_version_envelope.fbs new file mode 100644 index 000000000..0c8ff92bc --- /dev/null +++ b/score/flatbuffers/common/buffer_version_envelope.fbs @@ -0,0 +1,32 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +/// Minimal envelope schema for universal buffer identification. +/// +/// This schema mirrors the common root-table convention: field 0 is always +/// a BufferVersion table. Because FlatBuffers table access is purely +/// offset-based at runtime, a BufferVersionEnvelope is binary-compatible with +/// any root table whose first field is `version_info:BufferVersion`. +/// +/// The generated C++ header provides typed accessors (version_info(), Verify()) +/// so that the universal reader does not need any hardcoded vtable offsets. + +include "buffer_version.fbs"; + +namespace score.flatbuffers; + +table BufferVersionEnvelope { + version_info:BufferVersion; +} + +root_type BufferVersionEnvelope; diff --git a/score/flatbuffers/details/buffer_version_info.cpp b/score/flatbuffers/details/buffer_version_info.cpp new file mode 100644 index 000000000..7418a6e9c --- /dev/null +++ b/score/flatbuffers/details/buffer_version_info.cpp @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/buffer_version_info.hpp" + +namespace score +{ +namespace flatbuffers +{ + +std::string_view BufferVersionInfo::identifier() const noexcept +{ + return {identifier_, kIdentifierLength}; +} + +bool BufferVersionInfo::operator==(const BufferVersionInfo& rhs) const noexcept +{ + return (identifier() == rhs.identifier()) && (major_version == rhs.major_version) && + (minor_version == rhs.minor_version); +} + +bool BufferVersionInfo::operator!=(const BufferVersionInfo& rhs) const noexcept +{ + return !(*this == rhs); +} + +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/buffer_version_info_test.cpp b/score/flatbuffers/details/buffer_version_info_test.cpp new file mode 100644 index 000000000..9d3b8e57c --- /dev/null +++ b/score/flatbuffers/details/buffer_version_info_test.cpp @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/buffer_version_info.hpp" + +#include + +#include + +namespace score +{ +namespace flatbuffers +{ +namespace unit_test +{ + +struct InequalityParam +{ + score::flatbuffers::BufferVersionInfo lhs; + score::flatbuffers::BufferVersionInfo rhs; + bool expected_equal; + const char* name; +}; + +class BufferVersionInfoEqualityTest : public ::testing::TestWithParam +{ +}; + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + BoundaryValues, + BufferVersionInfoEqualityTest, + ::testing::Values( + InequalityParam{{"DEMO", 2U, 3U}, {"DEMO", 2U, 3U}, true, "EqualMidValues"}, + InequalityParam{{" ", 2U, 3U}, {"DEMO", 2U, 3U}, false, "IdentifierMinVsMid"}, + InequalityParam{{"~~~~", 2U, 3U}, {"DEMO", 2U, 3U}, false, "IdentifierMaxVsMid"}, + InequalityParam{{"DEMO", 0U, 3U}, {"DEMO", 1U, 3U}, false, "MajorMinVsMinPlusOne"}, + InequalityParam{{"DEMO", UINT16_MAX, 3U}, {"DEMO", UINT16_MAX - 1U, 3U}, false, "MajorMaxVsMaxMinusOne"}, + InequalityParam{{"DEMO", 2U, 0U}, {"DEMO", 2U, 1U}, false, "MinorMinVsMinPlusOne"}, + InequalityParam{{"DEMO", 2U, UINT16_MAX}, {"DEMO", 2U, UINT16_MAX - 1U}, false, "MinorMaxVsMaxMinusOne"} + ), + [](const ::testing::TestParamInfo& info) { return info.param.name; } +); +// clang-format on + +TEST_P(BufferVersionInfoEqualityTest, OperatorEquals) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "operator== returns true when all fields are equal and false when any field differs"); + + const auto& p = GetParam(); + EXPECT_EQ(p.lhs == p.rhs, p.expected_equal); +} + +TEST_P(BufferVersionInfoEqualityTest, OperatorNotEquals) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "operator!= returns the logical negation of operator==: " + "false when all fields are equal and true when any field differs"); + + const auto& p = GetParam(); + // operator!= must be the logical complement of operator==. + EXPECT_EQ(p.lhs != p.rhs, !p.expected_equal); +} + +} // namespace unit_test +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/error.cpp b/score/flatbuffers/details/error.cpp new file mode 100644 index 000000000..9b5b730be --- /dev/null +++ b/score/flatbuffers/details/error.cpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/error.hpp" + +namespace score +{ +namespace flatbuffers +{ + +namespace +{ + +class FlatbuffersErrorDomain final : public score::result::ErrorDomain +{ + public: + std::string_view MessageFor(const score::result::ErrorCode& code) const noexcept override + { + switch (code) + { + case static_cast(ErrorCode::kNullDataPointer): + return "Data pointer or derived root table pointer is null"; + case static_cast(ErrorCode::kVerificationFailed): + return "Structural buffer verification failed"; + case static_cast(ErrorCode::kVersionInfoNotPresent): + return "No version info available in buffer"; + case static_cast(ErrorCode::kVersionMismatch): + return "Buffer version does not match expected version"; + default: + return "Unknown FlatBuffers error"; + } + } +}; + +constexpr FlatbuffersErrorDomain flatbuffers_error_domain; + +} // namespace + +score::result::Error MakeError(const ErrorCode code, const std::string_view user_message) noexcept +{ + return {static_cast(code), flatbuffers_error_domain, user_message}; +} + +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/error_test.cpp b/score/flatbuffers/details/error_test.cpp new file mode 100644 index 000000000..b4ed53bd5 --- /dev/null +++ b/score/flatbuffers/details/error_test.cpp @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/error.hpp" + +#include + +namespace score +{ +namespace flatbuffers +{ +namespace unit_test +{ + +TEST(FlatbuffersErrorDomainTest, ProvidesMessageForEveryErrorCode) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "MakeError produces an Error whose Message() returns the expected human-readable string " + "for every defined ErrorCode, exercising all branches of FlatbuffersErrorDomain::MessageFor"); + + EXPECT_EQ(MakeError(score::flatbuffers::ErrorCode::kNullDataPointer).Message(), + "Data pointer or derived root table pointer is null"); + EXPECT_EQ(MakeError(score::flatbuffers::ErrorCode::kVerificationFailed).Message(), + "Structural buffer verification failed"); + EXPECT_EQ(MakeError(score::flatbuffers::ErrorCode::kVersionInfoNotPresent).Message(), + "No version info available in buffer"); + EXPECT_EQ(MakeError(score::flatbuffers::ErrorCode::kVersionMismatch).Message(), + "Buffer version does not match expected version"); + // Default branch: a code value that does not map to any named enumerator. + EXPECT_EQ(MakeError(static_cast(99)).Message(), "Unknown FlatBuffers error"); +} + +} // namespace unit_test +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/idl_parser_test.cpp b/score/flatbuffers/details/idl_parser_test.cpp new file mode 100644 index 000000000..979bfb7ad --- /dev/null +++ b/score/flatbuffers/details/idl_parser_test.cpp @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "flatbuffers/base.h" + +#include + +#include +#include + +namespace +{ + +TEST(IdlParserTest, VersionStringIsNonEmptyAndHasDotSeparatedFormat) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "FLATBUFFERS_VERSION() returns a non-empty string in X.Y.Z format"); + + const std::string version{flatbuffers::FLATBUFFERS_VERSION()}; + + // [0-9]+ one or more decimal digits (major / minor / revision) + // \. a literal dot separator + // Repeated three times and anchored to the full string by std::regex_match. + EXPECT_TRUE(std::regex_match(version, std::regex{R"([0-9]+\.[0-9]+\.[0-9]+)"})) + << "Version string '" << version << "' does not match expected #.#.# format"; +} + +} // namespace diff --git a/score/flatbuffers/details/version_reader.cpp b/score/flatbuffers/details/version_reader.cpp new file mode 100644 index 000000000..c2a08a287 --- /dev/null +++ b/score/flatbuffers/details/version_reader.cpp @@ -0,0 +1,140 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/version_reader.hpp" +#include "score/flatbuffers/details/version_reader_impl.hpp" + +#include + +static_assert(score::flatbuffers::BufferVersionInfo::kIdentifierLength == ::flatbuffers::kFileIdentifierLength, + "BufferVersionInfo identifier length must match FlatBuffers file identifier length"); + +namespace score +{ +namespace flatbuffers +{ + +// ─── Implementation ─────────────────────────────────────────────────────────── + +namespace +{ + +inline score::Result GetVersionVecImpl(const std::vector& buf) noexcept +{ + if (!buf.empty()) + { + return detail::GetVersionImpl<>(score::cpp::span{buf.data(), buf.size()}); + } + else + { + // Avoid calling GetVersionImpl with a null/zero-size span. + return MakeUnexpected(ErrorCode::kVerificationFailed); + } +} + +inline score::Result VerifyVersionImpl(const score::cpp::span buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + const auto result = detail::GetVersionImpl<>(buf); + if (!result.has_value()) + { + return score::MakeUnexpected(result.error()); + } + + const auto& actual = result.value(); + + const bool match = (mode == VersionMatchMode::kMinorMinimum) ? ((actual.identifier() == expected.identifier()) && + (actual.major_version == expected.major_version) && + (actual.minor_version >= expected.minor_version)) + : (actual == expected); + + if (!match) + { + return MakeUnexpected(ErrorCode::kVersionMismatch); + } + + return {}; +} + +inline score::Result VerifyVersionImpl(const std::vector& buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + if (!buf.empty()) + { + return VerifyVersionImpl(score::cpp::span{buf.data(), buf.size()}, expected, mode); + } + else + { + // Avoid calling VerifyVersionImpl with a null/zero-size span. + return MakeUnexpected(ErrorCode::kVerificationFailed); + } +} + +} // namespace + +// ─── VersionReader class ────────────────────────────────────────────────────── + +score::Result VersionReader::GetVersion(const score::cpp::span buf) noexcept +{ + return detail::GetVersionImpl<>(buf); +} + +score::Result VersionReader::GetVersion(const std::vector& buf) noexcept +{ + return GetVersionVecImpl(buf); +} + +score::Result VersionReader::VerifyVersion(const score::cpp::span buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + return VerifyVersionImpl(buf, expected, mode); +} + +score::Result VersionReader::VerifyVersion(const std::vector& buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + return VerifyVersionImpl(buf, expected, mode); +} + +// ─── Convenience free functions ────────────────────────────────────────────── + +score::Result GetVersion(const score::cpp::span buf) noexcept +{ + return detail::GetVersionImpl<>(buf); +} + +score::Result GetVersion(const std::vector& buf) noexcept +{ + return GetVersionVecImpl(buf); +} + +score::Result VerifyVersion(const score::cpp::span buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + return VerifyVersionImpl(buf, expected, mode); +} + +score::Result VerifyVersion(const std::vector& buf, + const BufferVersionInfo& expected, + const VersionMatchMode mode) noexcept +{ + return VerifyVersionImpl(buf, expected, mode); +} + +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/details/version_reader_impl.hpp b/score/flatbuffers/details/version_reader_impl.hpp new file mode 100644 index 000000000..0ca895e9f --- /dev/null +++ b/score/flatbuffers/details/version_reader_impl.hpp @@ -0,0 +1,107 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_VERSION_READER_IMPL_HPP +#define SCORE_LIB_FLATBUFFERS_VERSION_READER_IMPL_HPP + +/// @file version_reader_impl.hpp +/// +/// Internal implementation header — NOT part of the public API. +/// +/// Listed in the `flatbufferutils` Bazel target's `srcs` (like +/// `load_buffer_internal.hpp`) so that `version_reader.cpp` and the unit-test +/// translation unit can both include it without it becoming a public header. +/// +/// The `GetVersionImpl` function template lives here rather than in an +/// anonymous namespace inside `version_reader.cpp` so that tests can +/// instantiate it with a custom `GetEnvelope` function pointer, covering +/// the otherwise-unreachable `envelope == nullptr` defensive branch. + +#include "score/flatbuffers/buffer_version_info.hpp" +#include "score/flatbuffers/common/buffer_version_envelope_generated.h" +#include "score/flatbuffers/error.hpp" + +#include "flatbuffers/base.h" + +#include + +#include +#include +#include + +namespace score +{ +namespace flatbuffers +{ +namespace detail +{ + +/// @brief Core span-based implementation of GetVersion. +/// +/// The @p GetEnvelope template parameter is the injection point for unit tests. +template +inline score::Result GetVersionImpl(const score::cpp::span buf) noexcept +{ + if (buf.data() == nullptr) + { + return MakeUnexpected(ErrorCode::kNullDataPointer); + } + + // Structural integrity check using the generated Verifier for BufferVersionEnvelope. + // This verifies root offset, vtable, and the nested version table in one call. + ::flatbuffers::Verifier verifier(buf.data(), buf.size()); + if (!::score::flatbuffers::VerifyBufferVersionEnvelopeBuffer(verifier)) + { + return MakeUnexpected(ErrorCode::kVerificationFailed); + } + + const auto* envelope = GetEnvelope(buf.data()); + if (envelope == nullptr) + { + return MakeUnexpected(ErrorCode::kNullDataPointer); + } + + const auto* version = envelope->version_info(); + if (version == nullptr) + { + return MakeUnexpected(ErrorCode::kVersionInfoNotPresent); + } + + // Reject buffers where version_info was placed in the wrong vtable slot (user fbs). + // The FlatBuffers verifier is type-blind and would otherwise accept another + // field's bytes as a valid (all-default) BufferVersion table. + // type_marker presence and correct value ensure no accidental match with a similar + // table-based field. + const auto* marker = version->type_marker(); + // NOTE: The "BV" literal must be kept in sync with the type_marker value in + // inject_buffer_version.py. Changing it here without updating that script + // (or vice versa) will cause buffer version validation to fail at runtime. + if ((marker == nullptr) || (std::string_view{marker->c_str()} != "BV")) + { + return MakeUnexpected(ErrorCode::kVerificationFailed); + } + + // GetBufferIdentifier returns a non-null-terminated pointer into the buffer. + // The identifier is always exactly 4 characters (kFileIdentifierLength). + // Zero-initialize the array so id[kFileIdentifierLength] remains '\0' after memcpy. + char id[::flatbuffers::kFileIdentifierLength + 1U]{}; + std::memcpy(id, ::flatbuffers::GetBufferIdentifier(buf.data()), ::flatbuffers::kFileIdentifierLength); + + return BufferVersionInfo{id, version->major_version(), version->minor_version()}; +} + +} // namespace detail +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_VERSION_READER_IMPL_HPP diff --git a/score/flatbuffers/details/version_reader_test.cpp b/score/flatbuffers/details/version_reader_test.cpp new file mode 100644 index 000000000..fa52ab8d3 --- /dev/null +++ b/score/flatbuffers/details/version_reader_test.cpp @@ -0,0 +1,775 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/version_reader.hpp" +#include "score/flatbuffers/common/buffer_version_envelope_generated.h" +#include "score/flatbuffers/details/version_reader_impl.hpp" + +#include "score/flatbuffers/buffer_version_info.hpp" +#include "score/flatbuffers/error.hpp" +#include "score/flatbuffers/i_version_reader.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace score +{ + +namespace flatbuffers +{ + +namespace unit_test +{ + +// ─── Buffer builder helpers ──────────────────────────────────────────────────── +// +// These helpers use the generated FlatBuffers builder API to construct in-memory +// buffers. + +/// Build a structurally valid versioned buffer with the given 4-char @p identifier, +/// @p major and @p minor version. +static std::vector BuildValidBuffer(const char* identifier, uint16_t major, uint16_t minor) +{ + ::flatbuffers::FlatBufferBuilder fbb; + const auto type_marker = fbb.CreateString("BV"); + const auto version = score::flatbuffers::CreateBufferVersion(fbb, type_marker, major, minor); + const auto envelope = score::flatbuffers::CreateBufferVersionEnvelope(fbb, version); + fbb.Finish(envelope, identifier); + const uint8_t* ptr = fbb.GetBufferPointer(); + return std::vector(ptr, ptr + fbb.GetSize()); +} + +/// Build an envelope buffer that carries NO version_info field. +/// The FlatBuffers verifier accepts it (version_info is optional in the schema). +static std::vector BuildBufferWithoutVersionInfo() +{ + ::flatbuffers::FlatBufferBuilder fbb; + const auto envelope = score::flatbuffers::CreateBufferVersionEnvelope(fbb); + fbb.Finish(envelope); + const uint8_t* ptr = fbb.GetBufferPointer(); + return std::vector(ptr, ptr + fbb.GetSize()); +} + +/// Build a buffer where version_info.type_marker is absent (null offset). +static std::vector BuildBufferWithNullMarker() +{ + ::flatbuffers::FlatBufferBuilder fbb; + // CreateBufferVersion without a type_marker: pass 0 as the offset. + score::flatbuffers::BufferVersionBuilder bvb(fbb); + bvb.add_major_version(1U); + bvb.add_minor_version(0U); + const auto version = bvb.Finish(); + const auto envelope = score::flatbuffers::CreateBufferVersionEnvelope(fbb, version); + fbb.Finish(envelope); + const uint8_t* ptr = fbb.GetBufferPointer(); + return std::vector(ptr, ptr + fbb.GetSize()); +} + +/// Build a buffer whose version_info.type_marker is NOT the sentinel "BV". +static std::vector BuildBufferWithWrongMarker(const char* wrong_marker) +{ + ::flatbuffers::FlatBufferBuilder fbb; + const auto marker = fbb.CreateString(wrong_marker); + const auto version = score::flatbuffers::CreateBufferVersion(fbb, marker, 1U, 0U); + const auto envelope = score::flatbuffers::CreateBufferVersionEnvelope(fbb, version); + fbb.Finish(envelope); + const uint8_t* ptr = fbb.GetBufferPointer(); + return std::vector(ptr, ptr + fbb.GetSize()); +} + +// ─── Fixture ────────────────────────────────────────────────────────────────── + +class VersionReaderTest : public ::testing::Test +{ + protected: + std::unique_ptr reader_ = std::make_unique(); + score::flatbuffers::IVersionReader& version_reader_{*reader_}; +}; + +// ─── GetVersion – failure cases ─────────────────────────────────────────────── + +TEST_F(VersionReaderTest, GetVersionVectorRejectsEmptyVector) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kVerificationFailed is returned for an empty std::vector without invoking the span overload"); + + const auto result_vec = version_reader_.GetVersion(std::vector{}); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(std::vector{}); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionSpanRejectsNullPointer) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "kNullDataPointer is returned when span.data() is null"); + const score::cpp::span null_span{nullptr, 64U}; + + const auto result_span = version_reader_.GetVersion(null_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kNullDataPointer)); + + const auto result_free_span = score::flatbuffers::GetVersion(null_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kNullDataPointer)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsGarbageBuffer) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kVerificationFailed is returned for an all-zeros buffer that fails FlatBuffers verification"); + + const std::vector garbage(64U, 0U); + const score::cpp::span garbage_span{garbage.data(), garbage.size()}; + + const auto result_vec = version_reader_.GetVersion(garbage); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.GetVersion(garbage_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(garbage); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::GetVersion(garbage_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsTooSmallBuffer) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty( + "Description", + "kVerificationFailed is returned for a buffer that is too small to hold a valid FlatBuffer root offset"); + + const std::vector tiny = {0x00, 0x00, 0x00, 0x00}; + const score::cpp::span tiny_span{tiny.data(), tiny.size()}; + + const auto result_vec = version_reader_.GetVersion(tiny); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.GetVersion(tiny_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(tiny); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::GetVersion(tiny_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsBufferWithoutVersionInfo) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty( + "Description", + "kVersionInfoNotPresent is returned when the envelope passes verification but version_info field is absent"); + + const auto buf = BuildBufferWithoutVersionInfo(); + const score::cpp::span buf_span{buf.data(), buf.size()}; + + const auto result_vec = version_reader_.GetVersion(buf); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionInfoNotPresent)); + + const auto result_span = version_reader_.GetVersion(buf_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionInfoNotPresent)); + + const auto result_free_vec = score::flatbuffers::GetVersion(buf); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionInfoNotPresent)); + + const auto result_free_span = score::flatbuffers::GetVersion(buf_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionInfoNotPresent)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsWrongTypeMarker) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kVerificationFailed is returned when type_marker is present but not the sentinel value 'BV'"); + + const auto buf = BuildBufferWithWrongMarker("XX"); + const score::cpp::span buf_span{buf.data(), buf.size()}; + + const auto result_vec = version_reader_.GetVersion(buf); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.GetVersion(buf_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(buf); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::GetVersion(buf_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsNullTypeMarker) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty( + "Description", + "kVerificationFailed is returned when type_marker is absent (null offset) in the version_info table"); + + const auto buf = BuildBufferWithNullMarker(); + const score::cpp::span buf_span{buf.data(), buf.size()}; + + const auto result_vec = version_reader_.GetVersion(buf); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.GetVersion(buf_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(buf); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::GetVersion(buf_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsEmptyTypeMarker) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kVerificationFailed is returned when type_marker is an empty string instead of 'BV'"); + + const auto buf = BuildBufferWithWrongMarker(""); + const score::cpp::span buf_span{buf.data(), buf.size()}; + + const auto result_vec = version_reader_.GetVersion(buf); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.GetVersion(buf_span); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::GetVersion(buf); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::GetVersion(buf_span); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +// Injection helper: simulates a hypothetical future regression where +// GetBufferVersionEnvelope returns null for a verified buffer. +// Covers the defensive `envelope == nullptr` branch. +static const score::flatbuffers::BufferVersionEnvelope* ReturnNullEnvelope(const void* /*buf*/) noexcept +{ + return nullptr; +} + +TEST_F(VersionReaderTest, GetVersionReturnsNullDataPointerWhenEnvelopeIsNull) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kNullDataPointer is returned when the envelope getter is injected to return nullptr, " + "covering the defensive branch that is otherwise structurally unreachable"); + + // Any valid buffer will do: we only need it to pass the verifier so the injected getter is reached. + const auto buf = BuildValidBuffer("TEST", 1U, 0U); + const score::cpp::span buf_span{buf.data(), buf.size()}; + + const auto result = score::flatbuffers::detail::GetVersionImpl(buf_span); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kNullDataPointer)); +} + +// ─── GetVersion – success / boundary values ─────────────────────────────────── + +struct VersionBufferParam +{ + char identifier[5]; + uint16_t major; + uint16_t minor; + const char* name; +}; + +class VersionBufferParamTest : public ::testing::TestWithParam +{ +}; + +// ─── Null-byte identifier – library constraint documentation ───────────────── +// +// FlatBuffers uses strlen() to validate the file_identifier before writing it +// into the buffer (see FlatBufferBuilderImpl::Finish). A \x00 byte in any +// position makes strlen() return a value shorter than 4, causing an assertion +// failure (abort) inside the library. It is therefore impossible to produce a +// versioned buffer with a \x00 byte in the identifier. +// +// The death test below documents this hard constraint: the buffer cannot be +// constructed, so GetVersion / VerifyVersion can never observe a \x00 identifier. +TEST(VersionBufferNullByteIdentifierTest, BuildingBufferWithNullByteIdentifierAborts) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "BuildValidBuffer aborts inside FlatBuffers when the identifier contains a \\x00 byte"); + + // The assertion message from FlatBuffers: + // strlen(file_identifier) == kFileIdentifierLength + ASSERT_DEATH(BuildValidBuffer("\x00\x00\x00\x00", 0U, 0U), ""); +} + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + BoundaryValues, + VersionBufferParamTest, + ::testing::Values( + // Identifier byte boundaries: \x01 (minimum non-null byte) and \xFF (maximum byte). + // GetVersion preserves all byte values verbatim; the 4-byte string_view returned by + // identifier() is compared by value, not by printability. + // VerifyVersion is NOT expected to be called with non-printable expected identifiers + // in production — the parameterised mismatch tests cover rejection of such buffers. + VersionBufferParam{"\x01\x01\x01\x01", 0U, 0U, "MinByteIdentifier"}, + VersionBufferParam{"\xFF\xFF\xFF\xFF", UINT16_MAX, UINT16_MAX, "MaxByteIdentifier"}, + VersionBufferParam{" ", 0U, 0U, "MinMinVersion"}, + VersionBufferParam{" ", 0U, 1U, "MinMajorMinorOne"}, + VersionBufferParam{" ", 1U, 0U, "MajorOneMinMinor"}, + VersionBufferParam{"DEMO", 2U, 3U, "TypicalValues"}, + VersionBufferParam{"~~~~", UINT16_MAX, UINT16_MAX - 1U, "MaxMajorNearMaxMinor"}, + VersionBufferParam{"~~~~", UINT16_MAX, UINT16_MAX, "MaxMaxVersion"} + ), + [](const ::testing::TestParamInfo& info) { return info.param.name; } +); +// clang-format on + +TEST_P(VersionBufferParamTest, ReadsIdentifierAndVersionCorrectly) +{ + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "GetVersion correctly extracts identifier, major and minor from a built buffer"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.GetVersion(buf); + ASSERT_TRUE(result_vec.has_value()); + EXPECT_EQ(result_vec.value().identifier(), std::string_view(p.identifier, 4U)); + EXPECT_EQ(result_vec.value().major_version, p.major); + EXPECT_EQ(result_vec.value().minor_version, p.minor); + + const auto result_span = reader.GetVersion(buf_span); + ASSERT_TRUE(result_span.has_value()); + EXPECT_EQ(result_span.value().identifier(), std::string_view(p.identifier, 4U)); + EXPECT_EQ(result_span.value().major_version, p.major); + EXPECT_EQ(result_span.value().minor_version, p.minor); + + const auto result_free_vec = score::flatbuffers::GetVersion(buf); + ASSERT_TRUE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.value().identifier(), std::string_view(p.identifier, 4U)); + EXPECT_EQ(result_free_vec.value().major_version, p.major); + EXPECT_EQ(result_free_vec.value().minor_version, p.minor); + + const auto result_free_span = score::flatbuffers::GetVersion(buf_span); + ASSERT_TRUE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.value().identifier(), std::string_view(p.identifier, 4U)); + EXPECT_EQ(result_free_span.value().major_version, p.major); + EXPECT_EQ(result_free_span.value().minor_version, p.minor); +} + +// ─── VerifyVersion – invalid version ────────────────────────────────────────── + +TEST_F(VersionReaderTest, VerifyVersionVectorRejectsEmptyVector) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty( + "Description", + "VerifyVersion(vector) returns kVerificationFailed for an empty vector without calling the span overload"); + + const score::flatbuffers::BufferVersionInfo expected{"TEST", 1U, 0U}; + + const auto result_vec = version_reader_.VerifyVersion(std::vector{}, expected); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(std::vector{}, expected); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, VerifyVersionPropagatesGetVersionError) +{ + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion propagates the error returned by GetVersion when the buffer is invalid"); + + const std::vector garbage(64U, 0U); + const score::cpp::span garbage_span{garbage.data(), garbage.size()}; + const score::flatbuffers::BufferVersionInfo expected{"TEST", 1U, 0U}; + + const auto result_vec = version_reader_.VerifyVersion(garbage, expected); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_span = version_reader_.VerifyVersion(garbage_span, expected); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(garbage, expected); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result_free_span = score::flatbuffers::VerifyVersion(garbage_span, expected); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +// ─── VerifyVersion ──────────────────────────────────────────────────────────── +// All parameterised VerifyVersion tests reuse VersionBufferParam / BoundaryValues. + +TEST_P(VersionBufferParamTest, VerifyVersionExactMatchSucceeds) +{ + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kExact) returns success when identifier, major and minor all match exactly"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + const score::flatbuffers::BufferVersionInfo expected{p.identifier, p.major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected); + EXPECT_TRUE(result_vec.has_value()); + + const auto result_span = reader.VerifyVersion(buf_span, expected); + EXPECT_TRUE(result_span.has_value()); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(buf, expected); + EXPECT_TRUE(result_free_vec.has_value()); + + const auto result_free_span = score::flatbuffers::VerifyVersion(buf_span, expected); + EXPECT_TRUE(result_free_span.has_value()); +} + +TEST_P(VersionBufferParamTest, VerifyVersionFailsOnMajorMismatch) +{ + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kExact) returns kVersionMismatch when expected major differs from actual"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // Guaranteed ≠ p.major: use 42 unless p.major is already 42, then use 21. + const uint16_t wrong_major = (p.major == 42U) ? 21U : 42U; + const score::flatbuffers::BufferVersionInfo expected{p.identifier, wrong_major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = reader.VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(buf, expected); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = score::flatbuffers::VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionBufferParamTest, VerifyVersionFailsOnMinorMismatch) +{ + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kExact) returns kVersionMismatch when expected minor differs from actual"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // Guaranteed ≠ p.minor: use 42 unless p.minor is already 42, then use 21. + const uint16_t wrong_minor = (p.minor == 42U) ? 21U : 42U; + const score::flatbuffers::BufferVersionInfo expected{p.identifier, p.major, wrong_minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = reader.VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(buf, expected); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = score::flatbuffers::VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionBufferParamTest, VerifyVersionFailsOnIdentifierMismatch) +{ + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kExact) returns kVersionMismatch when the 4-char file identifier does not match"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // "MISM" is not used by any param identifier, so it never matches p.identifier. + const score::flatbuffers::BufferVersionInfo expected{"MISM", p.major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = reader.VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = score::flatbuffers::VerifyVersion(buf, expected); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = score::flatbuffers::VerifyVersion(buf_span, expected); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionBufferParamTest, VerifyVersionMinorMinimumSucceedsWhenMinorEquals) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kMinorMinimum) succeeds when actual minor equals the expected minimum"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + const score::flatbuffers::BufferVersionInfo expected{p.identifier, p.major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_vec.has_value()); + + const auto result_span = + reader.VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_span.has_value()); + + const auto result_free_vec = + score::flatbuffers::VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_free_vec.has_value()); + + const auto result_free_span = + score::flatbuffers::VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_free_span.has_value()); +} + +TEST_P(VersionBufferParamTest, VerifyVersionMinorMinimumSucceedsWhenActualMinorIsHigher) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kMinorMinimum) succeeds when actual minor is strictly greater than expected minimum"); + + const auto& p = GetParam(); + if (p.minor == 0U) + { + GTEST_SKIP() << "minor == 0: no value below it can serve as minimum"; + } + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // Expected minimum is one below actual – always strictly less than p.minor. + const uint16_t min_minor = static_cast(p.minor - 1U); + const score::flatbuffers::BufferVersionInfo expected{p.identifier, p.major, min_minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_vec.has_value()); + + const auto result_span = + reader.VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_span.has_value()); + + const auto result_free_vec = + score::flatbuffers::VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_free_vec.has_value()); + + const auto result_free_span = + score::flatbuffers::VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result_free_span.has_value()); +} + +TEST_P(VersionBufferParamTest, VerifyVersionMinorMinimumFailsWhenActualMinorIsLower) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty( + "Description", + "VerifyVersion(kMinorMinimum) returns kVersionMismatch when actual minor is below the required minimum"); + + const auto& p = GetParam(); + if (p.minor == UINT16_MAX) + { + GTEST_SKIP() << "minor == UINT16_MAX: no value above it can serve as minimum"; + } + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // Expected minimum is one above actual – always strictly greater than p.minor. + const uint16_t min_minor = static_cast(p.minor + 1U); + const score::flatbuffers::BufferVersionInfo expected{p.identifier, p.major, min_minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = + reader.VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = + score::flatbuffers::VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = + score::flatbuffers::VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionBufferParamTest, VerifyVersionMinorMinimumFailsOnIdentifierMismatch) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty( + "Description", + "VerifyVersion(kMinorMinimum) returns kVersionMismatch when the 4-char file identifier does not match, " + "regardless of whether major and minor satisfy the minimum"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // "MISM" is not used by any param identifier, so it never matches p.identifier. + const score::flatbuffers::BufferVersionInfo expected{"MISM", p.major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = + reader.VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = + score::flatbuffers::VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = + score::flatbuffers::VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionBufferParamTest, VerifyVersionMinorMinimumFailsOnMajorMismatch) +{ + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", + "VerifyVersion(kMinorMinimum) returns kVersionMismatch when the major version does not match, " + "even though the identifier matches and minor satisfies the minimum"); + + const auto& p = GetParam(); + const auto buf = BuildValidBuffer(p.identifier, p.major, p.minor); + const score::cpp::span buf_span{buf.data(), buf.size()}; + // Guaranteed ≠ p.major: use 42 unless p.major is already 42, then use 21. + const uint16_t wrong_major = (p.major == 42U) ? 21U : 42U; + const score::flatbuffers::BufferVersionInfo expected{p.identifier, wrong_major, p.minor}; + score::flatbuffers::VersionReader reader; + + const auto result_vec = reader.VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_vec.has_value()); + EXPECT_EQ(result_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_span = + reader.VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_span.has_value()); + EXPECT_EQ(result_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_vec = + score::flatbuffers::VerifyVersion(buf, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_vec.has_value()); + EXPECT_EQ(result_free_vec.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result_free_span = + score::flatbuffers::VerifyVersion(buf_span, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result_free_span.has_value()); + EXPECT_EQ(result_free_span.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +} // namespace unit_test +} // namespace flatbuffers +} // namespace score diff --git a/score/flatbuffers/error.hpp b/score/flatbuffers/error.hpp new file mode 100644 index 000000000..6b4c022fa --- /dev/null +++ b/score/flatbuffers/error.hpp @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_ERROR_HPP +#define SCORE_LIB_FLATBUFFERS_ERROR_HPP + +#include "score/result/result.h" + +namespace score +{ +namespace flatbuffers +{ + +/// @brief Error codes returned by the FlatBuffers utility functions. +enum class ErrorCode : score::result::ErrorCode +{ + /// A null data pointer was passed to a buffer function. + kNullDataPointer, + /// The FlatBuffer failed structural verification (too small, bad offset, etc.). + kVerificationFailed, + /// The version_info table is not present in the buffer. + kVersionInfoNotPresent, + /// The actual buffer version does not match the expected version. + kVersionMismatch, +}; + +score::result::Error MakeError(ErrorCode code, std::string_view user_message = "") noexcept; + +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_ERROR_HPP diff --git a/score/flatbuffers/i_version_reader.hpp b/score/flatbuffers/i_version_reader.hpp new file mode 100644 index 000000000..040e3353c --- /dev/null +++ b/score/flatbuffers/i_version_reader.hpp @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_I_VERSION_READER_HPP +#define SCORE_LIB_FLATBUFFERS_I_VERSION_READER_HPP + +#include "score/flatbuffers/buffer_version_info.hpp" +#include "score/flatbuffers/error.hpp" +#include "score/result/result.h" + +#include + +#include +#include + +namespace score +{ +namespace flatbuffers +{ + +/// @brief Abstract interface for reading and verifying the version information +/// embedded in a FlatBuffer that follows the common buffer +/// identification convention. +/// +/// Buffers passed to `IVersionReader::GetVersion` and `IVersionReader::VerifyVersion` +/// must have `version_info:score.flatbuffers.BufferVersion` as field 0 of their +/// root table, with version values explicitly stored in the binary. +/// This is guaranteed when the buffer was produced by e.g. +/// the `serialize_versioned_buffer` Bazel rule. +/// +/// ### Dependency injection +/// Components that need testability should accept `IVersionReader&` in their +/// constructor and receive a `VersionReader` instance in production code. +/// +/// If dependency injection is not required (e.g. standalone tools or simple +/// one-off call sites), use the free functions `GetVersion` / `VerifyVersion` +/// declared in `score/flatbuffers/version_reader.hpp` instead. +class IVersionReader +{ + public: + virtual ~IVersionReader() = default; + + /// @brief Reads the 4-char file_identifier and the `BufferVersion` + /// values from a raw FlatBuffer. + /// + /// @param[in] buf View over the FlatBuffer binary data. + /// + /// @returns `BufferVersionInfo` on success, or a `score::result::Error` + /// on failure. + virtual score::Result GetVersion(score::cpp::span buf) noexcept = 0; + + /// @brief Reads the 4-char file_identifier and the `BufferVersion` + /// values from a vector-backed FlatBuffer. + /// + /// @param[in] buf FlatBuffer binary data. + /// + /// @returns `BufferVersionInfo` on success, or a `score::result::Error` + /// on failure. + virtual score::Result GetVersion(const std::vector& buf) noexcept = 0; + + /// @brief Reads the buffer version and verifies it matches @p expected. + /// + /// Both the `file_identifier` (4-char tag) and the `major_version` / + /// `minor_version` fields are compared against @p expected. + /// + /// @param[in] buf View over the FlatBuffer binary data. + /// @param[in] expected The `BufferVersionInfo` the caller considers valid. + /// @param[in] mode `VersionMatchMode::kExact` (default) requires an + /// exact match on both version fields; `kMinorMinimum` + /// accepts any minor version >= the expected value + /// while still requiring an exact major version match. + /// + /// @returns `score::Result` on success, or a `score::result::Error` + /// if the buffer is invalid or the version does not match. + virtual score::Result VerifyVersion(score::cpp::span buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept = 0; + + /// @brief Reads the buffer version and verifies it matches @p expected. + /// + /// @param[in] buf FlatBuffer binary data. + /// @param[in] expected The `BufferVersionInfo` the caller considers valid. + /// @param[in] mode Version comparison mode. + /// + /// @returns `score::Result` on success, or a `score::result::Error` + /// if the buffer is invalid or the version does not match. + virtual score::Result VerifyVersion(const std::vector& buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept = 0; + + protected: + IVersionReader() = default; +}; + +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_I_VERSION_READER_HPP diff --git a/score/flatbuffers/test/serialize_buffer_rules/BUILD b/score/flatbuffers/test/serialize_buffer_rules/BUILD index f172209a1..701cc5a89 100644 --- a/score/flatbuffers/test/serialize_buffer_rules/BUILD +++ b/score/flatbuffers/test/serialize_buffer_rules/BUILD @@ -12,7 +12,7 @@ # ******************************************************************************* load("@bazel_skylib//rules:build_test.bzl", "build_test") -load("//score/flatbuffers/bazel:tools.bzl", "serialize_buffer", "serialize_multiple_buffers") +load("//score/flatbuffers/bazel:tools.bzl", "serialize_buffer", "serialize_multiple_buffers", "serialize_multiple_versioned_buffers", "serialize_versioned_buffer") serialize_buffer( name = "serialize_buffer_default_name", @@ -59,6 +59,42 @@ build_test( ], ) +# =================================================== +# Versioned buffer rule tests +# =================================================== + +serialize_versioned_buffer( + name = "serialize_versioned_buffer_single", + buffer_major_version = 1, + buffer_minor_version = 2, + data = "//score/flatbuffers/test/testdata:valid_data.json", + output = "versioned_data.bin", + schema = "//score/flatbuffers/test/testdata:test_versioned.fbs", +) + +serialize_multiple_versioned_buffers( + name = "serialize_versioned_buffer_multiple", + buffer_major_version = 3, + buffer_minor_version = 0, + data_dict = { + "//score/flatbuffers/test/testdata:valid_data.json": "versioned_multi_1.bin", + "//score/flatbuffers/test/testdata:valid_data2.json": "versioned_multi_2.bin", + }, + schema = "//score/flatbuffers/test/testdata:test_versioned.fbs", +) + +build_test( + name = "serialize_versioned_buffer_test", + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], # build_test doesn't work under QEMU + targets = [ + ":serialize_versioned_buffer_single", + ":serialize_versioned_buffer_multiple", + ], +) + # =================================================== # Manual: Rule fault injection tests for development. # =================================================== @@ -75,8 +111,44 @@ serialize_buffer( serialize_buffer( name = "serialize_wrong_schema_path", - data = "subdir/data.json", + data = "//score/flatbuffers/test/testdata:valid_data.json", output = "subdir/data_output_in_subdir.bin", schema = "not_existing_schema.fbs", tags = ["manual"], # This target is expected to fail due to wrong schema path ) + +# Demonstrates that serialize_versioned_buffer fails at build time when a version +# value is outside of uint16_t range [0, 65535] +serialize_versioned_buffer( + name = "serialize_versioned_buffer_negative_version", + buffer_major_version = -1, + buffer_minor_version = 0, + data = "//score/flatbuffers/test/testdata:valid_data.json", + output = "versioned_negative_version.bin", + schema = "//score/flatbuffers/test/testdata:test_versioned.fbs", + tags = ["manual"], # Expected to fail: -1 is outside of uint16_t range [0, 65535] +) + +serialize_versioned_buffer( + name = "serialize_versioned_buffer_overflow_version", + buffer_major_version = 65536, + buffer_minor_version = 0, + data = "//score/flatbuffers/test/testdata:valid_data.json", + output = "versioned_overflow_version.bin", + schema = "//score/flatbuffers/test/testdata:test_versioned.fbs", + tags = ["manual"], # Expected to fail: 65536 is outside of uint16_t range [0, 65535] +) + +# Demonstrates that serialize_versioned_buffer fails at build time when version_info +# is placed in a nested subtable instead of the root table. +# The inject step adds `version_info` to the root of the JSON; --strict-json then +# rejects it because MyComponentConfig in this schema has no such field. +serialize_versioned_buffer( + name = "serialize_versioned_buffer_wrong_placement", + buffer_major_version = 1, + buffer_minor_version = 0, + data = "//score/flatbuffers/test/testdata:valid_data.json", + output = "versioned_wrong_placement.bin", + schema = "//score/flatbuffers/test/testdata:test_versioned_wrong_placement.fbs", + tags = ["manual"], # Expected to fail: injected version_info rejected by --strict-json +) diff --git a/score/flatbuffers/test/testdata/BUILD b/score/flatbuffers/test/testdata/BUILD index 62bbcfb03..5be01cf88 100644 --- a/score/flatbuffers/test/testdata/BUILD +++ b/score/flatbuffers/test/testdata/BUILD @@ -11,13 +11,115 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("//score/flatbuffers/bazel:codegen.bzl", "generate_cpp") +load("//score/flatbuffers/bazel:tools.bzl", "serialize_multiple_versioned_buffers", "serialize_versioned_buffer") + exports_files( [ "test.fbs", + "test_versioned.fbs", + "test_versioned_min.fbs", + "test_versioned_max.fbs", + "test_versioned_min_range.fbs", + "test_versioned_max_range.fbs", + "test_versioned_wrong_placement.fbs", + "test_versioned_wrong_field.fbs", "invalid_data.json", "valid_data.json", "valid_data2.json", "valid_data3.json", + "versioned_buffer_data.json", ], visibility = ["//score/flatbuffers/test:__subpackages__"], ) + +# Serialize the versioned buffer data +serialize_versioned_buffer( + name = "gen_versioned_buffer_bin", + buffer_major_version = 2, + buffer_minor_version = 3, + data = "versioned_buffer_data.json", + output = "versioned_buffer.bin", + schema = "test_versioned.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Serialize the versioned buffer data with explicit ids in schema +serialize_versioned_buffer( + name = "gen_versioned_buffer_explicit_ids_bin", + buffer_major_version = 2, + buffer_minor_version = 3, + data = "versioned_buffer_data.json", + output = "versioned_buffer_explicit_ids.bin", + schema = "test_versioned_explicit_ids.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Wrong placement of version_info can only be detected by VersionReader +serialize_versioned_buffer( + name = "gen_versioned_buffer_version_info_wrong_field", + buffer_major_version = 1, + buffer_minor_version = 2, + data = "versioned_buffer_data.json", + output = "versioned_wrong_field.bin", + schema = "test_versioned_wrong_field.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Wrong placement of version_info can only be detected by VersionReader +serialize_versioned_buffer( + name = "gen_versioned_buffer_version_info_wrong_field_similar_type", + buffer_major_version = 1, + buffer_minor_version = 2, + data = "versioned_buffer_data_wrong_field_similar_type.json", + output = "versioned_wrong_field_similar_type.bin", + schema = "test_versioned_wrong_field_similar_type.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Minimum version buffers +# Tolerate the minimum version (0.0) for the versioned buffer data +# Identifier: ' ' +serialize_versioned_buffer( + name = "gen_versioned_min_buffer_bin", + buffer_major_version = 0, + buffer_minor_version = 0, + data = "versioned_buffer_data.json", + output = "versioned_min_buffer.bin", + schema = "test_versioned_min.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Identifier: ' !"#' +serialize_versioned_buffer( + name = "gen_versioned_min_range_buffer_bin", + buffer_major_version = 0, + buffer_minor_version = 1, + data = "versioned_buffer_data.json", + output = "versioned_min_range_buffer.bin", + schema = "test_versioned_min_range.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Maximum version buffers +# Identifier: '{|}~' +serialize_versioned_buffer( + name = "gen_versioned_max_range_buffer_bin", + buffer_major_version = 65535, + buffer_minor_version = 65534, + data = "versioned_buffer_data.json", + output = "versioned_max_range_buffer.bin", + schema = "test_versioned_max_range.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) + +# Identifier: '~~~~' +serialize_versioned_buffer( + name = "gen_versioned_max_buffer_bin", + buffer_major_version = 65535, + buffer_minor_version = 65535, + data = "versioned_buffer_data.json", + output = "versioned_max_buffer.bin", + schema = "test_versioned_max.fbs", + visibility = ["//score/flatbuffers:__pkg__"], +) diff --git a/score/flatbuffers/test/testdata/test_versioned.fbs b/score/flatbuffers/test/testdata/test_versioned.fbs new file mode 100644 index 000000000..605047d30 --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned.fbs @@ -0,0 +1,64 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +file_identifier "DEMO"; diff --git a/score/flatbuffers/test/testdata/test_versioned_explicit_ids.fbs b/score/flatbuffers/test/testdata/test_versioned_explicit_ids.fbs new file mode 100644 index 000000000..77f7ebb4e --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_explicit_ids.fbs @@ -0,0 +1,68 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion (id:0); + + /// Component identification + component_name:string (required, id:1); + component_id:uint32 (id:2); // Implicit default is 0 + + thread_pool_size:uint8 = 4 (id:3); // Explicit default value + max_memory_mb:uint16 = 64 (id:4); + + /// Nested table + advanced_settings:AdvancedSettings (required, id:5); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode (id:0); + + /// Vector examples + allowed_priorities:[uint8] (id:1); // Vector of primitives + feature_flags:[FeatureFlag] (id:2); // Vector of tables + + /// Union example: flexible data payload + // Unions with explicit IDs consume two consecutive field IDs: flatc + // auto-generates the type-tag field (data_type) and assigns it id:3, + // while the union value field (data) gets id:4. id:3 must therefore not + // be assigned to any other field. + data:DataPayloadUnion (id:4); +} + +/// Table used in vector +table FeatureFlag { + name:string (required, id:0); + enabled:bool = false (id:1); +} + +table StringData { + value:string (id:0); +} + +table BinaryData { + value:[uint8] (id:0); +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +file_identifier "DEMO"; diff --git a/score/flatbuffers/test/testdata/test_versioned_max.fbs b/score/flatbuffers/test/testdata/test_versioned_max.fbs new file mode 100644 index 000000000..72b9bdb64 --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_max.fbs @@ -0,0 +1,74 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +// Range of printable ASCII characters +// 0x20 (space) +// 0x21 (!) +// 0x22 (") +// 0x23 (#) +// ... +// 0x7B ({) +// 0x7C (|) +// 0x7D (}) +// 0x7E (~) +file_identifier "~~~~"; diff --git a/score/flatbuffers/test/testdata/test_versioned_max_range.fbs b/score/flatbuffers/test/testdata/test_versioned_max_range.fbs new file mode 100644 index 000000000..bed4a2b7e --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_max_range.fbs @@ -0,0 +1,74 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +// Range of printable ASCII characters +// 0x20 (space) +// 0x21 (!) +// 0x22 (") +// 0x23 (#) +// ... +// 0x7B ({) +// 0x7C (|) +// 0x7D (}) +// 0x7E (~) +file_identifier "{|}~"; diff --git a/score/flatbuffers/test/testdata/test_versioned_min.fbs b/score/flatbuffers/test/testdata/test_versioned_min.fbs new file mode 100644 index 000000000..2223d1d6e --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_min.fbs @@ -0,0 +1,74 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +// Range of printable ASCII characters +// 0x20 (space) +// 0x21 (!) +// 0x22 (") +// 0x23 (#) +// ... +// 0x7B ({) +// 0x7C (|) +// 0x7D (}) +// 0x7E (~) +file_identifier " "; diff --git a/score/flatbuffers/test/testdata/test_versioned_min_range.fbs b/score/flatbuffers/test/testdata/test_versioned_min_range.fbs new file mode 100644 index 000000000..527e47c2e --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_min_range.fbs @@ -0,0 +1,74 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Buffer version — MUST be field 0 for universal buffer identification + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +// Range of printable ASCII characters +// 0x20 (space) +// 0x21 (!) +// 0x22 (") +// 0x23 (#) +// ... +// 0x7B ({) +// 0x7C (|) +// 0x7D (}) +// 0x7E (~) +file_identifier " !\"#"; diff --git a/score/flatbuffers/test/testdata/test_versioned_wrong_field.fbs b/score/flatbuffers/test/testdata/test_versioned_wrong_field.fbs new file mode 100644 index 000000000..827842b08 --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_wrong_field.fbs @@ -0,0 +1,66 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Component identification — occupies vtable slot 0 (field 0), no version_info here + component_name:string (required); + + /// Buffer version — Should have been in slot 0 (field 0) + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +file_identifier "DEMO"; diff --git a/score/flatbuffers/test/testdata/test_versioned_wrong_field_similar_type.fbs b/score/flatbuffers/test/testdata/test_versioned_wrong_field_similar_type.fbs new file mode 100644 index 000000000..2abb5c41d --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_wrong_field_similar_type.fbs @@ -0,0 +1,73 @@ +/// Demo FlatBuffer schema for a component (versioned variant) +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Similar struct as BufferVersion — occupies vtable slot 0 (field 0), no version_info here + similar_to_version_info:StructurallySimilarToBufferVersion; + + /// Buffer version — Should have been in slot 0 (field 0) + version_info:score.flatbuffers.BufferVersion; + + /// Component identification + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +table StructurallySimilarToBufferVersion { + some_string:string; + some_val1:uint16; + some_val2:uint16; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +file_identifier "DEMO"; diff --git a/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs b/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs new file mode 100644 index 000000000..ca5b072bc --- /dev/null +++ b/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs @@ -0,0 +1,71 @@ +/// Demo FlatBuffer schema for a component — WRONG version_info placement. +/// This schema is intentionally incorrect: version_info is placed inside the +/// nested AdvancedSettings table instead of as field 0 of the root table. +/// FlatBuffers and the serialize_versioned_buffer rule will compile this +/// without error, but score::flatbuffers::GetVersion / VerifyVersion will fail +/// because the universal reader looks for version_info at vtable slot 0 of +/// the root table only. +include "buffer_version.fbs"; + +namespace my_component.demo; + +/// Generic enum for component run modes +enum OperationMode:uint8 { + EVENT_DRIVEN = 0, + POLLING = 1, + PERIODIC = 2, +} + +/// Union for flexible data types +union DataPayloadUnion { + StringData, + BinaryData +} + +table MyComponentConfig { + /// Component identification — occupies vtable slot 0 (field 0), no version_info here + component_name:string (required); + component_id:uint32; // Implicit default is 0 + + thread_pool_size:uint8 = 4; // Explicit default value + max_memory_mb:uint16 = 64; + + /// Nested table + advanced_settings:AdvancedSettings (required); +} + +table AdvancedSettings { + /// WRONG: version_info is buried inside a nested table, not the root table. + /// GetVersion / VerifyVersion will return kVerificationFailed or zeros at runtime. + version_info:score.flatbuffers.BufferVersion; + + /// Enum example: operational mode (defaults to first enum value: EVENT_DRIVEN) + mode:OperationMode; + + /// Vector examples + allowed_priorities:[uint8]; // Vector of primitives + feature_flags:[FeatureFlag]; // Vector of tables + + /// Union example: flexible data payload + data:DataPayloadUnion; +} + +/// Table used in vector +table FeatureFlag { + name:string (required); + enabled:bool = false; +} + +table StringData { + value:string; +} + +table BinaryData { + value:[uint8]; +} + +// Root configuration object +root_type MyComponentConfig; + +// Component specific file identifier +file_identifier "DEMO"; diff --git a/score/flatbuffers/test/testdata/versioned_buffer_data.json b/score/flatbuffers/test/testdata/versioned_buffer_data.json new file mode 100644 index 000000000..87b6817de --- /dev/null +++ b/score/flatbuffers/test/testdata/versioned_buffer_data.json @@ -0,0 +1,5 @@ +{ + "component_name": "test_component", + "advanced_settings": { + } +} diff --git a/score/flatbuffers/test/testdata/versioned_buffer_data_wrong_field_similar_type.json b/score/flatbuffers/test/testdata/versioned_buffer_data_wrong_field_similar_type.json new file mode 100644 index 000000000..354c6f9d1 --- /dev/null +++ b/score/flatbuffers/test/testdata/versioned_buffer_data_wrong_field_similar_type.json @@ -0,0 +1,10 @@ +{ + "similar_to_version_info": { + "some_string": "version_info_like_struct", + "some_val1": 1, + "some_val2": 2 + }, + "component_name": "test_component", + "advanced_settings": { + } +} diff --git a/score/flatbuffers/test/version_reader_test.cpp b/score/flatbuffers/test/version_reader_test.cpp new file mode 100644 index 000000000..011fef92b --- /dev/null +++ b/score/flatbuffers/test/version_reader_test.cpp @@ -0,0 +1,543 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/flatbuffers/version_reader.hpp" +#include "score/filesystem/path.h" +#include "score/flatbuffers/buffer_version_info.hpp" +#include "score/flatbuffers/i_version_reader.hpp" +#include "score/flatbuffers/load_buffer.hpp" + +#include + +#include + +#include +#include +#include +#include + +/// Integration test for `VersionReader` / `IVersionReader`. +/// +/// - The test fixture owns a `score::flatbuffers::VersionReader` (concrete +/// implementation) as a member and exposes it via the abstract +/// `IVersionReader&` reference. +/// - Each test calls the `IVersionReader` interface and the free function. +/// - The overloads with the vector parameter (buf) are tested since all public +/// APIs are forwarding to the span-based implementation. +/// +class VersionReaderTest : public ::testing::Test +{ + + protected: + std::vector data_; + + /// Concrete implementation – owned by the fixture, lifetime matches the test. + score::flatbuffers::VersionReader reader_; + + /// All tests access functionality through the abstract interface, + /// mirroring how production code should depend on IVersionReader. + score::flatbuffers::IVersionReader& version_reader_{reader_}; +}; + +// clang-format off +/// Equivalence classes for `GetVersion` +/// +/// | EC# | Input | Pointer | Buffer content / size | Expected result | Test case(s) | +/// |-----|------------------------------------------|---------|--------------------------------|------------------------------|-----------------------------------------------------------| +/// | EC1 | Null data pointer | null | size = 64 | Err: kNullDataPointer | GetVersionRejectsNullPointer | +/// | EC2 | Empty buffer | valid | size = 0 | Err: kVerificationFailed | GetVersionRejectsEmptyBuffer | +/// | EC3 | Buffer too small to hold version info | valid | size = 4 (4 zero bytes) | Err: kVerificationFailed | GetVersionRejectsTooSmallBuffer | +/// | EC4 | Buffer with invalid FlatBuffer content | valid | 64 zero bytes | Err: kVerificationFailed | GetVersionRejectsInvalidBuffer | +/// | EC5 | version_info not in field 0 of root table| valid | valid FlatBuffer, wrong field | Err: kVerificationFailed | GetVersionRejectsBufferWithVersionInfoInWrongField | +/// | EC6 | Valid buffer with version info | valid | valid FlatBuffer | Ok: BufferVersionInfo | ReadBufferVersionFromBinary | +// clang-format on + +TEST_F(VersionReaderTest, GetVersionRejectsNullPointer) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "kNullDataPointer is returned if null pointer is passed instead of buffer data"); + + const auto result = version_reader_.GetVersion(score::cpp::span{nullptr, 64U}); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kNullDataPointer)); + + const auto result2 = score::flatbuffers::GetVersion(score::cpp::span{nullptr, 64U}); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kNullDataPointer)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsEmptyBuffer) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "kVerificationFailed is returned if buffer is empty"); + + const auto result = version_reader_.GetVersion(std::vector{}); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result2 = score::flatbuffers::GetVersion(std::vector{}); + EXPECT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsTooSmallBuffer) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "kVerificationFailed is returned if buffer is too small to contain version info"); + + const std::vector tiny_buf = {0, 0, 0, 0}; + const auto result = version_reader_.GetVersion(tiny_buf); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result2 = score::flatbuffers::GetVersion(tiny_buf); + EXPECT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +TEST_F(VersionReaderTest, GetVersionRejectsInvalidBuffer) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "kVerificationFailed is returned if buffer does not contain valid version info"); + + const std::vector bad_buf(64, 0); + const auto result = version_reader_.GetVersion(bad_buf); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result2 = score::flatbuffers::GetVersion(bad_buf); + EXPECT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +// --------------------------------------------------------------------------- +// Parameterized fixture for wrong-field placement buffers: +// --------------------------------------------------------------------------- +struct WrongFieldParam +{ + std::string bin_path; + std::string name; // used as the GoogleTest instance suffix +}; + +class VersionReaderWrongFieldTest : public ::testing::TestWithParam +{ + protected: + score::flatbuffers::VersionReader reader_; + score::flatbuffers::IVersionReader& version_reader_{reader_}; +}; + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + WrongFieldBuffers, + VersionReaderWrongFieldTest, + ::testing::Values( + WrongFieldParam{ + "score/flatbuffers/test/testdata/versioned_wrong_field.bin", + "WrongField" + }, + WrongFieldParam{ + "score/flatbuffers/test/testdata/versioned_wrong_field_similar_type.bin", + "WrongFieldSimilarType" + } + ), + [](const ::testing::TestParamInfo& info) { return info.param.name; } +); +// clang-format on + +TEST_P(VersionReaderWrongFieldTest, GetVersionRejectsBufferWithVersionInfoInWrongField) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "fault-injection"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "kVerificationFailed is returned if version_info is not in field 0 of the root table"); + + const auto& param = GetParam(); + auto load_result = score::flatbuffers::LoadBuffer(score::filesystem::Path{param.bin_path}); + ASSERT_TRUE(load_result.has_value()) << "LoadBuffer failed for " << param.bin_path; + const auto& wrong_field_buf = load_result.value(); + + const auto result = version_reader_.GetVersion(wrong_field_buf); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); + + const auto result2 = score::flatbuffers::GetVersion(wrong_field_buf); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVerificationFailed)); +} + +// --------------------------------------------------------------------------- +// Parameterized fixture for version verification +// --------------------------------------------------------------------------- +struct VersionBufferParam +{ + std::string bin_path; + score::flatbuffers::BufferVersionInfo version_info; + std::string name; // used as the GoogleTest instance suffix +}; + +class VersionReaderBufferParamTest : public ::testing::TestWithParam +{ + protected: + void SetUp() override + { + const auto& param = GetParam(); + auto load_result = score::flatbuffers::LoadBuffer(score::filesystem::Path{param.bin_path}); + ASSERT_TRUE(load_result.has_value()) << "LoadBuffer failed: " << param.bin_path; + data_ = std::move(load_result).value(); + } + + std::vector data_; + score::flatbuffers::VersionReader reader_; + score::flatbuffers::IVersionReader& version_reader_{reader_}; +}; + +// clang-format off +/// Boundary values for `GetVersion` (via `ReadBufferVersionFromBinary`) +/// +/// Fields under test: identifier (4-char ASCII), major_version (uint16), minor_version (uint16). +/// +/// | BV# | Instance name | identifier | major_version | minor_version | Boundary | +/// |-----|-------------------|--------------|----------------|----------------|---------------------------------------------------| +/// | BV1 | Normal | "DEMO" | 2 | 3 | Representative valid mid-range value | +/// | BV2 | NormalExplicitIds | "DEMO" | 2 | 3 | Same as BV1 but with explicit FlatBuffer field IDs| +/// | BV3 | MinVersion | " " (0x20)| 0 | 0 | All fields at minimum and printable-ASCII boundary| +/// | BV4 | MinRangeVersion | " !\"#" | 0 | 1 | Identifier at lower printable-ASCII boundary | +/// | BV5 | MaxRangeVersion | "{|}~" | UINT16_MAX | 65534 | Identifier at upper printable-ASCII boundary | +/// | BV6 | MaxVersion | "~~~~"(0x7E) | UINT16_MAX | UINT16_MAX | All fields at maximum and printable-ASCII boundary| +/// +/// Expected result for all instances: version info is correctly read from buffer and matches the given values. +/// +INSTANTIATE_TEST_SUITE_P( + AllVersionBuffers, + VersionReaderBufferParamTest, + ::testing::Values( + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_buffer.bin", {"DEMO", 2U, 3U}, "Normal"}, + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_buffer_explicit_ids.bin", {"DEMO", 2U, 3U}, "NormalExplicitIds"}, + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_min_buffer.bin", {" ", 0U, 0U}, "MinVersion"}, + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_min_range_buffer.bin", {" !\"#", 0U, 1U}, "MinRangeVersion"}, + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_max_range_buffer.bin", {"{|}~", UINT16_MAX, 65534U}, "MaxRangeVersion"}, + VersionBufferParam{"score/flatbuffers/test/testdata/versioned_max_buffer.bin", {"~~~~", UINT16_MAX, UINT16_MAX}, "MaxVersion"} + ), + [](const ::testing::TestParamInfo& info) { return info.param.name; } +); +// clang-format on + +TEST_P(VersionReaderBufferParamTest, ReadBufferVersionFromBinary) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__buffer_identification"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "version info is correctly read from buffer"); + + const auto& param = GetParam(); + const auto result = version_reader_.GetVersion(data_); + + ASSERT_TRUE(result.has_value()) << "GetVersion failed"; + + const auto& info = result.value(); + + EXPECT_EQ(info.identifier(), param.version_info.identifier()); + EXPECT_EQ(info.major_version, param.version_info.major_version); + EXPECT_EQ(info.minor_version, param.version_info.minor_version); +} + +// clang-format off +/// Equivalence classes for `VerifyVersion` +/// +/// Fields under test: identifier, major_version, minor_version, VersionMatchMode. +/// +/// | EC# | Match mode | identifier | major_version | minor_version | Expected result | Test case(s) | +/// |------|---------------|-------------------------|--------------------|------------------------------------------|-----------------------|-------------------------------------------------------------------| +/// | EC1 | kExactMatch | Matching | Matching | Matching | Ok | VerifyVersionSucceedsOnMatch (BV1–BV6) | +/// | EC2 | kExactMatch | Non-matching ("XXXX") | Matching | Matching | Err: kVersionMismatch | VerifyVersionFailsOnIdentifierMismatch (BV1–BV6) | +/// | EC3 | kExactMatch | Matching | Non-matching (99) | Matching | Err: kVersionMismatch | VerifyVersionFailsOnMajorMismatch (BV1–BV6) | +/// | EC4 | kExactMatch | Matching | Matching | Non-matching (~actual) | Err: kVersionMismatch | VerifyVersionFailsOnMinorMismatch (BV1–BV6) | +/// | EC5 | kExactMatch | Non-matching ("XXXX") | Non-matching (99) | Non-matching (99) | Err: kVersionMismatch | VerifyVersionFailsOnVersionInfoMismatch (BV1–BV6) | +/// | EC6 | kMinorMinimum | Matching | Matching | actual >= expected (expected = 0) | Ok | VerifyVersionMinorMinimumSucceedsWhenActualMinorIsHigher (BV1–BV6)| +/// | EC7 | kMinorMinimum | Matching | Matching | actual == expected (exact match per BV) | Ok (boundary edge) | VerifyVersionMinorMinimumSucceedsWhenAllEqual (BV1–BV6) | +/// | EC8 | kMinorMinimum | Non-matching ("XXXX") | Matching | actual >= expected (expected = 0) | Err: kVersionMismatch | VerifyVersionMinorMinimumFailsOnIdentifierMismatch (BV1–BV6) | +/// | EC9 | kMinorMinimum | Matching | Non-matching (99) | actual >= expected (expected = 0) | Err: kVersionMismatch | VerifyVersionMinorMinimumFailsOnMajorMismatch (BV1–BV6) | +/// | EC10 | kMinorMinimum | Matching | Matching | actual < expected (UINT16_MAX) | Err: kVersionMismatch | VerifyVersionMinorMinimumFailsWhenActualMinorIsLower (BV1–BV4) | +/// | EC11 | kMinorMinimum | Non-matching ("XXXX") | Non-matching (99) | actual < expected (expected = UINT16_MAX)| Err: kVersionMismatch | VerifyVersionMinorMinimumFailsWhenAllWrong (BV1–BV6) | +/// +/// All parameterized `VerifyVersion` tests below (EC1–EC11) are executed against the full +/// boundary value set BV1–BV6 defined in the `GetVersion` boundary value table above. +/// +// clang-format on + +TEST_P(VersionReaderBufferParamTest, VerifyVersionSucceedsOnMatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "boundary-values"); + RecordProperty("Description", "VerifyVersion returns success if version info matches expected"); + + const auto& param = GetParam(); + + const auto result = version_reader_.VerifyVersion(data_, param.version_info); + EXPECT_TRUE(result.has_value()); + + const auto result2 = score::flatbuffers::VerifyVersion(data_, param.version_info); + EXPECT_TRUE(result2.has_value()); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionFailsOnIdentifierMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "VerifyVersion returns error if only identifier does not match expected"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + expected = {"XXXX", + param.version_info.major_version, + param.version_info.minor_version}; // wrong identifier; major and minor remain correct + + const auto result = version_reader_.VerifyVersion(data_, expected); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = score::flatbuffers::VerifyVersion(data_, expected); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionFailsOnMajorMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "VerifyVersion returns error if only major version does not match expected"); + + score::flatbuffers::BufferVersionInfo expected = GetParam().version_info; + expected.major_version = 99U; // wrong major; minor and identifier remain correct + + const auto result = version_reader_.VerifyVersion(data_, expected); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = score::flatbuffers::VerifyVersion(data_, expected); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionFailsOnMinorMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "VerifyVersion returns error if only minor version does not match expected"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + // Bitwise complement guarantees a different value for any uint16_t actual minor version + expected.minor_version = static_cast(~param.version_info.minor_version); + + const auto result = version_reader_.VerifyVersion(data_, expected); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = score::flatbuffers::VerifyVersion(data_, expected); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionFailsOnVersionInfoMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "requirements-based"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", "VerifyVersion returns error if identifier, major, and minor version are all wrong"); + + // identifier="XXXX", major=99, minor=99 are wrong for every BV instance; + // minor=99 avoids an accidental match with BV4 (actual minor=1) that minor=1 would cause. + score::flatbuffers::BufferVersionInfo expected = {"XXXX", 99U, 99U}; + + const auto result = version_reader_.VerifyVersion(data_, expected); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = score::flatbuffers::VerifyVersion(data_, expected); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumSucceedsWhenActualMinorIsHigher) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns success if actual minor version is higher than expected"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + // For the minimum version buffers we can only prove minor==0. + // All other will confirm that minor>=0 and therefore valid. + expected.minor_version = 0; + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result.has_value()); + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result2.has_value()); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumSucceedsWhenAllEqual) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns success if actual minor equals the expected minimum " + "(actual == expected)"); + + const auto& param = GetParam(); + // Set expected minor to the exact value stored in the buffer so actual == expected for each BV instance. + score::flatbuffers::BufferVersionInfo expected = param.version_info; + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result.has_value()); + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + EXPECT_TRUE(result2.has_value()); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumFailsOnIdentifierMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns error if identifier does not match, even if major and " + "minor satisfy the condition"); + + const auto& param = GetParam(); + // wrong identifier; major matches; minor=0 so actual >= 0 always satisfies the minimum condition + score::flatbuffers::BufferVersionInfo expected = {"XXXX", param.version_info.major_version, 0U}; + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumFailsOnMajorMismatch) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns error if major version does not match, even if minor " + "satisfies the minimum"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + expected.major_version = 99U; // wrong major + expected.minor_version = 0U; // actual >= 0 always, so minor condition is satisfied + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumFailsWhenActualMinorIsLower) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns error if actual minor version is lower than expected"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + expected.minor_version = UINT16_MAX; + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + if (param.version_info.minor_version == UINT16_MAX) + { + // If the actual minor version is at maximum we can't expect a higher value + // Test fallbacks to expected minor == actual (UINT16_MAX) which is valid. + EXPECT_TRUE(result.has_value()); + } + else + { + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + } + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + if (param.version_info.minor_version == UINT16_MAX) + { + // If the actual minor version is at maximum we can't expect a higher value + // Test fallbacks to expected minor == actual (UINT16_MAX) which is valid. + EXPECT_TRUE(result2.has_value()); + } + else + { + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + } +} + +TEST_P(VersionReaderBufferParamTest, VerifyVersionMinorMinimumFailsWhenAllWrong) +{ + RecordProperty("PartiallyVerifies", "comp_req__flatbuffers__version_check"); + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "equivalence-classes"); + RecordProperty("Description", + "VerifyVersion with kMinorMinimum returns error if both major and minor version do not match"); + + const auto& param = GetParam(); + score::flatbuffers::BufferVersionInfo expected = param.version_info; + expected.major_version = 99U; // wrong major + expected.minor_version = UINT16_MAX; // actual < UINT16_MAX for BV1–BV5; major mismatch causes failure regardless + expected = {"XXXX", 99U, UINT16_MAX}; // identifier, major, and minor all wrong + + const auto result = + version_reader_.VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); + + const auto result2 = + score::flatbuffers::VerifyVersion(data_, expected, score::flatbuffers::VersionMatchMode::kMinorMinimum); + ASSERT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), MakeError(score::flatbuffers::ErrorCode::kVersionMismatch)); +} diff --git a/score/flatbuffers/version_reader.hpp b/score/flatbuffers/version_reader.hpp new file mode 100644 index 000000000..6f83693df --- /dev/null +++ b/score/flatbuffers/version_reader.hpp @@ -0,0 +1,98 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_LIB_FLATBUFFERS_VERSION_READER_HPP +#define SCORE_LIB_FLATBUFFERS_VERSION_READER_HPP + +#include "score/flatbuffers/i_version_reader.hpp" + +namespace score +{ +namespace flatbuffers +{ + +/// @brief Default implementation of `IVersionReader`. +/// +/// Reads and verifies the `BufferVersionInfo` embedded in any FlatBuffer that +/// follows the common buffer identification convention (a root table with +/// `version_info:score.flatbuffers.BufferVersion` as field 0). +/// +class VersionReader : public IVersionReader +{ + public: + VersionReader() = default; + ~VersionReader() override = default; + + /// @copydoc IVersionReader::GetVersion(score::cpp::span) + score::Result GetVersion(score::cpp::span buf) noexcept override; + + /// @copydoc IVersionReader::GetVersion(const std::vector&) + score::Result GetVersion(const std::vector& buf) noexcept override; + + /// @copydoc IVersionReader::VerifyVersion(score::cpp::span, + /// const BufferVersionInfo&, VersionMatchMode) + score::Result VerifyVersion(score::cpp::span buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept override; + + /// @copydoc IVersionReader::VerifyVersion(const std::vector&, + /// const BufferVersionInfo&, VersionMatchMode) + score::Result VerifyVersion(const std::vector& buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept override; +}; + +// ─── Convenience free functions ────────────────────────────────────────────── +// Use these when dependency injection is not needed, e.g. in standalone tools, +// integration tests, or one-off call sites. For components that require +// testability via mocking, prefer accepting an `IVersionReader&` and passing a +// `VersionReader` instance at construction time instead. + +/// @brief Reads the 4-char file_identifier and the `BufferVersion` values +/// from any FlatBuffer that conforms to the common buffer +/// identification convention. +/// +/// @param[in] buf View over the FlatBuffer binary data. +score::Result GetVersion(score::cpp::span buf) noexcept; + +/// @brief Reads the 4-char file_identifier and the `BufferVersion` values +/// from any FlatBuffer that conforms to the common buffer +/// identification convention. +/// +/// @param[in] buf FlatBuffer binary data, e.g. as returned by +/// `score::flatbuffers::LoadBuffer`. +score::Result GetVersion(const std::vector& buf) noexcept; + +/// @brief Reads the buffer version and verifies it matches @p expected. +/// +/// @param[in] buf View over the FlatBuffer binary data. +/// @param[in] expected The `BufferVersionInfo` the caller considers valid. +/// @param[in] mode Comparison mode (exact or minor-minimum). +score::Result VerifyVersion(score::cpp::span buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept; + +/// @brief Reads the buffer version and verifies it matches @p expected. +/// +/// @param[in] buf FlatBuffer binary data, e.g. as returned by +/// `score::flatbuffers::LoadBuffer`. +/// @param[in] expected The `BufferVersionInfo` the caller considers valid. +/// @param[in] mode Comparison mode (exact or minor-minimum). +score::Result VerifyVersion(const std::vector& buf, + const BufferVersionInfo& expected, + VersionMatchMode mode = VersionMatchMode::kExact) noexcept; + +} // namespace flatbuffers +} // namespace score + +#endif // SCORE_LIB_FLATBUFFERS_VERSION_READER_HPP