From 99dfd07b3ed349ad161ef7a92c5ffd31eb378417 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 25 Oct 2026 00:58:19 +0200 Subject: [PATCH] [C] Reject a wire blockLength shorter than the fixed fields of the acting version. _wrap_for_decode and _wrap_for_decode return NULL with E100 or E108 below the extent of the fields present at the acting version. The minimum is per version because older versions have shorter blocks, so do not compare against _sbe_block_length(). Empty groups are not checked. --- .../real_logic/sbe/generation/Generators.java | 83 +++++++++++++- .../sbe/generation/c/CGenerator.java | 41 +++++-- sbe-tool/src/test/c/CMakeLists.txt | 7 +- sbe-tool/src/test/c/WireBlockLengthTest.cpp | 106 ++++++++++++++++++ sbe-tool/src/test/c/WireBuffer.h | 81 +++++++++++++ .../MinBlockLengthExpressionTest.java | 58 ++++++++++ .../group-with-versioned-composite.xml | 36 ++++++ .../resources/wire-bounds-test-schema.xml | 35 ++++++ 8 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 sbe-tool/src/test/c/WireBlockLengthTest.cpp create mode 100644 sbe-tool/src/test/c/WireBuffer.h create mode 100644 sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/MinBlockLengthExpressionTest.java create mode 100644 sbe-tool/src/test/resources/group-with-versioned-composite.xml create mode 100644 sbe-tool/src/test/resources/wire-bounds-test-schema.xml diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/Generators.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/Generators.java index c2af0d3738..ea27a708b8 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/Generators.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/Generators.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2025 Real Logic Limited. + * Copyright 2013-2026 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import uk.co.real_logic.sbe.ir.Token; import java.util.List; +import java.util.Map; +import java.util.TreeMap; import java.util.function.BiConsumer; /** @@ -104,4 +106,83 @@ public static Token findFirst(final String name, final List tokens, final throw new IllegalStateException("name not found: " + name); } + + /** + * Generate a C-family expression for the minimum block length a decoder must accept for a given acting version, + * i.e. the extent of the fixed fields present at that version. Fields are only appended in later schema versions, + * so a block encoded by an older version can be shorter than the current block but never shorter than the fields + * which existed in that version. Fields newer than the acting version are not read by the generated accessors. + * + * @param fields of the message or group block. + * @param actingVersionName the name of the variable holding the acting version. + * @return the expression or null if no fixed fields occupy the block. + */ + public static String minBlockLengthExpression(final List fields, final String actingVersionName) + { + final TreeMap extentByVersion = new TreeMap<>(); + for (int i = 0, size = fields.size(); i < size; ) + { + final Token fieldToken = fields.get(i); + if (Signal.BEGIN_FIELD != fieldToken.signal()) + { + i++; + continue; + } + + if (!fieldToken.isConstantEncoding()) + { + addExtentByVersion(fields, i + 1, fieldToken.version(), 0, extentByVersion); + } + + i += fieldToken.componentTokenCount(); + } + + int extent = 0; + String expression = "0"; + for (final Map.Entry entry : extentByVersion.entrySet()) + { + final int version = entry.getKey(); + if (entry.getValue() > extent) + { + extent = entry.getValue(); + expression = 0 == version ? + Integer.toString(extent) : + "(" + actingVersionName + " >= " + version + "u ? " + extent + "u : " + expression + ")"; + } + } + + return 0 == extent ? null : expression; + } + + // A composite member added in a later version than its field only counts from that version on, since a block + // encoded by an older version ends before it. + private static void addExtentByVersion( + final List tokens, + final int typeIndex, + final int sinceVersion, + final int baseOffset, + final TreeMap extentByVersion) + { + final Token typeToken = tokens.get(typeIndex); + if (typeToken.isConstantEncoding()) + { + return; + } + + final int version = Math.max(sinceVersion, typeToken.version()); + final int offset = baseOffset + typeToken.offset(); + if (Signal.BEGIN_COMPOSITE == typeToken.signal()) + { + final int endIndex = typeIndex + typeToken.componentTokenCount() - 1; + for (int i = typeIndex + 1; i < endIndex; ) + { + addExtentByVersion(tokens, i, version, offset, extentByVersion); + i += tokens.get(i).componentTokenCount(); + } + } + else + { + extentByVersion.merge(version, offset + typeToken.encodedLength(), Math::max); + } + } } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/c/CGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/c/CGenerator.java index 5099eb2d23..3e19bab541 100755 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/c/CGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/c/CGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2025 Real Logic Limited. + * Copyright 2013-2026 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -181,7 +181,7 @@ public void generate() throws IOException "};\n", structName)); - out.append(generateMessageFlyweightFunctions(structName, msgToken, ir.namespaces())); + out.append(generateMessageFlyweightFunctions(structName, msgToken, fields, ir.namespaces())); out.append(generateFieldFunctions(ir.namespaces(), structName, structName, fields)); @@ -213,15 +213,16 @@ private void generateGroups( final Token numInGroupToken = Generators.findFirst("numInGroup", tokens, i); final String cTypeForNumInGroup = cTypeName(numInGroupToken.encoding().primitiveType()); - generateGroupStruct(sb, groupName); - generateGroupHeaderFunctions(sb, scope, groupName, tokens, i); - + final int groupIndex = i; ++i; final int groupHeaderTokenCount = tokens.get(i).componentTokenCount(); i += groupHeaderTokenCount; final List fields = new ArrayList<>(); i = collectFields(tokens, i, fields); + + generateGroupStruct(sb, groupName); + generateGroupHeaderFunctions(sb, scope, groupName, tokens, fields, groupIndex); sb.append(generateFieldFunctions(scope, groupName, outermostStruct, fields)); final List groups = new ArrayList<>(); @@ -258,6 +259,7 @@ private static void generateGroupHeaderFunctions( final CharSequence[] scope, final String groupName, final List tokens, + final List fields, final int index) { final String dimensionsStructName = formatScopedName(scope, tokens.get(index + 1).name()); @@ -291,13 +293,15 @@ private static void generateGroupHeaderFunctions( " }\n\n" + " codec->block_length = %2$s_blockLength(&dimensions);\n" + " codec->count = %2$s_numInGroup(&dimensions);\n" + + "%4$s" + " codec->index = -1;\n" + " codec->acting_version = acting_version;\n" + " codec->position_ptr = pos;\n" + " *codec->position_ptr = *codec->position_ptr + %3$d;\n\n" + " return codec;\n" + "}\n", - groupName, dimensionsStructName, dimensionHeaderLength)); + groupName, dimensionsStructName, dimensionHeaderLength, + generateBlockLengthCheck(fields, "codec->block_length", "codec->count > 0", "E108"))); final long minCount = numInGroupToken.encoding().applicableMinValue().longValue(); final String minCheck = minCount > 0 ? "count < " + minCount + " || " : ""; @@ -1893,7 +1897,7 @@ private CharSequence generateMessageFlyweightStruct(final String structName) } private CharSequence generateMessageFlyweightFunctions( - final String structName, final Token token, final CharSequence[] scope) + final String structName, final Token token, final List fields, final CharSequence[] scope) { final String blockLengthType = cTypeName(ir.headerStructure().blockLengthType()); final String templateIdType = cTypeName(ir.headerStructure().templateIdType()); @@ -2048,6 +2052,7 @@ private CharSequence generateMessageFlyweightFunctions( " const uint64_t acting_version,\n" + " const uint64_t buffer_length)\n" + "{\n" + + "%14$s" + " return %10$s_reset(\n" + " codec,\n" + " buffer,\n" + @@ -2110,7 +2115,27 @@ private CharSequence generateMessageFlyweightFunctions( structName, messageHeaderStruct, semanticVersion, - toUppercaseWithUnderscores(structName)); + toUppercaseWithUnderscores(structName), + generateBlockLengthCheck(fields, "acting_block_length", null, "E100")); + } + + private static String generateBlockLengthCheck( + final List fields, final String blockLengthName, final String condition, final String errorCode) + { + final String minBlockLength = Generators.minBlockLengthExpression(fields, "acting_version"); + if (null == minBlockLength) + { + return ""; + } + + final String tooShort = "(" + blockLengthName + " < " + minBlockLength + ")"; + return + " if (SBE_BOUNDS_CHECK_EXPECT(" + + (null == condition ? tooShort : "(" + tooShort + " && (" + condition + "))") + ", false))\n" + + " {\n" + + " errno = " + errorCode + ";\n" + + " return NULL;\n" + + " }\n"; } private String toUppercaseWithUnderscores(final String camelCaseString) diff --git a/sbe-tool/src/test/c/CMakeLists.txt b/sbe-tool/src/test/c/CMakeLists.txt index 08a3720edf..86a6598aee 100644 --- a/sbe-tool/src/test/c/CMakeLists.txt +++ b/sbe-tool/src/test/c/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright 2013-2025 Real Logic Limited. +# Copyright 2013-2026 Real Logic Limited. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ set(GROUP_WITH_DATA_SCHEMA ${CODEC_SCHEMA_DIR}/group-with-data-schema.xml) set(ISSUE889_SCHEMA ${CODEC_SCHEMA_DIR}/issue889.xml) set(FLOATING_POINT_INFINITY_SCHEMA ${CODEC_SCHEMA_DIR}/floating-point-infinity-schema.xml) set(UINT64_LENGTH_SCHEMA ${CODEC_SCHEMA_DIR}/uint64-length-schema.xml) +set(WIRE_BOUNDS_SCHEMA ${CODEC_SCHEMA_DIR}/wire-bounds-test-schema.xml) set(GENERATED_CODECS ${C_CODEC_TARGET_DIR} @@ -45,7 +46,7 @@ set(GENERATED_CODECS add_custom_command( OUTPUT ${GENERATED_CODECS} - DEPENDS sbe-jar ${SBE_JAR} ${CODE_GENERATION_SCHEMA} ${CODE_GENERATION_SCHEMA_WITH_VERSION} ${GROUP_WITH_DATA_SCHEMA} ${ISSUE889_SCHEMA} ${FLOATING_POINT_INFINITY_SCHEMA} ${UINT64_LENGTH_SCHEMA} + DEPENDS sbe-jar ${SBE_JAR} ${CODE_GENERATION_SCHEMA} ${CODE_GENERATION_SCHEMA_WITH_VERSION} ${GROUP_WITH_DATA_SCHEMA} ${ISSUE889_SCHEMA} ${FLOATING_POINT_INFINITY_SCHEMA} ${UINT64_LENGTH_SCHEMA} ${WIRE_BOUNDS_SCHEMA} COMMAND ${Java_JAVA_EXECUTABLE} --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dsbe.output.dir=${C_CODEC_TARGET_DIR} @@ -58,6 +59,7 @@ add_custom_command( ${ISSUE889_SCHEMA} ${FLOATING_POINT_INFINITY_SCHEMA} ${UINT64_LENGTH_SCHEMA} + ${WIRE_BOUNDS_SCHEMA} ) add_custom_target(c_codecs DEPENDS ${GENERATED_CODECS}) @@ -69,6 +71,7 @@ sbe_test(GroupWithDataTest c_codecs) sbe_test(Issue889Test c_codecs) sbe_test(InfinityTest c_codecs) sbe_test(Uint64LengthTest c_codecs) +sbe_test(WireBlockLengthTest c_codecs) # Compile a dummy C source to test C compliance of generated headers. add_executable(CComplianceTest CComplianceTest.c) diff --git a/sbe-tool/src/test/c/WireBlockLengthTest.cpp b/sbe-tool/src/test/c/WireBlockLengthTest.cpp new file mode 100644 index 0000000000..8808c9928d --- /dev/null +++ b/sbe-tool/src/test/c/WireBlockLengthTest.cpp @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +#include + +#include "wire_bounds_test/versioned.h" +#include "WireBuffer.h" + +static WireBuffer versionedBlockV1() +{ + WireBuffer buffer; + buffer.u64(1).u64(2).u64(3); + return buffer; +} + +TEST(CWireBlockLengthTest, shouldRejectMessageBlockLengthZeroWithTruncatedBuffer) +{ + WireBuffer buffer; + buffer.u16(0) + .u16(wire_bounds_test_versioned_sbe_template_id()) + .u16(wire_bounds_test_versioned_sbe_schema_id()) + .u16(1); + wire_bounds_test_versioned decoder; + + errno = 0; + EXPECT_EQ(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 8, 0, 1, buffer.size()), nullptr); + EXPECT_EQ(errno, E100); +} + +TEST(CWireBlockLengthTest, shouldRejectMessageBlockLengthShorterThanActingVersionFields) +{ + WireBuffer buffer = versionedBlockV1(); + buffer.u16(16).u16(0).u32(0); + wire_bounds_test_versioned decoder; + + EXPECT_EQ(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 8, 0, buffer.size()), nullptr); + EXPECT_EQ(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 16, 1, buffer.size()), nullptr); + EXPECT_NE(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 24, 1, buffer.size()), nullptr); +} + +TEST(CWireBlockLengthTest, shouldDecodeOlderVersionWithShorterBlocks) +{ + WireBuffer buffer; + buffer.u64(1).u64(2); + buffer.u16(8).u16(1).u64(3); + buffer.u32(2).bytes("hi"); + wire_bounds_test_versioned decoder; + wire_bounds_test_versioned_entries entries; + + ASSERT_NE(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 16, 0, buffer.size()), nullptr); + EXPECT_EQ(wire_bounds_test_versioned_a(&decoder), 1u); + EXPECT_EQ(wire_bounds_test_versioned_b(&decoder), 2u); + EXPECT_EQ(wire_bounds_test_versioned_c(&decoder), wire_bounds_test_versioned_c_null_value()); + + ASSERT_NE(wire_bounds_test_versioned_get_entries(&decoder, &entries), nullptr); + ASSERT_EQ(wire_bounds_test_versioned_entries_count(&entries), 1u); + ASSERT_NE(wire_bounds_test_versioned_entries_next(&entries), nullptr); + EXPECT_EQ(wire_bounds_test_versioned_entries_x(&entries), 3u); + EXPECT_EQ(wire_bounds_test_versioned_entries_y(&entries), wire_bounds_test_versioned_entries_y_null_value()); + + const wire_bounds_test_versioned_string_view view = wire_bounds_test_versioned_get_payload_as_string_view(&decoder); + ASSERT_NE(view.data, nullptr); + EXPECT_EQ(std::string(view.data, view.length), "hi"); + EXPECT_EQ(wire_bounds_test_versioned_sbe_position(&decoder), buffer.size()); +} + +TEST(CWireBlockLengthTest, shouldRejectGroupBlockLengthZero) +{ + WireBuffer buffer = versionedBlockV1(); + buffer.u16(0).u16(2); + wire_bounds_test_versioned decoder; + wire_bounds_test_versioned_entries entries; + + ASSERT_NE(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 24, 1, buffer.size()), nullptr); + errno = 0; + EXPECT_EQ(wire_bounds_test_versioned_get_entries(&decoder, &entries), nullptr); + EXPECT_EQ(errno, E108); +} + +TEST(CWireBlockLengthTest, shouldAcceptEmptyGroupWithBlockLengthZero) +{ + WireBuffer buffer = versionedBlockV1(); + buffer.u16(0).u16(0).u32(0); + wire_bounds_test_versioned decoder; + wire_bounds_test_versioned_entries entries; + + ASSERT_NE(wire_bounds_test_versioned_wrap_for_decode(&decoder, buffer.data(), 0, 24, 1, buffer.size()), nullptr); + ASSERT_NE(wire_bounds_test_versioned_get_entries(&decoder, &entries), nullptr); + EXPECT_EQ(wire_bounds_test_versioned_entries_count(&entries), 0u); + EXPECT_EQ(wire_bounds_test_versioned_payload_length(&decoder), 0u); +} diff --git a/sbe-tool/src/test/c/WireBuffer.h b/sbe-tool/src/test/c/WireBuffer.h new file mode 100644 index 0000000000..bae78fd096 --- /dev/null +++ b/sbe-tool/src/test/c/WireBuffer.h @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef _SBE_C_TEST_WIRE_BUFFER_H +#define _SBE_C_TEST_WIRE_BUFFER_H + +#include +#include +#include + +/* + * Buffers are built byte by byte in little-endian order and sized exactly to the bytes written, so a read past + * the end of the buffer is a read past the end of the allocation. + */ +class WireBuffer +{ +public: + WireBuffer &u8(const std::uint8_t value) + { + m_bytes.push_back(static_cast(value)); + return *this; + } + + WireBuffer &u16(const std::uint16_t value) + { + return putLittleEndian(value, 2); + } + + WireBuffer &u32(const std::uint32_t value) + { + return putLittleEndian(value, 4); + } + + WireBuffer &u64(const std::uint64_t value) + { + return putLittleEndian(value, 8); + } + + WireBuffer &bytes(const std::string &value) + { + m_bytes.insert(m_bytes.end(), value.begin(), value.end()); + return *this; + } + + char *data() + { + return m_bytes.data(); + } + + std::uint64_t size() const + { + return m_bytes.size(); + } + +private: + WireBuffer &putLittleEndian(std::uint64_t value, const int length) + { + for (int i = 0; i < length; i++) + { + m_bytes.push_back(static_cast(value & 0xFF)); + value >>= 8; + } + return *this; + } + + std::vector m_bytes; +}; + +#endif diff --git a/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/MinBlockLengthExpressionTest.java b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/MinBlockLengthExpressionTest.java new file mode 100644 index 0000000000..c536aa8835 --- /dev/null +++ b/sbe-tool/src/test/java/uk/co/real_logic/sbe/generation/MinBlockLengthExpressionTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Real Logic Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package uk.co.real_logic.sbe.generation; + +import org.junit.jupiter.api.Test; +import uk.co.real_logic.sbe.Tests; +import uk.co.real_logic.sbe.ir.Ir; +import uk.co.real_logic.sbe.ir.Signal; +import uk.co.real_logic.sbe.ir.Token; +import uk.co.real_logic.sbe.xml.IrGenerator; +import uk.co.real_logic.sbe.xml.ParserOptions; + +import java.io.InputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse; + +class MinBlockLengthExpressionTest +{ + @Test + void shouldNotCountCompositeMembersNewerThanActingVersion() throws Exception + { + final Ir ir; + try (InputStream in = Tests.getLocalResource("group-with-versioned-composite.xml")) + { + ir = new IrGenerator().generate(parse(in, ParserOptions.builder().stopOnError(true).build())); + } + + final List tokens = ir.getMessage(1); + int groupIndex = 0; + while (Signal.BEGIN_GROUP != tokens.get(groupIndex).signal()) + { + groupIndex++; + } + + final int fieldsIndex = groupIndex + 1 + tokens.get(groupIndex + 1).componentTokenCount(); + final int groupEndIndex = groupIndex + tokens.get(groupIndex).componentTokenCount() - 1; + + // The px composite is 8 bytes in version 0; its side member is added in version 1. + assertEquals( + "(actingVersion >= 1u ? 9u : 8)", + Generators.minBlockLengthExpression(tokens.subList(fieldsIndex, groupEndIndex), "actingVersion")); + } +} diff --git a/sbe-tool/src/test/resources/group-with-versioned-composite.xml b/sbe-tool/src/test/resources/group-with-versioned-composite.xml new file mode 100644 index 0000000000..c5ae5c4c76 --- /dev/null +++ b/sbe-tool/src/test/resources/group-with-versioned-composite.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + 1 + 2 + + + + + + + + + + + + diff --git a/sbe-tool/src/test/resources/wire-bounds-test-schema.xml b/sbe-tool/src/test/resources/wire-bounds-test-schema.xml new file mode 100644 index 0000000000..65f7873319 --- /dev/null +++ b/sbe-tool/src/test/resources/wire-bounds-test-schema.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +