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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +