Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

/**
Expand Down Expand Up @@ -104,4 +106,83 @@ public static Token findFirst(final String name, final List<Token> 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<Token> fields, final String actingVersionName)
{
final TreeMap<Integer, Integer> 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<Integer, Integer> 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<Token> tokens,
final int typeIndex,
final int sinceVersion,
final int baseOffset,
final TreeMap<Integer, Integer> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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<Token> 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<Token> groups = new ArrayList<>();
Expand Down Expand Up @@ -258,6 +259,7 @@ private static void generateGroupHeaderFunctions(
final CharSequence[] scope,
final String groupName,
final List<Token> tokens,
final List<Token> fields,
final int index)
{
final String dimensionsStructName = formatScopedName(scope, tokens.get(index + 1).name());
Expand Down Expand Up @@ -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 + " || " : "";
Expand Down Expand Up @@ -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<Token> fields, final CharSequence[] scope)
{
final String blockLengthType = cTypeName(ir.headerStructure().blockLengthType());
final String templateIdType = cTypeName(ir.headerStructure().templateIdType());
Expand Down Expand Up @@ -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" +
Expand Down Expand Up @@ -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<Token> 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)
Expand Down
7 changes: 5 additions & 2 deletions sbe-tool/src/test/c/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -38,14 +38,15 @@ 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}
)

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}
Expand All @@ -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})
Expand All @@ -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)
Expand Down
106 changes: 106 additions & 0 deletions sbe-tool/src/test/c/WireBlockLengthTest.cpp
Original file line number Diff line number Diff line change
@@ -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 <cerrno>
#include <string>

#include <gtest/gtest.h>

#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);
}
Loading