From f4031728774938e968ad610b5a40f90c81c9c967 Mon Sep 17 00:00:00 2001 From: Oliver Heilwagen Date: Tue, 5 May 2026 16:02:59 +0000 Subject: [PATCH 1/4] flatbuffers: Add starlark rules for versioned buffer serialization Introduces major/minor version injection into FlatBuffer binary configuration files at build time, enabling universal buffer identification at runtime. - New serialize_versioned_buffer and serialize_multiple_versioned_buffers Bazel macros that patch version fields into JSON before flatc serialization - New inject_buffer_version.py script and Bazel rule that injects major/minor version into a JSON data file as a build action - Added BufferVersion schema under score/flatbuffers/common as shared include for versioned schemas - Added BufferVersionEnvelope schema under score/flatbuffers/common used internally to implement GetBufferVersion and VerifyBufferVersion without hardcoded vtable offsets - Extended existing serialize_buffer, serialize_multiple_buffers, generate_cpp and generate_json_schema rules with an includes attribute to support schema include dependencies - Added Starlark rule tests covering single/multiple versioned buffers and fault injection cases (out-of-range versions, wrong version_info placement) --- score/flatbuffers/bazel/BUILD | 24 ++ score/flatbuffers/bazel/codegen.bzl | 27 +- .../bazel/inject_buffer_version.py | 70 ++++++ score/flatbuffers/bazel/tools.bzl | 234 +++++++++++++++++- score/flatbuffers/common/BUILD.bazel | 51 ++++ score/flatbuffers/common/buffer_version.fbs | 50 ++++ .../common/buffer_version_envelope.fbs | 32 +++ .../test/serialize_buffer_rules/BUILD | 76 +++++- score/flatbuffers/test/testdata/BUILD | 2 + .../test/testdata/test_versioned.fbs | 64 +++++ .../test_versioned_wrong_placement.fbs | 71 ++++++ 11 files changed, 693 insertions(+), 8 deletions(-) create mode 100644 score/flatbuffers/bazel/inject_buffer_version.py create mode 100644 score/flatbuffers/common/BUILD.bazel create mode 100644 score/flatbuffers/common/buffer_version.fbs create mode 100644 score/flatbuffers/common/buffer_version_envelope.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs 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..abc61732c 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") @@ -81,11 +85,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 +116,38 @@ 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";`). + 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..af237fef0 --- /dev/null +++ b/score/flatbuffers/bazel/inject_buffer_version.py @@ -0,0 +1,70 @@ +#!/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 the serialize_buffer Bazel rule to inject +major_version and minor_version into the user's JSON data file before +flatc serialization. The version is stored as a 'version' 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, + } + + 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..674f859b2 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,194 @@ 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 = [], + **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, must be > 0 to have effect). + 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. + **kwargs: Additional arguments forwarded to serialize_buffer (e.g. visibility, tags). + + 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, + **kwargs + ) + + serialize_buffer( + name = name, + data = ":" + patched_name, + schema = schema, + output = output, + includes = ["@score_baselibs//score/flatbuffers/common:buffer_version.fbs"] + includes, + **kwargs + ) + +def serialize_multiple_versioned_buffers( + name, + data_dict, + schema, + buffer_major_version, + buffer_minor_version, + includes = [], + **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. + **kwargs: Additional arguments forwarded to serialize_multiple_buffers. + + 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, + **kwargs + ) + + 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, + **kwargs + ) + def _generate_json_schema_impl(ctx): """Implementation of the generate_json_schema rule.""" @@ -235,6 +442,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 +470,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 +501,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/common/BUILD.bazel b/score/flatbuffers/common/BUILD.bazel new file mode 100644 index 000000000..c28695e42 --- /dev/null +++ b/score/flatbuffers/common/BUILD.bazel @@ -0,0 +1,51 @@ +# ******************************************************************************* +# 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", + ], +) diff --git a/score/flatbuffers/common/buffer_version.fbs b/score/flatbuffers/common/buffer_version.fbs new file mode 100644 index 000000000..ee1c9a098 --- /dev/null +++ b/score/flatbuffers/common/buffer_version.fbs @@ -0,0 +1,50 @@ +// ******************************************************************************* +// 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.mw.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::mw::flatbuffers::utils::GetBufferVersion` +/// and `score::mw::flatbuffers::utils::VerifyBufferVersion`) 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.mw.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 { + 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..e812bf259 --- /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 BufferIdEnvelope 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.mw.flatbuffers; + +table BufferVersionEnvelope { + version_info:BufferVersion; +} + +root_type BufferVersionEnvelope; 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..39b190894 100644 --- a/score/flatbuffers/test/testdata/BUILD +++ b/score/flatbuffers/test/testdata/BUILD @@ -14,6 +14,8 @@ exports_files( [ "test.fbs", + "test_versioned.fbs", + "test_versioned_wrong_placement.fbs", "invalid_data.json", "valid_data.json", "valid_data2.json", diff --git a/score/flatbuffers/test/testdata/test_versioned.fbs b/score/flatbuffers/test/testdata/test_versioned.fbs new file mode 100644 index 000000000..9f8a70136 --- /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.mw.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_wrong_placement.fbs b/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs new file mode 100644 index 000000000..251bd623b --- /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 GetBufferVersion / VerifyBufferVersion 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. + /// GetBufferVersion will return kVerificationFailed or zeros at runtime. + version_info:score.mw.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"; From b6049f5152742d57e12cf94cc2bb731e613efb42 Mon Sep 17 00:00:00 2001 From: Oliver Heilwagen Date: Thu, 18 Jun 2026 09:22:27 +0000 Subject: [PATCH 2/4] flatbuffers: Add buffer version identification and verification Introduces IVersionReader and VersionReader to read and verify version metadata from FlatBuffer binaries at runtime using the universal buffer identification convention established by the BufferVersionEnvelope schema. - New BufferVersionInfo value type holding a 4-char file identifier plus major_version and minor_version fields extracted from a conforming buffer - New IVersionReader interface with GetVersion and VerifyVersion methods, supporting kExact and kMinorMinimum match modes - New VersionReader concrete implementation backed by FlatBuffers Verifier for structural integrity checking before field access - New ErrorCode error domain covering kNullDataPointer, kVerificationFailed, kVersionInfoNotPresent, and kVersionMismatch failure cases - Updated buffer_version.fbs and buffer_version_envelope.fbs schemas to align with the new convention and added type_marker to enable misplacement check - Added integration tests covering nominal reads, exact/minimum version matching, out-of-range versions, wrong-field placement, and null inputs - Added unit tests for BufferVersionInfo, I/VersionReader, ErrorCode, idl_parser --- .../flatbuffers/docs/architecture/index.rst | 2 +- .../flatbuffers/docs/requirements/index.rst | 2 + score/flatbuffers/BUILD | 78 ++ .../bazel/inject_buffer_version.py | 8 +- score/flatbuffers/bazel/tools.bzl | 33 +- score/flatbuffers/buffer_version_info.hpp | 87 ++ score/flatbuffers/common/buffer_version.fbs | 20 +- .../common/buffer_version_envelope.fbs | 4 +- .../details/buffer_version_info.cpp | 38 + .../details/buffer_version_info_test.cpp | 81 ++ score/flatbuffers/details/error.cpp | 55 ++ score/flatbuffers/details/error_test.cpp | 47 ++ score/flatbuffers/details/idl_parser_test.cpp | 39 + score/flatbuffers/details/version_reader.cpp | 140 ++++ .../details/version_reader_impl.hpp | 107 +++ .../details/version_reader_test.cpp | 775 ++++++++++++++++++ score/flatbuffers/error.hpp | 42 + .../flatbuffers/examples/config_usecase/BUILD | 1 + score/flatbuffers/i_version_reader.hpp | 108 +++ score/flatbuffers/test/testdata/BUILD | 100 +++ .../test/testdata/test_versioned.fbs | 2 +- .../testdata/test_versioned_explicit_ids.fbs | 68 ++ .../test/testdata/test_versioned_max.fbs | 74 ++ .../testdata/test_versioned_max_range.fbs | 74 ++ .../test/testdata/test_versioned_min.fbs | 74 ++ .../testdata/test_versioned_min_range.fbs | 74 ++ .../testdata/test_versioned_wrong_field.fbs | 66 ++ ...est_versioned_wrong_field_similar_type.fbs | 73 ++ .../test_versioned_wrong_placement.fbs | 6 +- .../test/testdata/versioned_buffer_data.json | 5 + ..._buffer_data_wrong_field_similar_type.json | 10 + .../flatbuffers/test/version_reader_test.cpp | 543 ++++++++++++ score/flatbuffers/version_reader.hpp | 98 +++ 33 files changed, 2916 insertions(+), 18 deletions(-) create mode 100644 score/flatbuffers/buffer_version_info.hpp create mode 100644 score/flatbuffers/details/buffer_version_info.cpp create mode 100644 score/flatbuffers/details/buffer_version_info_test.cpp create mode 100644 score/flatbuffers/details/error.cpp create mode 100644 score/flatbuffers/details/error_test.cpp create mode 100644 score/flatbuffers/details/idl_parser_test.cpp create mode 100644 score/flatbuffers/details/version_reader.cpp create mode 100644 score/flatbuffers/details/version_reader_impl.hpp create mode 100644 score/flatbuffers/details/version_reader_test.cpp create mode 100644 score/flatbuffers/error.hpp create mode 100644 score/flatbuffers/i_version_reader.hpp create mode 100644 score/flatbuffers/test/testdata/test_versioned_explicit_ids.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_max.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_max_range.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_min.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_min_range.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_wrong_field.fbs create mode 100644 score/flatbuffers/test/testdata/test_versioned_wrong_field_similar_type.fbs create mode 100644 score/flatbuffers/test/testdata/versioned_buffer_data.json create mode 100644 score/flatbuffers/test/testdata/versioned_buffer_data_wrong_field_similar_type.json create mode 100644 score/flatbuffers/test/version_reader_test.cpp create mode 100644 score/flatbuffers/version_reader.hpp 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/score/flatbuffers/BUILD b/score/flatbuffers/BUILD index cd7a8a3fd..2c912d9ae 100644 --- a/score/flatbuffers/BUILD +++ b/score/flatbuffers/BUILD @@ -43,15 +43,26 @@ 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_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 +70,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 +117,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 +151,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/inject_buffer_version.py b/score/flatbuffers/bazel/inject_buffer_version.py index af237fef0..3f69ef083 100644 --- a/score/flatbuffers/bazel/inject_buffer_version.py +++ b/score/flatbuffers/bazel/inject_buffer_version.py @@ -14,9 +14,9 @@ """Patches a JSON data file with buffer version information. -This script is invoked by the serialize_buffer Bazel rule to inject +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' object at +flatc serialization. The version is stored as a 'version_info' object at the top level of the JSON. Usage: @@ -59,6 +59,10 @@ def main(): 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: diff --git a/score/flatbuffers/bazel/tools.bzl b/score/flatbuffers/bazel/tools.bzl index 674f859b2..b6ca9935d 100644 --- a/score/flatbuffers/bazel/tools.bzl +++ b/score/flatbuffers/bazel/tools.bzl @@ -315,6 +315,7 @@ def serialize_versioned_buffer( buffer_major_version, buffer_minor_version, includes = [], + tags = [], **kwargs): """Injects version information into a JSON file and serializes it to a binary FlatBuffer. @@ -328,12 +329,20 @@ def serialize_versioned_buffer( 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, must be > 0 to have effect). + 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. - **kwargs: Additional arguments forwarded to serialize_buffer (e.g. visibility, tags). + 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( @@ -355,7 +364,8 @@ def serialize_versioned_buffer( output = patched_output, major_version = buffer_major_version, minor_version = buffer_minor_version, - **kwargs + visibility = ["//visibility:private"], + tags = tags, ) serialize_buffer( @@ -364,6 +374,7 @@ def serialize_versioned_buffer( schema = schema, output = output, includes = ["@score_baselibs//score/flatbuffers/common:buffer_version.fbs"] + includes, + tags = tags, **kwargs ) @@ -374,6 +385,7 @@ def serialize_multiple_versioned_buffers( buffer_major_version, buffer_minor_version, includes = [], + tags = [], **kwargs): """Injects version information into multiple JSON files and serializes them to binary FlatBuffers. @@ -391,7 +403,16 @@ def serialize_multiple_versioned_buffers( 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. - **kwargs: Additional arguments forwarded to serialize_multiple_buffers. + 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( @@ -420,7 +441,8 @@ def serialize_multiple_versioned_buffers( output = patched_output, major_version = buffer_major_version, minor_version = buffer_minor_version, - **kwargs + visibility = ["//visibility:private"], + tags = tags, ) patched_data_dict[":" + patched_name] = output_path @@ -430,6 +452,7 @@ def serialize_multiple_versioned_buffers( schema = schema, data_dict = patched_data_dict, includes = ["@score_baselibs//score/flatbuffers/common:buffer_version.fbs"] + includes, + tags = tags, **kwargs ) 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/buffer_version.fbs b/score/flatbuffers/common/buffer_version.fbs index ee1c9a098..a541120da 100644 --- a/score/flatbuffers/common/buffer_version.fbs +++ b/score/flatbuffers/common/buffer_version.fbs @@ -28,23 +28,35 @@ /// 2. Declare `version_info` as the FIRST field of your root table: /// /// table MyConfig { -/// version_info:score.mw.flatbuffers.BufferVersion; // MUST be field 0 +/// 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::mw::flatbuffers::utils::GetBufferVersion` -/// and `score::mw::flatbuffers::utils::VerifyBufferVersion`) rely on both the +/// 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.mw.flatbuffers; +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 index e812bf259..0c8ff92bc 100644 --- a/score/flatbuffers/common/buffer_version_envelope.fbs +++ b/score/flatbuffers/common/buffer_version_envelope.fbs @@ -15,7 +15,7 @@ /// /// 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 BufferIdEnvelope is binary-compatible with +/// 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()) @@ -23,7 +23,7 @@ include "buffer_version.fbs"; -namespace score.mw.flatbuffers; +namespace score.flatbuffers; table BufferVersionEnvelope { version_info:BufferVersion; 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/examples/config_usecase/BUILD b/score/flatbuffers/examples/config_usecase/BUILD index ad8eb43b8..558790409 100644 --- a/score/flatbuffers/examples/config_usecase/BUILD +++ b/score/flatbuffers/examples/config_usecase/BUILD @@ -41,6 +41,7 @@ serialize_buffer( # using filesystem and flatbufferscpp library. cc_test( name = "demo_app", + size = "small", srcs = [ "demo_app_test.cpp", ":demo_component_config", 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/testdata/BUILD b/score/flatbuffers/test/testdata/BUILD index 39b190894..5be01cf88 100644 --- a/score/flatbuffers/test/testdata/BUILD +++ b/score/flatbuffers/test/testdata/BUILD @@ -11,15 +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 index 9f8a70136..605047d30 100644 --- a/score/flatbuffers/test/testdata/test_versioned.fbs +++ b/score/flatbuffers/test/testdata/test_versioned.fbs @@ -18,7 +18,7 @@ union DataPayloadUnion { table MyComponentConfig { /// Buffer version — MUST be field 0 for universal buffer identification - version_info:score.mw.flatbuffers.BufferVersion; + version_info:score.flatbuffers.BufferVersion; /// Component identification component_name:string (required); 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 index 251bd623b..ca5b072bc 100644 --- a/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs +++ b/score/flatbuffers/test/testdata/test_versioned_wrong_placement.fbs @@ -2,7 +2,7 @@ /// 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 GetBufferVersion / VerifyBufferVersion will fail +/// 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"; @@ -36,8 +36,8 @@ table MyComponentConfig { table AdvancedSettings { /// WRONG: version_info is buried inside a nested table, not the root table. - /// GetBufferVersion will return kVerificationFailed or zeros at runtime. - version_info:score.mw.flatbuffers.BufferVersion; + /// 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; 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 From de049c7c1d0273b61609c222ce0038f00fe739d5 Mon Sep 17 00:00:00 2001 From: Oliver Heilwagen Date: Thu, 18 Jun 2026 14:21:59 +0000 Subject: [PATCH 3/4] flatbuffers: Add buffer version identification and verification Add example for versioned configuration and extend generate_cpp documentation. - Add flatbuffers config example demo_config_versioned, which showcases usage of a configuration with version information. - Rename base configuration example to demo_config. - Extend generate_cpp documentation to explain how included schemas are handled during the code generation. - Extend library flatbufferutils by buffer_version_generated_bare to enable access to common buffer_version_generated.h used for common version identification. --- score/flatbuffers/BUILD | 1 + score/flatbuffers/bazel/codegen.bzl | 26 +++++- score/flatbuffers/common/BUILD.bazel | 11 +++ .../flatbuffers/examples/config_usecase/BUILD | 46 ++++++++-- ...demo_app_test.cpp => demo_config_test.cpp} | 17 ++-- .../demo_config_versioned_test.cpp | 88 +++++++++++++++++++ .../config_usecase/demo_versioned.fbs | 85 ++++++++++++++++++ 7 files changed, 255 insertions(+), 19 deletions(-) rename score/flatbuffers/examples/config_usecase/{demo_app_test.cpp => demo_config_test.cpp} (82%) create mode 100644 score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp create mode 100644 score/flatbuffers/examples/config_usecase/demo_versioned.fbs diff --git a/score/flatbuffers/BUILD b/score/flatbuffers/BUILD index 2c912d9ae..28cb1ae21 100644 --- a/score/flatbuffers/BUILD +++ b/score/flatbuffers/BUILD @@ -61,6 +61,7 @@ cc_library( 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", diff --git a/score/flatbuffers/bazel/codegen.bzl b/score/flatbuffers/bazel/codegen.bzl index abc61732c..21a1154ae 100644 --- a/score/flatbuffers/bazel/codegen.bzl +++ b/score/flatbuffers/bazel/codegen.bzl @@ -75,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"). @@ -142,6 +145,27 @@ generate_cpp = rule( 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", diff --git a/score/flatbuffers/common/BUILD.bazel b/score/flatbuffers/common/BUILD.bazel index c28695e42..8c5961cd6 100644 --- a/score/flatbuffers/common/BUILD.bazel +++ b/score/flatbuffers/common/BUILD.bazel @@ -49,3 +49,14 @@ cc_library( "//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/examples/config_usecase/BUILD b/score/flatbuffers/examples/config_usecase/BUILD index 558790409..cce78176e 100644 --- a/score/flatbuffers/examples/config_usecase/BUILD +++ b/score/flatbuffers/examples/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,20 +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_app_test.cpp b/score/flatbuffers/examples/config_usecase/demo_config_test.cpp similarity index 82% rename from score/flatbuffers/examples/config_usecase/demo_app_test.cpp rename to score/flatbuffers/examples/config_usecase/demo_config_test.cpp index c82bd714e..08bc1558d 100644 --- a/score/flatbuffers/examples/config_usecase/demo_app_test.cpp +++ b/score/flatbuffers/examples/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 +// Generated C++ accessor header produced by generate_cpp() for demo.fbs #include "score/flatbuffers/examples/config_usecase/component_config.h" -// FlatBuffers verifier (part of flatbufferscpp / @flatbuffers headers) #include "flatbuffers/verifier.h" #include @@ -40,15 +35,15 @@ TEST(DemoAppTest, LoadsAndVerifiesBuffer) 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/score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp b/score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp new file mode 100644 index 000000000..0fa3aac85 --- /dev/null +++ b/score/flatbuffers/examples/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 "score/flatbuffers/examples/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 = "score/flatbuffers/examples/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/score/flatbuffers/examples/config_usecase/demo_versioned.fbs b/score/flatbuffers/examples/config_usecase/demo_versioned.fbs new file mode 100644 index 000000000..ebe3bc757 --- /dev/null +++ b/score/flatbuffers/examples/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"; From 2a9bf952bab880910e6160bdbf3fadab11a7e8ac Mon Sep 17 00:00:00 2001 From: Oliver Heilwagen Date: Thu, 25 Jun 2026 07:32:46 +0000 Subject: [PATCH 4/4] flatbuffers: Add starlark rules for versioned buffer serialization Introduces major/minor version injection into FlatBuffer binary configuration files at build time, enabling universal buffer identification at runtime. - Move score/flatbuffers/examples to examples/integration/flatbuffers - Change rules_python to a normal dependency --- .github/workflows/build_linux.yml | 2 +- MODULE.bazel | 2 +- examples/integration/MODULE.bazel | 2 ++ .../integration/flatbuffers}/config_usecase/BUILD | 0 .../integration/flatbuffers}/config_usecase/demo.fbs | 0 .../integration/flatbuffers}/config_usecase/demo_config.json | 0 .../flatbuffers}/config_usecase/demo_config_test.cpp | 4 ++-- .../config_usecase/demo_config_versioned_test.cpp | 4 ++-- .../flatbuffers}/config_usecase/demo_versioned.fbs | 0 9 files changed, 8 insertions(+), 6 deletions(-) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/BUILD (100%) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/demo.fbs (100%) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/demo_config.json (100%) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/demo_config_test.cpp (94%) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/demo_config_versioned_test.cpp (95%) rename {score/flatbuffers/examples => examples/integration/flatbuffers}/config_usecase/demo_versioned.fbs (100%) 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/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 100% rename from score/flatbuffers/examples/config_usecase/BUILD rename to examples/integration/flatbuffers/config_usecase/BUILD 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_config_test.cpp b/examples/integration/flatbuffers/config_usecase/demo_config_test.cpp similarity index 94% rename from score/flatbuffers/examples/config_usecase/demo_config_test.cpp rename to examples/integration/flatbuffers/config_usecase/demo_config_test.cpp index 08bc1558d..5722fcfa2 100644 --- a/score/flatbuffers/examples/config_usecase/demo_config_test.cpp +++ b/examples/integration/flatbuffers/config_usecase/demo_config_test.cpp @@ -16,7 +16,7 @@ #include "score/flatbuffers/load_buffer.hpp" // Generated C++ accessor header produced by generate_cpp() for demo.fbs -#include "score/flatbuffers/examples/config_usecase/component_config.h" +#include "flatbuffers/config_usecase/component_config.h" #include "flatbuffers/verifier.h" @@ -30,7 +30,7 @@ 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(); diff --git a/score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp b/examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp similarity index 95% rename from score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp rename to examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp index 0fa3aac85..10d5d7966 100644 --- a/score/flatbuffers/examples/config_usecase/demo_config_versioned_test.cpp +++ b/examples/integration/flatbuffers/config_usecase/demo_config_versioned_test.cpp @@ -20,7 +20,7 @@ #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 "score/flatbuffers/examples/config_usecase/component_config_versioned.h" +#include "flatbuffers/config_usecase/component_config_versioned.h" #include "flatbuffers/verifier.h" @@ -33,7 +33,7 @@ TEST(DemoConfigVersionedTest, VerifiesVersionThenLoadsAndReadsBuffer) // ------------------------------------------------------------------------- // Step 1: Load the binary FlatBuffer file from disk // ------------------------------------------------------------------------- - const std::string_view bin_path = "score/flatbuffers/examples/config_usecase/demo_config_versioned.bin"; + 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(); diff --git a/score/flatbuffers/examples/config_usecase/demo_versioned.fbs b/examples/integration/flatbuffers/config_usecase/demo_versioned.fbs similarity index 100% rename from score/flatbuffers/examples/config_usecase/demo_versioned.fbs rename to examples/integration/flatbuffers/config_usecase/demo_versioned.fbs